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