diff --git a/design.md b/design.md index 8b974294..2686f513 100644 --- a/design.md +++ b/design.md @@ -3,7 +3,7 @@ ## Design and Layout #### Async Client -The highest level structure that ties the other pieces of functionality together. The client is responsible for the Request/Response cycle. It can be thought of as a thin layer atop the `BaseClient` passing requests along for the `BaseClient` to handle. A user should be able to write their own `AsyncClient` using the `BaseClient`. It knows how to +The highest level structure that ties the other pieces of functionality together. The client is responsible for the Request/Response cycle. It can be thought of as a thin layer atop the `BaseClient` passing requests along for the `BaseClient` to handle. A user should be able to write their own `AsyncClient` using the `BaseClient`. It knows how to - login - send messages - encryption ... diff --git a/src/async_client.rs b/src/async_client.rs index 9ce12c68..718e423b 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -37,10 +37,10 @@ use ruma_api::{Endpoint, Outgoing}; use ruma_events::room::message::MessageEventContent; use ruma_events::EventResult; pub use ruma_events::EventType; -use ruma_identifiers::RoomId; +use ruma_identifiers::{RoomId, RoomIdOrAliasId, UserId}; #[cfg(feature = "encryption")] -use ruma_identifiers::{DeviceId, UserId}; +use ruma_identifiers::DeviceId; use crate::api; use crate::base_client::Client as BaseClient; @@ -181,7 +181,17 @@ impl SyncSettings { use api::r0::client_exchange::send_event_to_device; #[cfg(feature = "encryption")] use api::r0::keys::{claim_keys, get_keys, upload_keys, KeyAlgorithm}; +use api::r0::membership::join_room_by_id; +use api::r0::membership::join_room_by_id_or_alias; +use api::r0::membership::kick_user; +use api::r0::membership::leave_room; +use api::r0::membership::{ + invite_user::{self, InvitationRecipient}, + Invite3pid, +}; use api::r0::message::create_message_event; +use api::r0::message::get_message_events; +use api::r0::room::create_room; use api::r0::session::login; use api::r0::sync::sync_events; @@ -333,6 +343,207 @@ impl AsyncClient { Ok(response) } + /// Join a room by `RoomId`. + /// + /// Returns a `join_room_by_id::Response` consisting of the + /// joined rooms `RoomId`. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room to be joined. + pub async fn join_room_by_id(&mut self, room_id: &RoomId) -> Result { + let request = join_room_by_id::Request { + room_id: room_id.clone(), + third_party_signed: None, + }; + self.send(request).await + } + + /// Join a room by `RoomId`. + /// + /// Returns a `join_room_by_id_or_alias::Response` consisting of the + /// joined rooms `RoomId`. + /// + /// # Arguments + /// + /// * alias - The `RoomId` or `RoomAliasId` of the room to be joined. + /// An alias looks like this `#name:example.com` + pub async fn join_room_by_id_or_alias( + &mut self, + alias: &RoomIdOrAliasId, + ) -> Result { + let request = join_room_by_id_or_alias::Request { + room_id_or_alias: alias.clone(), + third_party_signed: None, + }; + self.send(request).await + } + + /// Kick a user out of the specified room. + /// + /// Returns a `kick_user::Response`, an empty response. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room the user should be kicked out of. + /// + /// * user_id - The `UserId` of the user that should be kicked out of the room. + /// + /// * reason - Optional reason why the room member is being kicked out. + pub async fn kick_user( + &mut self, + room_id: &RoomId, + user_id: &UserId, + reason: Option, + ) -> Result { + let request = kick_user::Request { + reason, + room_id: room_id.clone(), + user_id: user_id.clone(), + }; + self.send(request).await + } + + /// Leave the specified room. + /// + /// Returns a `leave_room::Response`, an empty response. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room to leave. + /// + pub async fn leave_room(&mut self, room_id: &RoomId) -> Result { + let request = leave_room::Request { + room_id: room_id.clone(), + }; + self.send(request).await + } + + /// Invite the specified user by `UserId` to the given room. + /// + /// Returns a `invite_user::Response`, an empty response. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room to invite the specified user to. + /// + /// * user_id - The `UserId` of the user to invite to the room. + pub async fn invite_user_by_id( + &mut self, + room_id: &RoomId, + user_id: &UserId, + ) -> Result { + let request = invite_user::Request { + room_id: room_id.clone(), + recipient: InvitationRecipient::UserId { + user_id: user_id.clone(), + }, + }; + self.send(request).await + } + + /// Invite the specified user by third party id to the given room. + /// + /// Returns a `invite_user::Response`, an empty response. + /// + /// # Arguments + /// + /// * room_id - The `RoomId` of the room to invite the specified user to. + /// + /// * invite_id - A third party id of a user to invite to the room. + pub async fn invite_user_by_3pid( + &mut self, + room_id: &RoomId, + invite_id: &Invite3pid, + ) -> Result { + let request = invite_user::Request { + room_id: room_id.clone(), + recipient: InvitationRecipient::ThirdPartyId(invite_id.clone()), + }; + self.send(request).await + } + + /// Create a room using the `RoomBuilder` and send the request. + /// + /// Sends a request to `/_matrix/client/r0/createRoom`, returns a `create_room::Response`, + /// this is an empty response. + /// + /// # Arguments + /// + /// * room - The easiest way to create this request is using the `RoomBuilder`. + /// + /// # Examples + /// ```no_run + /// use matrix_sdk::{AsyncClient, RoomBuilder}; + /// # use matrix_sdk::api::r0::room::Visibility; + /// # use url::Url; + /// + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// let mut builder = RoomBuilder::default(); + /// builder.creation_content(false) + /// .initial_state(vec![]) + /// .visibility(Visibility::Public) + /// .name("name") + /// .room_version("v1.0"); + /// + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); + /// # use futures::executor::block_on; + /// # block_on(async { + /// assert!(cli.create_room(builder).await.is_ok()); + /// # }); + /// ``` + pub async fn create_room>( + &mut self, + room: R, + ) -> Result { + let request = room.into(); + self.send(request).await + } + + /// Get messages starting at a specific sync point using the + /// `MessagesRequestBuilder`s `from` field as a starting point. + /// + /// Sends a request to `/_matrix/client/r0/rooms/{room_id}/messages` and + /// returns a `get_message_events::IncomingResponse` that contains chunks + /// of `RoomEvents`. + /// + /// # Arguments + /// + /// * request - The easiest way to create a `Request` is using the + /// `MessagesRequestBuilder`. + /// + /// # Examples + /// ```no_run + /// # use std::convert::TryFrom; + /// use matrix_sdk::{AsyncClient, MessagesRequestBuilder}; + /// # use matrix_sdk::identifiers::RoomId; + /// # use matrix_sdk::api::r0::filter::RoomEventFilter; + /// # use matrix_sdk::api::r0::message::get_message_events::Direction; + /// # use url::Url; + /// # use js_int::UInt; + /// + /// # let homeserver = Url::parse("http://example.com").unwrap(); + /// let mut builder = MessagesRequestBuilder::new(); + /// builder.room_id(RoomId::try_from("!roomid:example.com").unwrap()) + /// .from("t47429-4392820_219380_26003_2265".to_string()) + /// .to("t4357353_219380_26003_2265".to_string()) + /// .direction(Direction::Backward) + /// .limit(UInt::new(10).unwrap()); + /// + /// let mut cli = AsyncClient::new(homeserver, None).unwrap(); + /// # use futures::executor::block_on; + /// # block_on(async { + /// assert!(cli.room_messages(builder).await.is_ok()); + /// # }); + /// ``` + pub async fn room_messages>( + &mut self, + request: R, + ) -> Result { + let req = request.into(); + self.send(req).await + } + /// Synchronize the client's state with the latest state on the server. /// /// # Arguments @@ -635,6 +846,10 @@ impl AsyncClient { /// /// * `content` - The content of the message event. /// + /// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent` held + /// in it's unsigned field as `transaction_id`. If not given one is created for the + /// message. + /// /// # Example /// ```no_run /// # use matrix_sdk::Room; @@ -649,6 +864,7 @@ impl AsyncClient { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); /// # let mut client = AsyncClient::new(homeserver, None).unwrap(); /// # let room_id = RoomId::try_from("!test:localhost").unwrap(); + /// use uuid::Uuid; /// /// let content = MessageEventContent::Text(TextMessageEventContent { /// body: "Hello world".to_owned(), @@ -656,14 +872,15 @@ impl AsyncClient { /// formatted_body: None, /// relates_to: None, /// }); - /// - /// client.room_send(&room_id, content).await.unwrap(); + /// let txn_id = Uuid::new_v4(); + /// client.room_send(&room_id, content, Some(txn_id)).await.unwrap(); /// }) /// ``` pub async fn room_send( &mut self, room_id: &RoomId, #[allow(unused_mut)] mut content: MessageEventContent, + txn_id: Option, ) -> Result { #[allow(unused_mut)] let mut event_type = EventType::RoomMessage; @@ -722,7 +939,7 @@ impl AsyncClient { let request = create_message_event::Request { room_id: room_id.clone(), event_type, - txn_id: Uuid::new_v4().to_string(), + txn_id: txn_id.unwrap_or_else(Uuid::new_v4).to_string(), data: content, }; diff --git a/src/crypto/machine.rs b/src/crypto/machine.rs index d3fe2972..703f6836 100644 --- a/src/crypto/machine.rs +++ b/src/crypto/machine.rs @@ -18,7 +18,6 @@ use std::mem; #[cfg(feature = "sqlite-cryptostore")] use std::path::Path; use std::result::Result as StdResult; -use std::sync::Arc; use uuid::Uuid; use super::error::{OlmError, Result, SignatureError, VerificationResult}; @@ -34,7 +33,6 @@ use api::r0::keys; use cjson; use olm_rs::{session::OlmMessage, utility::OlmUtility}; use serde_json::{json, Value}; -use tokio::sync::Mutex; use tracing::{debug, error, info, instrument, trace, warn}; use ruma_client_api::r0::client_exchange::{ @@ -658,19 +656,17 @@ impl OlmMachine { return Ok(None); }; - for session in &*sessions.lock().await { + for session in &mut *sessions.lock().await { let mut matches = false; - let mut session_lock = session.lock().await; - if let OlmMessage::PreKey(m) = &message { - matches = session_lock.matches(sender_key, m.clone())?; + matches = session.matches(sender_key, m.clone()).await?; if !matches { continue; } } - let ret = session_lock.decrypt(message.clone()); + let ret = session.decrypt(message.clone()).await; if let Ok(p) = ret { self.store.save_session(session.clone()).await?; @@ -706,7 +702,7 @@ impl OlmMachine { } }; - let plaintext = session.decrypt(message)?; + let plaintext = session.decrypt(message).await?; self.store.add_and_save_session(session).await?; plaintext }; @@ -861,7 +857,7 @@ impl OlmMachine { async fn olm_encrypt( &mut self, - session: Arc>, + mut session: Session, recipient_device: &Device, event_type: EventType, content: Value, @@ -892,7 +888,7 @@ impl OlmMachine { let plaintext = cjson::to_string(&payload) .unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", payload))); - let ciphertext = session.lock().await.encrypt(&plaintext).to_tuple(); + let ciphertext = session.encrypt(&plaintext).await.to_tuple(); self.store.save_session(session).await?; let message_type: usize = ciphertext.0.into(); diff --git a/src/crypto/memory_stores.rs b/src/crypto/memory_stores.rs index 89704ad1..413e1e06 100644 --- a/src/crypto/memory_stores.rs +++ b/src/crypto/memory_stores.rs @@ -24,7 +24,7 @@ use crate::identifiers::{DeviceId, RoomId, UserId}; #[derive(Debug)] pub struct SessionStore { - entries: HashMap>>>>>, + entries: HashMap>>>, } impl SessionStore { @@ -34,25 +34,24 @@ impl SessionStore { } } - pub async fn add(&mut self, session: Session) -> Arc> { - if !self.entries.contains_key(&session.sender_key) { + pub async fn add(&mut self, session: Session) -> Session { + if !self.entries.contains_key(&*session.sender_key) { self.entries.insert( - session.sender_key.to_owned(), + session.sender_key.to_string(), Arc::new(Mutex::new(Vec::new())), ); } - let sessions = self.entries.get_mut(&session.sender_key).unwrap(); - let session = Arc::new(Mutex::new(session)); + let sessions = self.entries.get_mut(&*session.sender_key).unwrap(); sessions.lock().await.push(session.clone()); session } - pub fn get(&self, sender_key: &str) -> Option>>>>> { + pub fn get(&self, sender_key: &str) -> Option>>> { self.entries.get(sender_key).cloned() } - pub fn set_for_sender(&mut self, sender_key: &str, sessions: Vec>>) { + pub fn set_for_sender(&mut self, sender_key: &str, sessions: Vec) { self.entries .insert(sender_key.to_owned(), Arc::new(Mutex::new(sessions))); } diff --git a/src/crypto/olm.rs b/src/crypto/olm.rs index bf5e69ea..1fed7697 100644 --- a/src/crypto/olm.rs +++ b/src/crypto/olm.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::fmt; +use std::mem; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Instant; @@ -170,12 +171,14 @@ impl Account { .create_outbound_session(their_identity_key, &their_one_time_key.key)?; let now = Instant::now(); + let session_id = session.session_id(); Ok(Session { - inner: session, - sender_key: their_identity_key.to_owned(), - creation_time: now.clone(), - last_use_time: now, + inner: Arc::new(Mutex::new(session)), + session_id: Arc::new(session_id), + sender_key: Arc::new(their_identity_key.to_owned()), + creation_time: Arc::new(now.clone()), + last_use_time: Arc::new(now), }) } @@ -209,12 +212,14 @@ impl Account { ); let now = Instant::now(); + let session_id = session.session_id(); Ok(Session { - inner: session, - sender_key: their_identity_key.to_owned(), - creation_time: now.clone(), - last_use_time: now, + inner: Arc::new(Mutex::new(session)), + session_id: Arc::new(session_id), + sender_key: Arc::new(their_identity_key.to_owned()), + creation_time: Arc::new(now.clone()), + last_use_time: Arc::new(now), }) } } @@ -225,16 +230,17 @@ impl PartialEq for Account { } } -#[derive(Debug)] /// The Olm Session. /// /// Sessions are used to exchange encrypted messages between two /// accounts/devices. +#[derive(Debug, Clone)] pub struct Session { - inner: OlmSession, - pub(crate) sender_key: String, - pub(crate) creation_time: Instant, - pub(crate) last_use_time: Instant, + inner: Arc>, + session_id: Arc, + pub(crate) sender_key: Arc, + pub(crate) creation_time: Arc, + pub(crate) last_use_time: Arc, } impl Session { @@ -246,9 +252,9 @@ impl Session { /// # Arguments /// /// * `message` - The Olm message that should be decrypted. - pub fn decrypt(&mut self, message: OlmMessage) -> Result { - let plaintext = self.inner.decrypt(message)?; - self.last_use_time = Instant::now(); + pub async fn decrypt(&mut self, message: OlmMessage) -> Result { + let plaintext = self.inner.lock().await.decrypt(message)?; + mem::replace(&mut self.last_use_time, Arc::new(Instant::now())); Ok(plaintext) } @@ -259,9 +265,9 @@ impl Session { /// # Arguments /// /// * `plaintext` - The plaintext that should be encrypted. - pub fn encrypt(&mut self, plaintext: &str) -> OlmMessage { - let message = self.inner.encrypt(plaintext); - self.last_use_time = Instant::now(); + pub async fn encrypt(&mut self, plaintext: &str) -> OlmMessage { + let message = self.inner.lock().await.encrypt(plaintext); + mem::replace(&mut self.last_use_time, Arc::new(Instant::now())); message } @@ -276,18 +282,20 @@ impl Session { /// that encrypted this Olm message. /// /// * `message` - The pre-key Olm message that should be checked. - pub fn matches( + pub async fn matches( &self, their_identity_key: &str, message: PreKeyMessage, ) -> Result { self.inner + .lock() + .await .matches_inbound_session_from(their_identity_key, message) } /// Returns the unique identifier for this session. - pub fn session_id(&self) -> String { - self.inner.session_id() + pub fn session_id(&self) -> &str { + &self.session_id } /// Store the session as a base64 encoded string. @@ -296,8 +304,8 @@ impl Session { /// /// * `pickle_mode` - The mode that was used to pickle the session, either /// an unencrypted mode or an encrypted using passphrase. - pub fn pickle(&self, pickle_mode: PicklingMode) -> String { - self.inner.pickle(pickle_mode) + pub async fn pickle(&self, pickle_mode: PicklingMode) -> String { + self.inner.lock().await.pickle(pickle_mode) } /// Restore a Session from a previously pickled string. @@ -328,11 +336,14 @@ impl Session { last_use_time: Instant, ) -> Result { let session = OlmSession::unpickle(pickle, pickle_mode)?; + let session_id = session.session_id(); + Ok(Session { - inner: session, - sender_key, - creation_time, - last_use_time, + inner: Arc::new(Mutex::new(session)), + session_id: Arc::new(session_id), + sender_key: Arc::new(sender_key), + creation_time: Arc::new(creation_time), + last_use_time: Arc::new(last_use_time), }) } } @@ -665,7 +676,7 @@ mod test { let plaintext = "Hello world"; - let message = bob_session.encrypt(plaintext); + let message = bob_session.encrypt(plaintext).await; let prekey_message = match message.clone() { OlmMessage::PreKey(m) => m, @@ -680,7 +691,7 @@ mod test { assert_eq!(bob_session.session_id(), alice_session.session_id()); - let decyrpted = alice_session.decrypt(message).unwrap(); + let decyrpted = alice_session.decrypt(message).await.unwrap(); assert_eq!(plaintext, decyrpted); } } diff --git a/src/crypto/store/memorystore.rs b/src/crypto/store/memorystore.rs index d14e29b7..6daa1dba 100644 --- a/src/crypto/store/memorystore.rs +++ b/src/crypto/store/memorystore.rs @@ -52,7 +52,7 @@ impl CryptoStore for MemoryStore { Ok(()) } - async fn save_session(&mut self, _: Arc>) -> Result<()> { + async fn save_session(&mut self, _: Session) -> Result<()> { Ok(()) } @@ -61,10 +61,7 @@ impl CryptoStore for MemoryStore { Ok(()) } - async fn get_sessions( - &mut self, - sender_key: &str, - ) -> Result>>>>>> { + async fn get_sessions(&mut self, sender_key: &str) -> Result>>>> { Ok(self.sessions.get(sender_key)) } diff --git a/src/crypto/store/mod.rs b/src/crypto/store/mod.rs index 016cb334..d059b3a7 100644 --- a/src/crypto/store/mod.rs +++ b/src/crypto/store/mod.rs @@ -68,12 +68,9 @@ pub trait CryptoStore: Debug + Send + Sync { async fn load_account(&mut self) -> Result>; async fn save_account(&mut self, account: Account) -> Result<()>; - async fn save_session(&mut self, session: Arc>) -> Result<()>; + async fn save_session(&mut self, session: Session) -> Result<()>; async fn add_and_save_session(&mut self, session: Session) -> Result<()>; - async fn get_sessions( - &mut self, - sender_key: &str, - ) -> Result>>>>>>; + async fn get_sessions(&mut self, sender_key: &str) -> Result>>>>; async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result; async fn get_inbound_group_session( diff --git a/src/crypto/store/sqlite.rs b/src/crypto/store/sqlite.rs index 1b57a832..cc4aea7c 100644 --- a/src/crypto/store/sqlite.rs +++ b/src/crypto/store/sqlite.rs @@ -155,7 +155,7 @@ impl SqliteStore { async fn get_sessions_for( &mut self, sender_key: &str, - ) -> Result>>>>>> { + ) -> Result>>>> { let loaded_sessions = self.sessions.get(sender_key).is_some(); if !loaded_sessions { @@ -169,7 +169,7 @@ impl SqliteStore { Ok(self.sessions.get(sender_key)) } - async fn load_sessions_for(&mut self, sender_key: &str) -> Result>>> { + async fn load_sessions_for(&mut self, sender_key: &str) -> Result> { let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; @@ -196,15 +196,15 @@ impl SqliteStore { .checked_sub(serde_json::from_str::(&row.3)?) .ok_or(CryptoStoreError::SessionTimestampError)?; - Ok(Arc::new(Mutex::new(Session::from_pickle( + Ok(Session::from_pickle( pickle.to_string(), self.get_pickle_mode(), sender_key.to_string(), creation_time, last_use_time, - )?))) + )?) }) - .collect::>>>>()?) + .collect::>>()?) } async fn load_inbound_group_sessions(&self) -> Result> { @@ -322,15 +322,13 @@ impl CryptoStore for SqliteStore { Ok(()) } - async fn save_session(&mut self, session: Arc>) -> Result<()> { + async fn save_session(&mut self, session: Session) -> Result<()> { let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; - let session = session.lock().await; - let session_id = session.session_id(); let creation_time = serde_json::to_string(&session.creation_time.elapsed())?; let last_use_time = serde_json::to_string(&session.last_use_time.elapsed())?; - let pickle = session.pickle(self.get_pickle_mode()); + let pickle = session.pickle(self.get_pickle_mode()).await; let mut connection = self.connection.lock().await; @@ -341,9 +339,9 @@ impl CryptoStore for SqliteStore { ) .bind(&session_id) .bind(&account_id) - .bind(&creation_time) - .bind(&last_use_time) - .bind(&session.sender_key) + .bind(&*creation_time) + .bind(&*last_use_time) + .bind(&*session.sender_key) .bind(&pickle) .execute(&mut *connection) .await?; @@ -357,10 +355,7 @@ impl CryptoStore for SqliteStore { Ok(()) } - async fn get_sessions( - &mut self, - sender_key: &str, - ) -> Result>>>>>> { + async fn get_sessions(&mut self, sender_key: &str) -> Result>>>> { Ok(self.get_sessions_for(sender_key).await?) } @@ -565,7 +560,6 @@ mod test { async fn save_session() { let mut store = get_store().await; let (account, session) = get_account_and_session().await; - let session = Arc::new(Mutex::new(session)); assert!(store.save_session(session.clone()).await.is_err()); @@ -581,22 +575,19 @@ mod test { async fn load_sessions() { let mut store = get_store().await; let (account, session) = get_account_and_session().await; - let session = Arc::new(Mutex::new(session)); store .save_account(account.clone()) .await .expect("Can't save account"); store.save_session(session.clone()).await.unwrap(); - let sess = session.lock().await; - let sessions = store - .load_sessions_for(&sess.sender_key) + .load_sessions_for(&session.sender_key) .await .expect("Can't load sessions"); let loaded_session = &sessions[0]; - assert_eq!(*sess, *loaded_session.lock().await); + assert_eq!(&session, loaded_session); } #[tokio::test] @@ -604,7 +595,7 @@ mod test { let mut store = get_store().await; let (account, session) = get_account_and_session().await; let sender_key = session.sender_key.to_owned(); - let session_id = session.session_id(); + let session_id = session.session_id().to_owned(); store .save_account(account.clone()) @@ -616,7 +607,7 @@ mod test { let sessions_lock = sessions.lock().await; let session = &sessions_lock[0]; - assert_eq!(session_id, *session.lock().await.session_id()); + assert_eq!(session_id, session.session_id()); } #[tokio::test] diff --git a/src/lib.rs b/src/lib.rs index 28bf9375..b3c13bdb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ mod base_client; mod error; mod event_emitter; mod models; +mod request_builder; mod session; #[cfg(test)] @@ -50,5 +51,6 @@ pub use async_client::{AsyncClient, AsyncClientConfig, SyncSettings}; pub use base_client::Client; pub use event_emitter::EventEmitter; pub use models::Room; +pub use request_builder::{MessagesRequestBuilder, RoomBuilder}; pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/request_builder.rs b/src/request_builder.rs new file mode 100644 index 00000000..42f088f5 --- /dev/null +++ b/src/request_builder.rs @@ -0,0 +1,379 @@ +use crate::api; +use crate::events::room::power_levels::PowerLevelsEventContent; +use crate::identifiers::{RoomId, UserId}; +use api::r0::filter::RoomEventFilter; +use api::r0::membership::Invite3pid; +use api::r0::message::get_message_events::{self, Direction}; +use api::r0::room::{ + create_room::{self, CreationContent, InitialStateEvent, RoomPreset}, + Visibility, +}; + +use js_int::UInt; + +/// A builder used to create rooms. +/// +/// # Examples +/// ``` +/// # use std::convert::TryFrom; +/// # use matrix_sdk::{AsyncClient, RoomBuilder}; +/// # use matrix_sdk::api::r0::room::Visibility; +/// # use matrix_sdk::identifiers::UserId; +/// # use url::Url; +/// # let homeserver = Url::parse("http://example.com").unwrap(); +/// # let mut rt = tokio::runtime::Runtime::new().unwrap(); +/// # rt.block_on(async { +/// let mut builder = RoomBuilder::default(); +/// builder.creation_content(false) +/// .initial_state(vec![]) +/// .visibility(Visibility::Public) +/// .name("name") +/// .room_version("v1.0"); +/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); +/// cli.create_room(builder).await; +/// # }) +/// ``` +#[derive(Clone, Default)] +pub struct RoomBuilder { + /// Extra keys to be added to the content of the `m.room.create`. + creation_content: Option, + /// List of state events to send to the new room. + /// + /// Takes precedence over events set by preset, but gets overriden by + /// name and topic keys. + initial_state: Vec, + /// A list of user IDs to invite to the room. + /// + /// This will tell the server to invite everyone in the list to the newly created room. + invite: Vec, + /// List of third party IDs of users to invite. + invite_3pid: Vec, + /// If set, this sets the `is_direct` flag on room invites. + is_direct: Option, + /// If this is included, an `m.room.name` event will be sent into the room to indicate + /// the name of the room. + name: Option, + /// Power level content to override in the default power level event. + power_level_content_override: Option, + /// Convenience parameter for setting various default state events based on a preset. + preset: Option, + /// The desired room alias local part. + room_alias_name: Option, + /// Room version to set for the room. Defaults to homeserver's default if not specified. + room_version: Option, + /// If this is included, an `m.room.topic` event will be sent into the room to indicate + /// the topic for the room. + topic: Option, + /// A public visibility indicates that the room will be shown in the published room + /// list. A private visibility will hide the room from the published room list. Rooms + /// default to private visibility if this key is not included. + visibility: Option, +} + +impl RoomBuilder { + /// Returns an empty `RoomBuilder` for creating rooms. + pub fn new() -> Self { + Self::default() + } + + /// Set the `CreationContent`. + /// + /// Weather users on other servers can join this room. + pub fn creation_content(&mut self, federate: bool) -> &mut Self { + let federate = Some(federate); + self.creation_content = Some(CreationContent { federate }); + self + } + + /// Set the `InitialStateEvent` vector. + pub fn initial_state(&mut self, state: Vec) -> &mut Self { + self.initial_state = state; + self + } + + /// Set the vec of `UserId`s. + pub fn invite(&mut self, invite: Vec) -> &mut Self { + self.invite = invite; + self + } + + /// Set the vec of `Invite3pid`s. + pub fn invite_3pid(&mut self, invite: Vec) -> &mut Self { + self.invite_3pid = invite; + self + } + + /// Set the vec of `Invite3pid`s. + pub fn is_direct(&mut self, direct: bool) -> &mut Self { + self.is_direct = Some(direct); + self + } + + /// Set the room name. A `m.room.name` event will be sent to the room. + pub fn name>(&mut self, name: S) -> &mut Self { + self.name = Some(name.into()); + self + } + + /// Set the room's power levels. + pub fn power_level_override(&mut self, power: PowerLevelsEventContent) -> &mut Self { + self.power_level_content_override = Some(power); + self + } + + /// Convenience for setting various default state events based on a preset. + pub fn preset(&mut self, preset: RoomPreset) -> &mut Self { + self.preset = Some(preset); + self + } + + /// The local part of a room alias. + pub fn room_alias_name>(&mut self, alias: S) -> &mut Self { + self.room_alias_name = Some(alias.into()); + self + } + + /// Room version, defaults to homeserver's version if left unspecified. + pub fn room_version>(&mut self, version: S) -> &mut Self { + self.room_version = Some(version.into()); + self + } + + /// If included, a `m.room.topic` event will be sent to the room. + pub fn topic>(&mut self, topic: S) -> &mut Self { + self.topic = Some(topic.into()); + self + } + + /// A public visibility indicates that the room will be shown in the published + /// room list. A private visibility will hide the room from the published room list. + /// Rooms default to private visibility if this key is not included. + pub fn visibility(&mut self, vis: Visibility) -> &mut Self { + self.visibility = Some(vis); + self + } +} + +impl Into for RoomBuilder { + fn into(self) -> create_room::Request { + create_room::Request { + creation_content: self.creation_content, + initial_state: self.initial_state, + invite: self.invite, + invite_3pid: self.invite_3pid, + is_direct: self.is_direct, + name: self.name, + power_level_content_override: self.power_level_content_override, + preset: self.preset, + room_alias_name: self.room_alias_name, + room_version: self.room_version, + topic: self.topic, + visibility: self.visibility, + } + } +} + +/// Create a builder for making get_message_event requests. +/// +/// # Examples +/// ``` +/// # use matrix_sdk::{AsyncClient, MessagesRequestBuilder}; +/// # use matrix_sdk::api::r0::message::get_message_events::{self, Direction}; +/// # use matrix_sdk::identifiers::RoomId; +/// # use url::Url; +/// # let homeserver = Url::parse("http://example.com").unwrap(); +/// # let mut rt = tokio::runtime::Runtime::new().unwrap(); +/// # rt.block_on(async { +/// # let room_id = RoomId::new(homeserver.as_str()).unwrap(); +/// # let last_sync_token = "".to_string();; +/// let mut cli = AsyncClient::new(homeserver, None).unwrap(); +/// +/// let mut builder = MessagesRequestBuilder::new(); +/// builder.room_id(room_id) +/// .from(last_sync_token) +/// .direction(Direction::Forward); +/// +/// cli.room_messages(builder).await.is_err(); +/// # }) +/// ``` +#[derive(Clone, Default)] +pub struct MessagesRequestBuilder { + /// The room to get events from. + room_id: Option, + /// The token to start returning events from. + /// + /// This token can be obtained from a + /// prev_batch token returned for each room by the sync API, or from a start or end token + /// returned by a previous request to this endpoint. + from: Option, + /// The token to stop returning events at. + /// + /// This token can be obtained from a prev_batch + /// token returned for each room by the sync endpoint, or from a start or end token returned + /// by a previous request to this endpoint. + to: Option, + /// The direction to return events from. + direction: Option, + /// The maximum number of events to return. + /// + /// Default: 10. + limit: Option, + /// A filter of the returned events with. + filter: Option, +} + +impl MessagesRequestBuilder { + /// Create a `MessagesRequestBuilder` builder to make a `get_message_events::Request`. + /// + /// The `room_id` and `from`` fields **need to be set** to create the request. + pub fn new() -> Self { + Self::default() + } + + /// RoomId is required to create a `get_message_events::Request`. + pub fn room_id(&mut self, room_id: RoomId) -> &mut Self { + self.room_id = Some(room_id); + self + } + + /// A `next_batch` token or `start` or `end` from a previous `get_message_events` request. + /// + /// This is required to create a `get_message_events::Request`. + pub fn from(&mut self, from: String) -> &mut Self { + self.from = Some(from); + self + } + + /// A `next_batch` token or `start` or `end` from a previous `get_message_events` request. + /// + /// This token signals when to stop receiving events. + pub fn to(&mut self, to: String) -> &mut Self { + self.to = Some(to); + self + } + + /// The direction to return events from. + /// + /// If not specified `Direction::Backward` is used. + pub fn direction(&mut self, direction: Direction) -> &mut Self { + self.direction = Some(direction); + self + } + + /// The maximum number of events to return. + pub fn limit(&mut self, limit: UInt) -> &mut Self { + self.limit = Some(limit); + self + } + + /// Filter events by the given `RoomEventFilter`. + pub fn filter(&mut self, filter: RoomEventFilter) -> &mut Self { + self.filter = Some(filter); + self + } +} + +impl Into for MessagesRequestBuilder { + fn into(self) -> get_message_events::Request { + get_message_events::Request { + room_id: self.room_id.expect("`room_id` and `from` need to be set"), + from: self.from.expect("`room_id` and `from` need to be set"), + to: self.to, + dir: self.direction.unwrap_or(Direction::Backward), + limit: self.limit, + filter: self.filter, + } + } +} + +#[cfg(test)] +mod test { + use std::collections::HashMap; + + use super::*; + use crate::events::room::power_levels::NotificationPowerLevels; + use crate::{identifiers::RoomId, AsyncClient, Session}; + + use js_int::Int; + use mockito::{mock, Matcher}; + use std::convert::TryFrom; + use url::Url; + + #[tokio::test] + async fn create_room_builder() { + let homeserver = Url::parse(&mockito::server_url()).unwrap(); + + let _m = mock("POST", "/_matrix/client/r0/createRoom") + .with_status(200) + .with_body_from_file("./tests/data/room_id.json") + .create(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:localhost").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let mut builder = RoomBuilder::new(); + builder + .creation_content(false) + .initial_state(vec![]) + .visibility(Visibility::Public) + .name("room_name") + .room_version("v1.0") + .invite_3pid(vec![]) + .is_direct(true) + .power_level_override(PowerLevelsEventContent { + ban: Int::max_value(), + events: HashMap::default(), + events_default: Int::min_value(), + invite: Int::min_value(), + kick: Int::min_value(), + redact: Int::max_value(), + state_default: Int::min_value(), + users_default: Int::min_value(), + notifications: NotificationPowerLevels { + room: Int::min_value(), + }, + users: HashMap::default(), + }) + .preset(RoomPreset::PrivateChat) + .room_alias_name("room_alias") + .topic("room topic") + .visibility(Visibility::Private); + let mut cli = AsyncClient::new(homeserver, Some(session)).unwrap(); + assert!(cli.create_room(builder).await.is_ok()); + } + + #[tokio::test] + async fn get_message_events() { + let homeserver = Url::parse(&mockito::server_url()).unwrap(); + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/messages".to_string()), + ) + .with_status(200) + .with_body_from_file("./tests/data/room_messages.json") + .create(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:localhost").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let mut builder = MessagesRequestBuilder::new(); + builder + .room_id(RoomId::try_from("!roomid:example.com").unwrap()) + .from("t47429-4392820_219380_26003_2265".to_string()) + .to("t4357353_219380_26003_2265".to_string()) + .direction(Direction::Backward) + .limit(UInt::new(10).unwrap()); + // TODO this makes ruma error `Err(IntoHttp(IntoHttpError(Query(Custom("unsupported value")))))`?? + // .filter(RoomEventFilter::default()); + + let mut cli = AsyncClient::new(homeserver, Some(session)).unwrap(); + assert!(cli.room_messages(builder).await.is_ok()); + } +} diff --git a/tests/data/events/room_avatar.json b/tests/data/events/room_avatar.json index b92f9161..92ed2519 100644 --- a/tests/data/events/room_avatar.json +++ b/tests/data/events/room_avatar.json @@ -18,4 +18,3 @@ "age": 1234 } } - diff --git a/tests/data/sync2.json b/tests/data/sync2.json deleted file mode 100644 index 6adb2c13..00000000 --- a/tests/data/sync2.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "next_batch": "s72595_4483_1934", - "presence": { - "events": [ - { - "content": { - "avatar_url": "mxc://localhost:wefuiwegh8742w", - "last_active_ago": 2478593, - "presence": "online", - "currently_active": false, - "status_msg": "Making cupcakes" - }, - "type": "m.presence", - "sender": "@example:localhost" - } - ] - }, - "account_data": { - "events": [ - { - "type": "org.example.custom.config", - "content": { - "custom_config_key": "custom_config_value" - } - } - ] - }, - "rooms": { - "join": { - "!726s6s6q:example.com": { - "summary": { - "m.heroes": [ - "@alice:example.com", - "@bob:example.com" - ], - "m.joined_member_count": 2, - "m.invited_member_count": 0 - }, - "state": { - "events": [ - { - "content": { - "membership": "join", - "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", - "displayname": "Alice Margatroid" - }, - "type": "m.room.member", - "event_id": "$143273582443PhrSn:example.org", - "room_id": "!726s6s6q:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": { - "age": 1234 - }, - "state_key": "@alice:example.org" - } - ] - }, - "timeline": { - "events": [ - { - "content": { - "membership": "join", - "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", - "displayname": "Alice Margatroid" - }, - "type": "m.room.member", - "event_id": "$143273582443PhrSn:example.org", - "room_id": "!726s6s6q:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": { - "age": 1234 - }, - "state_key": "@alice:example.org" - }, - { - "content": { - "body": "This is an example text message", - "msgtype": "m.text", - "format": "org.matrix.custom.html", - "formatted_body": "This is an example text message" - }, - "type": "m.room.message", - "event_id": "$143273582443PhrSn:example.org", - "room_id": "!726s6s6q:example.com", - "sender": "@example:example.org", - "origin_server_ts": 1432735824653, - "unsigned": { - "age": 1234 - } - } - ], - "limited": true, - "prev_batch": "t34-23535_0_0" - }, - "ephemeral": { - "events": [ - { - "content": { - "user_ids": [ - "@alice:matrix.org", - "@bob:example.com" - ] - }, - "type": "m.typing", - "room_id": "!jEsUZKDJdhlrceRyVU:example.org" - } - ] - }, - "account_data": { - "events": [ - { - "content": { - "tags": { - "u.work": { - "order": 0.9 - } - } - }, - "type": "m.tag" - }, - { - "type": "org.example.custom.room.config", - "content": { - "custom_config_key": "custom_config_value" - } - } - ] - } - } - }, - "invite": { - "!696r7674:example.com": { - "invite_state": { - "events": [ - { - "sender": "@alice:example.com", - "type": "m.room.name", - "state_key": "", - "content": { - "name": "My Room Name" - } - }, - { - "sender": "@alice:example.com", - "type": "m.room.member", - "state_key": "@bob:example.com", - "content": { - "membership": "invite" - } - } - ] - } - } - }, - "leave": {} - } - } - \ No newline at end of file