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