matrix_sdk/client/builder/
mod.rs

1// Copyright 2022 The Matrix.org Foundation C.I.C.
2// Copyright 2022 Kévin Commaille
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16mod homeserver_config;
17
18#[cfg(feature = "sqlite")]
19use std::path::Path;
20use std::{collections::BTreeSet, fmt, sync::Arc};
21
22use homeserver_config::*;
23#[cfg(feature = "e2e-encryption")]
24use matrix_sdk_base::crypto::DecryptionSettings;
25use matrix_sdk_base::{store::StoreConfig, BaseClient, ThreadingSupport};
26#[cfg(feature = "sqlite")]
27use matrix_sdk_sqlite::SqliteStoreConfig;
28use ruma::{
29    api::{error::FromHttpResponseError, MatrixVersion, SupportedVersions},
30    OwnedServerName, ServerName,
31};
32use thiserror::Error;
33use tokio::sync::{broadcast, Mutex, OnceCell};
34use tracing::{debug, field::debug, instrument, Span};
35
36use super::{Client, ClientInner};
37#[cfg(feature = "e2e-encryption")]
38use crate::crypto::{CollectStrategy, TrustRequirement};
39#[cfg(feature = "e2e-encryption")]
40use crate::encryption::EncryptionSettings;
41#[cfg(not(target_family = "wasm"))]
42use crate::http_client::HttpSettings;
43use crate::{
44    authentication::{oauth::OAuthCtx, AuthCtx},
45    client::{
46        CachedValue::{Cached, NotSet},
47        ClientServerInfo,
48    },
49    config::RequestConfig,
50    error::RumaApiError,
51    http_client::HttpClient,
52    send_queue::SendQueueData,
53    sliding_sync::VersionBuilder as SlidingSyncVersionBuilder,
54    HttpError, IdParseError,
55};
56
57/// Builder that allows creating and configuring various parts of a [`Client`].
58///
59/// When setting the `StateStore` it is up to the user to open/connect
60/// the storage backend before client creation.
61///
62/// # Examples
63///
64/// ```
65/// use matrix_sdk::Client;
66/// // To pass all the request through mitmproxy set the proxy and disable SSL
67/// // verification
68///
69/// let client_builder = Client::builder()
70///     .proxy("http://localhost:8080")
71///     .disable_ssl_verification();
72/// ```
73///
74/// # Example for using a custom http client
75///
76/// Note: setting a custom http client will ignore `user_agent`, `proxy`, and
77/// `disable_ssl_verification` - you'd need to set these yourself if you want
78/// them.
79///
80/// ```
81/// use std::sync::Arc;
82///
83/// use matrix_sdk::Client;
84///
85/// // setting up a custom http client
86/// let reqwest_builder = reqwest::ClientBuilder::new()
87///     .https_only(true)
88///     .no_proxy()
89///     .user_agent("MyApp/v3.0");
90///
91/// let client_builder =
92///     Client::builder().http_client(reqwest_builder.build()?);
93/// # anyhow::Ok(())
94/// ```
95#[must_use]
96#[derive(Clone, Debug)]
97pub struct ClientBuilder {
98    homeserver_cfg: Option<HomeserverConfig>,
99    sliding_sync_version_builder: SlidingSyncVersionBuilder,
100    http_cfg: Option<HttpConfig>,
101    store_config: BuilderStoreConfig,
102    request_config: RequestConfig,
103    respect_login_well_known: bool,
104    server_versions: Option<BTreeSet<MatrixVersion>>,
105    handle_refresh_tokens: bool,
106    base_client: Option<BaseClient>,
107    #[cfg(feature = "e2e-encryption")]
108    encryption_settings: EncryptionSettings,
109    #[cfg(feature = "e2e-encryption")]
110    room_key_recipient_strategy: CollectStrategy,
111    #[cfg(feature = "e2e-encryption")]
112    decryption_settings: DecryptionSettings,
113    #[cfg(feature = "e2e-encryption")]
114    enable_share_history_on_invite: bool,
115    cross_process_store_locks_holder_name: String,
116    threading_support: ThreadingSupport,
117}
118
119impl ClientBuilder {
120    const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
121
122    pub(crate) fn new() -> Self {
123        Self {
124            homeserver_cfg: None,
125            sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
126            http_cfg: None,
127            store_config: BuilderStoreConfig::Custom(StoreConfig::new(
128                Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
129            )),
130            request_config: Default::default(),
131            respect_login_well_known: true,
132            server_versions: None,
133            handle_refresh_tokens: false,
134            base_client: None,
135            #[cfg(feature = "e2e-encryption")]
136            encryption_settings: Default::default(),
137            #[cfg(feature = "e2e-encryption")]
138            room_key_recipient_strategy: Default::default(),
139            #[cfg(feature = "e2e-encryption")]
140            decryption_settings: DecryptionSettings {
141                sender_device_trust_requirement: TrustRequirement::Untrusted,
142            },
143            #[cfg(feature = "e2e-encryption")]
144            enable_share_history_on_invite: false,
145            cross_process_store_locks_holder_name:
146                Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
147            threading_support: ThreadingSupport::Disabled,
148        }
149    }
150
151    /// Set the homeserver URL to use.
152    ///
153    /// The following methods are mutually exclusive: [`Self::homeserver_url`],
154    /// [`Self::server_name`] [`Self::insecure_server_name_no_tls`],
155    /// [`Self::server_name_or_homeserver_url`].
156    /// If you set more than one, then whatever was set last will be used.
157    pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
158        self.homeserver_cfg = Some(HomeserverConfig::HomeserverUrl(url.as_ref().to_owned()));
159        self
160    }
161
162    /// Set the server name to discover the homeserver from.
163    ///
164    /// We assume we can connect in HTTPS to that server. If that's not the
165    /// case, prefer using [`Self::insecure_server_name_no_tls`].
166    ///
167    /// The following methods are mutually exclusive: [`Self::homeserver_url`],
168    /// [`Self::server_name`] [`Self::insecure_server_name_no_tls`],
169    /// [`Self::server_name_or_homeserver_url`].
170    /// If you set more than one, then whatever was set last will be used.
171    pub fn server_name(mut self, server_name: &ServerName) -> Self {
172        self.homeserver_cfg = Some(HomeserverConfig::ServerName {
173            server: server_name.to_owned(),
174            // Assume HTTPS if not specified.
175            protocol: UrlScheme::Https,
176        });
177        self
178    }
179
180    /// Set the server name to discover the homeserver from, assuming an HTTP
181    /// (not secured) scheme. This also relaxes OAuth 2.0 discovery checks to
182    /// allow HTTP schemes.
183    ///
184    /// The following methods are mutually exclusive: [`Self::homeserver_url`],
185    /// [`Self::server_name`] [`Self::insecure_server_name_no_tls`],
186    /// [`Self::server_name_or_homeserver_url`].
187    /// If you set more than one, then whatever was set last will be used.
188    pub fn insecure_server_name_no_tls(mut self, server_name: &ServerName) -> Self {
189        self.homeserver_cfg = Some(HomeserverConfig::ServerName {
190            server: server_name.to_owned(),
191            protocol: UrlScheme::Http,
192        });
193        self
194    }
195
196    /// Set the server name to discover the homeserver from, falling back to
197    /// using it as a homeserver URL if discovery fails. When falling back to a
198    /// homeserver URL, a check is made to ensure that the server exists (unlike
199    /// [`Self::homeserver_url`], so you can guarantee that the client is ready
200    /// to use.
201    ///
202    /// The following methods are mutually exclusive: [`Self::homeserver_url`],
203    /// [`Self::server_name`] [`Self::insecure_server_name_no_tls`],
204    /// [`Self::server_name_or_homeserver_url`].
205    /// If you set more than one, then whatever was set last will be used.
206    pub fn server_name_or_homeserver_url(mut self, server_name_or_url: impl AsRef<str>) -> Self {
207        self.homeserver_cfg = Some(HomeserverConfig::ServerNameOrHomeserverUrl(
208            server_name_or_url.as_ref().to_owned(),
209        ));
210        self
211    }
212
213    /// Set sliding sync to a specific version.
214    pub fn sliding_sync_version_builder(
215        mut self,
216        version_builder: SlidingSyncVersionBuilder,
217    ) -> Self {
218        self.sliding_sync_version_builder = version_builder;
219        self
220    }
221
222    /// Set up the store configuration for an SQLite store.
223    #[cfg(feature = "sqlite")]
224    pub fn sqlite_store(mut self, path: impl AsRef<Path>, passphrase: Option<&str>) -> Self {
225        let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
226        self.store_config =
227            BuilderStoreConfig::Sqlite { config: sqlite_store_config, cache_path: None };
228
229        self
230    }
231
232    /// Set up the store configuration for an SQLite store with cached data
233    /// separated out from state/crypto data.
234    #[cfg(feature = "sqlite")]
235    pub fn sqlite_store_with_cache_path(
236        mut self,
237        path: impl AsRef<Path>,
238        cache_path: impl AsRef<Path>,
239        passphrase: Option<&str>,
240    ) -> Self {
241        let sqlite_store_config = SqliteStoreConfig::new(path).passphrase(passphrase);
242        self.store_config = BuilderStoreConfig::Sqlite {
243            config: sqlite_store_config,
244            cache_path: Some(cache_path.as_ref().to_owned()),
245        };
246
247        self
248    }
249
250    /// Set up the store configuration for an SQLite store with a store config,
251    /// and with an optional cache data separated out from state/crypto data.
252    #[cfg(feature = "sqlite")]
253    pub fn sqlite_store_with_config_and_cache_path(
254        mut self,
255        config: SqliteStoreConfig,
256        cache_path: Option<impl AsRef<Path>>,
257    ) -> Self {
258        self.store_config = BuilderStoreConfig::Sqlite {
259            config,
260            cache_path: cache_path.map(|cache_path| cache_path.as_ref().to_owned()),
261        };
262
263        self
264    }
265
266    /// Set up the store configuration for a IndexedDB store.
267    #[cfg(feature = "indexeddb")]
268    pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self {
269        self.store_config = BuilderStoreConfig::IndexedDb {
270            name: name.to_owned(),
271            passphrase: passphrase.map(ToOwned::to_owned),
272        };
273        self
274    }
275
276    /// Set up the store configuration.
277    ///
278    /// The easiest way to get a [`StoreConfig`] is to use the
279    /// `make_store_config` method from one of the store crates.
280    ///
281    /// # Arguments
282    ///
283    /// * `store_config` - The configuration of the store.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// # use matrix_sdk_base::store::MemoryStore;
289    /// # let custom_state_store = MemoryStore::new();
290    /// use matrix_sdk::{config::StoreConfig, Client};
291    ///
292    /// let store_config =
293    ///     StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
294    ///         .state_store(custom_state_store);
295    /// let client_builder = Client::builder().store_config(store_config);
296    /// ```
297    pub fn store_config(mut self, store_config: StoreConfig) -> Self {
298        self.store_config = BuilderStoreConfig::Custom(store_config);
299        self
300    }
301
302    /// Update the client's homeserver URL with the discovery information
303    /// present in the login response, if any.
304    pub fn respect_login_well_known(mut self, value: bool) -> Self {
305        self.respect_login_well_known = value;
306        self
307    }
308
309    /// Set the default timeout, fail and retry behavior for all HTTP requests.
310    pub fn request_config(mut self, request_config: RequestConfig) -> Self {
311        self.request_config = request_config;
312        self
313    }
314
315    /// Set the proxy through which all the HTTP requests should go.
316    ///
317    /// Note, only HTTP proxies are supported.
318    ///
319    /// # Arguments
320    ///
321    /// * `proxy` - The HTTP URL of the proxy.
322    ///
323    /// # Examples
324    ///
325    /// ```no_run
326    /// use matrix_sdk::Client;
327    ///
328    /// let client_config = Client::builder().proxy("http://localhost:8080");
329    /// ```
330    #[cfg(not(target_family = "wasm"))]
331    pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
332        self.http_settings().proxy = Some(proxy.as_ref().to_owned());
333        self
334    }
335
336    /// Disable SSL verification for the HTTP requests.
337    #[cfg(not(target_family = "wasm"))]
338    pub fn disable_ssl_verification(mut self) -> Self {
339        self.http_settings().disable_ssl_verification = true;
340        self
341    }
342
343    /// Set a custom HTTP user agent for the client.
344    #[cfg(not(target_family = "wasm"))]
345    pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
346        self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
347        self
348    }
349
350    /// Add the given list of certificates to the certificate store of the HTTP
351    /// client.
352    ///
353    /// These additional certificates will be trusted and considered when
354    /// establishing a HTTP request.
355    ///
356    /// Internally this will call the
357    /// [`reqwest::ClientBuilder::add_root_certificate()`] method.
358    #[cfg(not(target_family = "wasm"))]
359    pub fn add_root_certificates(mut self, certificates: Vec<reqwest::Certificate>) -> Self {
360        self.http_settings().additional_root_certificates = certificates;
361        self
362    }
363
364    /// Don't trust any system root certificates, only trust the certificates
365    /// provided through
366    /// [`add_root_certificates`][ClientBuilder::add_root_certificates].
367    #[cfg(not(target_family = "wasm"))]
368    pub fn disable_built_in_root_certificates(mut self) -> Self {
369        self.http_settings().disable_built_in_root_certificates = true;
370        self
371    }
372
373    /// Specify a [`reqwest::Client`] instance to handle sending requests and
374    /// receiving responses.
375    ///
376    /// This method is mutually exclusive with
377    /// [`proxy()`][ClientBuilder::proxy],
378    /// [`disable_ssl_verification`][ClientBuilder::disable_ssl_verification],
379    /// [`add_root_certificates`][ClientBuilder::add_root_certificates],
380    /// [`disable_built_in_root_certificates`][ClientBuilder::disable_built_in_root_certificates],
381    /// and [`user_agent()`][ClientBuilder::user_agent].
382    pub fn http_client(mut self, client: reqwest::Client) -> Self {
383        self.http_cfg = Some(HttpConfig::Custom(client));
384        self
385    }
386
387    /// Specify the Matrix versions supported by the homeserver manually, rather
388    /// than `build()` doing it using a `get_supported_versions` request.
389    ///
390    /// This is helpful for test code that doesn't care to mock that endpoint.
391    pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
392        self.server_versions = Some(value.into_iter().collect());
393        self
394    }
395
396    #[cfg(not(target_family = "wasm"))]
397    fn http_settings(&mut self) -> &mut HttpSettings {
398        self.http_cfg.get_or_insert_with(Default::default).settings()
399    }
400
401    /// Handle [refreshing access tokens] automatically.
402    ///
403    /// By default, the `Client` forwards any error and doesn't handle errors
404    /// with the access token, which means that
405    /// [`Client::refresh_access_token()`] needs to be called manually to
406    /// refresh access tokens.
407    ///
408    /// Enabling this setting means that the `Client` will try to refresh the
409    /// token automatically, which means that:
410    ///
411    /// * If refreshing the token fails, the error is forwarded, so any endpoint
412    ///   can return [`HttpError::RefreshToken`]. If an [`UnknownToken`] error
413    ///   is encountered, it means that the user needs to be logged in again.
414    ///
415    /// * The access token and refresh token need to be watched for changes,
416    ///   using the authentication API's `session_tokens_stream()` for example,
417    ///   to be able to [restore the session] later.
418    ///
419    /// [refreshing access tokens]: https://spec.matrix.org/v1.3/client-server-api/#refreshing-access-tokens
420    /// [`UnknownToken`]: ruma::api::client::error::ErrorKind::UnknownToken
421    /// [restore the session]: Client::restore_session
422    pub fn handle_refresh_tokens(mut self) -> Self {
423        self.handle_refresh_tokens = true;
424        self
425    }
426
427    /// Public for test only
428    #[doc(hidden)]
429    pub fn base_client(mut self, base_client: BaseClient) -> Self {
430        self.base_client = Some(base_client);
431        self
432    }
433
434    /// Enables specific encryption settings that will persist throughout the
435    /// entire lifetime of the `Client`.
436    #[cfg(feature = "e2e-encryption")]
437    pub fn with_encryption_settings(mut self, settings: EncryptionSettings) -> Self {
438        self.encryption_settings = settings;
439        self
440    }
441
442    /// Set the strategy to be used for picking recipient devices, when sending
443    /// an encrypted message.
444    #[cfg(feature = "e2e-encryption")]
445    pub fn with_room_key_recipient_strategy(mut self, strategy: CollectStrategy) -> Self {
446        self.room_key_recipient_strategy = strategy;
447        self
448    }
449
450    /// Set the trust requirement to be used when decrypting events.
451    #[cfg(feature = "e2e-encryption")]
452    pub fn with_decryption_settings(mut self, decryption_settings: DecryptionSettings) -> Self {
453        self.decryption_settings = decryption_settings;
454        self
455    }
456
457    /// Whether to enable the experimental support for sending and receiving
458    /// encrypted room history on invite, per [MSC4268].
459    ///
460    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
461    #[cfg(feature = "e2e-encryption")]
462    pub fn with_enable_share_history_on_invite(
463        mut self,
464        enable_share_history_on_invite: bool,
465    ) -> Self {
466        self.enable_share_history_on_invite = enable_share_history_on_invite;
467        self
468    }
469
470    /// Set the cross-process store locks holder name.
471    ///
472    /// The SDK provides cross-process store locks (see
473    /// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The
474    /// `holder_name` will be the value used for all cross-process store locks
475    /// used by the `Client` being built.
476    ///
477    /// If 2 concurrent `Client`s are running in 2 different process, this
478    /// method must be called with different `hold_name` values.
479    pub fn cross_process_store_locks_holder_name(mut self, holder_name: String) -> Self {
480        self.cross_process_store_locks_holder_name = holder_name;
481        self
482    }
483
484    /// Whether the threads feature is enabled throuoghout the SDK.
485    /// This will affect how timelines are setup, how read receipts are sent
486    /// and how room unreads are computed.
487    pub fn with_threading_support(mut self, threading_support: ThreadingSupport) -> Self {
488        self.threading_support = threading_support;
489        self
490    }
491
492    /// Create a [`Client`] with the options set on this builder.
493    ///
494    /// # Errors
495    ///
496    /// This method can fail for two general reasons:
497    ///
498    /// * Invalid input: a missing or invalid homeserver URL or invalid proxy
499    ///   URL
500    /// * HTTP error: If you supplied a user ID instead of a homeserver URL, a
501    ///   server discovery request is made which can fail; if you didn't set
502    ///   [`server_versions(false)`][Self::server_versions], that amounts to
503    ///   another request that can fail
504    #[instrument(skip_all, target = "matrix_sdk::client", fields(homeserver))]
505    pub async fn build(self) -> Result<Client, ClientBuildError> {
506        debug!("Starting to build the Client");
507
508        let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?;
509        Span::current().record("homeserver", debug(&homeserver_cfg));
510
511        #[cfg_attr(target_family = "wasm", allow(clippy::infallible_destructuring_match))]
512        let inner_http_client = match self.http_cfg.unwrap_or_default() {
513            #[cfg(not(target_family = "wasm"))]
514            HttpConfig::Settings(mut settings) => {
515                settings.timeout = self.request_config.timeout;
516                settings.make_client()?
517            }
518            HttpConfig::Custom(c) => c,
519        };
520
521        let base_client = if let Some(base_client) = self.base_client {
522            base_client
523        } else {
524            #[allow(unused_mut)]
525            let mut client = BaseClient::new(
526                build_store_config(self.store_config, &self.cross_process_store_locks_holder_name)
527                    .await?,
528                self.threading_support,
529            );
530
531            #[cfg(feature = "e2e-encryption")]
532            {
533                client.room_key_recipient_strategy = self.room_key_recipient_strategy;
534                client.decryption_settings = self.decryption_settings;
535            }
536
537            client
538        };
539
540        let http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
541
542        #[allow(unused_variables)]
543        let HomeserverDiscoveryResult { server, homeserver, supported_versions, well_known } =
544            homeserver_cfg.discover(&http_client).await?;
545
546        let sliding_sync_version = {
547            let supported_versions = match supported_versions {
548                Some(versions) => Some(versions),
549                None if self.sliding_sync_version_builder.needs_get_supported_versions() => {
550                    Some(get_supported_versions(&homeserver, &http_client).await?)
551                }
552                None => None,
553            };
554
555            let version = self.sliding_sync_version_builder.build(
556                supported_versions.map(|response| response.as_supported_versions()).as_ref(),
557            )?;
558
559            tracing::info!(?version, "selected sliding sync version");
560
561            version
562        };
563
564        let allow_insecure_oauth = homeserver.scheme() == "http";
565
566        let auth_ctx = Arc::new(AuthCtx {
567            handle_refresh_tokens: self.handle_refresh_tokens,
568            refresh_token_lock: Arc::new(Mutex::new(Ok(()))),
569            session_change_sender: broadcast::Sender::new(1),
570            auth_data: OnceCell::default(),
571            tokens: OnceCell::default(),
572            reload_session_callback: OnceCell::default(),
573            save_session_callback: OnceCell::default(),
574            oauth: OAuthCtx::new(allow_insecure_oauth),
575        });
576
577        // Enable the send queue by default.
578        let send_queue = Arc::new(SendQueueData::new(true));
579
580        let server_info = ClientServerInfo {
581            supported_versions: match self.server_versions {
582                Some(versions) => {
583                    Cached(SupportedVersions { versions, features: Default::default() })
584                }
585                None => NotSet,
586            },
587            well_known: Cached(well_known.map(Into::into)),
588        };
589
590        let event_cache = OnceCell::new();
591        let latest_events = OnceCell::new();
592
593        let inner = ClientInner::new(
594            auth_ctx,
595            server,
596            homeserver,
597            sliding_sync_version,
598            http_client,
599            base_client,
600            server_info,
601            self.respect_login_well_known,
602            event_cache,
603            send_queue,
604            latest_events,
605            #[cfg(feature = "e2e-encryption")]
606            self.encryption_settings,
607            #[cfg(feature = "e2e-encryption")]
608            self.enable_share_history_on_invite,
609            self.cross_process_store_locks_holder_name,
610        )
611        .await;
612
613        debug!("Done building the Client");
614
615        Ok(Client { inner })
616    }
617}
618
619/// Creates a server name from a user supplied string. The string is first
620/// sanitized by removing whitespace, the http(s) scheme and any trailing
621/// slashes before being parsed.
622pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseError> {
623    ServerName::parse(
624        s.trim().trim_start_matches("http://").trim_start_matches("https://").trim_end_matches('/'),
625    )
626}
627
628#[allow(clippy::unused_async, unused)] // False positive when building with !sqlite & !indexeddb
629async fn build_store_config(
630    builder_config: BuilderStoreConfig,
631    cross_process_store_locks_holder_name: &str,
632) -> Result<StoreConfig, ClientBuildError> {
633    #[allow(clippy::infallible_destructuring_match)]
634    let store_config = match builder_config {
635        #[cfg(feature = "sqlite")]
636        BuilderStoreConfig::Sqlite { config, cache_path } => {
637            let store_config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
638                .state_store(
639                    matrix_sdk_sqlite::SqliteStateStore::open_with_config(config.clone()).await?,
640                )
641                .event_cache_store({
642                    let mut config = config.clone();
643
644                    if let Some(cache_path) = cache_path {
645                        config = config.path(cache_path);
646                    }
647
648                    matrix_sdk_sqlite::SqliteEventCacheStore::open_with_config(config).await?
649                });
650
651            #[cfg(feature = "e2e-encryption")]
652            let store_config = store_config.crypto_store(
653                matrix_sdk_sqlite::SqliteCryptoStore::open_with_config(config).await?,
654            );
655
656            store_config
657        }
658
659        #[cfg(feature = "indexeddb")]
660        BuilderStoreConfig::IndexedDb { name, passphrase } => {
661            build_indexeddb_store_config(
662                &name,
663                passphrase.as_deref(),
664                cross_process_store_locks_holder_name,
665            )
666            .await?
667        }
668
669        BuilderStoreConfig::Custom(config) => config,
670    };
671    Ok(store_config)
672}
673
674// The indexeddb stores only implement `IntoStateStore` and `IntoCryptoStore` on
675// wasm32, so this only compiles there.
676#[cfg(all(target_family = "wasm", feature = "indexeddb"))]
677async fn build_indexeddb_store_config(
678    name: &str,
679    passphrase: Option<&str>,
680    cross_process_store_locks_holder_name: &str,
681) -> Result<StoreConfig, ClientBuildError> {
682    let cross_process_store_locks_holder_name = cross_process_store_locks_holder_name.to_owned();
683
684    #[cfg(feature = "e2e-encryption")]
685    let store_config = {
686        let (state_store, crypto_store) =
687            matrix_sdk_indexeddb::open_stores_with_name(name, passphrase).await?;
688        StoreConfig::new(cross_process_store_locks_holder_name)
689            .state_store(state_store)
690            .crypto_store(crypto_store)
691    };
692
693    #[cfg(not(feature = "e2e-encryption"))]
694    let store_config = {
695        let state_store = matrix_sdk_indexeddb::open_state_store(name, passphrase).await?;
696        StoreConfig::new(cross_process_store_locks_holder_name).state_store(state_store)
697    };
698
699    let store_config = {
700        tracing::warn!(
701            "The IndexedDB backend does not implement an event cache store, \
702             falling back to the in-memory event cache store…"
703        );
704        store_config.event_cache_store(matrix_sdk_base::event_cache::store::MemoryStore::new())
705    };
706
707    Ok(store_config)
708}
709
710#[cfg(all(not(target_family = "wasm"), feature = "indexeddb"))]
711#[allow(clippy::unused_async)]
712async fn build_indexeddb_store_config(
713    _name: &str,
714    _passphrase: Option<&str>,
715    _event_cache_store_lock_holder_name: &str,
716) -> Result<StoreConfig, ClientBuildError> {
717    panic!("the IndexedDB is only available on the 'wasm32' arch")
718}
719
720#[derive(Clone, Debug)]
721enum HttpConfig {
722    #[cfg(not(target_family = "wasm"))]
723    Settings(HttpSettings),
724    Custom(reqwest::Client),
725}
726
727#[cfg(not(target_family = "wasm"))]
728impl HttpConfig {
729    fn settings(&mut self) -> &mut HttpSettings {
730        match self {
731            Self::Settings(s) => s,
732            Self::Custom(_) => {
733                *self = Self::default();
734                match self {
735                    Self::Settings(s) => s,
736                    Self::Custom(_) => unreachable!(),
737                }
738            }
739        }
740    }
741}
742
743impl Default for HttpConfig {
744    fn default() -> Self {
745        #[cfg(not(target_family = "wasm"))]
746        return Self::Settings(HttpSettings::default());
747
748        #[cfg(target_family = "wasm")]
749        return Self::Custom(reqwest::Client::new());
750    }
751}
752
753#[derive(Clone)]
754enum BuilderStoreConfig {
755    #[cfg(feature = "sqlite")]
756    Sqlite {
757        config: SqliteStoreConfig,
758        cache_path: Option<std::path::PathBuf>,
759    },
760    #[cfg(feature = "indexeddb")]
761    IndexedDb {
762        name: String,
763        passphrase: Option<String>,
764    },
765    Custom(StoreConfig),
766}
767
768#[cfg(not(tarpaulin_include))]
769impl fmt::Debug for BuilderStoreConfig {
770    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771        #[allow(clippy::infallible_destructuring_match)]
772        match self {
773            #[cfg(feature = "sqlite")]
774            Self::Sqlite { config, cache_path, .. } => f
775                .debug_struct("Sqlite")
776                .field("config", config)
777                .field("cache_path", cache_path)
778                .finish_non_exhaustive(),
779
780            #[cfg(feature = "indexeddb")]
781            Self::IndexedDb { name, .. } => {
782                f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive()
783            }
784
785            Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(),
786        }
787    }
788}
789
790/// Errors that can happen in [`ClientBuilder::build`].
791#[derive(Debug, Error)]
792pub enum ClientBuildError {
793    /// No homeserver or user ID was configured
794    #[error("no homeserver or user ID was configured")]
795    MissingHomeserver,
796
797    /// The supplied server name was invalid.
798    #[error("The supplied server name is invalid")]
799    InvalidServerName,
800
801    /// Error looking up the .well-known endpoint on auto-discovery
802    #[error("Error looking up the .well-known endpoint on auto-discovery")]
803    AutoDiscovery(FromHttpResponseError<RumaApiError>),
804
805    /// Error when building the sliding sync version.
806    #[error(transparent)]
807    SlidingSyncVersion(#[from] crate::sliding_sync::VersionBuilderError),
808
809    /// An error encountered when trying to parse the homeserver url.
810    #[error(transparent)]
811    Url(#[from] url::ParseError),
812
813    /// Error doing an HTTP request.
814    #[error(transparent)]
815    Http(#[from] HttpError),
816
817    /// Error opening the indexeddb store.
818    #[cfg(feature = "indexeddb")]
819    #[error(transparent)]
820    IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
821
822    /// Error opening the sqlite store.
823    #[cfg(feature = "sqlite")]
824    #[error(transparent)]
825    SqliteStore(#[from] matrix_sdk_sqlite::OpenStoreError),
826}
827
828// The http mocking library is not supported for wasm32
829#[cfg(all(test, not(target_family = "wasm")))]
830pub(crate) mod tests {
831    use assert_matches::assert_matches;
832    use matrix_sdk_test::{async_test, test_json};
833    use serde_json::{json_internal, Value as JsonValue};
834    use wiremock::{
835        matchers::{method, path},
836        Mock, MockServer, ResponseTemplate,
837    };
838
839    use super::*;
840    use crate::sliding_sync::Version as SlidingSyncVersion;
841
842    #[test]
843    fn test_sanitize_server_name() {
844        assert_eq!(sanitize_server_name("matrix.org").unwrap().as_str(), "matrix.org");
845        assert_eq!(sanitize_server_name("https://matrix.org").unwrap().as_str(), "matrix.org");
846        assert_eq!(sanitize_server_name("http://matrix.org").unwrap().as_str(), "matrix.org");
847        assert_eq!(
848            sanitize_server_name("https://matrix.server.org").unwrap().as_str(),
849            "matrix.server.org"
850        );
851        assert_eq!(
852            sanitize_server_name("https://matrix.server.org/").unwrap().as_str(),
853            "matrix.server.org"
854        );
855        assert_eq!(
856            sanitize_server_name("  https://matrix.server.org// ").unwrap().as_str(),
857            "matrix.server.org"
858        );
859        assert_matches!(sanitize_server_name("https://matrix.server.org/something"), Err(_))
860    }
861
862    // Note: Due to a limitation of the http mocking library the following tests all
863    // supply an http:// url, to `server_name_or_homeserver_url` rather than the plain server name,
864    // otherwise  the builder will prepend https:// and the request will fail. In practice, this
865    // isn't a problem as the builder first strips the scheme and then checks if the
866    // name is a valid server name, so it is a close enough approximation.
867
868    #[async_test]
869    async fn test_discovery_invalid_server() {
870        // Given a new client builder.
871        let mut builder = ClientBuilder::new();
872
873        // When building a client with an invalid server name.
874        builder = builder.server_name_or_homeserver_url("⚠️ This won't work 🚫");
875        let error = builder.build().await.unwrap_err();
876
877        // Then the operation should fail due to the invalid server name.
878        assert_matches!(error, ClientBuildError::InvalidServerName);
879    }
880
881    #[async_test]
882    async fn test_discovery_no_server() {
883        // Given a new client builder.
884        let mut builder = ClientBuilder::new();
885
886        // When building a client with a valid server name that doesn't exist.
887        builder = builder.server_name_or_homeserver_url("localhost:3456");
888        let error = builder.build().await.unwrap_err();
889
890        // Then the operation should fail with an HTTP error.
891        println!("{error}");
892        assert_matches!(error, ClientBuildError::Http(_));
893    }
894
895    #[async_test]
896    async fn test_discovery_web_server() {
897        // Given a random web server that isn't a Matrix homeserver or hosting the
898        // well-known file for one.
899        let server = MockServer::start().await;
900        let mut builder = ClientBuilder::new();
901
902        // When building a client with the server's URL.
903        builder = builder.server_name_or_homeserver_url(server.uri());
904        let error = builder.build().await.unwrap_err();
905
906        // Then the operation should fail with a server discovery error.
907        assert_matches!(error, ClientBuildError::AutoDiscovery(FromHttpResponseError::Server(_)));
908    }
909
910    #[async_test]
911    async fn test_discovery_direct_legacy() {
912        // Given a homeserver without a well-known file.
913        let homeserver = make_mock_homeserver().await;
914        let mut builder = ClientBuilder::new();
915
916        // When building a client with the server's URL.
917        builder = builder.server_name_or_homeserver_url(homeserver.uri());
918        let _client = builder.build().await.unwrap();
919
920        // Then a client should be built with native support for sliding sync.
921        assert!(_client.sliding_sync_version().is_native());
922    }
923
924    #[async_test]
925    async fn test_discovery_well_known_parse_error() {
926        // Given a base server with a well-known file that has errors.
927        let server = MockServer::start().await;
928        let homeserver = make_mock_homeserver().await;
929        let mut builder = ClientBuilder::new();
930
931        let well_known = make_well_known_json(&homeserver.uri());
932        let bad_json = well_known.to_string().replace(',', "");
933        Mock::given(method("GET"))
934            .and(path("/.well-known/matrix/client"))
935            .respond_with(ResponseTemplate::new(200).set_body_json(bad_json))
936            .mount(&server)
937            .await;
938
939        // When building a client with the base server.
940        builder = builder.server_name_or_homeserver_url(server.uri());
941        let error = builder.build().await.unwrap_err();
942
943        // Then the operation should fail due to the well-known file's contents.
944        assert_matches!(
945            error,
946            ClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(_))
947        );
948    }
949
950    #[async_test]
951    async fn test_discovery_well_known_legacy() {
952        // Given a base server with a well-known file that points to a homeserver that
953        // doesn't support sliding sync.
954        let server = MockServer::start().await;
955        let homeserver = make_mock_homeserver().await;
956        let mut builder = ClientBuilder::new();
957
958        Mock::given(method("GET"))
959            .and(path("/.well-known/matrix/client"))
960            .respond_with(
961                ResponseTemplate::new(200).set_body_json(make_well_known_json(&homeserver.uri())),
962            )
963            .mount(&server)
964            .await;
965
966        // When building a client with the base server.
967        builder = builder.server_name_or_homeserver_url(server.uri());
968        let client = builder.build().await.unwrap();
969
970        // Then a client should be built with native support for sliding sync.
971        // It's native support because it's the default. Nothing is checked here.
972        assert!(client.sliding_sync_version().is_native());
973    }
974
975    #[async_test]
976    async fn test_sliding_sync_discover_native() {
977        // Given a homeserver with a `/versions` file.
978        let homeserver = make_mock_homeserver().await;
979        let mut builder = ClientBuilder::new();
980
981        // When building the client with sliding sync to auto-discover the
982        // native version.
983        builder = builder
984            .server_name_or_homeserver_url(homeserver.uri())
985            .sliding_sync_version_builder(SlidingSyncVersionBuilder::DiscoverNative);
986
987        let client = builder.build().await.unwrap();
988
989        // Then, sliding sync has the correct native version.
990        assert_matches!(client.sliding_sync_version(), SlidingSyncVersion::Native);
991    }
992
993    #[async_test]
994    #[cfg(feature = "e2e-encryption")]
995    async fn test_set_up_decryption_trust_requirement_cross_signed() {
996        let homeserver = make_mock_homeserver().await;
997        let builder = ClientBuilder::new()
998            .server_name_or_homeserver_url(homeserver.uri())
999            .with_decryption_settings(DecryptionSettings {
1000                sender_device_trust_requirement: TrustRequirement::CrossSigned,
1001            });
1002
1003        let client = builder.build().await.unwrap();
1004        assert_matches!(
1005            client.base_client().decryption_settings.sender_device_trust_requirement,
1006            TrustRequirement::CrossSigned
1007        );
1008    }
1009
1010    #[async_test]
1011    #[cfg(feature = "e2e-encryption")]
1012    async fn test_set_up_decryption_trust_requirement_untrusted() {
1013        let homeserver = make_mock_homeserver().await;
1014
1015        let builder = ClientBuilder::new()
1016            .server_name_or_homeserver_url(homeserver.uri())
1017            .with_decryption_settings(DecryptionSettings {
1018                sender_device_trust_requirement: TrustRequirement::Untrusted,
1019            });
1020
1021        let client = builder.build().await.unwrap();
1022        assert_matches!(
1023            client.base_client().decryption_settings.sender_device_trust_requirement,
1024            TrustRequirement::Untrusted
1025        );
1026    }
1027
1028    /* Helper functions */
1029
1030    async fn make_mock_homeserver() -> MockServer {
1031        let homeserver = MockServer::start().await;
1032        Mock::given(method("GET"))
1033            .and(path("/_matrix/client/versions"))
1034            .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
1035            .mount(&homeserver)
1036            .await;
1037        Mock::given(method("GET"))
1038            .and(path("/_matrix/client/r0/login"))
1039            .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_TYPES))
1040            .mount(&homeserver)
1041            .await;
1042        homeserver
1043    }
1044
1045    fn make_well_known_json(homeserver_url: &str) -> JsonValue {
1046        ::serde_json::Value::Object({
1047            let mut object = ::serde_json::Map::new();
1048            let _ = object.insert(
1049                "m.homeserver".into(),
1050                json_internal!({
1051                    "base_url": homeserver_url
1052                }),
1053            );
1054
1055            object
1056        })
1057    }
1058
1059    #[async_test]
1060    async fn test_cross_process_store_locks_holder_name() {
1061        {
1062            let homeserver = make_mock_homeserver().await;
1063            let client =
1064                ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
1065
1066            assert_eq!(client.cross_process_store_locks_holder_name(), "main");
1067        }
1068
1069        {
1070            let homeserver = make_mock_homeserver().await;
1071            let client = ClientBuilder::new()
1072                .homeserver_url(homeserver.uri())
1073                .cross_process_store_locks_holder_name("foo".to_owned())
1074                .build()
1075                .await
1076                .unwrap();
1077
1078            assert_eq!(client.cross_process_store_locks_holder_name(), "foo");
1079        }
1080    }
1081}