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