matrix_sdk_ffi/
widget.rs

1use std::sync::{Arc, Mutex};
2
3use language_tags::LanguageTag;
4use matrix_sdk::widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter};
5use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
6use ruma::events::MessageLikeEventType;
7use tracing::error;
8
9use crate::{room::Room, runtime::get_runtime_handle};
10
11#[derive(uniffi::Record)]
12pub struct WidgetDriverAndHandle {
13    pub driver: Arc<WidgetDriver>,
14    pub handle: Arc<WidgetDriverHandle>,
15}
16
17#[matrix_sdk_ffi_macros::export]
18pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHandle, ParseError> {
19    let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?);
20    Ok(WidgetDriverAndHandle {
21        driver: Arc::new(WidgetDriver(Mutex::new(Some(driver)))),
22        handle: Arc::new(WidgetDriverHandle(handle)),
23    })
24}
25
26/// An object that handles all interactions of a widget living inside a webview
27/// or IFrame with the Matrix world.
28#[derive(uniffi::Object)]
29pub struct WidgetDriver(Mutex<Option<matrix_sdk::widget::WidgetDriver>>);
30
31#[matrix_sdk_ffi_macros::export]
32impl WidgetDriver {
33    pub async fn run(
34        &self,
35        room: Arc<Room>,
36        capabilities_provider: Box<dyn WidgetCapabilitiesProvider>,
37    ) {
38        let Some(driver) = self.0.lock().unwrap().take() else {
39            error!("Can't call run multiple times on a WidgetDriver");
40            return;
41        };
42
43        let capabilities_provider = CapabilitiesProviderWrap(capabilities_provider.into());
44        if let Err(()) = driver.run(room.inner.clone(), capabilities_provider).await {
45            // TODO
46        }
47    }
48}
49
50/// Information about a widget.
51#[derive(uniffi::Record, Clone)]
52pub struct WidgetSettings {
53    /// Widget's unique identifier.
54    pub widget_id: String,
55    /// Whether or not the widget should be initialized on load message
56    /// (`ContentLoad` message), or upon creation/attaching of the widget to
57    /// the SDK's state machine that drives the API.
58    pub init_after_content_load: bool,
59    /// This contains the url from the widget state event.
60    /// In this url placeholders can be used to pass information from the client
61    /// to the widget. Possible values are: `$widgetId`, `$parentUrl`,
62    /// `$userId`, `$lang`, `$fontScale`, `$analyticsID`.
63    ///
64    /// # Examples
65    ///
66    /// e.g `http://widget.domain?username=$userId`
67    /// will become: `http://widget.domain?username=@user_matrix_id:server.domain`.
68    raw_url: String,
69}
70
71impl TryFrom<WidgetSettings> for matrix_sdk::widget::WidgetSettings {
72    type Error = ParseError;
73
74    fn try_from(value: WidgetSettings) -> Result<Self, Self::Error> {
75        let WidgetSettings { widget_id, init_after_content_load, raw_url } = value;
76        Ok(matrix_sdk::widget::WidgetSettings::new(widget_id, init_after_content_load, &raw_url)?)
77    }
78}
79
80impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
81    fn from(value: matrix_sdk::widget::WidgetSettings) -> Self {
82        WidgetSettings {
83            widget_id: value.widget_id().to_owned(),
84            init_after_content_load: value.init_on_content_load(),
85            raw_url: value.raw_url().to_string(),
86        }
87    }
88}
89
90/// Create the actual url that can be used to setup the WebView or IFrame
91/// that contains the widget.
92///
93/// # Arguments
94/// * `widget_settings` - The widget settings to generate the url for.
95/// * `room` - A Matrix room which is used to query the logged in username
96/// * `props` - Properties from the client that can be used by a widget to adapt
97///   to the client. e.g. language, font-scale...
98#[matrix_sdk_ffi_macros::export]
99pub async fn generate_webview_url(
100    widget_settings: WidgetSettings,
101    room: Arc<Room>,
102    props: ClientProperties,
103) -> Result<String, ParseError> {
104    Ok(matrix_sdk::widget::WidgetSettings::generate_webview_url(
105        &widget_settings.clone().try_into()?,
106        &room.inner,
107        props.into(),
108    )
109    .await
110    .map(|url| url.to_string())?)
111}
112
113/// `WidgetSettings` are usually created from a state event.
114/// (currently unimplemented)
115///
116/// In some cases the client wants to create custom `WidgetSettings`
117/// for specific rooms based on other conditions.
118/// This function returns a `WidgetSettings` object which can be used
119/// to setup a widget using `run_client_widget_api`
120/// and to generate the correct url for the widget.
121///
122/// # Arguments
123///
124/// * `props` - A struct containing the configuration parameters for a element
125///   call widget.
126#[matrix_sdk_ffi_macros::export]
127pub fn new_virtual_element_call_widget(
128    props: matrix_sdk::widget::VirtualElementCallWidgetProperties,
129    config: matrix_sdk::widget::VirtualElementCallWidgetConfig,
130) -> Result<WidgetSettings, ParseError> {
131    Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props, config)
132        .map(|w| w.into())?)
133}
134
135/// The Capabilities required to run a element call widget.
136///
137/// This is intended to be used in combination with: `acquire_capabilities` of
138/// the `CapabilitiesProvider`.
139///
140/// `acquire_capabilities` can simply return the `WidgetCapabilities` from this
141/// function. Even if there are non intersecting permissions to what the widget
142/// requested.
143///
144/// Editing and extending the capabilities from this function is also possible,
145/// but should only be done as temporal workarounds until this function is
146/// adjusted
147#[matrix_sdk_ffi_macros::export]
148pub fn get_element_call_required_permissions(
149    own_user_id: String,
150    own_device_id: String,
151) -> WidgetCapabilities {
152    use ruma::events::StateEventType;
153
154    let read_send = vec![
155        // To read and send rageshake requests from other room members
156        WidgetEventFilter::MessageLikeWithType {
157            event_type: "org.matrix.rageshake_request".to_owned(),
158        },
159        // To read and send encryption keys
160        WidgetEventFilter::ToDevice { event_type: "io.element.call.encryption_keys".to_owned() },
161        // TODO change this to the appropriate to-device version once ready
162        // remove this once all matrixRTC call apps supports to-device encryption.
163        WidgetEventFilter::MessageLikeWithType {
164            event_type: "io.element.call.encryption_keys".to_owned(),
165        },
166        // To read and send custom EC reactions. They are different to normal `m.reaction`
167        // because they can be send multiple times to the same event.
168        WidgetEventFilter::MessageLikeWithType {
169            event_type: "io.element.call.reaction".to_owned(),
170        },
171        // This allows send raise hand reactions.
172        WidgetEventFilter::MessageLikeWithType {
173            event_type: MessageLikeEventType::Reaction.to_string(),
174        },
175        // This allows to detect if someone does not raise their hand anymore.
176        WidgetEventFilter::MessageLikeWithType {
177            event_type: MessageLikeEventType::RoomRedaction.to_string(),
178        },
179        // This allows declining an incoming call and detect if someone declines a call.
180        WidgetEventFilter::MessageLikeWithType {
181            event_type: MessageLikeEventType::RtcDecline.to_string(),
182        },
183    ];
184
185    WidgetCapabilities {
186        read: vec![
187            // To compute the current state of the matrixRTC session.
188            WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
189            // To display the name of the room.
190            WidgetEventFilter::StateWithType { event_type: StateEventType::RoomName.to_string() },
191            // To detect leaving/kicked room members during a call.
192            WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
193            // To decide whether to encrypt the call streams based on the room encryption setting.
194            WidgetEventFilter::StateWithType {
195                event_type: StateEventType::RoomEncryption.to_string(),
196            },
197            // This allows the widget to check the room version, so it can know about
198            // version-specific auth rules (namely MSC3779).
199            WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
200        ]
201        .into_iter()
202        .chain(read_send.clone())
203        .collect(),
204        send: vec![
205            // To notify other users that a call has started.
206            WidgetEventFilter::MessageLikeWithType {
207                event_type: MessageLikeEventType::RtcNotification.to_string(),
208            },
209            // Also for call notifications, except this is the deprecated fallback type which
210            // Element Call still sends.
211            // Deprecated for now, kept for backward compatibility as widgets will send both
212            // CallNotify and RtcNotification.
213            WidgetEventFilter::MessageLikeWithType {
214                event_type: MessageLikeEventType::CallNotify.to_string(),
215            },
216            // To send the call participation state event (main MatrixRTC event).
217            // This is required for legacy state events (using only one event for all devices with
218            // a membership array). TODO: remove once legacy call member events are
219            // sunset.
220            WidgetEventFilter::StateWithTypeAndStateKey {
221                event_type: StateEventType::CallMember.to_string(),
222                state_key: own_user_id.clone(),
223            },
224            // `delayed_event`` version for session memberhips
225            // [MSC3779](https://github.com/matrix-org/matrix-spec-proposals/pull/3779), with no leading underscore.
226            WidgetEventFilter::StateWithTypeAndStateKey {
227                event_type: StateEventType::CallMember.to_string(),
228                state_key: format!("{own_user_id}_{own_device_id}"),
229            },
230            // Same as above for [MSC3779] and [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143),
231            // with application suffix
232            WidgetEventFilter::StateWithTypeAndStateKey {
233                event_type: StateEventType::CallMember.to_string(),
234                state_key: format!("{own_user_id}_{own_device_id}_m.call"),
235            },
236            // The same as above but with an underscore.
237            // To work around the issue that state events starting with `@` have to be Matrix id's
238            // but we use mxId+deviceId.
239            WidgetEventFilter::StateWithTypeAndStateKey {
240                event_type: StateEventType::CallMember.to_string(),
241                state_key: format!("_{own_user_id}_{own_device_id}"),
242            },
243            // Same as above for [MSC4143], with application suffix
244            WidgetEventFilter::StateWithTypeAndStateKey {
245                event_type: StateEventType::CallMember.to_string(),
246                state_key: format!("_{own_user_id}_{own_device_id}_m.call"),
247            },
248        ]
249        .into_iter()
250        .chain(read_send)
251        .collect(),
252        requires_client: true,
253        update_delayed_event: true,
254        send_delayed_event: true,
255    }
256}
257
258#[derive(uniffi::Record)]
259pub struct ClientProperties {
260    /// The client_id provides the widget with the option to behave differently
261    /// for different clients. e.g org.example.ios.
262    client_id: String,
263    /// The language tag the client is set to e.g. en-us. (Undefined and invalid
264    /// becomes: `en-US`)
265    language_tag: Option<String>,
266    /// A string describing the theme (dark, light) or org.example.dark.
267    /// (default: `light`)
268    theme: Option<String>,
269}
270
271impl From<ClientProperties> for matrix_sdk::widget::ClientProperties {
272    fn from(value: ClientProperties) -> Self {
273        let ClientProperties { client_id, language_tag, theme } = value;
274        let language_tag = language_tag.and_then(|l| LanguageTag::parse(&l).ok());
275        Self::new(&client_id, language_tag, theme)
276    }
277}
278
279/// A handle that encapsulates the communication between a widget driver and the
280/// corresponding widget (inside a webview or IFrame).
281#[derive(uniffi::Object)]
282pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
283
284#[matrix_sdk_ffi_macros::export]
285impl WidgetDriverHandle {
286    /// Receive a message from the widget driver.
287    ///
288    /// The message must be passed on to the widget.
289    ///
290    /// Returns `None` if the widget driver is no longer running.
291    pub async fn recv(&self) -> Option<String> {
292        self.0.recv().await
293    }
294
295    //// Send a message from the widget to the widget driver.
296    ///
297    /// Returns `false` if the widget driver is no longer running.
298    pub async fn send(&self, msg: String) -> bool {
299        self.0.send(msg).await
300    }
301}
302
303/// Capabilities that a widget can request from a client.
304#[derive(uniffi::Record)]
305pub struct WidgetCapabilities {
306    /// Types of the messages that a widget wants to be able to fetch.
307    pub read: Vec<WidgetEventFilter>,
308    /// Types of the messages that a widget wants to be able to send.
309    pub send: Vec<WidgetEventFilter>,
310    /// If this capability is requested by the widget, it can not operate
311    /// separately from the Matrix client.
312    ///
313    /// This means clients should not offer to open the widget in a separate
314    /// browser/tab/webview that is not connected to the postmessage widget-api.
315    pub requires_client: bool,
316    /// This allows the widget to ask the client to update delayed events.
317    pub update_delayed_event: bool,
318    /// This allows the widget to send events with a delay.
319    pub send_delayed_event: bool,
320}
321
322impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
323    fn from(value: WidgetCapabilities) -> Self {
324        Self {
325            read: value.read.into_iter().map(Into::into).collect(),
326            send: value.send.into_iter().map(Into::into).collect(),
327            requires_client: value.requires_client,
328            update_delayed_event: value.update_delayed_event,
329            send_delayed_event: value.send_delayed_event,
330        }
331    }
332}
333
334impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
335    fn from(value: matrix_sdk::widget::Capabilities) -> Self {
336        Self {
337            read: value.read.into_iter().map(Into::into).collect(),
338            send: value.send.into_iter().map(Into::into).collect(),
339            requires_client: value.requires_client,
340            update_delayed_event: value.update_delayed_event,
341            send_delayed_event: value.send_delayed_event,
342        }
343    }
344}
345
346/// Different kinds of filters that could be applied to the timeline events.
347#[derive(uniffi::Enum, Clone)]
348pub enum WidgetEventFilter {
349    /// Matches message-like events with the given `type`.
350    MessageLikeWithType { event_type: String },
351    /// Matches `m.room.message` events with the given `msgtype`.
352    RoomMessageWithMsgtype { msgtype: String },
353    /// Matches state events with the given `type`, regardless of `state_key`.
354    StateWithType { event_type: String },
355    /// Matches state events with the given `type` and `state_key`.
356    StateWithTypeAndStateKey { event_type: String, state_key: String },
357    /// Matches to-device events with the given `event_type`.
358    ToDevice { event_type: String },
359}
360
361impl From<WidgetEventFilter> for matrix_sdk::widget::Filter {
362    fn from(value: WidgetEventFilter) -> Self {
363        match value {
364            WidgetEventFilter::MessageLikeWithType { event_type } => {
365                Self::MessageLike(MessageLikeEventFilter::WithType(event_type.into()))
366            }
367            WidgetEventFilter::RoomMessageWithMsgtype { msgtype } => {
368                Self::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype(msgtype))
369            }
370            WidgetEventFilter::StateWithType { event_type } => {
371                Self::State(StateEventFilter::WithType(event_type.into()))
372            }
373            WidgetEventFilter::StateWithTypeAndStateKey { event_type, state_key } => {
374                Self::State(StateEventFilter::WithTypeAndStateKey(event_type.into(), state_key))
375            }
376            WidgetEventFilter::ToDevice { event_type } => {
377                Self::ToDevice(ToDeviceEventFilter { event_type: event_type.into() })
378            }
379        }
380    }
381}
382
383impl From<matrix_sdk::widget::Filter> for WidgetEventFilter {
384    fn from(value: matrix_sdk::widget::Filter) -> Self {
385        use matrix_sdk::widget::Filter as F;
386
387        match value {
388            F::MessageLike(MessageLikeEventFilter::WithType(event_type)) => {
389                Self::MessageLikeWithType { event_type: event_type.to_string() }
390            }
391            F::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype(msgtype)) => {
392                Self::RoomMessageWithMsgtype { msgtype }
393            }
394            F::State(StateEventFilter::WithType(event_type)) => {
395                Self::StateWithType { event_type: event_type.to_string() }
396            }
397            F::State(StateEventFilter::WithTypeAndStateKey(event_type, state_key)) => {
398                Self::StateWithTypeAndStateKey { event_type: event_type.to_string(), state_key }
399            }
400            F::ToDevice(ToDeviceEventFilter { event_type }) => {
401                Self::ToDevice { event_type: event_type.to_string() }
402            }
403        }
404    }
405}
406
407#[matrix_sdk_ffi_macros::export(callback_interface)]
408pub trait WidgetCapabilitiesProvider: SendOutsideWasm + SyncOutsideWasm {
409    fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
410}
411
412struct CapabilitiesProviderWrap(Arc<dyn WidgetCapabilitiesProvider>);
413
414impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
415    async fn acquire_capabilities(
416        &self,
417        capabilities: matrix_sdk::widget::Capabilities,
418    ) -> matrix_sdk::widget::Capabilities {
419        let this = self.0.clone();
420        // This could require a prompt to the user. Ideally the callback
421        // interface would just be async, but that's not supported yet so use
422        // one of tokio's blocking task threads instead.
423        get_runtime_handle()
424            .spawn_blocking(move || this.acquire_capabilities(capabilities.into()).into())
425            .await
426            // propagate panics from the blocking task
427            .unwrap()
428    }
429}
430
431#[derive(Debug, thiserror::Error, uniffi::Error)]
432#[uniffi(flat_error)]
433pub enum ParseError {
434    #[error("empty host")]
435    EmptyHost,
436    #[error("invalid international domain name")]
437    IdnaError,
438    #[error("invalid port number")]
439    InvalidPort,
440    #[error("invalid IPv4 address")]
441    InvalidIpv4Address,
442    #[error("invalid IPv6 address")]
443    InvalidIpv6Address,
444    #[error("invalid domain character")]
445    InvalidDomainCharacter,
446    #[error("relative URL without a base")]
447    RelativeUrlWithoutBase,
448    #[error("relative URL with a cannot-be-a-base base")]
449    RelativeUrlWithCannotBeABaseBase,
450    #[error("a cannot-be-a-base URL doesn’t have a host to set")]
451    SetHostOnCannotBeABaseUrl,
452    #[error("URLs more than 4 GB are not supported")]
453    Overflow,
454    #[error("unknown URL parsing error")]
455    Other,
456}
457
458impl From<url::ParseError> for ParseError {
459    fn from(value: url::ParseError) -> Self {
460        match value {
461            url::ParseError::EmptyHost => Self::EmptyHost,
462            url::ParseError::IdnaError => Self::IdnaError,
463            url::ParseError::InvalidPort => Self::InvalidPort,
464            url::ParseError::InvalidIpv4Address => Self::InvalidIpv4Address,
465            url::ParseError::InvalidIpv6Address => Self::InvalidIpv6Address,
466            url::ParseError::InvalidDomainCharacter => Self::InvalidDomainCharacter,
467            url::ParseError::RelativeUrlWithoutBase => Self::RelativeUrlWithoutBase,
468            url::ParseError::RelativeUrlWithCannotBeABaseBase => {
469                Self::RelativeUrlWithCannotBeABaseBase
470            }
471            url::ParseError::SetHostOnCannotBeABaseUrl => Self::SetHostOnCannotBeABaseUrl,
472            url::ParseError::Overflow => Self::Overflow,
473            _ => Self::Other,
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use matrix_sdk::widget::Capabilities;
481
482    use super::get_element_call_required_permissions;
483
484    #[test]
485    fn element_call_permissions_are_correct() {
486        let widget_cap = get_element_call_required_permissions(
487            "@my_user:my_domain.org".to_owned(),
488            "ABCDEFGHI".to_owned(),
489        );
490
491        // We test two things:
492
493        // Converting the WidgetCapability (ffi struct) to Capabilities (rust sdk
494        // struct)
495        let cap = Into::<Capabilities>::into(widget_cap);
496        // Converting Capabilities (rust sdk struct) to a json list.
497        let cap_json_repr = serde_json::to_string(&cap).unwrap();
498
499        // Converting to a Vec<String> allows to check if the required elements exist
500        // without breaking the test each time the order of permissions might
501        // change.
502        let permission_array: Vec<String> = serde_json::from_str(&cap_json_repr).unwrap();
503
504        let cap_assert = |capability: &str| {
505            assert!(
506                permission_array.contains(&capability.to_owned()),
507                "The \"{capability}\" capability was missing from the element call capability list."
508            );
509        };
510
511        cap_assert("io.element.requires_client");
512        cap_assert("org.matrix.msc4157.update_delayed_event");
513        cap_assert("org.matrix.msc4157.send.delayed_event");
514        cap_assert("org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call.member");
515        cap_assert("org.matrix.msc2762.receive.state_event:m.room.name");
516        cap_assert("org.matrix.msc2762.receive.state_event:m.room.member");
517        cap_assert("org.matrix.msc2762.receive.state_event:m.room.encryption");
518        cap_assert("org.matrix.msc2762.receive.event:org.matrix.rageshake_request");
519        cap_assert("org.matrix.msc2762.receive.event:io.element.call.encryption_keys");
520        cap_assert("org.matrix.msc2762.receive.state_event:m.room.create");
521        cap_assert(
522            "org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org",
523        );
524        cap_assert(
525            "org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI",
526        );
527        cap_assert(
528            "org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI_m.call",
529        );
530        cap_assert(
531            "org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI",
532        );
533        cap_assert(
534            "org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI_m.call",
535        );
536        cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
537        cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
538
539        // RTC decline
540        cap_assert("org.matrix.msc2762.receive.event:org.matrix.msc4310.rtc.decline");
541        cap_assert("org.matrix.msc2762.send.event:org.matrix.msc4310.rtc.decline");
542    }
543}