atagen

matrix-rust-sdk

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