diff --git a/.travis.yml b/.travis.yml index 0cc5a004..232a7e8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,26 +7,46 @@ addons: jobs: allow_failures: - - os: windows - - os: linux - name: wasm32-unknown-unknown + - os: osx + name: macOS 10.15 include: - - stage: Lint + - stage: Format os: linux before_script: - rustup component add rustfmt script: - cargo fmt --all -- --check + - stage: Clippy + os: linux + before_script: + - rustup component add clippy + script: + - cargo clippy --all-targets --all-features -- -D warnings + - stage: Test os: linux - dist: bionic - os: windows + script: + - cd matrix_sdk + - cargo test --no-default-features --features "messages" + - cd ../matrix_sdk_base + - cargo test --no-default-features --features "messages" - os: osx + - os: linux + name: Minimal build + script: + - cd matrix_sdk + - cargo build --no-default-features + + - os: osx + name: macOS 10.15 + osx_image: xcode12 + - os: linux name: Coverage before_script: diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index 505f1d73..b1438d11 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -21,7 +21,7 @@ http = "0.2.1" reqwest = "0.10.6" serde_json = "1.0.56" thiserror = "1.0.20" -tracing = "0.1.15" +tracing = "0.1.16" url = "2.1.1" matrix-sdk-common-macros = { version = "0.1.0", path = "../matrix_sdk_common_macros" } @@ -37,8 +37,8 @@ version = "0.2.4" default-features = false features = ["std", "std-future"] -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.futures-timer] -version = "3.0.2" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +futures-timer = "3.0.2" [target.'cfg(target_arch = "wasm32")'.dependencies.futures-timer] version = "3.0.2" diff --git a/matrix_sdk/examples/autojoin.rs b/matrix_sdk/examples/autojoin.rs index ed73a48e..d8813ae4 100644 --- a/matrix_sdk/examples/autojoin.rs +++ b/matrix_sdk/examples/autojoin.rs @@ -2,7 +2,7 @@ use std::{env, process::exit}; use matrix_sdk::{ self, - events::{room::member::MemberEventContent, StrippedStateEventStub}, + events::{room::member::MemberEventContent, StrippedStateEvent}, Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings, }; use matrix_sdk_common_macros::async_trait; @@ -23,7 +23,7 @@ impl EventEmitter for AutoJoinBot { async fn on_stripped_state_member( &self, room: SyncRoom, - room_member: &StrippedStateEventStub, + room_member: &StrippedStateEvent, _: Option, ) { if room_member.state_key != self.client.user_id().await.unwrap() { diff --git a/matrix_sdk/examples/command_bot.rs b/matrix_sdk/examples/command_bot.rs index 2a84c1d6..9e186c01 100644 --- a/matrix_sdk/examples/command_bot.rs +++ b/matrix_sdk/examples/command_bot.rs @@ -4,7 +4,7 @@ use matrix_sdk::{ self, events::{ room::message::{MessageEventContent, TextMessageEventContent}, - MessageEventStub, + SyncMessageEvent, }, Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings, }; @@ -25,9 +25,9 @@ impl CommandBot { #[async_trait] impl EventEmitter for CommandBot { - async fn on_room_message(&self, room: SyncRoom, event: &MessageEventStub) { + async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent) { if let SyncRoom::Joined(room) = room { - let msg_body = if let MessageEventStub { + let msg_body = if let SyncMessageEvent { content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), .. } = event diff --git a/matrix_sdk/examples/login.rs b/matrix_sdk/examples/login.rs index 73559a6d..ca3276f8 100644 --- a/matrix_sdk/examples/login.rs +++ b/matrix_sdk/examples/login.rs @@ -5,7 +5,7 @@ use matrix_sdk::{ self, events::{ room::message::{MessageEventContent, TextMessageEventContent}, - MessageEventStub, + SyncMessageEvent, }, Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings, }; @@ -15,9 +15,9 @@ struct EventCallback; #[async_trait] impl EventEmitter for EventCallback { - async fn on_room_message(&self, room: SyncRoom, event: &MessageEventStub) { + async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent) { if let SyncRoom::Joined(room) = room { - if let MessageEventStub { + if let SyncMessageEvent { content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), sender, .. diff --git a/matrix_sdk/examples/wasm_command_bot/src/lib.rs b/matrix_sdk/examples/wasm_command_bot/src/lib.rs index fa944b3c..0167d5c3 100644 --- a/matrix_sdk/examples/wasm_command_bot/src/lib.rs +++ b/matrix_sdk/examples/wasm_command_bot/src/lib.rs @@ -2,7 +2,7 @@ use matrix_sdk::{ api::r0::sync::sync_events::Response as SyncResponse, events::{ room::message::{MessageEventContent, TextMessageEventContent}, - AnyMessageEventStub, AnyRoomEventStub, MessageEventStub, + AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent, }, identifiers::RoomId, Client, ClientConfig, SyncSettings, @@ -17,9 +17,9 @@ impl WasmBot { async fn on_room_message( &self, room_id: &RoomId, - event: MessageEventStub, + event: SyncMessageEvent, ) { - let msg_body = if let MessageEventStub { + let msg_body = if let SyncMessageEvent { content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), .. } = event @@ -45,7 +45,7 @@ impl WasmBot { for (room_id, room) in response.rooms.join { for event in room.timeline.events { if let Ok(event) = event.deserialize() { - if let AnyRoomEventStub::Message(AnyMessageEventStub::RoomMessage(ev)) = event { + if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(ev)) = event { self.on_room_message(&room_id, ev).await } } diff --git a/matrix_sdk/src/client.rs b/matrix_sdk/src/client.rs index 74e1caf2..8ee4c134 100644 --- a/matrix_sdk/src/client.rs +++ b/matrix_sdk/src/client.rs @@ -31,7 +31,7 @@ use futures_timer::Delay as sleep; use std::future::Future; #[cfg(feature = "encryption")] use tracing::{debug, warn}; -use tracing::{info, instrument, trace}; +use tracing::{error, info, instrument, trace}; use http::Method as HttpMethod; use http::Response as HttpResponse; @@ -105,6 +105,7 @@ pub struct ClientConfig { user_agent: Option, disable_ssl_verification: bool, base_config: BaseClientConfig, + timeout: Option, } // #[cfg_attr(tarpaulin, skip)] @@ -198,11 +199,18 @@ impl ClientConfig { self.base_config = self.base_config.passphrase(passphrase); self } + + /// Set a timeout duration for all HTTP requests. The default is no timeout. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } } #[derive(Debug, Default, Clone)] /// Settings for a sync call. pub struct SyncSettings { + pub(crate) filter: Option, pub(crate) timeout: Option, pub(crate) token: Option, pub(crate) full_state: bool, @@ -235,6 +243,17 @@ impl SyncSettings { self } + /// Set the sync filter. + /// It can be either the filter ID, or the definition for the filter. + /// + /// # Arguments + /// + /// * `filter` - The filter configuration that should be used for the sync call. + pub fn filter(mut self, filter: sync_events::Filter) -> Self { + self.filter = Some(filter); + self + } + /// Should the server return the full state from the start of the timeline. /// /// This does nothing if no sync token is set. @@ -302,6 +321,11 @@ impl Client { #[cfg(not(target_arch = "wasm32"))] let http_client = { + let http_client = match config.timeout { + Some(x) => http_client.timeout(x), + None => http_client, + }; + let http_client = if config.disable_ssl_verification { http_client.danger_accept_invalid_certs(true) } else { @@ -448,7 +472,7 @@ impl Client { login_info: login::LoginInfo::Password { password: password.into(), }, - device_id: device_id.map(|d| d.into()), + device_id: device_id.map(|d| d.into().into_boxed_str()), initial_device_display_name: initial_device_display_name.map(|d| d.into()), }; @@ -1222,7 +1246,7 @@ impl Client { #[instrument] pub async fn sync(&self, sync_settings: SyncSettings) -> Result { let request = sync_events::Request { - filter: None, + filter: sync_settings.filter, since: sync_settings.token, full_state: sync_settings.full_state, set_presence: sync_events::SetPresence::Online, @@ -1302,6 +1326,7 @@ impl Client { C: Future, { let mut sync_settings = sync_settings; + let filter = sync_settings.filter.clone(); let mut last_sync_time: Option = None; if sync_settings.token.is_none() { @@ -1311,12 +1336,13 @@ impl Client { loop { let response = self.sync(sync_settings.clone()).await; - let response = if let Ok(r) = response { - r - } else { - sleep::new(Duration::from_secs(1)).await; - - continue; + let response = match response { + Ok(r) => r, + Err(e) => { + error!("Received an invalid response: {}", e); + sleep::new(Duration::from_secs(1)).await; + continue; + } }; // TODO send out to-device messages here @@ -1360,6 +1386,9 @@ impl Client { .await .expect("No sync token found after initial sync"), ); + if let Some(f) = filter.as_ref() { + sync_settings = sync_settings.filter(f.clone()); + } } } @@ -1378,7 +1407,7 @@ impl Client { #[instrument] async fn claim_one_time_keys( &self, - one_time_keys: BTreeMap>, + one_time_keys: BTreeMap, KeyAlgorithm>>, ) -> Result { let request = claim_keys::Request { timeout: None, @@ -1482,7 +1511,7 @@ impl Client { users_for_query ); - let mut device_keys: BTreeMap> = BTreeMap::new(); + let mut device_keys: BTreeMap>> = BTreeMap::new(); for user in users_for_query.drain() { device_keys.insert(user, Vec::new()); diff --git a/matrix_sdk/src/lib.rs b/matrix_sdk/src/lib.rs index 6bdfd0dc..9798c67e 100644 --- a/matrix_sdk/src/lib.rs +++ b/matrix_sdk/src/lib.rs @@ -40,6 +40,8 @@ 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}; +#[cfg(feature = "messages")] +pub use matrix_sdk_base::{MessageQueue, MessageWrapper, PossiblyRedactedExt}; pub use matrix_sdk_base::{RoomState, StateStore}; pub use matrix_sdk_common::*; pub use reqwest::header::InvalidHeaderValue; diff --git a/matrix_sdk/src/request_builder.rs b/matrix_sdk/src/request_builder.rs index 82d4b2ee..9d0cf748 100644 --- a/matrix_sdk/src/request_builder.rs +++ b/matrix_sdk/src/request_builder.rs @@ -152,6 +152,12 @@ impl RoomBuilder { } } +impl Default for RoomBuilder { + fn default() -> Self { + Self::new() + } +} + impl Into for RoomBuilder { fn into(mut self) -> create_room::Request { self.req.creation_content = Some(self.creation_content); @@ -269,7 +275,7 @@ impl Into for MessagesRequestBuilder { pub struct RegistrationBuilder { password: Option, username: Option, - device_id: Option, + device_id: Option>, initial_device_display_name: Option, auth: Option, kind: Option, @@ -303,7 +309,7 @@ impl RegistrationBuilder { /// /// 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: S) -> &mut Self { + pub fn device_id>>(&mut self, device_id: S) -> &mut Self { self.device_id = Some(device_id.into()); self } diff --git a/matrix_sdk_base/Cargo.toml b/matrix_sdk_base/Cargo.toml index d7f4d0dc..5c16e448 100644 --- a/matrix_sdk_base/Cargo.toml +++ b/matrix_sdk_base/Cargo.toml @@ -21,6 +21,7 @@ async-trait = "0.1.36" serde = "1.0.114" serde_json = "1.0.56" zeroize = "1.1.0" +tracing = "0.1.16" matrix-sdk-common-macros = { version = "0.1.0", path = "../matrix_sdk_common_macros" } matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } @@ -45,4 +46,4 @@ mockito = "0.26.0" tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.14" +wasm-bindgen-test = "0.3.15" diff --git a/matrix_sdk_base/src/client.rs b/matrix_sdk_base/src/client.rs index 75a422bf..3eb9e260 100644 --- a/matrix_sdk_base/src/client.rs +++ b/matrix_sdk_base/src/client.rs @@ -38,8 +38,8 @@ use crate::session::Session; use crate::state::{AllRooms, ClientState, StateStore}; use crate::EventEmitter; use matrix_sdk_common::events::{ - AnyBasicEvent, AnyEphemeralRoomEventStub, AnyMessageEventStub, AnyRoomEventStub, - AnyStateEventStub, AnyStrippedStateEventStub, EventJson, + AnyBasicEvent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncMessageEvent, + AnySyncRoomEvent, AnySyncStateEvent, EventJson, }; #[cfg(feature = "encryption")] @@ -94,8 +94,8 @@ pub struct AdditionalUnsignedData { /// [synapse-bug]: /// [discussion]: fn hoist_room_event_prev_content( - event: &EventJson, -) -> Option> { + event: &EventJson, +) -> Option> { let prev_content = serde_json::from_str::(event.json().get()) .map(|more_unsigned| more_unsigned.unsigned) .map(|additional| additional.prev_content) @@ -105,7 +105,7 @@ fn hoist_room_event_prev_content( let mut ev = event.deserialize().ok()?; match &mut ev { - AnyRoomEventStub::State(AnyStateEventStub::RoomMember(ref mut member)) + AnySyncRoomEvent::State(AnySyncStateEvent::RoomMember(ref mut member)) if member.prev_content.is_none() => { if let Ok(prev) = prev_content.deserialize() { @@ -122,8 +122,8 @@ fn hoist_room_event_prev_content( /// /// See comment of `hoist_room_event_prev_content`. fn hoist_state_event_prev_content( - event: &EventJson, -) -> Option> { + event: &EventJson, +) -> Option> { let prev_content = serde_json::from_str::(event.json().get()) .map(|more_unsigned| more_unsigned.unsigned) .map(|additional| additional.prev_content) @@ -132,7 +132,7 @@ fn hoist_state_event_prev_content( let mut ev = event.deserialize().ok()?; match &mut ev { - AnyStateEventStub::RoomMember(ref mut member) if member.prev_content.is_none() => { + AnySyncStateEvent::RoomMember(ref mut member) if member.prev_content.is_none() => { member.prev_content = Some(prev_content.deserialize().ok()?); Some(EventJson::from(ev)) } @@ -141,7 +141,7 @@ fn hoist_state_event_prev_content( } fn stripped_deserialize_prev_content( - event: &EventJson, + event: &EventJson, ) -> Option { serde_json::from_str::(event.json().get()) .map(|more_unsigned| more_unsigned.unsigned) @@ -488,7 +488,7 @@ impl BaseClient { *olm = Some( OlmMachine::new_with_store( session.user_id.to_owned(), - session.device_id.to_owned(), + session.device_id.as_str().into(), store, ) .await @@ -713,14 +713,14 @@ impl BaseClient { pub async fn receive_joined_timeline_event( &self, room_id: &RoomId, - event: &mut EventJson, + event: &mut EventJson, ) -> Result { match event.deserialize() { #[allow(unused_mut)] Ok(mut e) => { #[cfg(feature = "encryption")] { - if let AnyRoomEventStub::Message(AnyMessageEventStub::RoomEncrypted( + if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomEncrypted( ref mut encrypted_event, )) = e { @@ -742,8 +742,8 @@ impl BaseClient { let room_lock = self.get_or_create_joined_room(&room_id).await?; let mut room = room_lock.write().await; - if let AnyRoomEventStub::State(AnyStateEventStub::RoomMember(mem_event)) = &mut e { - let changed = room.handle_membership(mem_event); + if let AnySyncRoomEvent::State(AnySyncStateEvent::RoomMember(mem_event)) = &mut e { + let (changed, _) = room.handle_membership(mem_event, false); // The memberlist of the room changed, invalidate the group session // of the room. @@ -774,13 +774,13 @@ impl BaseClient { pub async fn receive_joined_state_event( &self, room_id: &RoomId, - event: &AnyStateEventStub, + event: &AnySyncStateEvent, ) -> Result { let room_lock = self.get_or_create_joined_room(room_id).await?; let mut room = room_lock.write().await; - if let AnyStateEventStub::RoomMember(e) = event { - let changed = room.handle_membership(e); + if let AnySyncStateEvent::RoomMember(e) = event { + let (changed, _) = room.handle_membership(e, true); // The memberlist of the room changed, invalidate the group session // of the room. @@ -808,7 +808,7 @@ impl BaseClient { pub async fn receive_invite_state_event( &self, room_id: &RoomId, - event: &AnyStrippedStateEventStub, + event: &AnyStrippedStateEvent, ) -> Result { let room_lock = self.get_or_create_invited_room(room_id).await?; let mut room = room_lock.write().await; @@ -828,7 +828,7 @@ impl BaseClient { pub async fn receive_left_timeline_event( &self, room_id: &RoomId, - event: &EventJson, + event: &EventJson, ) -> Result { match event.deserialize() { Ok(e) => { @@ -853,7 +853,7 @@ impl BaseClient { pub async fn receive_left_state_event( &self, room_id: &RoomId, - event: &AnyStateEventStub, + event: &AnySyncStateEvent, ) -> Result { let room_lock = self.get_or_create_left_room(room_id).await?; let mut room = room_lock.write().await; @@ -906,11 +906,11 @@ impl BaseClient { /// * `room_id` - The unique id of the room the event belongs to. /// /// * `event` - The presence event for a specified room member. - pub async fn receive_ephemeral_event(&self, event: &AnyEphemeralRoomEventStub) -> bool { + pub async fn receive_ephemeral_event(&self, event: &AnySyncEphemeralRoomEvent) -> bool { match event { - AnyEphemeralRoomEventStub::FullyRead(_) => {} - AnyEphemeralRoomEventStub::Receipt(_) => {} - AnyEphemeralRoomEventStub::Typing(_) => {} + AnySyncEphemeralRoomEvent::FullyRead(_) => {} + AnySyncEphemeralRoomEvent::Receipt(_) => {} + AnySyncEphemeralRoomEvent::Typing(_) => {} _ => {} }; false @@ -1197,7 +1197,7 @@ impl BaseClient { if let Ok(mut e) = event.deserialize() { // if the event is a m.room.member event the server will sometimes // send the `prev_content` field as part of the unsigned field. - if let AnyStrippedStateEventStub::RoomMember(_) = &mut e { + if let AnyStrippedStateEvent::RoomMember(_) = &mut e { if let Some(raw_content) = stripped_deserialize_prev_content(event) { let prev_content = match raw_content.prev_content { Some(json) => json.deserialize().ok(), @@ -1280,7 +1280,7 @@ impl BaseClient { pub async fn get_missing_sessions( &self, users: impl Iterator, - ) -> Result>> { + ) -> Result, KeyAlgorithm>>> { let mut olm = self.olm.lock().await; match &mut *olm { @@ -1437,7 +1437,7 @@ impl BaseClient { pub(crate) async fn emit_timeline_event( &self, room_id: &RoomId, - event: &AnyRoomEventStub, + event: &AnySyncRoomEvent, room_state: RoomStateType, ) { let lock = self.event_emitter.read().await; @@ -1472,52 +1472,54 @@ impl BaseClient { }; match event { - AnyRoomEventStub::State(event) => match event { - AnyStateEventStub::RoomMember(e) => event_emitter.on_room_member(room, e).await, - AnyStateEventStub::RoomName(e) => event_emitter.on_room_name(room, e).await, - AnyStateEventStub::RoomCanonicalAlias(e) => { + AnySyncRoomEvent::State(event) => match event { + AnySyncStateEvent::RoomMember(e) => event_emitter.on_room_member(room, e).await, + AnySyncStateEvent::RoomName(e) => event_emitter.on_room_name(room, e).await, + AnySyncStateEvent::RoomCanonicalAlias(e) => { event_emitter.on_room_canonical_alias(room, e).await } - AnyStateEventStub::RoomAliases(e) => event_emitter.on_room_aliases(room, e).await, - AnyStateEventStub::RoomAvatar(e) => event_emitter.on_room_avatar(room, e).await, - AnyStateEventStub::RoomPowerLevels(e) => { + AnySyncStateEvent::RoomAliases(e) => event_emitter.on_room_aliases(room, e).await, + AnySyncStateEvent::RoomAvatar(e) => event_emitter.on_room_avatar(room, e).await, + AnySyncStateEvent::RoomPowerLevels(e) => { event_emitter.on_room_power_levels(room, e).await } - AnyStateEventStub::RoomTombstone(e) => { + AnySyncStateEvent::RoomTombstone(e) => { event_emitter.on_room_tombstone(room, e).await } - AnyStateEventStub::RoomJoinRules(e) => { + AnySyncStateEvent::RoomJoinRules(e) => { event_emitter.on_room_join_rules(room, e).await } - AnyStateEventStub::Custom(e) => { + AnySyncStateEvent::Custom(e) => { event_emitter .on_unrecognized_event(room, &CustomOrRawEvent::State(e)) .await } _ => {} }, - AnyRoomEventStub::Message(event) => match event { - AnyMessageEventStub::RoomMessage(e) => event_emitter.on_room_message(room, e).await, - AnyMessageEventStub::RoomMessageFeedback(e) => { + AnySyncRoomEvent::Message(event) => match event { + AnySyncMessageEvent::RoomMessage(e) => event_emitter.on_room_message(room, e).await, + AnySyncMessageEvent::RoomMessageFeedback(e) => { event_emitter.on_room_message_feedback(room, e).await } - AnyMessageEventStub::RoomRedaction(e) => { + AnySyncMessageEvent::RoomRedaction(e) => { event_emitter.on_room_redaction(room, e).await } - AnyMessageEventStub::Custom(e) => { + AnySyncMessageEvent::Custom(e) => { event_emitter .on_unrecognized_event(room, &CustomOrRawEvent::Message(e)) .await } _ => {} }, + AnySyncRoomEvent::RedactedState(_event) => {} + AnySyncRoomEvent::RedactedMessage(_event) => {} } } pub(crate) async fn emit_state_event( &self, room_id: &RoomId, - event: &AnyStateEventStub, + event: &AnySyncStateEvent, room_state: RoomStateType, ) { let lock = self.event_emitter.read().await; @@ -1552,32 +1554,32 @@ impl BaseClient { }; match event { - AnyStateEventStub::RoomMember(member) => { + AnySyncStateEvent::RoomMember(member) => { event_emitter.on_state_member(room, &member).await } - AnyStateEventStub::RoomName(name) => event_emitter.on_state_name(room, &name).await, - AnyStateEventStub::RoomCanonicalAlias(canonical) => { + AnySyncStateEvent::RoomName(name) => event_emitter.on_state_name(room, &name).await, + AnySyncStateEvent::RoomCanonicalAlias(canonical) => { event_emitter .on_state_canonical_alias(room, &canonical) .await } - AnyStateEventStub::RoomAliases(aliases) => { + AnySyncStateEvent::RoomAliases(aliases) => { event_emitter.on_state_aliases(room, &aliases).await } - AnyStateEventStub::RoomAvatar(avatar) => { + AnySyncStateEvent::RoomAvatar(avatar) => { event_emitter.on_state_avatar(room, &avatar).await } - AnyStateEventStub::RoomPowerLevels(power) => { + AnySyncStateEvent::RoomPowerLevels(power) => { event_emitter.on_state_power_levels(room, &power).await } - AnyStateEventStub::RoomJoinRules(rules) => { + AnySyncStateEvent::RoomJoinRules(rules) => { event_emitter.on_state_join_rules(room, &rules).await } - AnyStateEventStub::RoomTombstone(tomb) => { + AnySyncStateEvent::RoomTombstone(tomb) => { // TODO make `on_state_tombstone` method event_emitter.on_room_tombstone(room, &tomb).await } - AnyStateEventStub::Custom(custom) => { + AnySyncStateEvent::Custom(custom) => { event_emitter .on_unrecognized_event(room, &CustomOrRawEvent::State(custom)) .await @@ -1589,7 +1591,7 @@ impl BaseClient { pub(crate) async fn emit_stripped_state_event( &self, room_id: &RoomId, - event: &AnyStrippedStateEventStub, + event: &AnyStrippedStateEvent, prev_content: Option, room_state: RoomStateType, ) { @@ -1625,33 +1627,33 @@ impl BaseClient { }; match event { - AnyStrippedStateEventStub::RoomMember(member) => { + AnyStrippedStateEvent::RoomMember(member) => { event_emitter .on_stripped_state_member(room, &member, prev_content) .await } - AnyStrippedStateEventStub::RoomName(name) => { + AnyStrippedStateEvent::RoomName(name) => { event_emitter.on_stripped_state_name(room, &name).await } - AnyStrippedStateEventStub::RoomCanonicalAlias(canonical) => { + AnyStrippedStateEvent::RoomCanonicalAlias(canonical) => { event_emitter .on_stripped_state_canonical_alias(room, &canonical) .await } - AnyStrippedStateEventStub::RoomAliases(aliases) => { + AnyStrippedStateEvent::RoomAliases(aliases) => { event_emitter .on_stripped_state_aliases(room, &aliases) .await } - AnyStrippedStateEventStub::RoomAvatar(avatar) => { + AnyStrippedStateEvent::RoomAvatar(avatar) => { event_emitter.on_stripped_state_avatar(room, &avatar).await } - AnyStrippedStateEventStub::RoomPowerLevels(power) => { + AnyStrippedStateEvent::RoomPowerLevels(power) => { event_emitter .on_stripped_state_power_levels(room, &power) .await } - AnyStrippedStateEventStub::RoomJoinRules(rules) => { + AnyStrippedStateEvent::RoomJoinRules(rules) => { event_emitter .on_stripped_state_join_rules(room, &rules) .await @@ -1716,7 +1718,7 @@ impl BaseClient { pub(crate) async fn emit_ephemeral_event( &self, room_id: &RoomId, - event: &AnyEphemeralRoomEventStub, + event: &AnySyncEphemeralRoomEvent, room_state: RoomStateType, ) { let lock = self.event_emitter.read().await; @@ -1751,13 +1753,13 @@ impl BaseClient { }; match event { - AnyEphemeralRoomEventStub::FullyRead(full_read) => { + AnySyncEphemeralRoomEvent::FullyRead(full_read) => { event_emitter.on_non_room_fully_read(room, &full_read).await } - AnyEphemeralRoomEventStub::Typing(typing) => { + AnySyncEphemeralRoomEvent::Typing(typing) => { event_emitter.on_non_room_typing(room, &typing).await } - AnyEphemeralRoomEventStub::Receipt(receipt) => { + AnySyncEphemeralRoomEvent::Receipt(receipt) => { event_emitter.on_non_room_receipt(room, &receipt).await } _ => {} @@ -1837,17 +1839,19 @@ impl BaseClient { #[cfg(test)] mod test { use crate::identifiers::{RoomId, UserId}; - use crate::{BaseClient, BaseClientConfig, Session}; - use matrix_sdk_common::events::{AnyRoomEventStub, EventJson}; + #[cfg(feature = "messages")] + use crate::{ + events::{AnySyncRoomEvent, EventJson}, + identifiers::EventId, + BaseClientConfig, JsonStore, + }; + use crate::{BaseClient, Session}; use matrix_sdk_common_macros::async_trait; use matrix_sdk_test::{async_test, test_json, EventBuilder, EventsJson}; use serde_json::json; use std::convert::TryFrom; use tempfile::tempdir; - #[cfg(feature = "messages")] - use crate::JsonStore; - #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; @@ -2006,7 +2010,7 @@ mod test { use crate::{EventEmitter, SyncRoom}; use matrix_sdk_common::events::{ room::member::{MemberEventContent, MembershipChange}, - StateEventStub, + SyncStateEvent, }; use matrix_sdk_common::locks::RwLock; use std::sync::{ @@ -2020,7 +2024,7 @@ mod test { async fn on_room_member( &self, room: SyncRoom, - event: &StateEventStub, + event: &SyncStateEvent, ) { if let SyncRoom::Joined(_) = room { if let MembershipChange::Joined = event.membership_change() { @@ -2157,8 +2161,8 @@ mod test { let member = room.joined_members.get(&user_id).unwrap(); assert_eq!(*member.display_name.as_ref().unwrap(), "changed"); - // The second part tests that the event is emitted correctly. If `prev_content` was - // missing, this bool is reset to false. + // The second part tests that the event is emitted correctly. If `prev_content` were + // missing, this bool would had been flipped. assert!(passed.load(Ordering::SeqCst)) } @@ -2393,7 +2397,7 @@ mod test { "type": "m.room.redaction", "redacts": "$152037280074GZeOm:localhost" }); - let mut event: EventJson = serde_json::from_value(json).unwrap(); + let mut event: EventJson = serde_json::from_value(json).unwrap(); client .receive_joined_timeline_event(&room_id, &mut event) .await @@ -2402,16 +2406,22 @@ mod test { // check that the message has actually been redacted for room in client.joined_rooms().read().await.values() { let queue = &room.read().await.messages; - if let crate::events::AnyMessageEventContent::RoomRedaction(content) = - &queue.msgs[0].content + if let crate::events::AnyPossiblyRedactedSyncMessageEvent::Redacted( + crate::events::AnyRedactedSyncMessageEvent::RoomMessage(event), + ) = &queue.msgs[0].deref() { - assert_eq!(content.reason, Some("😀".to_string())); + // this is the id from the message event in the sync response + assert_eq!( + event.event_id, + EventId::try_from("$152037280074GZeOm:localhost").unwrap() + ) } else { - panic!("[pre store sync] message event in message queue should be redacted") + panic!("message event in message queue should be redacted") } } - // `receive_joined_timeline_event` does not save the state to the store so we must + // `receive_joined_timeline_event` does not save the state to the store + // so we must do it ourselves client.store_room_state(&room_id).await.unwrap(); // we load state from the store only @@ -2424,10 +2434,15 @@ mod test { // properly for room in client.joined_rooms().read().await.values() { let queue = &room.read().await.messages; - if let crate::events::AnyMessageEventContent::RoomRedaction(content) = - &queue.msgs[0].content + if let crate::events::AnyPossiblyRedactedSyncMessageEvent::Redacted( + crate::events::AnyRedactedSyncMessageEvent::RoomMessage(event), + ) = &queue.msgs[0].deref() { - assert_eq!(content.reason, Some("😀".to_string())); + // this is the id from the message event in the sync response + assert_eq!( + event.event_id, + EventId::try_from("$152037280074GZeOm:localhost").unwrap() + ) } else { panic!("[post store sync] message event in message queue should be redacted") } diff --git a/matrix_sdk_base/src/event_emitter/mod.rs b/matrix_sdk_base/src/event_emitter/mod.rs index e0722eb1..610790d0 100644 --- a/matrix_sdk_base/src/event_emitter/mod.rs +++ b/matrix_sdk_base/src/event_emitter/mod.rs @@ -33,11 +33,11 @@ use crate::events::{ message::{feedback::FeedbackEventContent, MessageEventContent as MsgEventContent}, name::NameEventContent, power_levels::PowerLevelsEventContent, - redaction::RedactionEventStub, + redaction::SyncRedactionEvent, tombstone::TombstoneEventContent, }, typing::TypingEventContent, - BasicEvent, EphemeralRoomEvent, MessageEventStub, StateEventStub, StrippedStateEventStub, + BasicEvent, EphemeralRoomEvent, StrippedStateEvent, SyncMessageEvent, SyncStateEvent, }; use crate::{Room, RoomState}; use matrix_sdk_common_macros::async_trait; @@ -55,11 +55,11 @@ pub enum CustomOrRawEvent<'c> { /// A custom basic event. EphemeralRoom(&'c EphemeralRoomEvent), /// A custom room event. - Message(&'c MessageEventStub), + Message(&'c SyncMessageEvent), /// A custom state event. - State(&'c StateEventStub), + State(&'c SyncStateEvent), /// A custom stripped state event. - StrippedState(&'c StrippedStateEventStub), + StrippedState(&'c StrippedStateEvent), } /// This trait allows any type implementing `EventEmitter` to specify event callbacks for each event. @@ -74,7 +74,7 @@ pub enum CustomOrRawEvent<'c> { /// # self, /// # events::{ /// # room::message::{MessageEventContent, TextMessageEventContent}, -/// # MessageEventStub +/// # SyncMessageEvent /// # }, /// # EventEmitter, SyncRoom /// # }; @@ -85,9 +85,9 @@ pub enum CustomOrRawEvent<'c> { /// /// #[async_trait] /// impl EventEmitter for EventCallback { -/// async fn on_room_message(&self, room: SyncRoom, event: &MessageEventStub) { +/// async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent) { /// if let SyncRoom::Joined(room) = room { -/// if let MessageEventStub { +/// if let SyncMessageEvent { /// content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }), /// sender, /// .. @@ -112,114 +112,109 @@ pub enum CustomOrRawEvent<'c> { pub trait EventEmitter: Send + Sync { // ROOM EVENTS from `IncomingTimeline` /// Fires when `Client` receives a `RoomEvent::RoomMember` event. - async fn on_room_member(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `RoomEvent::RoomName` event. - async fn on_room_name(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event. async fn on_room_canonical_alias( &self, _: SyncRoom, - _: &StateEventStub, + _: &SyncStateEvent, ) { } /// Fires when `Client` receives a `RoomEvent::RoomAliases` event. - async fn on_room_aliases(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `RoomEvent::RoomAvatar` event. - async fn on_room_avatar(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `RoomEvent::RoomMessage` event. - async fn on_room_message(&self, _: SyncRoom, _: &MessageEventStub) {} + async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent) {} /// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event. async fn on_room_message_feedback( &self, _: SyncRoom, - _: &MessageEventStub, + _: &SyncMessageEvent, ) { } /// Fires when `Client` receives a `RoomEvent::RoomRedaction` event. - async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEventStub) {} + async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {} /// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event. - async fn on_room_power_levels(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_room_power_levels(&self, _: SyncRoom, _: &SyncStateEvent) { } /// Fires when `Client` receives a `RoomEvent::Tombstone` event. - async fn on_room_join_rules(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_room_join_rules(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `RoomEvent::Tombstone` event. - async fn on_room_tombstone(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent) {} // `RoomEvent`s from `IncomingState` /// Fires when `Client` receives a `StateEvent::RoomMember` event. - async fn on_state_member(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `StateEvent::RoomName` event. - async fn on_state_name(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event. async fn on_state_canonical_alias( &self, _: SyncRoom, - _: &StateEventStub, + _: &SyncStateEvent, ) { } /// Fires when `Client` receives a `StateEvent::RoomAliases` event. - async fn on_state_aliases(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `StateEvent::RoomAvatar` event. - async fn on_state_avatar(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent) {} /// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event. async fn on_state_power_levels( &self, _: SyncRoom, - _: &StateEventStub, + _: &SyncStateEvent, ) { } /// Fires when `Client` receives a `StateEvent::RoomJoinRules` event. - async fn on_state_join_rules(&self, _: SyncRoom, _: &StateEventStub) {} + async fn on_state_join_rules(&self, _: SyncRoom, _: &SyncStateEvent) {} // `AnyStrippedStateEvent`s /// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event. async fn on_stripped_state_member( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, _: Option, ) { } /// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event. - async fn on_stripped_state_name( - &self, - _: SyncRoom, - _: &StrippedStateEventStub, - ) { - } + async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedStateEvent) {} /// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event. async fn on_stripped_state_canonical_alias( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { } /// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event. async fn on_stripped_state_aliases( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { } /// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event. async fn on_stripped_state_avatar( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { } /// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event. async fn on_stripped_state_power_levels( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { } /// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event. async fn on_stripped_state_join_rules( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { } @@ -276,79 +271,79 @@ mod test { #[async_trait] impl EventEmitter for EvEmitterTest { - async fn on_room_member(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("member".to_string()) } - async fn on_room_name(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("name".to_string()) } async fn on_room_canonical_alias( &self, _: SyncRoom, - _: &StateEventStub, + _: &SyncStateEvent, ) { self.0.lock().await.push("canonical".to_string()) } - async fn on_room_aliases(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("aliases".to_string()) } - async fn on_room_avatar(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("avatar".to_string()) } - async fn on_room_message(&self, _: SyncRoom, _: &MessageEventStub) { + async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent) { self.0.lock().await.push("message".to_string()) } async fn on_room_message_feedback( &self, _: SyncRoom, - _: &MessageEventStub, + _: &SyncMessageEvent, ) { self.0.lock().await.push("feedback".to_string()) } - async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEventStub) { + async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) { self.0.lock().await.push("redaction".to_string()) } async fn on_room_power_levels( &self, _: SyncRoom, - _: &StateEventStub, + _: &SyncStateEvent, ) { self.0.lock().await.push("power".to_string()) } - async fn on_room_tombstone(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("tombstone".to_string()) } - async fn on_state_member(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("state member".to_string()) } - async fn on_state_name(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("state name".to_string()) } async fn on_state_canonical_alias( &self, _: SyncRoom, - _: &StateEventStub, + _: &SyncStateEvent, ) { self.0.lock().await.push("state canonical".to_string()) } - async fn on_state_aliases(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("state aliases".to_string()) } - async fn on_state_avatar(&self, _: SyncRoom, _: &StateEventStub) { + async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent) { self.0.lock().await.push("state avatar".to_string()) } async fn on_state_power_levels( &self, _: SyncRoom, - _: &StateEventStub, + _: &SyncStateEvent, ) { self.0.lock().await.push("state power".to_string()) } async fn on_state_join_rules( &self, _: SyncRoom, - _: &StateEventStub, + _: &SyncStateEvent, ) { self.0.lock().await.push("state rules".to_string()) } @@ -358,7 +353,7 @@ mod test { async fn on_stripped_state_member( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, _: Option, ) { self.0 @@ -370,7 +365,7 @@ mod test { async fn on_stripped_state_name( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { self.0.lock().await.push("stripped state name".to_string()) } @@ -378,7 +373,7 @@ mod test { async fn on_stripped_state_canonical_alias( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { self.0 .lock() @@ -389,7 +384,7 @@ mod test { async fn on_stripped_state_aliases( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { self.0 .lock() @@ -400,7 +395,7 @@ mod test { async fn on_stripped_state_avatar( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { self.0 .lock() @@ -411,7 +406,7 @@ mod test { async fn on_stripped_state_power_levels( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { self.0.lock().await.push("stripped state power".to_string()) } @@ -419,7 +414,7 @@ mod test { async fn on_stripped_state_join_rules( &self, _: SyncRoom, - _: &StrippedStateEventStub, + _: &StrippedStateEvent, ) { self.0.lock().await.push("stripped state rules".to_string()) } @@ -581,7 +576,7 @@ mod test { "unrecognized event", "redaction", "unrecognized event", - "unrecognized event", + // "unrecognized event", this is actually a redacted "m.room.messages" event "receipt event", "typing event" ], diff --git a/matrix_sdk_base/src/lib.rs b/matrix_sdk_base/src/lib.rs index 1ca09880..dc565c34 100644 --- a/matrix_sdk_base/src/lib.rs +++ b/matrix_sdk_base/src/lib.rs @@ -47,11 +47,16 @@ mod state; pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType}; pub use event_emitter::{CustomOrRawEvent, EventEmitter, SyncRoom}; +pub use models::Room; +pub use state::{AllRooms, ClientState}; + #[cfg(feature = "encryption")] pub use matrix_sdk_crypto::{Device, TrustState}; -pub use models::Room; -pub use state::AllRooms; -pub use state::ClientState; + +#[cfg(feature = "messages")] +#[cfg_attr(docsrs, doc(cfg(feature = "messages")))] +pub use models::{MessageQueue, MessageWrapper, PossiblyRedactedExt}; + #[cfg(not(target_arch = "wasm32"))] pub use state::JsonStore; pub use state::StateStore; diff --git a/matrix_sdk_base/src/models/message.rs b/matrix_sdk_base/src/models/message.rs index d1431e6c..fa47b899 100644 --- a/matrix_sdk_base/src/models/message.rs +++ b/matrix_sdk_base/src/models/message.rs @@ -3,17 +3,50 @@ //! The `Room` struct optionally holds a `MessageQueue` if the "messages" //! feature is enabled. -use std::cmp::Ordering; -use std::ops::{Deref, DerefMut}; -use std::vec::IntoIter; - -use crate::events::{AnyMessageEventContent, AnyMessageEventStub, MessageEventStub}; +use std::{ + cmp::Ordering, + ops::{Deref, DerefMut}, + time::SystemTime, + vec::IntoIter, +}; +use matrix_sdk_common::identifiers::EventId; use serde::{de, ser, Serialize}; +use crate::events::AnyPossiblyRedactedSyncMessageEvent; + +/// Exposes some of the field access methods found in the event held by +/// `AnyPossiblyRedacted*` enums. +/// +/// This is just an extension trait to aid the ease of use of certain event enums. +pub trait PossiblyRedactedExt { + /// Access the redacted or full events `event_id` field. + fn event_id(&self) -> &EventId; + /// Access the redacted or full events `origin_server_ts` field. + fn origin_server_ts(&self) -> &SystemTime; +} + +impl PossiblyRedactedExt for AnyPossiblyRedactedSyncMessageEvent { + /// Access the underlying events `event_id`. + fn event_id(&self) -> &EventId { + match self { + Self::Regular(e) => e.event_id(), + Self::Redacted(e) => e.event_id(), + } + } + + /// Access the underlying events `origin_server_ts`. + fn origin_server_ts(&self) -> &SystemTime { + match self { + Self::Regular(e) => e.origin_server_ts(), + Self::Redacted(e) => e.origin_server_ts(), + } + } +} + const MESSAGE_QUEUE_CAP: usize = 35; -pub type SyncMessageEvent = MessageEventStub; +pub type SyncMessageEvent = AnyPossiblyRedactedSyncMessageEvent; /// A queue that holds the 35 most recent messages received from the server. #[derive(Clone, Debug, Default)] @@ -29,18 +62,6 @@ pub struct MessageQueue { #[derive(Clone, Debug, Serialize)] pub struct MessageWrapper(pub SyncMessageEvent); -impl MessageWrapper { - pub fn clone_into_any_content(event: &AnyMessageEventStub) -> SyncMessageEvent { - MessageEventStub { - content: event.content(), - sender: event.sender().clone(), - origin_server_ts: *event.origin_server_ts(), - event_id: event.event_id().clone(), - unsigned: event.unsigned().clone(), - } - } -} - impl Deref for MessageWrapper { type Target = SyncMessageEvent; @@ -57,7 +78,7 @@ impl DerefMut for MessageWrapper { impl PartialEq for MessageWrapper { fn eq(&self, other: &MessageWrapper) -> bool { - self.0.event_id == other.0.event_id + self.0.event_id() == other.0.event_id() } } @@ -65,7 +86,7 @@ impl Eq for MessageWrapper {} impl PartialOrd for MessageWrapper { fn partial_cmp(&self, other: &MessageWrapper) -> Option { - Some(self.0.origin_server_ts.cmp(&other.0.origin_server_ts)) + Some(self.0.origin_server_ts().cmp(&other.0.origin_server_ts())) } } @@ -82,7 +103,7 @@ impl PartialEq for MessageQueue { .msgs .iter() .zip(other.msgs.iter()) - .all(|(msg_a, msg_b)| msg_a.event_id == msg_b.event_id) + .all(|(msg_a, msg_b)| msg_a.event_id() == msg_b.event_id()) } } @@ -100,7 +121,7 @@ impl MessageQueue { pub fn push(&mut self, msg: SyncMessageEvent) -> bool { // only push new messages into the queue if let Some(latest) = self.msgs.last() { - if msg.origin_server_ts < latest.origin_server_ts && self.msgs.len() >= 10 { + if msg.origin_server_ts() < latest.origin_server_ts() && self.msgs.len() >= 10 { return false; } } @@ -120,10 +141,12 @@ impl MessageQueue { true } + /// Iterate over the messages in the queue. pub fn iter(&self) -> impl Iterator { self.msgs.iter() } + /// Iterate over each message mutably. pub fn iter_mut(&mut self) -> impl Iterator { self.msgs.iter_mut() } @@ -183,17 +206,18 @@ pub(crate) mod ser_deser { #[cfg(test)] mod test { - use super::*; - use std::collections::HashMap; use std::convert::TryFrom; + use matrix_sdk_common::{ + events::{AnyPossiblyRedactedSyncMessageEvent, AnySyncMessageEvent}, + identifiers::{RoomId, UserId}, + }; + use matrix_sdk_test::test_json; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; - use matrix_sdk_test::test_json; - - use crate::identifiers::{RoomId, UserId}; + use super::*; use crate::Room; #[test] @@ -204,7 +228,9 @@ mod test { let mut room = Room::new(&id, &user); let json: &serde_json::Value = &test_json::MESSAGE_TEXT; - let msg = serde_json::from_value::(json.clone()).unwrap(); + let msg = AnyPossiblyRedactedSyncMessageEvent::Regular( + serde_json::from_value::(json.clone()).unwrap(), + ); let mut msgs = MessageQueue::new(); msgs.push(msg.clone()); @@ -216,7 +242,6 @@ mod test { serde_json::json!({ "!roomid:example.com": { "room_id": "!roomid:example.com", - "disambiguated_display_names": {}, "room_name": { "name": null, "canonical_alias": null, @@ -250,7 +275,9 @@ mod test { let mut room = Room::new(&id, &user); let json: &serde_json::Value = &test_json::MESSAGE_TEXT; - let msg = serde_json::from_value::(json.clone()).unwrap(); + let msg = AnyPossiblyRedactedSyncMessageEvent::Regular( + serde_json::from_value::(json.clone()).unwrap(), + ); let mut msgs = MessageQueue::new(); msgs.push(msg.clone()); @@ -262,7 +289,6 @@ mod test { let json = serde_json::json!({ "!roomid:example.com": { "room_id": "!roomid:example.com", - "disambiguated_display_names": {}, "room_name": { "name": null, "canonical_alias": null, diff --git a/matrix_sdk_base/src/models/mod.rs b/matrix_sdk_base/src/models/mod.rs index 211e5b5d..20514724 100644 --- a/matrix_sdk_base/src/models/mod.rs +++ b/matrix_sdk_base/src/models/mod.rs @@ -4,5 +4,8 @@ mod message; mod room; mod room_member; +#[cfg(feature = "messages")] +#[cfg_attr(docsrs, doc(cfg(feature = "messages")))] +pub use message::{MessageQueue, MessageWrapper, PossiblyRedactedExt}; pub use room::{Room, RoomName}; pub use room_member::RoomMember; diff --git a/matrix_sdk_base/src/models/room.rs b/matrix_sdk_base/src/models/room.rs index 54a99ed2..7442d412 100644 --- a/matrix_sdk_base/src/models/room.rs +++ b/matrix_sdk_base/src/models/room.rs @@ -13,77 +13,61 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::borrow::Cow; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::convert::TryFrom; +#[cfg(feature = "messages")] +use std::ops::DerefMut; + +use serde::{Deserialize, Serialize}; +use tracing::{debug, error, trace}; #[cfg(feature = "messages")] -use super::message::{MessageQueue, MessageWrapper}; +use super::message::MessageQueue; use super::RoomMember; - use crate::api::r0::sync::sync_events::{RoomSummary, UnreadNotificationsCount}; -use crate::events::presence::PresenceEvent; -use crate::events::room::{ - aliases::AliasesEventContent, - canonical_alias::CanonicalAliasEventContent, - encryption::EncryptionEventContent, - member::{MemberEventContent, MembershipChange}, - name::NameEventContent, - power_levels::{NotificationPowerLevels, PowerLevelsEventContent}, - tombstone::TombstoneEventContent, -}; - use crate::events::{ - Algorithm, AnyRoomEventStub, AnyStateEventStub, AnyStrippedStateEventStub, EventType, - StateEventStub, StrippedStateEventStub, + presence::{PresenceEvent, PresenceEventContent}, + room::{ + aliases::AliasesEventContent, + canonical_alias::CanonicalAliasEventContent, + encryption::EncryptionEventContent, + member::{MemberEventContent, MembershipChange, MembershipState}, + name::NameEventContent, + power_levels::{NotificationPowerLevels, PowerLevelsEventContent}, + tombstone::TombstoneEventContent, + }, + Algorithm, AnyStrippedStateEvent, AnySyncRoomEvent, AnySyncStateEvent, EventType, + StrippedStateEvent, SyncStateEvent, }; #[cfg(feature = "messages")] use crate::events::{ - room::redaction::{RedactionEvent, RedactionEventStub}, - AnyMessageEventContent, AnyMessageEventStub, EventJson, + room::redaction::SyncRedactionEvent, AnyPossiblyRedactedSyncMessageEvent, AnySyncMessageEvent, }; use crate::identifiers::{RoomAliasId, RoomId, UserId}; -use crate::js_int::{uint, Int, UInt}; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "messages")] -fn redaction_event_from_redaction_stub( - event: RedactionEventStub, - room_id: RoomId, -) -> RedactionEvent { - RedactionEvent { - content: event.content, - redacts: event.redacts, - event_id: event.event_id, - unsigned: event.unsigned, - sender: event.sender, - origin_server_ts: event.origin_server_ts, - room_id, - } -} +use crate::js_int::{int, uint, Int, UInt}; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] /// `RoomName` allows the calculation of a text room name. pub struct RoomName { /// The displayed name of the room. name: Option, - /// The canonical alias of the room ex. `#room-name:example.com` and port number. + /// The canonical alias of the room ex. `#room-name:example.com` and port + /// number. canonical_alias: Option, /// List of `RoomAliasId`s the room has been given. aliases: Vec, - /// Users which can be used to generate a room name if the room does not have - /// one. Required if room name or canonical aliases are not set or empty. + /// Users which can be used to generate a room name if the room does not + /// have one. Required if room name or canonical aliases are not set or + /// empty. pub heroes: Vec, - /// Number of users whose membership status is `join`. - /// Required if field has changed since last sync; otherwise, it may be - /// omitted. + /// Number of users whose membership status is `join`. Required if field + /// has changed since last sync; otherwise, it may be omitted. pub joined_member_count: Option, - /// Number of users whose membership status is `invite`. - /// Required if field has changed since last sync; otherwise, it may be - /// omitted. + /// Number of users whose membership status is `invite`. Required if field + /// has changed since last sync; otherwise, it may be omitted. pub invited_member_count: Option, } @@ -145,8 +129,8 @@ impl EncryptionInfo { } } -impl From<&StateEventStub> for EncryptionInfo { - fn from(event: &StateEventStub) -> Self { +impl From<&SyncStateEvent> for EncryptionInfo { + fn from(event: &SyncStateEvent) -> Self { EncryptionInfo { algorithm: event.content.algorithm.clone(), rotation_period_ms: event @@ -166,12 +150,6 @@ pub struct Tombstone { replacement: RoomId, } -#[derive(Debug, PartialEq, Eq)] -enum MemberDirection { - Entering, - Exiting, -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] /// A Matrix room. pub struct Room { @@ -190,8 +168,8 @@ pub struct Room { pub joined_members: HashMap, /// A queue of messages, holds no more than 10 of the most recent messages. /// - /// This is helpful when using a `StateStore` to avoid multiple requests - /// to the server for messages. + /// This is helpful when using a `StateStore` to avoid multiple requests to + /// the server for messages. #[cfg(feature = "messages")] #[cfg_attr(docsrs, doc(cfg(feature = "messages")))] #[serde(with = "super::message::ser_deser")] @@ -208,8 +186,6 @@ pub struct Room { pub unread_notifications: Option, /// The tombstone state of this room. pub tombstone: Option, - /// The map of disambiguated display names for users who have the same display name - disambiguated_display_names: HashMap, } impl RoomName { @@ -228,8 +204,8 @@ impl RoomName { true } - /// Calculate the canonical display name of a room, taking into account its name, aliases and - /// members. + /// Calculate the canonical display name of the room, taking into account + /// its name, aliases and members. /// /// The display name is calculated according to [this algorithm][spec]. /// @@ -283,7 +259,8 @@ impl RoomName { .collect::>(); names.sort(); - // TODO: What length does the spec want us to use here and in the `else`? + // TODO: What length does the spec want us to use here and in + // the `else`? format!("{}, and {} others", names.join(", "), (joined + invited)) } else { "Empty room".to_string() @@ -316,7 +293,6 @@ impl Room { unread_highlight: None, unread_notifications: None, tombstone: None, - disambiguated_display_names: HashMap::new(), } } @@ -341,193 +317,207 @@ impl Room { self.encrypted.as_ref() } - /// Get the disambiguated display name for a member of this room. + /// Process the join or invite event for a new room member. /// - /// If a member has no display name set, returns the MXID as a fallback. Additionally, we - /// return the MXID even if there is no such member in the room. + /// This method should only be called on events which add new members, not + /// those related to existing ones. /// - /// When displaying a room member's display name, clients *must* use this method to obtain the - /// name instead of displaying the `RoomMember::display_name` directly. This is because - /// multiple members can share the same display name in which case the display name has to be - /// disambiguated. - pub fn member_display_name<'a>(&'a self, id: &'a UserId) -> Cow<'a, str> { - let disambiguated_name = self - .disambiguated_display_names - .get(id) - .map(|s| s.as_str().into()); - - if let Some(name) = disambiguated_name { - // The display name of the member is non-unique so we return a disambiguated version. - name - } else if let Some(member) = self - .joined_members - .get(id) - .or_else(|| self.invited_members.get(id)) - { - // The display name of the member is unique so we can return it directly if it is set. - // If not, we return his MXID. - member.name().into() - } else { - // There is no member with the requested MXID in the room. We still return the MXID. - id.as_str().into() - } - } - - fn add_member(&mut self, event: &StateEventStub) -> bool { + /// Returns a tuple of: + /// + /// 1. True if the event made changes to the room's state, false otherwise. + /// 2. A map of display name ambiguity status changes (see + /// `disambiguation_updates`). + /// + /// # Arguments + /// + /// * `target_member` - The ID of the member to add. + /// * `event` - The join or invite event for the specified room member. + fn add_member( + &mut self, + target_member: &UserId, + event: &SyncStateEvent, + ) -> (bool, HashMap) { let new_member = RoomMember::new(event, &self.room_id); - if self.joined_members.contains_key(&new_member.user_id) - || self.invited_members.contains_key(&new_member.user_id) - { - return false; + if self.joined_members.contains_key(&new_member.user_id) { + error!("add_member called on event of an already joined user"); + return (false, HashMap::new()); } - match event.membership_change() { - MembershipChange::Joined => self - .joined_members - .insert(new_member.user_id.clone(), new_member.clone()), - MembershipChange::Invited => self - .invited_members - .insert(new_member.user_id.clone(), new_member.clone()), - _ => { - panic!("Room::add_member called on an event that is neither a join nor an invite.") - } - }; - - // Perform display name disambiguations, if necessary. - let disambiguations = self.disambiguation_updates(&new_member, MemberDirection::Entering); - for (id, name) in disambiguations.into_iter() { - match name { - None => self.disambiguated_display_names.remove(&id), - Some(name) => self.disambiguated_display_names.insert(id, name), - }; - } - - true - } - - /// Process the member event of a leaving user. - /// - /// Returns true if this made a change to the room's state, false otherwise. - fn remove_member(&mut self, event: &StateEventStub) -> bool { - let leaving_member = RoomMember::new(event, &self.room_id); - // Perform display name disambiguations, if necessary. let disambiguations = - self.disambiguation_updates(&leaving_member, MemberDirection::Exiting); - for (id, name) in disambiguations.into_iter() { - match name { - None => self.disambiguated_display_names.remove(&id), - Some(name) => self.disambiguated_display_names.insert(id, name), - }; + self.disambiguation_updates(target_member, None, new_member.display_name.clone()); + + debug!("add_member: disambiguations: {:#?}", disambiguations); + + match event.content.membership { + MembershipState::Join => { + // Since the member is now joined, he shouldn't be tracked as an invited member any + // longer if he was previously tracked as such. + self.invited_members.remove(target_member); + + self.joined_members + .insert(target_member.clone(), new_member) + } + + MembershipState::Invite => self + .invited_members + .insert(target_member.clone(), new_member), + + _ => panic!("Room::add_member called on event that is neither `join` nor `invite`."), + }; + + for (id, is_ambiguous) in disambiguations.iter() { + self.get_member_mut(id).unwrap().display_name_ambiguous = *is_ambiguous; } - if self.joined_members.contains_key(&leaving_member.user_id) { - self.joined_members.remove(&leaving_member.user_id); - true - } else if self.invited_members.contains_key(&leaving_member.user_id) { - self.invited_members.remove(&leaving_member.user_id); - true - } else { - false + (true, disambiguations) + } + + /// Process the leaving event for a room member. + /// + /// Returns a tuple of: + /// + /// 1. True if the event made changes to the room's state, false otherwise. + /// 2. A map of display name ambiguity status changes (see + /// `disambiguation_updates`). + /// + /// # Arguments + /// + /// * `target_member` - The ID of the member to remove. + /// * `event` - The leaving event for the specified room member. + fn remove_member( + &mut self, + target_member: &UserId, + event: &SyncStateEvent, + ) -> (bool, HashMap) { + let leaving_member = RoomMember::new(event, &self.room_id); + + if self.get_member(target_member).is_none() { + return (false, HashMap::new()); + } + + // Perform display name disambiguations, if necessary. + let disambiguations = + self.disambiguation_updates(target_member, leaving_member.display_name, None); + + debug!("remove_member: disambiguations: {:#?}", disambiguations); + + for (id, is_ambiguous) in disambiguations.iter() { + self.get_member_mut(id).unwrap().display_name_ambiguous = *is_ambiguous; + } + + // TODO: factor this out to a method called `remove_member` and rename this method + // to something like `process_member_leaving_event`. + self.joined_members + .remove(target_member) + .or_else(|| self.invited_members.remove(target_member)); + + (true, disambiguations) + } + + /// Check whether the user with the MXID `user_id` is joined or invited to + /// the room. + /// + /// Returns true if so, false otherwise. + pub fn member_is_tracked(&self, user_id: &UserId) -> bool { + self.invited_members.contains_key(&user_id) || self.joined_members.contains_key(&user_id) + } + + /// Get a room member by user ID. + /// + /// If there is no such member, returns `None`. + pub fn get_member(&self, user_id: &UserId) -> Option<&RoomMember> { + self.joined_members + .get(user_id) + .or_else(|| self.invited_members.get(user_id)) + } + + /// Get a room member by user ID. + /// + /// If there is no such member, returns `None`. + pub fn get_member_mut(&mut self, user_id: &UserId) -> Option<&mut RoomMember> { + match self.joined_members.get_mut(user_id) { + None => self.invited_members.get_mut(user_id), + Some(m) => Some(m), } } - /// Given a room `member`, return the list of members which have the same display name. - /// - /// The `inclusive` parameter controls whether the passed member should be included in the - /// list or not. - fn shares_displayname_with(&self, member: &RoomMember, inclusive: bool) -> Vec { + /// Given a display name, return the set of members which share it. + fn display_name_equivalence_set(&self, name: &str) -> HashSet { let members = self .invited_members .iter() .chain(self.joined_members.iter()); - // Find all other users that share the same display name as the joining user. + // Find all other users that share the display name with the joining user. members - .filter(|(_, existing_member)| { + .filter(|(_, member)| { member .display_name .as_ref() - .and_then(|new_member_name| { - existing_member - .display_name - .as_ref() - .map(|existing_member_name| new_member_name == existing_member_name) - }) + .map(|other_name| other_name == name) .unwrap_or(false) }) - // If not an inclusive search, do not consider the member for which we are disambiguating. - .filter(|(id, _)| inclusive || **id != member.user_id) - .map(|(id, _)| id) - .cloned() + .map(|(_, member)| member.user_id.clone()) .collect() } - /// Given a room member, generate a map of all display name disambiguations which are necessary - /// in order to make that member's display name unique. + /// Answers the question "If `member` changed their display name from + /// `old_name` to `new_name`, which members' display names would become + /// ambiguous and which would no longer be ambiguous?". /// - /// The `inclusive` parameter controls whether or not the member for which we are - /// disambiguating should be considered a current member of the room. + /// Returns the map of ambiguity status changes for those members which + /// would be affected by the change. /// - /// Returns a map from MXID to disambiguated name. - fn member_disambiguations( - &self, - member: &RoomMember, - inclusive: bool, - ) -> HashMap { - let users_with_same_name = self.shares_displayname_with(member, inclusive); - let disambiguate_with = |members: Vec, f: fn(&RoomMember) -> String| { - members - .into_iter() - .filter_map(|id| { - self.joined_members - .get(&id) - .or_else(|| self.invited_members.get(&id)) - .map(f) - .map(|m| (id, m)) - }) - .collect::>() - }; - - match users_with_same_name.len() { - 0 => HashMap::new(), - 1 => disambiguate_with(users_with_same_name, |m: &RoomMember| m.name()), - _ => disambiguate_with(users_with_same_name, |m: &RoomMember| m.unique_name()), - } - } - - /// Calculate disambiguation updates needed when a room member either enters or exits. + /// It is important that this method be called *before* any changes are made + /// to the model, i.e. before any actual display name changes. + /// + /// # Arguments + /// + /// - `member`: The MXID of the member who is changing their display name. + /// - `old_name`: The old display name of `member`. May be `None` if + /// `member` had no display name in the room before (because he had not + /// set it or he is just entering the room). + /// - `new_name`: The new display name of `member`. May be `None` if + /// `member` will no longer have a display name in the room after the + /// change (because he is removing it or exiting the room). fn disambiguation_updates( &self, - member: &RoomMember, - when: MemberDirection, - ) -> HashMap> { - let before; - let after; + member: &UserId, + old_name: Option, + new_name: Option, + ) -> HashMap { + let old_name_eq_set = match old_name { + None => HashSet::new(), + Some(name) => self.display_name_equivalence_set(&name), + }; - match when { - MemberDirection::Entering => { - before = self.member_disambiguations(member, false); - after = self.member_disambiguations(member, true); - } - MemberDirection::Exiting => { - before = self.member_disambiguations(member, true); - after = self.member_disambiguations(member, false); - } - } + let disambiguate_old = match old_name_eq_set.len().saturating_sub(1) { + n if n > 1 => vec![(member.clone(), false)].into_iter().collect(), + 1 => old_name_eq_set.into_iter().map(|m| (m, false)).collect(), + 0 => HashMap::new(), + _ => panic!("impossible"), + }; - let mut res = before; - res.extend(after.clone()); + // - res.into_iter() - .map(|(user_id, name)| { - if !after.contains_key(&user_id) { - (user_id, None) - } else { - (user_id, Some(name)) - } - }) + let mut new_name_eq_set = match new_name { + None => HashSet::new(), + Some(name) => self.display_name_equivalence_set(&name), + }; + + new_name_eq_set.insert(member.clone()); + + let disambiguate_new = match new_name_eq_set.len() { + 1 => HashMap::new(), + 2 => new_name_eq_set.into_iter().map(|m| (m, true)).collect(), + _ => vec![(member.clone(), true)].into_iter().collect(), + }; + + disambiguate_old + .into_iter() + .chain(disambiguate_new.into_iter()) .collect() } @@ -548,7 +538,7 @@ impl Room { true } - fn set_room_power_level(&mut self, event: &StateEventStub) -> bool { + fn set_room_power_level(&mut self, event: &SyncStateEvent) -> bool { let PowerLevelsEventContent { ban, events, @@ -595,33 +585,69 @@ impl Room { /// Handle a room.member updating the room state if necessary. /// - /// Returns true if the joined member list changed, false otherwise. - pub fn handle_membership(&mut self, event: &StateEventStub) -> bool { + /// Returns a tuple of: + /// + /// 1. True if the joined member list changed, false otherwise. + /// 2. A map of display name ambiguity status changes (see + /// `disambiguation_updates`). + pub fn handle_membership( + &mut self, + event: &SyncStateEvent, + state_event: bool, + ) -> (bool, HashMap) { use MembershipChange::*; + use MembershipState::*; - // TODO: This would not be handled correctly as all the MemberEvents have the `prev_content` - // inside of `unsigned` field. - match event.membership_change() { - Invited | Joined => self.add_member(event), - Kicked | Banned | KickedAndBanned | InvitationRejected | Left => { - self.remove_member(event) + trace!( + "Received {} event: {}", + if state_event { "state" } else { "timeline" }, + event.event_id + ); + + let target_user = match UserId::try_from(event.state_key.clone()) { + Ok(id) => id, + Err(e) => { + error!("Received a member event with invalid state_key: {}", e); + return (false, HashMap::new()); } - ProfileChanged { .. } => { - let user_id = if let Ok(id) = UserId::try_from(event.state_key.as_str()) { - id - } else { - return false; - }; + }; - if let Some(member) = self.joined_members.get_mut(&user_id) { - member.update_profile(event) - } else { - false + if state_event && !self.member_is_tracked(&target_user) { + debug!( + "handle_membership: User {user_id} is {state} the room {room_id} ({room_name})", + user_id = target_user, + state = event.content.membership.describe(), + room_id = self.room_id, + room_name = self.display_name(), + ); + + match event.content.membership { + Join | Invite => self.add_member(&target_user, event), + + // We are not interested in tracking past members for now + _ => (false, HashMap::new()), + } + } else { + let change = event.membership_change(); + + debug!( + "handle_membership: User {user_id} {action} the room {room_id} ({room_name})", + user_id = target_user, + action = change.describe(), + room_id = self.room_id, + room_name = self.display_name(), + ); + + match change { + Invited | Joined => self.add_member(&target_user, event), + Kicked | Banned | KickedAndBanned | InvitationRejected | Left => { + self.remove_member(&target_user, event) } - } + ProfileChanged { .. } => self.update_member_profile(&target_user, event, change), - // Not interested in other events. - _ => false, + // Not interested in other events. + _ => (false, HashMap::new()), + } } } @@ -630,29 +656,38 @@ impl Room { /// Returns true if `MessageQueue` was added to. #[cfg(feature = "messages")] #[cfg_attr(docsrs, doc(cfg(feature = "messages")))] - pub fn handle_message(&mut self, event: &AnyMessageEventStub) -> bool { - let message = MessageWrapper::clone_into_any_content(event); - self.messages.push(message) + pub fn handle_message(&mut self, event: &AnySyncMessageEvent) -> bool { + self.messages + .push(AnyPossiblyRedactedSyncMessageEvent::Regular(event.clone())) } /// Handle a room.redaction event and update the `MessageQueue`. /// /// Returns true if `MessageQueue` was updated. The effected message event - /// has it's contents replaced with the `RedactionEventContents` and the whole - /// redaction event is added to the `Unsigned` `redacted_because` field. + /// has it's contents replaced with the `RedactionEventContents` and the + /// whole redaction event is added to the `Unsigned` `redacted_because` + /// field. #[cfg(feature = "messages")] #[cfg_attr(docsrs, doc(cfg(feature = "messages")))] - pub fn handle_redaction(&mut self, event: &RedactionEventStub) -> bool { + pub fn handle_redaction(&mut self, redacted_event: &SyncRedactionEvent) -> bool { + use crate::identifiers::RoomVersionId; + use crate::models::message::PossiblyRedactedExt; + if let Some(msg) = self .messages .iter_mut() - .find(|msg| event.redacts == msg.event_id) + .find(|msg| &redacted_event.redacts == msg.event_id()) { - msg.content = AnyMessageEventContent::RoomRedaction(event.content.clone()); - - let redaction = - redaction_event_from_redaction_stub(event.clone(), self.room_id.clone()); - msg.unsigned.redacted_because = Some(EventJson::from(redaction)); + match msg.deref_mut() { + AnyPossiblyRedactedSyncMessageEvent::Regular(event) => { + msg.0 = AnyPossiblyRedactedSyncMessageEvent::Redacted( + event + .clone() + .redact(redacted_event.clone(), RoomVersionId::version_6()), + ); + } + AnyPossiblyRedactedSyncMessageEvent::Redacted(_) => return false, + } true } else { false @@ -662,7 +697,7 @@ impl Room { /// Handle a room.aliases event, updating the room state if necessary. /// /// Returns true if the room name changed, false otherwise. - pub fn handle_room_aliases(&mut self, event: &StateEventStub) -> bool { + pub fn handle_room_aliases(&mut self, event: &SyncStateEvent) -> bool { match event.content.aliases.as_slice() { [alias] => self.push_room_alias(alias), [alias, ..] => self.push_room_alias(alias), @@ -670,10 +705,11 @@ impl Room { } } - /// Handle a room.canonical_alias event, updating the room state if necessary. + /// Handle a room.canonical_alias event, updating the room state if + /// necessary. /// /// Returns true if the room name changed, false otherwise. - pub fn handle_canonical(&mut self, event: &StateEventStub) -> bool { + pub fn handle_canonical(&mut self, event: &SyncStateEvent) -> bool { match &event.content.alias { Some(name) => self.canonical_alias(&name), _ => false, @@ -683,7 +719,7 @@ impl Room { /// Handle a room.name event, updating the room state if necessary. /// /// Returns true if the room name changed, false otherwise. - pub fn handle_room_name(&mut self, event: &StateEventStub) -> bool { + pub fn handle_room_name(&mut self, event: &SyncStateEvent) -> bool { match event.content.name() { Some(name) => self.set_room_name(name), _ => false, @@ -695,7 +731,7 @@ impl Room { /// Returns true if the room name changed, false otherwise. pub fn handle_stripped_room_name( &mut self, - event: &StrippedStateEventStub, + event: &StrippedStateEvent, ) -> bool { match event.content.name() { Some(name) => self.set_room_name(name), @@ -706,7 +742,7 @@ impl Room { /// Handle a room.power_levels event, updating the room state if necessary. /// /// Returns true if the room name changed, false otherwise. - pub fn handle_power_level(&mut self, event: &StateEventStub) -> bool { + pub fn handle_power_level(&mut self, event: &SyncStateEvent) -> bool { // NOTE: this is always true, we assume that if we get an event their is an update. let mut updated = self.set_room_power_level(event); @@ -717,15 +753,16 @@ impl Room { for user in event.content.users.keys() { if let Some(member) = self.joined_members.get_mut(user) { - if member.update_power(event, max_power) { + if Room::update_member_power(member, event, max_power) { updated = true; } } } + updated } - fn handle_tombstone(&mut self, event: &StateEventStub) -> bool { + fn handle_tombstone(&mut self, event: &SyncStateEvent) -> bool { self.tombstone = Some(Tombstone { body: event.content.body.clone(), replacement: event.content.replacement_room.clone(), @@ -733,7 +770,7 @@ impl Room { true } - fn handle_encryption_event(&mut self, event: &StateEventStub) -> bool { + fn handle_encryption_event(&mut self, event: &SyncStateEvent) -> bool { self.encrypted = Some(event.into()); true } @@ -745,30 +782,31 @@ impl Room { /// # Arguments /// /// * `event` - The event of the room. - pub fn receive_timeline_event(&mut self, event: &AnyRoomEventStub) -> bool { + pub fn receive_timeline_event(&mut self, event: &AnySyncRoomEvent) -> bool { match event { - AnyRoomEventStub::State(event) => match event { + AnySyncRoomEvent::State(event) => match event { // update to the current members of the room - AnyStateEventStub::RoomMember(event) => self.handle_membership(event), + AnySyncStateEvent::RoomMember(event) => self.handle_membership(event, false).0, // finds all events related to the name of the room for later use - AnyStateEventStub::RoomName(event) => self.handle_room_name(event), - AnyStateEventStub::RoomCanonicalAlias(event) => self.handle_canonical(event), - AnyStateEventStub::RoomAliases(event) => self.handle_room_aliases(event), + AnySyncStateEvent::RoomName(event) => self.handle_room_name(event), + AnySyncStateEvent::RoomCanonicalAlias(event) => self.handle_canonical(event), + AnySyncStateEvent::RoomAliases(event) => self.handle_room_aliases(event), // power levels of the room members - AnyStateEventStub::RoomPowerLevels(event) => self.handle_power_level(event), - AnyStateEventStub::RoomTombstone(event) => self.handle_tombstone(event), - AnyStateEventStub::RoomEncryption(event) => self.handle_encryption_event(event), + AnySyncStateEvent::RoomPowerLevels(event) => self.handle_power_level(event), + AnySyncStateEvent::RoomTombstone(event) => self.handle_tombstone(event), + AnySyncStateEvent::RoomEncryption(event) => self.handle_encryption_event(event), _ => false, }, - AnyRoomEventStub::Message(event) => match event { + AnySyncRoomEvent::Message(event) => match event { #[cfg(feature = "messages")] // We ignore this variants event because `handle_message` takes the enum - // to store AnyMessageEventStub events in the `MessageQueue`. - AnyMessageEventStub::RoomMessage(_) => self.handle_message(event), + // to store AnySyncMessageEvent events in the `MessageQueue`. + AnySyncMessageEvent::RoomMessage(_) => self.handle_message(event), #[cfg(feature = "messages")] - AnyMessageEventStub::RoomRedaction(event) => self.handle_redaction(event), + AnySyncMessageEvent::RoomRedaction(event) => self.handle_redaction(event), _ => false, }, + AnySyncRoomEvent::RedactedMessage(_) | AnySyncRoomEvent::RedactedState(_) => false, } } @@ -779,18 +817,18 @@ impl Room { /// # Arguments /// /// * `event` - The event of the room. - pub fn receive_state_event(&mut self, event: &AnyStateEventStub) -> bool { + pub fn receive_state_event(&mut self, event: &AnySyncStateEvent) -> bool { match event { // update to the current members of the room - AnyStateEventStub::RoomMember(member) => self.handle_membership(member), + AnySyncStateEvent::RoomMember(member) => self.handle_membership(member, true).0, // finds all events related to the name of the room for later use - AnyStateEventStub::RoomName(name) => self.handle_room_name(name), - AnyStateEventStub::RoomCanonicalAlias(c_alias) => self.handle_canonical(c_alias), - AnyStateEventStub::RoomAliases(alias) => self.handle_room_aliases(alias), + AnySyncStateEvent::RoomName(name) => self.handle_room_name(name), + AnySyncStateEvent::RoomCanonicalAlias(c_alias) => self.handle_canonical(c_alias), + AnySyncStateEvent::RoomAliases(alias) => self.handle_room_aliases(alias), // power levels of the room members - AnyStateEventStub::RoomPowerLevels(power) => self.handle_power_level(power), - AnyStateEventStub::RoomTombstone(tomb) => self.handle_tombstone(tomb), - AnyStateEventStub::RoomEncryption(encrypt) => self.handle_encryption_event(encrypt), + AnySyncStateEvent::RoomPowerLevels(power) => self.handle_power_level(power), + AnySyncStateEvent::RoomTombstone(tomb) => self.handle_tombstone(tomb), + AnySyncStateEvent::RoomEncryption(encrypt) => self.handle_encryption_event(encrypt), _ => false, } } @@ -801,44 +839,248 @@ impl Room { /// /// # Arguments /// - /// * `event` - The `AnyStrippedStateEvent` sent by the server for invited but not - /// joined rooms. - pub fn receive_stripped_state_event(&mut self, event: &AnyStrippedStateEventStub) -> bool { + /// * `event` - The `AnyStrippedStateEvent` sent by the server for invited + /// but not joined rooms. + pub fn receive_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool { match event { - AnyStrippedStateEventStub::RoomName(event) => self.handle_stripped_room_name(event), + AnyStrippedStateEvent::RoomName(event) => self.handle_stripped_room_name(event), _ => false, } } - /// Receive a presence event from an `IncomingResponse` and updates the client state. + /// Receive a presence event for a member of the current room. /// - /// This will only update the user if found in the current room looped through - /// by `Client::sync`. - /// Returns true if the specific users presence has changed, false otherwise. + /// Returns true if the event causes a change to the member's presence, + /// false otherwise. /// /// # Arguments /// - /// * `event` - The presence event for a specified room member. + /// * `event` - The presence event to receive and process. pub fn receive_presence_event(&mut self, event: &PresenceEvent) -> bool { + let PresenceEvent { + content: + PresenceEventContent { + avatar_url, + currently_active, + displayname, + last_active_ago, + presence, + status_msg, + }, + .. + } = event; + if let Some(member) = self.joined_members.get_mut(&event.sender) { - if member.did_update_presence(event) { + if member.display_name == *displayname + && member.avatar_url == *avatar_url + && member.presence.as_ref() == Some(presence) + && member.status_msg == *status_msg + && member.last_active_ago == *last_active_ago + && member.currently_active == *currently_active + { + // Everything is the same, nothing to do. false } else { - member.update_presence(event); + // Something changed, do the update. + + member.presence_events.push(event.clone()); + member.avatar_url = avatar_url.clone(); + member.currently_active = *currently_active; + member.display_name = displayname.clone(); + member.last_active_ago = *last_active_ago; + member.presence = Some(*presence); + member.status_msg = status_msg.clone(); + true } } else { - // this is probably an error as we have a `PresenceEvent` for a user - // we don't know about + // This is probably an error as we have a `PresenceEvent` for a user + // we don't know about. false } } + + /// Process an update of a member's profile. + /// + /// Returns a tuple of: + /// + /// 1. True if the event made changes to the room's state, false otherwise. + /// 2. A map of display name ambiguity status changes (see + /// `disambiguation_updates`). + /// + /// # Arguments + /// + /// * `target_member` - The ID of the member to update. + /// * `event` - The profile update event for the specified room member. + pub fn update_member_profile( + &mut self, + target_member: &UserId, + event: &SyncStateEvent, + change: MembershipChange, + ) -> (bool, HashMap) { + let member = self.get_member(target_member); + let member = match member { + Some(member) => member, + + None => { + debug!("update_member_profile [{}]: Got a profile update for user {} but he's not a room member", + self.room_id, target_member); + return (false, HashMap::new()); + } + }; + + let old_name = member.display_name.clone(); + let new_name = event.content.displayname.clone(); + + match change { + MembershipChange::ProfileChanged { + displayname_changed, + avatar_url_changed, + } => { + if displayname_changed { + debug!( + "update_member_profile [{}]: {} changed display name from {:#?} to {:#?}", + self.room_id, target_member, old_name, &new_name + ); + } + + if avatar_url_changed { + debug!( + "update_member_profile [{}]: {} changed avatar URL from {:#?} to {:#?}", + self.room_id, target_member, &member.avatar_url, &new_name + ); + } + } + + _ => { + error!( + "update_member_profile [{}]: got a ProfileChanged but nothing changed", + self.room_id + ); + return (false, HashMap::new()); + } + } + + let disambiguations = + self.disambiguation_updates(target_member, old_name, new_name.clone()); + for (id, is_ambiguous) in disambiguations.iter() { + if self.get_member_mut(id).is_none() { + debug!("update_member_profile [{}]: Tried disambiguating display name for {} but he's not there", + self.room_id, + id); + } else { + self.get_member_mut(id).unwrap().display_name_ambiguous = *is_ambiguous; + } + } + + debug!( + "update_member_profile [{}]: disambiguations: {:#?}", + self.room_id, &disambiguations + ); + + let changed = match self.get_member_mut(target_member) { + Some(member) => { + member.display_name = new_name; + member.avatar_url = event.content.avatar_url.clone(); + true + } + None => { + error!( + "update_member_profile [{}]: user {} does not exist", + self.room_id, target_member + ); + + false + } + }; + + (changed, disambiguations) + } + + /// Process an update of a member's power level. + /// + /// # Arguments + /// + /// * `event` - The power level event to process. + /// * `max_power` - Maximum power level allowed. + pub fn update_member_power( + member: &mut RoomMember, + event: &SyncStateEvent, + max_power: Int, + ) -> bool { + let changed; + + if let Some(user_power) = event.content.users.get(&member.user_id) { + changed = member.power_level != Some(*user_power); + member.power_level = Some(*user_power); + } else { + changed = member.power_level != Some(event.content.users_default); + member.power_level = Some(event.content.users_default); + } + + if max_power > int!(0) { + member.power_level_norm = Some((member.power_level.unwrap() * int!(100)) / max_power); + } + + changed + } +} + +trait Describe { + fn describe(&self) -> String; +} + +impl Describe for MembershipState { + fn describe(&self) -> String { + match self { + Self::Ban => "is banned in", + Self::Invite => "is invited to", + Self::Join => "is a member of", + Self::Knock => "is requesting access", + Self::Leave => "left", + _ => "unhandled case of MembershipState", + } + .to_string() + } +} + +impl Describe for MembershipChange { + fn describe(&self) -> String { + match self { + Self::Invited => "got invited to", + Self::Joined => "joined", + Self::Kicked => "got kicked from", + Self::Banned => "got banned from", + Self::Unbanned => "got unbanned from", + Self::KickedAndBanned => "got kicked and banned from", + Self::InvitationRejected => "rejected the invitation to", + Self::InvitationRevoked => "got their invitation revoked from", + Self::Left => "left", + Self::ProfileChanged { + displayname_changed, + avatar_url_changed, + } => match (*displayname_changed, *avatar_url_changed) { + (true, true) => "changed their displayname and avatar", + (true, false) => "changed their displayname", + (false, true) => "changed their avatar", + _ => { + error!("Got ProfileChanged but nothing changed"); + "impossible: changed nothing in their profile" + } + }, + Self::None => "did nothing in", + Self::NotImplemented => "NOT IMPLEMENTED", + Self::Error => "ERROR", + _ => "unhandled case of MembershipChange", + } + .to_string() + } } #[cfg(test)] mod test { use super::*; - use crate::events::{room::encryption::EncryptionEventContent, UnsignedData}; + use crate::events::{room::encryption::EncryptionEventContent, EventJson, Unsigned}; use crate::identifiers::{EventId, UserId}; use crate::{BaseClient, Session}; use matrix_sdk_test::{async_test, sync_response, EventBuilder, EventsJson, SyncResponseFile}; @@ -886,6 +1128,113 @@ mod test { assert!(room.deref().power_levels.is_some()) } + #[async_test] + async fn member_is_not_both_invited_and_joined() { + let client = get_client().await; + let room_id = get_room_id(); + let user_id1 = UserId::try_from("@example:localhost").unwrap(); + let user_id2 = UserId::try_from("@example2:localhost").unwrap(); + + let member2_invite_event = serde_json::json!({ + "content": { + "avatar_url": null, + "displayname": "example2", + "membership": "invite" + }, + "event_id": "$16345217l517tabbz:localhost", + "membership": "join", + "origin_server_ts": 1455123234, + "sender": format!("{}", user_id1), + "state_key": format!("{}", user_id2), + "type": "m.room.member", + "unsigned": { + "age": 1989321234, + "replaces_state": "$1622a2311315tkjoA:localhost" + } + }); + + let member2_join_event = serde_json::json!({ + "content": { + "avatar_url": null, + "displayname": "example2", + "membership": "join" + }, + "event_id": "$163409224327jkbba:localhost", + "membership": "join", + "origin_server_ts": 1455123238, + "sender": format!("{}", user_id2), + "state_key": format!("{}", user_id2), + "type": "m.room.member", + "prev_content": { + "avatar_url": null, + "displayname": "example2", + "membership": "invite" + }, + "unsigned": { + "age": 1989321214, + "replaces_state": "$16345217l517tabbz:localhost" + } + }); + + let mut event_builder = EventBuilder::new(); + + let mut member1_join_sync_response = event_builder + .add_room_event(EventsJson::Member) + .build_sync_response(); + + let mut member2_invite_sync_response = event_builder + .add_custom_joined_event(&room_id, member2_invite_event) + .build_sync_response(); + + let mut member2_join_sync_response = event_builder + .add_custom_joined_event(&room_id, member2_join_event) + .build_sync_response(); + + // Test that `user` is either joined or invited to `room` but not both. + async fn invited_or_joined_but_not_both(client: &BaseClient, room: &RoomId, user: &UserId) { + let room = client.get_joined_room(&room).await.unwrap(); + let room = room.read().await; + + assert!( + room.invited_members.get(&user).is_none() + || room.joined_members.get(&user).is_none() + ); + assert!( + room.invited_members.get(&user).is_some() + || room.joined_members.get(&user).is_some() + ); + }; + + // First member joins. + client + .receive_sync_response(&mut member1_join_sync_response) + .await + .unwrap(); + + // The first member is not *both* invited and joined but it *is* one of those. + invited_or_joined_but_not_both(&client, &room_id, &user_id1).await; + + // First member invites second member. + client + .receive_sync_response(&mut member2_invite_sync_response) + .await + .unwrap(); + + // Neither member is *both* invited and joined, but they are both *at least one* of those. + invited_or_joined_but_not_both(&client, &room_id, &user_id1).await; + invited_or_joined_but_not_both(&client, &room_id, &user_id2).await; + + // Second member joins. + client + .receive_sync_response(&mut member2_join_sync_response) + .await + .unwrap(); + + // Repeat the previous test. + invited_or_joined_but_not_both(&client, &room_id, &user_id1).await; + invited_or_joined_but_not_both(&client, &room_id, &user_id2).await; + } + #[async_test] async fn test_member_display_name() { // Initialize @@ -919,6 +1268,47 @@ mod test { } }); + let member1_invites_member2_event = serde_json::json!({ + "content": { + "avatar_url": null, + "displayname": "example", + "membership": "invite" + }, + "event_id": "$16345217l517tabbz:localhost", + "membership": "invite", + "origin_server_ts": 1455123238, + "sender": format!("{}", user_id1), + "state_key": format!("{}", user_id2), + "type": "m.room.member", + "unsigned": { + "age": 1989321238, + "replaces_state": "$1622a2311315tkjoA:localhost" + } + }); + + let member2_name_change_event = serde_json::json!({ + "content": { + "avatar_url": null, + "displayname": "changed", + "membership": "join" + }, + "event_id": "$16345217l517tabbz:localhost", + "membership": "join", + "origin_server_ts": 1455123238, + "sender": format!("{}", user_id2), + "state_key": format!("{}", user_id2), + "type": "m.room.member", + "prev_content": { + "avatar_url": null, + "displayname": "example", + "membership": "join" + }, + "unsigned": { + "age": 1989321238, + "replaces_state": "$1622a2311315tkjoA:localhost" + } + }); + let member2_leave_event = serde_json::json!({ "content": { "avatar_url": null, @@ -995,19 +1385,29 @@ mod test { .build_sync_response(); let mut member2_join_sync_response = event_builder - .add_custom_joined_event(&room_id, member2_join_event) + .add_custom_joined_event(&room_id, member2_join_event.clone()) .build_sync_response(); let mut member3_join_sync_response = event_builder .add_custom_joined_event(&room_id, member3_join_event) .build_sync_response(); - let mut member2_leave_sync_response = event_builder + let mut member2_and_member3_leave_sync_response = event_builder .add_custom_joined_event(&room_id, member2_leave_event) + .add_custom_joined_event(&room_id, member3_leave_event) .build_sync_response(); - let mut member3_leave_sync_response = event_builder - .add_custom_joined_event(&room_id, member3_leave_event) + let mut member2_rejoins_when_invited_sync_response = event_builder + .add_custom_joined_event(&room_id, member1_invites_member2_event) + .add_custom_joined_event(&room_id, member2_join_event) + .build_sync_response(); + + let mut member1_name_change_sync_response = event_builder + .add_room_event(EventsJson::MemberNameChange) + .build_sync_response(); + + let mut member2_name_change_sync_response = event_builder + .add_custom_joined_event(&room_id, member2_name_change_event) .build_sync_response(); // First member with display name "example" joins @@ -1020,7 +1420,7 @@ mod test { { let room = client.get_joined_room(&room_id).await.unwrap(); let room = room.read().await; - let display_name1 = room.member_display_name(&user_id1); + let display_name1 = room.get_member(&user_id1).unwrap().disambiguated_name(); assert_eq!("example", display_name1); } @@ -1039,9 +1439,9 @@ mod test { { let room = client.get_joined_room(&room_id).await.unwrap(); let room = room.read().await; - let display_name1 = room.member_display_name(&user_id1); - let display_name2 = room.member_display_name(&user_id2); - let display_name3 = room.member_display_name(&user_id3); + let display_name1 = room.get_member(&user_id1).unwrap().disambiguated_name(); + let display_name2 = room.get_member(&user_id2).unwrap().disambiguated_name(); + let display_name3 = room.get_member(&user_id3).unwrap().disambiguated_name(); assert_eq!(format!("example ({})", user_id1), display_name1); assert_eq!(format!("example ({})", user_id2), display_name2); @@ -1050,11 +1450,7 @@ mod test { // Second and third member leave. The first's display name is now just "example" again. client - .receive_sync_response(&mut member2_leave_sync_response) - .await - .unwrap(); - client - .receive_sync_response(&mut member3_leave_sync_response) + .receive_sync_response(&mut member2_and_member3_leave_sync_response) .await .unwrap(); @@ -1062,10 +1458,64 @@ mod test { let room = client.get_joined_room(&room_id).await.unwrap(); let room = room.read().await; - let display_name1 = room.member_display_name(&user_id1); + let display_name1 = room.get_member(&user_id1).unwrap().disambiguated_name(); assert_eq!("example", display_name1); } + + // Second member rejoins after being invited by first member. Both of their names are + // disambiguated. + client + .receive_sync_response(&mut member2_rejoins_when_invited_sync_response) + .await + .unwrap(); + + { + let room = client.get_joined_room(&room_id).await.unwrap(); + let room = room.read().await; + + let display_name1 = room.get_member(&user_id1).unwrap().disambiguated_name(); + let display_name2 = room.get_member(&user_id2).unwrap().disambiguated_name(); + + assert_eq!(format!("example ({})", user_id1), display_name1); + assert_eq!(format!("example ({})", user_id2), display_name2); + } + + // First member changes his display name to "changed". None of the display names are + // disambiguated. + client + .receive_sync_response(&mut member1_name_change_sync_response) + .await + .unwrap(); + + { + let room = client.get_joined_room(&room_id).await.unwrap(); + let room = room.read().await; + + let display_name1 = room.get_member(&user_id1).unwrap().disambiguated_name(); + let display_name2 = room.get_member(&user_id2).unwrap().disambiguated_name(); + + assert_eq!("changed", display_name1); + assert_eq!("example", display_name2); + } + + // Second member *also* changes his display name to "changed". Again, both display name are + // disambiguated. + client + .receive_sync_response(&mut member2_name_change_sync_response) + .await + .unwrap(); + + { + let room = client.get_joined_room(&room_id).await.unwrap(); + let room = room.read().await; + + let display_name1 = room.get_member(&user_id1).unwrap().disambiguated_name(); + let display_name2 = room.get_member(&user_id2).unwrap().disambiguated_name(); + + assert_eq!(format!("changed ({})", user_id1), display_name1); + assert_eq!(format!("changed ({})", user_id2), display_name2); + } } #[async_test] @@ -1195,7 +1645,7 @@ mod test { "type": "m.room.redaction", "redacts": "$152037280074GZeOm:localhost" }); - let mut event: EventJson = serde_json::from_value(json).unwrap(); + let mut event: EventJson = serde_json::from_value(json).unwrap(); client .receive_joined_timeline_event(&room_id, &mut event) .await @@ -1203,10 +1653,15 @@ mod test { for room in client.joined_rooms().read().await.values() { let queue = &room.read().await.messages; - if let crate::events::AnyMessageEventContent::RoomRedaction(content) = - &queue.msgs[0].content + if let crate::events::AnyPossiblyRedactedSyncMessageEvent::Redacted( + crate::events::AnyRedactedSyncMessageEvent::RoomMessage(event), + ) = &queue.msgs[0].deref() { - assert_eq!(content.reason, Some("😀".to_string())); + // this is the id from the message event in the sync response + assert_eq!( + event.event_id, + EventId::try_from("$152037280074GZeOm:localhost").unwrap() + ) } else { panic!("message event in message queue should be redacted") } @@ -1230,17 +1685,17 @@ mod test { client.restore_login(session).await.unwrap(); client.receive_sync_response(&mut response).await.unwrap(); - let event = StateEventStub { + let mut content = EncryptionEventContent::new(Algorithm::MegolmV1AesSha2); + content.rotation_period_ms = Some(100_000u32.into()); + content.rotation_period_msgs = Some(100u32.into()); + + let event = SyncStateEvent { event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), origin_server_ts: SystemTime::now(), sender: user_id, state_key: "".into(), - unsigned: UnsignedData::default(), - content: EncryptionEventContent { - algorithm: Algorithm::MegolmV1AesSha2, - rotation_period_ms: Some(100_000u32.into()), - rotation_period_msgs: Some(100u32.into()), - }, + unsigned: Unsigned::default(), + content, prev_content: None, }; diff --git a/matrix_sdk_base/src/models/room_member.rs b/matrix_sdk_base/src/models/room_member.rs index 6b56d49c..cd45f866 100644 --- a/matrix_sdk_base/src/models/room_member.rs +++ b/matrix_sdk_base/src/models/room_member.rs @@ -15,16 +15,17 @@ use std::convert::TryFrom; -use crate::events::presence::{PresenceEvent, PresenceEventContent, PresenceState}; -use crate::events::room::{ - member::{MemberEventContent, MembershipChange, MembershipState}, - power_levels::PowerLevelsEventContent, +use matrix_sdk_common::{ + events::{ + presence::{PresenceEvent, PresenceState}, + room::member::MemberEventContent, + SyncStateEvent, + }, + identifiers::{RoomId, UserId}, + js_int::{Int, UInt}, }; -use crate::events::StateEventStub; -use crate::identifiers::{RoomId, UserId}; - -use crate::js_int::{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(Debug, Serialize, Deserialize, Clone)] @@ -34,6 +35,9 @@ pub struct RoomMember { pub user_id: UserId, /// The human readable name of the user. pub display_name: Option, + /// Whether the member's display name is ambiguous due to being shared with + /// other members. + pub display_name_ambiguous: bool, /// The matrix url of the users avatar. pub avatar_url: Option, /// The time, in ms, since the user interacted with the server. @@ -52,10 +56,18 @@ pub struct RoomMember { pub power_level: Option, /// The normalized power level of this `RoomMember` (0-100). pub power_level_norm: Option, - /// The `MembershipState` of this `RoomMember`. - pub membership: MembershipState, /// The human readable name of this room member. pub name: String, + // FIXME: The docstring below is currently a lie since we only store the initial event that + // creates the member (the one we pass to RoomMember::new). + // + // The intent of this field is to keep the last (or last few?) state events related to the room + // member cached so we can quickly go back to the previous one in case some of them get + // redacted. Keeping all state for each room member is probably too much. + // + // Needs design. + /// The events that created the state of this room member. + pub events: Vec>, /// The `PresenceEvent`s connected to this user. pub presence_events: Vec, } @@ -67,18 +79,20 @@ impl PartialEq for RoomMember { && self.user_id == other.user_id && self.name == other.name && self.display_name == other.display_name + && self.display_name_ambiguous == other.display_name_ambiguous && self.avatar_url == other.avatar_url && self.last_active_ago == other.last_active_ago } } impl RoomMember { - pub fn new(event: &StateEventStub, room_id: &RoomId) -> Self { + pub fn new(event: &SyncStateEvent, room_id: &RoomId) -> Self { Self { name: event.state_key.clone(), room_id: room_id.clone(), user_id: UserId::try_from(event.state_key.as_str()).unwrap(), display_name: event.content.displayname.clone(), + display_name_ambiguous: false, avatar_url: event.content.avatar_url.clone(), presence: None, status_msg: None, @@ -87,12 +101,13 @@ impl RoomMember { typing: None, power_level: None, power_level_norm: None, - membership: event.content.membership, - presence_events: vec![], + presence_events: Vec::default(), + events: vec![event.clone()], } } - /// Returns the most ergonomic name available for the member. + /// Returns the most ergonomic (but potentially ambiguous/non-unique) name + /// available for the member. /// /// This is the member's display name if it is set, otherwise their MXID. pub fn name(&self) -> String { @@ -101,10 +116,11 @@ impl RoomMember { .unwrap_or_else(|| format!("{}", self.user_id)) } - /// Returns a name for the member which is guaranteed to be unique. + /// Returns a name for the member which is guaranteed to be unique, but not + /// necessarily the most ergonomic. /// - /// This is either of the format "DISPLAY_NAME (MXID)" if the display name is set for the - /// member, or simply "MXID" if not. + /// This is either a name in the format "DISPLAY_NAME (MXID)" if the + /// member's display name is set, or simply "MXID" if not. pub fn unique_name(&self) -> String { self.display_name .clone() @@ -112,100 +128,28 @@ impl RoomMember { .unwrap_or_else(|| format!("{}", self.user_id)) } - /// Handle profile updates. - pub(crate) fn update_profile(&mut self, event: &StateEventStub) -> bool { - use MembershipChange::*; - - match event.membership_change() { - // we assume that the profile has changed - ProfileChanged { .. } => { - self.display_name = event.content.displayname.clone(); - self.avatar_url = event.content.avatar_url.clone(); - true - } - - // We're only interested in profile changes here. - _ => false, - } - } - - pub fn update_power( - &mut self, - event: &StateEventStub, - max_power: Int, - ) -> bool { - let changed; - if let Some(user_power) = event.content.users.get(&self.user_id) { - changed = self.power_level != Some(*user_power); - self.power_level = Some(*user_power); + /// Get the disambiguated display name for the member which is as ergonomic + /// as possible while still guaranteeing it is unique. + /// + /// If the member's display name is currently ambiguous (i.e. shared by + /// other room members), this method will return the same result as + /// `RoomMember::unique_name`. Otherwise, this method will return the same + /// result as `RoomMember::name`. + /// + /// This is usually the name you want when showing room messages from the + /// member or when showing the member in the member list. + /// + /// **Warning**: When displaying a room member's display name, clients + /// *must* use a disambiguated name, so they *must not* use + /// `RoomMember::display_name` directly. Clients *should* use this method to + /// obtain the name, but an acceptable alternative is to use + /// `RoomMember::unique_name` in certain situations. + pub fn disambiguated_name(&self) -> String { + if self.display_name_ambiguous { + self.unique_name() } else { - changed = self.power_level != Some(event.content.users_default); - self.power_level = Some(event.content.users_default); + self.name() } - - if max_power > int!(0) { - self.power_level_norm = Some((self.power_level.unwrap() * int!(100)) / max_power); - } - - changed - } - - /// If the current `PresenceEvent` updated the state of this `User`. - /// - /// Returns true if the specific users presence has changed, false otherwise. - /// - /// # Arguments - /// - /// * `presence` - The presence event for a this room member. - pub fn did_update_presence(&self, presence: &PresenceEvent) -> bool { - let PresenceEvent { - content: - PresenceEventContent { - avatar_url, - currently_active, - displayname, - last_active_ago, - presence, - status_msg, - }, - .. - } = presence; - self.display_name == *displayname - && self.avatar_url == *avatar_url - && self.presence.as_ref() == Some(presence) - && self.status_msg == *status_msg - && self.last_active_ago == *last_active_ago - && self.currently_active == *currently_active - } - - /// Updates the `User`s presence. - /// - /// This should only be used if `did_update_presence` was true. - /// - /// # Arguments - /// - /// * `presence` - The presence event for a this room member. - pub fn update_presence(&mut self, presence_ev: &PresenceEvent) { - let PresenceEvent { - content: - PresenceEventContent { - avatar_url, - currently_active, - displayname, - last_active_ago, - presence, - status_msg, - }, - .. - } = presence_ev; - - self.presence_events.push(presence_ev.clone()); - self.avatar_url = avatar_url.clone(); - self.currently_active = *currently_active; - self.display_name = displayname.clone(); - self.last_active_ago = *last_active_ago; - self.presence = Some(*presence); - self.status_msg = status_msg.clone(); } } @@ -234,7 +178,9 @@ mod test { client } - fn get_room_id() -> RoomId { + // TODO: Move this to EventBuilder since it's a magic room ID used in EventBuilder's example + // events. + fn test_room_id() -> RoomId { RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap() } @@ -242,11 +188,11 @@ mod test { async fn room_member_events() { let client = get_client().await; - let room_id = get_room_id(); + let room_id = test_room_id(); let mut response = EventBuilder::default() - .add_state_event(EventsJson::Member) - .add_state_event(EventsJson::PowerLevels) + .add_room_event(EventsJson::Member) + .add_room_event(EventsJson::PowerLevels) .build_sync_response(); client.receive_sync_response(&mut response).await.unwrap(); @@ -261,15 +207,65 @@ mod test { assert_eq!(member.power_level, Some(int!(100))); } + #[async_test] + async fn room_member_display_name_change() { + let client = get_client().await; + let room_id = test_room_id(); + + let mut builder = EventBuilder::default(); + let mut initial_response = builder + .add_room_event(EventsJson::Member) + .build_sync_response(); + let mut name_change_response = builder + .add_room_event(EventsJson::MemberNameChange) + .build_sync_response(); + + client + .receive_sync_response(&mut initial_response) + .await + .unwrap(); + + let room = client.get_joined_room(&room_id).await.unwrap(); + + // Initially, the display name is "example". + { + let room = room.read().await; + + let member = room + .joined_members + .get(&UserId::try_from("@example:localhost").unwrap()) + .unwrap(); + + assert_eq!(member.display_name.as_ref().unwrap(), "example"); + } + + client + .receive_sync_response(&mut name_change_response) + .await + .unwrap(); + + // Afterwards, the display name is "changed". + { + let room = room.read().await; + + let member = room + .joined_members + .get(&UserId::try_from("@example:localhost").unwrap()) + .unwrap(); + + assert_eq!(member.display_name.as_ref().unwrap(), "changed"); + } + } + #[async_test] async fn member_presence_events() { let client = get_client().await; - let room_id = get_room_id(); + let room_id = test_room_id(); let mut response = EventBuilder::default() - .add_state_event(EventsJson::Member) - .add_state_event(EventsJson::PowerLevels) + .add_room_event(EventsJson::Member) + .add_room_event(EventsJson::PowerLevels) .add_presence_event(EventsJson::Presence) .build_sync_response(); diff --git a/matrix_sdk_base/src/state/json_store.rs b/matrix_sdk_base/src/state/json_store.rs index f5074231..c33d2f02 100644 --- a/matrix_sdk_base/src/state/json_store.rs +++ b/matrix_sdk_base/src/state/json_store.rs @@ -39,6 +39,36 @@ impl JsonStore { user_path_set: AtomicBool::new(false), }) } + + /// Build a path for a file where the Room state to be stored in. + async fn build_room_path(&self, room_state: &str, room_id: &RoomId) -> PathBuf { + let mut path = self.path.read().await.clone(); + + path.push("rooms"); + path.push(room_state); + path.push(JsonStore::sanitize_room_id(room_id)); + path.set_extension("json"); + + path + } + + /// Build a path for the file where the Client state to be stored in. + async fn build_client_path(&self) -> PathBuf { + let mut path = self.path.read().await.clone(); + path.push("client"); + path.set_extension("json"); + + path + } + + /// Replace common characters that can't be used in a file name with an + /// underscore. + fn sanitize_room_id(room_id: &RoomId) -> String { + room_id.as_str().replace( + &['.', ':', '<', '>', '"', '/', '\\', '|', '?', '*'][..], + "_", + ) + } } impl fmt::Debug for JsonStore { @@ -57,8 +87,7 @@ impl StateStore for JsonStore { self.path.write().await.push(sess.user_id.localpart()) } - let mut path = self.path.read().await.clone(); - path.push("client.json"); + let path = self.build_client_path().await; let json = async_fs::read_to_string(path) .await @@ -114,8 +143,7 @@ impl StateStore for JsonStore { } async fn store_client_state(&self, state: ClientState) -> Result<()> { - let mut path = self.path.read().await.clone(); - path.push("client.json"); + let path = self.build_client_path().await; if !path.exists() { let mut dir = path.clone(); @@ -146,9 +174,7 @@ impl StateStore for JsonStore { self.path.write().await.push(room.own_user_id.localpart()) } - let mut path = self.path.read().await.clone(); - path.push("rooms"); - path.push(&format!("{}/{}.json", room_state, room.room_id)); + let path = self.build_room_path(room_state, &room.room_id).await; if !path.exists() { let mut dir = path.clone(); @@ -178,15 +204,13 @@ impl StateStore for JsonStore { return Err(Error::StateStore("path for JsonStore not set".into())); } - let mut to_del = self.path.read().await.clone(); - to_del.push("rooms"); - to_del.push(&format!("{}/{}.json", room_state, room_id)); + let path = self.build_room_path(room_state, room_id).await; - if !to_del.exists() { - return Err(Error::StateStore(format!("file {:?} not found", to_del))); + if !path.exists() { + return Err(Error::StateStore(format!("file {:?} not found", path))); } - tokio::fs::remove_file(to_del).await.map_err(Error::from) + tokio::fs::remove_file(path).await.map_err(Error::from) } } diff --git a/matrix_sdk_base/src/state/mod.rs b/matrix_sdk_base/src/state/mod.rs index c26f13bb..3b5d0203 100644 --- a/matrix_sdk_base/src/state/mod.rs +++ b/matrix_sdk_base/src/state/mod.rs @@ -159,7 +159,6 @@ mod test { "creator": null, "joined_members": {}, "invited_members": {}, - "disambiguated_display_names": {}, "typing_users": [], "power_levels": null, "encrypted": null, @@ -176,7 +175,6 @@ mod test { serde_json::json!({ "!roomid:example.com": { "room_id": "!roomid:example.com", - "disambiguated_display_names": {}, "room_name": { "name": null, "canonical_alias": null, diff --git a/matrix_sdk_common/Cargo.toml b/matrix_sdk_common/Cargo.toml index 2ab514b0..2dd86448 100644 --- a/matrix_sdk_common/Cargo.toml +++ b/matrix_sdk_common/Cargo.toml @@ -11,13 +11,13 @@ repository = "https://github.com/matrix-org/matrix-rust-sdk" version = "0.1.0" [dependencies] -instant = { version = "0.1.4", features = ["wasm-bindgen", "now"] } +instant = { version = "0.1.6", features = ["wasm-bindgen", "now"] } js_int = "0.1.8" [dependencies.ruma] -path = "/home/poljar/werk/priv/ruma/ruma" +git = "https://github.com/ruma/ruma" features = ["client-api"] -rev = "c19bcaab" +rev = "848b22568106d05c5444f3fe46070d5aa16e422b" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] uuid = { version = "0.8.1", features = ["v4"] } diff --git a/matrix_sdk_common_macros/Cargo.toml b/matrix_sdk_common_macros/Cargo.toml index 2b0c6564..959d5805 100644 --- a/matrix_sdk_common_macros/Cargo.toml +++ b/matrix_sdk_common_macros/Cargo.toml @@ -14,5 +14,5 @@ version = "0.1.0" proc-macro = true [dependencies] -syn = "1.0.33" +syn = "1.0.34" quote = "1.0.7" diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index a5e9833a..ea1fc330 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -29,7 +29,7 @@ url = "2.1.1" # Misc dependencies thiserror = "1.0.20" -tracing = "0.1.15" +tracing = "0.1.16" atomic = "0.4.6" dashmap = "3.11.7" diff --git a/matrix_sdk_crypto/src/device.rs b/matrix_sdk_crypto/src/device.rs index e7a5ec14..93996b75 100644 --- a/matrix_sdk_crypto/src/device.rs +++ b/matrix_sdk_crypto/src/device.rs @@ -33,9 +33,10 @@ use crate::verify_json; #[derive(Debug, Clone)] pub struct Device { user_id: Arc, - device_id: Arc, + device_id: Arc>, algorithms: Arc>, keys: Arc>, + signatures: Arc>>, display_name: Arc>, deleted: Arc, trust_state: Arc>, @@ -70,17 +71,19 @@ impl Device { /// Create a new Device. pub fn new( user_id: UserId, - device_id: DeviceId, + device_id: Box, display_name: Option, trust_state: TrustState, algorithms: Vec, keys: BTreeMap, + signatures: BTreeMap>, ) -> Self { Device { user_id: Arc::new(user_id), device_id: Arc::new(device_id), display_name: Arc::new(display_name), trust_state: Arc::new(Atomic::new(trust_state)), + signatures: Arc::new(signatures), algorithms: Arc::new(algorithms), keys: Arc::new(keys), deleted: Arc::new(AtomicBool::new(false)), @@ -104,8 +107,10 @@ impl Device { /// Get the key of the given key algorithm belonging to this device. pub fn get_key(&self, algorithm: KeyAlgorithm) -> Option<&String> { - self.keys - .get(&AlgorithmAndDeviceId(algorithm, self.device_id.to_string())) + self.keys.get(&AlgorithmAndDeviceId( + algorithm, + self.device_id.as_ref().clone(), + )) } /// Get a map containing all the device keys. @@ -113,6 +118,11 @@ impl Device { &self.keys } + /// Get a map containing all the device signatures. + pub fn signatures(&self) -> &BTreeMap> { + &self.signatures + } + /// Get the trust state of the device. pub fn trust_state(&self) -> TrustState { self.trust_state.load(Ordering::Relaxed) @@ -142,6 +152,7 @@ impl Device { self.algorithms = Arc::new(device_keys.algorithms.clone()); self.keys = Arc::new(device_keys.keys.clone()); + self.signatures = Arc::new(device_keys.signatures.clone()); self.display_name = display_name; Ok(()) @@ -173,37 +184,11 @@ impl Device { pub(crate) fn mark_as_deleted(&self) { self.deleted.store(true, Ordering::Relaxed); } -} -#[cfg(test)] -impl From<&OlmMachine> for Device { - fn from(machine: &OlmMachine) -> Self { - Device { - user_id: Arc::new(machine.user_id().clone()), - device_id: Arc::new(machine.device_id().clone()), - algorithms: Arc::new(vec![ - Algorithm::MegolmV1AesSha2, - Algorithm::OlmV1Curve25519AesSha2, - ]), - keys: Arc::new( - machine - .identity_keys() - .iter() - .map(|(key, value)| { - ( - AlgorithmAndDeviceId( - KeyAlgorithm::try_from(key.as_ref()).unwrap(), - machine.device_id().clone(), - ), - value.to_owned(), - ) - }) - .collect(), - ), - display_name: Arc::new(None), - deleted: Arc::new(AtomicBool::new(false)), - trust_state: Arc::new(Atomic::new(TrustState::Unset)), - } + #[cfg(test)] + pub async fn from_machine(machine: &OlmMachine) -> Device { + let device_keys = machine.account.device_keys().await; + Device::try_from(&device_keys).unwrap() } } @@ -215,6 +200,7 @@ impl TryFrom<&DeviceKeys> for Device { user_id: Arc::new(device_keys.user_id.clone()), device_id: Arc::new(device_keys.device_id.clone()), algorithms: Arc::new(device_keys.algorithms.clone()), + signatures: Arc::new(device_keys.signatures.clone()), keys: Arc::new(device_keys.keys.clone()), display_name: Arc::new( device_keys diff --git a/matrix_sdk_crypto/src/error.rs b/matrix_sdk_crypto/src/error.rs index 09895a6d..0ac18fd2 100644 --- a/matrix_sdk_crypto/src/error.rs +++ b/matrix_sdk_crypto/src/error.rs @@ -13,6 +13,7 @@ // limitations under the License. use cjson::Error as CjsonError; +use matrix_sdk_common::identifiers::{DeviceId, UserId}; use olm_rs::errors::{OlmGroupSessionError, OlmSessionError}; use serde_json::Error as SerdeError; use thiserror::Error; @@ -49,6 +50,14 @@ pub enum OlmError { /// The session with a device has become corrupted. #[error("decryption failed likely because a Olm session was wedged")] SessionWedged, + + /// Encryption failed because the device does not have a valid Olm session + /// with us. + #[error( + "encryption failed because the device does not \ + have a valid Olm session with us" + )] + MissingSession, } /// Error representing a failure during a group encryption operation. @@ -93,6 +102,9 @@ pub enum EventError { #[error("the Encrypted message is missing the signing key of the sender")] MissingSigningKey, + #[error("the Encrypted message is missing the sender key")] + MissingSenderKey, + #[error("the Encrypted message is missing the field {0}")] MissingField(String), @@ -121,6 +133,29 @@ pub enum SignatureError { VerificationError, } +#[derive(Error, Debug)] +pub(crate) enum SessionCreationError { + #[error( + "Failed to create a new Olm session for {0} {1}, the requested \ + one-time key isn't a signed curve key" + )] + OneTimeKeyNotSigned(UserId, Box), + #[error( + "Tried to create a new Olm session for {0} {1}, but the signed \ + one-time key is missing" + )] + OneTimeKeyMissing(UserId, Box), + #[error("Failed to verify the one-time key signatures for {0} {1}: {2:?}")] + InvalidSignature(UserId, Box, SignatureError), + #[error( + "Tried to create an Olm session for {0} {1}, but the device is missing \ + a curve25519 key" + )] + DeviceMissingCurveKey(UserId, Box), + #[error("Error creating new Olm session for {0} {1}: {2:?}")] + OlmError(UserId, Box, OlmSessionError), +} + impl From for SignatureError { fn from(error: CjsonError) -> Self { Self::CanonicalJsonError(error) diff --git a/matrix_sdk_crypto/src/lib.rs b/matrix_sdk_crypto/src/lib.rs index e7c26a06..2091f880 100644 --- a/matrix_sdk_crypto/src/lib.rs +++ b/matrix_sdk_crypto/src/lib.rs @@ -38,7 +38,7 @@ pub use device::{Device, TrustState}; pub use error::{MegolmError, OlmError}; pub use machine::{OlmMachine, OneTimeKeys}; pub use memory_stores::{DeviceStore, GroupSessionStore, SessionStore, UserDevices}; -pub use olm::{Account, InboundGroupSession, OutboundGroupSession, Session}; +pub use olm::{Account, IdentityKeys, InboundGroupSession, OutboundGroupSession, Session}; #[cfg(feature = "sqlite-cryptostore")] pub use store::sqlite::SqliteStore; pub use store::{CryptoStore, CryptoStoreError}; @@ -83,7 +83,7 @@ pub(crate) fn verify_json( json_object.insert("unsigned".to_string(), u); } - let key_id = AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, key_id.to_string()); + let key_id = AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, key_id.into()); let signatures = signatures.ok_or(SignatureError::NoSignatureFound)?; let signature_object = signatures diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index 58d30c39..c69ca24e 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -23,7 +23,6 @@ use std::result::Result as StdResult; use super::error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult}; use super::olm::{ Account, GroupSessionKey, IdentityKeys, InboundGroupSession, OlmMessage, OutboundGroupSession, - Session, }; use super::store::memorystore::MemoryStore; #[cfg(feature = "sqlite-cryptostore")] @@ -32,13 +31,10 @@ use super::{device::Device, store::Result as StoreResult, CryptoStore}; use matrix_sdk_common::api; use matrix_sdk_common::events::{ - forwarded_room_key::ForwardedRoomKeyEventContent, - room::encrypted::{CiphertextInfo, EncryptedEventContent, OlmV1Curve25519AesSha2Content}, - room::message::MessageEventContent, - room_key::RoomKeyEventContent, - room_key_request::RoomKeyRequestEventContent, - Algorithm, AnyRoomEventStub, AnyToDeviceEvent, EventJson, EventType, MessageEventStub, - ToDeviceEvent, + forwarded_room_key::ForwardedRoomKeyEventContent, room::encrypted::EncryptedEventContent, + room::message::MessageEventContent, room_key::RoomKeyEventContent, + room_key_request::RoomKeyRequestEventContent, Algorithm, AnySyncRoomEvent, AnyToDeviceEvent, + EventJson, EventType, SyncMessageEvent, ToDeviceEvent, }; use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId}; use matrix_sdk_common::uuid::Uuid; @@ -50,7 +46,7 @@ use api::r0::{ to_device::{send_event_to_device::Request as ToDeviceRequest, DeviceIdOrAllDevices}, }; -use serde_json::{json, Value}; +use serde_json::Value; use tracing::{debug, error, info, instrument, trace, warn}; /// A map from the algorithm and device id to a one-time key. @@ -64,9 +60,9 @@ pub struct OlmMachine { /// The unique user id that owns this account. user_id: UserId, /// The unique device id of the device that holds this account. - device_id: DeviceId, + device_id: Box, /// Our underlying Olm Account holding our identity keys. - account: Account, + pub(crate) account: Account, /// Store for the encryption keys. /// Persists all the encryption keys so a client can resume the session /// without the need to create new keys. @@ -98,10 +94,11 @@ impl OlmMachine { /// * `user_id` - The unique id of the user that owns this machine. /// /// * `device_id` - The unique id of the device that owns this machine. + #[allow(clippy::ptr_arg)] pub fn new(user_id: &UserId, device_id: &DeviceId) -> Self { OlmMachine { user_id: user_id.clone(), - device_id: device_id.to_owned(), + device_id: device_id.into(), account: Account::new(user_id, &device_id), store: Box::new(MemoryStore::new()), outbound_group_sessions: HashMap::new(), @@ -127,7 +124,7 @@ impl OlmMachine { /// the encryption keys. pub async fn new_with_store( user_id: UserId, - device_id: String, + device_id: Box, mut store: Box, ) -> StoreResult { let account = match store.load_account().await? { @@ -163,14 +160,14 @@ impl OlmMachine { /// * `device_id` - The unique id of the device that owns this machine. pub async fn new_with_default_store>( user_id: &UserId, - device_id: &str, + device_id: &DeviceId, path: P, passphrase: &str, ) -> StoreResult { let store = SqliteStore::open_with_passphrase(&user_id, device_id, path, passphrase).await?; - OlmMachine::new_with_store(user_id.to_owned(), device_id.to_owned(), Box::new(store)).await + OlmMachine::new_with_store(user_id.to_owned(), device_id.into(), Box::new(store)).await } /// The unique user id that owns this identity. @@ -254,7 +251,7 @@ impl OlmMachine { pub async fn get_missing_sessions( &mut self, users: impl Iterator, - ) -> OlmResult>> { + ) -> OlmResult, KeyAlgorithm>>> { let mut missing = BTreeMap::new(); for user_id in users { @@ -281,10 +278,8 @@ impl OlmMachine { } let user_map = missing.get_mut(user_id).unwrap(); - let _ = user_map.insert( - device.device_id().to_owned(), - KeyAlgorithm::SignedCurve25519, - ); + let _ = + user_map.insert(device.device_id().into(), KeyAlgorithm::SignedCurve25519); } } } @@ -306,76 +301,35 @@ impl OlmMachine { for (user_id, user_devices) in &response.one_time_keys { for (device_id, key_map) in user_devices { - let device = if let Some(d) = self - .store - .get_device(&user_id, device_id) - .await - .expect("Can't get devices") - { - d - } else { - warn!( - "Tried to create an Olm session for {} {}, but the device is unknown", - user_id, device_id - ); - continue; - }; - - // TODO move this logic into the account, pass the device to the - // account when creating an outbound session. - let one_time_key = if let Some(k) = key_map.values().next() { - match k { - OneTimeKey::SignedKey(k) => k, - OneTimeKey::Key(_) => { + let device: Device = match self.store.get_device(&user_id, device_id).await { + Ok(d) => { + if let Some(d) = d { + d + } else { warn!( - "Tried to create an Olm session for {} {}, but - the requested key isn't a signed curve key", + "Tried to create an Olm session for {} {}, but \ + the device is unknown", user_id, device_id ); continue; } } - } else { - warn!( - "Tried to create an Olm session for {} {}, but the - signed one-time key is missing", - user_id, device_id - ); - continue; - }; - - if let Err(e) = device.verify_one_time_key(&one_time_key) { - warn!( - "Failed to verify the one-time key signatures for {} {}: {:?}", - user_id, device_id, e - ); - continue; - } - - let curve_key = if let Some(k) = device.get_key(KeyAlgorithm::Curve25519) { - k - } else { - warn!( - "Tried to create an Olm session for {} {}, but the - device is missing the curve key", - user_id, device_id - ); - continue; + Err(e) => { + warn!( + "Tried to create an Olm session for {} {}, but \ + can't fetch the device from the store {:?}", + user_id, device_id, e + ); + continue; + } }; info!("Creating outbound Session for {} {}", user_id, device_id); - let session = match self - .account - .create_outbound_session(curve_key, &one_time_key) - .await - { + let session = match self.account.create_outbound_session(device, &key_map).await { Ok(s) => s, Err(e) => { - warn!( - "Error creating new Olm session for {} {}: {}", - user_id, device_id, e - ); + warn!("{:?}", e); continue; } }; @@ -396,7 +350,7 @@ impl OlmMachine { async fn handle_devices_from_key_query( &mut self, - device_keys_map: &BTreeMap>, + device_keys_map: &BTreeMap, DeviceKeys>>, ) -> StoreResult> { let mut changed_devices = Vec::new(); @@ -446,7 +400,8 @@ impl OlmMachine { changed_devices.push(device); } - let current_devices: HashSet<&DeviceId> = device_map.keys().collect(); + let current_devices: HashSet<&DeviceId> = + device_map.keys().map(|id| id.as_ref()).collect(); let stored_devices = self.store.get_user_devices(&user_id).await.unwrap(); let stored_devices_set: HashSet<&DeviceId> = stored_devices.keys().collect(); @@ -840,66 +795,51 @@ impl OlmMachine { Ok(session.encrypt(content).await) } - /// Encrypt some JSON content using the given Olm session. + /// Encrypt the given event for the given Device + /// + /// # Arguments + /// + /// * `reciepient_device` - The device that the event should be encrypted + /// for. + /// + /// * `event_type` - The type of the event. + /// + /// * `content` - The content of the event that should be encrypted. async fn olm_encrypt( &mut self, - mut session: Session, recipient_device: &Device, event_type: EventType, content: Value, ) -> OlmResult { - let identity_keys = self.account.identity_keys(); - - // TODO most of this could go into the session, the session already - // stores the curve key of the device, if we also store the ed25519 key - // with the session we'll only need to pass in the account to the - // session and all of this can live in the session. - - let recipient_signing_key = recipient_device - .get_key(KeyAlgorithm::Ed25519) - .ok_or(EventError::MissingSigningKey)?; - let recipient_sender_key = recipient_device - .get_key(KeyAlgorithm::Curve25519) - .ok_or(EventError::MissingSigningKey)?; - - let payload = json!({ - "sender": self.user_id, - "sender_device": self.device_id, - "keys": { - "ed25519": identity_keys.ed25519(), - }, - "recipient": recipient_device.user_id(), - "recipient_keys": { - "ed25519": recipient_signing_key, - }, - "type": event_type, - "content": content, - }); - - let plaintext = cjson::to_string(&payload) - .unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", payload))); - - let ciphertext = session.encrypt(&plaintext).await.to_tuple(); - - let message_type: usize = ciphertext.0.into(); - - let ciphertext = CiphertextInfo { - body: ciphertext.1, - message_type: (message_type as u32).into(), + let sender_key = if let Some(k) = recipient_device.get_key(KeyAlgorithm::Curve25519) { + k + } else { + warn!( + "Trying to encrypt a Megolm session for user {} on device {}, \ + but the device doesn't have a curve25519 key", + recipient_device.user_id(), + recipient_device.device_id() + ); + return Err(EventError::MissingSenderKey.into()); }; - let mut content = BTreeMap::new(); - - content.insert(recipient_sender_key.to_owned(), ciphertext); + let mut session = if let Some(s) = self.store.get_sessions(sender_key).await? { + let session = &s.lock().await[0]; + session.clone() + } else { + warn!( + "Trying to encrypt a Megolm session for user {} on device {}, \ + but no Olm session is found", + recipient_device.user_id(), + recipient_device.device_id() + ); + return Err(OlmError::MissingSession); + }; + let message = session.encrypt(recipient_device, event_type, content).await; self.store.save_sessions(&[session]).await?; - Ok(EncryptedEventContent::OlmV1Curve25519AesSha2( - OlmV1Curve25519AesSha2Content { - sender_key: identity_keys.curve25519().to_owned(), - ciphertext: content, - }, - )) + message } /// Should the client share a group session for the given room. @@ -946,102 +886,67 @@ impl OlmMachine { I: IntoIterator, { self.create_outbound_group_session(room_id).await?; - let megolm_session = self.outbound_group_sessions.get(room_id).unwrap(); + let session = self.outbound_group_sessions.get(room_id).unwrap(); - if megolm_session.shared() { + if session.shared() { panic!("Session is already shared"); } - let session_id = megolm_session.session_id().to_owned(); - // TODO don't mark the session as shared automatically only, when all // the requests are done, failure to send these requests will likely end // up in wedged sessions. We'll need to store the requests and let the // caller mark them as sent using an UUID. - megolm_session.mark_as_shared(); + session.mark_as_shared(); - // TODO the key content creation can go into the OutboundGroupSession - // struct. - - let key_content = json!({ - "algorithm": Algorithm::MegolmV1AesSha2, - "room_id": room_id, - "session_id": session_id.clone(), - "session_key": megolm_session.session_key().await, - "chain_index": megolm_session.message_index().await, - }); - - let mut user_map = Vec::new(); + let mut devices = Vec::new(); for user_id in users { for device in self.store.get_user_devices(user_id).await?.devices() { - let sender_key = if let Some(k) = device.get_key(KeyAlgorithm::Curve25519) { - k - } else { - warn!( - "The device {} of user {} doesn't have a curve 25519 key", - user_id, - device.device_id() - ); - // TODO mark the user for a key query. - continue; - }; - // TODO abort if the device isn't verified - let sessions = self.store.get_sessions(sender_key).await?; - - if let Some(s) = sessions { - let session = &s.lock().await[0]; - // TODO once the session has the all the device info, we - // won't need the device anymore to encrypt stuff with the - // session. - user_map.push((session.clone(), device.clone())); - } else { - warn!( - "Trying to encrypt a Megolm session for user - {} on device {}, but no Olm session is found", - user_id, - device.device_id() - ); - } + devices.push(device.clone()); } } - let mut message_vec = Vec::new(); + let mut requests = Vec::new(); + let key_content = session.as_json().await; - for user_map_chunk in user_map.chunks(OlmMachine::MAX_TO_DEVICE_MESSAGES) { + for device_map_chunk in devices.chunks(OlmMachine::MAX_TO_DEVICE_MESSAGES) { let mut messages = BTreeMap::new(); - for (session, device) in user_map_chunk { + for device in device_map_chunk { + let encrypted = self + .olm_encrypt(&device, EventType::RoomKey, key_content.clone()) + .await; + + let encrypted = match encrypted { + Ok(c) => c, + Err(OlmError::MissingSession) + | Err(OlmError::EventError(EventError::MissingSenderKey)) => { + continue; + } + Err(e) => return Err(e), + }; + if !messages.contains_key(device.user_id()) { messages.insert(device.user_id().clone(), BTreeMap::new()); }; let user_messages = messages.get_mut(device.user_id()).unwrap(); - let encrypted_content = self - .olm_encrypt( - session.clone(), - &device, - EventType::RoomKey, - key_content.clone(), - ) - .await?; - user_messages.insert( - DeviceIdOrAllDevices::DeviceId(device.device_id().clone()), - serde_json::value::to_raw_value(&encrypted_content)?, + DeviceIdOrAllDevices::DeviceId(device.device_id().into()), + serde_json::value::to_raw_value(&encrypted)?, ); } - message_vec.push(ToDeviceRequest { + requests.push(ToDeviceRequest { event_type: EventType::RoomEncrypted, txn_id: Uuid::new_v4().to_string(), messages, }); } - Ok(message_vec) + Ok(requests) } fn add_forwarded_room_key( @@ -1150,8 +1055,6 @@ impl OlmMachine { } }; - // TODO make sure private keys are cleared from the event - // before we replace the result. *event_result = decrypted_event; } AnyToDeviceEvent::RoomKeyRequest(e) => self.handle_room_key_request(e), @@ -1177,9 +1080,9 @@ impl OlmMachine { /// * `room_id` - The ID of the room where the event was sent to. pub async fn decrypt_room_event( &mut self, - event: &MessageEventStub, + event: &SyncMessageEvent, room_id: &RoomId, - ) -> MegolmResult> { + ) -> MegolmResult> { let content = match &event.content { EncryptedEventContent::MegolmV1AesSha2(c) => c, _ => return Err(EventError::UnsupportedAlgorithm.into()), @@ -1286,8 +1189,8 @@ mod test { encrypted::EncryptedEventContent, message::{MessageEventContent, TextMessageEventContent}, }, - AnyMessageEventStub, AnyRoomEventStub, AnyToDeviceEvent, EventJson, EventType, - MessageEventStub, ToDeviceEvent, UnsignedData, + AnySyncMessageEvent, AnySyncRoomEvent, AnyToDeviceEvent, EventJson, EventType, + SyncMessageEvent, ToDeviceEvent, Unsigned, }; use matrix_sdk_common::identifiers::{DeviceId, EventId, RoomId, UserId}; use matrix_sdk_test::test_json; @@ -1296,8 +1199,8 @@ mod test { UserId::try_from("@alice:example.org").unwrap() } - fn alice_device_id() -> DeviceId { - "JLAFKJWSCS".to_string() + fn alice_device_id() -> Box { + "JLAFKJWSCS".into() } fn user_id() -> UserId { @@ -1375,8 +1278,8 @@ mod test { let alice_device = alice_device_id(); let alice = OlmMachine::new(&alice_id, &alice_device); - let alice_deivce = Device::from(&alice); - let bob_device = Device::from(&bob); + let alice_deivce = Device::from_machine(&alice).await; + let bob_device = Device::from_machine(&bob).await; alice.store.save_devices(&[bob_device]).await.unwrap(); bob.store.save_devices(&[alice_deivce]).await.unwrap(); @@ -1409,16 +1312,6 @@ mod test { async fn get_machine_pair_with_setup_sessions() -> (OlmMachine, OlmMachine) { let (mut alice, mut bob) = get_machine_pair_with_session().await; - let session = alice - .store - .get_sessions(bob.account.identity_keys().curve25519()) - .await - .unwrap() - .unwrap() - .lock() - .await[0] - .clone(); - let bob_device = alice .store .get_device(&bob.user_id, &bob.device_id) @@ -1429,7 +1322,7 @@ mod test { let event = ToDeviceEvent { sender: alice.user_id.clone(), content: alice - .olm_encrypt(session, &bob_device, EventType::Dummy, json!({})) + .olm_encrypt(&bob_device, EventType::Dummy, json!({})) .await .unwrap(), }; @@ -1698,16 +1591,6 @@ mod test { async fn test_olm_encryption() { let (mut alice, mut bob) = get_machine_pair_with_session().await; - let session = alice - .store - .get_sessions(bob.account.identity_keys().curve25519()) - .await - .unwrap() - .unwrap() - .lock() - .await[0] - .clone(); - let bob_device = alice .store .get_device(&bob.user_id, &bob.device_id) @@ -1718,7 +1601,7 @@ mod test { let event = ToDeviceEvent { sender: alice.user_id.clone(), content: alice - .olm_encrypt(session, &bob_device, EventType::Dummy, json!({})) + .olm_encrypt(&bob_device, EventType::Dummy, json!({})) .await .unwrap(), }; @@ -1804,12 +1687,12 @@ mod test { let encrypted_content = alice.encrypt(&room_id, content.clone()).await.unwrap(); - let event = MessageEventStub { + let event = SyncMessageEvent { event_id: EventId::try_from("$xxxxx:example.org").unwrap(), origin_server_ts: SystemTime::now(), sender: alice.user_id().clone(), content: encrypted_content, - unsigned: UnsignedData::default(), + unsigned: Unsigned::default(), }; let decrypted_event = bob @@ -1820,7 +1703,7 @@ mod test { .unwrap(); match decrypted_event { - AnyRoomEventStub::Message(AnyMessageEventStub::RoomMessage(MessageEventStub { + AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(SyncMessageEvent { sender, content, .. diff --git a/matrix_sdk_crypto/src/memory_stores.rs b/matrix_sdk_crypto/src/memory_stores.rs index be7aba34..72d91395 100644 --- a/matrix_sdk_crypto/src/memory_stores.rs +++ b/matrix_sdk_crypto/src/memory_stores.rs @@ -129,24 +129,24 @@ impl GroupSessionStore { /// In-memory store holding the devices of users. #[derive(Clone, Debug, Default)] pub struct DeviceStore { - entries: Arc>>, + entries: Arc, Device>>>, } /// A read only view over all devices belonging to a user. #[derive(Debug)] pub struct UserDevices { - entries: ReadOnlyView, + entries: ReadOnlyView, Device>, } impl UserDevices { /// Get the specific device with the given device id. - pub fn get(&self, device_id: &str) -> Option { + pub fn get(&self, device_id: &DeviceId) -> Option { self.entries.get(device_id).cloned() } /// Iterator over all the device ids of the user devices. pub fn keys(&self) -> impl Iterator { - self.entries.keys() + self.entries.keys().map(|id| id.as_ref()) } /// Iterator over all the devices of the user devices. @@ -175,12 +175,12 @@ impl DeviceStore { let device_map = self.entries.get_mut(&user_id).unwrap(); device_map - .insert(device.device_id().to_owned(), device) + .insert(device.device_id().into(), device) .is_none() } /// Get the device with the given device_id and belonging to the given user. - pub fn get(&self, user_id: &UserId, device_id: &str) -> Option { + pub fn get(&self, user_id: &UserId, device_id: &DeviceId) -> Option { self.entries .get(user_id) .and_then(|m| m.get(device_id).map(|d| d.value().clone())) @@ -189,7 +189,7 @@ impl DeviceStore { /// Remove the device with the given device_id and belonging to the given user. /// /// Returns the device if it was removed, None if it wasn't in the store. - pub fn remove(&self, user_id: &UserId, device_id: &str) -> Option { + pub fn remove(&self, user_id: &UserId, device_id: &DeviceId) -> Option { self.entries .get(user_id) .and_then(|m| m.remove(device_id)) @@ -292,8 +292,8 @@ mod test { let user_devices = store.user_devices(device.user_id()); - assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id()); - assert_eq!(user_devices.devices().nth(0).unwrap(), &device); + assert_eq!(user_devices.keys().next().unwrap(), device.device_id()); + assert_eq!(user_devices.devices().next().unwrap(), &device); let loaded_device = user_devices.get(device.device_id()).unwrap(); diff --git a/matrix_sdk_crypto/src/olm.rs b/matrix_sdk_crypto/src/olm.rs deleted file mode 100644 index e1e33a20..00000000 --- a/matrix_sdk_crypto/src/olm.rs +++ /dev/null @@ -1,1157 +0,0 @@ -// Copyright 2020 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use matrix_sdk_common::instant::Instant; -use std::convert::TryFrom; -use std::convert::TryInto; -use std::fmt; -use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering}; -use std::sync::Arc; - -use matrix_sdk_common::locks::Mutex; -use serde::Serialize; -use serde_json::{json, Value}; -use std::collections::BTreeMap; -use zeroize::Zeroize; - -pub use olm_rs::account::IdentityKeys; -use olm_rs::account::{OlmAccount, OneTimeKeys}; -use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError}; -use olm_rs::inbound_group_session::OlmInboundGroupSession; -use olm_rs::outbound_group_session::OlmOutboundGroupSession; -use olm_rs::session::OlmSession; -use olm_rs::PicklingMode; - -use crate::error::{EventError, MegolmResult}; -pub use olm_rs::{ - session::{OlmMessage, PreKeyMessage}, - utility::OlmUtility, -}; - -use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId}; -use matrix_sdk_common::{ - api::r0::keys::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey, SignedKey}, - events::{ - room::{ - encrypted::{EncryptedEventContent, MegolmV1AesSha2Content}, - message::MessageEventContent, - }, - Algorithm, AnyRoomEventStub, EventJson, EventType, MessageEventStub, - }, -}; - -/// Account holding identity keys for which sessions can be created. -/// -/// An account is the central identity for encrypted communication between two -/// devices. -#[derive(Clone)] -pub struct Account { - user_id: Arc, - device_id: Arc, - inner: Arc>, - identity_keys: Arc, - shared: Arc, - /// The number of signed one-time keys we have uploaded to the server. If - /// this is None, no action will be taken. After a sync request the client - /// needs to set this for us, depending on the count we will suggest the - /// client to upload new keys. - uploaded_signed_key_count: Arc, -} - -// #[cfg_attr(tarpaulin, skip)] -impl fmt::Debug for Account { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Account") - .field("identity_keys", self.identity_keys()) - .field("shared", &self.shared()) - .finish() - } -} - -impl Account { - const ALGORITHMS: &'static [&'static Algorithm] = &[ - &Algorithm::OlmV1Curve25519AesSha2, - &Algorithm::MegolmV1AesSha2, - ]; - - /// Create a fresh new account, this will generate the identity key-pair. - pub fn new(user_id: &UserId, device_id: &DeviceId) -> Self { - let account = OlmAccount::new(); - let identity_keys = account.parsed_identity_keys(); - - Account { - user_id: Arc::new(user_id.to_owned()), - device_id: Arc::new(device_id.to_owned()), - inner: Arc::new(Mutex::new(account)), - identity_keys: Arc::new(identity_keys), - shared: Arc::new(AtomicBool::new(false)), - uploaded_signed_key_count: Arc::new(AtomicI64::new(0)), - } - } - - /// Get the public parts of the identity keys for the account. - pub fn identity_keys(&self) -> &IdentityKeys { - &self.identity_keys - } - - /// Update the uploaded key count. - /// - /// # Arguments - /// - /// * `new_count` - The new count that was reported by the server. - pub(crate) fn update_uploaded_key_count(&self, new_count: u64) { - let key_count = i64::try_from(new_count).unwrap_or(i64::MAX); - self.uploaded_signed_key_count - .store(key_count, Ordering::Relaxed); - } - - /// Get the currently known uploaded key count. - pub fn uploaded_key_count(&self) -> i64 { - self.uploaded_signed_key_count.load(Ordering::Relaxed) - } - - /// Has the account been shared with the server. - pub fn shared(&self) -> bool { - self.shared.load(Ordering::Relaxed) - } - - /// Mark the account as shared. - /// - /// Messages shouldn't be encrypted with the session before it has been - /// shared. - pub(crate) fn mark_as_shared(&self) { - self.shared.store(true, Ordering::Relaxed); - } - - /// Get the one-time keys of the account. - /// - /// This can be empty, keys need to be generated first. - pub(crate) async fn one_time_keys(&self) -> OneTimeKeys { - self.inner.lock().await.parsed_one_time_keys() - } - - /// Generate count number of one-time keys. - pub(crate) async fn generate_one_time_keys_helper(&self, count: usize) { - self.inner.lock().await.generate_one_time_keys(count); - } - - /// Get the maximum number of one-time keys the account can hold. - pub(crate) async fn max_one_time_keys(&self) -> usize { - self.inner.lock().await.max_number_of_one_time_keys() - } - - /// Get a tuple of device and one-time keys that need to be uploaded. - /// - /// Returns an empty error if no keys need to be uploaded. - pub(crate) async fn generate_one_time_keys(&self) -> Result { - let count = self.uploaded_key_count() as u64; - let max_keys = self.max_one_time_keys().await; - let max_on_server = (max_keys as u64) / 2; - - if count >= (max_on_server) { - return Err(()); - } - - let key_count = (max_on_server) - count; - let key_count: usize = key_count.try_into().unwrap_or(max_keys); - - self.generate_one_time_keys_helper(key_count).await; - Ok(key_count as u64) - } - - /// Should account or one-time keys be uploaded to the server. - pub(crate) async fn should_upload_keys(&self) -> bool { - if !self.shared() { - return true; - } - - let count = self.uploaded_key_count() as u64; - - // If we have a known key count, check that we have more than - // max_one_time_Keys() / 2, otherwise tell the client to upload more. - let max_keys = self.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 > (max_keys / 2) { - return false; - } - - let key_count = (max_keys / 2) - count; - key_count > 0 - } - - /// Get a tuple of device and one-time keys that need to be uploaded. - /// - /// Returns an empty error if no keys need to be uploaded. - pub(crate) async fn keys_for_upload( - &self, - ) -> Result< - ( - Option, - Option>, - ), - (), - > { - if !self.should_upload_keys().await { - return Err(()); - } - - let device_keys = if !self.shared() { - Some(self.device_keys().await) - } else { - None - }; - - let one_time_keys = self.signed_one_time_keys().await.ok(); - - Ok((device_keys, one_time_keys)) - } - - /// Mark the current set of one-time keys as being published. - pub(crate) async fn mark_keys_as_published(&self) { - self.inner.lock().await.mark_keys_as_published(); - } - - /// Sign the given string using the accounts signing key. - /// - /// Returns the signature as a base64 encoded string. - pub async fn sign(&self, string: &str) -> String { - self.inner.lock().await.sign(string) - } - - /// Store the account as a base64 encoded string. - /// - /// # Arguments - /// - /// * `pickle_mode` - The mode that was used to pickle the account, either an - /// unencrypted mode or an encrypted using passphrase. - pub async fn pickle(&self, pickle_mode: PicklingMode) -> String { - self.inner.lock().await.pickle(pickle_mode) - } - - /// Restore an account from a previously pickled string. - /// - /// # Arguments - /// - /// * `pickle` - The pickled string of the account. - /// - /// * `pickle_mode` - The mode that was used to pickle the account, either an - /// unencrypted mode or an encrypted using passphrase. - /// - /// * `shared` - Boolean determining if the account was uploaded to the - /// server. - pub fn from_pickle( - pickle: String, - pickle_mode: PicklingMode, - shared: bool, - uploaded_signed_key_count: i64, - user_id: &UserId, - device_id: &DeviceId, - ) -> Result { - let account = OlmAccount::unpickle(pickle, pickle_mode)?; - let identity_keys = account.parsed_identity_keys(); - - Ok(Account { - user_id: Arc::new(user_id.to_owned()), - device_id: Arc::new(device_id.to_owned()), - inner: Arc::new(Mutex::new(account)), - identity_keys: Arc::new(identity_keys), - shared: Arc::new(AtomicBool::from(shared)), - uploaded_signed_key_count: Arc::new(AtomicI64::new(uploaded_signed_key_count)), - }) - } - - /// Sign the device keys of the account and return them so they can be - /// uploaded. - pub(crate) async fn device_keys(&self) -> DeviceKeys { - let identity_keys = self.identity_keys(); - - let mut keys = BTreeMap::new(); - - keys.insert( - AlgorithmAndDeviceId(KeyAlgorithm::Curve25519, (*self.device_id).clone()), - identity_keys.curve25519().to_owned(), - ); - keys.insert( - AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()), - identity_keys.ed25519().to_owned(), - ); - - let device_keys = json!({ - "user_id": (*self.user_id).clone(), - "device_id": (*self.device_id).clone(), - "algorithms": Account::ALGORITHMS, - "keys": keys, - }); - - let mut signatures = BTreeMap::new(); - - let mut signature = BTreeMap::new(); - signature.insert( - AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()), - self.sign_json(&device_keys).await, - ); - signatures.insert((*self.user_id).clone(), signature); - - DeviceKeys { - user_id: (*self.user_id).clone(), - device_id: (*self.device_id).clone(), - algorithms: vec![ - Algorithm::OlmV1Curve25519AesSha2, - Algorithm::MegolmV1AesSha2, - ], - keys, - signatures, - unsigned: None, - } - } - - /// Convert a JSON value to the canonical representation and sign the JSON - /// string. - /// - /// # Arguments - /// - /// * `json` - The value that should be converted into a canonical JSON - /// string. - /// - /// # Panic - /// - /// Panics if the json value can't be serialized. - pub async fn sign_json(&self, json: &Value) -> String { - let canonical_json = cjson::to_string(json) - .unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", json))); - self.sign(&canonical_json).await - } - - /// Generate, sign and prepare one-time keys to be uploaded. - /// - /// If no one-time keys need to be uploaded returns an empty error. - pub(crate) async fn signed_one_time_keys( - &self, - ) -> Result, ()> { - let _ = self.generate_one_time_keys().await?; - - let one_time_keys = self.one_time_keys().await; - let mut one_time_key_map = BTreeMap::new(); - - for (key_id, key) in one_time_keys.curve25519().iter() { - let key_json = json!({ - "key": key, - }); - - let signature = self.sign_json(&key_json).await; - - let mut signature_map = BTreeMap::new(); - - signature_map.insert( - AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()), - signature, - ); - - let mut signatures = BTreeMap::new(); - signatures.insert((*self.user_id).clone(), signature_map); - - let signed_key = SignedKey { - key: key.to_owned(), - signatures, - }; - - one_time_key_map.insert( - AlgorithmAndDeviceId(KeyAlgorithm::SignedCurve25519, key_id.to_owned()), - OneTimeKey::SignedKey(signed_key), - ); - } - - Ok(one_time_key_map) - } - - /// Create a new session with another account given a one-time key. - /// - /// Returns the newly created session or a `OlmSessionError` if creating a - /// session failed. - /// - /// # Arguments - /// * `their_identity_key` - The other account's identity/curve25519 key. - /// - /// * `their_one_time_key` - A signed one-time key that the other account - /// created and shared with us. - pub(crate) async fn create_outbound_session( - &self, - their_identity_key: &str, - their_one_time_key: &SignedKey, - ) -> Result { - let session = self - .inner - .lock() - .await - .create_outbound_session(their_identity_key, &their_one_time_key.key)?; - - let now = Instant::now(); - let session_id = session.session_id(); - - Ok(Session { - 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), - last_use_time: Arc::new(now), - }) - } - - /// Create a new session with another account given a pre-key Olm message. - /// - /// Returns the newly created session or a `OlmSessionError` if creating a - /// session failed. - /// - /// # Arguments - /// * `their_identity_key` - The other account's identitiy/curve25519 key. - /// - /// * `message` - A pre-key Olm message that was sent to us by the other - /// account. - pub(crate) async fn create_inbound_session( - &self, - their_identity_key: &str, - message: PreKeyMessage, - ) -> Result { - let session = self - .inner - .lock() - .await - .create_inbound_session_from(their_identity_key, message)?; - - self.inner - .lock() - .await - .remove_one_time_keys(&session) - .expect( - "Session was successfully created but the account doesn't hold a matching one-time key", - ); - - let now = Instant::now(); - let session_id = session.session_id(); - - Ok(Session { - 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), - last_use_time: Arc::new(now), - }) - } - - /// Create a group session pair. - /// - /// This session pair can be used to encrypt and decrypt messages meant for - /// a large group of participants. - /// - /// The outbound session is used to encrypt messages while the inbound one - /// is used to decrypt messages encrypted by the outbound one. - /// - /// # Arguments - /// - /// * `room_id` - The ID of the room where the group session will be used. - pub(crate) async fn create_group_session_pair( - &self, - room_id: &RoomId, - ) -> (OutboundGroupSession, InboundGroupSession) { - let outbound = - OutboundGroupSession::new(self.device_id.clone(), self.identity_keys.clone(), room_id); - let identity_keys = self.identity_keys(); - - let sender_key = identity_keys.curve25519(); - let signing_key = identity_keys.ed25519(); - - let inbound = InboundGroupSession::new( - sender_key, - signing_key, - &room_id, - outbound.session_key().await, - ) - .expect("Can't create inbound group session from a newly created outbound group session"); - - (outbound, inbound) - } -} - -impl PartialEq for Account { - fn eq(&self, other: &Self) -> bool { - self.identity_keys() == other.identity_keys() && self.shared() == other.shared() - } -} - -/// Cryptographic session that enables secure communication between two -/// `Account`s -#[derive(Clone)] -pub struct Session { - inner: Arc>, - session_id: Arc, - pub(crate) sender_key: Arc, - pub(crate) creation_time: Arc, - pub(crate) last_use_time: Arc, -} - -// #[cfg_attr(tarpaulin, skip)] -impl fmt::Debug for Session { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Session") - .field("session_id", &self.session_id()) - .field("sender_key", &self.sender_key) - .finish() - } -} - -impl Session { - /// Decrypt the given Olm message. - /// - /// Returns the decrypted plaintext or an `OlmSessionError` if decryption - /// failed. - /// - /// # Arguments - /// - /// * `message` - The Olm message that should be decrypted. - pub async fn decrypt(&mut self, message: OlmMessage) -> Result { - let plaintext = self.inner.lock().await.decrypt(message)?; - self.last_use_time = Arc::new(Instant::now()); - Ok(plaintext) - } - - /// Encrypt the given plaintext as a OlmMessage. - /// - /// Returns the encrypted Olm message. - /// - /// # Arguments - /// - /// * `plaintext` - The plaintext that should be encrypted. - pub async fn encrypt(&mut self, plaintext: &str) -> OlmMessage { - let message = self.inner.lock().await.encrypt(plaintext); - self.last_use_time = Arc::new(Instant::now()); - message - } - - /// Check if a pre-key Olm message was encrypted for this session. - /// - /// Returns true if it matches, false if not and a OlmSessionError if there - /// was an error checking if it matches. - /// - /// # Arguments - /// - /// * `their_identity_key` - The identity/curve25519 key of the account - /// that encrypted this Olm message. - /// - /// * `message` - The pre-key Olm message that should be checked. - 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) -> &str { - &self.session_id - } - - /// Store the session as a base64 encoded string. - /// - /// # Arguments - /// - /// * `pickle_mode` - The mode that was used to pickle the session, either - /// an unencrypted mode or an encrypted using passphrase. - pub async fn pickle(&self, pickle_mode: PicklingMode) -> String { - self.inner.lock().await.pickle(pickle_mode) - } - - /// Restore a Session from a previously pickled string. - /// - /// Returns the restored Olm Session or a `OlmSessionError` if there was an - /// error. - /// - /// # Arguments - /// - /// * `pickle` - The pickled string of the session. - /// - /// * `pickle_mode` - The mode that was used to pickle the session, either - /// an unencrypted mode or an encrypted using passphrase. - /// - /// * `sender_key` - The public curve25519 key of the account that - /// established the session with us. - /// - /// * `creation_time` - The timestamp that marks when the session was - /// created. - /// - /// * `last_use_time` - The timestamp that marks when the session was - /// last used to encrypt or decrypt an Olm message. - pub fn from_pickle( - pickle: String, - pickle_mode: PicklingMode, - sender_key: String, - creation_time: Instant, - last_use_time: Instant, - ) -> Result { - let session = OlmSession::unpickle(pickle, pickle_mode)?; - let session_id = session.session_id(); - - Ok(Session { - 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), - }) - } -} - -impl PartialEq for Session { - fn eq(&self, other: &Self) -> bool { - self.session_id() == other.session_id() - } -} - -/// The private session key of a group session. -/// Can be used to create a new inbound group session. -#[derive(Clone, Debug, Serialize, Zeroize)] -#[zeroize(drop)] -pub struct GroupSessionKey(pub String); - -/// Inbound group session. -/// -/// Inbound group sessions are used to exchange room messages between a group of -/// participants. Inbound group sessions are used to decrypt the room messages. -#[derive(Clone)] -pub struct InboundGroupSession { - inner: Arc>, - session_id: Arc, - pub(crate) sender_key: Arc, - pub(crate) signing_key: Arc, - pub(crate) room_id: Arc, - forwarding_chains: Arc>>>, -} - -impl InboundGroupSession { - /// Create a new inbound group session for the given room. - /// - /// These sessions are used to decrypt room messages. - /// - /// # Arguments - /// - /// * `sender_key` - The public curve25519 key of the account that - /// sent us the session - /// - /// * `signing_key` - The public ed25519 key of the account that - /// sent us the session. - /// - /// * `room_id` - The id of the room that the session is used in. - /// - /// * `session_key` - The private session key that is used to decrypt - /// messages. - pub fn new( - sender_key: &str, - signing_key: &str, - room_id: &RoomId, - session_key: GroupSessionKey, - ) -> Result { - let session = OlmInboundGroupSession::new(&session_key.0)?; - let session_id = session.session_id(); - - Ok(InboundGroupSession { - inner: Arc::new(Mutex::new(session)), - session_id: Arc::new(session_id), - sender_key: Arc::new(sender_key.to_owned()), - signing_key: Arc::new(signing_key.to_owned()), - room_id: Arc::new(room_id.clone()), - forwarding_chains: Arc::new(Mutex::new(None)), - }) - } - - /// Store the group session as a base64 encoded string. - /// - /// # Arguments - /// - /// * `pickle_mode` - The mode that was used to pickle the group session, - /// either an unencrypted mode or an encrypted using passphrase. - pub async fn pickle(&self, pickle_mode: PicklingMode) -> String { - self.inner.lock().await.pickle(pickle_mode) - } - - /// Restore a Session from a previously pickled string. - /// - /// Returns the restored group session or a `OlmGroupSessionError` if there - /// was an error. - /// - /// # Arguments - /// - /// * `pickle` - The pickled string of the group session session. - /// - /// * `pickle_mode` - The mode that was used to pickle the session, either - /// an unencrypted mode or an encrypted using passphrase. - /// - /// * `sender_key` - The public curve25519 key of the account that - /// sent us the session - /// - /// * `signing_key` - The public ed25519 key of the account that - /// sent us the session. - /// - /// * `room_id` - The id of the room that the session is used in. - pub fn from_pickle( - pickle: String, - pickle_mode: PicklingMode, - sender_key: String, - signing_key: String, - room_id: RoomId, - ) -> Result { - let session = OlmInboundGroupSession::unpickle(pickle, pickle_mode)?; - let session_id = session.session_id(); - - Ok(InboundGroupSession { - inner: Arc::new(Mutex::new(session)), - session_id: Arc::new(session_id), - sender_key: Arc::new(sender_key), - signing_key: Arc::new(signing_key), - room_id: Arc::new(room_id), - forwarding_chains: Arc::new(Mutex::new(None)), - }) - } - - /// Returns the unique identifier for this session. - pub fn session_id(&self) -> &str { - &self.session_id - } - - /// Get the first message index we know how to decrypt. - pub async fn first_known_index(&self) -> u32 { - self.inner.lock().await.first_known_index() - } - - /// Decrypt the given ciphertext. - /// - /// Returns the decrypted plaintext or an `OlmGroupSessionError` if - /// decryption failed. - /// - /// # Arguments - /// - /// * `message` - The message that should be decrypted. - pub async fn decrypt_helper( - &self, - message: String, - ) -> Result<(String, u32), OlmGroupSessionError> { - self.inner.lock().await.decrypt(message) - } - - /// Decrypt an event from a room timeline. - /// - /// # Arguments - /// - /// * `event` - The event that should be decrypted. - pub async fn decrypt( - &self, - event: &MessageEventStub, - ) -> MegolmResult<(EventJson, u32)> { - let content = match &event.content { - EncryptedEventContent::MegolmV1AesSha2(c) => c, - _ => return Err(EventError::UnsupportedAlgorithm.into()), - }; - - let (plaintext, message_index) = self.decrypt_helper(content.ciphertext.clone()).await?; - - let mut decrypted_value = serde_json::from_str::(&plaintext)?; - let decrypted_object = decrypted_value - .as_object_mut() - .ok_or(EventError::NotAnObject)?; - - // TODO better number conversion here. - let server_ts = event - .origin_server_ts - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let server_ts: i64 = server_ts.try_into().unwrap_or_default(); - - decrypted_object.insert("sender".to_owned(), event.sender.to_string().into()); - decrypted_object.insert("event_id".to_owned(), event.event_id.to_string().into()); - decrypted_object.insert("origin_server_ts".to_owned(), server_ts.into()); - - decrypted_object.insert( - "unsigned".to_owned(), - serde_json::to_value(&event.unsigned).unwrap_or_default(), - ); - - Ok(( - serde_json::from_value::>(decrypted_value)?, - message_index, - )) - } -} - -// #[cfg_attr(tarpaulin, skip)] -impl fmt::Debug for InboundGroupSession { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("InboundGroupSession") - .field("session_id", &self.session_id()) - .finish() - } -} - -impl PartialEq for InboundGroupSession { - fn eq(&self, other: &Self) -> bool { - self.session_id() == other.session_id() - } -} - -/// Outbound group session. -/// -/// Outbound group sessions are used to exchange room messages between a group -/// of participants. Outbound group sessions are used to encrypt the room -/// messages. -#[derive(Clone)] -pub struct OutboundGroupSession { - inner: Arc>, - device_id: Arc, - account_identity_keys: Arc, - session_id: Arc, - room_id: Arc, - creation_time: Arc, - message_count: Arc, - shared: Arc, -} - -impl OutboundGroupSession { - /// Create a new outbound group session for the given room. - /// - /// Outbound group sessions are used to encrypt room messages. - /// - /// # Arguments - /// - /// * `device_id` - The id of the device that created this session. - /// - /// * `identity_keys` - The identity keys of the account that created this - /// session. - /// - /// * `room_id` - The id of the room that the session is used in. - fn new(device_id: Arc, identity_keys: Arc, room_id: &RoomId) -> Self { - let session = OlmOutboundGroupSession::new(); - let session_id = session.session_id(); - - OutboundGroupSession { - inner: Arc::new(Mutex::new(session)), - room_id: Arc::new(room_id.to_owned()), - device_id, - account_identity_keys: identity_keys, - session_id: Arc::new(session_id), - creation_time: Arc::new(Instant::now()), - message_count: Arc::new(AtomicUsize::new(0)), - shared: Arc::new(AtomicBool::new(false)), - } - } - - /// Encrypt the given plaintext using this session. - /// - /// Returns the encrypted ciphertext. - /// - /// # Arguments - /// - /// * `plaintext` - The plaintext that should be encrypted. - async fn encrypt_helper(&self, plaintext: String) -> String { - let session = self.inner.lock().await; - session.encrypt(plaintext) - } - - /// Encrypt a room message for the given room. - /// - /// Beware that a group session needs to be shared before this method can be - /// called using the `share_group_session()` method. - /// - /// Since group sessions can expire or become invalid if the room membership - /// changes client authors should check with the - /// `should_share_group_session()` method if a new group session needs to - /// be shared. - /// - /// # Arguments - /// - /// * `content` - The plaintext content of the message that should be - /// encrypted. - /// - /// # Panics - /// - /// Panics if the content can't be serialized. - pub async fn encrypt(&self, content: MessageEventContent) -> EncryptedEventContent { - let json_content = json!({ - "content": content, - "room_id": &*self.room_id, - "type": EventType::RoomMessage, - }); - - let plaintext = cjson::to_string(&json_content).unwrap_or_else(|_| { - panic!(format!( - "Can't serialize {} to canonical JSON", - json_content - )) - }); - - let ciphertext = self.encrypt_helper(plaintext).await; - - EncryptedEventContent::MegolmV1AesSha2(MegolmV1AesSha2Content { - ciphertext, - sender_key: self.account_identity_keys.curve25519().to_owned(), - session_id: self.session_id().to_owned(), - device_id: (&*self.device_id).to_owned(), - }) - } - - /// Check if the session has expired and if it should be rotated. - /// - /// A session will expire after some time or if enough messages have been - /// encrypted using it. - pub fn expired(&self) -> bool { - // TODO implement this. - false - } - - /// Mark the session as shared. - /// - /// Messages shouldn't be encrypted with the session before it has been - /// shared. - pub fn mark_as_shared(&self) { - self.shared.store(true, Ordering::Relaxed); - } - - /// Check if the session has been marked as shared. - pub fn shared(&self) -> bool { - self.shared.load(Ordering::Relaxed) - } - - /// Get the session key of this session. - /// - /// A session key can be used to to create an `InboundGroupSession`. - pub async fn session_key(&self) -> GroupSessionKey { - let session = self.inner.lock().await; - GroupSessionKey(session.session_key()) - } - - /// Returns the unique identifier for this session. - pub fn session_id(&self) -> &str { - &self.session_id - } - - /// Get the current message index for this session. - /// - /// Each message is sent with an increasing index. This returns the - /// message index that will be used for the next encrypted message. - pub async fn message_index(&self) -> u32 { - let session = self.inner.lock().await; - session.session_message_index() - } -} - -// #[cfg_attr(tarpaulin, skip)] -impl std::fmt::Debug for OutboundGroupSession { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("OutboundGroupSession") - .field("session_id", &self.session_id) - .field("room_id", &self.room_id) - .field("creation_time", &self.creation_time) - .field("message_count", &self.message_count) - .finish() - } -} - -#[cfg(test)] -pub(crate) mod test { - use crate::olm::{Account, InboundGroupSession, Session}; - use matrix_sdk_common::api::r0::keys::SignedKey; - use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId}; - use olm_rs::session::OlmMessage; - use std::collections::BTreeMap; - use std::convert::TryFrom; - - fn alice_id() -> UserId { - UserId::try_from("@alice:example.org").unwrap() - } - - fn alice_device_id() -> DeviceId { - "ALICEDEVICE".to_string() - } - - fn bob_id() -> UserId { - UserId::try_from("@bob:example.org").unwrap() - } - - fn bob_device_id() -> DeviceId { - "BOBDEVICE".to_string() - } - - pub(crate) async fn get_account_and_session() -> (Account, Session) { - let alice = Account::new(&alice_id(), &alice_device_id()); - let bob = Account::new(&bob_id(), &bob_device_id()); - - bob.generate_one_time_keys_helper(1).await; - let one_time_key = bob - .one_time_keys() - .await - .curve25519() - .iter() - .nth(0) - .unwrap() - .1 - .to_owned(); - let one_time_key = SignedKey { - key: one_time_key, - signatures: BTreeMap::new(), - }; - let sender_key = bob.identity_keys().curve25519().to_owned(); - let session = alice - .create_outbound_session(&sender_key, &one_time_key) - .await - .unwrap(); - - (alice, session) - } - - #[test] - fn account_creation() { - let account = Account::new(&alice_id(), &alice_device_id()); - let identyty_keys = account.identity_keys(); - - assert!(!account.shared()); - assert!(!identyty_keys.ed25519().is_empty()); - assert_ne!(identyty_keys.values().len(), 0); - assert_ne!(identyty_keys.keys().len(), 0); - assert_ne!(identyty_keys.iter().len(), 0); - assert!(identyty_keys.contains_key("ed25519")); - assert_eq!( - identyty_keys.ed25519(), - identyty_keys.get("ed25519").unwrap() - ); - assert!(!identyty_keys.curve25519().is_empty()); - - account.mark_as_shared(); - assert!(account.shared()); - } - - #[tokio::test] - async fn one_time_keys_creation() { - let account = Account::new(&alice_id(), &alice_device_id()); - let one_time_keys = account.one_time_keys().await; - - assert!(one_time_keys.curve25519().is_empty()); - assert_ne!(account.max_one_time_keys().await, 0); - - account.generate_one_time_keys_helper(10).await; - let one_time_keys = account.one_time_keys().await; - - assert!(!one_time_keys.curve25519().is_empty()); - assert_ne!(one_time_keys.values().len(), 0); - assert_ne!(one_time_keys.keys().len(), 0); - assert_ne!(one_time_keys.iter().len(), 0); - assert!(one_time_keys.contains_key("curve25519")); - assert_eq!(one_time_keys.curve25519().keys().len(), 10); - assert_eq!( - one_time_keys.curve25519(), - one_time_keys.get("curve25519").unwrap() - ); - - account.mark_keys_as_published().await; - let one_time_keys = account.one_time_keys().await; - assert!(one_time_keys.curve25519().is_empty()); - } - - #[tokio::test] - async fn session_creation() { - let alice = Account::new(&alice_id(), &alice_device_id()); - let bob = Account::new(&bob_id(), &bob_device_id()); - let alice_keys = alice.identity_keys(); - alice.generate_one_time_keys_helper(1).await; - let one_time_keys = alice.one_time_keys().await; - alice.mark_keys_as_published().await; - - let one_time_key = one_time_keys - .curve25519() - .iter() - .nth(0) - .unwrap() - .1 - .to_owned(); - - let one_time_key = SignedKey { - key: one_time_key, - signatures: BTreeMap::new(), - }; - - let mut bob_session = bob - .create_outbound_session(alice_keys.curve25519(), &one_time_key) - .await - .unwrap(); - - let plaintext = "Hello world"; - - let message = bob_session.encrypt(plaintext).await; - - let prekey_message = match message.clone() { - OlmMessage::PreKey(m) => m, - OlmMessage::Message(_) => panic!("Incorrect message type"), - }; - - let bob_keys = bob.identity_keys(); - let mut alice_session = alice - .create_inbound_session(bob_keys.curve25519(), prekey_message.clone()) - .await - .unwrap(); - - assert!(alice_session - .matches(bob_keys.curve25519(), prekey_message) - .await - .unwrap()); - - assert_eq!(bob_session.session_id(), alice_session.session_id()); - - let decyrpted = alice_session.decrypt(message).await.unwrap(); - assert_eq!(plaintext, decyrpted); - } - - #[tokio::test] - async fn group_session_creation() { - let alice = Account::new(&alice_id(), &alice_device_id()); - let room_id = RoomId::try_from("!test:localhost").unwrap(); - - let (outbound, _) = alice.create_group_session_pair(&room_id).await; - - assert_eq!(0, outbound.message_index().await); - assert!(!outbound.shared()); - outbound.mark_as_shared(); - assert!(outbound.shared()); - - let inbound = InboundGroupSession::new( - "test_key", - "test_key", - &room_id, - outbound.session_key().await, - ) - .unwrap(); - - assert_eq!(0, inbound.first_known_index().await); - - assert_eq!(outbound.session_id(), inbound.session_id()); - - let plaintext = "This is a secret to everybody".to_owned(); - let ciphertext = outbound.encrypt_helper(plaintext.clone()).await; - - assert_eq!( - plaintext, - inbound.decrypt_helper(ciphertext).await.unwrap().0 - ); - } -} diff --git a/matrix_sdk_crypto/src/olm/account.rs b/matrix_sdk_crypto/src/olm/account.rs new file mode 100644 index 00000000..a1308666 --- /dev/null +++ b/matrix_sdk_crypto/src/olm/account.rs @@ -0,0 +1,550 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use matrix_sdk_common::instant::Instant; +use std::convert::TryFrom; +use std::convert::TryInto; +use std::fmt; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::sync::Arc; + +use matrix_sdk_common::locks::Mutex; +use serde_json::{json, Value}; +use std::collections::BTreeMap; + +pub use olm_rs::account::IdentityKeys; +use olm_rs::account::{OlmAccount, OneTimeKeys}; +use olm_rs::errors::{OlmAccountError, OlmSessionError}; +use olm_rs::PicklingMode; + +use crate::device::Device; +use crate::error::SessionCreationError; +pub use olm_rs::{ + session::{OlmMessage, PreKeyMessage}, + utility::OlmUtility, +}; + +use matrix_sdk_common::{ + api::r0::keys::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey, SignedKey}, + events::Algorithm, + identifiers::{DeviceId, RoomId, UserId}, +}; + +use super::{InboundGroupSession, OutboundGroupSession, Session}; + +/// Account holding identity keys for which sessions can be created. +/// +/// An account is the central identity for encrypted communication between two +/// devices. +#[derive(Clone)] +pub struct Account { + pub(crate) user_id: Arc, + pub(crate) device_id: Arc>, + inner: Arc>, + pub(crate) identity_keys: Arc, + shared: Arc, + /// The number of signed one-time keys we have uploaded to the server. If + /// this is None, no action will be taken. After a sync request the client + /// needs to set this for us, depending on the count we will suggest the + /// client to upload new keys. + uploaded_signed_key_count: Arc, +} + +// #[cfg_attr(tarpaulin, skip)] +impl fmt::Debug for Account { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Account") + .field("identity_keys", self.identity_keys()) + .field("shared", &self.shared()) + .finish() + } +} + +impl Account { + const ALGORITHMS: &'static [&'static Algorithm] = &[ + &Algorithm::OlmV1Curve25519AesSha2, + &Algorithm::MegolmV1AesSha2, + ]; + + /// Create a fresh new account, this will generate the identity key-pair. + #[allow(clippy::ptr_arg)] + pub fn new(user_id: &UserId, device_id: &DeviceId) -> Self { + let account = OlmAccount::new(); + let identity_keys = account.parsed_identity_keys(); + + Account { + user_id: Arc::new(user_id.to_owned()), + device_id: Arc::new(device_id.into()), + inner: Arc::new(Mutex::new(account)), + identity_keys: Arc::new(identity_keys), + shared: Arc::new(AtomicBool::new(false)), + uploaded_signed_key_count: Arc::new(AtomicI64::new(0)), + } + } + + /// Get the public parts of the identity keys for the account. + pub fn identity_keys(&self) -> &IdentityKeys { + &self.identity_keys + } + + /// Update the uploaded key count. + /// + /// # Arguments + /// + /// * `new_count` - The new count that was reported by the server. + pub(crate) fn update_uploaded_key_count(&self, new_count: u64) { + let key_count = i64::try_from(new_count).unwrap_or(i64::MAX); + self.uploaded_signed_key_count + .store(key_count, Ordering::Relaxed); + } + + /// Get the currently known uploaded key count. + pub fn uploaded_key_count(&self) -> i64 { + self.uploaded_signed_key_count.load(Ordering::Relaxed) + } + + /// Has the account been shared with the server. + pub fn shared(&self) -> bool { + self.shared.load(Ordering::Relaxed) + } + + /// Mark the account as shared. + /// + /// Messages shouldn't be encrypted with the session before it has been + /// shared. + pub(crate) fn mark_as_shared(&self) { + self.shared.store(true, Ordering::Relaxed); + } + + /// Get the one-time keys of the account. + /// + /// This can be empty, keys need to be generated first. + pub(crate) async fn one_time_keys(&self) -> OneTimeKeys { + self.inner.lock().await.parsed_one_time_keys() + } + + /// Generate count number of one-time keys. + pub(crate) async fn generate_one_time_keys_helper(&self, count: usize) { + self.inner.lock().await.generate_one_time_keys(count); + } + + /// Get the maximum number of one-time keys the account can hold. + pub(crate) async fn max_one_time_keys(&self) -> usize { + self.inner.lock().await.max_number_of_one_time_keys() + } + + /// Get a tuple of device and one-time keys that need to be uploaded. + /// + /// Returns an empty error if no keys need to be uploaded. + pub(crate) async fn generate_one_time_keys(&self) -> Result { + let count = self.uploaded_key_count() as u64; + let max_keys = self.max_one_time_keys().await; + let max_on_server = (max_keys as u64) / 2; + + if count >= (max_on_server) { + return Err(()); + } + + let key_count = (max_on_server) - count; + let key_count: usize = key_count.try_into().unwrap_or(max_keys); + + self.generate_one_time_keys_helper(key_count).await; + Ok(key_count as u64) + } + + /// Should account or one-time keys be uploaded to the server. + pub(crate) async fn should_upload_keys(&self) -> bool { + if !self.shared() { + return true; + } + + let count = self.uploaded_key_count() as u64; + + // If we have a known key count, check that we have more than + // max_one_time_Keys() / 2, otherwise tell the client to upload more. + let max_keys = self.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 > (max_keys / 2) { + return false; + } + + let key_count = (max_keys / 2) - count; + key_count > 0 + } + + /// Get a tuple of device and one-time keys that need to be uploaded. + /// + /// Returns an empty error if no keys need to be uploaded. + pub(crate) async fn keys_for_upload( + &self, + ) -> Result< + ( + Option, + Option>, + ), + (), + > { + if !self.should_upload_keys().await { + return Err(()); + } + + let device_keys = if !self.shared() { + Some(self.device_keys().await) + } else { + None + }; + + let one_time_keys = self.signed_one_time_keys().await.ok(); + + Ok((device_keys, one_time_keys)) + } + + /// Mark the current set of one-time keys as being published. + pub(crate) async fn mark_keys_as_published(&self) { + self.inner.lock().await.mark_keys_as_published(); + } + + /// Sign the given string using the accounts signing key. + /// + /// Returns the signature as a base64 encoded string. + pub async fn sign(&self, string: &str) -> String { + self.inner.lock().await.sign(string) + } + + /// Store the account as a base64 encoded string. + /// + /// # Arguments + /// + /// * `pickle_mode` - The mode that was used to pickle the account, either an + /// unencrypted mode or an encrypted using passphrase. + pub async fn pickle(&self, pickle_mode: PicklingMode) -> String { + self.inner.lock().await.pickle(pickle_mode) + } + + /// Restore an account from a previously pickled string. + /// + /// # Arguments + /// + /// * `pickle` - The pickled string of the account. + /// + /// * `pickle_mode` - The mode that was used to pickle the account, either an + /// unencrypted mode or an encrypted using passphrase. + /// + /// * `shared` - Boolean determining if the account was uploaded to the + /// server. + #[allow(clippy::ptr_arg)] + pub fn from_pickle( + pickle: String, + pickle_mode: PicklingMode, + shared: bool, + uploaded_signed_key_count: i64, + user_id: &UserId, + device_id: &DeviceId, + ) -> Result { + let account = OlmAccount::unpickle(pickle, pickle_mode)?; + let identity_keys = account.parsed_identity_keys(); + + Ok(Account { + user_id: Arc::new(user_id.to_owned()), + device_id: Arc::new(device_id.into()), + inner: Arc::new(Mutex::new(account)), + identity_keys: Arc::new(identity_keys), + shared: Arc::new(AtomicBool::from(shared)), + uploaded_signed_key_count: Arc::new(AtomicI64::new(uploaded_signed_key_count)), + }) + } + + /// Sign the device keys of the account and return them so they can be + /// uploaded. + pub(crate) async fn device_keys(&self) -> DeviceKeys { + let identity_keys = self.identity_keys(); + + let mut keys = BTreeMap::new(); + + keys.insert( + AlgorithmAndDeviceId(KeyAlgorithm::Curve25519, (*self.device_id).clone()), + identity_keys.curve25519().to_owned(), + ); + keys.insert( + AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()), + identity_keys.ed25519().to_owned(), + ); + + let device_keys = json!({ + "user_id": (*self.user_id).clone(), + "device_id": (*self.device_id).clone(), + "algorithms": Account::ALGORITHMS, + "keys": keys, + }); + + let mut signatures = BTreeMap::new(); + + let mut signature = BTreeMap::new(); + signature.insert( + AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()), + self.sign_json(&device_keys).await, + ); + signatures.insert((*self.user_id).clone(), signature); + + DeviceKeys { + user_id: (*self.user_id).clone(), + device_id: (*self.device_id).clone(), + algorithms: vec![ + Algorithm::OlmV1Curve25519AesSha2, + Algorithm::MegolmV1AesSha2, + ], + keys, + signatures, + unsigned: None, + } + } + + /// Convert a JSON value to the canonical representation and sign the JSON + /// string. + /// + /// # Arguments + /// + /// * `json` - The value that should be converted into a canonical JSON + /// string. + /// + /// # Panic + /// + /// Panics if the json value can't be serialized. + pub async fn sign_json(&self, json: &Value) -> String { + let canonical_json = cjson::to_string(json) + .unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", json))); + self.sign(&canonical_json).await + } + + /// Generate, sign and prepare one-time keys to be uploaded. + /// + /// If no one-time keys need to be uploaded returns an empty error. + pub(crate) async fn signed_one_time_keys( + &self, + ) -> Result, ()> { + let _ = self.generate_one_time_keys().await?; + + let one_time_keys = self.one_time_keys().await; + let mut one_time_key_map = BTreeMap::new(); + + for (key_id, key) in one_time_keys.curve25519().iter() { + let key_json = json!({ + "key": key, + }); + + let signature = self.sign_json(&key_json).await; + + let mut signature_map = BTreeMap::new(); + + signature_map.insert( + AlgorithmAndDeviceId(KeyAlgorithm::Ed25519, (*self.device_id).clone()), + signature, + ); + + let mut signatures = BTreeMap::new(); + signatures.insert((*self.user_id).clone(), signature_map); + + let signed_key = SignedKey { + key: key.to_owned(), + signatures, + }; + + one_time_key_map.insert( + AlgorithmAndDeviceId(KeyAlgorithm::SignedCurve25519, key_id.as_str().into()), + OneTimeKey::SignedKey(signed_key), + ); + } + + Ok(one_time_key_map) + } + + /// Create a new session with another account given a one-time key. + /// + /// Returns the newly created session or a `OlmSessionError` if creating a + /// session failed. + /// + /// # Arguments + /// * `their_identity_key` - The other account's identity/curve25519 key. + /// + /// * `their_one_time_key` - A signed one-time key that the other account + /// created and shared with us. + pub(crate) async fn create_outbound_session_helper( + &self, + their_identity_key: &str, + their_one_time_key: &SignedKey, + ) -> Result { + let session = self + .inner + .lock() + .await + .create_outbound_session(their_identity_key, &their_one_time_key.key)?; + + let now = Instant::now(); + let session_id = session.session_id(); + + Ok(Session { + user_id: self.user_id.clone(), + device_id: self.device_id.clone(), + our_identity_keys: self.identity_keys.clone(), + 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), + last_use_time: Arc::new(now), + }) + } + + /// Create a new session with another account given a one-time key and a + /// device. + /// + /// Returns the newly created session or a `OlmSessionError` if creating a + /// session failed. + /// + /// # Arguments + /// * `device` - The other account's device. + /// + /// * `key_map` - A map from the algorithm and device id to the one-time + /// key that the other account created and shared with us. + pub(crate) async fn create_outbound_session( + &self, + device: Device, + key_map: &BTreeMap, + ) -> Result { + let one_time_key = key_map.values().next().ok_or_else(|| { + SessionCreationError::OneTimeKeyMissing( + device.user_id().to_owned(), + device.device_id().into(), + ) + })?; + + let one_time_key = match one_time_key { + OneTimeKey::SignedKey(k) => k, + OneTimeKey::Key(_) => { + return Err(SessionCreationError::OneTimeKeyNotSigned( + device.user_id().to_owned(), + device.device_id().into(), + )); + } + }; + + device.verify_one_time_key(&one_time_key).map_err(|e| { + SessionCreationError::InvalidSignature( + device.user_id().to_owned(), + device.device_id().into(), + e, + ) + })?; + + let curve_key = device.get_key(KeyAlgorithm::Curve25519).ok_or_else(|| { + SessionCreationError::DeviceMissingCurveKey( + device.user_id().to_owned(), + device.device_id().into(), + ) + })?; + + self.create_outbound_session_helper(curve_key, &one_time_key) + .await + .map_err(|e| { + SessionCreationError::OlmError( + device.user_id().to_owned(), + device.device_id().into(), + e, + ) + }) + } + + /// Create a new session with another account given a pre-key Olm message. + /// + /// Returns the newly created session or a `OlmSessionError` if creating a + /// session failed. + /// + /// # Arguments + /// * `their_identity_key` - The other account's identitiy/curve25519 key. + /// + /// * `message` - A pre-key Olm message that was sent to us by the other + /// account. + pub(crate) async fn create_inbound_session( + &self, + their_identity_key: &str, + message: PreKeyMessage, + ) -> Result { + let session = self + .inner + .lock() + .await + .create_inbound_session_from(their_identity_key, message)?; + + self.inner + .lock() + .await + .remove_one_time_keys(&session) + .expect( + "Session was successfully created but the account doesn't hold a matching one-time key", + ); + + let now = Instant::now(); + let session_id = session.session_id(); + + Ok(Session { + user_id: self.user_id.clone(), + device_id: self.device_id.clone(), + our_identity_keys: self.identity_keys.clone(), + 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), + last_use_time: Arc::new(now), + }) + } + + /// Create a group session pair. + /// + /// This session pair can be used to encrypt and decrypt messages meant for + /// a large group of participants. + /// + /// The outbound session is used to encrypt messages while the inbound one + /// is used to decrypt messages encrypted by the outbound one. + /// + /// # Arguments + /// + /// * `room_id` - The ID of the room where the group session will be used. + pub(crate) async fn create_group_session_pair( + &self, + room_id: &RoomId, + ) -> (OutboundGroupSession, InboundGroupSession) { + let outbound = + OutboundGroupSession::new(self.device_id.clone(), self.identity_keys.clone(), room_id); + let identity_keys = self.identity_keys(); + + let sender_key = identity_keys.curve25519(); + let signing_key = identity_keys.ed25519(); + + let inbound = InboundGroupSession::new( + sender_key, + signing_key, + &room_id, + outbound.session_key().await, + ) + .expect("Can't create inbound group session from a newly created outbound group session"); + + (outbound, inbound) + } +} + +impl PartialEq for Account { + fn eq(&self, other: &Self) -> bool { + self.identity_keys() == other.identity_keys() && self.shared() == other.shared() + } +} diff --git a/matrix_sdk_crypto/src/olm/group_sessions.rs b/matrix_sdk_crypto/src/olm/group_sessions.rs new file mode 100644 index 00000000..7b977d3c --- /dev/null +++ b/matrix_sdk_crypto/src/olm/group_sessions.rs @@ -0,0 +1,412 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use matrix_sdk_common::instant::Instant; +use std::convert::TryInto; +use std::fmt; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; + +use matrix_sdk_common::locks::Mutex; +use serde::Serialize; +use serde_json::{json, Value}; +use zeroize::Zeroize; + +pub use olm_rs::account::IdentityKeys; +use olm_rs::errors::OlmGroupSessionError; +use olm_rs::inbound_group_session::OlmInboundGroupSession; +use olm_rs::outbound_group_session::OlmOutboundGroupSession; +use olm_rs::PicklingMode; + +use crate::error::{EventError, MegolmResult}; +pub use olm_rs::{ + session::{OlmMessage, PreKeyMessage}, + utility::OlmUtility, +}; + +use matrix_sdk_common::{ + events::{ + room::{ + encrypted::{EncryptedEventContent, MegolmV1AesSha2Content}, + message::MessageEventContent, + }, + Algorithm, AnySyncRoomEvent, EventJson, EventType, SyncMessageEvent, + }, + identifiers::{DeviceId, RoomId}, +}; + +/// The private session key of a group session. +/// Can be used to create a new inbound group session. +#[derive(Clone, Debug, Serialize, Zeroize)] +#[zeroize(drop)] +pub struct GroupSessionKey(pub String); + +/// Inbound group session. +/// +/// Inbound group sessions are used to exchange room messages between a group of +/// participants. Inbound group sessions are used to decrypt the room messages. +#[derive(Clone)] +pub struct InboundGroupSession { + inner: Arc>, + session_id: Arc, + pub(crate) sender_key: Arc, + pub(crate) signing_key: Arc, + pub(crate) room_id: Arc, + forwarding_chains: Arc>>>, +} + +impl InboundGroupSession { + /// Create a new inbound group session for the given room. + /// + /// These sessions are used to decrypt room messages. + /// + /// # Arguments + /// + /// * `sender_key` - The public curve25519 key of the account that + /// sent us the session + /// + /// * `signing_key` - The public ed25519 key of the account that + /// sent us the session. + /// + /// * `room_id` - The id of the room that the session is used in. + /// + /// * `session_key` - The private session key that is used to decrypt + /// messages. + pub fn new( + sender_key: &str, + signing_key: &str, + room_id: &RoomId, + session_key: GroupSessionKey, + ) -> Result { + let session = OlmInboundGroupSession::new(&session_key.0)?; + let session_id = session.session_id(); + + Ok(InboundGroupSession { + inner: Arc::new(Mutex::new(session)), + session_id: Arc::new(session_id), + sender_key: Arc::new(sender_key.to_owned()), + signing_key: Arc::new(signing_key.to_owned()), + room_id: Arc::new(room_id.clone()), + forwarding_chains: Arc::new(Mutex::new(None)), + }) + } + + /// Store the group session as a base64 encoded string. + /// + /// # Arguments + /// + /// * `pickle_mode` - The mode that was used to pickle the group session, + /// either an unencrypted mode or an encrypted using passphrase. + pub async fn pickle(&self, pickle_mode: PicklingMode) -> String { + self.inner.lock().await.pickle(pickle_mode) + } + + /// Restore a Session from a previously pickled string. + /// + /// Returns the restored group session or a `OlmGroupSessionError` if there + /// was an error. + /// + /// # Arguments + /// + /// * `pickle` - The pickled string of the group session session. + /// + /// * `pickle_mode` - The mode that was used to pickle the session, either + /// an unencrypted mode or an encrypted using passphrase. + /// + /// * `sender_key` - The public curve25519 key of the account that + /// sent us the session + /// + /// * `signing_key` - The public ed25519 key of the account that + /// sent us the session. + /// + /// * `room_id` - The id of the room that the session is used in. + pub fn from_pickle( + pickle: String, + pickle_mode: PicklingMode, + sender_key: String, + signing_key: String, + room_id: RoomId, + ) -> Result { + let session = OlmInboundGroupSession::unpickle(pickle, pickle_mode)?; + let session_id = session.session_id(); + + Ok(InboundGroupSession { + inner: Arc::new(Mutex::new(session)), + session_id: Arc::new(session_id), + sender_key: Arc::new(sender_key), + signing_key: Arc::new(signing_key), + room_id: Arc::new(room_id), + forwarding_chains: Arc::new(Mutex::new(None)), + }) + } + + /// Returns the unique identifier for this session. + pub fn session_id(&self) -> &str { + &self.session_id + } + + /// Get the first message index we know how to decrypt. + pub async fn first_known_index(&self) -> u32 { + self.inner.lock().await.first_known_index() + } + + /// Decrypt the given ciphertext. + /// + /// Returns the decrypted plaintext or an `OlmGroupSessionError` if + /// decryption failed. + /// + /// # Arguments + /// + /// * `message` - The message that should be decrypted. + pub async fn decrypt_helper( + &self, + message: String, + ) -> Result<(String, u32), OlmGroupSessionError> { + self.inner.lock().await.decrypt(message) + } + + /// Decrypt an event from a room timeline. + /// + /// # Arguments + /// + /// * `event` - The event that should be decrypted. + pub async fn decrypt( + &self, + event: &SyncMessageEvent, + ) -> MegolmResult<(EventJson, u32)> { + let content = match &event.content { + EncryptedEventContent::MegolmV1AesSha2(c) => c, + _ => return Err(EventError::UnsupportedAlgorithm.into()), + }; + + let (plaintext, message_index) = self.decrypt_helper(content.ciphertext.clone()).await?; + + let mut decrypted_value = serde_json::from_str::(&plaintext)?; + let decrypted_object = decrypted_value + .as_object_mut() + .ok_or(EventError::NotAnObject)?; + + // TODO better number conversion here. + let server_ts = event + .origin_server_ts + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + let server_ts: i64 = server_ts.try_into().unwrap_or_default(); + + decrypted_object.insert("sender".to_owned(), event.sender.to_string().into()); + decrypted_object.insert("event_id".to_owned(), event.event_id.to_string().into()); + decrypted_object.insert("origin_server_ts".to_owned(), server_ts.into()); + + decrypted_object.insert( + "unsigned".to_owned(), + serde_json::to_value(&event.unsigned).unwrap_or_default(), + ); + + Ok(( + serde_json::from_value::>(decrypted_value)?, + message_index, + )) + } +} + +// #[cfg_attr(tarpaulin, skip)] +impl fmt::Debug for InboundGroupSession { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InboundGroupSession") + .field("session_id", &self.session_id()) + .finish() + } +} + +impl PartialEq for InboundGroupSession { + fn eq(&self, other: &Self) -> bool { + self.session_id() == other.session_id() + } +} + +/// Outbound group session. +/// +/// Outbound group sessions are used to exchange room messages between a group +/// of participants. Outbound group sessions are used to encrypt the room +/// messages. +#[derive(Clone)] +pub struct OutboundGroupSession { + inner: Arc>, + device_id: Arc>, + account_identity_keys: Arc, + session_id: Arc, + room_id: Arc, + creation_time: Arc, + message_count: Arc, + shared: Arc, +} + +impl OutboundGroupSession { + /// Create a new outbound group session for the given room. + /// + /// Outbound group sessions are used to encrypt room messages. + /// + /// # Arguments + /// + /// * `device_id` - The id of the device that created this session. + /// + /// * `identity_keys` - The identity keys of the account that created this + /// session. + /// + /// * `room_id` - The id of the room that the session is used in. + pub fn new( + device_id: Arc>, + identity_keys: Arc, + room_id: &RoomId, + ) -> Self { + let session = OlmOutboundGroupSession::new(); + let session_id = session.session_id(); + + OutboundGroupSession { + inner: Arc::new(Mutex::new(session)), + room_id: Arc::new(room_id.to_owned()), + device_id, + account_identity_keys: identity_keys, + session_id: Arc::new(session_id), + creation_time: Arc::new(Instant::now()), + message_count: Arc::new(AtomicUsize::new(0)), + shared: Arc::new(AtomicBool::new(false)), + } + } + + /// Encrypt the given plaintext using this session. + /// + /// Returns the encrypted ciphertext. + /// + /// # Arguments + /// + /// * `plaintext` - The plaintext that should be encrypted. + pub(crate) async fn encrypt_helper(&self, plaintext: String) -> String { + let session = self.inner.lock().await; + session.encrypt(plaintext) + } + + /// Encrypt a room message for the given room. + /// + /// Beware that a group session needs to be shared before this method can be + /// called using the `share_group_session()` method. + /// + /// Since group sessions can expire or become invalid if the room membership + /// changes client authors should check with the + /// `should_share_group_session()` method if a new group session needs to + /// be shared. + /// + /// # Arguments + /// + /// * `content` - The plaintext content of the message that should be + /// encrypted. + /// + /// # Panics + /// + /// Panics if the content can't be serialized. + pub async fn encrypt(&self, content: MessageEventContent) -> EncryptedEventContent { + let json_content = json!({ + "content": content, + "room_id": &*self.room_id, + "type": EventType::RoomMessage, + }); + + let plaintext = cjson::to_string(&json_content).unwrap_or_else(|_| { + panic!(format!( + "Can't serialize {} to canonical JSON", + json_content + )) + }); + + let ciphertext = self.encrypt_helper(plaintext).await; + + EncryptedEventContent::MegolmV1AesSha2(MegolmV1AesSha2Content::new( + matrix_sdk_common::events::room::encrypted::MegolmV1AesSha2ContentInit { + ciphertext, + sender_key: self.account_identity_keys.curve25519().to_owned(), + session_id: self.session_id().to_owned(), + device_id: (&*self.device_id).to_owned(), + }, + )) + } + + /// Check if the session has expired and if it should be rotated. + /// + /// A session will expire after some time or if enough messages have been + /// encrypted using it. + pub fn expired(&self) -> bool { + // TODO implement this. + false + } + + /// Mark the session as shared. + /// + /// Messages shouldn't be encrypted with the session before it has been + /// shared. + pub fn mark_as_shared(&self) { + self.shared.store(true, Ordering::Relaxed); + } + + /// Check if the session has been marked as shared. + pub fn shared(&self) -> bool { + self.shared.load(Ordering::Relaxed) + } + + /// Get the session key of this session. + /// + /// A session key can be used to to create an `InboundGroupSession`. + pub async fn session_key(&self) -> GroupSessionKey { + let session = self.inner.lock().await; + GroupSessionKey(session.session_key()) + } + + /// Returns the unique identifier for this session. + pub fn session_id(&self) -> &str { + &self.session_id + } + + /// Get the current message index for this session. + /// + /// Each message is sent with an increasing index. This returns the + /// message index that will be used for the next encrypted message. + pub async fn message_index(&self) -> u32 { + let session = self.inner.lock().await; + session.session_message_index() + } + + /// Get the outbound group session key as a json value that can be sent as a + /// m.room_key. + pub async fn as_json(&self) -> Value { + json!({ + "algorithm": Algorithm::MegolmV1AesSha2, + "room_id": &*self.room_id, + "session_id": &*self.session_id, + "session_key": self.session_key().await, + "chain_index": self.message_index().await, + }) + } +} + +// #[cfg_attr(tarpaulin, skip)] +impl std::fmt::Debug for OutboundGroupSession { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OutboundGroupSession") + .field("session_id", &self.session_id) + .field("room_id", &self.room_id) + .field("creation_time", &self.creation_time) + .field("message_count", &self.message_count) + .finish() + } +} diff --git a/matrix_sdk_crypto/src/olm/mod.rs b/matrix_sdk_crypto/src/olm/mod.rs new file mode 100644 index 00000000..e6e31c3f --- /dev/null +++ b/matrix_sdk_crypto/src/olm/mod.rs @@ -0,0 +1,208 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod account; +mod group_sessions; +mod session; + +pub use account::{Account, IdentityKeys}; +pub use group_sessions::{GroupSessionKey, InboundGroupSession, OutboundGroupSession}; +pub use session::{OlmMessage, Session}; + +#[cfg(test)] +pub(crate) mod test { + use crate::olm::{Account, InboundGroupSession, Session}; + use matrix_sdk_common::api::r0::keys::SignedKey; + use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId}; + use olm_rs::session::OlmMessage; + use std::collections::BTreeMap; + use std::convert::TryFrom; + + fn alice_id() -> UserId { + UserId::try_from("@alice:example.org").unwrap() + } + + fn alice_device_id() -> Box { + "ALICEDEVICE".into() + } + + fn bob_id() -> UserId { + UserId::try_from("@bob:example.org").unwrap() + } + + fn bob_device_id() -> Box { + "BOBDEVICE".into() + } + + pub(crate) async fn get_account_and_session() -> (Account, Session) { + let alice = Account::new(&alice_id(), &alice_device_id()); + let bob = Account::new(&bob_id(), &bob_device_id()); + + bob.generate_one_time_keys_helper(1).await; + let one_time_key = bob + .one_time_keys() + .await + .curve25519() + .iter() + .next() + .unwrap() + .1 + .to_owned(); + let one_time_key = SignedKey { + key: one_time_key, + signatures: BTreeMap::new(), + }; + let sender_key = bob.identity_keys().curve25519().to_owned(); + let session = alice + .create_outbound_session_helper(&sender_key, &one_time_key) + .await + .unwrap(); + + (alice, session) + } + + #[test] + fn account_creation() { + let account = Account::new(&alice_id(), &alice_device_id()); + let identyty_keys = account.identity_keys(); + + assert!(!account.shared()); + assert!(!identyty_keys.ed25519().is_empty()); + assert_ne!(identyty_keys.values().len(), 0); + assert_ne!(identyty_keys.keys().len(), 0); + assert_ne!(identyty_keys.iter().len(), 0); + assert!(identyty_keys.contains_key("ed25519")); + assert_eq!( + identyty_keys.ed25519(), + identyty_keys.get("ed25519").unwrap() + ); + assert!(!identyty_keys.curve25519().is_empty()); + + account.mark_as_shared(); + assert!(account.shared()); + } + + #[tokio::test] + async fn one_time_keys_creation() { + let account = Account::new(&alice_id(), &alice_device_id()); + let one_time_keys = account.one_time_keys().await; + + assert!(one_time_keys.curve25519().is_empty()); + assert_ne!(account.max_one_time_keys().await, 0); + + account.generate_one_time_keys_helper(10).await; + let one_time_keys = account.one_time_keys().await; + + assert!(!one_time_keys.curve25519().is_empty()); + assert_ne!(one_time_keys.values().len(), 0); + assert_ne!(one_time_keys.keys().len(), 0); + assert_ne!(one_time_keys.iter().len(), 0); + assert!(one_time_keys.contains_key("curve25519")); + assert_eq!(one_time_keys.curve25519().keys().len(), 10); + assert_eq!( + one_time_keys.curve25519(), + one_time_keys.get("curve25519").unwrap() + ); + + account.mark_keys_as_published().await; + let one_time_keys = account.one_time_keys().await; + assert!(one_time_keys.curve25519().is_empty()); + } + + #[tokio::test] + async fn session_creation() { + let alice = Account::new(&alice_id(), &alice_device_id()); + let bob = Account::new(&bob_id(), &bob_device_id()); + let alice_keys = alice.identity_keys(); + alice.generate_one_time_keys_helper(1).await; + let one_time_keys = alice.one_time_keys().await; + alice.mark_keys_as_published().await; + + let one_time_key = one_time_keys + .curve25519() + .iter() + .next() + .unwrap() + .1 + .to_owned(); + + let one_time_key = SignedKey { + key: one_time_key, + signatures: BTreeMap::new(), + }; + + let mut bob_session = bob + .create_outbound_session_helper(alice_keys.curve25519(), &one_time_key) + .await + .unwrap(); + + let plaintext = "Hello world"; + + let message = bob_session.encrypt_helper(plaintext).await; + + let prekey_message = match message.clone() { + OlmMessage::PreKey(m) => m, + OlmMessage::Message(_) => panic!("Incorrect message type"), + }; + + let bob_keys = bob.identity_keys(); + let mut alice_session = alice + .create_inbound_session(bob_keys.curve25519(), prekey_message.clone()) + .await + .unwrap(); + + assert!(alice_session + .matches(bob_keys.curve25519(), prekey_message) + .await + .unwrap()); + + assert_eq!(bob_session.session_id(), alice_session.session_id()); + + let decyrpted = alice_session.decrypt(message).await.unwrap(); + assert_eq!(plaintext, decyrpted); + } + + #[tokio::test] + async fn group_session_creation() { + let alice = Account::new(&alice_id(), &alice_device_id()); + let room_id = RoomId::try_from("!test:localhost").unwrap(); + + let (outbound, _) = alice.create_group_session_pair(&room_id).await; + + assert_eq!(0, outbound.message_index().await); + assert!(!outbound.shared()); + outbound.mark_as_shared(); + assert!(outbound.shared()); + + let inbound = InboundGroupSession::new( + "test_key", + "test_key", + &room_id, + outbound.session_key().await, + ) + .unwrap(); + + assert_eq!(0, inbound.first_known_index().await); + + assert_eq!(outbound.session_id(), inbound.session_id()); + + let plaintext = "This is a secret to everybody".to_owned(); + let ciphertext = outbound.encrypt_helper(plaintext.clone()).await; + + assert_eq!( + plaintext, + inbound.decrypt_helper(ciphertext).await.unwrap().0 + ); + } +} diff --git a/matrix_sdk_crypto/src/olm/session.rs b/matrix_sdk_crypto/src/olm/session.rs new file mode 100644 index 00000000..d6b11de8 --- /dev/null +++ b/matrix_sdk_crypto/src/olm/session.rs @@ -0,0 +1,246 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeMap; +use std::fmt; +use std::sync::Arc; + +use olm_rs::errors::OlmSessionError; +use olm_rs::session::OlmSession; +use olm_rs::PicklingMode; + +use serde_json::{json, Value}; + +pub use olm_rs::{ + session::{OlmMessage, PreKeyMessage}, + utility::OlmUtility, +}; + +use super::IdentityKeys; +use crate::error::{EventError, OlmResult}; +use crate::Device; + +use matrix_sdk_common::{ + api::r0::keys::KeyAlgorithm, + events::{ + room::encrypted::{CiphertextInfo, EncryptedEventContent, OlmV1Curve25519AesSha2Content}, + EventType, + }, + identifiers::{DeviceId, UserId}, + instant::Instant, + locks::Mutex, +}; + +/// Cryptographic session that enables secure communication between two +/// `Account`s +#[derive(Clone)] +pub struct Session { + pub(crate) user_id: Arc, + pub(crate) device_id: Arc>, + pub(crate) our_identity_keys: Arc, + pub(crate) inner: Arc>, + pub(crate) session_id: Arc, + pub(crate) sender_key: Arc, + pub(crate) creation_time: Arc, + pub(crate) last_use_time: Arc, +} + +// #[cfg_attr(tarpaulin, skip)] +impl fmt::Debug for Session { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Session") + .field("session_id", &self.session_id()) + .field("sender_key", &self.sender_key) + .finish() + } +} + +impl Session { + /// Decrypt the given Olm message. + /// + /// Returns the decrypted plaintext or an `OlmSessionError` if decryption + /// failed. + /// + /// # Arguments + /// + /// * `message` - The Olm message that should be decrypted. + pub async fn decrypt(&mut self, message: OlmMessage) -> Result { + let plaintext = self.inner.lock().await.decrypt(message)?; + self.last_use_time = Arc::new(Instant::now()); + Ok(plaintext) + } + + /// Encrypt the given plaintext as a OlmMessage. + /// + /// Returns the encrypted Olm message. + /// + /// # Arguments + /// + /// * `plaintext` - The plaintext that should be encrypted. + pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmMessage { + let message = self.inner.lock().await.encrypt(plaintext); + self.last_use_time = Arc::new(Instant::now()); + message + } + + /// Encrypt the given event content content as an m.room.encrypted event + /// content. + /// + /// # Arguments + /// + /// * `recipient_device` - The device for which this message is going to be + /// encrypted, this needs to be the device that was used to create this + /// session with. + /// + /// * `event_type` - The type of the event. + /// + /// * `content` - The content of the event. + pub async fn encrypt( + &mut self, + recipient_device: &Device, + event_type: EventType, + content: Value, + ) -> OlmResult { + let recipient_signing_key = recipient_device + .get_key(KeyAlgorithm::Ed25519) + .ok_or(EventError::MissingSigningKey)?; + + let payload = json!({ + "sender": self.user_id.as_str(), + "sender_device": self.device_id.as_ref(), + "keys": { + "ed25519": self.our_identity_keys.ed25519(), + }, + "recipient": recipient_device.user_id(), + "recipient_keys": { + "ed25519": recipient_signing_key, + }, + "type": event_type, + "content": content, + }); + + let plaintext = cjson::to_string(&payload) + .unwrap_or_else(|_| panic!(format!("Can't serialize {} to canonical JSON", payload))); + + let ciphertext = self.encrypt_helper(&plaintext).await.to_tuple(); + + let message_type = ciphertext.0; + let ciphertext = CiphertextInfo::new(ciphertext.1, (message_type as u32).into()); + + let mut content = BTreeMap::new(); + content.insert((&*self.sender_key).to_owned(), ciphertext); + + Ok(EncryptedEventContent::OlmV1Curve25519AesSha2( + OlmV1Curve25519AesSha2Content::new( + content, + self.our_identity_keys.curve25519().to_string(), + ), + )) + } + + /// Check if a pre-key Olm message was encrypted for this session. + /// + /// Returns true if it matches, false if not and a OlmSessionError if there + /// was an error checking if it matches. + /// + /// # Arguments + /// + /// * `their_identity_key` - The identity/curve25519 key of the account + /// that encrypted this Olm message. + /// + /// * `message` - The pre-key Olm message that should be checked. + 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) -> &str { + &self.session_id + } + + /// Store the session as a base64 encoded string. + /// + /// # Arguments + /// + /// * `pickle_mode` - The mode that was used to pickle the session, either + /// an unencrypted mode or an encrypted using passphrase. + pub async fn pickle(&self, pickle_mode: PicklingMode) -> String { + self.inner.lock().await.pickle(pickle_mode) + } + + /// Restore a Session from a previously pickled string. + /// + /// Returns the restored Olm Session or a `OlmSessionError` if there was an + /// error. + /// + /// # Arguments + /// + /// * `user_id` - Our own user id that the session belongs to. + /// + /// * `device_id` - Our own device id that the session belongs to. + /// + /// * `our_idenity_keys` - An clone of the Arc to our own identity keys. + /// + /// * `pickle` - The pickled string of the session. + /// + /// * `pickle_mode` - The mode that was used to pickle the session, either + /// an unencrypted mode or an encrypted using passphrase. + /// + /// * `sender_key` - The public curve25519 key of the account that + /// established the session with us. + /// + /// * `creation_time` - The timestamp that marks when the session was + /// created. + /// + /// * `last_use_time` - The timestamp that marks when the session was + /// last used to encrypt or decrypt an Olm message. + #[allow(clippy::too_many_arguments)] + pub fn from_pickle( + user_id: Arc, + device_id: Arc>, + our_identity_keys: Arc, + pickle: String, + pickle_mode: PicklingMode, + sender_key: String, + creation_time: Instant, + last_use_time: Instant, + ) -> Result { + let session = OlmSession::unpickle(pickle, pickle_mode)?; + let session_id = session.session_id(); + + Ok(Session { + user_id, + device_id, + our_identity_keys, + 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), + }) + } +} + +impl PartialEq for Session { + fn eq(&self, other: &Self) -> bool { + self.session_id() == other.session_id() + } +} diff --git a/matrix_sdk_crypto/src/store/memorystore.rs b/matrix_sdk_crypto/src/store/memorystore.rs index ccef2efd..a911320f 100644 --- a/matrix_sdk_crypto/src/store/memorystore.rs +++ b/matrix_sdk_crypto/src/store/memorystore.rs @@ -200,8 +200,8 @@ mod test { let user_devices = store.get_user_devices(device.user_id()).await.unwrap(); - assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id()); - assert_eq!(user_devices.devices().nth(0).unwrap(), &device); + assert_eq!(user_devices.keys().next().unwrap(), device.device_id()); + assert_eq!(user_devices.devices().next().unwrap(), &device); let loaded_device = user_devices.get(device.device_id()).unwrap(); diff --git a/matrix_sdk_crypto/src/store/mod.rs b/matrix_sdk_crypto/src/store/mod.rs index 7a690ce2..2e40503e 100644 --- a/matrix_sdk_crypto/src/store/mod.rs +++ b/matrix_sdk_crypto/src/store/mod.rs @@ -87,7 +87,7 @@ pub enum CryptoStoreError { pub type Result = std::result::Result; #[async_trait] -#[warn(clippy::type_complexity)] +#[allow(clippy::type_complexity)] #[cfg_attr(not(target_arch = "wasm32"), send_sync)] /// Trait abstracting a store that the `OlmMachine` uses to store cryptographic /// keys. diff --git a/matrix_sdk_crypto/src/store/sqlite.rs b/matrix_sdk_crypto/src/store/sqlite.rs index 2287afe9..ba545a0c 100644 --- a/matrix_sdk_crypto/src/store/sqlite.rs +++ b/matrix_sdk_crypto/src/store/sqlite.rs @@ -26,9 +26,10 @@ use olm_rs::PicklingMode; use sqlx::{query, query_as, sqlite::SqliteQueryAs, Connect, Executor, SqliteConnection}; use zeroize::Zeroizing; -use super::{Account, CryptoStore, CryptoStoreError, InboundGroupSession, Result, Session}; +use super::{CryptoStore, CryptoStoreError, Result}; use crate::device::{Device, TrustState}; use crate::memory_stores::{DeviceStore, GroupSessionStore, SessionStore, UserDevices}; +use crate::{Account, IdentityKeys, InboundGroupSession, Session}; use matrix_sdk_common::api::r0::keys::{AlgorithmAndDeviceId, KeyAlgorithm}; use matrix_sdk_common::events::Algorithm; use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId}; @@ -36,8 +37,8 @@ use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId}; /// SQLite based implementation of a `CryptoStore`. pub struct SqliteStore { user_id: Arc, - device_id: Arc, - account_id: Option, + device_id: Arc>, + account_info: Option, path: PathBuf, sessions: SessionStore, @@ -50,6 +51,11 @@ pub struct SqliteStore { pickle_passphrase: Option>, } +struct AccountInfo { + account_id: i64, + identity_keys: Arc, +} + static DATABASE_NAME: &str = "matrix-sdk-crypto.db"; impl SqliteStore { @@ -66,7 +72,7 @@ impl SqliteStore { /// * `path` - The path where the database file should reside in. pub async fn open>( user_id: &UserId, - device_id: &str, + device_id: &DeviceId, path: P, ) -> Result { SqliteStore::open_helper(user_id, device_id, path, None).await @@ -88,7 +94,7 @@ impl SqliteStore { /// the encryption keys. pub async fn open_with_passphrase>( user_id: &UserId, - device_id: &str, + device_id: &DeviceId, path: P, passphrase: &str, ) -> Result { @@ -109,7 +115,7 @@ impl SqliteStore { async fn open_helper>( user_id: &UserId, - device_id: &str, + device_id: &DeviceId, path: P, passphrase: Option>, ) -> Result { @@ -118,8 +124,8 @@ impl SqliteStore { let connection = SqliteConnection::connect(url.as_ref()).await?; let store = SqliteStore { user_id: Arc::new(user_id.to_owned()), - device_id: Arc::new(device_id.to_owned()), - account_id: None, + device_id: Arc::new(device_id.into()), + account_info: None, sessions: SessionStore::new(), inbound_group_sessions: GroupSessionStore::new(), devices: DeviceStore::new(), @@ -133,6 +139,10 @@ impl SqliteStore { Ok(store) } + fn account_id(&self) -> Option { + self.account_info.as_ref().map(|i| i.account_id) + } + async fn create_tables(&self) -> Result<()> { let mut connection = self.connection.lock().await; connection @@ -262,6 +272,25 @@ impl SqliteStore { ) .await?; + connection + .execute( + r#" + CREATE TABLE IF NOT EXISTS device_signatures ( + "id" INTEGER NOT NULL PRIMARY KEY, + "device_id" INTEGER NOT NULL, + "user_id" TEXT NOT NULL, + "key_algorithm" TEXT NOT NULL, + "signature" TEXT NOT NULL, + FOREIGN KEY ("device_id") REFERENCES "devices" ("id") + ON DELETE CASCADE + UNIQUE(device_id, user_id, key_algorithm) + ); + + CREATE INDEX IF NOT EXISTS "device_keys_device_id" ON "device_keys" ("device_id"); + "#, + ) + .await?; + Ok(()) } @@ -288,14 +317,17 @@ impl SqliteStore { } async fn load_sessions_for(&mut self, sender_key: &str) -> Result> { - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let account_info = self + .account_info + .as_ref() + .ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; let rows: Vec<(String, String, String, String)> = query_as( "SELECT pickle, sender_key, creation_time, last_use_time FROM sessions WHERE account_id = ? and sender_key = ?", ) - .bind(account_id) + .bind(account_info.account_id) .bind(sender_key) .fetch_all(&mut *connection) .await?; @@ -315,6 +347,9 @@ impl SqliteStore { .ok_or(CryptoStoreError::SessionTimestampError)?; Ok(Session::from_pickle( + self.user_id.clone(), + self.device_id.clone(), + account_info.identity_keys.clone(), pickle.to_string(), self.get_pickle_mode(), sender_key.to_string(), @@ -326,7 +361,7 @@ impl SqliteStore { } async fn load_inbound_group_sessions(&self) -> Result> { - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; let rows: Vec<(String, String, String, String)> = query_as( @@ -357,7 +392,7 @@ impl SqliteStore { } async fn save_tracked_user(&self, user: &UserId, dirty: bool) -> Result<()> { - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; query( @@ -378,7 +413,7 @@ impl SqliteStore { } async fn load_tracked_users(&self) -> Result<(HashSet, HashSet)> { - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; let rows: Vec<(String, bool)> = query_as( @@ -410,7 +445,7 @@ impl SqliteStore { } async fn load_devices(&self) -> Result { - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; let rows: Vec<(i64, String, String, Option, i64)> = query_as( @@ -456,31 +491,64 @@ impl SqliteStore { .fetch_all(&mut *connection) .await?; - let mut keys = BTreeMap::new(); + let keys: BTreeMap = key_rows + .into_iter() + .filter_map(|row| { + let algorithm = KeyAlgorithm::try_from(&*row.0).ok()?; + let key = row.1; - for row in key_rows { - let algorithm: &str = &row.0; - let algorithm = if let Ok(a) = KeyAlgorithm::try_from(algorithm) { - a + Some(( + AlgorithmAndDeviceId(algorithm, device_id.as_str().into()), + key, + )) + }) + .collect(); + + let signature_rows: Vec<(String, String, String)> = query_as( + "SELECT user_id, key_algorithm, signature + FROM device_signatures WHERE device_id = ?", + ) + .bind(device_row_id) + .fetch_all(&mut *connection) + .await?; + + let mut signatures: BTreeMap> = + BTreeMap::new(); + + for row in signature_rows { + let user_id = if let Ok(u) = UserId::try_from(&*row.0) { + u } else { continue; }; - let key = &row.1; + let key_algorithm = if let Ok(k) = KeyAlgorithm::try_from(&*row.1) { + k + } else { + continue; + }; - keys.insert( - AlgorithmAndDeviceId(algorithm, device_id.clone()), - key.to_owned(), + let signature = row.2; + + if !signatures.contains_key(&user_id) { + let _ = signatures.insert(user_id.clone(), BTreeMap::new()); + } + let user_map = signatures.get_mut(&user_id).unwrap(); + + user_map.insert( + AlgorithmAndDeviceId(key_algorithm, device_id.as_str().into()), + signature.to_owned(), ); } let device = Device::new( user_id, - device_id.to_owned(), + device_id.as_str().into(), display_name.clone(), trust_state, algorithms, keys, + signatures, ); store.add(device); @@ -490,7 +558,7 @@ impl SqliteStore { } async fn save_device_helper(&self, device: Device) -> Result<()> { - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; @@ -550,6 +618,23 @@ impl SqliteStore { .await?; } + for (user_id, signature_map) in device.signatures() { + for (key_id, signature) in signature_map { + query( + "INSERT OR IGNORE INTO device_signatures ( + device_id, user_id, key_algorithm, signature + ) VALUES (?1, ?2, ?3, ?4) + ", + ) + .bind(device_row_id) + .bind(user_id.as_str()) + .bind(key_id.0.to_string()) + .bind(signature) + .execute(&mut *connection) + .await?; + } + } + Ok(()) } @@ -573,20 +658,26 @@ impl CryptoStore for SqliteStore { WHERE user_id = ? and device_id = ?", ) .bind(self.user_id.as_str()) - .bind(&*self.device_id) + .bind((&*self.device_id).as_ref()) .fetch_optional(&mut *connection) .await?; let result = if let Some((id, pickle, shared, uploaded_key_count)) = row { - self.account_id = Some(id); - Some(Account::from_pickle( + let account = Account::from_pickle( pickle, self.get_pickle_mode(), shared, uploaded_key_count, &self.user_id, &self.device_id, - )?) + )?; + + self.account_info = Some(AccountInfo { + account_id: id, + identity_keys: account.identity_keys.clone(), + }); + + Some(account) } else { return Ok(None); }; @@ -640,19 +731,22 @@ impl CryptoStore for SqliteStore { .fetch_one(&mut *connection) .await?; - self.account_id = Some(account_id.0); + self.account_info = Some(AccountInfo { + account_id: account_id.0, + identity_keys: account.identity_keys.clone(), + }); Ok(()) } async fn save_sessions(&mut self, sessions: &[Session]) -> Result<()> { + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; + // TODO turn this into a transaction for session in sessions { self.lazy_load_sessions(&session.sender_key).await?; self.sessions.add(session.clone()).await; - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; - 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())?; @@ -683,7 +777,7 @@ impl CryptoStore for SqliteStore { } async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result { - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let pickle = session.pickle(self.get_pickle_mode()).await; let mut connection = self.connection.lock().await; let session_id = session.session_id(); @@ -753,7 +847,7 @@ impl CryptoStore for SqliteStore { } async fn delete_device(&self, device: Device) -> Result<()> { - let account_id = self.account_id.ok_or(CryptoStoreError::AccountUnset)?; + let account_id = self.account_id().ok_or(CryptoStoreError::AccountUnset)?; let mut connection = self.connection.lock().await; query( @@ -804,7 +898,7 @@ mod test { use super::{Account, CryptoStore, InboundGroupSession, RoomId, Session, SqliteStore, TryFrom}; static USER_ID: &str = "@example:localhost"; - static DEVICE_ID: &str = "DEVICEID"; + static DEVICE_ID: &DeviceId = "DEVICEID"; async fn get_store(passphrase: Option<&str>) -> (SqliteStore, tempfile::TempDir) { let tmpdir = tempdir().unwrap(); @@ -840,16 +934,16 @@ mod test { UserId::try_from("@alice:example.org").unwrap() } - fn alice_device_id() -> DeviceId { - "ALICEDEVICE".to_string() + fn alice_device_id() -> Box { + "ALICEDEVICE".into() } fn bob_id() -> UserId { UserId::try_from("@bob:example.org").unwrap() } - fn bob_device_id() -> DeviceId { - "BOBDEVICE".to_string() + fn bob_device_id() -> Box { + "BOBDEVICE".into() } fn get_account() -> Account { @@ -866,7 +960,7 @@ mod test { .await .curve25519() .iter() - .nth(0) + .next() .unwrap() .1 .to_owned(); @@ -876,7 +970,7 @@ mod test { }; let sender_key = bob.identity_keys().curve25519().to_owned(); let session = alice - .create_outbound_session(&sender_key, &one_time_key) + .create_outbound_session_helper(&sender_key, &one_time_key) .await .unwrap(); @@ -1165,8 +1259,8 @@ mod test { assert_eq!(device.keys(), loaded_device.keys()); let user_devices = store.get_user_devices(device.user_id()).await.unwrap(); - assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id()); - assert_eq!(user_devices.devices().nth(0).unwrap(), &device); + assert_eq!(user_devices.keys().next().unwrap(), device.device_id()); + assert_eq!(user_devices.devices().next().unwrap(), &device); } #[tokio::test] diff --git a/matrix_sdk_crypto/src/verification/sas.rs b/matrix_sdk_crypto/src/verification/sas.rs index 01d07380..475e3e63 100644 --- a/matrix_sdk_crypto/src/verification/sas.rs +++ b/matrix_sdk_crypto/src/verification/sas.rs @@ -1,8 +1,8 @@ use crate::Device; use matrix_sdk_common::events::key::verification::{ - start::{StartEvent, StartEventContent}, accept::AcceptEvent, + start::{StartEvent, StartEventContent}, HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, ShortAuthenticationString, VerificationMethod, }; @@ -11,7 +11,7 @@ use matrix_sdk_common::uuid::Uuid; struct SasIds { own_user_id: UserId, - own_device_id: DeviceId, + own_device_id: Box, other_device: Device, } @@ -27,7 +27,7 @@ struct AcceptedProtocols { key_agreement_protocol: KeyAgreementProtocol, hash: HashAlgorithm, message_auth_code: MessageAuthenticationCode, - short_auth_string: Vec + short_auth_string: Vec, } struct Sas { @@ -38,11 +38,11 @@ struct Sas { } impl Sas { - fn new(own_user_id: UserId, own_device_id: DeviceId, other_device: Device) -> Sas { + fn new(own_user_id: UserId, own_device_id: &DeviceId, other_device: Device) -> Sas { Sas { ids: SasIds { own_user_id, - own_device_id, + own_device_id: own_device_id.into(), other_device, }, verification_flow_id: Uuid::new_v4(), @@ -71,12 +71,12 @@ impl Sas { state: Accepted { commitment: content.commitment.clone(), accepted_protocols: AcceptedProtocols { - method: content.method, - hash: content.hash, - key_agreement_protocol: content.key_agreement_protocol, - message_auth_code: content.message_authentication_code, - short_auth_string: content.short_authentication_string.clone(), - } + method: content.method, + hash: content.hash, + key_agreement_protocol: content.key_agreement_protocol, + message_auth_code: content.message_authentication_code, + short_auth_string: content.short_authentication_string.clone(), + }, }, } } @@ -89,7 +89,7 @@ struct Started {} impl Sas { fn from_start_event( own_user_id: UserId, - own_device_id: DeviceId, + own_device_id: &DeviceId, other_device: Device, event: &StartEvent, ) -> Sas { @@ -102,7 +102,7 @@ impl Sas { Sas { ids: SasIds { own_user_id, - own_device_id, + own_device_id: own_device_id.into(), other_device, }, verification_flow_id: Uuid::new_v4(), diff --git a/matrix_sdk_test/Cargo.toml b/matrix_sdk_test/Cargo.toml index 07a0c8a6..17ca6f84 100644 --- a/matrix_sdk_test/Cargo.toml +++ b/matrix_sdk_test/Cargo.toml @@ -16,4 +16,4 @@ http = "0.2.1" matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } matrix-sdk-test-macros = { version = "0.1.0", path = "../matrix_sdk_test_macros" } lazy_static = "1.4.0" -serde = "1.0.111" +serde = "1.0.114" diff --git a/matrix_sdk_test/src/lib.rs b/matrix_sdk_test/src/lib.rs index 2538ce77..2a33ce3a 100644 --- a/matrix_sdk_test/src/lib.rs +++ b/matrix_sdk_test/src/lib.rs @@ -6,8 +6,8 @@ use http::Response; use matrix_sdk_common::api::r0::sync::sync_events::Response as SyncResponse; use matrix_sdk_common::events::{ - presence::PresenceEvent, AnyBasicEvent, AnyEphemeralRoomEventStub, AnyRoomEventStub, - AnyStateEventStub, + presence::PresenceEvent, AnyBasicEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent, + AnySyncStateEvent, }; use matrix_sdk_common::identifiers::RoomId; use serde_json::Value as JsonValue; @@ -26,6 +26,7 @@ pub enum EventsJson { HistoryVisibility, JoinRules, Member, + MemberNameChange, MessageEmote, MessageNotice, MessageText, @@ -42,21 +43,54 @@ pub enum EventsJson { Typing, } -/// Easily create events to stream into either a Client or a `Room` for testing. +/// The `EventBuilder` struct can be used to easily generate valid sync responses for testing. +/// These can be then fed into either `Client` or `Room`. +/// +/// It supports generated a number of canned events, such as a member entering a room, his power +/// level and display name changing and similar. It also supports insertion of custom events in the +/// form of `EventsJson` values. +/// +/// **Important** You *must* use the *same* builder when sending multiple sync responses to +/// a single client. Otherwise, the subsequent responses will be *ignored* by the client because +/// the `next_batch` sync token will not be rotated properly. +/// +/// # Example usage +/// +/// ```rust +/// use matrix_sdk_test::{EventBuilder, EventsJson}; +/// +/// let mut builder = EventBuilder::new(); +/// +/// // response1 now contains events that add an example member to the room and change their power +/// // level +/// let response1 = builder +/// .add_room_event(EventsJson::Member) +/// .add_room_event(EventsJson::PowerLevels) +/// .build_sync_response(); +/// +/// // response2 is now empty (nothing changed) +/// let response2 = builder.build_sync_response(); +/// +/// // response3 contains a display name change for member example +/// let response3 = builder +/// .add_room_event(EventsJson::MemberNameChange) +/// .build_sync_response(); +/// ``` + #[derive(Default)] pub struct EventBuilder { /// The events that determine the state of a `Room`. - joined_room_events: HashMap>, + joined_room_events: HashMap>, /// The events that determine the state of a `Room`. - invited_room_events: HashMap>, + invited_room_events: HashMap>, /// The events that determine the state of a `Room`. - left_room_events: HashMap>, + left_room_events: HashMap>, /// The presence events that determine the presence state of a `RoomMember`. presence_events: Vec, /// The state events that determine the state of a `Room`. - state_events: Vec, + state_events: Vec, /// The ephemeral room events that determine the state of a `Room`. - ephemeral: Vec, + ephemeral: Vec, /// The account data events that determine the state of a `Room`. account_data: Vec, /// Internal counter to enable the `prev_batch` and `next_batch` of each sync response to vary. @@ -76,7 +110,7 @@ impl EventBuilder { _ => panic!("unknown ephemeral event {:?}", json), }; - let event = serde_json::from_value::(val.clone()).unwrap(); + let event = serde_json::from_value::(val.clone()).unwrap(); self.ephemeral.push(event); self } @@ -97,11 +131,12 @@ impl EventBuilder { pub fn add_room_event(&mut self, json: EventsJson) -> &mut Self { let val: &JsonValue = match json { EventsJson::Member => &test_json::MEMBER, + EventsJson::MemberNameChange => &test_json::MEMBER_NAME_CHANGE, EventsJson::PowerLevels => &test_json::POWER_LEVELS, _ => panic!("unknown room event json {:?}", json), }; - let event = serde_json::from_value::(val.clone()).unwrap(); + let event = serde_json::from_value::(val.clone()).unwrap(); self.add_joined_event( &RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap(), @@ -115,12 +150,12 @@ impl EventBuilder { room_id: &RoomId, event: serde_json::Value, ) -> &mut Self { - let event = serde_json::from_value::(event).unwrap(); + let event = serde_json::from_value::(event).unwrap(); self.add_joined_event(room_id, event); self } - fn add_joined_event(&mut self, room_id: &RoomId, event: AnyRoomEventStub) { + fn add_joined_event(&mut self, room_id: &RoomId, event: AnySyncRoomEvent) { self.joined_room_events .entry(room_id.clone()) .or_insert_with(Vec::new) @@ -132,7 +167,7 @@ impl EventBuilder { room_id: &RoomId, event: serde_json::Value, ) -> &mut Self { - let event = serde_json::from_value::(event).unwrap(); + let event = serde_json::from_value::(event).unwrap(); self.invited_room_events .entry(room_id.clone()) .or_insert_with(Vec::new) @@ -145,7 +180,7 @@ impl EventBuilder { room_id: &RoomId, event: serde_json::Value, ) -> &mut Self { - let event = serde_json::from_value::(event).unwrap(); + let event = serde_json::from_value::(event).unwrap(); self.left_room_events .entry(room_id.clone()) .or_insert_with(Vec::new) @@ -164,7 +199,7 @@ impl EventBuilder { _ => panic!("unknown state event {:?}", json), }; - let event = serde_json::from_value::(val.clone()).unwrap(); + let event = serde_json::from_value::(val.clone()).unwrap(); self.state_events.push(event); self } @@ -181,7 +216,8 @@ impl EventBuilder { self } - /// Consumes `ResponseBuilder` and returns `SyncResponse`. + /// Builds a `SyncResponse` containing the events we queued so far. The next response returned + /// by `build_sync_response` will then be empty if no further events were queued. pub fn build_sync_response(&mut self) -> SyncResponse { let main_room_id = RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap(); @@ -293,12 +329,26 @@ impl EventBuilder { let response = Response::builder() .body(serde_json::to_vec(&body).unwrap()) .unwrap(); + + // Clear state so that the next sync response will be empty if nothing was added. + self.clear(); + SyncResponse::try_from(response).unwrap() } fn generate_sync_token(&self) -> String { format!("t392-516_47314_0_7_1_1_1_11444_{}", self.batch_counter) } + + pub fn clear(&mut self) { + self.account_data.clear(); + self.ephemeral.clear(); + self.invited_room_events.clear(); + self.joined_room_events.clear(); + self.left_room_events.clear(); + self.presence_events.clear(); + self.state_events.clear(); + } } /// Embedded sync reponse files diff --git a/matrix_sdk_test/src/test_json/events.rs b/matrix_sdk_test/src/test_json/events.rs index a4ba0ced..da3bc5a4 100644 --- a/matrix_sdk_test/src/test_json/events.rs +++ b/matrix_sdk_test/src/test_json/events.rs @@ -208,6 +208,7 @@ lazy_static! { }); } +// TODO: Move `prev_content` into `unsigned` once ruma supports it lazy_static! { pub static ref MEMBER: JsonValue = json!({ "content": { @@ -221,14 +222,40 @@ lazy_static! { "sender": "@example:localhost", "state_key": "@example:localhost", "type": "m.room.member", + "prev_content": { + "avatar_url": null, + "displayname": "example", + "membership": "invite" + }, "unsigned": { "age": 297036, - "replaces_state": "$151800111315tsynI:localhost", - "prev_content": { - "avatar_url": null, - "displayname": "example", - "membership": "invite" - } + "replaces_state": "$151800111315tsynI:localhost" + } + }); +} + +// TODO: Move `prev_content` into `unsigned` once ruma supports it +lazy_static! { + pub static ref MEMBER_NAME_CHANGE: JsonValue = json!({ + "content": { + "avatar_url": null, + "displayname": "changed", + "membership": "join" + }, + "event_id": "$151800234427abgho:localhost", + "membership": "join", + "origin_server_ts": 151800152, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "m.room.member", + "prev_content": { + "avatar_url": null, + "displayname": "example", + "membership": "join" + }, + "unsigned": { + "age": 297032, + "replaces_state": "$151800140517rfvjc:localhost" } }); } @@ -552,6 +579,7 @@ lazy_static! { }); } +// TODO: Move `prev_content` into `unsigned` once ruma supports it lazy_static! { pub static ref TOPIC: JsonValue = json!({ "content": { @@ -562,11 +590,11 @@ lazy_static! { "sender": "@example:localhost", "state_key": "", "type": "m.room.topic", + "prev_content": { + "topic": "test" + }, "unsigned": { "age": 1392989, - "prev_content": { - "topic": "test" - }, "prev_sender": "@example:localhost", "replaces_state": "$151957069225EVYKm:localhost" } diff --git a/matrix_sdk_test/src/test_json/mod.rs b/matrix_sdk_test/src/test_json/mod.rs index 474a397f..b85e0bad 100644 --- a/matrix_sdk_test/src/test_json/mod.rs +++ b/matrix_sdk_test/src/test_json/mod.rs @@ -9,8 +9,8 @@ pub mod sync; pub use events::{ ALIAS, ALIASES, EVENT_ID, KEYS_QUERY, KEYS_UPLOAD, LOGIN, LOGIN_RESPONSE_ERR, LOGOUT, MEMBER, - MESSAGE_EDIT, MESSAGE_TEXT, NAME, POWER_LEVELS, PRESENCE, PUBLIC_ROOMS, REACTION, REDACTED, - REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR, ROOM_ID, ROOM_MESSAGES, - TYPING, + MEMBER_NAME_CHANGE, MESSAGE_EDIT, MESSAGE_TEXT, NAME, POWER_LEVELS, PRESENCE, PUBLIC_ROOMS, + REACTION, REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR, + ROOM_ID, ROOM_MESSAGES, TYPING, }; pub use sync::{DEFAULT_SYNC_SUMMARY, INVITE_SYNC, LEAVE_SYNC, LEAVE_SYNC_EVENT, MORE_SYNC, SYNC};