diff --git a/crates/ruma-client/src/client.rs b/crates/ruma-client/src/client.rs index 12701a7d..ede3aa94 100644 --- a/crates/ruma-client/src/client.rs +++ b/crates/ruma-client/src/client.rs @@ -17,10 +17,13 @@ use ruma_common::presence::PresenceState; use ruma_identifiers::{DeviceId, UserId}; use crate::{ - add_user_id_to_query, send_customized_request, DefaultConstructibleHttpClient, Error, - HttpClient, ResponseError, ResponseResult, + add_user_id_to_query, send_customized_request, Error, HttpClient, ResponseError, ResponseResult, }; +mod builder; + +pub use self::builder::ClientBuilder; + /// A client for the Matrix client-server API. #[derive(Clone, Debug)] pub struct Client(Arc>); @@ -34,26 +37,21 @@ struct ClientData { /// The underlying HTTP client. http_client: C, - /// User session data. + /// The access token, if logged in. access_token: Mutex>, + + /// The (known) Matrix versions the homeserver supports. + supported_matrix_versions: Vec, +} + +impl Client<()> { + /// Creates a new client builder. + pub fn builder() -> ClientBuilder { + ClientBuilder::new() + } } impl Client { - /// Creates a new client using the given underlying HTTP client. - /// - /// This allows the user to configure the details of HTTP as desired. - pub fn with_http_client( - http_client: C, - homeserver_url: String, - access_token: Option, - ) -> Self { - Self(Arc::new(ClientData { - homeserver_url, - http_client, - access_token: Mutex::new(access_token), - })) - } - /// Get a copy of the current `access_token`, if any. /// /// Useful for serializing and persisting the session to be restored later. @@ -62,32 +60,16 @@ impl Client { } } -impl Client { - /// Creates a new client based on a default-constructed hyper HTTP client. - pub fn new(homeserver_url: String, access_token: Option) -> Self { - Self(Arc::new(ClientData { - homeserver_url, - http_client: DefaultConstructibleHttpClient::default(), - access_token: Mutex::new(access_token), - })) - } -} - impl Client { /// Makes a request to a Matrix API endpoint. - pub async fn send_request( - &self, - request: R, - for_versions: &[MatrixVersion], - ) -> ResponseResult { - self.send_customized_request(request, for_versions, |_| Ok(())).await + pub async fn send_request(&self, request: R) -> ResponseResult { + self.send_customized_request(request, |_| Ok(())).await } /// Makes a request to a Matrix API endpoint including additional URL parameters. pub async fn send_customized_request( &self, request: R, - for_versions: &[MatrixVersion], customize: F, ) -> ResponseResult where @@ -104,7 +86,7 @@ impl Client { &self.0.http_client, &self.0.homeserver_url, send_access_token, - for_versions, + &self.0.supported_matrix_versions, request, customize, ) @@ -119,10 +101,8 @@ impl Client { &self, user_id: &UserId, request: R, - for_versions: &[MatrixVersion], ) -> ResponseResult { - self.send_customized_request(request, for_versions, add_user_id_to_query::(user_id)) - .await + self.send_customized_request(request, add_user_id_to_query::(user_id)).await } /// Log in with a username and password. @@ -142,7 +122,7 @@ impl Client { device_id, initial_device_display_name, } - ), &[MatrixVersion::V1_0]) + )) .await?; *self.0.access_token.lock().unwrap() = Some(response.access_token.clone()); @@ -158,10 +138,7 @@ impl Client { &self, ) -> Result> { let response = self - .send_request( - assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest }), - &[MatrixVersion::V1_0], - ) + .send_request(assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest })) .await?; *self.0.access_token.lock().unwrap() = response.access_token.clone(); @@ -182,10 +159,9 @@ impl Client { password: &str, ) -> Result> { let response = self - .send_request( - assign!(register::v3::Request::new(), { username, password: Some(password)}), - &[MatrixVersion::V1_0], - ) + .send_request(assign!(register::v3::Request::new(), { + username, password: Some(password) + })) .await?; *self.0.access_token.lock().unwrap() = response.access_token.clone(); @@ -200,13 +176,15 @@ impl Client { /// ```no_run /// use std::time::Duration; /// - /// # type MatrixClient = ruma_client::Client; /// # use ruma_common::presence::PresenceState; /// # use tokio_stream::{StreamExt as _}; /// # let homeserver_url = "https://example.com".parse().unwrap(); - /// # let client = MatrixClient::new(homeserver_url, None); - /// # let next_batch_token = String::new(); /// # async { + /// # let client = ruma_client::Client::builder() + /// # .homeserver_url(homeserver_url) + /// # .build::() + /// # .await?; + /// # let next_batch_token = String::new(); /// let mut sync_stream = Box::pin(client.sync( /// None, /// next_batch_token, @@ -235,7 +213,7 @@ impl Client { since: Some(&since), set_presence, timeout, - }), &[MatrixVersion::V1_0]) + })) .await?; since = response.next_batch.clone(); diff --git a/crates/ruma-client/src/client/builder.rs b/crates/ruma-client/src/client/builder.rs new file mode 100644 index 00000000..d1d3d22f --- /dev/null +++ b/crates/ruma-client/src/client/builder.rs @@ -0,0 +1,95 @@ +use std::sync::{Arc, Mutex}; + +use ruma_api::{MatrixVersion, SendAccessToken}; +use ruma_client_api::discover::get_supported_versions; + +use super::{Client, ClientData}; +use crate::{DefaultConstructibleHttpClient, Error, HttpClient, HttpClientExt}; + +/// A [`Client`] builder. +/// +/// This type can be used to construct a `Client` through a few method calls. +pub struct ClientBuilder { + homeserver_url: Option, + access_token: Option, + supported_matrix_versions: Option>, +} + +impl ClientBuilder { + pub(super) fn new() -> Self { + Self { homeserver_url: None, access_token: None, supported_matrix_versions: None } + } + + /// Set the homeserver URL. + /// + /// The homeserver URL must be set before calling [`build()`][Self::build] or + /// [`http_client()`][Self::http_client]. + pub fn homeserver_url(self, url: String) -> Self { + Self { homeserver_url: Some(url), ..self } + } + + /// Set the access token. + pub fn access_token(self, access_token: Option) -> Self { + Self { access_token, ..self } + } + + /// Set the supported Matrix versions. + /// + /// This method generally *shouldn't* be called. The [`build()`][Self::build] or + /// [`http_client()`][Self::http_client] method will take care of doing a + /// [`get_supported_versions`] request to find out about the supported versions. + pub fn supported_matrix_versions(self, versions: Vec) -> Self { + Self { supported_matrix_versions: Some(versions), ..self } + } + + /// Finish building the [`Client`]. + /// + /// Uses [`DefaultConstructibleHttpClient::default()`] to create an HTTP client instance. + /// Unless the supported Matrix versions were manually set via + /// [`supported_matrix_versions`][Self::supported_matrix_versions], this will do a + /// [`get_supported_versions`] request to find out about the supported versions. + pub async fn build(self) -> Result, Error> + where + C: DefaultConstructibleHttpClient, + { + self.http_client(C::default()).await + } + + /// Set the HTTP client to finish building the [`Client`]. + /// + /// Unless the supported Matrix versions were manually set via + /// [`supported_matrix_versions`][Self::supported_matrix_versions], this will do a + /// [`get_supported_versions`] request to find out about the supported versions. + pub async fn http_client( + self, + http_client: C, + ) -> Result, Error> + where + C: HttpClient, + { + let homeserver_url = self + .homeserver_url + .expect("homeserver URL has to be set prior to calling .build() or .http_client()"); + + let supported_matrix_versions = match self.supported_matrix_versions { + Some(versions) => versions, + None => http_client + .send_matrix_request( + &homeserver_url, + SendAccessToken::None, + &[MatrixVersion::V1_0], + get_supported_versions::Request::new(), + ) + .await? + .known_versions() + .collect(), + }; + + Ok(Client(Arc::new(ClientData { + homeserver_url, + http_client, + access_token: Mutex::new(self.access_token), + supported_matrix_versions, + }))) + } +} diff --git a/crates/ruma-client/src/lib.rs b/crates/ruma-client/src/lib.rs index 5bb1b8e0..f9e8adf8 100644 --- a/crates/ruma-client/src/lib.rs +++ b/crates/ruma-client/src/lib.rs @@ -10,11 +10,14 @@ //! //! ```ignore //! # // HACK: "ignore" the doctest here because client.log_in needs client-api feature. -//! // type MatrixClient = ruma_client::Client; -//! # type MatrixClient = ruma_client::Client; +//! // type HttpClient = ruma_client::http_client::_; +//! # type HttpClient = ruma_client::http_client::Dummy; //! # let work = async { //! let homeserver_url = "https://example.com".parse().unwrap(); -//! let client = MatrixClient::new(homeserver_url, None); +//! let client = ruma::Client::builder() +//! .homeserver_url(homeserver_url) +//! .build::() +//! .await?; //! //! let session = client //! .log_in("@alice:example.com", "secret", None, None) @@ -31,14 +34,19 @@ //! application service that does not need to log in, but uses the access_token directly: //! //! ```no_run -//! # type MatrixClient = ruma_client::Client; +//! # type HttpClient = ruma_client::http_client::Dummy; +//! # +//! # async { +//! let homeserver_url = "https://example.com".parse().unwrap(); +//! let client = ruma_client::Client::builder() +//! .homeserver_url(homeserver_url) +//! .access_token(Some("as_access_token".into())) +//! .build::() +//! .await?; //! -//! let work = async { -//! let homeserver_url = "https://example.com".parse().unwrap(); -//! let client = MatrixClient::new(homeserver_url, Some("as_access_token".into())); -//! -//! // make calls to the API -//! }; +//! // make calls to the API +//! # Result::<(), ruma_client::Error<_, _>>::Ok(()) +//! # }; //! ``` //! //! The `Client` type also provides methods for registering a new account if you don't already have @@ -51,27 +59,25 @@ //! For example: //! //! ```no_run -//! # type MatrixClient = ruma_client::Client; //! # let homeserver_url = "https://example.com".parse().unwrap(); -//! # let client = MatrixClient::new(homeserver_url, None); +//! # async { +//! # let client = ruma_client::Client::builder() +//! # .homeserver_url(homeserver_url) +//! # .build::() +//! # .await?; //! use std::convert::TryFrom; //! //! use ruma_api::MatrixVersion; //! use ruma_client_api::alias::get_alias; //! use ruma_identifiers::{room_alias_id, room_id}; //! -//! async { -//! let response = client -//! .send_request( -//! get_alias::v3::Request::new(room_alias_id!("#example_room:example.com")), -//! &[MatrixVersion::V1_0], -//! ) -//! .await?; +//! let response = client +//! .send_request(get_alias::v3::Request::new(room_alias_id!("#example_room:example.com"))) +//! .await?; //! -//! assert_eq!(response.room_id, room_id!("!n8f893n9:example.com")); -//! # Result::<(), ruma_client::Error<_, _>>::Ok(()) -//! } -//! # ; +//! assert_eq!(response.room_id, room_id!("!n8f893n9:example.com")); +//! # Result::<(), ruma_client::Error<_, _>>::Ok(()) +//! # }; //! ``` //! //! # Crate features @@ -115,7 +121,7 @@ mod error; pub mod http_client; #[cfg(feature = "client-api")] -pub use self::client::Client; +pub use self::client::{Client, ClientBuilder}; pub use self::{ error::Error, http_client::{DefaultConstructibleHttpClient, HttpClient, HttpClientExt}, diff --git a/crates/ruma/examples/hello_isahc.rs b/crates/ruma/examples/hello_isahc.rs index 569fdae0..1f6bbba4 100644 --- a/crates/ruma/examples/hello_isahc.rs +++ b/crates/ruma/examples/hello_isahc.rs @@ -10,8 +10,6 @@ use ruma::{ }; use ruma_api::MatrixVersion; -type MatrixClient = ruma::Client; - async fn hello_world( homeserver_url: String, username: &str, @@ -19,25 +17,18 @@ async fn hello_world( room_alias: &RoomAliasId, ) -> anyhow::Result<()> { let http_client = isahc::HttpClient::new()?; - let client = MatrixClient::with_http_client(http_client, homeserver_url, None); + let client = + ruma::Client::builder().homeserver_url(homeserver_url).http_client(http_client).await?; client.log_in(username, password, None, Some("ruma-example-client")).await?; - let room_id = client - .send_request(get_alias::v3::Request::new(room_alias), &[MatrixVersion::V1_0]) - .await? - .room_id; + let room_id = client.send_request(get_alias::v3::Request::new(room_alias)).await?.room_id; + client.send_request(join_room_by_id::v3::Request::new(&room_id)).await?; client - .send_request(join_room_by_id::v3::Request::new(&room_id), &[MatrixVersion::V1_0]) - .await?; - client - .send_request( - send_message_event::Request::new( - &room_id, - "1", - &RoomMessageEventContent::text_plain("Hello World!"), - )?, - &[MatrixVersion::V1_0], - ) + .send_request(send_message_event::Request::new( + &room_id, + "1", + &RoomMessageEventContent::text_plain("Hello World!"), + )?) .await?; Ok(()) diff --git a/crates/ruma/examples/hello_world.rs b/crates/ruma/examples/hello_world.rs index 707456ab..de3e6776 100644 --- a/crates/ruma/examples/hello_world.rs +++ b/crates/ruma/examples/hello_world.rs @@ -5,10 +5,9 @@ use ruma::{ events::room::message::RoomMessageEventContent, RoomAliasId, }; -use ruma_api::MatrixVersion; use ruma_identifiers::TransactionId; -type MatrixClient = ruma::Client; +type HttpClient = ruma::client::http_client::HyperNativeTls; async fn hello_world( homeserver_url: String, @@ -16,25 +15,18 @@ async fn hello_world( password: &str, room_alias: &RoomAliasId, ) -> anyhow::Result<()> { - let client = MatrixClient::new(homeserver_url, None); + let client = + ruma::Client::builder().homeserver_url(homeserver_url).build::().await?; client.log_in(username, password, None, Some("ruma-example-client")).await?; - let room_id = client - .send_request(get_alias::v3::Request::new(room_alias), &[MatrixVersion::V1_0]) - .await? - .room_id; + let room_id = client.send_request(get_alias::v3::Request::new(room_alias)).await?.room_id; + client.send_request(join_room_by_id::v3::Request::new(&room_id)).await?; client - .send_request(join_room_by_id::v3::Request::new(&room_id), &[MatrixVersion::V1_0]) - .await?; - client - .send_request( - send_message_event::v3::Request::new( - &room_id, - &TransactionId::new(), - &RoomMessageEventContent::text_plain("Hello World!"), - )?, - &[MatrixVersion::V1_0], - ) + .send_request(send_message_event::v3::Request::new( + &room_id, + &TransactionId::new(), + &RoomMessageEventContent::text_plain("Hello World!"), + )?) .await?; Ok(()) diff --git a/crates/ruma/examples/message_log.rs b/crates/ruma/examples/message_log.rs index 2f9d6d9d..194ee422 100644 --- a/crates/ruma/examples/message_log.rs +++ b/crates/ruma/examples/message_log.rs @@ -11,25 +11,23 @@ use ruma::{ }; use tokio_stream::StreamExt as _; -type MatrixClient = ruma::Client; +type HttpClient = ruma::client::http_client::HyperNativeTls; async fn log_messages( homeserver_url: String, username: &str, password: &str, ) -> anyhow::Result<()> { - let client = MatrixClient::new(homeserver_url, None); + let client = + ruma::Client::builder().homeserver_url(homeserver_url).build::().await?; client.log_in(username, password, None, None).await?; let filter = FilterDefinition::ignore_all().into(); let initial_sync_response = client - .send_request( - assign!(sync_events::v3::Request::new(), { - filter: Some(&filter), - }), - &[ruma_api::MatrixVersion::V1_0], - ) + .send_request(assign!(sync_events::v3::Request::new(), { + filter: Some(&filter), + })) .await?; let mut sync_stream = Box::pin(client.sync( diff --git a/examples/joke_bot/src/main.rs b/examples/joke_bot/src/main.rs index d438086e..9e67cf4a 100644 --- a/examples/joke_bot/src/main.rs +++ b/examples/joke_bot/src/main.rs @@ -2,12 +2,9 @@ use std::{convert::TryInto, error::Error, io, process::exit, time::Duration}; use futures_util::future::{join, join_all}; use ruma::{ - api::{ - client::{ - filter::FilterDefinition, membership::join_room_by_id, message::send_message_event, - sync::sync_events, - }, - MatrixVersion, + api::client::{ + filter::FilterDefinition, membership::join_room_by_id, message::send_message_event, + sync::sync_events, }, assign, client, events::{ @@ -41,11 +38,11 @@ async fn run() -> Result<(), Box> { let http_client = hyper::Client::builder().build::<_, hyper::Body>(hyper_tls::HttpsConnector::new()); let matrix_client = if let Some(state) = read_state().await.ok().flatten() { - MatrixClient::with_http_client( - http_client.clone(), - config.homeserver.to_owned(), - Some(state.access_token), - ) + ruma::Client::builder() + .homeserver_url(config.homeserver.clone()) + .access_token(Some(state.access_token)) + .http_client(http_client.clone()) + .await? } else if config.password.is_some() { let client = create_matrix_session(http_client.clone(), &config).await?; @@ -63,12 +60,9 @@ async fn run() -> Result<(), Box> { let filter = FilterDefinition::ignore_all().into(); let initial_sync_response = matrix_client - .send_request( - assign!(sync_events::v3::Request::new(), { - filter: Some(&filter), - }), - &[MatrixVersion::V1_0], - ) + .send_request(assign!(sync_events::v3::Request::new(), { + filter: Some(&filter), + })) .await?; let user_id = &config.username; let not_senders = &[user_id.clone()]; @@ -121,8 +115,11 @@ async fn create_matrix_session( config: &Config, ) -> Result> { if let Some(password) = &config.password { - let client = - MatrixClient::with_http_client(http_client, config.homeserver.to_owned(), None); + let client = ruma::Client::builder() + .homeserver_url(config.homeserver.clone()) + .http_client(http_client) + .await?; + if let Err(e) = client.log_in(config.username.as_ref(), password, None, None).await { let reason = match e { client::Error::AuthenticationRequired => "invalid credentials specified".to_owned(), @@ -136,6 +133,7 @@ async fn create_matrix_session( }; return Err(format!("Failed to log in: {}", reason).into()); } + Ok(client) } else { Err("Failed to create session: no password stored in config".to_owned().into()) @@ -167,10 +165,11 @@ async fn handle_message( let txn_id = TransactionId::new(); let req = send_message_event::v3::Request::new(room_id, &txn_id, &joke_content)?; // Do nothing if we can't send the message. - let _ = matrix_client.send_request(req, &[MatrixVersion::V1_0]).await; + let _ = matrix_client.send_request(req).await; } } } + Ok(()) } @@ -180,16 +179,14 @@ async fn handle_invitations( room_id: &RoomId, ) -> Result<(), Box> { println!("invited to {}", &room_id); - matrix_client - .send_request(join_room_by_id::v3::Request::new(room_id), &[MatrixVersion::V1_0]) - .await?; + matrix_client.send_request(join_room_by_id::v3::Request::new(room_id)).await?; let greeting = "Hello! My name is Mr. Bot! I like to tell jokes. Like this one: "; let joke = get_joke(http_client).await.unwrap_or_else(|_| "err... never mind.".to_owned()); let content = RoomMessageEventContent::text_plain(format!("{}\n{}", greeting, joke)); let txn_id = TransactionId::new(); let message = send_message_event::v3::Request::new(room_id, &txn_id, &content)?; - matrix_client.send_request(message, &[MatrixVersion::V1_0]).await?; + matrix_client.send_request(message).await?; Ok(()) }