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