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