matrix_sdk_ffi/
client_builder.rs

1// Allow UniFFI to use methods marked as `#[deprecated]`.
2#![allow(deprecated)]
3
4use std::{num::NonZeroUsize, sync::Arc, time::Duration};
5
6#[cfg(not(target_family = "wasm"))]
7use matrix_sdk::reqwest::Certificate;
8use matrix_sdk::{
9    encryption::{BackupDownloadStrategy, EncryptionSettings},
10    event_cache::EventCacheError,
11    ruma::{ServerName, UserId},
12    sliding_sync::{
13        Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
14        VersionBuilderError,
15    },
16    Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
17    RumaApiError, ThreadingSupport,
18};
19use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
20use ruma::api::error::{DeserializationError, FromHttpResponseError};
21use tracing::debug;
22
23use super::client::Client;
24#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
25use crate::store;
26use crate::{
27    client::ClientSessionDelegate,
28    error::ClientError,
29    helpers::unwrap_or_clone_arc,
30    store::{StoreBuilder, StoreBuilderOutcome},
31};
32
33/// A list of bytes containing a certificate in DER or PEM form.
34pub type CertificateBytes = Vec<u8>;
35
36#[derive(Debug, Clone)]
37enum HomeserverConfig {
38    Url(String),
39    ServerName(String),
40    ServerNameOrUrl(String),
41}
42
43#[derive(Debug, thiserror::Error, uniffi::Error)]
44#[uniffi(flat_error)]
45pub enum ClientBuildError {
46    #[error("The supplied server name is invalid.")]
47    InvalidServerName,
48    #[error(transparent)]
49    ServerUnreachable(HttpError),
50    #[error(transparent)]
51    WellKnownLookupFailed(RumaApiError),
52    #[error(transparent)]
53    WellKnownDeserializationError(DeserializationError),
54    #[error(transparent)]
55    #[allow(dead_code)] // rustc's drunk, this is used
56    SlidingSync(MatrixSlidingSyncError),
57    #[error(transparent)]
58    SlidingSyncVersion(VersionBuilderError),
59    #[error(transparent)]
60    Sdk(MatrixClientBuildError),
61    #[error(transparent)]
62    EventCache(#[from] EventCacheError),
63    #[error("Failed to build the client: {message}")]
64    Generic { message: String },
65}
66
67impl From<MatrixClientBuildError> for ClientBuildError {
68    fn from(e: MatrixClientBuildError) -> Self {
69        match e {
70            MatrixClientBuildError::InvalidServerName => ClientBuildError::InvalidServerName,
71            MatrixClientBuildError::Http(e) => ClientBuildError::ServerUnreachable(e),
72            MatrixClientBuildError::AutoDiscovery(FromHttpResponseError::Server(e)) => {
73                ClientBuildError::WellKnownLookupFailed(e)
74            }
75            MatrixClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(e)) => {
76                ClientBuildError::WellKnownDeserializationError(e)
77            }
78            MatrixClientBuildError::SlidingSyncVersion(e) => {
79                ClientBuildError::SlidingSyncVersion(e)
80            }
81            _ => ClientBuildError::Sdk(e),
82        }
83    }
84}
85
86impl From<IdParseError> for ClientBuildError {
87    fn from(e: IdParseError) -> ClientBuildError {
88        ClientBuildError::Generic { message: format!("{e:#}") }
89    }
90}
91
92impl From<std::io::Error> for ClientBuildError {
93    fn from(e: std::io::Error) -> ClientBuildError {
94        ClientBuildError::Generic { message: format!("{e:#}") }
95    }
96}
97
98impl From<url::ParseError> for ClientBuildError {
99    fn from(e: url::ParseError) -> ClientBuildError {
100        ClientBuildError::Generic { message: format!("{e:#}") }
101    }
102}
103
104impl From<ClientError> for ClientBuildError {
105    fn from(e: ClientError) -> ClientBuildError {
106        ClientBuildError::Generic { message: format!("{e:#}") }
107    }
108}
109
110#[derive(Clone, uniffi::Object)]
111pub struct ClientBuilder {
112    store: Option<StoreBuilder>,
113    system_is_memory_constrained: bool,
114    username: Option<String>,
115    homeserver_cfg: Option<HomeserverConfig>,
116    sliding_sync_version_builder: SlidingSyncVersionBuilder,
117    disable_automatic_token_refresh: bool,
118    cross_process_store_locks_holder_name: Option<String>,
119    enable_oidc_refresh_lock: bool,
120    session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
121    encryption_settings: EncryptionSettings,
122    room_key_recipient_strategy: CollectStrategy,
123    decryption_settings: DecryptionSettings,
124    enable_share_history_on_invite: bool,
125    request_config: Option<RequestConfig>,
126
127    #[cfg(not(target_family = "wasm"))]
128    user_agent: Option<String>,
129    #[cfg(not(target_family = "wasm"))]
130    proxy: Option<String>,
131    #[cfg(not(target_family = "wasm"))]
132    disable_ssl_verification: bool,
133    #[cfg(not(target_family = "wasm"))]
134    disable_built_in_root_certificates: bool,
135    #[cfg(not(target_family = "wasm"))]
136    additional_root_certificates: Vec<Vec<u8>>,
137
138    threading_support: ThreadingSupport,
139}
140
141/// The timeout applies to each read operation, and resets after a successful
142/// read. This is more appropriate for detecting stalled connections when the
143/// size isn’t known beforehand.
144const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
145
146#[matrix_sdk_ffi_macros::export]
147impl ClientBuilder {
148    #[uniffi::constructor]
149    pub fn new() -> Arc<Self> {
150        Arc::new(Self {
151            store: None,
152            system_is_memory_constrained: false,
153            username: None,
154            homeserver_cfg: None,
155            #[cfg(not(target_family = "wasm"))]
156            user_agent: None,
157            sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
158            #[cfg(not(target_family = "wasm"))]
159            proxy: None,
160            #[cfg(not(target_family = "wasm"))]
161            disable_ssl_verification: false,
162            disable_automatic_token_refresh: false,
163            cross_process_store_locks_holder_name: None,
164            enable_oidc_refresh_lock: false,
165            session_delegate: None,
166            #[cfg(not(target_family = "wasm"))]
167            additional_root_certificates: Default::default(),
168            #[cfg(not(target_family = "wasm"))]
169            disable_built_in_root_certificates: false,
170            encryption_settings: EncryptionSettings {
171                auto_enable_cross_signing: false,
172                backup_download_strategy:
173                    matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure,
174                auto_enable_backups: false,
175            },
176            room_key_recipient_strategy: Default::default(),
177            decryption_settings: DecryptionSettings {
178                sender_device_trust_requirement: TrustRequirement::Untrusted,
179            },
180            enable_share_history_on_invite: false,
181            request_config: Default::default(),
182            threading_support: ThreadingSupport::Disabled,
183        })
184    }
185
186    pub fn cross_process_store_locks_holder_name(
187        self: Arc<Self>,
188        holder_name: String,
189    ) -> Arc<Self> {
190        let mut builder = unwrap_or_clone_arc(self);
191        builder.cross_process_store_locks_holder_name = Some(holder_name);
192        Arc::new(builder)
193    }
194
195    pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
196        let mut builder = unwrap_or_clone_arc(self);
197        builder.enable_oidc_refresh_lock = true;
198        Arc::new(builder)
199    }
200
201    pub fn set_session_delegate(
202        self: Arc<Self>,
203        session_delegate: Box<dyn ClientSessionDelegate>,
204    ) -> Arc<Self> {
205        let mut builder = unwrap_or_clone_arc(self);
206        builder.session_delegate = Some(session_delegate.into());
207        Arc::new(builder)
208    }
209
210    /// Tell the client that the system is memory constrained, like in a push
211    /// notification process for example.
212    ///
213    /// So far, at the time of writing (2025-04-07), it changes the defaults of
214    /// `matrix_sdk::SqliteStoreConfig` (if the `sqlite` feature is enabled).
215    /// Please check
216    /// `matrix_sdk::SqliteStoreConfig::with_low_memory_config`.
217    pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
218        let mut builder = unwrap_or_clone_arc(self);
219        builder.system_is_memory_constrained = true;
220        Arc::new(builder)
221    }
222
223    pub fn username(self: Arc<Self>, username: String) -> Arc<Self> {
224        let mut builder = unwrap_or_clone_arc(self);
225        builder.username = Some(username);
226        Arc::new(builder)
227    }
228
229    pub fn server_name(self: Arc<Self>, server_name: String) -> Arc<Self> {
230        let mut builder = unwrap_or_clone_arc(self);
231        builder.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name));
232        Arc::new(builder)
233    }
234
235    pub fn homeserver_url(self: Arc<Self>, url: String) -> Arc<Self> {
236        let mut builder = unwrap_or_clone_arc(self);
237        builder.homeserver_cfg = Some(HomeserverConfig::Url(url));
238        Arc::new(builder)
239    }
240
241    pub fn server_name_or_homeserver_url(self: Arc<Self>, server_name_or_url: String) -> Arc<Self> {
242        let mut builder = unwrap_or_clone_arc(self);
243        builder.homeserver_cfg = Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url));
244        Arc::new(builder)
245    }
246
247    pub fn sliding_sync_version_builder(
248        self: Arc<Self>,
249        version_builder: SlidingSyncVersionBuilder,
250    ) -> Arc<Self> {
251        let mut builder = unwrap_or_clone_arc(self);
252        builder.sliding_sync_version_builder = version_builder;
253        Arc::new(builder)
254    }
255
256    pub fn disable_automatic_token_refresh(self: Arc<Self>) -> Arc<Self> {
257        let mut builder = unwrap_or_clone_arc(self);
258        builder.disable_automatic_token_refresh = true;
259        Arc::new(builder)
260    }
261
262    pub fn auto_enable_cross_signing(
263        self: Arc<Self>,
264        auto_enable_cross_signing: bool,
265    ) -> Arc<Self> {
266        let mut builder = unwrap_or_clone_arc(self);
267        builder.encryption_settings.auto_enable_cross_signing = auto_enable_cross_signing;
268        Arc::new(builder)
269    }
270
271    /// Select a strategy to download room keys from the backup. By default
272    /// we download after a decryption failure.
273    ///
274    /// Take a look at the [`BackupDownloadStrategy`] enum for more options.
275    pub fn backup_download_strategy(
276        self: Arc<Self>,
277        backup_download_strategy: BackupDownloadStrategy,
278    ) -> Arc<Self> {
279        let mut builder = unwrap_or_clone_arc(self);
280        builder.encryption_settings.backup_download_strategy = backup_download_strategy;
281        Arc::new(builder)
282    }
283
284    /// Automatically create a backup version if no backup exists.
285    pub fn auto_enable_backups(self: Arc<Self>, auto_enable_backups: bool) -> Arc<Self> {
286        let mut builder = unwrap_or_clone_arc(self);
287        builder.encryption_settings.auto_enable_backups = auto_enable_backups;
288        Arc::new(builder)
289    }
290
291    /// Set the strategy to be used for picking recipient devices when sending
292    /// an encrypted message.
293    pub fn room_key_recipient_strategy(self: Arc<Self>, strategy: CollectStrategy) -> Arc<Self> {
294        let mut builder = unwrap_or_clone_arc(self);
295        builder.room_key_recipient_strategy = strategy;
296        Arc::new(builder)
297    }
298
299    /// Set the trust requirement to be used when decrypting events.
300    pub fn decryption_settings(
301        self: Arc<Self>,
302        decryption_settings: DecryptionSettings,
303    ) -> Arc<Self> {
304        let mut builder = unwrap_or_clone_arc(self);
305        builder.decryption_settings = decryption_settings;
306        Arc::new(builder)
307    }
308
309    /// Set whether to enable the experimental support for sending and receiving
310    /// encrypted room history on invite, per [MSC4268].
311    ///
312    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
313    pub fn enable_share_history_on_invite(
314        self: Arc<Self>,
315        enable_share_history_on_invite: bool,
316    ) -> Arc<Self> {
317        let mut builder = unwrap_or_clone_arc(self);
318        builder.enable_share_history_on_invite = enable_share_history_on_invite;
319        Arc::new(builder)
320    }
321
322    /// Add a default request config to this client.
323    pub fn request_config(self: Arc<Self>, config: RequestConfig) -> Arc<Self> {
324        let mut builder = unwrap_or_clone_arc(self);
325        builder.request_config = Some(config);
326        Arc::new(builder)
327    }
328
329    /// Whether the client should support threads client-side or not, and enable
330    /// experimental support for MSC4306 (threads subscriptions) or not.
331    pub fn threads_enabled(
332        self: Arc<Self>,
333        enabled: bool,
334        thread_subscriptions: bool,
335    ) -> Arc<Self> {
336        let mut builder = unwrap_or_clone_arc(self);
337        let support = if enabled {
338            ThreadingSupport::Enabled { with_subscriptions: thread_subscriptions }
339        } else {
340            ThreadingSupport::Disabled
341        };
342        builder.threading_support = support;
343        Arc::new(builder)
344    }
345
346    /// Use in-memory session storage.
347    pub fn in_memory_store(self: Arc<Self>) -> Arc<Self> {
348        let mut builder = unwrap_or_clone_arc(self);
349        builder.store = Some(StoreBuilder::InMemory);
350        Arc::new(builder)
351    }
352
353    pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
354        let builder = unwrap_or_clone_arc(self);
355        let mut inner_builder = MatrixClient::builder();
356
357        if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
358            inner_builder =
359                inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
360        }
361
362        let store_path = if let Some(store) = &builder.store {
363            match store.build()? {
364                #[cfg(feature = "sqlite")]
365                StoreBuilderOutcome::Sqlite { config, cache_path, store_path: data_path } => {
366                    inner_builder = inner_builder
367                        .sqlite_store_with_config_and_cache_path(config, Some(cache_path));
368
369                    Some(data_path)
370                }
371                #[cfg(feature = "indexeddb")]
372                StoreBuilderOutcome::IndexedDb { name, passphrase } => {
373                    inner_builder = inner_builder.indexeddb_store(&name, passphrase.as_deref());
374
375                    None
376                }
377
378                StoreBuilderOutcome::InMemory => None,
379            }
380        } else {
381            debug!("Not using a session store");
382            None
383        };
384
385        // Determine server either from URL, server name or user ID.
386        inner_builder = match builder.homeserver_cfg {
387            Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
388            Some(HomeserverConfig::ServerName(server_name)) => {
389                let server_name = ServerName::parse(server_name)?;
390                inner_builder.server_name(&server_name)
391            }
392            Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url)) => {
393                inner_builder.server_name_or_homeserver_url(server_name_or_url)
394            }
395            None => {
396                if let Some(username) = builder.username {
397                    let user = UserId::parse(username)?;
398                    inner_builder.server_name(user.server_name())
399                } else {
400                    return Err(ClientBuildError::Generic {
401                        message: "Failed to build: One of homeserver_url, server_name, server_name_or_homeserver_url or username must be called.".to_owned(),
402                    });
403                }
404            }
405        };
406
407        #[cfg(not(target_family = "wasm"))]
408        {
409            let mut certificates = Vec::new();
410
411            for certificate in builder.additional_root_certificates {
412                // We don't really know what type of certificate we may get here, so let's try
413                // first one type, then the other.
414                match Certificate::from_der(&certificate) {
415                    Ok(cert) => {
416                        certificates.push(cert);
417                    }
418                    Err(der_error) => {
419                        let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
420                            ClientBuildError::Generic {
421                                message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
422                            }
423                        })?;
424                        certificates.push(cert);
425                    }
426                }
427            }
428
429            inner_builder = inner_builder.add_root_certificates(certificates);
430
431            if builder.disable_built_in_root_certificates {
432                inner_builder = inner_builder.disable_built_in_root_certificates();
433            }
434
435            if let Some(proxy) = builder.proxy {
436                inner_builder = inner_builder.proxy(proxy);
437            }
438
439            if builder.disable_ssl_verification {
440                inner_builder = inner_builder.disable_ssl_verification();
441            }
442
443            if let Some(user_agent) = builder.user_agent {
444                inner_builder = inner_builder.user_agent(user_agent);
445            }
446        }
447
448        if !builder.disable_automatic_token_refresh {
449            inner_builder = inner_builder.handle_refresh_tokens();
450        }
451
452        inner_builder = inner_builder
453            .with_encryption_settings(builder.encryption_settings)
454            .with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
455            .with_decryption_settings(builder.decryption_settings)
456            .with_enable_share_history_on_invite(builder.enable_share_history_on_invite);
457
458        match builder.sliding_sync_version_builder {
459            SlidingSyncVersionBuilder::None => {
460                inner_builder = inner_builder
461                    .sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::None)
462            }
463            SlidingSyncVersionBuilder::Native => {
464                inner_builder = inner_builder
465                    .sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::Native)
466            }
467            SlidingSyncVersionBuilder::DiscoverNative => {
468                inner_builder = inner_builder
469                    .sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverNative)
470            }
471        }
472
473        if let Some(config) = builder.request_config {
474            let mut updated_config = matrix_sdk::config::RequestConfig::default();
475            if let Some(retry_limit) = config.retry_limit {
476                updated_config =
477                    updated_config.retry_limit(retry_limit.try_into().unwrap_or(usize::MAX));
478            }
479            if let Some(timeout) = config.timeout {
480                updated_config = updated_config.timeout(Duration::from_millis(timeout));
481            }
482            updated_config = updated_config.read_timeout(DEFAULT_READ_TIMEOUT);
483            if let Some(max_concurrent_requests) = config.max_concurrent_requests {
484                if max_concurrent_requests > 0 {
485                    updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
486                        max_concurrent_requests as usize,
487                    ));
488                }
489            }
490            if let Some(max_retry_time) = config.max_retry_time {
491                updated_config =
492                    updated_config.max_retry_time(Duration::from_millis(max_retry_time));
493            }
494            inner_builder = inner_builder.request_config(updated_config);
495        }
496
497        inner_builder = inner_builder.with_threading_support(builder.threading_support);
498
499        let sdk_client = inner_builder.build().await?;
500
501        Ok(Arc::new(
502            Client::new(
503                sdk_client,
504                builder.enable_oidc_refresh_lock,
505                builder.session_delegate,
506                store_path,
507            )
508            .await?,
509        ))
510    }
511}
512
513#[cfg(feature = "sqlite")]
514#[matrix_sdk_ffi_macros::export]
515impl ClientBuilder {
516    /// Use SQLite as the session storage.
517    pub fn sqlite_store(self: Arc<Self>, config: Arc<store::SqliteStoreBuilder>) -> Arc<Self> {
518        let mut builder = unwrap_or_clone_arc(self);
519        builder.store = Some(StoreBuilder::Sqlite(unwrap_or_clone_arc(config)));
520        Arc::new(builder)
521    }
522
523    /// Sets the paths that the client will use to store its data and caches
524    /// with SQLite.
525    ///
526    /// Both paths **must** be unique per session as the SDK
527    /// stores aren't capable of handling multiple users, however it is
528    /// valid to use the same path for both stores on a single session.
529    #[deprecated = "Use `ClientBuilder::session_store_with_sqlite` instead"]
530    pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
531        let mut builder = unwrap_or_clone_arc(self);
532        builder.store =
533            Some(StoreBuilder::Sqlite(store::SqliteStoreBuilder::raw_new(data_path, cache_path)));
534        Arc::new(builder)
535    }
536}
537
538#[cfg(feature = "indexeddb")]
539#[matrix_sdk_ffi_macros::export]
540impl ClientBuilder {
541    /// Use IndexedDB as the session storage.
542    pub fn indexeddb_store(
543        self: Arc<Self>,
544        config: Arc<store::IndexedDbStoreBuilder>,
545    ) -> Arc<Self> {
546        let mut builder = unwrap_or_clone_arc(self);
547        builder.store = Some(StoreBuilder::IndexedDb(unwrap_or_clone_arc(config)));
548        Arc::new(builder)
549    }
550}
551
552#[matrix_sdk_ffi_macros::export]
553impl ClientBuilder {
554    pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
555        let mut builder = unwrap_or_clone_arc(self);
556        #[cfg(not(target_family = "wasm"))]
557        {
558            builder.proxy = Some(url);
559        }
560        Arc::new(builder)
561    }
562
563    pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
564        let mut builder = unwrap_or_clone_arc(self);
565        #[cfg(not(target_family = "wasm"))]
566        {
567            builder.disable_ssl_verification = true;
568        }
569        Arc::new(builder)
570    }
571
572    pub fn add_root_certificates(
573        self: Arc<Self>,
574        certificates: Vec<CertificateBytes>,
575    ) -> Arc<Self> {
576        let mut builder = unwrap_or_clone_arc(self);
577
578        #[cfg(not(target_family = "wasm"))]
579        {
580            builder.additional_root_certificates = certificates;
581        }
582
583        Arc::new(builder)
584    }
585
586    /// Don't trust any system root certificates, only trust the certificates
587    /// provided through
588    /// [`add_root_certificates`][ClientBuilder::add_root_certificates].
589    pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
590        let mut builder = unwrap_or_clone_arc(self);
591        #[cfg(not(target_family = "wasm"))]
592        {
593            builder.disable_built_in_root_certificates = true;
594        }
595        Arc::new(builder)
596    }
597
598    pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
599        let mut builder = unwrap_or_clone_arc(self);
600        #[cfg(not(target_family = "wasm"))]
601        {
602            builder.user_agent = Some(user_agent);
603        }
604        Arc::new(builder)
605    }
606}
607
608/// The config to use for HTTP requests by default in this client.
609#[derive(Clone, uniffi::Record)]
610pub struct RequestConfig {
611    /// Max number of retries.
612    retry_limit: Option<u64>,
613    /// Timeout for a request in milliseconds.
614    timeout: Option<u64>,
615    /// Max number of concurrent requests. No value means no limits.
616    max_concurrent_requests: Option<u64>,
617    /// Base delay between retries.
618    max_retry_time: Option<u64>,
619}
620
621#[derive(Clone, uniffi::Enum)]
622pub enum SlidingSyncVersionBuilder {
623    None,
624    Native,
625    DiscoverNative,
626}