diff --git a/Cargo.toml b/Cargo.toml index 57e18193..fc993d0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "matrix_sdk_test_macros", "matrix_sdk_crypto", "matrix_sdk_common", + "matrix_sdk_common_macros", ] diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index b6fdfddd..d970ef84 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["Damir Jelić "] description = "A high level Matrix client-server library." edition = "2018" homepage = "https://github.com/matrix-org/matrix-rust-sdk" @@ -25,7 +25,7 @@ tracing = "0.1.14" url = "2.1.1" futures-timer = { version = "3.0.2", features = ["wasm-bindgen"] } - +matrix-sdk-common-macros = { version = "0.1.0", path = "../matrix_sdk_common_macros" } matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } [dependencies.matrix-sdk-base] diff --git a/matrix_sdk/examples/command_bot.rs b/matrix_sdk/examples/command_bot.rs index a0e655b7..a89e2219 100644 --- a/matrix_sdk/examples/command_bot.rs +++ b/matrix_sdk/examples/command_bot.rs @@ -5,6 +5,7 @@ use matrix_sdk::{ events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent}, Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings, }; +use matrix_sdk_common_macros::async_trait; use url::Url; struct CommandBot { @@ -19,7 +20,7 @@ impl CommandBot { } } -#[async_trait::async_trait] +#[async_trait] impl EventEmitter for CommandBot { async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) { if let SyncRoom::Joined(room) = room { diff --git a/matrix_sdk/examples/login.rs b/matrix_sdk/examples/login.rs index 10251fb8..8eb30cb3 100644 --- a/matrix_sdk/examples/login.rs +++ b/matrix_sdk/examples/login.rs @@ -6,10 +6,11 @@ use matrix_sdk::{ events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent}, Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings, }; +use matrix_sdk_common_macros::async_trait; struct EventCallback; -#[async_trait::async_trait] +#[async_trait] impl EventEmitter for EventCallback { async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) { if let SyncRoom::Joined(room) = room { diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 5dd87ff1..9a9c84fe 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -17,6 +17,7 @@ use std::collections::BTreeMap; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; +use std::fmt::{self, Debug}; use std::path::Path; use std::result::Result as StdResult; use std::sync::Arc; @@ -66,8 +67,8 @@ pub struct Client { } #[cfg_attr(tarpaulin, skip)] -impl std::fmt::Debug for Client { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { +impl Debug for Client { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { write!(fmt, "Client {{ homeserver: {} }}", self.homeserver) } } @@ -106,8 +107,8 @@ pub struct ClientConfig { } #[cfg_attr(tarpaulin, skip)] -impl std::fmt::Debug for ClientConfig { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> { +impl Debug for ClientConfig { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { let mut res = fmt.debug_struct("ClientConfig"); #[cfg(not(target_arch = "wasm32"))] @@ -246,6 +247,7 @@ impl SyncSettings { } } +use api::r0::account::register; #[cfg(feature = "encryption")] use api::r0::keys::{claim_keys, get_keys, upload_keys, KeyAlgorithm}; use api::r0::membership::{ @@ -263,6 +265,7 @@ use api::r0::sync::sync_events; #[cfg(feature = "encryption")] use api::r0::to_device::send_event_to_device; use api::r0::typing::create_typing_event; +use api::r0::uiaa::UiaaResponse; impl Client { /// Creates a new client for making HTTP requests to the given homeserver. @@ -413,7 +416,7 @@ impl Client { /// device_id from a previous login call. Note that this should be done /// only if the client also holds the encryption keys for this device. #[instrument(skip(password))] - pub async fn login + std::fmt::Debug>( + pub async fn login + Debug>( &self, user: S, password: S, @@ -447,6 +450,42 @@ impl Client { Ok(self.base_client.restore_login(session).await?) } + /// Register a user to the server. + /// + /// # Arguments + /// + /// * `registration` - The easiest way to create this request is using the `RegistrationBuilder`. + /// + /// + /// # Examples + /// ``` + /// # use std::convert::TryFrom; + /// # use matrix_sdk::{Client, RegistrationBuilder}; + /// # use matrix_sdk::api::r0::account::register::RegistrationKind; + /// # use matrix_sdk::identifiers::DeviceId; + /// # 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 = RegistrationBuilder::default(); + /// builder.password("pass") + /// .username("user") + /// .kind(RegistrationKind::User); + /// let mut client = Client::new(homeserver).unwrap(); + /// client.register_user(builder).await; + /// # }) + /// ``` + #[instrument(skip(registration))] + pub async fn register_user>( + &self, + registration: R, + ) -> Result { + info!("Registering to {}", self.homeserver); + + let request = registration.into(); + self.send_uiaa(request).await + } + /// Join a room by `RoomId`. /// /// Returns a `join_room_by_id::Response` consisting of the @@ -876,7 +915,79 @@ impl Client { Ok(response) } - /// Send an arbitrary request to the server, without updating client state + async fn send_request( + &self, + requires_auth: bool, + method: HttpMethod, + request: http::Request>, + ) -> Result { + let url = request.uri(); + let path_and_query = url.path_and_query().unwrap(); + let mut url = self.homeserver.clone(); + + url.set_path(path_and_query.path()); + url.set_query(path_and_query.query()); + + let request_builder = match method { + HttpMethod::GET => self.http_client.get(url), + HttpMethod::POST => { + let body = request.body().clone(); + self.http_client + .post(url) + .body(body) + .header(reqwest::header::CONTENT_TYPE, "application/json") + } + HttpMethod::PUT => { + let body = request.body().clone(); + self.http_client + .put(url) + .body(body) + .header(reqwest::header::CONTENT_TYPE, "application/json") + } + HttpMethod::DELETE => { + let body = request.body().clone(); + self.http_client + .delete(url) + .body(body) + .header(reqwest::header::CONTENT_TYPE, "application/json") + } + method => panic!("Unsuported method {}", method), + }; + + let request_builder = if requires_auth { + let session = self.base_client.session().read().await; + + if let Some(session) = session.as_ref() { + let header_value = format!("Bearer {}", &session.access_token); + request_builder.header(AUTHORIZATION, header_value) + } else { + return Err(Error::AuthenticationRequired); + } + } else { + request_builder + }; + + Ok(request_builder.send().await?) + } + + async fn response_to_http_response( + &self, + mut response: reqwest::Response, + ) -> Result>> { + let status = response.status(); + let mut http_builder = HttpResponse::builder().status(status); + let headers = http_builder.headers_mut().unwrap(); + + for (k, v) in response.headers_mut().drain() { + if let Some(key) = k { + headers.insert(key, v); + } + } + let body = response.bytes().await?.as_ref().to_owned(); + Ok(http_builder.body(body).unwrap()) + } + + /// Send an arbitrary request to the server, without updating client state. /// /// **Warning:** Because this method *does not* update the client state, it is /// important to make sure than you account for this yourself, and use wrapper methods @@ -914,69 +1025,79 @@ impl Client { /// // returned /// # }) /// ``` - pub async fn send + std::fmt::Debug>( + pub async fn send + Debug>( &self, request: Request, ) -> Result { let request: http::Request> = request.try_into()?; - let url = request.uri(); - let path_and_query = url.path_and_query().unwrap(); - let mut url = self.homeserver.clone(); - - url.set_path(path_and_query.path()); - url.set_query(path_and_query.query()); - - trace!("Doing request {:?}", url); - - let request_builder = match Request::METADATA.method { - HttpMethod::GET => self.http_client.get(url), - HttpMethod::POST => { - let body = request.body().clone(); - self.http_client - .post(url) - .body(body) - .header(reqwest::header::CONTENT_TYPE, "application/json") - } - HttpMethod::PUT => { - let body = request.body().clone(); - self.http_client - .put(url) - .body(body) - .header(reqwest::header::CONTENT_TYPE, "application/json") - } - HttpMethod::DELETE => unimplemented!(), - _ => panic!("Unsuported method"), - }; - - let request_builder = if Request::METADATA.requires_authentication { - let session = self.base_client.session().read().await; - - if let Some(session) = session.as_ref() { - let header_value = format!("Bearer {}", &session.access_token); - request_builder.header(AUTHORIZATION, header_value) - } else { - return Err(Error::AuthenticationRequired); - } - } else { - request_builder - }; - let mut response = request_builder.send().await?; + let response = self + .send_request( + Request::METADATA.requires_authentication, + Request::METADATA.method, + request, + ) + .await?; trace!("Got response: {:?}", response); - let status = response.status(); - let mut http_builder = HttpResponse::builder().status(status); - let headers = http_builder.headers_mut().unwrap(); + let response = self.response_to_http_response(response).await?; - for (k, v) in response.headers_mut().drain() { - if let Some(key) = k { - headers.insert(key, v); - } - } - let body = response.bytes().await?.as_ref().to_owned(); - let http_response = http_builder.body(body).unwrap(); + Ok(::try_from(response)?) + } - Ok(::try_from(http_response)?) + /// Send an arbitrary request to the server, without updating client state. + /// + /// This version allows the client to make registration requests. + /// + /// **Warning:** Because this method *does not* update the client state, it is + /// important to make sure than you account for this yourself, and use wrapper methods + /// where available. This method should *only* be used if a wrapper method for the + /// endpoint you'd like to use is not available. + /// + /// # Arguments + /// + /// * `request` - This version of send is for dealing with types that return + /// a `UiaaResponse` as the `Endpoint` associated type. + /// + /// # Examples + /// ``` + /// # use std::convert::TryFrom; + /// # use matrix_sdk::{Client, RegistrationBuilder}; + /// # use matrix_sdk::api::r0::account::register::{RegistrationKind, Request}; + /// # use matrix_sdk::identifiers::DeviceId; + /// # 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 = RegistrationBuilder::default(); + /// builder.password("pass") + /// .username("user") + /// .kind(RegistrationKind::User); + /// let mut client = Client::new(homeserver).unwrap(); + /// let req: Request = builder.into(); + /// client.send_uiaa(req).await; + /// # }) + /// ``` + pub async fn send_uiaa + Debug>( + &self, + request: Request, + ) -> Result { + let request: http::Request> = request.try_into()?; + let response = self + .send_request( + Request::METADATA.requires_authentication, + Request::METADATA.method, + request, + ) + .await?; + + trace!("Got response: {:?}", response); + + let response = self.response_to_http_response(response).await?; + + let uiaa: Result<_> = ::try_from(response).map_err(Into::into); + + Ok(uiaa?) } /// Synchronize the client's state with the latest state on the server. @@ -1274,13 +1395,15 @@ impl Client { #[cfg(test)] mod test { use super::{ - ban_user, create_receipt, create_typing_event, forget_room, invite_user, kick_user, - leave_room, set_read_marker, Invite3pid, MessageEventContent, + api::r0::uiaa::AuthData, ban_user, create_receipt, create_typing_event, forget_room, + invite_user, kick_user, leave_room, register::RegistrationKind, set_read_marker, + Invite3pid, MessageEventContent, }; use super::{Client, ClientConfig, Session, SyncSettings, Url}; use crate::events::collections::all::RoomEvent; use crate::events::room::message::TextMessageEventContent; use crate::identifiers::{EventId, RoomId, RoomIdOrAliasId, UserId}; + use crate::RegistrationBuilder; use matrix_sdk_base::JsonStore; use matrix_sdk_test::{EventBuilder, EventsFile}; @@ -1461,6 +1584,44 @@ mod test { } } + #[tokio::test] + async fn register_error() { + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let _m = mock("POST", "/_matrix/client/r0/register") + .with_status(403) + .with_body_from_file("../test_data/registration_response_error.json") + .create(); + + let mut user = RegistrationBuilder::default(); + + user.username("user") + .password("password") + .auth(AuthData::FallbackAcknowledgement { + session: "foobar".to_string(), + }) + .kind(RegistrationKind::User); + + let client = Client::new(homeserver).unwrap(); + + if let Err(err) = client.register_user(user).await { + if let crate::Error::UiaaError(crate::FromHttpResponseError::Http( + // TODO this should be a UiaaError need to investigate + crate::ServerError::Unknown(e), + )) = err + { + assert!(e.to_string().starts_with("EOF while parsing")) + } else { + panic!( + "found the wrong `Error` type {:#?}, expected `ServerError::Unknown", + err + ); + } + } else { + panic!("this request should return an `Err` variant") + } + } + #[tokio::test] async fn join_room_by_id() { let homeserver = Url::from_str(&mockito::server_url()).unwrap(); diff --git a/matrix_sdk/src/error.rs b/matrix_sdk/src/error.rs index 2670a989..c293797a 100644 --- a/matrix_sdk/src/error.rs +++ b/matrix_sdk/src/error.rs @@ -20,6 +20,7 @@ use thiserror::Error; use matrix_sdk_base::Error as MatrixError; +use crate::api::r0::uiaa::UiaaResponse as UiaaError; use crate::api::Error as RumaClientError; use crate::FromHttpResponseError as RumaResponseError; use crate::IntoHttpError as RumaIntoHttpError; @@ -50,9 +51,23 @@ pub enum Error { #[error("can't convert between ruma_client_api and hyper types.")] IntoHttp(RumaIntoHttpError), - /// An error occured in the Matrix client library. + /// An error occurred in the Matrix client library. #[error(transparent)] MatrixError(#[from] MatrixError), + + /// An error occurred while authenticating. + /// + /// When registering or authenticating the Matrix server can send a `UiaaResponse` + /// as the error type, this is a User-Interactive Authentication API response. This + /// represents an error with information about how to authenticate the user. + #[error("User-Interactive Authentication required.")] + UiaaError(RumaResponseError), +} + +impl From> for Error { + fn from(error: RumaResponseError) -> Self { + Self::UiaaError(error) + } } impl From> for Error { diff --git a/matrix_sdk/src/lib.rs b/matrix_sdk/src/lib.rs index 3834a7f4..b584c255 100644 --- a/matrix_sdk/src/lib.rs +++ b/matrix_sdk/src/lib.rs @@ -36,6 +36,7 @@ unused_qualifications )] +pub use matrix_sdk_base::Error as BaseError; #[cfg(not(target_arch = "wasm32"))] pub use matrix_sdk_base::JsonStore; pub use matrix_sdk_base::{CustomOrRawEvent, EventEmitter, Room, Session, SyncRoom}; @@ -51,7 +52,7 @@ mod error; mod request_builder; pub use client::{Client, ClientConfig, SyncSettings}; pub use error::{Error, Result}; -pub use request_builder::{MessagesRequestBuilder, RoomBuilder}; +pub use request_builder::{MessagesRequestBuilder, RegistrationBuilder, RoomBuilder}; #[cfg(not(target_arch = "wasm32"))] pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/matrix_sdk/src/request_builder.rs b/matrix_sdk/src/request_builder.rs index d7c52615..e76b2644 100644 --- a/matrix_sdk/src/request_builder.rs +++ b/matrix_sdk/src/request_builder.rs @@ -1,7 +1,9 @@ use crate::api; use crate::events::room::power_levels::PowerLevelsEventContent; use crate::events::EventJson; -use crate::identifiers::{RoomId, UserId}; +use crate::identifiers::{DeviceId, RoomId, UserId}; +use api::r0::account::register; +use api::r0::account::register::RegistrationKind; use api::r0::filter::RoomEventFilter; use api::r0::membership::Invite3pid; use api::r0::message::get_message_events::{self, Direction}; @@ -9,6 +11,7 @@ use api::r0::room::{ create_room::{self, CreationContent, InitialStateEvent, RoomPreset}, Visibility, }; +use api::r0::uiaa::AuthData; use crate::js_int::UInt; @@ -288,6 +291,120 @@ impl Into for MessagesRequestBuilder { } } +/// A builder used to register users. +/// +/// # Examples +/// ``` +/// # use std::convert::TryFrom; +/// # use matrix_sdk::{Client, RegistrationBuilder}; +/// # use matrix_sdk::api::r0::account::register::RegistrationKind; +/// # use matrix_sdk::identifiers::DeviceId; +/// # 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 = RegistrationBuilder::default(); +/// builder.password("pass") +/// .username("user") +/// .kind(RegistrationKind::User); +/// let mut client = Client::new(homeserver).unwrap(); +/// client.register_user(builder).await; +/// # }) +/// ``` +#[derive(Clone, Debug, Default)] +pub struct RegistrationBuilder { + password: Option, + username: Option, + device_id: Option, + initial_device_display_name: Option, + auth: Option, + kind: Option, + inhibit_login: bool, +} + +impl RegistrationBuilder { + /// Create a `RegistrationBuilder` builder to make a `register::Request`. + /// + /// The `room_id` and `from`` fields **need to be set** to create the request. + pub fn new() -> Self { + Self::default() + } + + /// The desired password for the account. + /// + /// May be empty for accounts that should not be able to log in again + /// with a password, e.g., for guest or application service accounts. + pub fn password(&mut self, password: &str) -> &mut Self { + self.password = Some(password.to_string()); + self + } + + /// local part of the desired Matrix ID. + /// + /// If omitted, the homeserver MUST generate a Matrix ID local part. + pub fn username(&mut self, username: &str) -> &mut Self { + self.username = Some(username.to_string()); + self + } + + /// ID of the client device. + /// + /// If this does not correspond to a known client device, a new device will be created. + /// The server will auto-generate a device_id if this is not specified. + pub fn device_id(&mut self, device_id: &str) -> &mut Self { + self.device_id = Some(device_id.to_string()); + self + } + + /// A display name to assign to the newly-created device. + /// + /// Ignored if `device_id` corresponds to a known device. + pub fn initial_device_display_name(&mut self, initial_device_display_name: &str) -> &mut Self { + self.initial_device_display_name = Some(initial_device_display_name.to_string()); + self + } + + /// Additional authentication information for the user-interactive authentication API. + /// + /// Note that this information is not used to define how the registered user should be + /// authenticated, but is instead used to authenticate the register call itself. + /// It should be left empty, or omitted, unless an earlier call returned an response + /// with status code 401. + pub fn auth(&mut self, auth: AuthData) -> &mut Self { + self.auth = Some(auth); + self + } + + /// Kind of account to register + /// + /// Defaults to `User` if omitted. + pub fn kind(&mut self, kind: RegistrationKind) -> &mut Self { + self.kind = Some(kind); + self + } + + /// If `true`, an `access_token` and `device_id` should not be returned + /// from this call, therefore preventing an automatic login. + pub fn inhibit_login(&mut self, inhibit_login: bool) -> &mut Self { + self.inhibit_login = inhibit_login; + self + } +} + +impl Into for RegistrationBuilder { + fn into(self) -> register::Request { + register::Request { + password: self.password, + username: self.username, + device_id: self.device_id, + initial_device_display_name: self.initial_device_display_name, + auth: self.auth, + kind: self.kind, + inhibit_login: self.inhibit_login, + } + } +} + #[cfg(test)] mod test { use std::collections::BTreeMap; diff --git a/matrix_sdk_base/Cargo.toml b/matrix_sdk_base/Cargo.toml index c7b34878..1acb56b7 100644 --- a/matrix_sdk_base/Cargo.toml +++ b/matrix_sdk_base/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["Damir Jelić "] description = "The base component to build a Matrix client library." edition = "2018" homepage = "https://github.com/matrix-org/matrix-rust-sdk" @@ -22,6 +22,7 @@ serde = "1.0.110" serde_json = "1.0.53" zeroize = "1.1.0" +matrix-sdk-common-macros = { version = "0.1.0", path = "../matrix_sdk_common_macros" } matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } matrix-sdk-crypto = { version = "0.1.0", path = "../matrix_sdk_crypto", optional = true } diff --git a/matrix_sdk_base/src/client.rs b/matrix_sdk_base/src/client.rs index 334e873b..515ba1b3 100644 --- a/matrix_sdk_base/src/client.rs +++ b/matrix_sdk_base/src/client.rs @@ -1676,6 +1676,7 @@ impl BaseClient { NonRoomEvent::FullyRead(full_read) => { event_emitter.on_non_room_fully_read(room, &full_read).await } + NonRoomEvent::Typing(typing) => event_emitter.on_non_room_typing(room, &typing).await, _ => {} } } @@ -1732,6 +1733,7 @@ impl BaseClient { NonRoomEvent::FullyRead(full_read) => { event_emitter.on_non_room_fully_read(room, &full_read).await } + NonRoomEvent::Typing(typing) => event_emitter.on_non_room_typing(room, &typing).await, _ => {} } } @@ -1813,6 +1815,7 @@ mod test { events::{collections::all::RoomEvent, stripped::AnyStrippedStateEvent}, BaseClient, Session, }; + use matrix_sdk_common_macros::async_trait; use matrix_sdk_test::{async_test, EventBuilder, EventsFile}; use serde_json::json; use std::convert::TryFrom; @@ -1979,7 +1982,7 @@ mod test { }; struct EE(Arc); - #[async_trait::async_trait] + #[async_trait] impl EventEmitter for EE { async fn on_room_member(&self, room: SyncRoom, event: &MemberEvent) { if let SyncRoom::Joined(_) = room { @@ -2075,7 +2078,7 @@ mod test { }; struct EE(Arc); - #[async_trait::async_trait] + #[async_trait] impl EventEmitter for EE { async fn on_unrecognized_event(&self, room: SyncRoom, event: &CustomOrRawEvent<'_>) { if let SyncRoom::Joined(_) = room { @@ -2171,7 +2174,7 @@ mod test { }; struct EE(Arc); - #[async_trait::async_trait] + #[async_trait] impl EventEmitter for EE { async fn on_unrecognized_event(&self, room: SyncRoom, event: &CustomOrRawEvent<'_>) { if let SyncRoom::Joined(_) = room { diff --git a/matrix_sdk_base/src/event_emitter/mod.rs b/matrix_sdk_base/src/event_emitter/mod.rs index 951462e2..e767587d 100644 --- a/matrix_sdk_base/src/event_emitter/mod.rs +++ b/matrix_sdk_base/src/event_emitter/mod.rs @@ -43,6 +43,7 @@ use crate::events::{ CustomEvent, CustomRoomEvent, CustomStateEvent, }; use crate::{Room, RoomState}; +use matrix_sdk_common_macros::async_trait; /// Type alias for `RoomState` enum when passed to `EventEmitter` methods. pub type SyncRoom = RoomState>>; @@ -78,10 +79,11 @@ pub enum CustomOrRawEvent<'c> { /// # EventEmitter, SyncRoom /// # }; /// # use matrix_sdk_common::locks::RwLock; +/// # use matrix_sdk_common_macros::async_trait; /// /// struct EventCallback; /// -/// #[async_trait::async_trait] +/// #[async_trait] /// impl EventEmitter for EventCallback { /// async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) { /// if let SyncRoom::Joined(room) = room { @@ -106,7 +108,7 @@ pub enum CustomOrRawEvent<'c> { /// } /// } /// ``` -#[async_trait::async_trait] +#[async_trait] pub trait EventEmitter: Send + Sync { // ROOM EVENTS from `IncomingTimeline` /// Fires when `Client` receives a `RoomEvent::RoomMember` event. @@ -200,6 +202,7 @@ pub trait EventEmitter: Send + Sync { mod test { use super::*; use matrix_sdk_common::locks::Mutex; + use matrix_sdk_common_macros::async_trait; use matrix_sdk_test::{async_test, sync_response, SyncResponseFile}; use std::sync::Arc; @@ -209,7 +212,7 @@ mod test { #[derive(Clone)] pub struct EvEmitterTest(Arc>>); - #[async_trait::async_trait] + #[async_trait] impl EventEmitter for EvEmitterTest { async fn on_room_member(&self, _: SyncRoom, _: &MemberEvent) { self.0.lock().await.push("member".to_string()) @@ -319,9 +322,15 @@ mod test { async fn on_non_room_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) { self.0.lock().await.push("account read".to_string()) } + async fn on_non_room_typing(&self, _: SyncRoom, _: &TypingEvent) { + self.0.lock().await.push("typing event".to_string()) + } async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) { self.0.lock().await.push("presence event".to_string()) } + async fn on_unrecognized_event(&self, _: SyncRoom, _: &CustomOrRawEvent<'_>) { + self.0.lock().await.push("unrecognized event".to_string()) + } } use crate::identifiers::UserId; @@ -417,4 +426,30 @@ mod test { ], ) } + + #[async_test] + async fn event_emitter_more_events() { + let vec = Arc::new(Mutex::new(Vec::new())); + let test_vec = Arc::clone(&vec); + let emitter = Box::new(EvEmitterTest(vec)); + + let client = get_client().await; + client.add_event_emitter(emitter).await; + + let mut response = sync_response(SyncResponseFile::All); + client.receive_sync_response(&mut response).await.unwrap(); + + let v = test_vec.lock().await; + assert_eq!( + v.as_slice(), + [ + "message", + "unrecognized event", + "redaction", + "unrecognized event", + "unrecognized event", + "typing event" + ], + ) + } } diff --git a/matrix_sdk_base/src/models/room.rs b/matrix_sdk_base/src/models/room.rs index ce09da2a..be412840 100644 --- a/matrix_sdk_base/src/models/room.rs +++ b/matrix_sdk_base/src/models/room.rs @@ -43,8 +43,7 @@ use crate::identifiers::{RoomAliasId, RoomId, UserId}; use crate::js_int::{Int, UInt}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] -#[cfg_attr(test, derive(Clone))] +#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)] /// `RoomName` allows the calculation of a text room name. pub struct RoomName { /// The displayed name of the room. @@ -66,8 +65,7 @@ pub struct RoomName { pub invited_member_count: Option, } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(test, derive(Clone))] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] pub struct PowerLevels { /// The level required to ban a user. pub ban: Int, @@ -138,8 +136,7 @@ impl From<&EncryptionEvent> for EncryptionInfo { } } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(test, derive(Clone))] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] pub struct Tombstone { /// A server-defined message. body: String, @@ -153,8 +150,7 @@ enum MemberDirection { Exiting, } -#[derive(Debug, PartialEq, Serialize, Deserialize)] -#[cfg_attr(test, derive(Clone))] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] /// A Matrix room. pub struct Room { /// The unique id of the room. diff --git a/matrix_sdk_base/src/models/room_member.rs b/matrix_sdk_base/src/models/room_member.rs index fab43f3f..a50f1e3c 100644 --- a/matrix_sdk_base/src/models/room_member.rs +++ b/matrix_sdk_base/src/models/room_member.rs @@ -27,7 +27,7 @@ use crate::js_int::{Int, UInt}; use serde::{Deserialize, Serialize}; // Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] /// A Matrix room member. /// pub struct RoomMember { diff --git a/matrix_sdk_base/src/state/mod.rs b/matrix_sdk_base/src/state/mod.rs index 803a9965..6bd18820 100644 --- a/matrix_sdk_base/src/state/mod.rs +++ b/matrix_sdk_base/src/state/mod.rs @@ -27,6 +27,9 @@ use crate::events::push_rules::Ruleset; use crate::identifiers::{RoomId, UserId}; use crate::{Result, Room, RoomState, Session}; +#[cfg(not(target_arch = "wasm32"))] +use matrix_sdk_common_macros::send_sync; + /// `ClientState` holds all the information to restore a `BaseClient` /// except the `access_token` as the default store is not secure. /// @@ -85,7 +88,8 @@ pub struct AllRooms { /// Abstraction around the data store to avoid unnecessary request on client initialization. #[async_trait::async_trait] -pub trait StateStore: Send + Sync { +#[cfg_attr(not(target_arch = "wasm32"), send_sync)] +pub trait StateStore { /// Loads the state of `BaseClient` through `ClientState` type. /// /// An `Option::None` should be returned only if the `StateStore` tries to diff --git a/matrix_sdk_common/Cargo.toml b/matrix_sdk_common/Cargo.toml index cb34e411..2d5d2c47 100644 --- a/matrix_sdk_common/Cargo.toml +++ b/matrix_sdk_common/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["Damir Jelić "] description = "Collection of common types used in the matrix-sdk" edition = "2018" homepage = "https://github.com/matrix-org/matrix-rust-sdk" diff --git a/matrix_sdk_common_macros/Cargo.toml b/matrix_sdk_common_macros/Cargo.toml new file mode 100644 index 00000000..3df8a0bf --- /dev/null +++ b/matrix_sdk_common_macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +description = "Helper macros for the Matrix SDK" +authors = ["MTRnord "] +edition = "2018" +homepage = "https://github.com/matrix-org/matrix-rust-sdk" +keywords = ["matrix", "chat", "messaging", "ruma"] +license = "Apache-2.0" +name = "matrix-sdk-common-macros" +readme = "README.md" +repository = "https://github.com/matrix-org/matrix-rust-sdk" +version = "0.1.0" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +quote = "1.0" diff --git a/matrix_sdk_common_macros/src/lib.rs b/matrix_sdk_common_macros/src/lib.rs new file mode 100644 index 00000000..31564805 --- /dev/null +++ b/matrix_sdk_common_macros/src/lib.rs @@ -0,0 +1,30 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, parse_quote, ItemTrait}; + +/// Attribute to use `Send + Sync` for everything but wasm32 +#[proc_macro_attribute] +pub fn send_sync(_attr: TokenStream, input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let mut input = parse_macro_input!(input as ItemTrait); + + let send_trait_bound = parse_quote!(std::marker::Send); + let sync_trait_bound = parse_quote!(std::marker::Sync); + input.supertraits.push(send_trait_bound); + input.supertraits.push(sync_trait_bound); + + TokenStream::from(quote!(#input)) +} + +/// A wasm32 compatible wrapper for the async_trait::async_trait macro +#[proc_macro_attribute] +pub fn async_trait(_attr: TokenStream, item: TokenStream) -> TokenStream { + let attrs = r#" + #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] + #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] + "#; + + let mut out: TokenStream = attrs.parse().unwrap(); + out.extend(item); + out +} diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index fe154516..868f7372 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["Damir Jelić "] description = "Matrix encryption library" edition = "2018" homepage = "https://github.com/matrix-org/matrix-rust-sdk" @@ -17,6 +17,7 @@ sqlite-cryptostore = ["sqlx"] [dependencies] async-trait = "0.1.31" +matrix-sdk-common-macros = { version = "0.1.0", path = "../matrix_sdk_common_macros" } matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } olm-rs = { version = "0.5.0", features = ["serde"] } diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index 6eceadfb..b1ed1088 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -215,6 +215,12 @@ impl OlmMachine { match &self.uploaded_signed_key_count { Some(count) => { let max_keys = self.account.max_one_time_keys().await as u64; + // If there are more keys already uploaded than max_key / 2 + // bail out returning false, this also avoids overflow. + if count.load(Ordering::Relaxed) > (max_keys / 2) { + return false; + } + let key_count = (max_keys / 2) - count.load(Ordering::Relaxed); key_count > 0 } diff --git a/matrix_sdk_crypto/src/store/mod.rs b/matrix_sdk_crypto/src/store/mod.rs index 80839b74..91be9598 100644 --- a/matrix_sdk_crypto/src/store/mod.rs +++ b/matrix_sdk_crypto/src/store/mod.rs @@ -27,12 +27,16 @@ use super::device::Device; use super::memory_stores::UserDevices; use super::olm::{Account, InboundGroupSession, Session}; use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId}; +use matrix_sdk_common_macros::send_sync; use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError}; pub mod memorystore; + +#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "sqlite-cryptostore")] pub mod sqlite; +#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "sqlite-cryptostore")] use sqlx::Error as SqlxError; @@ -83,9 +87,10 @@ pub enum CryptoStoreError { pub type Result = std::result::Result; #[async_trait] +#[cfg_attr(not(target_arch = "wasm32"), send_sync)] /// Trait abstracting a store that the `OlmMachine` uses to store cryptographic /// keys. -pub trait CryptoStore: Debug + Send + Sync { +pub trait CryptoStore: Debug { /// Load an account that was previously stored. async fn load_account(&mut self) -> Result>; diff --git a/matrix_sdk_test/src/lib.rs b/matrix_sdk_test/src/lib.rs index 85180777..321ee85b 100644 --- a/matrix_sdk_test/src/lib.rs +++ b/matrix_sdk_test/src/lib.rs @@ -345,6 +345,7 @@ impl EventBuilder { /// Embedded sync reponse files pub enum SyncResponseFile { + All, Default, DefaultWithSummary, Invite, @@ -354,6 +355,7 @@ pub enum SyncResponseFile { /// Get specific API responses for testing pub fn sync_response(kind: SyncResponseFile) -> SyncResponse { let data = match kind { + SyncResponseFile::All => include_bytes!("../test_data/more_sync.json").to_vec(), SyncResponseFile::Default => include_bytes!("../test_data/sync.json").to_vec(), SyncResponseFile::DefaultWithSummary => { include_bytes!("../test_data/sync_with_summary.json").to_vec() diff --git a/matrix_sdk_test/test_data/more_sync.json b/matrix_sdk_test/test_data/more_sync.json new file mode 100644 index 00000000..f487281c --- /dev/null +++ b/matrix_sdk_test/test_data/more_sync.json @@ -0,0 +1,151 @@ +{ + "device_one_time_keys_count": {}, + "next_batch": "s526_47314_0_7_1_1_1_11444_1", + "device_lists": { + "changed": [ + "@example:example.org" + ], + "left": [] + }, + "rooms": { + "invite": {}, + "join": { + "!SVkFJHzfwvuaIEawgC:localhost": { + "summary": {}, + "account_data": { + "events": [] + }, + "ephemeral": { + "events": [ + { + "content": { + "$151680659217152dPKjd:localhost": { + "m.read": { + "@example:localhost": { + "ts": 1516809890615 + } + } + } + }, + "type": "m.receipt" + }, + { + "content": { + "user_ids": [ + "@alice:matrix.org", + "@bob:example.com" + ] + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.typing" + } + ] + }, + "state": { + "events": [] + }, + "timeline": { + "events": [ + { + "content": { + "body": "baba", + "format": "org.matrix.custom.html", + "formatted_body": "baba", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1520372800469, + "sender": "@example:localhost", + "type": "m.room.message", + "unsigned": { + "age": 598971425 + } + }, + { + "content": { + "body": " * edited message", + "m.new_content": { + "body": "edited message", + "msgtype": "m.text" + }, + "m.relates_to": { + "event_id": "some event id", + "rel_type": "m.replace" + }, + "msgtype": "m.text" + }, + "event_id": "edit event id", + "origin_server_ts": 1590262659984, + "sender": "@alice:matrix.org", + "type": "m.room.message", + "unsigned": { + "age": 85 + } + }, + { + "content": { + "reason": "😀" + }, + "event_id": "$151957878228ssqrJ:localhost", + "origin_server_ts": 1519578782185, + "sender": "@example:localhost", + "type": "m.room.redaction", + "redacts": "$151957878228ssqrj:localhost" + }, + { + "content": {}, + "event_id": "$15275046980maRLj:localhost", + "origin_server_ts": 1527504698685, + "sender": "@example:localhost", + "type": "m.room.message", + "unsigned": { + "age": 19334, + "redacted_because": { + "content": {}, + "event_id": "$15275047031IXQRi:localhost", + "origin_server_ts": 1527504703496, + "redacts": "$15275046980maRLj:localhost", + "sender": "@example:localhost", + "type": "m.room.redaction", + "unsigned": { + "age": 14523 + } + }, + "redacted_by": "$15275047031IXQRi:localhost" + } + }, + { + "content": { + "m.relates_to": { + "event_id": "some event id", + "key": "👍", + "rel_type": "m.annotation" + } + }, + "event_id": "event id", + "origin_server_ts": 1590275813161, + "sender": "@alice:matrix.org", + "type": "m.reaction", + "unsigned": { + "age": 85 + } + } + ], + "limited": true, + "prev_batch": "t392-516_47314_0_7_1_1_1_11444_1" + }, + "unread_notifications": { + "highlight_count": 0, + "notification_count": 11 + } + } + }, + "leave": {} + }, + "to_device": { + "events": [] + }, + "presence": { + "events": [] + } +} diff --git a/test_data/events/message_edit.json b/test_data/events/message_edit.json index 60fc1a77..8a84c4be 100644 --- a/test_data/events/message_edit.json +++ b/test_data/events/message_edit.json @@ -1,19 +1,19 @@ { "content": { - "body": " * f fjkdslasdf $$$$$$$$$$$$$$$$$$$$$$$$$$$$", + "body": " * edited message", "m.new_content": { - "body": "f fjkdslasdf $$$$$$$$$$$$$$$$$$$$$$$$$$$$", + "body": "edited message", "msgtype": "m.text" }, "m.relates_to": { - "event_id": "$MbS0nMfvub-CPbytp7KRmExAp3oVfdjWOvf2ifG1zWI", + "event_id": "some event id", "rel_type": "m.replace" }, "msgtype": "m.text" }, - "event_id": "$xXL9cVB_10jkpxUFTsubeusygV0yv5b_63ADjgiQnOA", + "event_id": "edit event id", "origin_server_ts": 1590262659984, - "sender": "@devinr528:matrix.org", + "sender": "@alice:matrix.org", "type": "m.room.message", "unsigned": { "age": 85 diff --git a/test_data/events/reaction.json b/test_data/events/reaction.json index ef0ee79b..4fdce2e0 100644 --- a/test_data/events/reaction.json +++ b/test_data/events/reaction.json @@ -1,12 +1,12 @@ { "content": { "m.relates_to": { - "event_id": "$MDit176PkuBlpP7S6c64iuf74KC2HqZ3peV1NrV4PKA", + "event_id": "$MDitXXXXXXuBlpP7S6c6XXXXXXXC2HqZ3peV1NrV4PKA", "key": "👍", "rel_type": "m.annotation" } }, - "event_id": "$QZn9xEx72PUfd2tAGFH-FFgsffZlVMobk47Tl5Lpdtg", + "event_id": "$QZn9xEXXXXXfd2tAGFH-XXgsffZlVMobk47Tl5Lpdtg", "origin_server_ts": 1590275813161, "sender": "@devinr528:matrix.org", "type": "m.reaction", diff --git a/test_data/more_sync.json b/test_data/more_sync.json new file mode 100644 index 00000000..27b2e8d6 --- /dev/null +++ b/test_data/more_sync.json @@ -0,0 +1,129 @@ +{ + "device_one_time_keys_count": {}, + "next_batch": "s526_47314_0_7_1_1_1_11444_1", + "device_lists": { + "changed": [ + "@example:example.org" + ], + "left": [] + }, + "rooms": { + "invite": {}, + "join": { + "!SVkFJHzfwvuaIEawgC:localhost": { + "summary": {}, + "account_data": { + "events": [] + }, + "ephemeral": { + "events": [ + { + "content": { + "$151680659217152dPKjd:localhost": { + "m.read": { + "@example:localhost": { + "ts": 1516809890615 + } + } + } + }, + "type": "m.receipt" + }, + { + "content": { + "user_ids": [ + "@alice:matrix.org", + "@bob:example.com" + ] + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.typing" + } + ] + }, + "state": { + "events": [] + }, + "timeline": { + "events": [ + { + "content": { + "body": "baba", + "format": "org.matrix.custom.html", + "formatted_body": "baba", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1520372800469, + "sender": "@example:localhost", + "type": "m.room.message", + "unsigned": { + "age": 598971425 + } + }, + { + "content": { + "body": " * edited message", + "m.new_content": { + "body": "edited message", + "msgtype": "m.text" + }, + "m.relates_to": { + "event_id": "some event id", + "rel_type": "m.replace" + }, + "msgtype": "m.text" + }, + "event_id": "edit event id", + "origin_server_ts": 1590262659984, + "sender": "@alice:matrix.org", + "type": "m.room.message", + "unsigned": { + "age": 85 + } + }, + { + "content": { + "reason": "😀" + }, + "event_id": "$151957878228ssqrJ:localhost", + "origin_server_ts": 1519578782185, + "sender": "@example:localhost", + "type": "m.room.redaction", + "redacts": "$151957878228ssqrj:localhost" + }, + { + "content": { + "m.relates_to": { + "event_id": "some event id", + "key": "👍", + "rel_type": "m.annotation" + } + }, + "event_id": "event id", + "origin_server_ts": 1590275813161, + "sender": "@alice:matrix.org", + "type": "m.reaction", + "unsigned": { + "age": 85 + } + } + ], + "limited": true, + "prev_batch": "t392-516_47314_0_7_1_1_1_11444_1" + }, + "unread_notifications": { + "highlight_count": 0, + "notification_count": 11 + } + } + }, + "leave": {} + }, + "to_device": { + "events": [] + }, + "presence": { + "events": [] + } +} diff --git a/test_data/registration_response_error.json b/test_data/registration_response_error.json new file mode 100644 index 00000000..b34244c1 --- /dev/null +++ b/test_data/registration_response_error.json @@ -0,0 +1,19 @@ +{ + "errcode": "M_FORBIDDEN", + "error": "Invalid password", + "completed": ["example.type.foo"], + "flows": [ + { + "stages": ["example.type.foo", "example.type.bar"] + }, + { + "stages": ["example.type.foo", "example.type.baz"] + } + ], + "params": { + "example.type.baz": { + "example_key": "foobar" + } + }, + "session": "xxxxxx" +}