Skip to main content

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