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#[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 }
47 }
48}
49
50#[derive(uniffi::Record, Clone)]
52pub struct WidgetSettings {
53 pub widget_id: String,
55 pub init_after_content_load: bool,
59 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#[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#[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#[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 WidgetEventFilter::MessageLikeWithType {
157 event_type: "org.matrix.rageshake_request".to_owned(),
158 },
159 WidgetEventFilter::ToDevice { event_type: "io.element.call.encryption_keys".to_owned() },
161 WidgetEventFilter::MessageLikeWithType {
164 event_type: "io.element.call.encryption_keys".to_owned(),
165 },
166 WidgetEventFilter::MessageLikeWithType {
169 event_type: "io.element.call.reaction".to_owned(),
170 },
171 WidgetEventFilter::MessageLikeWithType {
173 event_type: MessageLikeEventType::Reaction.to_string(),
174 },
175 WidgetEventFilter::MessageLikeWithType {
177 event_type: MessageLikeEventType::RoomRedaction.to_string(),
178 },
179 WidgetEventFilter::MessageLikeWithType {
181 event_type: MessageLikeEventType::RtcDecline.to_string(),
182 },
183 ];
184
185 WidgetCapabilities {
186 read: vec![
187 WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
189 WidgetEventFilter::StateWithType { event_type: StateEventType::RoomName.to_string() },
191 WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
193 WidgetEventFilter::StateWithType {
195 event_type: StateEventType::RoomEncryption.to_string(),
196 },
197 WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
200 ]
201 .into_iter()
202 .chain(read_send.clone())
203 .collect(),
204 send: vec![
205 WidgetEventFilter::MessageLikeWithType {
207 event_type: MessageLikeEventType::RtcNotification.to_string(),
208 },
209 WidgetEventFilter::MessageLikeWithType {
214 event_type: MessageLikeEventType::CallNotify.to_string(),
215 },
216 WidgetEventFilter::StateWithTypeAndStateKey {
221 event_type: StateEventType::CallMember.to_string(),
222 state_key: own_user_id.clone(),
223 },
224 WidgetEventFilter::StateWithTypeAndStateKey {
227 event_type: StateEventType::CallMember.to_string(),
228 state_key: format!("{own_user_id}_{own_device_id}"),
229 },
230 WidgetEventFilter::StateWithTypeAndStateKey {
233 event_type: StateEventType::CallMember.to_string(),
234 state_key: format!("{own_user_id}_{own_device_id}_m.call"),
235 },
236 WidgetEventFilter::StateWithTypeAndStateKey {
240 event_type: StateEventType::CallMember.to_string(),
241 state_key: format!("_{own_user_id}_{own_device_id}"),
242 },
243 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 client_id: String,
263 language_tag: Option<String>,
266 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#[derive(uniffi::Object)]
282pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
283
284#[matrix_sdk_ffi_macros::export]
285impl WidgetDriverHandle {
286 pub async fn recv(&self) -> Option<String> {
292 self.0.recv().await
293 }
294
295 pub async fn send(&self, msg: String) -> bool {
299 self.0.send(msg).await
300 }
301}
302
303#[derive(uniffi::Record)]
305pub struct WidgetCapabilities {
306 pub read: Vec<WidgetEventFilter>,
308 pub send: Vec<WidgetEventFilter>,
310 pub requires_client: bool,
316 pub update_delayed_event: bool,
318 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#[derive(uniffi::Enum, Clone)]
348pub enum WidgetEventFilter {
349 MessageLikeWithType { event_type: String },
351 RoomMessageWithMsgtype { msgtype: String },
353 StateWithType { event_type: String },
355 StateWithTypeAndStateKey { event_type: String, state_key: String },
357 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 get_runtime_handle()
424 .spawn_blocking(move || this.acquire_capabilities(capabilities.into()).into())
425 .await
426 .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 let cap = Into::<Capabilities>::into(widget_cap);
496 let cap_json_repr = serde_json::to_string(&cap).unwrap();
498
499 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 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}