1use std::sync::{Arc, Mutex};
2
3use language_tags::LanguageTag;
4use matrix_sdk::{
5 async_trait,
6 widget::{MessageLikeEventFilter, StateEventFilter},
7};
8use ruma::events::MessageLikeEventType;
9use tracing::error;
10
11use crate::{room::Room, RUNTIME};
12
13#[derive(uniffi::Record)]
14pub struct WidgetDriverAndHandle {
15 pub driver: Arc<WidgetDriver>,
16 pub handle: Arc<WidgetDriverHandle>,
17}
18
19#[matrix_sdk_ffi_macros::export]
20pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHandle, ParseError> {
21 let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?);
22 Ok(WidgetDriverAndHandle {
23 driver: Arc::new(WidgetDriver(Mutex::new(Some(driver)))),
24 handle: Arc::new(WidgetDriverHandle(handle)),
25 })
26}
27
28#[derive(uniffi::Object)]
31pub struct WidgetDriver(Mutex<Option<matrix_sdk::widget::WidgetDriver>>);
32
33#[matrix_sdk_ffi_macros::export]
34impl WidgetDriver {
35 pub async fn run(
36 &self,
37 room: Arc<Room>,
38 capabilities_provider: Box<dyn WidgetCapabilitiesProvider>,
39 ) {
40 let Some(driver) = self.0.lock().unwrap().take() else {
41 error!("Can't call run multiple times on a WidgetDriver");
42 return;
43 };
44
45 let capabilities_provider = CapabilitiesProviderWrap(capabilities_provider.into());
46 if let Err(()) = driver.run(room.inner.clone(), capabilities_provider).await {
47 }
49 }
50}
51
52#[derive(uniffi::Record, Clone)]
54pub struct WidgetSettings {
55 pub widget_id: String,
57 pub init_after_content_load: bool,
61 raw_url: String,
71}
72
73impl TryFrom<WidgetSettings> for matrix_sdk::widget::WidgetSettings {
74 type Error = ParseError;
75
76 fn try_from(value: WidgetSettings) -> Result<Self, Self::Error> {
77 let WidgetSettings { widget_id, init_after_content_load, raw_url } = value;
78 Ok(matrix_sdk::widget::WidgetSettings::new(widget_id, init_after_content_load, &raw_url)?)
79 }
80}
81
82impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
83 fn from(value: matrix_sdk::widget::WidgetSettings) -> Self {
84 WidgetSettings {
85 widget_id: value.widget_id().to_owned(),
86 init_after_content_load: value.init_on_content_load(),
87 raw_url: value.raw_url().to_string(),
88 }
89 }
90}
91
92#[matrix_sdk_ffi_macros::export]
101pub async fn generate_webview_url(
102 widget_settings: WidgetSettings,
103 room: Arc<Room>,
104 props: ClientProperties,
105) -> Result<String, ParseError> {
106 Ok(matrix_sdk::widget::WidgetSettings::generate_webview_url(
107 &widget_settings.clone().try_into()?,
108 &room.inner,
109 props.into(),
110 )
111 .await
112 .map(|url| url.to_string())?)
113}
114
115#[derive(uniffi::Enum, Clone)]
119pub enum EncryptionSystem {
120 Unencrypted,
122 PerParticipantKeys,
125 SharedSecret {
128 secret: String,
130 },
131}
132
133impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
134 fn from(value: EncryptionSystem) -> Self {
135 match value {
136 EncryptionSystem::Unencrypted => Self::Unencrypted,
137 EncryptionSystem::PerParticipantKeys => Self::PerParticipantKeys,
138 EncryptionSystem::SharedSecret { secret } => Self::SharedSecret { secret },
139 }
140 }
141}
142
143#[derive(uniffi::Record, Clone)]
145pub struct VirtualElementCallWidgetOptions {
146 pub element_call_url: String,
150
151 pub widget_id: String,
153
154 pub parent_url: Option<String>,
168
169 pub hide_header: Option<bool>,
173
174 pub preload: Option<bool>,
179
180 pub font_scale: Option<f64>,
184
185 pub app_prompt: Option<bool>,
190
191 pub skip_lobby: Option<bool>,
195
196 pub confine_to_room: Option<bool>,
200
201 pub font: Option<String>,
203
204 pub analytics_id: Option<String>,
206
207 pub encryption: EncryptionSystem,
211}
212
213impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
214 fn from(value: VirtualElementCallWidgetOptions) -> Self {
215 Self {
216 element_call_url: value.element_call_url,
217 widget_id: value.widget_id,
218 parent_url: value.parent_url,
219 hide_header: value.hide_header,
220 preload: value.preload,
221 font_scale: value.font_scale,
222 app_prompt: value.app_prompt,
223 skip_lobby: value.skip_lobby,
224 confine_to_room: value.confine_to_room,
225 font: value.font,
226 analytics_id: value.analytics_id,
227 encryption: value.encryption.into(),
228 }
229 }
230}
231
232#[matrix_sdk_ffi_macros::export]
246pub fn new_virtual_element_call_widget(
247 props: VirtualElementCallWidgetOptions,
248) -> Result<WidgetSettings, ParseError> {
249 Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props.into())
250 .map(|w| w.into())?)
251}
252
253#[matrix_sdk_ffi_macros::export]
266pub fn get_element_call_required_permissions(
267 own_user_id: String,
268 own_device_id: String,
269) -> WidgetCapabilities {
270 use ruma::events::StateEventType;
271
272 let read_send = vec![
273 WidgetEventFilter::MessageLikeWithType {
275 event_type: "org.matrix.rageshake_request".to_owned(),
276 },
277 WidgetEventFilter::MessageLikeWithType {
280 event_type: "io.element.call.encryption_keys".to_owned(),
281 },
282 WidgetEventFilter::MessageLikeWithType {
285 event_type: "io.element.call.reaction".to_owned(),
286 },
287 WidgetEventFilter::MessageLikeWithType {
289 event_type: MessageLikeEventType::Reaction.to_string(),
290 },
291 WidgetEventFilter::MessageLikeWithType {
293 event_type: MessageLikeEventType::RoomRedaction.to_string(),
294 },
295 ];
296
297 WidgetCapabilities {
298 read: vec![
299 WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
301 WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
303 WidgetEventFilter::StateWithType {
305 event_type: StateEventType::RoomEncryption.to_string(),
306 },
307 WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
310 ]
311 .into_iter()
312 .chain(read_send.clone())
313 .collect(),
314 send: vec![
315 WidgetEventFilter::StateWithTypeAndStateKey {
320 event_type: StateEventType::CallMember.to_string(),
321 state_key: own_user_id.clone(),
322 },
323 WidgetEventFilter::StateWithTypeAndStateKey {
326 event_type: StateEventType::CallMember.to_string(),
327 state_key: format!("{own_user_id}_{own_device_id}"),
328 },
329 WidgetEventFilter::StateWithTypeAndStateKey {
333 event_type: StateEventType::CallMember.to_string(),
334 state_key: format!("_{own_user_id}_{own_device_id}"),
335 },
336 ]
337 .into_iter()
338 .chain(read_send)
339 .collect(),
340 requires_client: true,
341 update_delayed_event: true,
342 send_delayed_event: true,
343 }
344}
345
346#[derive(uniffi::Record)]
347pub struct ClientProperties {
348 client_id: String,
351 language_tag: Option<String>,
354 theme: Option<String>,
357}
358
359impl From<ClientProperties> for matrix_sdk::widget::ClientProperties {
360 fn from(value: ClientProperties) -> Self {
361 let ClientProperties { client_id, language_tag, theme } = value;
362 let language_tag = language_tag.and_then(|l| LanguageTag::parse(&l).ok());
363 Self::new(&client_id, language_tag, theme)
364 }
365}
366
367#[derive(uniffi::Object)]
370pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
371
372#[matrix_sdk_ffi_macros::export]
373impl WidgetDriverHandle {
374 pub async fn recv(&self) -> Option<String> {
380 self.0.recv().await
381 }
382
383 pub async fn send(&self, msg: String) -> bool {
387 self.0.send(msg).await
388 }
389}
390
391#[derive(uniffi::Record)]
393pub struct WidgetCapabilities {
394 pub read: Vec<WidgetEventFilter>,
396 pub send: Vec<WidgetEventFilter>,
398 pub requires_client: bool,
404 pub update_delayed_event: bool,
406 pub send_delayed_event: bool,
408}
409
410impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
411 fn from(value: WidgetCapabilities) -> Self {
412 Self {
413 read: value.read.into_iter().map(Into::into).collect(),
414 send: value.send.into_iter().map(Into::into).collect(),
415 requires_client: value.requires_client,
416 update_delayed_event: value.update_delayed_event,
417 send_delayed_event: value.send_delayed_event,
418 }
419 }
420}
421
422impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
423 fn from(value: matrix_sdk::widget::Capabilities) -> Self {
424 Self {
425 read: value.read.into_iter().map(Into::into).collect(),
426 send: value.send.into_iter().map(Into::into).collect(),
427 requires_client: value.requires_client,
428 update_delayed_event: value.update_delayed_event,
429 send_delayed_event: value.send_delayed_event,
430 }
431 }
432}
433
434#[derive(uniffi::Enum, Clone)]
436pub enum WidgetEventFilter {
437 MessageLikeWithType { event_type: String },
439 RoomMessageWithMsgtype { msgtype: String },
441 StateWithType { event_type: String },
443 StateWithTypeAndStateKey { event_type: String, state_key: String },
445}
446
447impl From<WidgetEventFilter> for matrix_sdk::widget::EventFilter {
448 fn from(value: WidgetEventFilter) -> Self {
449 match value {
450 WidgetEventFilter::MessageLikeWithType { event_type } => {
451 Self::MessageLike(MessageLikeEventFilter::WithType(event_type.into()))
452 }
453 WidgetEventFilter::RoomMessageWithMsgtype { msgtype } => {
454 Self::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype(msgtype))
455 }
456 WidgetEventFilter::StateWithType { event_type } => {
457 Self::State(StateEventFilter::WithType(event_type.into()))
458 }
459 WidgetEventFilter::StateWithTypeAndStateKey { event_type, state_key } => {
460 Self::State(StateEventFilter::WithTypeAndStateKey(event_type.into(), state_key))
461 }
462 }
463 }
464}
465
466impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
467 fn from(value: matrix_sdk::widget::EventFilter) -> Self {
468 use matrix_sdk::widget::EventFilter as F;
469
470 match value {
471 F::MessageLike(MessageLikeEventFilter::WithType(event_type)) => {
472 Self::MessageLikeWithType { event_type: event_type.to_string() }
473 }
474 F::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype(msgtype)) => {
475 Self::RoomMessageWithMsgtype { msgtype }
476 }
477 F::State(StateEventFilter::WithType(event_type)) => {
478 Self::StateWithType { event_type: event_type.to_string() }
479 }
480 F::State(StateEventFilter::WithTypeAndStateKey(event_type, state_key)) => {
481 Self::StateWithTypeAndStateKey { event_type: event_type.to_string(), state_key }
482 }
483 }
484 }
485}
486
487#[matrix_sdk_ffi_macros::export(callback_interface)]
488pub trait WidgetCapabilitiesProvider: Send + Sync {
489 fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
490}
491
492struct CapabilitiesProviderWrap(Arc<dyn WidgetCapabilitiesProvider>);
493
494#[async_trait]
495impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
496 async fn acquire_capabilities(
497 &self,
498 capabilities: matrix_sdk::widget::Capabilities,
499 ) -> matrix_sdk::widget::Capabilities {
500 let this = self.0.clone();
501 RUNTIME
505 .spawn_blocking(move || this.acquire_capabilities(capabilities.into()).into())
506 .await
507 .unwrap()
509 }
510}
511
512#[derive(Debug, thiserror::Error, uniffi::Error)]
513#[uniffi(flat_error)]
514pub enum ParseError {
515 #[error("empty host")]
516 EmptyHost,
517 #[error("invalid international domain name")]
518 IdnaError,
519 #[error("invalid port number")]
520 InvalidPort,
521 #[error("invalid IPv4 address")]
522 InvalidIpv4Address,
523 #[error("invalid IPv6 address")]
524 InvalidIpv6Address,
525 #[error("invalid domain character")]
526 InvalidDomainCharacter,
527 #[error("relative URL without a base")]
528 RelativeUrlWithoutBase,
529 #[error("relative URL with a cannot-be-a-base base")]
530 RelativeUrlWithCannotBeABaseBase,
531 #[error("a cannot-be-a-base URL doesn’t have a host to set")]
532 SetHostOnCannotBeABaseUrl,
533 #[error("URLs more than 4 GB are not supported")]
534 Overflow,
535 #[error("unknown URL parsing error")]
536 Other,
537}
538
539impl From<url::ParseError> for ParseError {
540 fn from(value: url::ParseError) -> Self {
541 match value {
542 url::ParseError::EmptyHost => Self::EmptyHost,
543 url::ParseError::IdnaError => Self::IdnaError,
544 url::ParseError::InvalidPort => Self::InvalidPort,
545 url::ParseError::InvalidIpv4Address => Self::InvalidIpv4Address,
546 url::ParseError::InvalidIpv6Address => Self::InvalidIpv6Address,
547 url::ParseError::InvalidDomainCharacter => Self::InvalidDomainCharacter,
548 url::ParseError::RelativeUrlWithoutBase => Self::RelativeUrlWithoutBase,
549 url::ParseError::RelativeUrlWithCannotBeABaseBase => {
550 Self::RelativeUrlWithCannotBeABaseBase
551 }
552 url::ParseError::SetHostOnCannotBeABaseUrl => Self::SetHostOnCannotBeABaseUrl,
553 url::ParseError::Overflow => Self::Overflow,
554 _ => Self::Other,
555 }
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use matrix_sdk::widget::Capabilities;
562
563 use super::get_element_call_required_permissions;
564
565 #[test]
566 fn element_call_permissions_are_correct() {
567 let widget_cap = get_element_call_required_permissions(
568 "@my_user:my_domain.org".to_owned(),
569 "ABCDEFGHI".to_owned(),
570 );
571
572 let cap = Into::<Capabilities>::into(widget_cap);
577 let cap_json_repr = serde_json::to_string(&cap).unwrap();
579
580 let permission_array: Vec<String> = serde_json::from_str(&cap_json_repr).unwrap();
584
585 let cap_assert = |capability: &str| {
586 assert!(
587 permission_array.contains(&capability.to_owned()),
588 "The \"{}\" capability was missing from the element call capability list.",
589 capability
590 );
591 };
592
593 cap_assert("io.element.requires_client");
594 cap_assert("org.matrix.msc4157.update_delayed_event");
595 cap_assert("org.matrix.msc4157.send.delayed_event");
596 cap_assert("org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call.member");
597 cap_assert("org.matrix.msc2762.receive.state_event:m.room.member");
598 cap_assert("org.matrix.msc2762.receive.state_event:m.room.encryption");
599 cap_assert("org.matrix.msc2762.receive.event:org.matrix.rageshake_request");
600 cap_assert("org.matrix.msc2762.receive.event:io.element.call.encryption_keys");
601 cap_assert("org.matrix.msc2762.receive.state_event:m.room.create");
602 cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org");
603 cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI");
604 cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI");
605 cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
606 cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
607 }
608}