From 49891083249e99a2d6d30d565ace41663698328b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sun, 20 Oct 2019 13:56:46 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 2 + Cargo.toml | 32 +++++++ LICENSE | 21 +++++ README.md | 15 +++ examples/login.rs | 58 ++++++++++++ src/error.rs | 81 ++++++++++++++++ src/lib.rs | 233 ++++++++++++++++++++++++++++++++++++++++++++++ src/session.rs | 44 +++++++++ 8 files changed, 486 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/login.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/session.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1b72444a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/Cargo.lock +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..e9400e07 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +authors = ["Damir Jelić Result<(), matrix_nio::Error> { + let client_config = AsyncClientConfig::new() + .proxy("http://localhost:8080")? + .disable_ssl_verification(); + let mut client = AsyncClient::new_with_config(&homeserver_url, None, client_config).unwrap(); + + client.login(username, password, None).await?; + let response = client.sync(SyncSettings::new()).await?; + + for (room_id, room) in response.rooms.join { + println!("Room {}", room_id); + + for event in room.timeline.events { + if let RoomEvent::RoomMessage(MessageEvent { + content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), + sender, + .. + }) = event + { + println!("{}: {}", sender, msg_body); + } + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), matrix_nio::Error> { + let (homeserver_url, username, password) = + match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) { + (Some(a), Some(b), Some(c)) => (a, b, c), + _ => { + eprintln!( + "Usage: {} ", + env::args().next().unwrap() + ); + exit(1) + } + }; + + login(homeserver_url, username, password).await +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..15268732 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,81 @@ +//! Error conditions. + +use std::error::Error as StdError; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +use http::uri::InvalidUri; +use reqwest::Error as ReqwestError; +use ruma_api::Error as RumaApiError; +use serde_json::Error as SerdeJsonError; +use serde_urlencoded::ser::Error as SerdeUrlEncodedSerializeError; + +/// An error that can occur during client operations. +#[derive(Debug)] +pub struct Error(pub(crate) InnerError); + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + let message = match self.0 { + InnerError::AuthenticationRequired => "The queried endpoint requires authentication but was called with an anonymous client.", + InnerError::Reqwest(_) => "An HTTP error occurred.", + InnerError::ConfigurationError(_) => "Error configuring the client", + InnerError::Uri(_) => "Provided string could not be converted into a URI.", + InnerError::RumaApi(_) => "An error occurred converting between ruma_client_api and hyper types.", + InnerError::SerdeJson(_) => "A serialization error occurred.", + InnerError::SerdeUrlEncodedSerialize(_) => "An error occurred serializing data to a query string.", + }; + + write!(f, "{}", message) + } +} + +impl StdError for Error {} + +/// Internal representation of errors. +#[derive(Debug)] +pub(crate) enum InnerError { + /// Queried endpoint requires authentication but was called on an anonymous client. + AuthenticationRequired, + /// An error in the client configuration. + ConfigurationError(String), + /// An error at the HTTP layer. + Reqwest(ReqwestError), + /// An error when parsing a string as a URI. + Uri(InvalidUri), + /// An error converting between ruma_client_api types and Hyper types. + RumaApi(RumaApiError), + /// An error when serializing or deserializing a JSON value. + SerdeJson(SerdeJsonError), + /// An error when serializing a query string value. + SerdeUrlEncodedSerialize(SerdeUrlEncodedSerializeError), +} + +impl From for Error { + fn from(error: InvalidUri) -> Self { + Self(InnerError::Uri(error)) + } +} + +impl From for Error { + fn from(error: RumaApiError) -> Self { + Self(InnerError::RumaApi(error)) + } +} + +impl From for Error { + fn from(error: SerdeJsonError) -> Self { + Self(InnerError::SerdeJson(error)) + } +} + +impl From for Error { + fn from(error: SerdeUrlEncodedSerializeError) -> Self { + Self(InnerError::SerdeUrlEncodedSerialize(error)) + } +} + +impl From for Error { + fn from(error: ReqwestError) -> Self { + Self(InnerError::Reqwest(error)) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..ef6dafae --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,233 @@ +//! Crate `ruma_client` is a [Matrix](https://matrix.org/) client library. +//! +use std::convert::{TryFrom, TryInto}; + +use http::Method as HttpMethod; +use http::Response as HttpResponse; +use js_int::UInt; +use reqwest; +use ruma_api::Endpoint; +use url::Url; + +use crate::error::InnerError; + +pub use crate::{error::Error, session::Session}; +pub use ruma_client_api as api; +pub use ruma_events as events; + +//pub mod api; +mod error; +mod session; + +#[derive(Debug)] +pub struct AsyncClient { + /// The URL of the homeserver to connect to. + homeserver: Url, + /// The underlying HTTP client. + client: reqwest::Client, + /// User session data. + session: Option, +} + +#[derive(Default, Debug)] +pub struct AsyncClientConfig { + proxy: Option, + use_sys_proxy: bool, + disable_ssl_verification: bool, +} + +impl AsyncClientConfig { + pub fn new() -> Self { + Default::default() + } + + pub fn proxy(mut self, proxy: &str) -> Result { + if self.use_sys_proxy { + return Err(Error(InnerError::ConfigurationError( + "Using the system proxy has been previously configured.".to_string(), + ))); + } + self.proxy = Some(reqwest::Proxy::all(proxy)?); + Ok(self) + } + + pub fn use_sys_proxy(mut self) -> Result { + if self.proxy.is_some() { + return Err(Error(InnerError::ConfigurationError( + "A proxy has already been configured.".to_string(), + ))); + } + self.use_sys_proxy = true; + Ok(self) + } + + pub fn disable_ssl_verification(mut self) -> Self { + self.disable_ssl_verification = true; + self + } +} + +#[derive(Debug, Default)] +pub struct SyncSettings { + pub(crate) timeout: Option, + pub(crate) token: Option, + pub(crate) full_state: Option, +} + +impl SyncSettings { + pub fn new() -> Self { + Default::default() + } + + pub fn token>(mut self, token: S) -> Self { + self.token = Some(token.into()); + self + } + + pub fn timeout>(mut self, timeout: T) -> Result + where + js_int::TryFromIntError: + std::convert::From<>::Error>, + { + self.timeout = Some(timeout.try_into()?); + Ok(self) + } + + pub fn full_state(mut self, full_state: bool) -> Self { + self.full_state = Some(full_state); + self + } +} + +use api::r0::session::login; +use api::r0::sync::sync_events; + +impl AsyncClient { + /// Creates a new client for making HTTP requests to the given homeserver. + pub fn new(homeserver_url: &str, session: Option) -> Result { + let homeserver = Url::parse(homeserver_url)?; + let client = reqwest::Client::new(); + + Ok(Self { + homeserver, + client, + session, + }) + } + + pub fn new_with_config( + homeserver_url: &str, + session: Option, + config: AsyncClientConfig, + ) -> Result { + let homeserver = Url::parse(homeserver_url)?; + let client = reqwest::Client::builder(); + + let client = if config.disable_ssl_verification { + client.danger_accept_invalid_certs(true) + } else { + client + }; + + let client = match config.proxy { + Some(p) => client.proxy(p), + None => client, + }; + + let client = if config.use_sys_proxy { + client.use_sys_proxy() + } else { + client + }; + + let mut headers = reqwest::header::HeaderMap::new(); + + headers.insert(reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_static("ruma")); + + let client = client.default_headers(headers).build().unwrap(); + + Ok(Self { + homeserver, + client, + session, + }) + } + + pub async fn login>( + &mut self, + user: S, + password: S, + device_id: Option, + ) -> Result { + let request = login::Request { + address: None, + login_type: login::LoginType::Password, + medium: None, + device_id: device_id.map(|d| d.into()), + password: password.into(), + user: user.into(), + }; + + let response = self.send(request).await.unwrap(); + + let session = Session { + access_token: response.access_token.clone(), + device_id: response.device_id.clone(), + user_id: response.user_id.clone(), + }; + + self.session = Some(session.clone()); + + Ok(response) + } + + pub async fn sync(&self, sync_settings: SyncSettings) -> Result { + let request = sync_events::Request { + filter: None, + since: sync_settings.token, + full_state: sync_settings.full_state, + set_presence: None, + timeout: sync_settings.timeout, + }; + + let response = self.send(request).await.unwrap(); + + Ok(response) + } + + async fn send(&self, request: Request) -> Result { + let request: http::Request> = request.try_into()?; + let url = request.uri(); + let url = self.homeserver.join(url.path()).unwrap(); + + let request_builder = match Request::METADATA.method { + HttpMethod::GET => self.client.get(url), + HttpMethod::POST => { + let body = request.body().clone(); + self.client.post(url).body(body) + } + HttpMethod::PUT => unimplemented!(), + HttpMethod::DELETE => unimplemented!(), + _ => panic!("Unsuported method"), + }; + + let request_builder = if Request::METADATA.requires_authentication { + if let Some(ref session) = self.session { + request_builder.bearer_auth(&session.access_token) + } else { + return Err(Error(InnerError::AuthenticationRequired)); + } + } else { + request_builder + }; + + let response = request_builder.send().await?; + + let status = response.status(); + let body = response.bytes().await?.as_ref().to_owned(); + let response = HttpResponse::builder().status(status).body(body).unwrap(); + let response = Request::Response::try_from(response)?; + + Ok(response) + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 00000000..331baeb7 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,44 @@ +//! User sessions. + +use ruma_identifiers::UserId; + +/// A user session, containing an access token and information about the associated user account. +#[derive(Clone, Debug, serde::Deserialize, Eq, Hash, PartialEq, serde::Serialize)] +pub struct Session { + /// The access token used for this session. + pub access_token: String, + /// The user the access token was issued for. + pub user_id: UserId, + /// The ID of the client device + pub device_id: String, +} + +impl Session { + /// Create a new user session from an access token and a user ID. + #[deprecated] + pub fn new(access_token: String, user_id: UserId, device_id: String) -> Self { + Self { + access_token, + user_id, + device_id, + } + } + + /// Get the access token associated with this session. + #[deprecated] + pub fn access_token(&self) -> &str { + &self.access_token + } + + /// Get the ID of the user the session belongs to. + #[deprecated] + pub fn user_id(&self) -> &UserId { + &self.user_id + } + + /// Get ID of the device the session belongs to. + #[deprecated] + pub fn device_id(&self) -> &str { + &self.device_id + } +}