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