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#[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 }
50 }
51}
52
53#[derive(uniffi::Record, Clone)]
55pub struct WidgetSettings {
56 pub widget_id: String,
58 pub init_after_content_load: bool,
62 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#[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#[derive(uniffi::Enum, Clone)]
120pub enum EncryptionSystem {
121 Unencrypted,
123 PerParticipantKeys,
126 SharedSecret {
129 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#[derive(uniffi::Enum, Clone)]
148pub enum Intent {
149 StartCall,
151 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#[derive(uniffi::Record, Clone)]
165pub struct VirtualElementCallWidgetOptions {
166 pub element_call_url: String,
170
171 pub widget_id: String,
173
174 pub parent_url: Option<String>,
188
189 pub hide_header: Option<bool>,
193
194 pub preload: Option<bool>,
199
200 pub font_scale: Option<f64>,
204
205 pub app_prompt: Option<bool>,
210
211 pub confine_to_room: Option<bool>,
215
216 pub font: Option<String>,
218
219 pub encryption: EncryptionSystem,
223
224 pub intent: Option<Intent>,
228
229 pub hide_screensharing: bool,
231
232 pub posthog_user_id: Option<String>,
234 pub posthog_api_host: Option<String>,
237 pub posthog_api_key: Option<String>,
240
241 pub rageshake_submit_url: Option<String>,
244
245 pub sentry_dsn: Option<String>,
248 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#[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#[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 WidgetEventFilter::MessageLikeWithType {
321 event_type: "org.matrix.rageshake_request".to_owned(),
322 },
323 WidgetEventFilter::MessageLikeWithType {
326 event_type: "io.element.call.encryption_keys".to_owned(),
327 },
328 WidgetEventFilter::MessageLikeWithType {
331 event_type: "io.element.call.reaction".to_owned(),
332 },
333 WidgetEventFilter::MessageLikeWithType {
335 event_type: MessageLikeEventType::Reaction.to_string(),
336 },
337 WidgetEventFilter::MessageLikeWithType {
339 event_type: MessageLikeEventType::RoomRedaction.to_string(),
340 },
341 ];
342
343 WidgetCapabilities {
344 read: vec![
345 WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
347 WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
349 WidgetEventFilter::StateWithType {
351 event_type: StateEventType::RoomEncryption.to_string(),
352 },
353 WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
356 ]
357 .into_iter()
358 .chain(read_send.clone())
359 .collect(),
360 send: vec![
361 WidgetEventFilter::StateWithTypeAndStateKey {
366 event_type: StateEventType::CallMember.to_string(),
367 state_key: own_user_id.clone(),
368 },
369 WidgetEventFilter::StateWithTypeAndStateKey {
372 event_type: StateEventType::CallMember.to_string(),
373 state_key: format!("{own_user_id}_{own_device_id}"),
374 },
375 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 client_id: String,
397 language_tag: Option<String>,
400 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#[derive(uniffi::Object)]
416pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
417
418#[matrix_sdk_ffi_macros::export]
419impl WidgetDriverHandle {
420 pub async fn recv(&self) -> Option<String> {
426 self.0.recv().await
427 }
428
429 pub async fn send(&self, msg: String) -> bool {
433 self.0.send(msg).await
434 }
435}
436
437#[derive(uniffi::Record)]
439pub struct WidgetCapabilities {
440 pub read: Vec<WidgetEventFilter>,
442 pub send: Vec<WidgetEventFilter>,
444 pub requires_client: bool,
450 pub update_delayed_event: bool,
452 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#[derive(uniffi::Enum, Clone)]
482pub enum WidgetEventFilter {
483 MessageLikeWithType { event_type: String },
485 RoomMessageWithMsgtype { msgtype: String },
487 StateWithType { event_type: String },
489 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 get_runtime_handle()
551 .spawn_blocking(move || this.acquire_capabilities(capabilities.into()).into())
552 .await
553 .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 let cap = Into::<Capabilities>::into(widget_cap);
623 let cap_json_repr = serde_json::to_string(&cap).unwrap();
625
626 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}