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