diff --git a/Cargo.toml b/Cargo.toml index 9f1a1619..64458347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ hyper = "0.11.1" ruma-api = "0.4.0" ruma-client-api = "0.1.0" ruma-identifiers = "0.11.0" -serde = "1.0.9" serde_json = "1.0.2" serde_urlencoded = "0.5.1" tokio-core = "0.1.8" @@ -30,6 +29,10 @@ version = "0.1.2" optional = true version = "0.1.4" +[dev-dependencies] +futures-await = { git = 'https://github.com/alexcrichton/futures-await' } +ruma-events = "0.9.0" + [features] default = ["tls"] tls = ["hyper-tls", "native-tls"] diff --git a/examples/hello_world.rs b/examples/hello_world.rs new file mode 100644 index 00000000..e444eeee --- /dev/null +++ b/examples/hello_world.rs @@ -0,0 +1,88 @@ +#![feature(conservative_impl_trait)] +#![feature(try_from)] + +extern crate futures; +extern crate ruma_client; +extern crate ruma_events; +extern crate ruma_identifiers; +extern crate tokio_core; +extern crate url; + +use std::convert::TryFrom; +use std::env; +use std::process::exit; + +use futures::Future; +use ruma_client::Client; +use ruma_client::api::r0; +use ruma_events::EventType; +use ruma_events::room::message::{MessageEventContent, MessageType, TextMessageEventContent}; +use ruma_identifiers::RoomAliasId; +use tokio_core::reactor::{Core, Handle}; +use url::Url; + +// from https://stackoverflow.com/a/43992218/1592377 +#[macro_export] +macro_rules! clone { + (@param _) => ( _ ); + (@param $x:ident) => ( $x ); + ($($n:ident),+ => move || $body:expr) => ( + { + $( let $n = $n.clone(); )+ + move || $body + } + ); + ($($n:ident),+ => move |$($p:tt),+| $body:expr) => ( + { + $( let $n = $n.clone(); )+ + move |$(clone!(@param $p),)+| $body + } + ); +} + +fn hello_world( + tokio_handle: &Handle, + homeserver_url: Url, + room: String, +) -> impl Future + 'static { + let client = Client::https(tokio_handle, homeserver_url, None).unwrap(); + + client.register_guest().and_then(clone!(client => move |_| { + r0::alias::get_alias::call(client, r0::alias::get_alias::Request { + room_alias: RoomAliasId::try_from(&room[..]).unwrap(), + }) + })).and_then(clone!(client => move |response| { + let room_id = response.room_id; + + r0::membership::join_room_by_id::call(client.clone(), r0::membership::join_room_by_id::Request { + room_id: room_id.clone(), + third_party_signed: None, + }).and_then(move |_| { + r0::send::send_message_event::call(client, r0::send::send_message_event::Request { + room_id: room_id, + event_type: EventType::RoomMessage, + txn_id: "1".to_owned(), + data: MessageEventContent::Text(TextMessageEventContent { + body: "Hello World!".to_owned(), + msgtype: MessageType::Text, + }), + }) + }) + })).map(|_| ()) +} + +fn main() { + 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) + } + }; + + let mut core = Core::new().unwrap(); + let handle = core.handle(); + let server = Url::parse(&homeserver_url).unwrap(); + + core.run(hello_world(&handle, server, room)).unwrap(); +} diff --git a/examples/hello_world_await.rs b/examples/hello_world_await.rs new file mode 100644 index 00000000..cd5dd3f5 --- /dev/null +++ b/examples/hello_world_await.rs @@ -0,0 +1,85 @@ +#![feature(conservative_impl_trait)] +#![feature(generators)] +#![feature(proc_macro)] +#![feature(try_from)] + +extern crate futures_await as futures; +extern crate ruma_client; +extern crate ruma_events; +extern crate ruma_identifiers; +extern crate tokio_core; +extern crate url; + +use std::convert::TryFrom; +use std::env; +use std::process::exit; + +use futures::prelude::*; +use ruma_client::Client; +use ruma_client::api::r0; +use ruma_events::EventType; +use ruma_events::room::message::{MessageEventContent, MessageType, TextMessageEventContent}; +use ruma_identifiers::RoomAliasId; +use tokio_core::reactor::{Core, Handle}; +use url::Url; + +fn hello_world( + tokio_handle: &Handle, + homeserver_url: Url, + room: String, +) -> impl Future + 'static { + let client = Client::https(tokio_handle, homeserver_url, None).unwrap(); + + async_block! { + await!(client.register_guest())?; + + let response = await!(r0::alias::get_alias::call( + client.clone(), + r0::alias::get_alias::Request { + room_alias: RoomAliasId::try_from(&room[..]).unwrap(), + } + ))?; + + let room_id = response.room_id; + + await!(r0::membership::join_room_by_id::call( + client.clone(), + r0::membership::join_room_by_id::Request { + room_id: room_id.clone(), + third_party_signed: None, + } + ))?; + + await!(r0::send::send_message_event::call( + client.clone(), + r0::send::send_message_event::Request { + room_id: room_id, + event_type: EventType::RoomMessage, + txn_id: "1".to_owned(), + data: MessageEventContent::Text(TextMessageEventContent { + body: "Hello World!".to_owned(), + msgtype: MessageType::Text, + }), + } + ))?; + + Ok(()) + } +} + +fn main() { + 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) + } + }; + + + let mut core = Core::new().unwrap(); + let handle = core.handle(); + let server = Url::parse(&homeserver_url).unwrap(); + + core.run(hello_world(&handle, server, room)).unwrap(); +} diff --git a/src/api.rs b/src/api.rs index 4049e448..2983a307 100644 --- a/src/api.rs +++ b/src/api.rs @@ -32,10 +32,10 @@ macro_rules! endpoint { use {Client, Error}; /// Make a request to this API endpoint. - pub fn call<'a, C>( - client: &'a Client, + pub fn call( + client: Client, request: Request, - ) -> impl Future + 'a + ) -> impl Future where C: Connect, { diff --git a/src/lib.rs b/src/lib.rs index bae4d650..836d68dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,6 @@ extern crate native_tls; extern crate ruma_api; extern crate ruma_client_api; extern crate ruma_identifiers; -extern crate serde; extern crate serde_json; extern crate serde_urlencoded; extern crate tokio_core; @@ -25,6 +24,7 @@ use std::rc::Rc; use std::str::FromStr; use futures::future::{Future, FutureFrom, IntoFuture}; +use futures::stream::{self, Stream}; use hyper::{Client as HyperClient, Uri}; use hyper::client::{Connect, HttpConnector}; #[cfg(feature = "hyper-tls")] @@ -35,7 +35,6 @@ use ruma_api::Endpoint; use tokio_core::reactor::Handle; use url::Url; -use api::r0::session::login; pub use error::Error; pub use session::Session; @@ -45,24 +44,28 @@ mod error; mod session; /// A client for the Matrix client-server API. -#[derive(Clone, Debug)] -pub struct Client +#[derive(Debug)] +pub struct Client(Rc>); + +/// Data contained in Client's Rc +#[derive(Debug)] +pub struct ClientData where C: Connect, { homeserver_url: Url, - hyper: Rc>, + hyper: HyperClient, session: RefCell>, } impl Client { /// Creates a new client for making HTTP requests to the given homeserver. pub fn new(handle: &Handle, homeserver_url: Url, session: Option) -> Self { - Client { - homeserver_url, - hyper: Rc::new(HyperClient::configure().keep_alive(true).build(handle)), + Client(Rc::new(ClientData { + homeserver_url: homeserver_url, + hyper: HyperClient::configure().keep_alive(true).build(handle), session: RefCell::new(session), - } + })) } } @@ -72,16 +75,16 @@ impl Client> { pub fn https(handle: &Handle, homeserver_url: Url, session: Option) -> Result { let connector = HttpsConnector::new(4, handle)?; - Ok(Client { - homeserver_url, - hyper: Rc::new( + Ok(Client(Rc::new(ClientData { + homeserver_url: homeserver_url, + hyper: { HyperClient::configure() .connector(connector) .keep_alive(true) - .build(handle), - ), + .build(handle) + }, session: RefCell::new(session), - }) + }))) } } @@ -93,41 +96,145 @@ where /// /// This allows the user to configure the details of HTTP as desired. pub fn custom(hyper_client: HyperClient, homeserver_url: Url, session: Option) -> Self { - Client { - homeserver_url, - hyper: Rc::new(hyper_client), + Client(Rc::new(ClientData { + homeserver_url: homeserver_url, + hyper: hyper_client, session: RefCell::new(session), - } + })) } - /// Log in to the homeserver with a username and password. - pub fn log_in<'a>(&'a self, user: String, password: String) - -> impl Future + 'a { - let request = login::Request { + /// 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 fn log_in(&self, user: String, password: String) + -> impl Future { + use api::r0::session::login; + + let data = self.0.clone(); + + login::call(self.clone(), login::Request { address: None, login_type: login::LoginType::Password, medium: None, password, user, + }).map(move |response| { + let session = Session::new(response.access_token, response.user_id); + *data.session.borrow_mut() = Some(session.clone()); + + 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 fn register_guest(&self) -> impl Future { + use api::r0::account::register; + + let data = self.0.clone(); + + register::call(self.clone(), register::Request { + auth: None, + bind_email: None, + device_id: None, + initial_device_display_name: None, + kind: Some(register::RegistrationKind::Guest), + password: None, + username: None, + }).map(move |response| { + let session = Session::new(response.access_token, response.user_id); + *data.session.borrow_mut() = Some(session.clone()); + + 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 fn register_user( + &self, + username: Option, + password: String, + ) -> impl Future { + use api::r0::account::register; + + let data = self.0.clone(); + + register::call(self.clone(), register::Request { + auth: None, + bind_email: None, + device_id: None, + initial_device_display_name: None, + kind: Some(register::RegistrationKind::User), + password: Some(password), + username: username, + }).map(move |response| { + let session = Session::new(response.access_token, response.user_id); + *data.session.borrow_mut() = Some(session.clone()); + + 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 occured 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: bool, + ) -> impl Stream { + use api::r0::sync::sync_events; + + let client = self.clone(); + let set_presence = if set_presence { + None + } else { + Some(sync_events::SetPresence::Offline) }; - login::call(self, request).and_then(move |response| { - *self.session.borrow_mut() = Some(Session::new(response.access_token, response.user_id)); - - Ok(()) + stream::unfold(since, move |since| { + Some( + sync_events::call( + client.clone(), + sync_events::Request { + filter: filter.clone(), + since, + full_state: None, + set_presence: set_presence.clone(), + timeout: None, + }, + ).map(|res| { + let next_batch_clone = res.next_batch.clone(); + (res, Some(next_batch_clone)) + }) + ) }) } /// Makes a request to a Matrix API endpoint. - pub(crate) fn request<'a, E>( - &'a self, + pub(crate) fn request( + self, request: ::Request, - ) -> impl Future + 'a + ) -> impl Future where E: Endpoint, - ::Response: 'a, { - let mut url = self.homeserver_url.clone(); + let data1 = self.0.clone(); + let data2 = self.0.clone(); + let mut url = self.0.homeserver_url.clone(); request .try_into() @@ -141,7 +248,7 @@ where url.set_query(uri.query()); if E::METADATA.requires_authentication { - if let Some(ref session) = *self.session.borrow() { + if let Some(ref session) = *data1.session.borrow() { url.query_pairs_mut().append_pair("access_token", session.access_token()); } else { return Err(Error::AuthenticationRequired); @@ -149,15 +256,14 @@ where } } - Uri::from_str(url.as_ref()) + Uri::from_str(url.as_ref().as_ref()) .map(move |uri| (uri, hyper_request)) .map_err(Error::from) }) .and_then(move |(uri, mut hyper_request)| { hyper_request.set_uri(uri); - self.hyper - .clone() + data2.hyper .request(hyper_request) .map_err(Error::from) }) @@ -166,3 +272,9 @@ where }) } } + +impl Clone for Client { + fn clone(&self) -> Client { + Client(self.0.clone()) + } +}