Merge branch 'master' of https://github.com/matrix-org/matrix-rust-sdk into bot-example

master
Devin R 2020-04-14 08:41:31 -04:00
commit 018ffaf1a8
12 changed files with 677 additions and 249 deletions

View File

@ -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 ...

View File

@ -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<join_room_by_id::Response> {
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<join_room_by_id_or_alias::Response> {
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<String>,
) -> Result<kick_user::Response> {
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<leave_room::Response> {
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<invite_user::Response> {
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<invite_user::Response> {
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<R: Into<create_room::Request>>(
&mut self,
room: R,
) -> Result<create_room::Response> {
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<R: Into<get_message_events::Request>>(
&mut self,
request: R,
) -> Result<get_message_events::IncomingResponse> {
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<Uuid>,
) -> Result<create_message_event::Response> {
#[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,
};

View File

@ -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<Mutex<Session>>,
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();

View File

@ -24,7 +24,7 @@ use crate::identifiers::{DeviceId, RoomId, UserId};
#[derive(Debug)]
pub struct SessionStore {
entries: HashMap<String, Arc<Mutex<Vec<Arc<Mutex<Session>>>>>>,
entries: HashMap<String, Arc<Mutex<Vec<Session>>>>,
}
impl SessionStore {
@ -34,25 +34,24 @@ impl SessionStore {
}
}
pub async fn add(&mut self, session: Session) -> Arc<Mutex<Session>> {
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<Arc<Mutex<Vec<Arc<Mutex<Session>>>>>> {
pub fn get(&self, sender_key: &str) -> Option<Arc<Mutex<Vec<Session>>>> {
self.entries.get(sender_key).cloned()
}
pub fn set_for_sender(&mut self, sender_key: &str, sessions: Vec<Arc<Mutex<Session>>>) {
pub fn set_for_sender(&mut self, sender_key: &str, sessions: Vec<Session>) {
self.entries
.insert(sender_key.to_owned(), Arc::new(Mutex::new(sessions)));
}

View File

@ -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<Mutex<OlmSession>>,
session_id: Arc<String>,
pub(crate) sender_key: Arc<String>,
pub(crate) creation_time: Arc<Instant>,
pub(crate) last_use_time: Arc<Instant>,
}
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<String, OlmSessionError> {
let plaintext = self.inner.decrypt(message)?;
self.last_use_time = Instant::now();
pub async fn decrypt(&mut self, message: OlmMessage) -> Result<String, OlmSessionError> {
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<bool, OlmSessionError> {
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<Self, OlmSessionError> {
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);
}
}

View File

@ -52,7 +52,7 @@ impl CryptoStore for MemoryStore {
Ok(())
}
async fn save_session(&mut self, _: Arc<Mutex<Session>>) -> 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<Option<Arc<Mutex<Vec<Arc<Mutex<Session>>>>>>> {
async fn get_sessions(&mut self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
Ok(self.sessions.get(sender_key))
}

View File

@ -68,12 +68,9 @@ pub trait CryptoStore: Debug + Send + Sync {
async fn load_account(&mut self) -> Result<Option<Account>>;
async fn save_account(&mut self, account: Account) -> Result<()>;
async fn save_session(&mut self, session: Arc<Mutex<Session>>) -> 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<Option<Arc<Mutex<Vec<Arc<Mutex<Session>>>>>>>;
async fn get_sessions(&mut self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>>;
async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result<bool>;
async fn get_inbound_group_session(

View File

@ -155,7 +155,7 @@ impl SqliteStore {
async fn get_sessions_for(
&mut self,
sender_key: &str,
) -> Result<Option<Arc<Mutex<Vec<Arc<Mutex<Session>>>>>>> {
) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
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<Vec<Arc<Mutex<Session>>>> {
async fn load_sessions_for(&mut self, sender_key: &str) -> Result<Vec<Session>> {
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::<Duration>(&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::<Result<Vec<Arc<Mutex<Session>>>>>()?)
.collect::<Result<Vec<Session>>>()?)
}
async fn load_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
@ -322,15 +322,13 @@ impl CryptoStore for SqliteStore {
Ok(())
}
async fn save_session(&mut self, session: Arc<Mutex<Session>>) -> 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<Option<Arc<Mutex<Vec<Arc<Mutex<Session>>>>>>> {
async fn get_sessions(&mut self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
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]

View File

@ -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");

379
src/request_builder.rs Normal file
View File

@ -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<CreationContent>,
/// 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<InitialStateEvent>,
/// 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<UserId>,
/// List of third party IDs of users to invite.
invite_3pid: Vec<Invite3pid>,
/// If set, this sets the `is_direct` flag on room invites.
is_direct: Option<bool>,
/// If this is included, an `m.room.name` event will be sent into the room to indicate
/// the name of the room.
name: Option<String>,
/// Power level content to override in the default power level event.
power_level_content_override: Option<PowerLevelsEventContent>,
/// Convenience parameter for setting various default state events based on a preset.
preset: Option<RoomPreset>,
/// The desired room alias local part.
room_alias_name: Option<String>,
/// Room version to set for the room. Defaults to homeserver's default if not specified.
room_version: Option<String>,
/// If this is included, an `m.room.topic` event will be sent into the room to indicate
/// the topic for the room.
topic: Option<String>,
/// 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<Visibility>,
}
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<InitialStateEvent>) -> &mut Self {
self.initial_state = state;
self
}
/// Set the vec of `UserId`s.
pub fn invite(&mut self, invite: Vec<UserId>) -> &mut Self {
self.invite = invite;
self
}
/// Set the vec of `Invite3pid`s.
pub fn invite_3pid(&mut self, invite: Vec<Invite3pid>) -> &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<S: Into<String>>(&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<S: Into<String>>(&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<S: Into<String>>(&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<S: Into<String>>(&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<create_room::Request> 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<RoomId>,
/// 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<String>,
/// 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<String>,
/// The direction to return events from.
direction: Option<Direction>,
/// The maximum number of events to return.
///
/// Default: 10.
limit: Option<UInt>,
/// A filter of the returned events with.
filter: Option<RoomEventFilter>,
}
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<get_message_events::Request> 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());
}
}

View File

@ -18,4 +18,3 @@
"age": 1234
}
}

View File

@ -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": "<b>This is an example text message</b>"
},
"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": {}
}
}