From 628e2fb716bb3a827d32a42cf897d5c7e9dfc638 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 11 Mar 2022 15:31:11 +0100 Subject: [PATCH 1/6] Allow configuring assert_identity on the client level only Completely removing it from RequestConfig is left up to a future refactoring. --- crates/matrix-sdk-appservice/src/lib.rs | 11 ++++------- crates/matrix-sdk/src/config/client.rs | 13 +++++++++++++ crates/matrix-sdk/src/config/request.rs | 13 ------------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index ef4b24fec..e434a4cff 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -311,19 +311,16 @@ impl AppService { async fn create_and_cache_client( &self, localpart: &str, - config: ClientConfig, + mut config: ClientConfig, ) -> Result { let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?; // The `as_token` in the `Session` maps to the [`MainUser`] // (`sender_localpart`) by default, so we don't need to assert identity // in that case - let config = if localpart != self.registration.sender_localpart { - let request_config = config.get_request_config().assert_identity(); - config.request_config(request_config) - } else { - config - }; + if localpart != self.registration.sender_localpart { + config = config.assert_identity(); + } let client = Client::with_config(self.homeserver_url.clone(), config.appservice_mode()).await?; diff --git a/crates/matrix-sdk/src/config/client.rs b/crates/matrix-sdk/src/config/client.rs index b4389b164..459fe08be 100644 --- a/crates/matrix-sdk/src/config/client.rs +++ b/crates/matrix-sdk/src/config/client.rs @@ -236,4 +236,17 @@ impl ClientConfig { self.use_discovery_response = true; self } + + /// All outgoing http requests will have a GET query key-value appended with + /// `user_id` being the key and the `user_id` from the `Session` being + /// the value. Will error if there's no `Session`. This is called + /// [identity assertion] in the Matrix Application Service Spec + /// + /// [identity assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion + #[cfg(feature = "appservice")] + #[must_use] + pub fn assert_identity(mut self) -> Self { + self.request_config.assert_identity = true; + self + } } diff --git a/crates/matrix-sdk/src/config/request.rs b/crates/matrix-sdk/src/config/request.rs index 28292316b..2d9c03686 100644 --- a/crates/matrix-sdk/src/config/request.rs +++ b/crates/matrix-sdk/src/config/request.rs @@ -121,17 +121,4 @@ impl RequestConfig { self.force_auth = true; self } - - /// All outgoing http requests will have a GET query key-value appended with - /// `user_id` being the key and the `user_id` from the `Session` being - /// the value. Will error if there's no `Session`. This is called - /// [identity assertion] in the Matrix Application Service Spec - /// - /// [identity assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion - #[cfg(feature = "appservice")] - #[must_use] - pub fn assert_identity(mut self) -> Self { - self.assert_identity = true; - self - } } From bd7fbea15565746869c66d495ea2085efcf10896 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 11 Mar 2022 18:04:32 +0100 Subject: [PATCH 2/6] Fix intra-doc link --- crates/matrix-sdk/src/config/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/config/mod.rs b/crates/matrix-sdk/src/config/mod.rs index 6f759d643..39ade13e7 100644 --- a/crates/matrix-sdk/src/config/mod.rs +++ b/crates/matrix-sdk/src/config/mod.rs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Configuration to change the behaviour of the [`Client`]. -//! -//! [`Client`]: #crate.Client +//! Configuration to change the behaviour of the [`Client`][crate::Client]. mod client; mod request; From 147e948fe6ef0d6f762cd8ede0dc7282a532103d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 11 Mar 2022 18:29:43 +0100 Subject: [PATCH 3/6] Replace ClientConfig with ClientBuilder This includes a few not-strictly-related changes that made sense to do at the same time: * Check for a functioning homeserver via get_supported_versions regardless of whether a homeserver URL or user ID was supplied * Rename use_discovery_response to respect_login_well_known * Small appservice documentation improvement because those docs had to be touched anyways * Some test refactorings in tests that had to be touched anyways --- crates/matrix-sdk-appservice/src/lib.rs | 55 +-- crates/matrix-sdk-appservice/tests/tests.rs | 16 +- crates/matrix-sdk/examples/autojoin.rs | 23 +- crates/matrix-sdk/examples/command_bot.rs | 22 +- crates/matrix-sdk/src/client/builder.rs | 401 ++++++++++++++++++ .../src/{client.rs => client/mod.rs} | 236 ++++------- crates/matrix-sdk/src/config/client.rs | 252 ----------- crates/matrix-sdk/src/config/mod.rs | 2 - crates/matrix-sdk/src/config/request.rs | 2 +- crates/matrix-sdk/src/docs/encryption.md | 4 +- crates/matrix-sdk/src/http_client.rs | 83 ++-- crates/matrix-sdk/src/lib.rs | 2 +- crates/matrix-sdk/src/store.rs | 4 +- 13 files changed, 604 insertions(+), 498 deletions(-) create mode 100644 crates/matrix-sdk/src/client/builder.rs rename crates/matrix-sdk/src/{client.rs => client/mod.rs} (95%) delete mode 100644 crates/matrix-sdk/src/config/client.rs diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index e434a4cff..b16d6b3ef 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -90,10 +90,9 @@ pub use matrix_sdk; pub use matrix_sdk::ruma; use matrix_sdk::{ bytes::Bytes, - config::ClientConfig, event_handler::{EventHandler, EventHandlerResult, SyncEvent}, reqwest::Url, - Client, Session, + Client, ClientBuildError, ClientBuilder, Session, }; use regex::Regex; use ruma::{ @@ -210,8 +209,8 @@ impl AppService { /// Create new AppService /// /// Also creates and caches a [`Client`] for the [`MainUser`]. - /// The default [`ClientConfig`] is used, if you want to customize it - /// use [`Self::with_config()`] instead. + /// A default [`ClientBuilder`] is used, if you want to customize it + /// use [`with_client_builder()`][Self::with_client_builder] instead. /// /// # Arguments /// @@ -228,19 +227,19 @@ impl AppService { registration: AppServiceRegistration, ) -> Result { let appservice = - Self::with_config(homeserver_url, server_name, registration, ClientConfig::default()) + Self::with_client_builder(homeserver_url, server_name, registration, Client::builder()) .await?; Ok(appservice) } - /// Same as [`Self::new()`] but lets you provide a [`ClientConfig`] for the - /// [`Client`] - pub async fn with_config( + /// Same as [`new()`][Self::new] but lets you provide a [`ClientBuilder`] + /// for the [`Client`] + pub async fn with_client_builder( homeserver_url: impl TryInto, server_name: impl TryInto, Error = identifiers::Error>, registration: AppServiceRegistration, - client_config: ClientConfig, + builder: ClientBuilder, ) -> Result { let homeserver_url = homeserver_url.try_into()?; let server_name = server_name.try_into()?; @@ -253,7 +252,7 @@ impl AppService { AppService { homeserver_url, server_name, registration, clients, event_handler }; // we create and cache the [`MainUser`] by default - appservice.create_and_cache_client(&sender_localpart, client_config).await?; + appservice.create_and_cache_client(&sender_localpart, builder).await?; Ok(appservice) } @@ -266,8 +265,9 @@ impl AppService { /// /// This method is a singleton that saves the client internally for re-use /// based on the `localpart`. The cached [`Client`] can be retrieved either - /// by calling this method again or by calling [`Self::get_cached_client()`] - /// which is non-async convenience wrapper. + /// by calling this method again or by calling + /// [`get_cached_client()`][Self::get_cached_client] which is non-async + /// convenience wrapper. /// /// Note that if you want to do actions like joining rooms with a virtual /// user it needs to be registered first. `Self::register_virtual_user()` @@ -281,20 +281,20 @@ impl AppService { /// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion pub async fn virtual_user_client(&self, localpart: impl AsRef) -> Result { let client = - self.virtual_user_client_with_config(localpart, ClientConfig::default()).await?; + self.virtual_user_client_with_client_builder(localpart, Client::builder()).await?; Ok(client) } - /// Same as [`Self::virtual_user_client()`] but with the ability to pass in - /// a [`ClientConfig`] + /// Same as [`virtual_user_client()`][Self::virtual_user_client] but with + /// the ability to pass in a [`ClientBuilder`] /// /// Since this method is a singleton follow-up calls with different - /// [`ClientConfig`]s will be ignored. - pub async fn virtual_user_client_with_config( + /// [`ClientBuilder`]s will be ignored. + pub async fn virtual_user_client_with_client_builder( &self, localpart: impl AsRef, - config: ClientConfig, + builder: ClientBuilder, ) -> Result { // TODO: check if localpart is covered by namespace? let localpart = localpart.as_ref(); @@ -302,7 +302,7 @@ impl AppService { let client = if let Some(client) = self.clients.get(localpart) { client.clone() } else { - self.create_and_cache_client(localpart, config).await? + self.create_and_cache_client(localpart, builder).await? }; Ok(client) @@ -311,7 +311,7 @@ impl AppService { async fn create_and_cache_client( &self, localpart: &str, - mut config: ClientConfig, + mut builder: ClientBuilder, ) -> Result { let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?; @@ -319,11 +319,15 @@ impl AppService { // (`sender_localpart`) by default, so we don't need to assert identity // in that case if localpart != self.registration.sender_localpart { - config = config.assert_identity(); + builder = builder.assert_identity(); } - let client = - Client::with_config(self.homeserver_url.clone(), config.appservice_mode()).await?; + let client = builder + .homeserver_url(self.homeserver_url.clone()) + .appservice_mode() + .build() + .await + .map_err(ClientBuildError::assert_valid_builder_args)?; let session = Session { access_token: self.registration.as_token.clone(), @@ -341,8 +345,9 @@ impl AppService { /// Get cached [`Client`] /// /// Will return the client for the given `localpart` if previously - /// constructed with [`Self::virtual_user_client()`] or - /// [`Self::virtual_user_client_with_config()`]. + /// constructed with [`virtual_user_client()`][Self::virtual_user_client] or + /// [`virtual_user_client_with_config()`][Self:: + /// virtual_user_client_with_client_builder]. /// /// If no `localpart` is given it assumes the [`MainUser`]'s `localpart`. If /// no client for `localpart` is found it will return an Error. diff --git a/crates/matrix-sdk-appservice/tests/tests.rs b/crates/matrix-sdk-appservice/tests/tests.rs index 4092db182..f18c974f2 100644 --- a/crates/matrix-sdk-appservice/tests/tests.rs +++ b/crates/matrix-sdk-appservice/tests/tests.rs @@ -4,8 +4,9 @@ use std::{ }; use matrix_sdk::{ - config::{ClientConfig, RequestConfig}, + config::RequestConfig, ruma::{api::appservice::Registration, events::room::member::SyncRoomMemberEvent}, + Client, }; use matrix_sdk_appservice::*; use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson}; @@ -32,10 +33,17 @@ async fn appservice(registration: Option) -> Result { let homeserver_url = mockito::server_url(); let server_name = "localhost"; - let client_config = - ClientConfig::default().request_config(RequestConfig::default().disable_retry()); + let client_builder = Client::builder() + .request_config(RequestConfig::default().disable_retry()) + .check_supported_versions(false); - AppService::with_config(homeserver_url.as_ref(), server_name, registration, client_config).await + AppService::with_client_builder( + homeserver_url.as_ref(), + server_name, + registration, + client_builder, + ) + .await } #[async_test] diff --git a/crates/matrix-sdk/examples/autojoin.rs b/crates/matrix-sdk/examples/autojoin.rs index 490ce29d1..237532197 100644 --- a/crates/matrix-sdk/examples/autojoin.rs +++ b/crates/matrix-sdk/examples/autojoin.rs @@ -1,13 +1,9 @@ use std::{env, process::exit}; use matrix_sdk::{ - config::{ClientConfig, SyncSettings}, - room::Room, - ruma::events::room::member::StrippedRoomMemberEvent, - Client, + config::SyncSettings, room::Room, ruma::events::room::member::StrippedRoomMemberEvent, Client, }; use tokio::time::{sleep, Duration}; -use url::Url; async fn on_stripped_state_member( room_member: StrippedRoomMemberEvent, @@ -44,11 +40,9 @@ async fn login_and_sync( homeserver_url: String, username: &str, password: &str, -) -> Result<(), matrix_sdk::Error> { - #[cfg(not(any(feature = "sled_state_store", feature = "indexeddb_state_store")))] - let client_config = ClientConfig::new(); - #[cfg(any(feature = "sled_state_store", feature = "indexeddb_state_store"))] - let mut client_config = ClientConfig::new(); +) -> anyhow::Result<()> { + #[allow(unused_mut)] + let mut client_builder = Client::builder().homeserver_url(homeserver_url); #[cfg(feature = "sled_state_store")] { @@ -56,17 +50,16 @@ async fn login_and_sync( let mut home = dirs::home_dir().expect("no home directory found"); home.push("autojoin_bot"); let state_store = matrix_sdk_sled::StateStore::open_with_path(home)?; - client_config = client_config.state_store(Box::new(state_store)); + client_builder = client_builder.state_store(Box::new(state_store)); } #[cfg(feature = "indexeddb_state_store")] { let state_store = matrix_sdk_indexeddb::StateStore::open(); - client_config = client_config.state_store(Box::new(state_store)); + client_builder = client_builder.state_store(Box::new(state_store)); } - let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); - let client = Client::with_config(homeserver_url, client_config).await.unwrap(); + let client = client_builder.build().await?; client.login(username, password, None, Some("autojoin bot")).await?; @@ -80,7 +73,7 @@ async fn login_and_sync( } #[tokio::main] -async fn main() -> Result<(), matrix_sdk::Error> { +async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let (homeserver_url, username, password) = diff --git a/crates/matrix-sdk/examples/command_bot.rs b/crates/matrix-sdk/examples/command_bot.rs index 3028a1f37..747602983 100644 --- a/crates/matrix-sdk/examples/command_bot.rs +++ b/crates/matrix-sdk/examples/command_bot.rs @@ -1,14 +1,13 @@ use std::{env, process::exit}; use matrix_sdk::{ - config::{ClientConfig, SyncSettings}, + config::SyncSettings, room::Room, ruma::events::room::message::{ MessageType, RoomMessageEventContent, SyncRoomMessageEvent, TextMessageEventContent, }, Client, }; -use url::Url; async fn on_room_message(event: SyncRoomMessageEvent, room: Room) { if let Room::Joined(room) = room { @@ -36,11 +35,9 @@ async fn login_and_sync( homeserver_url: String, username: String, password: String, -) -> Result<(), matrix_sdk::Error> { - #[cfg(not(any(feature = "sled_state_store", feature = "indexeddb_state_store")))] - let client_config = ClientConfig::new(); - #[cfg(any(feature = "sled_state_store", feature = "indexeddb_state_store"))] - let mut client_config = ClientConfig::new(); +) -> anyhow::Result<()> { + #[allow(unused_mut)] + let mut client_builder = Client::builder().homeserver_url(homeserver_url); #[cfg(feature = "sled_state_store")] { @@ -48,19 +45,16 @@ async fn login_and_sync( let mut home = dirs::home_dir().expect("no home directory found"); home.push("party_bot"); let state_store = matrix_sdk_sled::StateStore::open_with_path(home)?; - client_config = client_config.state_store(Box::new(state_store)); + client_builder = client_builder.state_store(Box::new(state_store)); } #[cfg(feature = "indexeddb_state_store")] { let state_store = matrix_sdk_indexeddb::StateStore::open(); - client_config = client_config.state_store(Box::new(state_store)); + client_builder = client_builder.state_store(Box::new(state_store)); } - let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); - // create a new Client with the given homeserver url and config - let client = Client::with_config(homeserver_url, client_config).await.unwrap(); - + let client = client_builder.build().await.unwrap(); client.login(&username, &password, None, Some("command bot")).await?; println!("logged in as {}", username); @@ -84,7 +78,7 @@ async fn login_and_sync( } #[tokio::main] -async fn main() -> Result<(), matrix_sdk::Error> { +async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let (homeserver_url, username, password) = diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs new file mode 100644 index 000000000..1755d84fd --- /dev/null +++ b/crates/matrix-sdk/src/client/builder.rs @@ -0,0 +1,401 @@ +use std::sync::Arc; + +use matrix_sdk_base::{locks::RwLock, store::StoreConfig, BaseClient, StateStore}; +use ruma::{api::client::discover::discover_homeserver, UserId}; +use thiserror::Error; +use url::Url; + +use super::{Client, ClientInner}; +use crate::{ + config::RequestConfig, + http_client::{HttpClient, HttpSend, HttpSettings}, + HttpError, +}; + +/// Builder that allows creating and configuring various parts of a [`Client`]. +/// +/// When setting the `StateStore` it is up to the user to open/connect +/// the storage backend before client creation. +/// +/// # Example +/// +/// ``` +/// use matrix_sdk::Client; +/// // To pass all the request through mitmproxy set the proxy and disable SSL +/// // verification +/// +/// let client_builder = Client::builder() +/// .proxy("http://localhost:8080")? +/// .disable_ssl_verification(); +/// ``` +/// +/// # Example for using a custom http client +/// +/// Note: setting a custom http client will ignore `user_agent`, `proxy`, and +/// `disable_ssl_verification` - you'd need to set these yourself if you want +/// them. +/// +/// ``` +/// use matrix_sdk::Client; +/// use std::sync::Arc; +/// +/// // setting up a custom http client +/// let reqwest_builder = reqwest::ClientBuilder::new() +/// .https_only(true) +/// .no_proxy() +/// .user_agent("MyApp/v3.0"); +/// +/// let client_builder = Client::builder() +/// .http_client(Arc::new(reqwest_builder.build()?)); +/// # anyhow::Ok(()) +/// ``` +#[must_use] +#[derive(Debug)] +pub struct ClientBuilder { + homeserver_cfg: Option, + http_cfg: Option, + store_config: StoreConfig, + request_config: RequestConfig, + respect_login_well_known: bool, + appservice_mode: bool, + check_supported_versions: bool, +} + +impl ClientBuilder { + pub(crate) fn new() -> Self { + Self { + homeserver_cfg: None, + http_cfg: None, + store_config: Default::default(), + request_config: Default::default(), + respect_login_well_known: true, + appservice_mode: false, + check_supported_versions: true, + } + } + + /// Set the homeserver URL to use. + /// + /// This method is mutually exclusive with [`user_id()`][Self::user_id], if + /// you set both whatever was set last will be used. + pub fn homeserver_url(mut self, url: impl AsRef) -> Self { + self.homeserver_cfg = Some(HomeserverConfig::Url(url.as_ref().to_owned())); + self + } + + /// Set the user ID to discover the homeserver from. + /// + /// This method is mutually exclusive with + /// [`homeserver_url()`][Self::homeserver_url], if you set both whatever was + /// set last will be used. + pub fn user_id(mut self, user_id: &UserId) -> Self { + self.homeserver_cfg = Some(HomeserverConfig::UserId(user_id.to_owned())); + self + } + + /// Create a new `ClientConfig` with the given [`StoreConfig`]. + /// + /// The easiest way to get a [`StoreConfig`] is to use the + /// [`make_store_config`] method from the [`store`] module or directly from + /// one of the store crates. + /// + /// # Arguments + /// + /// * `store_config` - The configuration of the store. + /// + /// # Example + /// + /// ``` + /// # use matrix_sdk_base::store::MemoryStore; + /// # let custom_state_store = Box::new(MemoryStore::new()); + /// use matrix_sdk::{Client, config::StoreConfig}; + /// + /// let store_config = StoreConfig::new().state_store(custom_state_store); + /// let client_builder = Client::builder().store_config(store_config); + /// ``` + /// [`make_store_config`]: crate::store::make_store_config + /// [`store`]: crate::store + pub fn store_config(mut self, store_config: StoreConfig) -> Self { + self.store_config = store_config; + self + } + + /// Set a custom implementation of a `StateStore`. + /// + /// The state store should be opened before being set. + pub fn state_store(mut self, store: Box) -> Self { + self.store_config = self.store_config.state_store(store); + self + } + + /// Set a custom implementation of a `CryptoStore`. + /// + /// The crypto store should be opened before being set. + #[cfg(feature = "encryption")] + pub fn crypto_store( + mut self, + store: Box, + ) -> Self { + self.store_config = self.store_config.crypto_store(store); + self + } + + /// Update the client's homeserver URL with the discovery information + /// present in the login response, if any. + pub fn respect_login_well_known(mut self, value: bool) -> Self { + self.respect_login_well_known = value; + self + } + + /// Set the default timeout, fail and retry behavior for all HTTP requests. + pub fn request_config(mut self, request_config: RequestConfig) -> Self { + self.request_config = request_config; + self + } + + /// Set the proxy through which all the HTTP requests should go. + /// + /// Note, only HTTP proxies are supported. + /// + /// # Arguments + /// + /// * `proxy` - The HTTP URL of the proxy. + /// + /// # Example + /// + /// ``` + /// # futures::executor::block_on(async { + /// use matrix_sdk::{Client, config::ClientConfig}; + /// + /// let client_config = ClientConfig::new() + /// .proxy("http://localhost:8080")?; + /// + /// # Result::<_, matrix_sdk::Error>::Ok(()) + /// # }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub fn proxy(mut self, proxy: impl AsRef) -> Self { + self.http_settings().proxy = Some(proxy.as_ref().to_owned()); + self + } + + /// Disable SSL verification for the HTTP requests. + #[cfg(not(target_arch = "wasm32"))] + pub fn disable_ssl_verification(mut self) -> Self { + self.http_settings().disable_ssl_verification = true; + self + } + + /// Set a custom HTTP user agent for the client. + #[cfg(not(target_arch = "wasm32"))] + pub fn user_agent(mut self, user_agent: impl AsRef) -> Self { + self.http_settings().user_agent = Some(user_agent.as_ref().to_owned()); + self + } + + /// Specify an HTTP client to handle sending requests and receiving + /// responses. + /// + /// Any type that implements the `HttpSend` trait can be used to send / + /// receive `http` types. + /// + /// This method is mutually exclusive with + /// [`user_agent()`][Self::user_agent], + pub fn http_client(mut self, client: Arc) -> Self { + self.http_cfg = Some(HttpConfig::Custom(client)); + self + } + + /// Puts the client into application service mode + /// + /// This is low-level functionality. For an high-level API check the + /// `matrix_sdk_appservice` crate. + #[doc(hidden)] + #[cfg(feature = "appservice")] + pub fn appservice_mode(mut self) -> Self { + self.appservice_mode = true; + self + } + + /// All outgoing http requests will have a GET query key-value appended with + /// `user_id` being the key and the `user_id` from the `Session` being + /// the value. Will error if there's no `Session`. This is called + /// [identity assertion] in the Matrix Application Service Spec + /// + /// [identity assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion + #[doc(hidden)] + #[cfg(feature = "appservice")] + pub fn assert_identity(mut self) -> Self { + self.request_config.assert_identity = true; + self + } + + /// Specify whether the homeserver functionality should be checked through a + /// get_supported_versions request. + /// + /// This is helpful for test code that doesn't care to mock that endpoint. + #[doc(hidden)] + pub fn check_supported_versions(mut self, value: bool) -> Self { + self.check_supported_versions = value; + self + } + + #[cfg(not(target_arch = "wasm32"))] + fn http_settings(&mut self) -> &mut HttpSettings { + self.http_cfg.get_or_insert_with(Default::default).settings() + } + + /// Create a [`Client`] with the options set on this builder. + /// + /// # Errors + /// + /// This method can fail for two general reasons: + /// + /// * Invalid input: a missing or invalid homeserver URL or invalid proxy + /// URL + /// * HTTP error: If you supplied a user ID instead of a homeserver URL, a + /// server discovery request is made which can fail; if you didn't set + /// [`check_supported_versions(false)`][Self::check_supported_versions], + /// that amounts to another request that can fail + pub async fn build(self) -> Result { + let homeserver_cfg = self.homeserver_cfg.ok_or(ClientBuildError::MissingHomeserver)?; + + let inner_http_client = match self.http_cfg.unwrap_or_default() { + #[allow(unused_mut)] + HttpConfig::Settings(mut settings) => { + #[cfg(not(target_arch = "wasm32"))] + { + settings.timeout = self.request_config.timeout; + } + + Arc::new(settings.make_client()?) + } + HttpConfig::Custom(c) => c, + }; + + let base_client = BaseClient::with_store_config(self.store_config); + + let mk_http_client = |homeserver| { + HttpClient::new( + inner_http_client.clone(), + homeserver, + base_client.session().clone(), + self.request_config, + ) + }; + + let homeserver = match homeserver_cfg { + HomeserverConfig::Url(url) => url, + HomeserverConfig::UserId(user_id) => { + let homeserver = homeserver_from_user_id(&user_id)?; + let http_client = mk_http_client(Arc::new(RwLock::new(homeserver))); + let well_known = + http_client.send(discover_homeserver::Request::new(), None).await?; + + well_known.homeserver.base_url + } + }; + + let homeserver = Arc::new(RwLock::new(Url::parse(&homeserver)?)); + let http_client = mk_http_client(homeserver.clone()); + + let inner = Arc::new(ClientInner { + homeserver, + http_client, + base_client, + #[cfg(feature = "encryption")] + group_session_locks: Default::default(), + #[cfg(feature = "encryption")] + key_claim_lock: Default::default(), + members_request_locks: Default::default(), + typing_notice_times: Default::default(), + event_handlers: Default::default(), + event_handler_data: Default::default(), + notification_handlers: Default::default(), + appservice_mode: self.appservice_mode, + respect_login_well_known: self.respect_login_well_known, + sync_beat: event_listener::Event::new(), + }); + let client = Client { inner }; + + if self.check_supported_versions { + client.get_supported_versions().await?; + } + + Ok(client) + } +} + +fn homeserver_from_user_id(user_id: &UserId) -> Result { + let homeserver = format!("https://{}", user_id.server_name()); + #[allow(unused_mut)] + let mut result = Url::parse(homeserver.as_str())?; + // Mockito only knows how to test http endpoints: + // https://github.com/lipanski/mockito/issues/127 + #[cfg(test)] + let _ = result.set_scheme("http"); + Ok(result) +} + +#[derive(Debug)] +enum HomeserverConfig { + Url(String), + UserId(Box), +} + +#[derive(Debug)] +enum HttpConfig { + Settings(HttpSettings), + Custom(Arc), +} + +#[cfg(not(target_arch = "wasm32"))] +impl HttpConfig { + fn settings(&mut self) -> &mut HttpSettings { + match self { + Self::Settings(s) => s, + Self::Custom(_) => { + *self = Self::default(); + match self { + Self::Settings(s) => s, + Self::Custom(_) => unreachable!(), + } + } + } + } +} + +impl Default for HttpConfig { + fn default() -> Self { + Self::Settings(HttpSettings::default()) + } +} + +/// Errors that can happen in [`ClientBuilder::build`]. +#[derive(Debug, Error)] +pub enum ClientBuildError { + /// No homeserver or user ID was configured + #[error("no homeserver or user ID was configured")] + MissingHomeserver, + + /// An error encountered when trying to parse the homeserver url. + #[error(transparent)] + Url(#[from] url::ParseError), + + /// Error doing an HTTP request. + #[error(transparent)] + Http(#[from] HttpError), +} + +impl ClientBuildError { + /// Assert that a valid homeserver URL was given to the builder and no other + /// invalid options were specified, which means the only possible error + /// case is [`Self::Http`]. + #[doc(hidden)] + pub fn assert_valid_builder_args(self) -> HttpError { + match self { + ClientBuildError::Http(e) => e, + _ => unreachable!("homeserver URL was asserted to be valid"), + } + } +} diff --git a/crates/matrix-sdk/src/client.rs b/crates/matrix-sdk/src/client/mod.rs similarity index 95% rename from crates/matrix-sdk/src/client.rs rename to crates/matrix-sdk/src/client/mod.rs index 83fd8a0f4..0f06eaabe 100644 --- a/crates/matrix-sdk/src/client.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -44,7 +44,7 @@ use ruma::{ capabilities::{get_capabilities, Capabilities}, device::{delete_devices, get_devices}, directory::{get_public_rooms, get_public_rooms_filtered}, - discover::{discover_homeserver, get_supported_versions}, + discover::get_supported_versions, filter::{create_filter::v3::Request as FilterUploadRequest, FilterDefinition}, media::{create_content, get_content, get_content_thumbnail}, membership::{join_room_by_id, join_room_by_id_or_alias}, @@ -67,13 +67,17 @@ use url::Url; use crate::{ attachment::{AttachmentInfo, Thumbnail}, - config::{ClientConfig, RequestConfig}, + config::RequestConfig, error::{HttpError, HttpResult}, event_handler::{EventHandler, EventHandlerData, EventHandlerResult, EventKind, SyncEvent}, - http_client::{client_with_config, HttpClient}, + http_client::HttpClient, room, Account, Error, Result, }; +mod builder; + +pub use self::builder::{ClientBuildError, ClientBuilder}; + /// A conservative upload speed of 1Mbps const DEFAULT_UPLOAD_SPEED: u64 = 125_000; /// 5 min minimal upload request timeout, used to clamp the request timeout. @@ -139,7 +143,7 @@ pub(crate) struct ClientInner { appservice_mode: bool, /// Whether the client should update its homeserver URL with the discovery /// information present in the login response. - use_discovery_response: bool, + respect_login_well_known: bool, /// An event that can be listened on to wait for a successful sync. The /// event will only be fired if a sync loop is running. Can be used for /// synchronization, e.g. if we send out a request to create a room, we can @@ -161,53 +165,12 @@ impl Client { /// # Arguments /// /// * `homeserver_url` - The homeserver that the client should connect to. - pub async fn new(homeserver_url: Url) -> Result { - let config = ClientConfig::new(); - Client::with_config(homeserver_url, config).await - } - - /// Create a new [`Client`] for the given homeserver and use the given - /// configuration. - /// - /// # Arguments - /// - /// * `homeserver_url` - The homeserver that the client should connect to. - /// - /// * `config` - Configuration for the client. - pub async fn with_config(homeserver_url: Url, config: ClientConfig) -> Result { - let homeserver = Arc::new(RwLock::new(homeserver_url)); - - let client = if let Some(client) = config.client { - client - } else { - Arc::new(client_with_config(&config)?) - }; - - let base_client = BaseClient::with_store_config(config.store_config); - let session = base_client.session().clone(); - - let http_client = - HttpClient::new(client, homeserver.clone(), session, config.request_config); - - let inner = Arc::new(ClientInner { - homeserver, - http_client, - base_client, - #[cfg(feature = "encryption")] - group_session_locks: Default::default(), - #[cfg(feature = "encryption")] - key_claim_lock: Default::default(), - members_request_locks: Default::default(), - typing_notice_times: Default::default(), - event_handlers: Default::default(), - event_handler_data: Default::default(), - notification_handlers: Default::default(), - appservice_mode: config.appservice_mode, - use_discovery_response: config.use_discovery_response, - sync_beat: event_listener::Event::new(), - }); - - Ok(Self { inner }) + pub async fn new(homeserver_url: Url) -> Result { + Self::builder() + .homeserver_url(homeserver_url) + .build() + .await + .map_err(ClientBuildError::assert_valid_builder_args) } /// Create a new [`Client`] using homeserver auto discovery. @@ -240,37 +203,17 @@ impl Client { /// ``` /// /// [spec]: https://spec.matrix.org/unstable/client-server-api/#well-known-uri - pub async fn new_from_user_id(user_id: &UserId) -> Result { - let config = ClientConfig::new(); - Client::new_from_user_id_with_config(user_id, config).await + pub async fn new_from_user_id(user_id: &UserId) -> Result { + Self::builder() + .user_id(user_id) + .build() + .await + .map_err(ClientBuildError::assert_valid_builder_args) } - /// Create a new [`Client`] using homeserver auto discovery. - /// - /// This method will create a [`Client`] object that will attempt to - /// discover and configure the homeserver for the given user. Follows the - /// homeserver discovery directions described in the [spec]. - /// - /// # Arguments - /// - /// * `user_id` - The id of the user whose homeserver the client should - /// connect to. - /// - /// * `config` - Configuration for the client. - /// - /// [spec]: https://spec.matrix.org/unstable/client-server-api/#well-known-uri - pub async fn new_from_user_id_with_config( - user_id: &UserId, - config: ClientConfig, - ) -> Result { - let homeserver = Client::homeserver_from_user_id(user_id)?; - let client = Client::with_config(homeserver, config).await?; - - let well_known = client.discover_homeserver().await?; - let well_known = Url::parse(well_known.homeserver.base_url.as_ref())?; - client.set_homeserver(well_known).await; - client.get_supported_versions().await?; - Ok(client) + /// Create a new [`ClientBuilder`]. + pub fn builder() -> ClientBuilder { + ClientBuilder::new() } pub(crate) fn base_client(&self) -> &BaseClient { @@ -291,22 +234,6 @@ impl Client { self.base_client().mark_request_as_sent(request_id, response).await } - fn homeserver_from_user_id(user_id: &UserId) -> Result { - let homeserver = format!("https://{}", user_id.server_name()); - #[allow(unused_mut)] - let mut result = Url::parse(homeserver.as_str())?; - // Mockito only knows how to test http endpoints: - // https://github.com/lipanski/mockito/issues/127 - #[cfg(test)] - let _ = result.set_scheme("http"); - Ok(result) - } - - async fn discover_homeserver(&self) -> HttpResult { - self.send(discover_homeserver::Request::new(), Some(RequestConfig::new().disable_retry())) - .await - } - /// Change the homeserver URL used by this client. /// /// # Arguments @@ -472,7 +399,12 @@ impl Client { /// use serde::{Deserialize, Serialize}; /// /// # block_on(async { - /// # let client = Client::new(homeserver).await.unwrap(); + /// # let client = matrix_sdk::Client::builder() + /// # .homeserver_url(homeserver) + /// # .check_supported_versions(false) + /// # .build() + /// # .await + /// # .unwrap(); /// client /// .register_event_handler( /// |ev: SyncRoomMessageEvent, room: Room, client: Client| async move { @@ -586,7 +518,12 @@ impl Client { /// # fn obtain_gui_handle() -> SomeType { SomeType } /// # let homeserver = url::Url::parse("http://localhost:8080").unwrap(); /// # block_on(async { - /// # let client = matrix_sdk::Client::new(homeserver).await.unwrap(); + /// # let client = matrix_sdk::Client::builder() + /// # .homeserver_url(homeserver) + /// # .check_supported_versions(false) + /// # .build() + /// # .await + /// # .unwrap(); /// /// // Handle used to send messages to the UI part of the app /// let my_gui_handle: SomeType = obtain_gui_handle(); @@ -1135,7 +1072,7 @@ impl Client { /// /// * `response` - A successful login response. async fn receive_login_response(&self, response: &login::v3::Response) -> Result<()> { - if self.inner.use_discovery_response { + if self.inner.respect_login_well_known { if let Some(well_known) = &response.well_known { if let Ok(homeserver) = Url::parse(&well_known.homeserver.base_url) { self.set_homeserver(homeserver).await; @@ -2325,6 +2262,7 @@ impl Client { }) } } + // mockito (the http mocking library) is not supported for wasm32 #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod test { @@ -2367,26 +2305,38 @@ pub(crate) mod test { mxc_uri, room_id, thirdparty, uint, user_id, TransactionId, UserId, }; use serde_json::json; + use url::Url; - use super::{Client, Session, Url}; + use super::{Client, ClientBuilder, Session}; use crate::{ attachment::{ AttachmentConfig, AttachmentInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo, Thumbnail, }, - config::{ClientConfig, RequestConfig, SyncSettings}, + config::{RequestConfig, SyncSettings}, HttpError, RoomMember, }; + fn test_client_builder() -> ClientBuilder { + let homeserver = Url::parse(&mockito::server_url()).unwrap(); + Client::builder().homeserver_url(homeserver).check_supported_versions(false) + } + + async fn no_retry_test_client() -> Client { + test_client_builder() + .request_config(RequestConfig::new().disable_retry()) + .build() + .await + .unwrap() + } + pub(crate) async fn logged_in_client() -> Client { let session = Session { access_token: "1234".to_owned(), user_id: user_id!("@example:localhost").to_owned(), device_id: device_id!("DEVICEID").to_owned(), }; - let homeserver = url::Url::parse(&mockito::server_url()).unwrap(); - let config = ClientConfig::new().request_config(RequestConfig::new().disable_retry()); - let client = Client::with_config(homeserver, config).await.unwrap(); + let client = no_retry_test_client().await; client.restore_login(session).await.unwrap(); client @@ -2394,12 +2344,8 @@ pub(crate) mod test { #[async_test] async fn set_homeserver() { + let client = no_retry_test_client().await; let homeserver = Url::from_str("http://example.com/").unwrap(); - - let client = Client::new(homeserver).await.unwrap(); - - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - client.set_homeserver(homeserver.clone()).await; assert_eq!(client.homeserver().await, homeserver); @@ -2450,8 +2396,7 @@ pub(crate) mod test { #[async_test] async fn login() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - - let client = Client::new(homeserver.clone()).await.unwrap(); + let client = no_retry_test_client().await; let _m_types = mock("GET", "/_matrix/client/r0/login") .with_status(200) @@ -2482,10 +2427,7 @@ pub(crate) mod test { #[async_test] async fn login_with_discovery() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let config = ClientConfig::new().use_discovery_response(); - - let client = Client::with_config(homeserver, config).await.unwrap(); + let client = no_retry_test_client().await; let _m_login = mock("POST", "/_matrix/client/r0/login") .with_status(200) @@ -2502,10 +2444,7 @@ pub(crate) mod test { #[async_test] async fn login_no_discovery() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let config = ClientConfig::new().use_discovery_response(); - - let client = Client::with_config(homeserver.clone(), config).await.unwrap(); + let client = no_retry_test_client().await; let _m_login = mock("POST", "/_matrix/client/r0/login") .with_status(200) @@ -2517,7 +2456,7 @@ pub(crate) mod test { let logged_in = client.logged_in().await; assert!(logged_in, "Client should be logged in"); - assert_eq!(client.homeserver().await, homeserver); + assert_eq!(client.homeserver().await, Url::parse(&mockito::server_url()).unwrap()); } #[cfg(feature = "sso_login")] @@ -2529,7 +2468,7 @@ pub(crate) mod test { .create(); let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let client = Client::new(homeserver).await.unwrap(); + let client = no_retry_test_client().await; let idp = crate::client::get_login_types::v3::IdentityProvider::new( "some-id".to_owned(), "idp-name".to_owned(), @@ -2564,9 +2503,7 @@ pub(crate) mod test { #[async_test] async fn login_with_sso_token() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - - let client = Client::new(homeserver).await.unwrap(); + let client = no_retry_test_client().await; let _m = mock("GET", "/_matrix/client/r0/login") .with_status(200) @@ -2610,7 +2547,6 @@ pub(crate) mod test { #[async_test] async fn test_join_leave_room() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) @@ -2633,8 +2569,7 @@ pub(crate) mod test { assert!(room.is_some()); // test store reloads with correct room state from the state store - let config = ClientConfig::new().request_config(RequestConfig::new().disable_retry()); - let joined_client = Client::with_config(homeserver, config).await.unwrap(); + let joined_client = no_retry_test_client().await; joined_client.restore_login(session).await.unwrap(); // joined room reloaded from state store @@ -2694,9 +2629,7 @@ pub(crate) mod test { #[async_test] async fn login_error() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let config = ClientConfig::default().request_config(RequestConfig::new().disable_retry()); - let client = Client::with_config(homeserver, config).await.unwrap(); + let client = no_retry_test_client().await; let _m = mock("POST", "/_matrix/client/r0/login") .with_status(403) @@ -2724,8 +2657,7 @@ pub(crate) mod test { #[async_test] async fn register_error() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let client = Client::new(homeserver).await.unwrap(); + let client = no_retry_test_client().await; let _m = mock("POST", Matcher::Regex(r"^/_matrix/client/r0/register\?.*$".to_owned())) .with_status(403) @@ -2868,8 +2800,7 @@ pub(crate) mod test { #[async_test] async fn room_search_all() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let client = Client::new(homeserver).await.unwrap(); + let client = no_retry_test_client().await; let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/publicRooms".to_owned())) .with_status(200) @@ -3554,8 +3485,7 @@ pub(crate) mod test { #[async_test] async fn delete_devices() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let client = Client::new(homeserver).await.unwrap(); + let client = no_retry_test_client().await; let _m = mock("POST", "/_matrix/client/r0/delete_devices") .with_status(401) @@ -3610,10 +3540,13 @@ pub(crate) mod test { #[async_test] async fn retry_limit_http_requests() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let config = ClientConfig::default().request_config(RequestConfig::new().retry_limit(3)); - assert!(config.request_config.retry_limit.unwrap() == 3); - let client = Client::with_config(homeserver, config).await.unwrap(); + let client = test_client_builder() + .request_config(RequestConfig::new().retry_limit(3)) + .build() + .await + .unwrap(); + + assert!(client.inner.http_client.request_config.retry_limit.unwrap() == 3); let m = mock("POST", "/_matrix/client/r0/login").with_status(501).expect(3).create(); @@ -3626,13 +3559,15 @@ pub(crate) mod test { #[async_test] async fn retry_timeout_http_requests() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); // Keep this timeout small so that the test doesn't take long let retry_timeout = Duration::from_secs(5); - let config = ClientConfig::default() - .request_config(RequestConfig::new().retry_timeout(retry_timeout)); - assert!(config.request_config.retry_timeout.unwrap() == retry_timeout); - let client = Client::with_config(homeserver, config).await.unwrap(); + let client = test_client_builder() + .request_config(RequestConfig::new().retry_timeout(retry_timeout)) + .build() + .await + .unwrap(); + + assert!(client.inner.http_client.request_config.retry_timeout.unwrap() == retry_timeout); let m = mock("POST", "/_matrix/client/r0/login").with_status(501).expect_at_least(2).create(); @@ -3646,8 +3581,7 @@ pub(crate) mod test { #[async_test] async fn short_retry_initial_http_requests() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); - let client = Client::new(homeserver).await.unwrap(); + let client = test_client_builder().build().await.unwrap(); let m = mock("POST", "/_matrix/client/r0/login").with_status(501).expect_at_least(3).create(); @@ -3759,7 +3693,6 @@ pub(crate) mod test { #[async_test] async fn test_state_event_getting() { - let homeserver = Url::from_str(&mockito::server_url()).unwrap(); let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); let session = Session { @@ -3828,8 +3761,11 @@ pub(crate) mod test { .with_body(sync.to_string()) .create(); - let config = ClientConfig::default().request_config(RequestConfig::new().retry_limit(3)); - let client = Client::with_config(homeserver.clone(), config).await.unwrap(); + let client = test_client_builder() + .request_config(RequestConfig::new().retry_limit(3)) + .build() + .await + .unwrap(); client.restore_login(session.clone()).await.unwrap(); let room = client.get_joined_room(room_id); diff --git a/crates/matrix-sdk/src/config/client.rs b/crates/matrix-sdk/src/config/client.rs deleted file mode 100644 index 459fe08be..000000000 --- a/crates/matrix-sdk/src/config/client.rs +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#[allow(unused_imports)] -use std::{ - fmt::{self, Debug}, - path::Path, - sync::Arc, -}; - -use http::header::InvalidHeaderValue; -use matrix_sdk_base::StateStore; - -use crate::{ - config::{RequestConfig, StoreConfig}, - HttpSend, Result, -}; - -/// Configuration for the creation of the `Client`. -/// -/// When setting the `StateStore` it is up to the user to open/connect -/// the storage backend before client creation. -/// -/// # Example -/// -/// ``` -/// use matrix_sdk::config::ClientConfig; -/// // To pass all the request through mitmproxy set the proxy and disable SSL -/// // verification -/// -/// # futures::executor::block_on(async { -/// let client_config = ClientConfig::new() -/// .proxy("http://localhost:8080")? -/// .disable_ssl_verification(); -/// # matrix_sdk::Result::<()>::Ok(()) -/// # }); -/// ``` -/// -/// # Example for using a custom client -/// Note: setting a custom client will ignore `user_agent`, `proxy`, and -/// `disable_ssl_verification` - you'd need to set these yourself if you -/// want them. -/// -/// ``` -/// use matrix_sdk::config::ClientConfig; -/// use reqwest::ClientBuilder; -/// use std::sync::Arc; -/// -/// // setting up a custom builder -/// let builder = ClientBuilder::new() -/// .https_only(true) -/// .no_proxy() -/// .user_agent("MyApp/v3.0"); -/// -/// # futures::executor::block_on(async { -/// let client_config = ClientConfig::new() -/// .client(Arc::new(builder.build()?)); -/// # matrix_sdk::Result::<()>::Ok(()) -/// # }); -/// ``` -#[derive(Default)] -pub struct ClientConfig { - #[cfg(not(target_arch = "wasm32"))] - pub(crate) proxy: Option, - pub(crate) user_agent: Option, - pub(crate) disable_ssl_verification: bool, - pub(crate) store_config: StoreConfig, - pub(crate) request_config: RequestConfig, - pub(crate) client: Option>, - pub(crate) appservice_mode: bool, - pub(crate) use_discovery_response: bool, -} - -#[cfg(not(tarpaulin_include))] -impl Debug for ClientConfig { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut res = fmt.debug_struct("ClientConfig"); - - #[cfg(not(target_arch = "wasm32"))] - let res = res.field("proxy", &self.proxy); - - res.field("user_agent", &self.user_agent) - .field("disable_ssl_verification", &self.disable_ssl_verification) - .field("request_config", &self.request_config) - .finish() - } -} - -impl ClientConfig { - /// Create a new default `ClientConfig`. - pub fn new() -> Self { - Self::default() - } - - /// Create a new `ClientConfig` with the given [`StoreConfig`]. - /// - /// The easiest way to get a [`StoreConfig`] is to use the - /// [`make_store_config`] method from the [`store`] module or directly from - /// one of the store crates. - /// - /// # Arguments - /// - /// * `store_config` - The configuration of the store. - /// - /// # Example - /// - /// ``` - /// # futures::executor::block_on(async { - /// # use matrix_sdk_base::store::MemoryStore; - /// # let custom_state_store = Box::new(MemoryStore::new()); - /// use matrix_sdk::{Client, config::{ClientConfig, StoreConfig}}; - /// - /// let store_config = StoreConfig::new().state_store(custom_state_store); - /// let client_config = ClientConfig::new() - /// .store_config(store_config) - /// .use_discovery_response(); - /// - /// # Result::<_, matrix_sdk::Error>::Ok(()) - /// # }); - /// ``` - /// [`make_store_config`]: crate::store::make_store_config - /// [`store`]: crate::store - pub fn store_config(mut self, store_config: StoreConfig) -> Self { - self.store_config = store_config; - self - } - - /// Set the proxy through which all the HTTP requests should go. - /// - /// Note, only HTTP proxies are supported. - /// - /// # Arguments - /// - /// * `proxy` - The HTTP URL of the proxy. - /// - /// # Example - /// - /// ``` - /// # futures::executor::block_on(async { - /// use matrix_sdk::{Client, config::ClientConfig}; - /// - /// let client_config = ClientConfig::new() - /// .proxy("http://localhost:8080")?; - /// - /// # Result::<_, matrix_sdk::Error>::Ok(()) - /// # }); - /// ``` - #[cfg(not(target_arch = "wasm32"))] - pub fn proxy(mut self, proxy: &str) -> Result { - self.proxy = Some(reqwest::Proxy::all(proxy)?); - Ok(self) - } - - /// Disable SSL verification for the HTTP requests. - #[must_use] - pub fn disable_ssl_verification(mut self) -> Self { - self.disable_ssl_verification = true; - self - } - - /// Set a custom HTTP user agent for the client. - pub fn user_agent(mut self, user_agent: &str) -> Result { - self.user_agent = Some(user_agent.to_owned()); - Ok(self) - } - - /// Set a custom implementation of a `StateStore`. - /// - /// The state store should be opened before being set. - pub fn state_store(mut self, store: Box) -> Self { - self.store_config = self.store_config.state_store(store); - self - } - - /// Set the default timeout, fail and retry behavior for all HTTP requests. - #[must_use] - pub fn request_config(mut self, request_config: RequestConfig) -> Self { - self.request_config = request_config; - self - } - - /// Get the [`RequestConfig`] - pub fn get_request_config(&self) -> &RequestConfig { - &self.request_config - } - - /// Specify a client to handle sending requests and receiving responses. - /// - /// Any type that implements the `HttpSend` trait can be used to - /// send/receive `http` types. - #[must_use] - pub fn client(mut self, client: Arc) -> Self { - self.client = Some(client); - self - } - - /// Puts the client into application service mode - /// - /// This is low-level functionality. For an high-level API check the - /// `matrix_sdk_appservice` crate. - #[cfg(feature = "appservice")] - #[must_use] - pub fn appservice_mode(mut self) -> Self { - self.appservice_mode = true; - self - } - - /// Set a custom implementation of a `CryptoStore`. - /// - /// The crypto store should be opened before being set. - #[cfg(feature = "encryption")] - #[must_use] - pub fn crypto_store( - mut self, - store: Box, - ) -> Self { - self.store_config = self.store_config.crypto_store(store); - self - } - - /// Update the client's homeserver URL with the discovery information - /// present in the login response, if any. - #[must_use] - pub fn use_discovery_response(mut self) -> Self { - self.use_discovery_response = true; - self - } - - /// All outgoing http requests will have a GET query key-value appended with - /// `user_id` being the key and the `user_id` from the `Session` being - /// the value. Will error if there's no `Session`. This is called - /// [identity assertion] in the Matrix Application Service Spec - /// - /// [identity assertion]: https://spec.matrix.org/unstable/application-service-api/#identity-assertion - #[cfg(feature = "appservice")] - #[must_use] - pub fn assert_identity(mut self) -> Self { - self.request_config.assert_identity = true; - self - } -} diff --git a/crates/matrix-sdk/src/config/mod.rs b/crates/matrix-sdk/src/config/mod.rs index 39ade13e7..4716b25f7 100644 --- a/crates/matrix-sdk/src/config/mod.rs +++ b/crates/matrix-sdk/src/config/mod.rs @@ -14,11 +14,9 @@ //! Configuration to change the behaviour of the [`Client`][crate::Client]. -mod client; mod request; mod sync; -pub use client::ClientConfig; pub use matrix_sdk_base::store::StoreConfig; pub use request::RequestConfig; pub use sync::SyncSettings; diff --git a/crates/matrix-sdk/src/config/request.rs b/crates/matrix-sdk/src/config/request.rs index 2d9c03686..54eedeb40 100644 --- a/crates/matrix-sdk/src/config/request.rs +++ b/crates/matrix-sdk/src/config/request.rs @@ -17,7 +17,7 @@ use std::{ time::Duration, }; -const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); +use crate::http_client::DEFAULT_REQUEST_TIMEOUT; /// Configuration for requests the `Client` makes. /// diff --git a/crates/matrix-sdk/src/docs/encryption.md b/crates/matrix-sdk/src/docs/encryption.md index 5f6c2db07..34de706dd 100644 --- a/crates/matrix-sdk/src/docs/encryption.md +++ b/crates/matrix-sdk/src/docs/encryption.md @@ -168,7 +168,7 @@ to work. 2. To persist the encryption keys, you can use one of the provided backend constructors as described in the documentation of the [`store`] module or you can provide your own backend that implements [`CryptoStore`] in a -[`StoreConfig`] or via [`ClientConfig::crypto_store()`]. +[`StoreConfig`] or via [`ClientBuilder::crypto_store()`]. ## Restoring a client @@ -233,4 +233,4 @@ is **not** supported using the default store. [`store`]: crate::store [`CryptoStore`]: matrix_sdk_base::crypto::store::CryptoStore [`StoreConfig`]: crate::config::StoreConfig -[`ClientConfig::crypto_store()`]: crate::config::ClientConfig::crypto_store() +[`ClientBuilder::crypto_store()`]: crate::ClientBuilder::crypto_store() diff --git a/crates/matrix-sdk/src/http_client.rs b/crates/matrix-sdk/src/http_client.rs index 50e510a1e..89782f93f 100644 --- a/crates/matrix-sdk/src/http_client.rs +++ b/crates/matrix-sdk/src/http_client.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{convert::TryFrom, fmt::Debug, sync::Arc}; +use std::{convert::TryFrom, fmt::Debug, sync::Arc, time::Duration}; use bytes::{Bytes, BytesMut}; use http::Response as HttpResponse; @@ -25,11 +25,9 @@ use ruma::api::{ use tracing::trace; use url::Url; -use crate::{ - config::{ClientConfig, RequestConfig}, - error::HttpError, - Session, -}; +use crate::{config::RequestConfig, error::HttpError, Session}; + +pub(crate) const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); /// Abstraction around the http layer. The allows implementors to use different /// http libraries. @@ -203,33 +201,58 @@ impl HttpClient { } } -/// Build a client with the specified configuration. -pub(crate) fn client_with_config(config: &ClientConfig) -> Result { - let http_client = reqwest::Client::builder(); - +#[derive(Debug)] +pub(crate) struct HttpSettings { #[cfg(not(target_arch = "wasm32"))] - let http_client = { - let http_client = if config.disable_ssl_verification { - http_client.danger_accept_invalid_certs(true) - } else { - http_client + pub(crate) disable_ssl_verification: bool, + #[cfg(not(target_arch = "wasm32"))] + pub(crate) proxy: Option, + #[cfg(not(target_arch = "wasm32"))] + pub(crate) user_agent: Option, + #[cfg(not(target_arch = "wasm32"))] + pub(crate) timeout: Duration, +} + +#[allow(clippy::derivable_impls)] +impl Default for HttpSettings { + fn default() -> Self { + Self { + #[cfg(not(target_arch = "wasm32"))] + disable_ssl_verification: false, + #[cfg(not(target_arch = "wasm32"))] + proxy: None, + #[cfg(not(target_arch = "wasm32"))] + user_agent: None, + #[cfg(not(target_arch = "wasm32"))] + timeout: DEFAULT_REQUEST_TIMEOUT, + } + } +} + +impl HttpSettings { + /// Build a client with the specified configuration. + pub(crate) fn make_client(&self) -> Result { + #[allow(unused_mut)] + let mut http_client = reqwest::Client::builder(); + + #[cfg(not(target_arch = "wasm32"))] + { + if self.disable_ssl_verification { + http_client = http_client.danger_accept_invalid_certs(true) + } + + if let Some(p) = &self.proxy { + http_client = http_client.proxy(reqwest::Proxy::all(p.as_str())?); + } + + let user_agent = + self.user_agent.clone().unwrap_or_else(|| "matrix-rust-sdk".to_owned()); + + http_client = http_client.user_agent(user_agent).timeout(self.timeout); }; - let http_client = match &config.proxy { - Some(p) => http_client.proxy(p.clone()), - None => http_client, - }; - - let user_agent = config.user_agent.clone().unwrap_or_else(|| "matrix-rust-sdk".to_owned()); - - http_client.user_agent(user_agent).timeout(config.request_config.timeout) - }; - - #[cfg(target_arch = "wasm32")] - #[allow(unused)] - let _ = config; - - Ok(http_client.build()?) + Ok(http_client.build()?) + } } async fn response_to_http_response( diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index bb0c55640..ddc9bc127 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -57,7 +57,7 @@ mod sync; pub mod encryption; pub use account::Account; -pub use client::{Client, LoopCtrl}; +pub use client::{Client, ClientBuildError, ClientBuilder, LoopCtrl}; #[cfg(feature = "image_proc")] pub use error::ImageError; pub use error::{Error, HttpError, HttpResult, Result}; diff --git a/crates/matrix-sdk/src/store.rs b/crates/matrix-sdk/src/store.rs index 60d123463..422b7d3d7 100644 --- a/crates/matrix-sdk/src/store.rs +++ b/crates/matrix-sdk/src/store.rs @@ -24,10 +24,10 @@ //! implementation for WebAssembly. //! //! Both options provide a `make_store_config` convenience method to create a -//! [`StoreConfig`] for [`ClientConfig::store_config()`]. +//! [`StoreConfig`] for [`ClientBuilder::store_config()`]. //! //! [`StoreConfig`]: crate::config::StoreConfig -//! [`ClientConfig::store_config()`]: crate::config::ClientConfig::store_config() +//! [`ClientBuilder::store_config()`]: crate::ClientBuilder::store_config #[cfg(any(feature = "indexeddb_state_store", feature = "indexeddb_cryptostore"))] pub use matrix_sdk_indexeddb::*; From 81d07d886c4b8226ed94b35defb3c668398fb703 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 11 Mar 2022 18:30:29 +0100 Subject: [PATCH 4/6] Rename Client::{new_from_user_id => for_user_id} --- crates/matrix-sdk/README.md | 2 +- crates/matrix-sdk/src/client/builder.rs | 8 ++++---- crates/matrix-sdk/src/client/mod.rs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 559794933..9dd733517 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -34,7 +34,7 @@ use matrix_sdk::{ #[tokio::main] async fn main() -> Result<()> { let alice = user_id!("@alice:example.org"); - let client = Client::new_from_user_id(alice).await?; + let client = Client::for_user_id(alice).await?; // First we need to log in. client.login(alice, "password", None, None).await?; diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index 1755d84fd..6152c5cfe 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -25,7 +25,7 @@ use crate::{ /// // verification /// /// let client_builder = Client::builder() -/// .proxy("http://localhost:8080")? +/// .proxy("http://localhost:8080") /// .disable_ssl_verification(); /// ``` /// @@ -165,10 +165,10 @@ impl ClientBuilder { /// /// ``` /// # futures::executor::block_on(async { - /// use matrix_sdk::{Client, config::ClientConfig}; + /// use matrix_sdk::Client; /// - /// let client_config = ClientConfig::new() - /// .proxy("http://localhost:8080")?; + /// let client_config = Client::builder() + /// .proxy("http://localhost:8080"); /// /// # Result::<_, matrix_sdk::Error>::Ok(()) /// # }); diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 0f06eaabe..97db68adf 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -195,7 +195,7 @@ impl Client { /// let alice = UserId::parse("@alice:example.org")?; /// /// // Now let's try to discover the homeserver and create a client object. - /// let client = Client::new_from_user_id(&alice).await?; + /// let client = Client::for_user_id(&alice).await?; /// /// // Finally let's try to login. /// client.login(alice, "password", None, None).await?; @@ -203,7 +203,7 @@ impl Client { /// ``` /// /// [spec]: https://spec.matrix.org/unstable/client-server-api/#well-known-uri - pub async fn new_from_user_id(user_id: &UserId) -> Result { + pub async fn for_user_id(user_id: &UserId) -> Result { Self::builder() .user_id(user_id) .build() @@ -2368,7 +2368,7 @@ pub(crate) mod test { .with_status(200) .with_body(test_json::VERSIONS.to_string()) .create(); - let client = Client::new_from_user_id(&alice).await.unwrap(); + let client = Client::for_user_id(&alice).await.unwrap(); assert_eq!(client.homeserver().await, Url::parse(server_url.as_ref()).unwrap()); } @@ -2387,7 +2387,7 @@ pub(crate) mod test { .create(); assert!( - Client::new_from_user_id(&alice).await.is_err(), + Client::for_user_id(&alice).await.is_err(), "Creating a client from a user ID should fail when the \ .well-known server returns no version information." ); From d94ba8c64fbe9d3419d8c0fe289be65711c3265c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 14 Mar 2022 09:47:31 +0100 Subject: [PATCH 5/6] Remove Client::for_user_id --- crates/matrix-sdk/README.md | 6 ++--- crates/matrix-sdk/src/client/mod.rs | 42 ++--------------------------- 2 files changed, 5 insertions(+), 43 deletions(-) diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 9dd733517..298658ac2 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -27,14 +27,14 @@ This is demonstrated in the example below. ```rust,no_run use std::convert::TryFrom; use matrix_sdk::{ - Client, config::SyncSettings, Result, + Client, config::SyncSettings, ruma::{user_id, events::room::message::SyncRoomMessageEvent}, }; #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> anyhow::Result<()> { let alice = user_id!("@alice:example.org"); - let client = Client::for_user_id(alice).await?; + let client = Client::builder().user_id(alice).build().await?; // First we need to log in. client.login(alice, "password", None, None).await?; diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 97db68adf..aac63b522 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -173,44 +173,6 @@ impl Client { .map_err(ClientBuildError::assert_valid_builder_args) } - /// Create a new [`Client`] using homeserver auto discovery. - /// - /// This method will create a [`Client`] object that will attempt to - /// discover and configure the homeserver for the given user. Follows the - /// homeserver discovery directions described in the [spec]. - /// - /// # Arguments - /// - /// * `user_id` - The id of the user whose homeserver the client should - /// connect to. - /// - /// # Example - /// ```no_run - /// # use std::convert::TryFrom; - /// # use futures::executor::block_on; - /// # block_on(async { - /// use matrix_sdk::{Client, ruma::UserId}; - /// - /// // First let's try to construct an user id, presumably from user input. - /// let alice = UserId::parse("@alice:example.org")?; - /// - /// // Now let's try to discover the homeserver and create a client object. - /// let client = Client::for_user_id(&alice).await?; - /// - /// // Finally let's try to login. - /// client.login(alice, "password", None, None).await?; - /// # Result::<_, matrix_sdk::Error>::Ok(()) }); - /// ``` - /// - /// [spec]: https://spec.matrix.org/unstable/client-server-api/#well-known-uri - pub async fn for_user_id(user_id: &UserId) -> Result { - Self::builder() - .user_id(user_id) - .build() - .await - .map_err(ClientBuildError::assert_valid_builder_args) - } - /// Create a new [`ClientBuilder`]. pub fn builder() -> ClientBuilder { ClientBuilder::new() @@ -2368,7 +2330,7 @@ pub(crate) mod test { .with_status(200) .with_body(test_json::VERSIONS.to_string()) .create(); - let client = Client::for_user_id(&alice).await.unwrap(); + let client = Client::builder().user_id(&alice).build().await.unwrap(); assert_eq!(client.homeserver().await, Url::parse(server_url.as_ref()).unwrap()); } @@ -2387,7 +2349,7 @@ pub(crate) mod test { .create(); assert!( - Client::for_user_id(&alice).await.is_err(), + Client::builder().user_id(&alice).build().await.is_err(), "Creating a client from a user ID should fail when the \ .well-known server returns no version information." ); From a2ad7741127afaa9038ac4d9f008316fd3d415c9 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 14 Mar 2022 09:54:39 +0100 Subject: [PATCH 6/6] Add ClientBuilder::server_name --- crates/matrix-sdk/src/client/builder.rs | 37 +++++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index 6152c5cfe..5dcdacaf7 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use matrix_sdk_base::{locks::RwLock, store::StoreConfig, BaseClient, StateStore}; -use ruma::{api::client::discover::discover_homeserver, UserId}; +use ruma::{api::client::discover::discover_homeserver, ServerName, UserId}; use thiserror::Error; use url::Url; @@ -85,11 +85,23 @@ impl ClientBuilder { /// Set the user ID to discover the homeserver from. /// + /// `builder.user_id(id)` is a shortcut for + /// `builder.server_name(id.server_name())`. + /// /// This method is mutually exclusive with /// [`homeserver_url()`][Self::homeserver_url], if you set both whatever was /// set last will be used. - pub fn user_id(mut self, user_id: &UserId) -> Self { - self.homeserver_cfg = Some(HomeserverConfig::UserId(user_id.to_owned())); + pub fn user_id(self, user_id: &UserId) -> Self { + self.server_name(user_id.server_name()) + } + + /// Set the server name to discover the homeserver from. + /// + /// This method is mutually exclusive with + /// [`homeserver_url()`][Self::homeserver_url], if you set both whatever was + /// set last will be used. + pub fn server_name(mut self, server_name: &ServerName) -> Self { + self.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name.to_owned())); self } @@ -286,8 +298,8 @@ impl ClientBuilder { let homeserver = match homeserver_cfg { HomeserverConfig::Url(url) => url, - HomeserverConfig::UserId(user_id) => { - let homeserver = homeserver_from_user_id(&user_id)?; + HomeserverConfig::ServerName(server_name) => { + let homeserver = homeserver_from_name(&server_name)?; let http_client = mk_http_client(Arc::new(RwLock::new(homeserver))); let well_known = http_client.send(discover_homeserver::Request::new(), None).await?; @@ -326,21 +338,22 @@ impl ClientBuilder { } } -fn homeserver_from_user_id(user_id: &UserId) -> Result { - let homeserver = format!("https://{}", user_id.server_name()); - #[allow(unused_mut)] - let mut result = Url::parse(homeserver.as_str())?; +fn homeserver_from_name(server_name: &ServerName) -> Result { + #[cfg(not(test))] + let homeserver = format!("https://{}", server_name); + // Mockito only knows how to test http endpoints: // https://github.com/lipanski/mockito/issues/127 #[cfg(test)] - let _ = result.set_scheme("http"); - Ok(result) + let homeserver = format!("http://{}", server_name); + + Url::parse(&homeserver) } #[derive(Debug)] enum HomeserverConfig { Url(String), - UserId(Box), + ServerName(Box), } #[derive(Debug)]