diff --git a/ruma-client/.builds/beta.yml b/ruma-client/.builds/beta.yml new file mode 100644 index 00000000..a076189b --- /dev/null +++ b/ruma-client/.builds/beta.yml @@ -0,0 +1,27 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-client +tasks: + - rustup: | + # We specify --profile minimal because we'd otherwise download docs + rustup toolchain install beta --profile minimal -c rustfmt -c clippy + rustup default beta + - test: | + cd ruma-client + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + cargo fmt -- --check + fmt_exit=$? + + cargo clippy --all-targets --all-features -- -D warnings + clippy_exit=$? + + cargo test --verbose + test_exit=$? + + exit $(( $fmt_exit || $clippy_exit || $test_exit )) diff --git a/ruma-client/.builds/nightly.yml b/ruma-client/.builds/nightly.yml new file mode 100644 index 00000000..db3fe2e6 --- /dev/null +++ b/ruma-client/.builds/nightly.yml @@ -0,0 +1,32 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-client +tasks: + - rustup: | + rustup toolchain install nightly --profile minimal + rustup default nightly + + # Try installing rustfmt & clippy for nightly, but don't fail the build + # if they are not available + rustup component add rustfmt || true + rustup component add clippy || true + - test: | + cd ruma-client + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + if ( rustup component list | grep -q rustfmt ); then + cargo fmt -- --check + fi + fmt_exit=$? + + if ( rustup component list | grep -q clippy ); then + cargo clippy --all-targets --all-features -- -D warnings + fi + clippy_exit=$? + + exit $(( $fmt_exit || $clippy_exit )) diff --git a/ruma-client/.builds/stable.yml b/ruma-client/.builds/stable.yml new file mode 100644 index 00000000..421d3baa --- /dev/null +++ b/ruma-client/.builds/stable.yml @@ -0,0 +1,29 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-client +tasks: + - rustup: | + # We specify --profile minimal because we'd otherwise download docs + rustup toolchain install stable --profile minimal -c rustfmt -c clippy + rustup default stable + - test: | + cd ruma-client + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + cargo fmt -- --check + fmt_exit=$? + + cargo clippy --all-targets --all-features -- -D warnings + clippy_exit=$? + + cargo test --verbose + test_exit=$? + + exit $(( $fmt_exit || $clippy_exit || $test_exit )) + # TODO: Add audit task once cargo-audit binary releases are available. + # See https://github.com/RustSec/cargo-audit/issues/66 diff --git a/ruma-client/.gitignore b/ruma-client/.gitignore new file mode 100644 index 00000000..1b72444a --- /dev/null +++ b/ruma-client/.gitignore @@ -0,0 +1,2 @@ +/Cargo.lock +/target diff --git a/ruma-client/Cargo.toml b/ruma-client/Cargo.toml new file mode 100644 index 00000000..6f65a811 --- /dev/null +++ b/ruma-client/Cargo.toml @@ -0,0 +1,40 @@ +[package] +authors = [ + "Jimmy Cuadra ", + "Jonas Platte ", +] +categories = ["api-bindings", "web-programming"] +description = "A Matrix client library." +documentation = "https://docs.rs/ruma-client" +edition = "2018" +homepage = "https://github.com/ruma/ruma-client" +keywords = ["matrix", "chat", "messaging", "ruma"] +license = "MIT" +name = "ruma-client" +readme = "README.md" +repository = "https://github.com/ruma/ruma-client" +version = "0.4.0" + +[dependencies] +futures-core = "0.3.5" +futures-util = "0.3.5" +http = "0.2.1" +hyper = "0.13.5" +hyper-tls = { version = "0.4.1", optional = true } +ruma-api = "=0.17.0-alpha.1" +ruma-client-api = "=0.10.0-alpha.1" +ruma-common = "0.2.0" +ruma-events = "=0.22.0-alpha.1" +ruma-identifiers = "0.17.1" +serde = { version = "1.0.110", features = ["derive"] } +serde_json = "1.0.53" +serde_urlencoded = "0.6.1" +url = "2.1.1" + +[dev-dependencies] +anyhow = "1.0.31" +tokio = { version = "0.2.21", features = ["macros"] } + +[features] +default = ["tls"] +tls = ["hyper-tls"] diff --git a/ruma-client/LICENSE b/ruma-client/LICENSE new file mode 100644 index 00000000..de62627d --- /dev/null +++ b/ruma-client/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 Jimmy Cuadra + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/ruma-client/README.md b/ruma-client/README.md new file mode 100644 index 00000000..ee77efb5 --- /dev/null +++ b/ruma-client/README.md @@ -0,0 +1,38 @@ +# ruma-client + +[![crates.io page](https://img.shields.io/crates/v/ruma-client.svg)](https://crates.io/crates/ruma-client) +[![docs.rs page](https://docs.rs/ruma-client/badge.svg)](https://docs.rs/ruma-client/) +[![build status](https://travis-ci.org/ruma/ruma-client.svg?branch=master)](https://travis-ci.org/ruma/ruma-client) +![license: MIT](https://img.shields.io/crates/l/ruma-client.svg) + +**ruma-client** is a [Matrix][] client library for [Rust][]. + +[Matrix]: https://matrix.org/ +[Rust]: https://www.rust-lang.org/ + +## Status + +This project is a work in progress and not ready for production usage yet. Most endpoints that are +available in this crate are usable with an up-to-date synapse server, but no comprehensive testing +has been done so far. + +As long as the matrix client-server API is still at version 0.x, only the latest API revision is +considered supported. However, due to the low amount of available manpower, it can take a long time +for all changes from a new API revision to arrive in ruma-client (at the time of writing only few +endpoints have received an update for r0.4.0). + +## Contributing + +If you want to help out, have a look at the issues here and on the other [ruma-\*][gh-org] +repositories (ruma-client-api and ruma-events in particular contain much of the code that powers +ruma-client). + +There is also a [room for ruma on matrix.org][#ruma:matrix.org], which can be used for questions +and discussion related to any of the crates in this project. + +[gh-org]: https://github.com/ruma +[#ruma:matrix.org]: https://matrix.to/#/#ruma:matrix.org + +## Minimum Rust version + +ruma-client requires Rust 1.39.0 or later. diff --git a/ruma-client/examples/hello_world.rs b/ruma-client/examples/hello_world.rs new file mode 100644 index 00000000..df3ff6ad --- /dev/null +++ b/ruma-client/examples/hello_world.rs @@ -0,0 +1,65 @@ +use std::{convert::TryFrom, env, process::exit}; + +use ruma_client::{ + self, + api::r0, + events::{ + room::message::{MessageEventContent, TextMessageEventContent}, + EventType, + }, + identifiers::RoomAliasId, + Client, +}; +use serde_json::value::to_raw_value as to_raw_json_value; +use url::Url; + +async fn hello_world(homeserver_url: Url, room: String) -> anyhow::Result<()> { + let client = Client::new(homeserver_url, None); + + client.register_guest().await?; + let response = client + .request(r0::alias::get_alias::Request { + room_alias: RoomAliasId::try_from(&room[..]).unwrap(), + }) + .await?; + + let room_id = response.room_id; + + client + .request(r0::membership::join_room_by_id::Request { + room_id: room_id.clone(), + third_party_signed: None, + }) + .await?; + + client + .request(r0::message::create_message_event::Request { + room_id, + event_type: EventType::RoomMessage, + txn_id: "1".to_owned(), + data: to_raw_json_value(&MessageEventContent::Text(TextMessageEventContent { + body: "Hello World!".to_owned(), + formatted: None, + relates_to: None, + }))?, + }) + .await?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let (homeserver_url, room) = match (env::args().nth(1), env::args().nth(2)) { + (Some(a), Some(b)) => (a, b), + _ => { + eprintln!( + "Usage: {} ", + env::args().next().unwrap() + ); + exit(1) + } + }; + + hello_world(homeserver_url.parse().unwrap(), room).await +} diff --git a/ruma-client/examples/message_log.rs b/ruma-client/examples/message_log.rs new file mode 100644 index 00000000..222243c6 --- /dev/null +++ b/ruma-client/examples/message_log.rs @@ -0,0 +1,81 @@ +use std::{env, process::exit, time::Duration}; + +use futures_util::stream::{StreamExt as _, TryStreamExt as _}; +use ruma_client::{ + self, + events::room::message::{MessageEventContent, TextMessageEventContent}, + HttpClient, +}; +use ruma_common::presence::PresenceState; +use ruma_events::{AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent}; +use url::Url; + +async fn log_messages( + homeserver_url: Url, + username: String, + password: String, +) -> anyhow::Result<()> { + let client = HttpClient::new(homeserver_url, None); + + client.log_in(username, password, None, None).await?; + + let mut sync_stream = Box::pin( + client + .sync( + None, + None, + PresenceState::Online, + Some(Duration::from_secs(30)), + ) + // TODO: This is a horrible way to obtain an initial next_batch token that generates way + // too much server load and network traffic. Fix this! + .skip(1), + ); + + while let Some(res) = sync_stream.try_next().await? { + // Only look at rooms the user hasn't left yet + for (room_id, room) in res.rooms.join { + for event in room + .timeline + .events + .into_iter() + .flat_map(|r| r.deserialize()) + { + // Filter out the text messages + if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage( + SyncMessageEvent { + content: + MessageEventContent::Text(TextMessageEventContent { + body: msg_body, .. + }), + sender, + .. + }, + )) = event + { + println!("{:?} in {:?}: {}", sender, room_id, msg_body); + } + } + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + 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) + } + }; + + let server = Url::parse(&homeserver_url).unwrap(); + log_messages(server, username, password).await +} diff --git a/ruma-client/src/error.rs b/ruma-client/src/error.rs new file mode 100644 index 00000000..f517d2d2 --- /dev/null +++ b/ruma-client/src/error.rs @@ -0,0 +1,69 @@ +//! Error conditions. + +use std::fmt::{self, Debug, Display, Formatter}; + +use ruma_api::error::{FromHttpResponseError, IntoHttpError}; + +/// An error that can occur during client operations. +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// Queried endpoint requires authentication but was called on an anonymous client. + AuthenticationRequired, + /// Construction of the HTTP request failed (this should never happen). + IntoHttp(IntoHttpError), + /// The request's URL is invalid (this should never happen). + Url(UrlError), + /// Couldn't obtain an HTTP response (e.g. due to network or DNS issues). + Response(ResponseError), + /// Converting the HTTP response to one of ruma's types failed. + FromHttpResponse(FromHttpResponseError), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::AuthenticationRequired => { + write!(f, "The queried endpoint requires authentication but was called with an anonymous client.") + } + Self::IntoHttp(err) => write!(f, "HTTP request construction failed: {}", err), + Self::Url(UrlError(err)) => write!(f, "Invalid URL: {}", err), + Self::Response(ResponseError(err)) => write!(f, "Couldn't obtain a response: {}", err), + Self::FromHttpResponse(err) => write!(f, "HTTP response conversion failed: {}", err), + } + } +} + +impl From for Error { + fn from(err: IntoHttpError) -> Self { + Error::IntoHttp(err) + } +} + +#[doc(hidden)] +impl From for Error { + fn from(err: http::uri::InvalidUri) -> Self { + Error::Url(UrlError(err)) + } +} + +#[doc(hidden)] +impl From for Error { + fn from(err: hyper::Error) -> Self { + Error::Response(ResponseError(err)) + } +} + +impl From> for Error { + fn from(err: FromHttpResponseError) -> Self { + Error::FromHttpResponse(err) + } +} + +impl std::error::Error for Error {} + +#[derive(Debug)] +pub struct UrlError(http::uri::InvalidUri); + +#[derive(Debug)] +pub struct ResponseError(hyper::Error); diff --git a/ruma-client/src/lib.rs b/ruma-client/src/lib.rs new file mode 100644 index 00000000..49a13e8e --- /dev/null +++ b/ruma-client/src/lib.rs @@ -0,0 +1,423 @@ +//! Crate `ruma_client` is a [Matrix](https://matrix.org/) client library. +//! +//! # Usage +//! +//! Begin by creating a `Client` type, usually using the `https` method for a client that supports +//! secure connections, and then logging in: +//! +//! ```no_run +//! use ruma_client::Client; +//! +//! let work = async { +//! let homeserver_url = "https://example.com".parse().unwrap(); +//! let client = Client::https(homeserver_url, None); +//! +//! let session = client +//! .log_in("@alice:example.com".to_string(), "secret".to_string(), None, None) +//! .await?; +//! +//! // You're now logged in! Write the session to a file if you want to restore it later. +//! // Then start using the API! +//! # Result::<(), ruma_client::Error<_>>::Ok(()) +//! }; +//! ``` +//! +//! You can also pass an existing session to the `Client` constructor to restore a previous session +//! rather than calling `log_in`. This can also be used to create a session for an application service +//! that does not need to log in, but uses the access_token directly: +//! +//! ```no_run +//! use ruma_client::{Client, Session}; +//! +//! let work = async { +//! let homeserver_url = "https://example.com".parse().unwrap(); +//! let session = Session{access_token: "as_access_token".to_string(), identification: None}; +//! let client = Client::https(homeserver_url, Some(session)); +//! +//! // make calls to the API +//! }; +//! ``` +//! +//! For the standard use case of synchronizing with the homeserver (i.e. getting all the latest +//! events), use the `Client::sync`: +//! +//! ```no_run +//! use std::time::Duration; +//! +//! # use futures_util::stream::{StreamExt as _, TryStreamExt as _}; +//! # use ruma_client::Client; +//! # use ruma_common::presence::PresenceState; +//! # let homeserver_url = "https://example.com".parse().unwrap(); +//! # let client = Client::https(homeserver_url, None); +//! # let next_batch_token = String::new(); +//! # async { +//! let mut sync_stream = Box::pin(client.sync( +//! None, +//! Some(next_batch_token), +//! PresenceState::Online, +//! Some(Duration::from_secs(30)), +//! )); +//! while let Some(response) = sync_stream.try_next().await? { +//! // Do something with the data in the response... +//! } +//! # Result::<(), ruma_client::Error<_>>::Ok(()) +//! # }; +//! ``` +//! +//! The `Client` type also provides methods for registering a new account if you don't already have +//! one with the given homeserver. +//! +//! Beyond these basic convenience methods, `ruma-client` gives you access to the entire Matrix +//! client-server API via the `api` module. Each leaf module under this tree of modules contains +//! the necessary types for one API endpoint. Simply call the module's `call` method, passing it +//! the logged in `Client` and the relevant `Request` type. `call` will return a future that will +//! resolve to the relevant `Response` type. +//! +//! For example: +//! +//! ```no_run +//! # use ruma_client::Client; +//! # let homeserver_url = "https://example.com".parse().unwrap(); +//! # let client = Client::https(homeserver_url, None); +//! use std::convert::TryFrom; +//! +//! use ruma_client::api::r0::alias::get_alias; +//! use ruma_identifiers::{RoomAliasId, RoomId}; +//! +//! async { +//! let response = client +//! .request(get_alias::Request { +//! room_alias: RoomAliasId::try_from("#example_room:example.com").unwrap(), +//! }) +//! .await?; +//! +//! assert_eq!(response.room_id, RoomId::try_from("!n8f893n9:example.com").unwrap()); +//! # Result::<(), ruma_client::Error<_>>::Ok(()) +//! } +//! # ; +//! ``` + +#![warn(rust_2018_idioms)] +#![deny( + missing_copy_implementations, + missing_debug_implementations, + missing_docs +)] + +use std::{ + convert::TryFrom, + str::FromStr, + sync::{Arc, Mutex}, + time::Duration, +}; + +use futures_core::{ + future::Future, + stream::{Stream, TryStream}, +}; +use futures_util::stream; +use http::Response as HttpResponse; +use hyper::{client::HttpConnector, Client as HyperClient, Uri}; +#[cfg(feature = "hyper-tls")] +use hyper_tls::HttpsConnector; +use ruma_api::Endpoint; +use ruma_identifiers::DeviceId; +use std::collections::BTreeMap; +use url::Url; + +pub use ruma_client_api as api; +pub use ruma_events as events; +pub use ruma_identifiers as identifiers; + +mod error; +mod session; + +pub use self::{error::Error, session::Identification, session::Session}; + +/// A client for the Matrix client-server API. +#[derive(Debug)] +pub struct Client(Arc>); + +/// Data contained in Client's Rc +#[derive(Debug)] +struct ClientData { + /// The URL of the homeserver to connect to. + homeserver_url: Url, + /// The underlying HTTP client. + hyper: HyperClient, + /// User session data. + session: Mutex>, +} + +/// Non-secured variant of the client (using plain HTTP requests) +pub type HttpClient = Client; + +impl HttpClient { + /// Creates a new client for making HTTP requests to the given homeserver. + pub fn new(homeserver_url: Url, session: Option) -> Self { + Self(Arc::new(ClientData { + homeserver_url, + hyper: HyperClient::builder().build_http(), + session: Mutex::new(session), + })) + } +} + +/// Secured variant of the client (using HTTPS requests) +#[cfg(feature = "tls")] +pub type HttpsClient = Client>; + +#[cfg(feature = "tls")] +impl HttpsClient { + /// Creates a new client for making HTTPS requests to the given homeserver. + pub fn https(homeserver_url: Url, session: Option) -> Self { + let connector = HttpsConnector::new(); + + Self(Arc::new(ClientData { + homeserver_url, + hyper: HyperClient::builder().build(connector), + session: Mutex::new(session), + })) + } +} + +impl Client +where + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, +{ + /// Creates a new client using the given `hyper::Client`. + /// + /// This allows the user to configure the details of HTTP as desired. + pub fn custom( + hyper_client: HyperClient, + homeserver_url: Url, + session: Option, + ) -> Self { + Self(Arc::new(ClientData { + homeserver_url, + hyper: hyper_client, + session: Mutex::new(session), + })) + } + + /// Get a copy of the current `Session`, if any. + /// + /// Useful for serializing and persisting the session to be restored later. + pub fn session(&self) -> Option { + self.0 + .session + .lock() + .expect("session mutex was poisoned") + .clone() + } + + /// Log in with a username and password. + /// + /// In contrast to `api::r0::session::login::call()`, this method stores the + /// session data returned by the endpoint in this client, instead of + /// returning it. + pub async fn log_in( + &self, + user: String, + password: String, + device_id: Option>, + initial_device_display_name: Option, + ) -> Result> { + use api::r0::session::login; + + let response = self + .request(login::Request { + user: login::UserInfo::MatrixId(user), + login_info: login::LoginInfo::Password { password }, + device_id, + initial_device_display_name, + }) + .await?; + + let session = Session { + access_token: response.access_token, + identification: Some(Identification { + device_id: response.device_id, + user_id: response.user_id, + }), + }; + *self.0.session.lock().unwrap() = Some(session.clone()); + + Ok(session) + } + + /// Register as a guest. In contrast to `api::r0::account::register::call()`, + /// this method stores the session data returned by the endpoint in this + /// client, instead of returning it. + pub async fn register_guest(&self) -> Result> { + use api::r0::account::register; + + let response = self + .request(register::Request { + auth: None, + device_id: None, + inhibit_login: false, + initial_device_display_name: None, + kind: Some(register::RegistrationKind::Guest), + password: None, + username: None, + }) + .await?; + + let session = Session { + // since we supply inhibit_login: false above, the access token needs to be there + // TODO: maybe unwrap is not the best solution though + access_token: response.access_token.unwrap(), + identification: Some(Identification { + // same as access_token + device_id: response.device_id.unwrap(), + user_id: response.user_id, + }), + }; + *self.0.session.lock().unwrap() = Some(session.clone()); + + Ok(session) + } + + /// Register as a new user on this server. + /// + /// In contrast to `api::r0::account::register::call()`, this method stores + /// the session data returned by the endpoint in this client, instead of + /// returning it. + /// + /// The username is the local part of the returned user_id. If it is + /// omitted from this request, the server will generate one. + pub async fn register_user( + &self, + username: Option, + password: String, + ) -> Result> { + use api::r0::account::register; + + let response = self + .request(register::Request { + auth: None, + device_id: None, + inhibit_login: false, + initial_device_display_name: None, + kind: Some(register::RegistrationKind::User), + password: Some(password), + username, + }) + .await?; + + let session = Session { + // since we supply inhibit_login: false above, the access token needs to be there + // TODO: maybe unwrap is not the best solution though + access_token: response.access_token.unwrap(), + identification: Some(Identification { + // same as access_token + device_id: response.device_id.unwrap(), + user_id: response.user_id, + }), + }; + *self.0.session.lock().unwrap() = Some(session.clone()); + + Ok(session) + } + + /// Convenience method that represents repeated calls to the sync_events endpoint as a stream. + /// + /// If the since parameter is None, the first Item might take a significant time to arrive and + /// be deserialized, because it contains all events that have occurred in the whole lifetime of + /// the logged-in users account and are visible to them. + pub fn sync( + &self, + filter: Option, + since: Option, + set_presence: ruma_common::presence::PresenceState, + timeout: Option, + ) -> impl Stream>> + + TryStream> { + use api::r0::sync::sync_events; + + let client = self.clone(); + stream::try_unfold(since, move |since| { + let client = client.clone(); + let filter = filter.clone(); + + async move { + let response = client + .request(sync_events::Request { + filter, + since, + full_state: false, + set_presence, + timeout, + }) + .await?; + + let next_batch_clone = response.next_batch.clone(); + Ok(Some((response, Some(next_batch_clone)))) + } + }) + } + + /// Makes a request to a Matrix API endpoint. + pub fn request( + &self, + request: Request, + ) -> impl Future>> { + self.request_with_url_params(request, None) + } + + /// Makes a request to a Matrix API endpoint including additional URL parameters. + pub fn request_with_url_params( + &self, + request: Request, + params: Option>, + ) -> impl Future>> { + let client = self.0.clone(); + + let mut url = client.homeserver_url.clone(); + + async move { + let mut hyper_request = request.try_into()?.map(hyper::Body::from); + + { + let uri = hyper_request.uri(); + + url.set_path(uri.path()); + url.set_query(uri.query()); + + if let Some(params) = params { + for (key, value) in params { + url.query_pairs_mut().append_pair(&key, &value); + } + } + + if Request::METADATA.requires_authentication { + if let Some(ref session) = *client.session.lock().unwrap() { + url.query_pairs_mut() + .append_pair("access_token", &session.access_token); + } else { + return Err(Error::AuthenticationRequired); + } + } + } + + *hyper_request.uri_mut() = Uri::from_str(url.as_ref())?; + + let hyper_response = client.hyper.request(hyper_request).await?; + let (head, body) = hyper_response.into_parts(); + + // FIXME: We read the response into a contiguous buffer here (not actually required for + // deserialization) and then copy the whole thing to convert from Bytes to Vec. + let full_body = hyper::body::to_bytes(body).await?; + let full_response = HttpResponse::from_parts(head, full_body.as_ref().to_owned()); + + Ok(Request::Response::try_from(full_response)?) + } + } +} + +impl Clone for Client { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} diff --git a/ruma-client/src/session.rs b/ruma-client/src/session.rs new file mode 100644 index 00000000..8a540204 --- /dev/null +++ b/ruma-client/src/session.rs @@ -0,0 +1,57 @@ +//! User sessions. + +use ruma_identifiers::{DeviceId, 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, + /// Identification information for a user + pub identification: Option, +} + +/// The identification information about the associated user account if the session is associated with +/// a single user account. +#[derive(Clone, Debug, serde::Deserialize, Eq, Hash, PartialEq, serde::Serialize)] +pub struct Identification { + /// The user the access token was issued for. + pub user_id: UserId, + /// The ID of the client device + pub device_id: Box, +} + +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: Box) -> Self { + Self { + access_token, + identification: Some(Identification { 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) -> Option<&UserId> { + if let Some(identification) = &self.identification { + return Some(&identification.user_id); + } + None + } + + /// Get ID of the device the session belongs to. + #[deprecated] + pub fn device_id(&self) -> Option<&DeviceId> { + if let Some(identification) = &self.identification { + return Some(&identification.device_id); + } + None + } +}