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