matrix_sdk_ffi/
widget.rs

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