use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use async_once_cell::OnceCell;
use matrix_sdk_base::{
locks::{Mutex, RwLock},
store::StoreConfig,
BaseClient, StateStore,
};
use ruma::{
api::{client::discovery::discover_homeserver, error::FromHttpResponseError, MatrixVersion},
OwnedServerName, ServerName, UserId,
};
use thiserror::Error;
#[cfg(not(target_arch = "wasm32"))]
use tokio::sync::OnceCell;
use url::Url;
use super::{Client, ClientInner};
use crate::{
config::RequestConfig,
error::RumaApiError,
http_client::{HttpClient, HttpSend, HttpSettings},
HttpError,
};
#[must_use]
#[derive(Clone, Debug)]
pub struct ClientBuilder {
homeserver_cfg: Option<HomeserverConfig>,
http_cfg: Option<HttpConfig>,
store_config: StoreConfig,
request_config: RequestConfig,
respect_login_well_known: bool,
appservice_mode: bool,
server_versions: Option<Box<[MatrixVersion]>>,
handle_refresh_tokens: 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,
server_versions: None,
handle_refresh_tokens: false,
}
}
pub fn homeserver_url(mut self, url: impl AsRef<str>) -> Self {
self.homeserver_cfg = Some(HomeserverConfig::Url(url.as_ref().to_owned()));
self
}
#[deprecated = "Use `server_name(user_id.server_name())` instead"]
pub fn user_id(self, user_id: &UserId) -> Self {
self.server_name(user_id.server_name())
}
pub fn server_name(mut self, server_name: &ServerName) -> Self {
self.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name.to_owned()));
self
}
#[cfg(feature = "sled")]
pub fn sled_store(
self,
path: impl AsRef<std::path::Path>,
passphrase: Option<&str>,
) -> Result<Self, matrix_sdk_sled::OpenStoreError> {
let config = matrix_sdk_sled::make_store_config(path, passphrase)?;
Ok(self.store_config(config))
}
#[cfg(feature = "indexeddb")]
pub async fn indexeddb_store(
self,
name: &str,
passphrase: Option<&str>,
) -> Result<Self, matrix_sdk_indexeddb::OpenStoreError> {
let config = matrix_sdk_indexeddb::make_store_config(name, passphrase).await?;
Ok(self.store_config(config))
}
pub fn store_config(mut self, store_config: StoreConfig) -> Self {
self.store_config = store_config;
self
}
#[deprecated = "\
Use [`store_config`](#method.store_config), \
[`sled_store`](#method.sled_store) or \
[`indexeddb_store`](#method.indexeddb_store) instead
"]
pub fn state_store(mut self, store: impl StateStore + 'static) -> Self {
self.store_config = self.store_config.state_store(store);
self
}
#[deprecated = "\
Use [`store_config`](#method.store_config), \
[`sled_store`](#method.sled_store) or \
[`indexeddb_store`](#method.indexeddb_store) instead
"]
#[cfg(feature = "e2e-encryption")]
pub fn crypto_store(
mut self,
store: impl matrix_sdk_base::crypto::store::CryptoStore + 'static,
) -> Self {
self.store_config = self.store_config.crypto_store(store);
self
}
pub fn respect_login_well_known(mut self, value: bool) -> Self {
self.respect_login_well_known = value;
self
}
pub fn request_config(mut self, request_config: RequestConfig) -> Self {
self.request_config = request_config;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn proxy(mut self, proxy: impl AsRef<str>) -> Self {
self.http_settings().proxy = Some(proxy.as_ref().to_owned());
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn disable_ssl_verification(mut self) -> Self {
self.http_settings().disable_ssl_verification = true;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn user_agent(mut self, user_agent: impl AsRef<str>) -> Self {
self.http_settings().user_agent = Some(user_agent.as_ref().to_owned());
self
}
pub fn http_client(mut self, client: Arc<dyn HttpSend>) -> Self {
self.http_cfg = Some(HttpConfig::Custom(client));
self
}
#[doc(hidden)]
#[cfg(feature = "appservice")]
pub fn appservice_mode(mut self) -> Self {
self.appservice_mode = true;
self
}
#[doc(hidden)]
#[cfg(feature = "appservice")]
pub fn assert_identity(mut self) -> Self {
self.request_config.assert_identity = true;
self
}
pub fn server_versions(mut self, value: impl IntoIterator<Item = MatrixVersion>) -> Self {
self.server_versions = Some(value.into_iter().collect());
self
}
#[cfg(not(target_arch = "wasm32"))]
fn http_settings(&mut self) -> &mut HttpSettings {
self.http_cfg.get_or_insert_with(Default::default).settings()
}
pub fn handle_refresh_tokens(mut self) -> Self {
self.handle_refresh_tokens = true;
self
}
pub async fn build(self) -> Result<Client, ClientBuildError> {
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 http_client = HttpClient::new(inner_http_client.clone(), self.request_config);
let mut authentication_issuer: Option<Url> = None;
let homeserver = match homeserver_cfg {
HomeserverConfig::Url(url) => url,
HomeserverConfig::ServerName(server_name) => {
let homeserver = homeserver_from_name(&server_name);
let well_known = http_client
.send(
discover_homeserver::Request::new(),
Some(RequestConfig::short_retry()),
homeserver,
None,
None,
&[MatrixVersion::V1_0],
)
.await
.map_err(|e| match e {
HttpError::Api(err) => ClientBuildError::AutoDiscovery(err),
err => ClientBuildError::Http(err),
})?;
if let Some(issuer) = well_known.authentication.map(|auth| auth.issuer) {
authentication_issuer = Url::parse(&issuer).ok();
};
well_known.homeserver.base_url
}
};
let homeserver = RwLock::new(Url::parse(&homeserver)?);
let authentication_issuer = authentication_issuer.map(RwLock::new);
let inner = Arc::new(ClientInner {
homeserver,
authentication_issuer,
http_client,
base_client,
server_versions: OnceCell::new_with(self.server_versions),
#[cfg(feature = "e2e-encryption")]
group_session_locks: Default::default(),
#[cfg(feature = "e2e-encryption")]
key_claim_lock: Default::default(),
members_request_locks: Default::default(),
typing_notice_times: Default::default(),
event_handlers: 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(),
handle_refresh_tokens: self.handle_refresh_tokens,
refresh_token_lock: Mutex::new(Ok(())),
});
Ok(Client { inner })
}
}
fn homeserver_from_name(server_name: &ServerName) -> String {
#[cfg(not(test))]
return format!("https://{server_name}");
#[cfg(test)]
return format!("http://{server_name}");
}
#[derive(Clone, Debug)]
enum HomeserverConfig {
Url(String),
ServerName(OwnedServerName),
}
#[derive(Clone, Debug)]
enum HttpConfig {
Settings(HttpSettings),
Custom(Arc<dyn HttpSend>),
}
#[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())
}
}
#[derive(Debug, Error)]
pub enum ClientBuildError {
#[error("no homeserver or user ID was configured")]
MissingHomeserver,
#[error("Error looking up the .well-known endpoint on auto-discovery")]
AutoDiscovery(FromHttpResponseError<RumaApiError>),
#[error(transparent)]
Url(#[from] url::ParseError),
#[error(transparent)]
Http(#[from] HttpError),
#[cfg(feature = "indexeddb")]
#[error(transparent)]
IndexeddbStore(#[from] matrix_sdk_indexeddb::OpenStoreError),
#[cfg(feature = "sled")]
#[error(transparent)]
SledStore(#[from] matrix_sdk_sled::OpenStoreError),
}
impl ClientBuildError {
#[doc(hidden)]
pub fn assert_valid_builder_args(self) -> HttpError {
match self {
ClientBuildError::Http(e) => e,
_ => unreachable!("homeserver URL was asserted to be valid"),
}
}
}