matrix_sdk_ffi/
client.rs

1use std::{
2    collections::HashMap,
3    fmt::Debug,
4    path::PathBuf,
5    sync::{Arc, OnceLock},
6    time::Duration,
7};
8
9use anyhow::{anyhow, Context as _};
10use futures_util::pin_mut;
11#[cfg(not(target_family = "wasm"))]
12use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
13#[cfg(feature = "sqlite")]
14use matrix_sdk::STATE_STORE_DATABASE_NAME;
15use matrix_sdk::{
16    authentication::oauth::{
17        AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
18    },
19    deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
20    media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
21    ruma::{
22        api::client::{
23            discovery::{
24                discover_homeserver::RtcFocusInfo,
25                get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
26            },
27            push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
28            room::{create_room, Visibility},
29            session::get_login_types,
30            user_directory::search_users,
31        },
32        events::{
33            room::{
34                avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent,
35                message::MessageType,
36            },
37            AnyInitialStateEvent, InitialStateEvent,
38        },
39        serde::Raw,
40        EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
41    },
42    sliding_sync::Version as SdkSlidingSyncVersion,
43    store::RoomLoadSettings as SdkRoomLoadSettings,
44    Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
45};
46use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
47use matrix_sdk_ui::{
48    notification_client::{
49        NotificationClient as MatrixNotificationClient,
50        NotificationProcessSetup as MatrixNotificationProcessSetup,
51    },
52    spaces::SpaceService as UISpaceService,
53    unable_to_decrypt_hook::UtdHookManager,
54};
55use mime::Mime;
56use oauth2::Scope;
57use ruma::{
58    api::client::{
59        alias::get_alias,
60        error::ErrorKind,
61        profile::{AvatarUrl, DisplayName},
62        uiaa::UserIdentifier,
63    },
64    events::{
65        direct::DirectEventContent,
66        fully_read::FullyReadEventContent,
67        identity_server::IdentityServerEventContent,
68        ignored_user_list::IgnoredUserListEventContent,
69        key::verification::request::ToDeviceKeyVerificationRequestEvent,
70        marked_unread::{MarkedUnreadEventContent, UnstableMarkedUnreadEventContent},
71        push_rules::PushRulesEventContent,
72        room::{
73            history_visibility::RoomHistoryVisibilityEventContent,
74            join_rules::{
75                AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
76            },
77            message::{OriginalSyncRoomMessageEvent, Relation},
78            power_levels::RoomPowerLevelsEventContent,
79        },
80        secret_storage::{
81            default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
82        },
83        tag::TagEventContent,
84        AnyMessageLikeEventContent, AnySyncTimelineEvent,
85        GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
86        RoomAccountDataEvent as RumaRoomAccountDataEvent,
87    },
88    push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
89    room_version_rules::AuthorizationRules,
90    OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
91};
92use serde::{Deserialize, Serialize};
93use serde_json::{json, Value};
94use tokio::sync::broadcast::error::RecvError;
95use tracing::{debug, error};
96use url::Url;
97
98use super::{
99    room::{room_info::RoomInfo, Room},
100    session_verification::SessionVerificationController,
101};
102use crate::{
103    authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
104    client,
105    encryption::Encryption,
106    notification::{
107        NotificationClient, NotificationEvent, NotificationItem, NotificationRoomInfo,
108        NotificationSenderInfo,
109    },
110    notification_settings::NotificationSettings,
111    qr_code::{GrantLoginWithQrCodeHandler, LoginWithQrCodeHandler},
112    room::{RoomHistoryVisibility, RoomInfoListener, RoomSendQueueUpdate},
113    room_directory_search::RoomDirectorySearch,
114    room_preview::RoomPreview,
115    ruma::{
116        AccountDataEvent, AccountDataEventType, AuthData, InviteAvatars, MediaPreviewConfig,
117        MediaPreviews, MediaSource, RoomAccountDataEvent, RoomAccountDataEventType,
118    },
119    runtime::get_runtime_handle,
120    spaces::SpaceService,
121    sync_service::{SyncService, SyncServiceBuilder},
122    task_handle::TaskHandle,
123    utd::{UnableToDecryptDelegate, UtdHook},
124    utils::AsyncRuntimeDropped,
125    ClientError,
126};
127
128#[derive(Clone, uniffi::Record)]
129pub struct PusherIdentifiers {
130    pub pushkey: String,
131    pub app_id: String,
132}
133
134impl From<PusherIdentifiers> for PusherIds {
135    fn from(value: PusherIdentifiers) -> Self {
136        Self::new(value.pushkey, value.app_id)
137    }
138}
139
140#[derive(Clone, uniffi::Record)]
141pub struct HttpPusherData {
142    pub url: String,
143    pub format: Option<PushFormat>,
144    pub default_payload: Option<String>,
145}
146
147#[derive(Clone, uniffi::Enum)]
148pub enum PusherKind {
149    Http { data: HttpPusherData },
150    Email,
151}
152
153impl TryFrom<PusherKind> for RumaPusherKind {
154    type Error = anyhow::Error;
155
156    fn try_from(value: PusherKind) -> anyhow::Result<Self> {
157        match value {
158            PusherKind::Http { data } => {
159                let mut ruma_data = RumaHttpPusherData::new(data.url);
160                if let Some(payload) = data.default_payload {
161                    let json: Value = serde_json::from_str(&payload)?;
162                    ruma_data.data.insert("default_payload".to_owned(), json);
163                }
164                ruma_data.format = data.format.map(Into::into);
165                Ok(Self::Http(ruma_data))
166            }
167            PusherKind::Email => {
168                let ruma_data = EmailPusherData::new();
169                Ok(Self::Email(ruma_data))
170            }
171        }
172    }
173}
174
175#[derive(Clone, uniffi::Enum)]
176pub enum PushFormat {
177    EventIdOnly,
178}
179
180impl From<PushFormat> for RumaPushFormat {
181    fn from(value: PushFormat) -> Self {
182        match value {
183            client::PushFormat::EventIdOnly => Self::EventIdOnly,
184        }
185    }
186}
187
188#[matrix_sdk_ffi_macros::export(callback_interface)]
189pub trait ClientDelegate: SyncOutsideWasm + SendOutsideWasm {
190    fn did_receive_auth_error(&self, is_soft_logout: bool);
191}
192
193#[matrix_sdk_ffi_macros::export(callback_interface)]
194pub trait ClientSessionDelegate: SyncOutsideWasm + SendOutsideWasm {
195    fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
196    fn save_session_in_keychain(&self, session: Session);
197}
198
199#[matrix_sdk_ffi_macros::export(callback_interface)]
200pub trait ProgressWatcher: SyncOutsideWasm + SendOutsideWasm {
201    fn transmission_progress(&self, progress: TransmissionProgress);
202}
203
204/// A listener to the global (client-wide) update reporter of the send queue.
205#[matrix_sdk_ffi_macros::export(callback_interface)]
206pub trait SendQueueRoomUpdateListener: SyncOutsideWasm + SendOutsideWasm {
207    /// Called every time the send queue emits an update for a given room.
208    fn on_update(&self, room_id: String, update: RoomSendQueueUpdate);
209}
210
211/// A listener to the global (client-wide) error reporter of the send queue.
212#[matrix_sdk_ffi_macros::export(callback_interface)]
213pub trait SendQueueRoomErrorListener: SyncOutsideWasm + SendOutsideWasm {
214    /// Called every time the send queue has ran into an error for a given room,
215    /// which will disable the send queue for that particular room.
216    fn on_error(&self, room_id: String, error: ClientError);
217}
218
219/// A listener for changes of global account data events.
220#[matrix_sdk_ffi_macros::export(callback_interface)]
221pub trait AccountDataListener: SyncOutsideWasm + SendOutsideWasm {
222    /// Called when a global account data event has changed.
223    fn on_change(&self, event: AccountDataEvent);
224}
225
226/// A listener for changes of room account data events.
227#[matrix_sdk_ffi_macros::export(callback_interface)]
228pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
229    /// Called when a room account data event was changed.
230    fn on_change(&self, event: RoomAccountDataEvent, room_id: String);
231}
232
233/// A listener for notifications generated from sync responses.
234///
235/// This is called during sync for each event that triggers a notification
236/// based on the user's push rules.
237#[matrix_sdk_ffi_macros::export(callback_interface)]
238pub trait SyncNotificationListener: SyncOutsideWasm + SendOutsideWasm {
239    /// Called when a notifying event is received during sync.
240    fn on_notification(&self, notification: NotificationItem, room_id: String);
241}
242
243#[derive(Clone, Copy, uniffi::Record)]
244pub struct TransmissionProgress {
245    pub current: u64,
246    pub total: u64,
247}
248
249impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
250    fn from(value: matrix_sdk::TransmissionProgress) -> Self {
251        Self {
252            current: value.current.try_into().unwrap_or(u64::MAX),
253            total: value.total.try_into().unwrap_or(u64::MAX),
254        }
255    }
256}
257
258#[derive(uniffi::Object)]
259pub struct Client {
260    pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
261
262    delegate: OnceLock<Arc<dyn ClientDelegate>>,
263
264    pub(crate) utd_hook_manager: OnceLock<Arc<UtdHookManager>>,
265
266    session_verification_controller:
267        Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
268
269    /// The path to the directory where the state store and the crypto store are
270    /// located, if the `Client` instance has been built with a store (either
271    /// SQLite or IndexedDB).
272    #[cfg_attr(not(feature = "sqlite"), allow(unused))]
273    store_path: Option<PathBuf>,
274}
275
276impl Client {
277    pub async fn new(
278        sdk_client: MatrixClient,
279        enable_oidc_refresh_lock: bool,
280        session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
281        store_path: Option<PathBuf>,
282    ) -> Result<Self, ClientError> {
283        let session_verification_controller: Arc<
284            tokio::sync::RwLock<Option<SessionVerificationController>>,
285        > = Default::default();
286        let controller = session_verification_controller.clone();
287        sdk_client.add_event_handler(
288            move |event: ToDeviceKeyVerificationRequestEvent| async move {
289                if let Some(session_verification_controller) = &*controller.clone().read().await {
290                    session_verification_controller
291                        .process_incoming_verification_request(
292                            &event.sender,
293                            event.content.transaction_id,
294                        )
295                        .await;
296                }
297            },
298        );
299
300        let controller = session_verification_controller.clone();
301        sdk_client.add_event_handler(move |event: OriginalSyncRoomMessageEvent| async move {
302            if let MessageType::VerificationRequest(_) = &event.content.msgtype {
303                if let Some(session_verification_controller) = &*controller.clone().read().await {
304                    session_verification_controller
305                        .process_incoming_verification_request(&event.sender, event.event_id)
306                        .await;
307                }
308            }
309        });
310
311        let cross_process_store_locks_holder_name =
312            sdk_client.cross_process_store_locks_holder_name().to_owned();
313
314        let client = Client {
315            inner: AsyncRuntimeDropped::new(sdk_client.clone()),
316            delegate: OnceLock::new(),
317            utd_hook_manager: OnceLock::new(),
318            session_verification_controller,
319            store_path,
320        };
321
322        if enable_oidc_refresh_lock {
323            if session_delegate.is_none() {
324                return Err(anyhow::anyhow!(
325                    "missing session delegates when enabling the cross-process lock"
326                ))?;
327            }
328
329            client
330                .inner
331                .oauth()
332                .enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
333                .await?;
334        }
335
336        if let Some(session_delegate) = session_delegate {
337            client.inner.set_session_callbacks(
338                {
339                    let session_delegate = session_delegate.clone();
340                    Box::new(move |client| {
341                        let session_delegate = session_delegate.clone();
342                        let user_id = client.user_id().context("user isn't logged in")?;
343                        Ok(Self::retrieve_session(session_delegate, user_id)?)
344                    })
345                },
346                {
347                    let session_delegate = session_delegate.clone();
348                    Box::new(move |client| {
349                        let session_delegate = session_delegate.clone();
350                        Ok(Self::save_session(session_delegate, client)?)
351                    })
352                },
353            )?;
354        }
355
356        Ok(client)
357    }
358}
359
360#[matrix_sdk_ffi_macros::export]
361impl Client {
362    /// Perform database optimizations if any are available, i.e. vacuuming in
363    /// SQLite.
364    pub async fn optimize_stores(&self) -> Result<(), ClientError> {
365        Ok(self.inner.optimize_stores().await?)
366    }
367
368    /// Returns the sizes of the existing stores, if known.
369    pub async fn get_store_sizes(&self) -> Result<StoreSizes, ClientError> {
370        Ok(self.inner.get_store_sizes().await?.into())
371    }
372
373    /// Information about login options for the client's homeserver.
374    pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
375        let oauth = self.inner.oauth();
376        let (supports_oidc_login, supported_oidc_prompts) = match oauth.server_metadata().await {
377            Ok(metadata) => {
378                let prompts =
379                    metadata.prompt_values_supported.into_iter().map(Into::into).collect();
380
381                (true, prompts)
382            }
383            Err(error) => {
384                error!("Failed to fetch OIDC provider metadata: {error}");
385                (false, Default::default())
386            }
387        };
388
389        let login_types = self.inner.matrix_auth().get_login_types().await.ok();
390        let supports_password_login = login_types
391            .as_ref()
392            .map(|login_types| {
393                login_types.flows.iter().any(|login_type| {
394                    matches!(login_type, get_login_types::v3::LoginType::Password(_))
395                })
396            })
397            .unwrap_or(false);
398        let supports_sso_login = login_types
399            .as_ref()
400            .map(|login_types| {
401                login_types
402                    .flows
403                    .iter()
404                    .any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Sso(_)))
405            })
406            .unwrap_or(false);
407        let sliding_sync_version = self.sliding_sync_version();
408
409        Arc::new(HomeserverLoginDetails {
410            url: self.homeserver(),
411            sliding_sync_version,
412            supports_oidc_login,
413            supported_oidc_prompts,
414            supports_sso_login,
415            supports_password_login,
416        })
417    }
418
419    /// Login using a username and password.
420    pub async fn login(
421        &self,
422        username: String,
423        password: String,
424        initial_device_name: Option<String>,
425        device_id: Option<String>,
426    ) -> Result<(), ClientError> {
427        let mut builder = self.inner.matrix_auth().login_username(&username, &password);
428        if let Some(initial_device_name) = initial_device_name.as_ref() {
429            builder = builder.initial_device_display_name(initial_device_name);
430        }
431        if let Some(device_id) = device_id.as_ref() {
432            builder = builder.device_id(device_id);
433        }
434        builder.send().await?;
435        Ok(())
436    }
437
438    /// Login using JWT
439    /// This is an implementation of the custom_login https://docs.rs/matrix-sdk/latest/matrix_sdk/matrix_auth/struct.MatrixAuth.html#method.login_custom
440    /// For more information on logging in with JWT: https://element-hq.github.io/synapse/latest/jwt.html
441    pub async fn custom_login_with_jwt(
442        &self,
443        jwt: String,
444        initial_device_name: Option<String>,
445        device_id: Option<String>,
446    ) -> Result<(), ClientError> {
447        let data = json!({ "token": jwt }).as_object().unwrap().clone();
448
449        let mut builder = self.inner.matrix_auth().login_custom("org.matrix.login.jwt", data)?;
450
451        if let Some(initial_device_name) = initial_device_name.as_ref() {
452            builder = builder.initial_device_display_name(initial_device_name);
453        }
454
455        if let Some(device_id) = device_id.as_ref() {
456            builder = builder.device_id(device_id);
457        }
458
459        builder.send().await?;
460        Ok(())
461    }
462
463    /// Login using an email and password.
464    pub async fn login_with_email(
465        &self,
466        email: String,
467        password: String,
468        initial_device_name: Option<String>,
469        device_id: Option<String>,
470    ) -> Result<(), ClientError> {
471        let mut builder = self
472            .inner
473            .matrix_auth()
474            .login_identifier(UserIdentifier::Email { address: email }, &password);
475
476        if let Some(initial_device_name) = initial_device_name.as_ref() {
477            builder = builder.initial_device_display_name(initial_device_name);
478        }
479
480        if let Some(device_id) = device_id.as_ref() {
481            builder = builder.device_id(device_id);
482        }
483
484        builder.send().await?;
485
486        Ok(())
487    }
488
489    /// Returns a handler to start the SSO login process.
490    pub(crate) async fn start_sso_login(
491        self: &Arc<Self>,
492        redirect_url: String,
493        idp_id: Option<String>,
494    ) -> Result<Arc<SsoHandler>, SsoError> {
495        let auth = self.inner.matrix_auth();
496        let url = auth
497            .get_sso_login_url(redirect_url.as_str(), idp_id.as_deref())
498            .await
499            .map_err(|e| SsoError::Generic { message: e.to_string() })?;
500        Ok(Arc::new(SsoHandler { client: Arc::clone(self), url }))
501    }
502
503    /// Requests the URL needed for opening a web view using OIDC. Once the web
504    /// view has succeeded, call `login_with_oidc_callback` with the callback it
505    /// returns. If a failure occurs and a callback isn't available, make sure
506    /// to call `abort_oidc_auth` to inform the client of this.
507    ///
508    /// # Arguments
509    ///
510    /// * `oidc_configuration` - The configuration used to load the credentials
511    ///   of the client if it is already registered with the authorization
512    ///   server, or register the client and store its credentials if it isn't.
513    ///
514    /// * `prompt` - The desired user experience in the web UI. No value means
515    ///   that the user wishes to login into an existing account, and a value of
516    ///   `Create` means that the user wishes to register a new account.
517    ///
518    /// * `login_hint` - A generic login hint that an identity provider can use
519    ///   to pre-fill the login form. The format of this hint is not restricted
520    ///   by the spec as external providers all have their own way to handle the hint.
521    ///   However, it should be noted that when providing a user ID as a hint
522    ///   for MAS (with no upstream provider), then the format to use is defined
523    ///   by [MSC4198]: https://github.com/matrix-org/matrix-spec-proposals/pull/4198
524    ///
525    /// * `device_id` - The unique ID that will be associated with the session.
526    ///   If not set, a random one will be generated. It can be an existing
527    ///   device ID from a previous login call. Note that this should be done
528    ///   only if the client also holds the corresponding encryption keys.
529    ///
530    /// * `additional_scopes` - Additional scopes to request from the
531    ///   authorization server, e.g. "urn:matrix:client:com.example.msc9999.foo".
532    ///   The scopes for API access and the device ID according to the
533    ///   [specification](https://spec.matrix.org/v1.15/client-server-api/#allocated-scope-tokens)
534    ///   are always requested.
535    pub async fn url_for_oidc(
536        &self,
537        oidc_configuration: &OidcConfiguration,
538        prompt: Option<OidcPrompt>,
539        login_hint: Option<String>,
540        device_id: Option<String>,
541        additional_scopes: Option<Vec<String>>,
542    ) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
543        let registration_data = oidc_configuration.registration_data()?;
544        let redirect_uri = oidc_configuration.redirect_uri()?;
545
546        let device_id = device_id.map(OwnedDeviceId::from);
547
548        let additional_scopes =
549            additional_scopes.map(|scopes| scopes.into_iter().map(Scope::new).collect::<Vec<_>>());
550
551        let mut url_builder = self.inner.oauth().login(
552            redirect_uri,
553            device_id,
554            Some(registration_data),
555            additional_scopes,
556        );
557
558        if let Some(prompt) = prompt {
559            url_builder = url_builder.prompt(vec![prompt.into()]);
560        }
561        if let Some(login_hint) = login_hint {
562            url_builder = url_builder.login_hint(login_hint);
563        }
564
565        let data = url_builder.build().await?;
566
567        Ok(Arc::new(data))
568    }
569
570    /// Aborts an existing OIDC login operation that might have been cancelled,
571    /// failed etc.
572    pub async fn abort_oidc_auth(&self, authorization_data: Arc<OAuthAuthorizationData>) {
573        self.inner.oauth().abort_login(&authorization_data.state).await;
574    }
575
576    /// Completes the OIDC login process.
577    pub async fn login_with_oidc_callback(&self, callback_url: String) -> Result<(), OidcError> {
578        let url = Url::parse(&callback_url).or(Err(OidcError::CallbackUrlInvalid))?;
579
580        self.inner.oauth().finish_login(url.into()).await?;
581
582        Ok(())
583    }
584
585    /// Create a handler for requesting an existing device to grant login to
586    /// this device by way of a QR code.
587    ///
588    /// # Arguments
589    ///
590    /// * `oidc_configuration` - The data to restore or register the client with
591    ///   the server.
592    pub fn new_login_with_qr_code_handler(
593        self: Arc<Self>,
594        oidc_configuration: OidcConfiguration,
595    ) -> LoginWithQrCodeHandler {
596        LoginWithQrCodeHandler::new(self.inner.oauth(), oidc_configuration)
597    }
598
599    /// Create a handler for granting login from this device to a new device by
600    /// way of a QR code.
601    pub fn new_grant_login_with_qr_code_handler(self: Arc<Self>) -> GrantLoginWithQrCodeHandler {
602        GrantLoginWithQrCodeHandler::new(self.inner.oauth())
603    }
604
605    /// Restores the client from a `Session`.
606    ///
607    /// It reloads the entire set of rooms from the previous session.
608    ///
609    /// If you want to control the amount of rooms to reloads, check
610    /// [`Client::restore_session_with`].
611    pub async fn restore_session(&self, session: Session) -> Result<(), ClientError> {
612        self.restore_session_with(session, RoomLoadSettings::All).await
613    }
614
615    /// Restores the client from a `Session`.
616    ///
617    /// It reloads a set of rooms controlled by [`RoomLoadSettings`].
618    pub async fn restore_session_with(
619        &self,
620        session: Session,
621        room_load_settings: RoomLoadSettings,
622    ) -> Result<(), ClientError> {
623        let sliding_sync_version = session.sliding_sync_version.clone();
624        let auth_session: AuthSession = session.try_into()?;
625
626        self.inner
627            .restore_session_with(
628                auth_session,
629                room_load_settings
630                    .try_into()
631                    .map_err(|error| ClientError::from_str(error, None))?,
632            )
633            .await?;
634        self.inner.set_sliding_sync_version(sliding_sync_version.try_into()?);
635
636        Ok(())
637    }
638
639    /// Enables or disables all the room send queues at once.
640    ///
641    /// When connectivity is lost on a device, it is recommended to disable the
642    /// room sending queues.
643    ///
644    /// This can be controlled for individual rooms, using
645    /// [`Room::enable_send_queue`].
646    pub async fn enable_all_send_queues(&self, enable: bool) {
647        self.inner.send_queue().set_enabled(enable).await;
648    }
649
650    /// Enables or disables progress reporting for media uploads in the send
651    /// queue.
652    pub fn enable_send_queue_upload_progress(&self, enable: bool) {
653        self.inner.send_queue().enable_upload_progress(enable);
654    }
655
656    /// Subscribe to the global send queue update reporter, at the
657    /// client-wide level.
658    ///
659    /// The given listener will be immediately called with
660    /// `RoomSendQueueUpdate::NewLocalEvent` for each local echo existing in
661    /// the queue.
662    pub async fn subscribe_to_send_queue_updates(
663        &self,
664        listener: Box<dyn SendQueueRoomUpdateListener>,
665    ) -> Result<Arc<TaskHandle>, ClientError> {
666        let q = self.inner.send_queue();
667        let local_echoes = q.local_echoes().await?;
668        let mut subscriber = q.subscribe();
669
670        for (room_id, local_echoes) in local_echoes {
671            for local_echo in local_echoes {
672                listener.on_update(
673                    room_id.clone().into(),
674                    RoomSendQueueUpdate::NewLocalEvent {
675                        transaction_id: local_echo.transaction_id.into(),
676                    },
677                );
678            }
679        }
680
681        Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
682            loop {
683                match subscriber.recv().await {
684                    Ok(update) => {
685                        let room_id = update.room_id.to_string();
686                        match update.update.try_into() {
687                            Ok(update) => listener.on_update(room_id, update),
688                            Err(err) => error!("error when converting send queue update: {err}"),
689                        }
690                    }
691                    Err(err) => {
692                        error!("error when listening to the send queue update reporter: {err}");
693                    }
694                }
695            }
696        }))))
697    }
698
699    /// Subscribe to the global enablement status of the send queue, at the
700    /// client-wide level.
701    ///
702    /// The given listener will be immediately called with the initial value of
703    /// the enablement status.
704    pub fn subscribe_to_send_queue_status(
705        &self,
706        listener: Box<dyn SendQueueRoomErrorListener>,
707    ) -> Arc<TaskHandle> {
708        let q = self.inner.send_queue();
709        let mut subscriber = q.subscribe_errors();
710
711        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
712            // Respawn tasks for rooms that had unsent events. At this point we've just
713            // created the subscriber, so it'll be notified about errors.
714            q.respawn_tasks_for_rooms_with_unsent_requests().await;
715
716            loop {
717                match subscriber.recv().await {
718                    Ok(report) => listener
719                        .on_error(report.room_id.to_string(), ClientError::from_err(report.error)),
720                    Err(err) => {
721                        error!("error when listening to the send queue error reporter: {err}");
722                    }
723                }
724            }
725        })))
726    }
727
728    /// Subscribe to updates of global account data events.
729    ///
730    /// Be careful that only the most recent value can be observed. Subscribers
731    /// are notified when a new value is sent, but there is no guarantee that
732    /// they will see all values.
733    pub fn observe_account_data_event(
734        &self,
735        event_type: AccountDataEventType,
736        listener: Box<dyn AccountDataListener>,
737    ) -> Arc<TaskHandle> {
738        macro_rules! observe {
739            ($t:ty, $cb: expr) => {{
740                // Using an Arc here is mandatory or else the subscriber will never trigger
741                let observer =
742                    Arc::new(self.inner.observe_events::<RumaGlobalAccountDataEvent<$t>, ()>());
743
744                Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
745                    let mut subscriber = observer.subscribe();
746                    loop {
747                        if let Some(next) = subscriber.next().await {
748                            $cb(next.0);
749                        }
750                    }
751                })))
752            }};
753
754            ($t:ty) => {{
755                observe!($t, |event: RumaGlobalAccountDataEvent<$t>| {
756                    listener.on_change(event.into());
757                })
758            }};
759        }
760
761        match event_type {
762            AccountDataEventType::Direct => {
763                observe!(DirectEventContent)
764            }
765            AccountDataEventType::IdentityServer => {
766                observe!(IdentityServerEventContent)
767            }
768            AccountDataEventType::IgnoredUserList => {
769                observe!(IgnoredUserListEventContent)
770            }
771            AccountDataEventType::PushRules => {
772                observe!(PushRulesEventContent, |event: RumaGlobalAccountDataEvent<
773                    PushRulesEventContent,
774                >| {
775                    if let Ok(event) = event.try_into() {
776                        listener.on_change(event);
777                    }
778                })
779            }
780            AccountDataEventType::SecretStorageDefaultKey => {
781                observe!(SecretStorageDefaultKeyEventContent)
782            }
783            AccountDataEventType::SecretStorageKey { key_id } => {
784                observe!(SecretStorageKeyEventContent, |event: RumaGlobalAccountDataEvent<
785                    SecretStorageKeyEventContent,
786                >| {
787                    if event.content.key_id != key_id {
788                        return;
789                    }
790                    if let Ok(event) = event.try_into() {
791                        listener.on_change(event);
792                    }
793                })
794            }
795        }
796    }
797
798    /// Subscribe to updates of room account data events.
799    ///
800    /// Be careful that only the most recent value can be observed. Subscribers
801    /// are notified when a new value is sent, but there is no guarantee that
802    /// they will see all values.
803    pub fn observe_room_account_data_event(
804        &self,
805        room_id: String,
806        event_type: RoomAccountDataEventType,
807        listener: Box<dyn RoomAccountDataListener>,
808    ) -> Result<Arc<TaskHandle>, ClientError> {
809        macro_rules! observe {
810            ($t:ty, $cb: expr) => {{
811                // Using an Arc here is mandatory or else the subscriber will never trigger
812                let observer =
813                    Arc::new(self.inner.observe_room_events::<RumaRoomAccountDataEvent<$t>, ()>(
814                        &RoomId::parse(&room_id)?,
815                    ));
816
817                Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
818                    let mut subscriber = observer.subscribe();
819                    loop {
820                        if let Some(next) = subscriber.next().await {
821                            $cb(next.0);
822                        }
823                    }
824                }))))
825            }};
826
827            ($t:ty) => {{
828                observe!($t, |event: RumaRoomAccountDataEvent<$t>| {
829                    listener.on_change(event.into(), room_id.clone());
830                })
831            }};
832        }
833
834        match event_type {
835            RoomAccountDataEventType::FullyRead => {
836                observe!(FullyReadEventContent)
837            }
838            RoomAccountDataEventType::MarkedUnread => {
839                observe!(MarkedUnreadEventContent)
840            }
841            RoomAccountDataEventType::Tag => {
842                observe!(TagEventContent, |event: RumaRoomAccountDataEvent<TagEventContent>| {
843                    if let Ok(event) = event.try_into() {
844                        listener.on_change(event, room_id.clone());
845                    }
846                })
847            }
848            RoomAccountDataEventType::UnstableMarkedUnread => {
849                observe!(UnstableMarkedUnreadEventContent)
850            }
851        }
852    }
853
854    /// Register a handler for notifications generated from sync responses.
855    ///
856    /// The handler will be called during sync for each event that triggers
857    /// a notification based on the user's push rules.
858    ///
859    /// The handler receives:
860    /// - The notification with push actions and event data
861    /// - The room ID where the notification occurred
862    ///
863    /// This is useful for implementing custom notification logic, such as
864    /// displaying local notifications or updating notification badges.
865    pub async fn register_notification_handler(&self, listener: Box<dyn SyncNotificationListener>) {
866        let listener = Arc::new(listener);
867        self.inner
868            .register_notification_handler(move |notification, room, _client| {
869                let listener = listener.clone();
870                let room_id = room.room_id().to_string();
871
872                async move {
873                    // Extract information about the actions
874                    let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
875                    let has_mention = notification.actions.iter().any(|a| a.is_highlight());
876
877                    // Convert SDK actions to FFI type
878                    let actions: Vec<crate::notification_settings::Action> = notification
879                        .actions
880                        .into_iter()
881                        .filter_map(|action| action.try_into().ok())
882                        .collect();
883
884                    // Convert SDK event to FFI type
885                    let (sender, event, thread_id) = match notification.event {
886                        RawAnySyncOrStrippedTimelineEvent::Sync(raw) => match raw.deserialize() {
887                            Ok(deserialized) => {
888                                let sender = deserialized.sender().to_owned();
889                                let thread_id = match &deserialized {
890                                    AnySyncTimelineEvent::MessageLike(event) => {
891                                        match event.original_content() {
892                                            Some(AnyMessageLikeEventContent::RoomMessage(
893                                                content,
894                                            )) => match content.relates_to {
895                                                Some(Relation::Thread(thread)) => {
896                                                    Some(thread.event_id.to_string())
897                                                }
898                                                _ => None,
899                                            },
900                                            _ => None,
901                                        }
902                                    }
903                                    _ => None,
904                                };
905                                let event = NotificationEvent::Timeline {
906                                    event: Arc::new(crate::event::TimelineEvent(Box::new(
907                                        deserialized,
908                                    ))),
909                                };
910                                (sender, event, thread_id)
911                            }
912                            Err(err) => {
913                                tracing::warn!("Failed to deserialize timeline event: {err}");
914                                return;
915                            }
916                        },
917                        RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
918                            match raw.deserialize() {
919                                Ok(deserialized) => {
920                                    let sender = deserialized.sender().to_owned();
921                                    let event =
922                                        NotificationEvent::Invite { sender: sender.to_string() };
923                                    let thread_id = None;
924                                    (sender, event, thread_id)
925                                }
926                                Err(err) => {
927                                    tracing::warn!(
928                                        "Failed to deserialize stripped state event: {err}"
929                                    );
930                                    return;
931                                }
932                            }
933                        }
934                    };
935
936                    // Compile sender info
937                    let sender = room.get_member_no_sync(&sender).await.ok().flatten();
938                    let sender_info = if let Some(sender) = sender.as_ref() {
939                        NotificationSenderInfo {
940                            display_name: sender.display_name().map(|name| name.to_owned()),
941                            avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
942                            is_name_ambiguous: sender.name_ambiguous(),
943                        }
944                    } else {
945                        NotificationSenderInfo {
946                            display_name: None,
947                            avatar_url: None,
948                            is_name_ambiguous: false,
949                        }
950                    };
951
952                    // Compile room info
953                    let display_name = match room.display_name().await {
954                        Ok(name) => name.to_string(),
955                        Err(err) => {
956                            tracing::warn!("Failed to calculate the room's display name: {err}");
957                            return;
958                        }
959                    };
960                    let is_direct = match room.is_direct().await {
961                        Ok(is_direct) => is_direct,
962                        Err(err) => {
963                            tracing::warn!("Failed to determine if room is direct or not: {err}");
964                            return;
965                        }
966                    };
967                    let room_info = NotificationRoomInfo {
968                        display_name,
969                        avatar_url: room.avatar_url().map(Into::into),
970                        canonical_alias: room.canonical_alias().map(Into::into),
971                        topic: room.topic(),
972                        join_rule: room
973                            .join_rule()
974                            .map(TryInto::try_into)
975                            .transpose()
976                            .ok()
977                            .flatten(),
978                        joined_members_count: room.joined_members_count(),
979                        is_encrypted: Some(room.encryption_state().is_encrypted()),
980                        is_direct,
981                        is_space: room.is_space(),
982                    };
983
984                    listener.on_notification(
985                        NotificationItem {
986                            event,
987                            sender_info,
988                            room_info,
989                            is_noisy: Some(is_noisy),
990                            has_mention: Some(has_mention),
991                            thread_id,
992                            actions: Some(actions),
993                        },
994                        room_id,
995                    );
996                }
997            })
998            .await;
999    }
1000
1001    /// Allows generic GET requests to be made through the SDK's internal HTTP
1002    /// client. This is useful when the caller's native HTTP client wouldn't
1003    /// have the same configuration (such as certificates, proxies, etc.) This
1004    /// method returns the raw bytes of the response, so that any kind of
1005    /// resource can be fetched including images, files, etc.
1006    ///
1007    /// Note: When an HTTP error occurs, the error response can be found in the
1008    /// `ClientError::Generic`'s `details` field.
1009    pub async fn get_url(&self, url: String) -> Result<Vec<u8>, ClientError> {
1010        let response = self.inner.http_client().get(url).send().await?;
1011        if response.status().is_success() {
1012            Ok(response.bytes().await?.into())
1013        } else {
1014            Err(ClientError::Generic {
1015                msg: response.status().to_string(),
1016                details: response.text().await.ok(),
1017            })
1018        }
1019    }
1020
1021    /// Empty the server version and unstable features cache.
1022    ///
1023    /// Since the SDK caches the supported versions, it's possible to have a
1024    /// stale entry in the cache. This functions makes it possible to force
1025    /// reset it.
1026    pub async fn reset_supported_versions(&self) -> Result<(), ClientError> {
1027        Ok(self.inner.reset_supported_versions().await?)
1028    }
1029
1030    /// Empty the well-known cache.
1031    ///
1032    /// Since the SDK caches the well-known, it's possible to have a stale
1033    /// entry in the cache. This functions makes it possible to force reset
1034    /// it.
1035    pub async fn reset_well_known(&self) -> Result<(), ClientError> {
1036        Ok(self.inner.reset_well_known().await?)
1037    }
1038}
1039
1040#[matrix_sdk_ffi_macros::export]
1041impl Client {
1042    /// Retrieves a media file from the media source
1043    ///
1044    /// Not available on Wasm platforms, due to lack of accessible file system.
1045    pub async fn get_media_file(
1046        &self,
1047        media_source: Arc<MediaSource>,
1048        filename: Option<String>,
1049        mime_type: String,
1050        use_cache: bool,
1051        temp_dir: Option<String>,
1052    ) -> Result<Arc<MediaFileHandle>, ClientError> {
1053        #[cfg(not(target_family = "wasm"))]
1054        {
1055            let source = (*media_source).clone();
1056            let mime_type: mime::Mime = mime_type.parse()?;
1057
1058            let handle = self
1059                .inner
1060                .media()
1061                .get_media_file(
1062                    &MediaRequestParameters {
1063                        source: source.media_source,
1064                        format: MediaFormat::File,
1065                    },
1066                    filename,
1067                    &mime_type,
1068                    use_cache,
1069                    temp_dir,
1070                )
1071                .await?;
1072
1073            Ok(Arc::new(MediaFileHandle::new(handle)))
1074        }
1075
1076        /// MediaFileHandle uses SdkMediaFileHandle which requires an
1077        /// intermediate TempFile which is not available on wasm
1078        /// platforms due to lack of an accessible file system.
1079        #[cfg(target_family = "wasm")]
1080        Err(ClientError::Generic {
1081            msg: "get_media_file is not supported on wasm platforms".to_owned(),
1082            details: None,
1083        })
1084    }
1085
1086    pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
1087        #[cfg(not(target_family = "wasm"))]
1088        {
1089            self.inner
1090                .account()
1091                .set_display_name(Some(name.as_str()))
1092                .await
1093                .context("Unable to set display name")?;
1094        }
1095
1096        #[cfg(target_family = "wasm")]
1097        {
1098            self.inner.account().set_display_name(Some(name.as_str())).await.map_err(|e| {
1099                ClientError::Generic {
1100                    msg: "Unable to set display name".to_owned(),
1101                    details: Some(e.to_string()),
1102                }
1103            })?;
1104        }
1105
1106        Ok(())
1107    }
1108}
1109
1110#[matrix_sdk_ffi_macros::export]
1111impl Client {
1112    /// The sliding sync version.
1113    pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
1114        self.inner.sliding_sync_version().into()
1115    }
1116
1117    /// Find all sliding sync versions that are available.
1118    ///
1119    /// Be careful: This method may hit the store and will send new requests for
1120    /// each call. It can be costly to call it repeatedly.
1121    ///
1122    /// If `.well-known` or `/versions` is unreachable, it will simply move
1123    /// potential sliding sync versions aside. No error will be reported.
1124    pub async fn available_sliding_sync_versions(&self) -> Vec<SlidingSyncVersion> {
1125        self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect()
1126    }
1127
1128    /// Sets the [ClientDelegate] which will inform about authentication errors.
1129    /// Returns an error if the delegate was already set.
1130    pub fn set_delegate(
1131        self: Arc<Self>,
1132        delegate: Option<Box<dyn ClientDelegate>>,
1133    ) -> Result<Option<Arc<TaskHandle>>, ClientError> {
1134        if self.delegate.get().is_some() {
1135            return Err(ClientError::Generic {
1136                msg: "Delegate already initialized".to_owned(),
1137                details: None,
1138            });
1139        }
1140
1141        Ok(delegate.map(|delegate| {
1142            let mut session_change_receiver = self.inner.subscribe_to_session_changes();
1143            let client_clone = self.clone();
1144            let session_change_task = get_runtime_handle().spawn(async move {
1145                loop {
1146                    match session_change_receiver.recv().await {
1147                        Ok(session_change) => client_clone.process_session_change(session_change),
1148                        Err(receive_error) => {
1149                            if let RecvError::Closed = receive_error {
1150                                break;
1151                            }
1152                        }
1153                    }
1154                }
1155            });
1156
1157            self.delegate.get_or_init(|| Arc::from(delegate));
1158
1159            Arc::new(TaskHandle::new(session_change_task))
1160        }))
1161    }
1162
1163    /// Sets the [UnableToDecryptDelegate] which will inform about UTDs.
1164    /// Returns an error if the delegate was already set.
1165    pub async fn set_utd_delegate(
1166        self: Arc<Self>,
1167        utd_delegate: Box<dyn UnableToDecryptDelegate>,
1168    ) -> Result<(), ClientError> {
1169        if self.utd_hook_manager.get().is_some() {
1170            return Err(ClientError::Generic {
1171                msg: "UTD delegate already initialized".to_owned(),
1172                details: None,
1173            });
1174        }
1175
1176        // UTDs detected before this duration may be reclassified as "late decryption"
1177        // events (or discarded, if they get decrypted fast enough).
1178        const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
1179
1180        let mut utd_hook_manager = UtdHookManager::new(
1181            Arc::new(UtdHook { delegate: utd_delegate.into() }),
1182            (*self.inner).clone(),
1183        )
1184        .with_max_delay(UTD_HOOK_GRACE_PERIOD);
1185
1186        if let Err(e) = utd_hook_manager.reload_from_store().await {
1187            error!("Unable to reload UTD hook data from data store: {e}");
1188            // Carry on with the setup anyway; we shouldn't fail setup just
1189            // because the UTD hook failed to load its data.
1190        }
1191
1192        self.utd_hook_manager.get_or_init(|| Arc::new(utd_hook_manager));
1193
1194        Ok(())
1195    }
1196
1197    pub fn session(&self) -> Result<Session, ClientError> {
1198        Self::session_inner((*self.inner).clone())
1199    }
1200
1201    pub async fn account_url(
1202        &self,
1203        action: Option<AccountManagementAction>,
1204    ) -> Result<Option<String>, ClientError> {
1205        if !matches!(self.inner.auth_api(), Some(AuthApi::OAuth(..))) {
1206            return Ok(None);
1207        }
1208
1209        let mut url_builder = match self.inner.oauth().account_management_url().await {
1210            Ok(Some(url_builder)) => url_builder,
1211            Ok(None) => return Ok(None),
1212            Err(e) => {
1213                error!("Failed retrieving account management URL: {e}");
1214                return Err(e.into());
1215            }
1216        };
1217
1218        if let Some(action) = action {
1219            url_builder = url_builder.action(action.into());
1220        }
1221
1222        Ok(Some(url_builder.build().to_string()))
1223    }
1224
1225    pub fn user_id(&self) -> Result<String, ClientError> {
1226        let user_id = self.inner.user_id().context("No User ID found")?;
1227        Ok(user_id.to_string())
1228    }
1229
1230    /// The server name part of the current user ID
1231    pub fn user_id_server_name(&self) -> Result<String, ClientError> {
1232        let user_id = self.inner.user_id().context("No User ID found")?;
1233        Ok(user_id.server_name().to_string())
1234    }
1235
1236    pub async fn display_name(&self) -> Result<String, ClientError> {
1237        let display_name =
1238            self.inner.account().get_display_name().await?.context("No User ID found")?;
1239        Ok(display_name)
1240    }
1241
1242    pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
1243        let mime: Mime = mime_type.parse()?;
1244        self.inner.account().upload_avatar(&mime, data).await?;
1245        Ok(())
1246    }
1247
1248    pub async fn remove_avatar(&self) -> Result<(), ClientError> {
1249        self.inner.account().set_avatar_url(None).await?;
1250        Ok(())
1251    }
1252
1253    /// Sends a request to retrieve the avatar URL. Will fill the cache used by
1254    /// [`Self::cached_avatar_url`] on success.
1255    pub async fn avatar_url(&self) -> Result<Option<String>, ClientError> {
1256        let avatar_url = self.inner.account().get_avatar_url().await?;
1257
1258        Ok(avatar_url.map(|u| u.to_string()))
1259    }
1260
1261    /// Retrieves an avatar cached from a previous call to [`Self::avatar_url`].
1262    pub async fn cached_avatar_url(&self) -> Result<Option<String>, ClientError> {
1263        Ok(self.inner.account().get_cached_avatar_url().await?.map(Into::into))
1264    }
1265
1266    pub fn device_id(&self) -> Result<String, ClientError> {
1267        let device_id = self.inner.device_id().context("No Device ID found")?;
1268        Ok(device_id.to_string())
1269    }
1270
1271    pub async fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
1272        let response = self.inner.create_room(request.try_into()?).await?;
1273        Ok(String::from(response.room_id()))
1274    }
1275
1276    /// Get the content of the event of the given type out of the account data
1277    /// store.
1278    ///
1279    /// It will be returned as a JSON string.
1280    pub async fn account_data(&self, event_type: String) -> Result<Option<String>, ClientError> {
1281        let event = self.inner.account().account_data_raw(event_type.into()).await?;
1282        Ok(event.map(|e| e.json().get().to_owned()))
1283    }
1284
1285    /// Set the given account data content for the given event type.
1286    ///
1287    /// It should be supplied as a JSON string.
1288    pub async fn set_account_data(
1289        &self,
1290        event_type: String,
1291        content: String,
1292    ) -> Result<(), ClientError> {
1293        let raw_content = Raw::from_json_string(content)?;
1294        self.inner.account().set_account_data_raw(event_type.into(), raw_content).await?;
1295        Ok(())
1296    }
1297
1298    pub async fn upload_media(
1299        &self,
1300        mime_type: String,
1301        data: Vec<u8>,
1302        progress_watcher: Option<Box<dyn ProgressWatcher>>,
1303    ) -> Result<String, ClientError> {
1304        let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
1305        let request = self.inner.media().upload(&mime_type, data, None);
1306
1307        if let Some(progress_watcher) = progress_watcher {
1308            let mut subscriber = request.subscribe_to_send_progress();
1309            get_runtime_handle().spawn(async move {
1310                while let Some(progress) = subscriber.next().await {
1311                    progress_watcher.transmission_progress(progress.into());
1312                }
1313            });
1314        }
1315
1316        let response = request.await?;
1317
1318        Ok(String::from(response.content_uri))
1319    }
1320
1321    pub async fn get_media_content(
1322        &self,
1323        media_source: Arc<MediaSource>,
1324    ) -> Result<Vec<u8>, ClientError> {
1325        let source = (*media_source).clone().media_source;
1326
1327        debug!(?source, "requesting media file");
1328        Ok(self
1329            .inner
1330            .media()
1331            .get_media_content(&MediaRequestParameters { source, format: MediaFormat::File }, true)
1332            .await?)
1333    }
1334
1335    pub async fn get_media_thumbnail(
1336        &self,
1337        media_source: Arc<MediaSource>,
1338        width: u64,
1339        height: u64,
1340    ) -> Result<Vec<u8>, ClientError> {
1341        let source = (*media_source).clone().media_source;
1342
1343        debug!(?source, width, height, "requesting media thumbnail");
1344        Ok(self
1345            .inner
1346            .media()
1347            .get_media_content(
1348                &MediaRequestParameters {
1349                    source,
1350                    format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
1351                        UInt::new(width).unwrap(),
1352                        UInt::new(height).unwrap(),
1353                    )),
1354                },
1355                true,
1356            )
1357            .await?)
1358    }
1359
1360    pub async fn get_session_verification_controller(
1361        &self,
1362    ) -> Result<Arc<SessionVerificationController>, ClientError> {
1363        if let Some(session_verification_controller) =
1364            &*self.session_verification_controller.read().await
1365        {
1366            return Ok(Arc::new(session_verification_controller.clone()));
1367        }
1368        let user_id = self.inner.user_id().context("Failed retrieving current user_id")?;
1369        let user_identity = self
1370            .inner
1371            .encryption()
1372            .get_user_identity(user_id)
1373            .await?
1374            .context("Failed retrieving user identity")?;
1375
1376        let session_verification_controller = SessionVerificationController::new(
1377            self.inner.encryption(),
1378            user_identity,
1379            self.inner.account(),
1380        );
1381
1382        *self.session_verification_controller.write().await =
1383            Some(session_verification_controller.clone());
1384
1385        Ok(Arc::new(session_verification_controller))
1386    }
1387
1388    /// Log the current user out.
1389    pub async fn logout(&self) -> Result<(), ClientError> {
1390        Ok(self.inner.logout().await?)
1391    }
1392
1393    /// Registers a pusher with given parameters
1394    pub async fn set_pusher(
1395        &self,
1396        identifiers: PusherIdentifiers,
1397        kind: PusherKind,
1398        app_display_name: String,
1399        device_display_name: String,
1400        profile_tag: Option<String>,
1401        lang: String,
1402    ) -> Result<(), ClientError> {
1403        let ids = identifiers.into();
1404
1405        let pusher_init = PusherInit {
1406            ids,
1407            kind: kind.try_into()?,
1408            app_display_name,
1409            device_display_name,
1410            profile_tag,
1411            lang,
1412        };
1413        self.inner.pusher().set(pusher_init.into()).await?;
1414        Ok(())
1415    }
1416
1417    /// Deletes a pusher of given pusher ids
1418    pub async fn delete_pusher(&self, identifiers: PusherIdentifiers) -> Result<(), ClientError> {
1419        self.inner.pusher().delete(identifiers.into()).await?;
1420        Ok(())
1421    }
1422
1423    /// The homeserver this client is configured to use.
1424    pub fn homeserver(&self) -> String {
1425        self.inner.homeserver().to_string()
1426    }
1427
1428    /// The URL of the server.
1429    ///
1430    /// Not to be confused with the `Self::homeserver`. `server` is usually
1431    /// the server part in a user ID, e.g. with `@mnt_io:matrix.org`, here
1432    /// `matrix.org` is the server, whilst `matrix-client.matrix.org` is the
1433    /// homeserver (at the time of writing — 2024-08-28).
1434    ///
1435    /// This value is optional depending on how the `Client` has been built.
1436    /// If it's been built from a homeserver URL directly, we don't know the
1437    /// server. However, if the `Client` has been built from a server URL or
1438    /// name, then the homeserver has been discovered, and we know both.
1439    pub fn server(&self) -> Option<String> {
1440        self.inner.server().map(ToString::to_string)
1441    }
1442
1443    pub fn rooms(&self) -> Vec<Arc<Room>> {
1444        self.inner
1445            .rooms()
1446            .into_iter()
1447            .map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
1448            .collect()
1449    }
1450
1451    /// Get a room by its ID.
1452    ///
1453    /// # Arguments
1454    ///
1455    /// * `room_id` - The ID of the room to get.
1456    ///
1457    /// # Returns
1458    ///
1459    /// A `Result` containing an optional room, or a `ClientError`.
1460    /// This method will not initialize the room's timeline or populate it with
1461    /// events.
1462    pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
1463        let room_id = RoomId::parse(room_id)?;
1464        let sdk_room = self.inner.get_room(&room_id);
1465
1466        let room =
1467            sdk_room.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())));
1468        Ok(room)
1469    }
1470
1471    pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
1472        let user_id = UserId::parse(user_id)?;
1473        let sdk_room = self.inner.get_dm_room(&user_id);
1474        let dm =
1475            sdk_room.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())));
1476        Ok(dm)
1477    }
1478
1479    pub async fn search_users(
1480        &self,
1481        search_term: String,
1482        limit: u64,
1483    ) -> Result<SearchUsersResults, ClientError> {
1484        let response = self.inner.search_users(&search_term, limit).await?;
1485        Ok(SearchUsersResults::from(response))
1486    }
1487
1488    pub async fn get_profile(&self, user_id: String) -> Result<UserProfile, ClientError> {
1489        let user_id = <&UserId>::try_from(user_id.as_str())?;
1490        UserProfile::fetch(&self.inner.account(), user_id).await
1491    }
1492
1493    pub async fn notification_client(
1494        self: Arc<Self>,
1495        process_setup: NotificationProcessSetup,
1496    ) -> Result<Arc<NotificationClient>, ClientError> {
1497        Ok(Arc::new(NotificationClient {
1498            inner: MatrixNotificationClient::new((*self.inner).clone(), process_setup.into())
1499                .await?,
1500            client: self.clone(),
1501        }))
1502    }
1503
1504    pub fn sync_service(&self) -> Arc<SyncServiceBuilder> {
1505        SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
1506    }
1507
1508    pub fn space_service(&self) -> Arc<SpaceService> {
1509        let inner = UISpaceService::new((*self.inner).clone());
1510        Arc::new(SpaceService::new(inner))
1511    }
1512
1513    pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
1514        let inner = self.inner.notification_settings().await;
1515
1516        Arc::new(NotificationSettings::new((*self.inner).clone(), inner))
1517    }
1518
1519    pub fn encryption(self: Arc<Self>) -> Arc<Encryption> {
1520        Arc::new(Encryption { inner: self.inner.encryption(), _client: self.clone() })
1521    }
1522
1523    // Ignored users
1524
1525    pub async fn ignored_users(&self) -> Result<Vec<String>, ClientError> {
1526        if let Some(raw_content) =
1527            self.inner.account().fetch_account_data_static::<IgnoredUserListEventContent>().await?
1528        {
1529            let content = raw_content.deserialize()?;
1530            let user_ids: Vec<String> =
1531                content.ignored_users.keys().map(|id| id.to_string()).collect();
1532
1533            return Ok(user_ids);
1534        }
1535
1536        Ok(vec![])
1537    }
1538
1539    pub async fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
1540        let user_id = UserId::parse(user_id)?;
1541        self.inner.account().ignore_user(&user_id).await?;
1542        Ok(())
1543    }
1544
1545    pub async fn unignore_user(&self, user_id: String) -> Result<(), ClientError> {
1546        let user_id = UserId::parse(user_id)?;
1547        self.inner.account().unignore_user(&user_id).await?;
1548        Ok(())
1549    }
1550
1551    pub fn subscribe_to_ignored_users(
1552        &self,
1553        listener: Box<dyn IgnoredUsersListener>,
1554    ) -> Arc<TaskHandle> {
1555        let mut subscriber = self.inner.subscribe_to_ignore_user_list_changes();
1556        Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
1557            while let Some(user_ids) = subscriber.next().await {
1558                listener.call(user_ids);
1559            }
1560        })))
1561    }
1562
1563    pub fn room_directory_search(&self) -> Arc<RoomDirectorySearch> {
1564        Arc::new(RoomDirectorySearch::new(
1565            matrix_sdk::room_directory_search::RoomDirectorySearch::new((*self.inner).clone()),
1566        ))
1567    }
1568
1569    /// Join a room by its ID.
1570    ///
1571    /// Use this method when the homeserver already knows of the given room ID.
1572    /// Otherwise use `join_room_by_id_or_alias` so you can pass a list of
1573    /// server names for the homeserver to find the room.
1574    pub async fn join_room_by_id(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
1575        let room_id = RoomId::parse(room_id)?;
1576        let room = self.inner.join_room_by_id(room_id.as_ref()).await?;
1577        Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
1578    }
1579
1580    /// Join a room by its ID or alias.
1581    ///
1582    /// When supplying the room's ID, you can also supply a list of server names
1583    /// for the homeserver to find the room. Typically these server names
1584    /// come from a permalink's `via` parameters, or from resolving a room's
1585    /// alias into an ID.
1586    pub async fn join_room_by_id_or_alias(
1587        &self,
1588        room_id_or_alias: String,
1589        server_names: Vec<String>,
1590    ) -> Result<Arc<Room>, ClientError> {
1591        let room_id = RoomOrAliasId::parse(&room_id_or_alias)?;
1592        let server_names = server_names
1593            .iter()
1594            .map(|name| OwnedServerName::try_from(name.as_str()))
1595            .collect::<Result<Vec<_>, _>>()?;
1596        let room =
1597            self.inner.join_room_by_id_or_alias(room_id.as_ref(), server_names.as_ref()).await?;
1598        Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
1599    }
1600
1601    /// Knock on a room to join it using its ID or alias.
1602    pub async fn knock(
1603        &self,
1604        room_id_or_alias: String,
1605        reason: Option<String>,
1606        server_names: Vec<String>,
1607    ) -> Result<Arc<Room>, ClientError> {
1608        let room_id = RoomOrAliasId::parse(&room_id_or_alias)?;
1609        let server_names =
1610            server_names.iter().map(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
1611        let room = self.inner.knock(room_id, reason, server_names).await?;
1612        Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
1613    }
1614
1615    pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
1616        Ok(self
1617            .inner
1618            .account()
1619            .get_recently_visited_rooms()
1620            .await?
1621            .into_iter()
1622            .map(Into::into)
1623            .collect())
1624    }
1625
1626    pub async fn track_recently_visited_room(&self, room: String) -> Result<(), ClientError> {
1627        let room_id = RoomId::parse(room)?;
1628        self.inner.account().track_recently_visited_room(room_id).await?;
1629        Ok(())
1630    }
1631
1632    /// Resolves the given room alias to a room ID (and a list of servers), if
1633    /// possible.
1634    pub async fn resolve_room_alias(
1635        &self,
1636        room_alias: String,
1637    ) -> Result<Option<ResolvedRoomAlias>, ClientError> {
1638        let room_alias = RoomAliasId::parse(&room_alias)?;
1639        match self.inner.resolve_room_alias(&room_alias).await {
1640            Ok(response) => Ok(Some(response.into())),
1641            Err(error) => match error.client_api_error_kind() {
1642                // The room alias wasn't found, so we return None.
1643                Some(ErrorKind::NotFound) => Ok(None),
1644                // In any other case we just return the error, mapped.
1645                _ => Err(error.into()),
1646            },
1647        }
1648    }
1649
1650    /// Checks if a room alias exists in the current homeserver.
1651    pub async fn room_alias_exists(&self, room_alias: String) -> Result<bool, ClientError> {
1652        self.resolve_room_alias(room_alias).await.map(|ret| ret.is_some())
1653    }
1654
1655    /// Given a room id, get the preview of a room, to interact with it.
1656    ///
1657    /// The list of `via_servers` must be a list of servers that know
1658    /// about the room and can resolve it, and that may appear as a `via`
1659    /// parameter in e.g. a permalink URL. This list can be empty.
1660    pub async fn get_room_preview_from_room_id(
1661        &self,
1662        room_id: String,
1663        via_servers: Vec<String>,
1664    ) -> Result<Arc<RoomPreview>, ClientError> {
1665        let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;
1666
1667        let via_servers = via_servers
1668            .into_iter()
1669            .map(ServerName::parse)
1670            .collect::<Result<Vec<_>, _>>()
1671            .context("at least one `via` server name is invalid")?;
1672
1673        // The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
1674        // rustc win that one fight.
1675        let room_id: &RoomId = &room_id;
1676
1677        let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
1678
1679        Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
1680    }
1681
1682    /// Given a room alias, get the preview of a room, to interact with it.
1683    pub async fn get_room_preview_from_room_alias(
1684        &self,
1685        room_alias: String,
1686    ) -> Result<Arc<RoomPreview>, ClientError> {
1687        let room_alias =
1688            RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;
1689
1690        // The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
1691        // rustc win that one fight.
1692        let room_alias: &RoomAliasId = &room_alias;
1693
1694        let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
1695
1696        Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
1697    }
1698
1699    /// Waits until an at least partially synced room is received, and returns
1700    /// it.
1701    ///
1702    /// **Note: this function will loop endlessly until either it finds the room
1703    /// or an externally set timeout happens.**
1704    pub async fn await_room_remote_echo(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
1705        let room_id = RoomId::parse(room_id)?;
1706        Ok(Arc::new(Room::new(
1707            self.inner.await_room_remote_echo(&room_id).await,
1708            self.utd_hook_manager.get().cloned(),
1709        )))
1710    }
1711
1712    /// Lets the user know whether this is an `m.login.password` based
1713    /// auth and if the account can actually be deactivated
1714    pub fn can_deactivate_account(&self) -> bool {
1715        matches!(self.inner.auth_api(), Some(AuthApi::Matrix(_)))
1716    }
1717
1718    /// Deactivate this account definitively.
1719    /// Similarly to `encryption::reset_identity` this
1720    /// will only work with password-based authentication (`m.login.password`)
1721    ///
1722    /// # Arguments
1723    ///
1724    /// * `auth_data` - This request uses the [User-Interactive Authentication
1725    ///   API][uiaa]. The first request needs to set this to `None` and will
1726    ///   always fail and the same request needs to be made but this time with
1727    ///   some `auth_data` provided.
1728    pub async fn deactivate_account(
1729        &self,
1730        auth_data: Option<AuthData>,
1731        erase_data: bool,
1732    ) -> Result<(), ClientError> {
1733        if let Some(auth_data) = auth_data {
1734            _ = self.inner.account().deactivate(None, Some(auth_data.into()), erase_data).await?;
1735        } else {
1736            _ = self.inner.account().deactivate(None, None, erase_data).await?;
1737        }
1738
1739        Ok(())
1740    }
1741
1742    /// Checks if a room alias is not in use yet.
1743    ///
1744    /// Returns:
1745    /// - `Ok(true)` if the room alias is available.
1746    /// - `Ok(false)` if it's not (the resolve alias request returned a `404`
1747    ///   status code).
1748    /// - An `Err` otherwise.
1749    pub async fn is_room_alias_available(&self, alias: String) -> Result<bool, ClientError> {
1750        let alias = RoomAliasId::parse(alias)?;
1751        self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
1752    }
1753
1754    /// Set the media retention policy.
1755    pub async fn set_media_retention_policy(
1756        &self,
1757        policy: MediaRetentionPolicy,
1758    ) -> Result<(), ClientError> {
1759        let closure = async || -> Result<_, Error> {
1760            let store = self.inner.media_store().lock().await?;
1761            Ok(store.set_media_retention_policy(policy).await?)
1762        };
1763
1764        Ok(closure().await?)
1765    }
1766
1767    /// Clear all the non-critical caches for this Client instance.
1768    ///
1769    /// WARNING: This will clear all the caches, including the base store (state
1770    /// store), so callers must make sure that the Client is at rest before
1771    /// calling it.
1772    ///
1773    /// In particular, if a [`SyncService`] is running, it must be passed here
1774    /// as a parameter, or stopped before calling this method. Ideally, the
1775    /// send queues should have been disabled and must all be inactive (i.e.
1776    /// not sending events); this method will disable them, but it might not
1777    /// be enough if the queues are still processing events.
1778    ///
1779    /// After the method returns, the Client will be in an unstable
1780    /// state, and it is required that the caller reinstantiates a new
1781    /// Client instance, be it via dropping the previous and re-creating it,
1782    /// restarting their application, or any other similar means.
1783    ///
1784    /// - This will get rid of the backing state store file, if provided.
1785    /// - This will empty all the room's persisted event caches, so all rooms
1786    ///   will start as if they were empty.
1787    /// - This will empty the media cache according to the current media
1788    ///   retention policy.
1789    pub async fn clear_caches(
1790        &self,
1791        sync_service: Option<Arc<SyncService>>,
1792    ) -> Result<(), ClientError> {
1793        let closure = async || -> Result<_, ClientError> {
1794            // First, make sure to expire sessions in the sync service.
1795            if let Some(sync_service) = sync_service {
1796                sync_service.inner.expire_sessions().await;
1797            }
1798
1799            // Disable the send queues, as they might read and write to the state store.
1800            // Events being send might still be active, and cause errors if
1801            // processing finishes, so this will only minimize damage. Since
1802            // this method should only be called in exceptional cases, this has
1803            // been deemed acceptable.
1804            self.inner.send_queue().set_enabled(false).await;
1805
1806            // Clean up the media cache according to the current media retention policy.
1807            self.inner
1808                .media_store()
1809                .lock()
1810                .await
1811                .map_err(Error::from)?
1812                .clean()
1813                .await
1814                .map_err(Error::from)?;
1815
1816            // Clear all the room chunks. It's important to *not* call
1817            // `EventCacheStore::clear_all_linked_chunks` here, because there might be live
1818            // observers of the linked chunks, and that would cause some very bad state
1819            // mismatch.
1820            self.inner.event_cache().clear_all_rooms().await?;
1821
1822            // Delete the state store file, if it exists.
1823            #[cfg(feature = "sqlite")]
1824            if let Some(store_path) = &self.store_path {
1825                debug!("Removing the state store: {}", store_path.display());
1826
1827                // The state store and the crypto store both live in the same store path, so we
1828                // can't blindly delete the directory.
1829                //
1830                // Delete the state store SQLite file, as well as the write-ahead log (WAL) and
1831                // shared-memory (SHM) files, if they exist.
1832
1833                for file_name in [
1834                    PathBuf::from(STATE_STORE_DATABASE_NAME),
1835                    PathBuf::from(format!("{STATE_STORE_DATABASE_NAME}.wal")),
1836                    PathBuf::from(format!("{STATE_STORE_DATABASE_NAME}.shm")),
1837                ] {
1838                    let file_path = store_path.join(file_name);
1839                    if file_path.exists() {
1840                        debug!("Removing file: {}", file_path.display());
1841                        std::fs::remove_file(&file_path).map_err(|err| ClientError::Generic {
1842                            msg: format!(
1843                                "couldn't delete the state store file {}: {err}",
1844                                file_path.display()
1845                            ),
1846                            details: None,
1847                        })?;
1848                    }
1849                }
1850            }
1851
1852            Ok(())
1853        };
1854
1855        closure().await
1856    }
1857
1858    /// Checks if the server supports the report room API.
1859    pub async fn is_report_room_api_supported(&self) -> Result<bool, ClientError> {
1860        Ok(self.inner.server_versions().await?.contains(&ruma::api::MatrixVersion::V1_13))
1861    }
1862
1863    /// Checks if the server supports the LiveKit RTC focus for placing calls.
1864    pub async fn is_livekit_rtc_supported(&self) -> Result<bool, ClientError> {
1865        Ok(self
1866            .inner
1867            .rtc_foci()
1868            .await?
1869            .iter()
1870            .any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_))))
1871    }
1872
1873    /// Checks if the server supports login using a QR code.
1874    pub async fn is_login_with_qr_code_supported(&self) -> Result<bool, ClientError> {
1875        Ok(matches!(self.inner.auth_api(), Some(AuthApi::OAuth(_)))
1876            && self.inner.unstable_features().await?.contains(&ruma::api::FeatureFlag::Msc4108))
1877    }
1878
1879    /// Get server vendor information from the federation API.
1880    ///
1881    /// This method retrieves information about the server's name and version
1882    /// by calling the `/_matrix/federation/v1/version` endpoint.
1883    pub async fn server_vendor_info(&self) -> Result<matrix_sdk::ServerVendorInfo, ClientError> {
1884        Ok(self.inner.server_vendor_info(None).await?)
1885    }
1886
1887    /// Subscribe to changes in the media preview configuration.
1888    pub async fn subscribe_to_media_preview_config(
1889        &self,
1890        listener: Box<dyn MediaPreviewConfigListener>,
1891    ) -> Result<Arc<TaskHandle>, ClientError> {
1892        let (initial_value, stream) = self.inner.account().observe_media_preview_config().await?;
1893        Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
1894            // Send the initial value to the listener.
1895            listener.on_change(initial_value.map(|config| config.into()));
1896            // Listen for changes and notify the listener.
1897            pin_mut!(stream);
1898            while let Some(media_preview_config) = stream.next().await {
1899                listener.on_change(Some(media_preview_config.into()));
1900            }
1901        }))))
1902    }
1903
1904    /// Set the media previews timeline display policy
1905    pub async fn set_media_preview_display_policy(
1906        &self,
1907        policy: MediaPreviews,
1908    ) -> Result<(), ClientError> {
1909        self.inner.account().set_media_previews_display_policy(policy.into()).await?;
1910        Ok(())
1911    }
1912
1913    /// Get the media previews timeline display policy
1914    /// currently stored in the cache.
1915    pub async fn get_media_preview_display_policy(
1916        &self,
1917    ) -> Result<Option<MediaPreviews>, ClientError> {
1918        let configuration = self.inner.account().get_media_preview_config_event_content().await?;
1919        match configuration {
1920            Some(configuration) => Ok(configuration.media_previews.map(Into::into)),
1921            None => Ok(None),
1922        }
1923    }
1924
1925    /// Set the invite request avatars display policy
1926    pub async fn set_invite_avatars_display_policy(
1927        &self,
1928        policy: InviteAvatars,
1929    ) -> Result<(), ClientError> {
1930        self.inner.account().set_invite_avatars_display_policy(policy.into()).await?;
1931        Ok(())
1932    }
1933
1934    /// Get the invite request avatars display policy
1935    /// currently stored in the cache.
1936    pub async fn get_invite_avatars_display_policy(
1937        &self,
1938    ) -> Result<Option<InviteAvatars>, ClientError> {
1939        let configuration = self.inner.account().get_media_preview_config_event_content().await?;
1940        match configuration {
1941            Some(configuration) => Ok(configuration.invite_avatars.map(Into::into)),
1942            None => Ok(None),
1943        }
1944    }
1945
1946    /// Fetch the media preview configuration from the server.
1947    pub async fn fetch_media_preview_config(
1948        &self,
1949    ) -> Result<Option<MediaPreviewConfig>, ClientError> {
1950        Ok(self.inner.account().fetch_media_preview_config_event_content().await?.map(Into::into))
1951    }
1952
1953    /// Gets the `max_upload_size` value from the homeserver, which controls the
1954    /// max size a media upload request can have.
1955    pub async fn get_max_media_upload_size(&self) -> Result<u64, ClientError> {
1956        let max_upload_size = self.inner.load_or_fetch_max_upload_size().await?;
1957        Ok(max_upload_size.into())
1958    }
1959
1960    /// Subscribe to [`RoomInfo`] updates given a provided [`RoomId`].
1961    ///
1962    /// This works even for rooms we haven't received yet, so we can subscribe
1963    /// to this and wait until we receive updates from them when sync responses
1964    /// are processed.
1965    ///
1966    /// Note this method should be used sparingly since using callback
1967    /// interfaces is expensive, as well as keeping them alive for a long
1968    /// time. Usages of this method should be short-lived and dropped as
1969    /// soon as possible.
1970    pub async fn subscribe_to_room_info(
1971        &self,
1972        room_id: String,
1973        listener: Box<dyn RoomInfoListener>,
1974    ) -> Result<Arc<TaskHandle>, ClientError> {
1975        let room_id = RoomId::parse(room_id)?;
1976
1977        // Emit the initial event, if present
1978        if let Some(room) = self.inner.get_room(&room_id) {
1979            if let Ok(room_info) = RoomInfo::new(&room).await {
1980                listener.call(room_info);
1981            }
1982        }
1983
1984        Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn({
1985            let client = self.inner.clone();
1986            let mut receiver = client.room_info_notable_update_receiver();
1987            async move {
1988                while let Ok(room_update) = receiver.recv().await {
1989                    if room_update.room_id != room_id {
1990                        continue;
1991                    }
1992
1993                    if let Some(room) = client.get_room(&room_id) {
1994                        if let Ok(room_info) = RoomInfo::new(&room).await {
1995                            listener.call(room_info);
1996                        }
1997                    }
1998                }
1999            }
2000        }))))
2001    }
2002}
2003
2004#[cfg(feature = "experimental-element-recent-emojis")]
2005mod recent_emoji {
2006    use crate::{client::Client, error::ClientError};
2007
2008    /// Represents an emoji recently used for reactions.
2009    #[derive(Debug, uniffi::Record)]
2010    pub struct RecentEmoji {
2011        /// The actual emoji text representation.
2012        pub emoji: String,
2013        /// The number of times this emoji has been used for reactions.
2014        pub count: u64,
2015    }
2016
2017    #[matrix_sdk_ffi_macros::export]
2018    impl Client {
2019        /// Adds a recently used emoji to the list and uploads the updated
2020        /// `io.element.recent_emoji` content to the global account data.
2021        pub async fn add_recent_emoji(&self, emoji: String) -> Result<(), ClientError> {
2022            Ok(self.inner.account().add_recent_emoji(&emoji).await?)
2023        }
2024
2025        /// Gets the list of recently used emojis from the
2026        /// `io.element.recent_emoji` global account data.
2027        pub async fn get_recent_emojis(&self) -> Result<Vec<RecentEmoji>, ClientError> {
2028            Ok(self
2029                .inner
2030                .account()
2031                .get_recent_emojis(false)
2032                .await?
2033                .into_iter()
2034                .map(|(emoji, count)| RecentEmoji { emoji, count: count.into() })
2035                .collect::<Vec<RecentEmoji>>())
2036        }
2037    }
2038}
2039
2040#[matrix_sdk_ffi_macros::export(callback_interface)]
2041pub trait MediaPreviewConfigListener: SyncOutsideWasm + SendOutsideWasm {
2042    fn on_change(&self, media_preview_config: Option<MediaPreviewConfig>);
2043}
2044
2045#[matrix_sdk_ffi_macros::export(callback_interface)]
2046pub trait IgnoredUsersListener: SyncOutsideWasm + SendOutsideWasm {
2047    fn call(&self, ignored_user_ids: Vec<String>);
2048}
2049
2050#[derive(uniffi::Enum)]
2051pub enum NotificationProcessSetup {
2052    MultipleProcesses,
2053    SingleProcess { sync_service: Arc<SyncService> },
2054}
2055
2056impl From<NotificationProcessSetup> for MatrixNotificationProcessSetup {
2057    fn from(value: NotificationProcessSetup) -> Self {
2058        match value {
2059            NotificationProcessSetup::MultipleProcesses => {
2060                MatrixNotificationProcessSetup::MultipleProcesses
2061            }
2062            NotificationProcessSetup::SingleProcess { sync_service } => {
2063                MatrixNotificationProcessSetup::SingleProcess {
2064                    sync_service: sync_service.inner.clone(),
2065                }
2066            }
2067        }
2068    }
2069}
2070
2071/// Information about a room, that was resolved from a room alias.
2072#[derive(uniffi::Record)]
2073pub struct ResolvedRoomAlias {
2074    /// The room ID that the alias resolved to.
2075    pub room_id: String,
2076    /// A list of servers that can be used to find the room by its room ID.
2077    pub servers: Vec<String>,
2078}
2079
2080impl From<get_alias::v3::Response> for ResolvedRoomAlias {
2081    fn from(value: get_alias::v3::Response) -> Self {
2082        Self {
2083            room_id: value.room_id.to_string(),
2084            servers: value.servers.iter().map(ToString::to_string).collect(),
2085        }
2086    }
2087}
2088
2089#[derive(uniffi::Record)]
2090pub struct SearchUsersResults {
2091    pub results: Vec<UserProfile>,
2092    pub limited: bool,
2093}
2094
2095impl From<search_users::v3::Response> for SearchUsersResults {
2096    fn from(value: search_users::v3::Response) -> Self {
2097        let results: Vec<UserProfile> = value.results.iter().map(UserProfile::from).collect();
2098        SearchUsersResults { results, limited: value.limited }
2099    }
2100}
2101
2102#[derive(uniffi::Record)]
2103pub struct UserProfile {
2104    pub user_id: String,
2105    pub display_name: Option<String>,
2106    pub avatar_url: Option<String>,
2107}
2108
2109impl UserProfile {
2110    /// Fetch the profile for the given user ID, using the given [`Account`]
2111    /// API.
2112    pub(crate) async fn fetch(account: &Account, user_id: &UserId) -> Result<Self, ClientError> {
2113        let response = account.fetch_user_profile_of(user_id).await?;
2114        let display_name = response.get_static::<DisplayName>()?;
2115        let avatar_url = response.get_static::<AvatarUrl>()?.map(|url| url.to_string());
2116
2117        Ok(UserProfile { user_id: user_id.to_string(), display_name, avatar_url })
2118    }
2119}
2120
2121impl From<&search_users::v3::User> for UserProfile {
2122    fn from(value: &search_users::v3::User) -> Self {
2123        UserProfile {
2124            user_id: value.user_id.to_string(),
2125            display_name: value.display_name.clone(),
2126            avatar_url: value.avatar_url.as_ref().map(|url| url.to_string()),
2127        }
2128    }
2129}
2130
2131impl Client {
2132    fn process_session_change(&self, session_change: SessionChange) {
2133        if let Some(delegate) = self.delegate.get().cloned() {
2134            debug!("Applying session change: {session_change:?}");
2135            get_runtime_handle().spawn_blocking(move || match session_change {
2136                SessionChange::UnknownToken { soft_logout } => {
2137                    delegate.did_receive_auth_error(soft_logout);
2138                }
2139                SessionChange::TokensRefreshed => {}
2140            });
2141        } else {
2142            debug!(
2143                "No client delegate found, session change couldn't be applied: {session_change:?}"
2144            );
2145        }
2146    }
2147
2148    fn retrieve_session(
2149        session_delegate: Arc<dyn ClientSessionDelegate>,
2150        user_id: &UserId,
2151    ) -> anyhow::Result<SessionTokens> {
2152        Ok(session_delegate.retrieve_session_from_keychain(user_id.to_string())?.into_tokens())
2153    }
2154
2155    fn session_inner(client: matrix_sdk::Client) -> Result<Session, ClientError> {
2156        let auth_api = client.auth_api().context("Missing authentication API")?;
2157
2158        let homeserver_url = client.homeserver().into();
2159        let sliding_sync_version = client.sliding_sync_version();
2160
2161        Session::new(auth_api, homeserver_url, sliding_sync_version.into())
2162    }
2163
2164    fn save_session(
2165        session_delegate: Arc<dyn ClientSessionDelegate>,
2166        client: matrix_sdk::Client,
2167    ) -> anyhow::Result<()> {
2168        let session = Self::session_inner(client)?;
2169        session_delegate.save_session_in_keychain(session);
2170        Ok(())
2171    }
2172}
2173
2174/// Configure how many rooms will be restored when restoring the session with
2175/// [`Client::restore_session_with`].
2176///
2177/// Please, see the documentation of [`matrix_sdk::store::RoomLoadSettings`] to
2178/// learn more.
2179#[derive(uniffi::Enum)]
2180pub enum RoomLoadSettings {
2181    /// Load all rooms from the `StateStore` into the in-memory state store
2182    /// `BaseStateStore`.
2183    All,
2184
2185    /// Load a single room from the `StateStore` into the in-memory state
2186    /// store `BaseStateStore`.
2187    ///
2188    /// Please, be careful with this option. Read the documentation of
2189    /// [`RoomLoadSettings`].
2190    One { room_id: String },
2191}
2192
2193impl TryInto<SdkRoomLoadSettings> for RoomLoadSettings {
2194    type Error = String;
2195
2196    fn try_into(self) -> Result<SdkRoomLoadSettings, Self::Error> {
2197        Ok(match self {
2198            Self::All => SdkRoomLoadSettings::All,
2199            Self::One { room_id } => {
2200                SdkRoomLoadSettings::One(RoomId::parse(room_id).map_err(|error| error.to_string())?)
2201            }
2202        })
2203    }
2204}
2205
2206#[derive(uniffi::Record)]
2207pub struct NotificationPowerLevels {
2208    pub room: i32,
2209}
2210
2211impl From<NotificationPowerLevels> for ruma::power_levels::NotificationPowerLevels {
2212    fn from(value: NotificationPowerLevels) -> Self {
2213        let mut notification_power_levels = Self::new();
2214        notification_power_levels.room = value.room.into();
2215        notification_power_levels
2216    }
2217}
2218
2219#[derive(uniffi::Record)]
2220pub struct PowerLevels {
2221    pub users_default: Option<i32>,
2222    pub events_default: Option<i32>,
2223    pub state_default: Option<i32>,
2224    pub ban: Option<i32>,
2225    pub kick: Option<i32>,
2226    pub redact: Option<i32>,
2227    pub invite: Option<i32>,
2228    pub notifications: Option<NotificationPowerLevels>,
2229    pub users: HashMap<String, i32>,
2230    pub events: HashMap<String, i32>,
2231}
2232
2233impl From<PowerLevels> for RoomPowerLevelsEventContent {
2234    fn from(value: PowerLevels) -> Self {
2235        let mut power_levels = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
2236
2237        if let Some(users_default) = value.users_default {
2238            power_levels.users_default = users_default.into();
2239        }
2240        if let Some(state_default) = value.state_default {
2241            power_levels.state_default = state_default.into();
2242        }
2243        if let Some(events_default) = value.events_default {
2244            power_levels.events_default = events_default.into();
2245        }
2246        if let Some(ban) = value.ban {
2247            power_levels.ban = ban.into();
2248        }
2249        if let Some(kick) = value.kick {
2250            power_levels.kick = kick.into();
2251        }
2252        if let Some(redact) = value.redact {
2253            power_levels.redact = redact.into();
2254        }
2255        if let Some(invite) = value.invite {
2256            power_levels.invite = invite.into();
2257        }
2258        if let Some(notifications) = value.notifications {
2259            power_levels.notifications = notifications.into()
2260        }
2261        power_levels.users = value
2262            .users
2263            .iter()
2264            .filter_map(|(user_id, power_level)| match UserId::parse(user_id) {
2265                Ok(id) => Some((id, (*power_level).into())),
2266                Err(e) => {
2267                    error!(user_id, "Skipping invalid user ID, error: {e}");
2268                    None
2269                }
2270            })
2271            .collect();
2272
2273        power_levels.events = value
2274            .events
2275            .iter()
2276            .map(|(event_type, power_level)| {
2277                let event_type: ruma::events::TimelineEventType = event_type.as_str().into();
2278                (event_type, (*power_level).into())
2279            })
2280            .collect();
2281
2282        power_levels
2283    }
2284}
2285
2286#[derive(uniffi::Record)]
2287pub struct CreateRoomParameters {
2288    pub name: Option<String>,
2289    #[uniffi(default = None)]
2290    pub topic: Option<String>,
2291    pub is_encrypted: bool,
2292    #[uniffi(default = false)]
2293    pub is_direct: bool,
2294    pub visibility: RoomVisibility,
2295    pub preset: RoomPreset,
2296    #[uniffi(default = None)]
2297    pub invite: Option<Vec<String>>,
2298    #[uniffi(default = None)]
2299    pub avatar: Option<String>,
2300    #[uniffi(default = None)]
2301    pub power_level_content_override: Option<PowerLevels>,
2302    #[uniffi(default = None)]
2303    pub join_rule_override: Option<JoinRule>,
2304    #[uniffi(default = None)]
2305    pub history_visibility_override: Option<RoomHistoryVisibility>,
2306    #[uniffi(default = None)]
2307    pub canonical_alias: Option<String>,
2308}
2309
2310impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
2311    type Error = ClientError;
2312
2313    fn try_from(value: CreateRoomParameters) -> Result<create_room::v3::Request, Self::Error> {
2314        let mut request = create_room::v3::Request::new();
2315        request.name = value.name;
2316        request.topic = value.topic;
2317        request.is_direct = value.is_direct;
2318        request.visibility = value.visibility.into();
2319        request.preset = Some(value.preset.into());
2320        request.room_alias_name = value.canonical_alias;
2321        request.invite = match value.invite {
2322            Some(invite) => invite
2323                .iter()
2324                .filter_map(|user_id| match UserId::parse(user_id) {
2325                    Ok(id) => Some(id),
2326                    Err(e) => {
2327                        error!(user_id, "Skipping invalid user ID, error: {e}");
2328                        None
2329                    }
2330                })
2331                .collect(),
2332            None => vec![],
2333        };
2334
2335        let mut initial_state: Vec<Raw<AnyInitialStateEvent>> = vec![];
2336
2337        if value.is_encrypted {
2338            let content =
2339                RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
2340            initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
2341        }
2342
2343        if let Some(url) = value.avatar {
2344            let mut content = RoomAvatarEventContent::new();
2345            content.url = Some(url.into());
2346            initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
2347        }
2348
2349        if let Some(join_rule_override) = value.join_rule_override {
2350            let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?);
2351            initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
2352        }
2353
2354        if let Some(history_visibility_override) = value.history_visibility_override {
2355            let content =
2356                RoomHistoryVisibilityEventContent::new(history_visibility_override.try_into()?);
2357            initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
2358        }
2359
2360        request.initial_state = initial_state;
2361
2362        if let Some(power_levels) = value.power_level_content_override {
2363            match Raw::new(&power_levels.into()) {
2364                Ok(power_levels) => {
2365                    request.power_level_content_override = Some(power_levels);
2366                }
2367                Err(e) => {
2368                    return Err(ClientError::Generic {
2369                        msg: format!("Failed to serialize power levels, error: {e}"),
2370                        details: Some(format!("{e:?}")),
2371                    });
2372                }
2373            }
2374        }
2375
2376        Ok(request)
2377    }
2378}
2379
2380#[derive(uniffi::Enum)]
2381pub enum RoomVisibility {
2382    /// Indicates that the room will be shown in the published room list.
2383    Public,
2384
2385    /// Indicates that the room will not be shown in the published room list.
2386    Private,
2387
2388    /// A custom value that's not present in the spec.
2389    Custom { value: String },
2390}
2391
2392impl From<RoomVisibility> for Visibility {
2393    fn from(value: RoomVisibility) -> Self {
2394        match value {
2395            RoomVisibility::Public => Self::Public,
2396            RoomVisibility::Private => Self::Private,
2397            RoomVisibility::Custom { value } => value.as_str().into(),
2398        }
2399    }
2400}
2401
2402impl From<Visibility> for RoomVisibility {
2403    fn from(value: Visibility) -> Self {
2404        match value {
2405            Visibility::Public => Self::Public,
2406            Visibility::Private => Self::Private,
2407            _ => Self::Custom { value: value.as_str().to_owned() },
2408        }
2409    }
2410}
2411
2412#[derive(uniffi::Enum)]
2413#[allow(clippy::enum_variant_names)]
2414pub enum RoomPreset {
2415    /// `join_rules` is set to `invite` and `history_visibility` is set to
2416    /// `shared`.
2417    PrivateChat,
2418
2419    /// `join_rules` is set to `public` and `history_visibility` is set to
2420    /// `shared`.
2421    PublicChat,
2422
2423    /// Same as `PrivateChat`, but all initial invitees get the same power level
2424    /// as the creator.
2425    TrustedPrivateChat,
2426}
2427
2428impl From<RoomPreset> for create_room::v3::RoomPreset {
2429    fn from(value: RoomPreset) -> Self {
2430        match value {
2431            RoomPreset::PrivateChat => Self::PrivateChat,
2432            RoomPreset::PublicChat => Self::PublicChat,
2433            RoomPreset::TrustedPrivateChat => Self::TrustedPrivateChat,
2434        }
2435    }
2436}
2437
2438#[derive(uniffi::Record)]
2439pub struct Session {
2440    // Same fields as the Session type in matrix-sdk, just simpler types
2441    /// The access token used for this session.
2442    pub access_token: String,
2443    /// The token used for [refreshing the access token], if any.
2444    ///
2445    /// [refreshing the access token]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens
2446    pub refresh_token: Option<String>,
2447    /// The user the access token was issued for.
2448    pub user_id: String,
2449    /// The ID of the client device.
2450    pub device_id: String,
2451
2452    // FFI-only fields (for now)
2453    /// The URL for the homeserver used for this session.
2454    pub homeserver_url: String,
2455    /// Additional data for this session if OpenID Connect was used for
2456    /// authentication.
2457    pub oidc_data: Option<String>,
2458    /// The sliding sync version used for this session.
2459    pub sliding_sync_version: SlidingSyncVersion,
2460}
2461
2462impl Session {
2463    fn new(
2464        auth_api: AuthApi,
2465        homeserver_url: String,
2466        sliding_sync_version: SlidingSyncVersion,
2467    ) -> Result<Session, ClientError> {
2468        match auth_api {
2469            // Build the session from the regular Matrix Auth Session.
2470            AuthApi::Matrix(a) => {
2471                let matrix_sdk::authentication::matrix::MatrixSession {
2472                    meta: matrix_sdk::SessionMeta { user_id, device_id },
2473                    tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
2474                } = a.session().context("Missing session")?;
2475
2476                Ok(Session {
2477                    access_token,
2478                    refresh_token,
2479                    user_id: user_id.to_string(),
2480                    device_id: device_id.to_string(),
2481                    homeserver_url,
2482                    oidc_data: None,
2483                    sliding_sync_version,
2484                })
2485            }
2486            // Build the session from the OIDC UserSession.
2487            AuthApi::OAuth(api) => {
2488                let matrix_sdk::authentication::oauth::UserSession {
2489                    meta: matrix_sdk::SessionMeta { user_id, device_id },
2490                    tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
2491                } = api.user_session().context("Missing session")?;
2492                let client_id = api.client_id().context("OIDC client ID is missing.")?.clone();
2493                let oidc_data = OidcSessionData { client_id };
2494
2495                let oidc_data = serde_json::to_string(&oidc_data).ok();
2496                Ok(Session {
2497                    access_token,
2498                    refresh_token,
2499                    user_id: user_id.to_string(),
2500                    device_id: device_id.to_string(),
2501                    homeserver_url,
2502                    oidc_data,
2503                    sliding_sync_version,
2504                })
2505            }
2506            _ => Err(anyhow!("Unknown authentication API").into()),
2507        }
2508    }
2509
2510    fn into_tokens(self) -> matrix_sdk::SessionTokens {
2511        SessionTokens { access_token: self.access_token, refresh_token: self.refresh_token }
2512    }
2513}
2514
2515impl TryFrom<Session> for AuthSession {
2516    type Error = ClientError;
2517    fn try_from(value: Session) -> Result<Self, Self::Error> {
2518        let Session {
2519            access_token,
2520            refresh_token,
2521            user_id,
2522            device_id,
2523            homeserver_url: _,
2524            oidc_data,
2525            sliding_sync_version: _,
2526        } = value;
2527
2528        if let Some(oidc_data) = oidc_data {
2529            // Create an OidcSession.
2530            let oidc_data = serde_json::from_str::<OidcSessionData>(&oidc_data)?;
2531
2532            let user_session = matrix_sdk::authentication::oauth::UserSession {
2533                meta: matrix_sdk::SessionMeta {
2534                    user_id: user_id.try_into()?,
2535                    device_id: device_id.into(),
2536                },
2537                tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
2538            };
2539
2540            let session = OAuthSession { client_id: oidc_data.client_id, user: user_session };
2541
2542            Ok(AuthSession::OAuth(session.into()))
2543        } else {
2544            // Create a regular Matrix Session.
2545            let session = matrix_sdk::authentication::matrix::MatrixSession {
2546                meta: matrix_sdk::SessionMeta {
2547                    user_id: user_id.try_into()?,
2548                    device_id: device_id.into(),
2549                },
2550                tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
2551            };
2552
2553            Ok(AuthSession::Matrix(session))
2554        }
2555    }
2556}
2557
2558/// Represents a client registration against an OpenID Connect authentication
2559/// issuer.
2560#[derive(Serialize, Deserialize)]
2561pub(crate) struct OidcSessionData {
2562    client_id: ClientId,
2563}
2564
2565#[derive(uniffi::Enum)]
2566pub enum AccountManagementAction {
2567    Profile,
2568    SessionsList,
2569    SessionView { device_id: String },
2570    SessionEnd { device_id: String },
2571    AccountDeactivate,
2572    CrossSigningReset,
2573}
2574
2575impl From<AccountManagementAction> for AccountManagementActionFull {
2576    fn from(value: AccountManagementAction) -> Self {
2577        match value {
2578            AccountManagementAction::Profile => Self::Profile,
2579            AccountManagementAction::SessionsList => Self::SessionsList,
2580            AccountManagementAction::SessionView { device_id } => {
2581                Self::SessionView { device_id: device_id.into() }
2582            }
2583            AccountManagementAction::SessionEnd { device_id } => {
2584                Self::SessionEnd { device_id: device_id.into() }
2585            }
2586            AccountManagementAction::AccountDeactivate => Self::AccountDeactivate,
2587            AccountManagementAction::CrossSigningReset => Self::CrossSigningReset,
2588        }
2589    }
2590}
2591
2592#[matrix_sdk_ffi_macros::export]
2593fn gen_transaction_id() -> String {
2594    TransactionId::new().to_string()
2595}
2596
2597/// A file handle that takes ownership of a media file on disk. When the handle
2598/// is dropped, the file will be removed from the disk.
2599#[derive(uniffi::Object)]
2600pub struct MediaFileHandle {
2601    #[cfg(not(target_family = "wasm"))]
2602    inner: std::sync::RwLock<Option<SdkMediaFileHandle>>,
2603}
2604
2605impl MediaFileHandle {
2606    #[cfg(not(target_family = "wasm"))]
2607    fn new(handle: SdkMediaFileHandle) -> Self {
2608        Self { inner: std::sync::RwLock::new(Some(handle)) }
2609    }
2610}
2611
2612#[matrix_sdk_ffi_macros::export]
2613impl MediaFileHandle {
2614    /// Get the media file's path.
2615    pub fn path(&self) -> Result<String, ClientError> {
2616        #[cfg(not(target_family = "wasm"))]
2617        return Ok(self
2618            .inner
2619            .read()
2620            .unwrap()
2621            .as_ref()
2622            .context("MediaFileHandle must not be used after calling persist")?
2623            .path()
2624            .to_str()
2625            .unwrap()
2626            .to_owned());
2627        #[cfg(target_family = "wasm")]
2628        Err(ClientError::Generic {
2629            msg: "MediaFileHandle.path() is not supported on WASM targets".to_string(),
2630            details: None,
2631        })
2632    }
2633
2634    pub fn persist(&self, path: String) -> Result<bool, ClientError> {
2635        #[cfg(not(target_family = "wasm"))]
2636        {
2637            let mut guard = self.inner.write().unwrap();
2638            Ok(
2639                match guard
2640                    .take()
2641                    .context("MediaFileHandle was already persisted")?
2642                    .persist(path.as_ref())
2643                {
2644                    Ok(_) => true,
2645                    Err(e) => {
2646                        *guard = Some(e.file);
2647                        false
2648                    }
2649                },
2650            )
2651        }
2652        #[cfg(target_family = "wasm")]
2653        Err(ClientError::Generic {
2654            msg: "MediaFileHandle.persist() is not supported on WASM targets".to_string(),
2655            details: None,
2656        })
2657    }
2658}
2659
2660#[derive(Clone, uniffi::Enum)]
2661pub enum SlidingSyncVersion {
2662    None,
2663    Native,
2664}
2665
2666impl From<SdkSlidingSyncVersion> for SlidingSyncVersion {
2667    fn from(value: SdkSlidingSyncVersion) -> Self {
2668        match value {
2669            SdkSlidingSyncVersion::None => Self::None,
2670            SdkSlidingSyncVersion::Native => Self::Native,
2671        }
2672    }
2673}
2674
2675impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
2676    type Error = ClientError;
2677
2678    fn try_from(value: SlidingSyncVersion) -> Result<Self, Self::Error> {
2679        Ok(match value {
2680            SlidingSyncVersion::None => Self::None,
2681            SlidingSyncVersion::Native => Self::Native,
2682        })
2683    }
2684}
2685
2686#[derive(Clone, uniffi::Enum)]
2687pub enum OidcPrompt {
2688    /// The Authorization Server should prompt the End-User to create a user
2689    /// account.
2690    ///
2691    /// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
2692    Create,
2693
2694    /// The Authorization Server should prompt the End-User for
2695    /// reauthentication.
2696    Login,
2697
2698    /// The Authorization Server should prompt the End-User for consent before
2699    /// returning information to the Client.
2700    Consent,
2701
2702    /// An unknown value.
2703    Unknown { value: String },
2704}
2705
2706impl From<RumaOidcPrompt> for OidcPrompt {
2707    fn from(value: RumaOidcPrompt) -> Self {
2708        match value {
2709            RumaOidcPrompt::Create => Self::Create,
2710            value => match value.as_str() {
2711                "consent" => Self::Consent,
2712                "login" => Self::Login,
2713                _ => Self::Unknown { value: value.to_string() },
2714            },
2715        }
2716    }
2717}
2718
2719impl From<OidcPrompt> for RumaOidcPrompt {
2720    fn from(value: OidcPrompt) -> Self {
2721        match value {
2722            OidcPrompt::Create => Self::Create,
2723            OidcPrompt::Consent => Self::from("consent"),
2724            OidcPrompt::Login => Self::from("login"),
2725            OidcPrompt::Unknown { value } => value.into(),
2726        }
2727    }
2728}
2729
2730/// The rule used for users wishing to join this room.
2731#[derive(Debug, Clone, uniffi::Enum)]
2732pub enum JoinRule {
2733    /// Anyone can join the room without any prior action.
2734    Public,
2735
2736    /// A user who wishes to join the room must first receive an invite to the
2737    /// room from someone already inside of the room.
2738    Invite,
2739
2740    /// Users can join the room if they are invited, or they can request an
2741    /// invite to the room.
2742    ///
2743    /// They can be allowed (invited) or denied (kicked/banned) access.
2744    Knock,
2745
2746    /// Reserved but not yet implemented by the Matrix specification.
2747    Private,
2748
2749    /// Users can join the room if they are invited, or if they meet any of the
2750    /// conditions described in a set of [`AllowRule`]s.
2751    Restricted { rules: Vec<AllowRule> },
2752
2753    /// Users can join the room if they are invited, or if they meet any of the
2754    /// conditions described in a set of [`AllowRule`]s, or they can request
2755    /// an invite to the room.
2756    KnockRestricted { rules: Vec<AllowRule> },
2757
2758    /// A custom join rule, up for interpretation by the consumer.
2759    Custom {
2760        /// The string representation for this custom rule.
2761        repr: String,
2762    },
2763}
2764
2765/// An allow rule which defines a condition that allows joining a room.
2766#[derive(Debug, Clone, uniffi::Enum)]
2767pub enum AllowRule {
2768    /// Only a member of the `room_id` Room can join the one this rule is used
2769    /// in.
2770    RoomMembership { room_id: String },
2771
2772    /// A custom allow rule implementation, containing its JSON representation
2773    /// as a `String`.
2774    Custom { json: String },
2775}
2776
2777impl TryFrom<JoinRule> for RumaJoinRule {
2778    type Error = ClientError;
2779
2780    fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
2781        match value {
2782            JoinRule::Public => Ok(Self::Public),
2783            JoinRule::Invite => Ok(Self::Invite),
2784            JoinRule::Knock => Ok(Self::Knock),
2785            JoinRule::Private => Ok(Self::Private),
2786            JoinRule::Restricted { rules } => {
2787                let rules = ruma_allow_rules_from_ffi(rules)?;
2788                Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
2789            }
2790            JoinRule::KnockRestricted { rules } => {
2791                let rules = ruma_allow_rules_from_ffi(rules)?;
2792                Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
2793            }
2794            JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
2795        }
2796    }
2797}
2798
2799fn ruma_allow_rules_from_ffi(value: Vec<AllowRule>) -> Result<Vec<RumaAllowRule>, ClientError> {
2800    let mut ret = Vec::with_capacity(value.len());
2801    for rule in value {
2802        let rule: Result<RumaAllowRule, ClientError> = rule.try_into();
2803        match rule {
2804            Ok(rule) => ret.push(rule),
2805            Err(error) => return Err(error),
2806        }
2807    }
2808    Ok(ret)
2809}
2810
2811impl TryFrom<AllowRule> for RumaAllowRule {
2812    type Error = ClientError;
2813
2814    fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
2815        match value {
2816            AllowRule::RoomMembership { room_id } => {
2817                let room_id = RoomId::parse(room_id)?;
2818                Ok(Self::RoomMembership(ruma::room::RoomMembership::new(room_id)))
2819            }
2820            AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
2821        }
2822    }
2823}
2824
2825impl TryFrom<RumaJoinRule> for JoinRule {
2826    type Error = String;
2827    fn try_from(value: RumaJoinRule) -> Result<Self, Self::Error> {
2828        match value {
2829            RumaJoinRule::Knock => Ok(JoinRule::Knock),
2830            RumaJoinRule::Public => Ok(JoinRule::Public),
2831            RumaJoinRule::Private => Ok(JoinRule::Private),
2832            RumaJoinRule::KnockRestricted(restricted) => {
2833                let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
2834                    Vec<_>,
2835                    Self::Error,
2836                >>(
2837                )?;
2838                Ok(JoinRule::KnockRestricted { rules })
2839            }
2840            RumaJoinRule::Restricted(restricted) => {
2841                let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
2842                    Vec<_>,
2843                    Self::Error,
2844                >>(
2845                )?;
2846                Ok(JoinRule::Restricted { rules })
2847            }
2848            RumaJoinRule::Invite => Ok(JoinRule::Invite),
2849            RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
2850            _ => Err(format!("Unknown JoinRule: {value:?}")),
2851        }
2852    }
2853}
2854
2855impl TryFrom<RumaAllowRule> for AllowRule {
2856    type Error = String;
2857    fn try_from(value: RumaAllowRule) -> Result<Self, Self::Error> {
2858        match value {
2859            RumaAllowRule::RoomMembership(membership) => {
2860                Ok(AllowRule::RoomMembership { room_id: membership.room_id.to_string() })
2861            }
2862            RumaAllowRule::_Custom(repr) => {
2863                let json = serde_json::to_string(&repr)
2864                    .map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
2865                Ok(Self::Custom { json })
2866            }
2867            _ => Err(format!("Invalid AllowRule: {value:?}")),
2868        }
2869    }
2870}
2871
2872/// Contains the disk size of the different stores, if known. It won't be
2873/// available for in-memory stores.
2874#[derive(Debug, Clone, uniffi::Record)]
2875pub struct StoreSizes {
2876    /// The size of the CryptoStore.
2877    crypto_store: Option<u64>,
2878    /// The size of the StateStore.
2879    state_store: Option<u64>,
2880    /// The size of the EventCacheStore.
2881    event_cache_store: Option<u64>,
2882    /// The size of the MediaStore.
2883    media_store: Option<u64>,
2884}
2885
2886impl From<matrix_sdk::StoreSizes> for StoreSizes {
2887    fn from(value: matrix_sdk::StoreSizes) -> Self {
2888        Self {
2889            crypto_store: value.crypto_store.map(|v| v as u64),
2890            state_store: value.state_store.map(|v| v as u64),
2891            event_cache_store: value.event_cache_store.map(|v| v as u64),
2892            media_store: value.media_store.map(|v| v as u64),
2893        }
2894    }
2895}