diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 1e6cd3ea9..60b5ce570 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -55,7 +55,10 @@ ruma = { version = "0.6.1", features = ["client-api-c", "signatures"] } [dev-dependencies] futures = { version = "0.3.21", default-features = false, features = ["executor"] } +tracing = { version = "0.1.26", features = ["log"] } http = "0.2.6" +assign = "1.1.1" +env_logger = "0.9.0" matrix-sdk-test = { version = "0.4.0", path = "../matrix-sdk-test" } tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] } diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index aa2ec71db..fac873aee 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1301,12 +1301,15 @@ impl Default for BaseClient { #[cfg(test)] mod tests { - use matrix_sdk_test::{async_test, EventBuilder}; - use ruma::{room_id, user_id}; + use matrix_sdk_test::{async_test, response_from_file, EventBuilder}; + use ruma::{ + api::{client as api, IncomingResponse}, + room_id, user_id, + }; use serde_json::json; use super::BaseClient; - use crate::{RoomType, Session}; + use crate::{DisplayName, RoomType, Session}; #[async_test] async fn invite_after_leaving() { @@ -1363,4 +1366,100 @@ mod tests { client.receive_sync_response(response).await.unwrap(); assert_eq!(client.get_room(room_id).unwrap().room_type(), RoomType::Invited); } + + #[async_test] + async fn invite_displayname_integration_test() { + let user_id = user_id!("@alice:example.org"); + let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org"); + + let client = BaseClient::new(); + client + .restore_login(Session { + access_token: "token".to_owned(), + user_id: user_id.to_owned(), + device_id: "FOOBAR".into(), + }) + .await + .unwrap(); + + let response = api::sync::sync_events::v3::Response::try_from_http_response(response_from_file(&json!({ + "next_batch": "asdkl;fjasdkl;fj;asdkl;f", + "device_one_time_keys_count": { + "signed_curve25519": 50u64 + }, + "device_unused_fallback_key_types": [ + "signed_curve25519" + ], + "rooms": { + "invite": { + "!ithpyNKDtmhneaTQja:example.org": { + "invite_state": { + "events": [ + { + "content": { + "creator": "@test:example.org", + "room_version": "9" + }, + "sender": "@test:example.org", + "state_key": "", + "type": "m.room.create" + }, + { + "content": { + "join_rule": "invite" + }, + "sender": "@test:example.org", + "state_key": "", + "type": "m.room.join_rules" + }, + { + "content": { + "algorithm": "m.megolm.v1.aes-sha2" + }, + "sender": "@test:example.org", + "state_key": "", + "type": "m.room.encryption" + }, + { + "content": { + "avatar_url": "mxc://example.org/dcBBDwuWEUrjfrOchvkirUST", + "displayname": "Kyra", + "membership": "join" + }, + "sender": "@test:example.org", + "state_key": "@test:example.org", + "type": "m.room.member" + }, + { + "content": { + "avatar_url": "mxc://example.org/ABFEXSDrESxovWwEnCYdNcHT", + "displayname": "alice", + "is_direct": true, + "membership": "invite" + }, + "origin_server_ts": 1650878657984u64, + "sender": "@test:example.org", + "state_key": "@alice:example.org", + "type": "m.room.member", + "unsigned": { + "age": 14u64 + }, + "event_id": "$fLDqltg9Puj-kWItLSFVHPGN4YkgpYQf2qImPzdmgrE" + } + ] + } + } + } + } + }))).expect("static json doesn't fail to parse"); + + client.receive_sync_response(response).await.unwrap(); + + let room = client.get_room(room_id).expect("Room not found"); + assert_eq!(room.room_type(), RoomType::Invited); + assert_eq!( + room.display_name().await.expect("fetching display name failed"), + DisplayName::Calculated("Kyra".to_owned()) + ); + } } diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 255b17b9d..12a825978 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -40,5 +40,5 @@ pub use client::BaseClient; pub use http; #[cfg(feature = "e2e-encryption")] pub use matrix_sdk_crypto as crypto; -pub use rooms::{Room, RoomInfo, RoomMember, RoomType}; +pub use rooms::{DisplayName, Room, RoomInfo, RoomMember, RoomType}; pub use store::{StateChanges, StateStore, Store, StoreError}; diff --git a/crates/matrix-sdk-base/src/rooms/members.rs b/crates/matrix-sdk-base/src/rooms/members.rs index 62a7333bd..239f13ca0 100644 --- a/crates/matrix-sdk-base/src/rooms/members.rs +++ b/crates/matrix-sdk-base/src/rooms/members.rs @@ -18,17 +18,19 @@ use ruma::{ events::{ presence::PresenceEvent, room::{ - member::{MembershipState, OriginalSyncRoomMemberEvent, RoomMemberEventContent}, + member::{MembershipState, RoomMemberEventContent}, power_levels::SyncRoomPowerLevelsEvent, }, }, MxcUri, UserId, }; +use crate::deserialized_responses::MemberEvent; + /// A member of a room. #[derive(Clone, Debug)] pub struct RoomMember { - pub(crate) event: Arc, + pub(crate) event: Arc, pub(crate) profile: Arc>, #[allow(dead_code)] pub(crate) presence: Arc>, @@ -41,7 +43,7 @@ pub struct RoomMember { impl RoomMember { /// Get the unique user id of this member. pub fn user_id(&self) -> &UserId { - &self.event.state_key + self.event.user_id() } /// Get the display name of the member if there is one. @@ -49,7 +51,7 @@ impl RoomMember { if let Some(p) = self.profile.as_ref() { p.displayname.as_deref() } else { - self.event.content.displayname.as_deref() + self.event.content().displayname.as_deref() } } @@ -69,7 +71,7 @@ impl RoomMember { pub fn avatar_url(&self) -> Option<&MxcUri> { match self.profile.as_ref() { Some(p) => p.avatar_url.as_deref(), - None => self.event.content.avatar_url.as_deref(), + None => self.event.content().avatar_url.as_deref(), } } @@ -112,7 +114,7 @@ impl RoomMember { if let Some(p) = self.profile.as_ref() { &p.membership } else { - &self.event.content.membership + &self.event.content().membership } } } diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index bef5bfd36..cd596f689 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -1,7 +1,7 @@ mod members; mod normal; -use std::{cmp::max, collections::HashSet}; +use std::{cmp::max, collections::HashSet, fmt}; pub use members::RoomMember; pub use normal::{Room, RoomInfo, RoomType}; @@ -18,6 +18,36 @@ use ruma::{ }; use serde::{Deserialize, Serialize}; +/// The name of the room, either from the metadata or calculaetd +/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room) +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum DisplayName { + /// The room has been named explicitly as + Named(String), + /// The room has a canonical alias that should be used + Aliased(String), + /// The room has not given an explicit name but a name could be + /// calculated + Calculated(String), + /// The room doesn't have a name right now, but used to have one + /// e.g. because it was a DM and everyone has left the room + EmptyWas(String), + /// No useful name could be calculated or ever found + Empty, +} + +impl fmt::Display for DisplayName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => { + write!(f, "{}", s) + } + DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {})", s), + DisplayName::Empty => write!(f, "Empty Room"), + } + } +} + /// A base room info struct that is the backbone of normal as well as stripped /// rooms. Holds all the state events that are important to present a room to /// users. @@ -61,7 +91,7 @@ impl BaseRoomInfo { joined_member_count: u64, invited_member_count: u64, heroes: Vec, - ) -> String { + ) -> DisplayName { calculate_room_name( joined_member_count, invited_member_count, @@ -190,14 +220,12 @@ impl Default for BaseRoomInfo { } } -/// Calculate room name according to step 3 of the [naming algorithm.][spec] -/// -/// [spec]: +/// Calculate room name according to step 3 of the [naming algorithm.] fn calculate_room_name( joined_member_count: u64, invited_member_count: u64, heroes: Vec<&str>, -) -> String { +) -> DisplayName { let heroes_count = heroes.len() as u64; let invited_joined = invited_member_count + joined_member_count; let invited_joined_minus_one = invited_joined.saturating_sub(1); @@ -221,12 +249,12 @@ fn calculate_room_name( // User is alone. if invited_joined <= 1 { if names.is_empty() { - "Empty room".to_owned() + DisplayName::Empty } else { - format!("Empty room (was {})", names) + DisplayName::EmptyWas(names) } } else { - names + DisplayName::Calculated(names) } } @@ -237,33 +265,33 @@ mod tests { fn test_calculate_room_name() { let mut actual = calculate_room_name(2, 0, vec!["a"]); - assert_eq!("a", actual); + assert_eq!(DisplayName::Calculated("a".to_owned()), actual); actual = calculate_room_name(3, 0, vec!["a", "b"]); - assert_eq!("a, b", actual); + assert_eq!(DisplayName::Calculated("a, b".to_owned()), actual); actual = calculate_room_name(4, 0, vec!["a", "b", "c"]); - assert_eq!("a, b, c", actual); + assert_eq!(DisplayName::Calculated("a, b, c".to_owned()), actual); actual = calculate_room_name(5, 0, vec!["a", "b", "c"]); - assert_eq!("a, b, c, and 2 others", actual); + assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual); actual = calculate_room_name(0, 0, vec![]); - assert_eq!("Empty room", actual); + assert_eq!(DisplayName::Empty, actual); actual = calculate_room_name(1, 0, vec![]); - assert_eq!("Empty room", actual); + assert_eq!(DisplayName::Empty, actual); actual = calculate_room_name(0, 1, vec![]); - assert_eq!("Empty room", actual); + assert_eq!(DisplayName::Empty, actual); actual = calculate_room_name(1, 0, vec!["a"]); - assert_eq!("Empty room (was a)", actual); + assert_eq!(DisplayName::EmptyWas("a".to_owned()), actual); actual = calculate_room_name(1, 0, vec!["a", "b"]); - assert_eq!("Empty room (was a, b)", actual); + assert_eq!(DisplayName::EmptyWas("a, b".to_owned()), actual); actual = calculate_room_name(1, 0, vec!["a", "b", "c"]); - assert_eq!("Empty room (was a, b, c)", actual); + assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual); } } diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 49a45f380..215636ba7 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -46,7 +46,7 @@ use ruma::{ }; use serde::{Deserialize, Serialize}; -use super::{BaseRoomInfo, RoomMember}; +use super::{BaseRoomInfo, DisplayName, RoomMember}; use crate::{ deserialized_responses::UnreadNotificationsCount, store::{Result as StoreResult, StateStore}, @@ -276,7 +276,7 @@ impl Room { /// The display name is calculated according to [this algorithm][spec]. /// /// [spec]: - pub async fn display_name(&self) -> StoreResult { + pub async fn display_name(&self) -> StoreResult { self.calculate_name().await } @@ -338,24 +338,19 @@ impl Room { Ok(members) } - async fn calculate_name(&self) -> StoreResult { + async fn calculate_name(&self) -> StoreResult { let summary = { let inner = self.inner.read().unwrap(); if let Some(name) = &inner.base_info.name { let name = name.trim(); - return Ok(name.to_owned()); + return Ok(DisplayName::Named(name.to_owned())); } else if let Some(alias) = &inner.base_info.canonical_alias { let alias = alias.alias().trim(); - return Ok(alias.to_owned()); + return Ok(DisplayName::Aliased(alias.to_owned())); } inner.summary.clone() }; - // TODO what should we do here? We have correct counts only if lazy - // loading is used. - let joined = summary.joined_member_count; - let invited = summary.invited_member_count; - let heroes_count = summary.heroes.len() as u64; let is_own_member = |m: &RoomMember| m.user_id() == &*self.own_user_id; let is_own_user_id = |u: &str| u == self.own_user_id().as_str(); @@ -377,11 +372,27 @@ impl Room { members? }; + let (joined, invited) = match self.room_type() { + RoomType::Invited => { + // when we were invited we don't have a proper summary, we have to do best + // guessing + (members.len() as u64, 1u64) + } + RoomType::Joined if summary.joined_member_count == 0 => { + // joined but the summary is not completed yet + ( + (members.len() as u64) + 1, // we've taken ourselves out of the count + summary.invited_member_count, + ) + } + _ => (summary.joined_member_count, summary.invited_member_count), + }; + tracing::debug!( room_id = self.room_id().as_str(), own_user = self.own_user_id.as_str(), - heroes_count = heroes_count, - heroes = ?summary.heroes, + joined, invited, + heroes = ?members, "Calculating name for a room", ); @@ -444,14 +455,18 @@ impl Room { .store .get_users_with_display_name( self.room_id(), - member_event.content.displayname.as_deref().unwrap_or_else(|| user_id.localpart()), + member_event + .content() + .displayname + .as_deref() + .unwrap_or_else(|| user_id.localpart()), ) .await? .len() > 1; Ok(Some(RoomMember { - event: member_event.into(), + event: Arc::new(member_event), profile: profile.into(), presence: presence.into(), power_levels: power.into(), @@ -735,3 +750,212 @@ impl RoomInfo { self.base_info.create.as_ref().map(|c| &c.room_version) } } + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use assign::assign; + use ruma::{ + event_id, + events::{ + room::member::{ + MembershipState, OriginalSyncRoomMemberEvent, RoomMemberEventContent, + StrippedRoomMemberEvent, + }, + StateUnsigned, + }, + room_id, user_id, MilliSecondsSinceUnixEpoch, RoomAliasId, + }; + + use super::*; + use crate::store::{MemoryStore, StateChanges}; + + fn make_room(room_type: RoomType) -> (Arc, Room) { + let store = Arc::new(MemoryStore::new()); + let user_id = user_id!("@me:example.org"); + let room_id = room_id!("!test:localhost"); + + (store.clone(), Room::new(user_id, store, room_id, room_type)) + } + + fn make_stripped_member_event(user_id: &UserId, name: &str) -> StrippedRoomMemberEvent { + StrippedRoomMemberEvent { + content: assign!(RoomMemberEventContent::new(MembershipState::Join), { + displayname: Some(name.to_owned()) + }), + sender: user_id.to_owned(), + state_key: user_id.to_owned(), + } + } + + fn make_member_event(user_id: &UserId, name: &str) -> OriginalSyncRoomMemberEvent { + OriginalSyncRoomMemberEvent { + content: assign!(RoomMemberEventContent::new(MembershipState::Join), { + displayname: Some(name.to_owned()) + }), + sender: user_id.to_owned(), + state_key: user_id.to_owned(), + event_id: event_id!("$h29iv0s1:example.com").to_owned(), + origin_server_ts: MilliSecondsSinceUnixEpoch(208u32.into()), + unsigned: StateUnsigned::default(), + } + } + + #[tokio::test] + async fn test_display_name_default() { + let _ = env_logger::try_init(); + let (_, room) = make_room(RoomType::Joined); + assert_eq!(room.display_name().await.unwrap(), DisplayName::Empty); + + // has precedence + room.inner.write().unwrap().base_info.canonical_alias = + Some(RoomAliasId::parse("#test:example.com").unwrap()); + assert_eq!(room.display_name().await.unwrap(), DisplayName::Aliased("test".to_owned())); + + // has precedence + room.inner.write().unwrap().base_info.name = Some("Test Room".to_owned()); + assert_eq!(room.display_name().await.unwrap(), DisplayName::Named("Test Room".to_owned())); + + let (_, room) = make_room(RoomType::Invited); + assert_eq!(room.display_name().await.unwrap(), DisplayName::Empty); + + // has precedence + room.inner.write().unwrap().base_info.canonical_alias = + Some(RoomAliasId::parse("#test:example.com").unwrap()); + assert_eq!(room.display_name().await.unwrap(), DisplayName::Aliased("test".to_owned())); + + // has precedence + room.inner.write().unwrap().base_info.name = Some("Test Room".to_owned()); + assert_eq!(room.display_name().await.unwrap(), DisplayName::Named("Test Room".to_owned())); + } + + #[tokio::test] + async fn test_display_name_dm_invited() { + let _ = env_logger::try_init(); + let (store, room) = make_room(RoomType::Invited); + let room_id = room_id!("!test:localhost"); + let matthew = user_id!("@matthew:example.org"); + let me = user_id!("@me:example.org"); + let mut changes = StateChanges::new("".to_owned()); + let summary = assign!(RumaSummary::new(), { + heroes: vec![me.to_string(), matthew.to_string()], + }); + + changes.add_stripped_member(room_id, make_stripped_member_event(matthew, "Matthew")); + changes.add_stripped_member(room_id, make_stripped_member_event(me, "Me")); + store.save_changes(&changes).await.unwrap(); + + room.inner.write().unwrap().update_summary(&summary); + assert_eq!( + room.display_name().await.unwrap(), + DisplayName::Calculated("Matthew".to_owned()) + ); + } + + #[tokio::test] + async fn test_display_name_dm_invited_no_heroes() { + let _ = env_logger::try_init(); + let (store, room) = make_room(RoomType::Invited); + let room_id = room_id!("!test:localhost"); + let matthew = user_id!("@matthew:example.org"); + let me = user_id!("@me:example.org"); + let mut changes = StateChanges::new("".to_owned()); + + changes.add_stripped_member(room_id, make_stripped_member_event(matthew, "Matthew")); + changes.add_stripped_member(room_id, make_stripped_member_event(me, "Me")); + store.save_changes(&changes).await.unwrap(); + + assert_eq!( + room.display_name().await.unwrap(), + DisplayName::Calculated("Matthew".to_owned()) + ); + } + + #[tokio::test] + async fn test_display_name_dm_joined() { + let _ = env_logger::try_init(); + let (store, room) = make_room(RoomType::Joined); + let room_id = room_id!("!test:localhost"); + let matthew = user_id!("@matthew:example.org"); + let me = user_id!("@me:example.org"); + let mut changes = StateChanges::new("".to_owned()); + let summary = assign!(RumaSummary::new(), { + joined_member_count: Some(2u32.into()), + heroes: vec![me.to_string(), matthew.to_string()], + }); + + changes + .members + .entry(room_id.to_owned()) + .or_default() + .insert(matthew.to_owned(), make_member_event(matthew, "Matthew")); + changes + .members + .entry(room_id.to_owned()) + .or_default() + .insert(me.to_owned(), make_member_event(me, "Me")); + store.save_changes(&changes).await.unwrap(); + + room.inner.write().unwrap().update_summary(&summary); + assert_eq!( + room.display_name().await.unwrap(), + DisplayName::Calculated("Matthew".to_owned()) + ); + } + + #[tokio::test] + async fn test_display_name_dm_joined_no_heroes() { + let _ = env_logger::try_init(); + let (store, room) = make_room(RoomType::Joined); + let room_id = room_id!("!test:localhost"); + let matthew = user_id!("@matthew:example.org"); + let me = user_id!("@me:example.org"); + let mut changes = StateChanges::new("".to_owned()); + + changes + .members + .entry(room_id.to_owned()) + .or_default() + .insert(matthew.to_owned(), make_member_event(matthew, "Matthew")); + changes + .members + .entry(room_id.to_owned()) + .or_default() + .insert(me.to_owned(), make_member_event(me, "Me")); + store.save_changes(&changes).await.unwrap(); + + assert_eq!( + room.display_name().await.unwrap(), + DisplayName::Calculated("Matthew".to_owned()) + ); + } + #[tokio::test] + async fn test_display_name_dm_alone() { + let _ = env_logger::try_init(); + let (store, room) = make_room(RoomType::Joined); + let room_id = room_id!("!test:localhost"); + let matthew = user_id!("@matthew:example.org"); + let me = user_id!("@me:example.org"); + let mut changes = StateChanges::new("".to_owned()); + let summary = assign!(RumaSummary::new(), { + joined_member_count: Some(1u32.into()), + heroes: vec![me.to_string(), matthew.to_string()], + }); + + changes + .members + .entry(room_id.to_owned()) + .or_default() + .insert(matthew.to_owned(), make_member_event(matthew, "Matthew")); + changes + .members + .entry(room_id.to_owned()) + .or_default() + .insert(me.to_owned(), make_member_event(me, "Me")); + store.save_changes(&changes).await.unwrap(); + + room.inner.write().unwrap().update_summary(&summary); + assert_eq!(room.display_name().await.unwrap(), DisplayName::EmptyWas("Matthew".to_owned())); + } +} diff --git a/crates/matrix-sdk-base/src/store/ambiguity_map.rs b/crates/matrix-sdk-base/src/store/ambiguity_map.rs index 8abedead9..c048b14da 100644 --- a/crates/matrix-sdk-base/src/store/ambiguity_map.rs +++ b/crates/matrix-sdk-base/src/store/ambiguity_map.rs @@ -14,7 +14,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use matrix_sdk_common::deserialized_responses::AmbiguityChange; +use matrix_sdk_common::deserialized_responses::{AmbiguityChange, MemberEvent}; use ruma::{ events::room::member::{MembershipState, OriginalSyncRoomMemberEvent}, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, @@ -158,13 +158,13 @@ impl AmbiguityCache { let old_event = if let Some(m) = changes.members.get(room_id).and_then(|m| m.get(&member_event.state_key)) { - Some(m.clone()) + Some(MemberEvent::Original(m.clone())) } else { self.store.get_member_event(room_id, &member_event.state_key).await? }; let old_display_name = if let Some(event) = old_event { - if matches!(event.content.membership, Join | Invite) { + if matches!(event.content().membership, Join | Invite) { let display_name = if let Some(d) = changes .profiles .get(room_id) @@ -180,10 +180,10 @@ impl AmbiguityCache { { Some(d) } else { - event.content.displayname.clone() + event.content().displayname.clone() }; - Some(display_name.unwrap_or_else(|| event.state_key.localpart().to_owned())) + Some(display_name.unwrap_or_else(|| event.user_id().localpart().to_owned())) } else { None } diff --git a/crates/matrix-sdk-base/src/store/integration_tests.rs b/crates/matrix-sdk-base/src/store/integration_tests.rs index 3df98579c..94331df4d 100644 --- a/crates/matrix-sdk-base/src/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/store/integration_tests.rs @@ -265,6 +265,20 @@ macro_rules! statestore_integration_tests { serde_json::from_value(event).unwrap() } + fn stripped_membership_event() -> StrippedRoomMemberEvent { + custom_stripped_membership_event(user_id()) + } + + fn custom_stripped_membership_event( + user_id: &UserId, + ) -> StrippedRoomMemberEvent { + StrippedRoomMemberEvent { + content: RoomMemberEventContent::new(MembershipState::Join), + sender: user_id.to_owned(), + state_key: user_id.to_owned(), + } + } + fn membership_event() -> OriginalSyncRoomMemberEvent { custom_membership_event(user_id(), event_id!("$h29iv0s8:example.com").to_owned()) } @@ -281,7 +295,6 @@ macro_rules! statestore_integration_tests { state_key: user_id.to_owned(), unsigned: StateUnsigned::default(), } - } #[async_test] @@ -344,6 +357,27 @@ macro_rules! statestore_integration_tests { assert!(!members.is_empty(), "We expected to find members for the room") } + #[async_test] + async fn test_stripped_member_saving() { + let store = get_store().await.unwrap(); + let room_id = room_id!("!test_stripped_member_saving:localhost"); + let user_id = user_id(); + + assert!(store.get_member_event(room_id, user_id).await.unwrap().is_none()); + let mut changes = StateChanges::default(); + changes + .stripped_members + .entry(room_id.to_owned()) + .or_default() + .insert(user_id.to_owned(), stripped_membership_event()); + + store.save_changes(&changes).await.unwrap(); + assert!(store.get_member_event(room_id, user_id).await.unwrap().is_some()); + + let members = store.get_user_ids(room_id).await.unwrap(); + assert!(!members.is_empty(), "We expected to find members for the room") + } + #[async_test] async fn test_power_level_saving() { let store = get_store().await.unwrap(); diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index 9957e3180..767d1b6f1 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -51,7 +51,10 @@ use ruma::{ #[cfg(feature = "experimental-timeline")] use super::BoxStream; use super::{Result, RoomInfo, StateChanges, StateStore}; -use crate::media::{MediaRequest, UniqueKey}; +use crate::{ + deserialized_responses::MemberEvent, + media::{MediaRequest, UniqueKey}, +}; #[cfg(feature = "experimental-timeline")] use crate::{deserialized_responses::SyncRoomEvent, StoreError}; @@ -79,6 +82,8 @@ pub struct MemoryStore { DashMap>>>, >, stripped_members: Arc>>, + stripped_joined_user_ids: Arc>>, + stripped_invited_user_ids: Arc>>, presence: Arc>>, room_user_receipts: Arc>>>, @@ -116,6 +121,8 @@ impl MemoryStore { stripped_room_infos: Default::default(), stripped_room_state: Default::default(), stripped_members: Default::default(), + stripped_joined_user_ids: Default::default(), + stripped_invited_user_ids: Default::default(), presence: Default::default(), room_user_receipts: Default::default(), room_event_receipts: Default::default(), @@ -247,6 +254,39 @@ impl MemoryStore { for (room, events) in &changes.stripped_members { for event in events.values() { + match event.content.membership { + MembershipState::Join => { + self.stripped_joined_user_ids + .entry(room.clone()) + .or_insert_with(DashSet::new) + .insert(event.state_key.clone()); + self.stripped_invited_user_ids + .entry(room.clone()) + .or_insert_with(DashSet::new) + .remove(&event.state_key); + } + MembershipState::Invite => { + self.stripped_invited_user_ids + .entry(room.clone()) + .or_insert_with(DashSet::new) + .insert(event.state_key.clone()); + self.stripped_joined_user_ids + .entry(room.clone()) + .or_insert_with(DashSet::new) + .remove(&event.state_key); + } + _ => { + self.stripped_joined_user_ids + .entry(room.clone()) + .or_insert_with(DashSet::new) + .remove(&event.state_key); + self.stripped_invited_user_ids + .entry(room.clone()) + .or_insert_with(DashSet::new) + .remove(&event.state_key); + } + } + self.stripped_members .entry(room.clone()) .or_default() @@ -467,15 +507,28 @@ impl MemoryStore { &self, room_id: &RoomId, state_key: &UserId, - ) -> Result> { - Ok(self.members.get(room_id).and_then(|m| m.get(state_key).map(|m| m.clone()))) + ) -> Result> { + if let Some(e) = self.members.get(room_id).and_then(|m| m.get(state_key).map(|m| m.clone())) + { + Ok(Some(e.into())) + } else if let Some(e) = + self.stripped_members.get(room_id).and_then(|m| m.get(state_key).map(|m| m.clone())) + { + Ok(Some(e.into())) + } else { + Ok(None) + } } fn get_user_ids(&self, room_id: &RoomId) -> Vec { - self.members - .get(room_id) - .map(|u| u.iter().map(|u| u.key().clone()).collect()) - .unwrap_or_default() + if let Some(u) = self.members.get(room_id) { + u.iter().map(|u| u.key().clone()).collect() + } else { + self.stripped_members + .get(room_id) + .map(|u| u.iter().map(|u| u.key().clone()).collect()) + .unwrap_or_default() + } } fn get_invited_user_ids(&self, room_id: &RoomId) -> Vec { @@ -492,6 +545,20 @@ impl MemoryStore { .unwrap_or_default() } + fn get_stripped_invited_user_ids(&self, room_id: &RoomId) -> Vec { + self.stripped_invited_user_ids + .get(room_id) + .map(|u| u.iter().map(|u| u.clone()).collect()) + .unwrap_or_default() + } + + fn get_stripped_joined_user_ids(&self, room_id: &RoomId) -> Vec { + self.stripped_joined_user_ids + .get(room_id) + .map(|u| u.iter().map(|u| u.clone()).collect()) + .unwrap_or_default() + } + fn get_room_infos(&self) -> Vec { self.room_info.iter().map(|r| r.clone()).collect() } @@ -686,7 +753,7 @@ impl StateStore for MemoryStore { &self, room_id: &RoomId, state_key: &UserId, - ) -> Result> { + ) -> Result> { self.get_member_event(room_id, state_key).await } @@ -695,11 +762,19 @@ impl StateStore for MemoryStore { } async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result> { - Ok(self.get_invited_user_ids(room_id)) + let v = self.get_invited_user_ids(room_id); + if !v.is_empty() { + return Ok(v); + } + Ok(self.get_stripped_invited_user_ids(room_id)) } async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result> { - Ok(self.get_joined_user_ids(room_id)) + let v = self.get_joined_user_ids(room_id); + if !v.is_empty() { + return Ok(v); + } + Ok(self.get_stripped_joined_user_ids(room_id)) } async fn get_room_infos(&self) -> Result> { diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index b19d4e338..5c71b7ddd 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -59,6 +59,7 @@ pub type BoxStream = Pin + Send>>; #[cfg(feature = "experimental-timeline")] use crate::deserialized_responses::{SyncRoomEvent, TimelineSlice}; use crate::{ + deserialized_responses::MemberEvent, media::MediaRequest, rooms::{RoomInfo, RoomType}, Room, Session, @@ -179,7 +180,7 @@ pub trait StateStore: AsyncTraitDeps { user_id: &UserId, ) -> Result>; - /// Get a raw `MemberEvent` for the given state key in the given room id. + /// Get the `MemberEvent` for the given state key in the given room id. /// /// # Arguments /// @@ -190,17 +191,18 @@ pub trait StateStore: AsyncTraitDeps { &self, room_id: &RoomId, state_key: &UserId, - ) -> Result>; + ) -> Result>; - /// Get all the user ids of members for a given room. + /// Get all the user ids of members for a given room, for stripped and + /// regular rooms alike. async fn get_user_ids(&self, room_id: &RoomId) -> Result>; /// Get all the user ids of members that are in the invited state for a - /// given room. + /// given room, for stripped and regular rooms alike. async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result>; /// Get all the user ids of members that are in the joined state for a - /// given room. + /// given room, for stripped and regular rooms alike. async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result>; /// Get all the pure `RoomInfo`s the store knows about. diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index 2669d8642..e869d9625 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -8,9 +8,15 @@ use ruma::{ State, ToDevice, UnreadNotificationsCount as RumaUnreadNotificationsCount, }, }, - events::{room::member::OriginalRoomMemberEvent, AnyRoomEvent, AnySyncRoomEvent}, + events::{ + room::member::{ + OriginalRoomMemberEvent, OriginalSyncRoomMemberEvent, RoomMemberEventContent, + StrippedRoomMemberEvent, + }, + AnyRoomEvent, AnySyncRoomEvent, + }, serde::Raw, - DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, + DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, UserId, }; use serde::{Deserialize, Serialize}; @@ -290,6 +296,43 @@ impl TimelineSlice { } } +/// Wrapper around both MemberEvent-Types +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum MemberEvent { + Stripped(StrippedRoomMemberEvent), + Original(OriginalSyncRoomMemberEvent), +} + +impl MemberEvent { + /// The inner Content of the wrapped Event + pub fn content(&self) -> &RoomMemberEventContent { + match &*self { + MemberEvent::Stripped(e) => &e.content, + MemberEvent::Original(e) => &e.content, + } + } + + /// The user id associated to this member event + pub fn user_id(&self) -> &UserId { + match &*self { + MemberEvent::Stripped(e) => &e.state_key, + MemberEvent::Original(e) => &e.state_key, + } + } +} + +impl From for MemberEvent { + fn from(other: StrippedRoomMemberEvent) -> Self { + MemberEvent::Stripped(other) + } +} +impl From for MemberEvent { + fn from(other: OriginalSyncRoomMemberEvent) -> Self { + MemberEvent::Original(other) + } +} + /// A deserialized response for the rooms members API call. /// /// [GET /_matrix/client/r0/rooms/{roomId}/members](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-members) diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index 678b1b430..a62e3e53d 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -19,19 +19,20 @@ use async_trait::async_trait; #[cfg(feature = "experimental-timeline")] use futures_util::stream; use indexed_db_futures::prelude::*; -#[cfg(feature = "experimental-timeline")] -use matrix_sdk_base::{deserialized_responses::SyncRoomEvent, store::BoxStream}; use matrix_sdk_base::{ + deserialized_responses::MemberEvent, media::{MediaRequest, UniqueKey}, store::{Result as StoreResult, StateChanges, StateStore, StoreError}, RoomInfo, }; +#[cfg(feature = "experimental-timeline")] +use matrix_sdk_base::{deserialized_responses::SyncRoomEvent, store::BoxStream}; use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher}; use ruma::{ events::{ presence::PresenceEvent, receipt::Receipt, - room::member::{MembershipState, OriginalSyncRoomMemberEvent, RoomMemberEventContent}, + room::member::{MembershipState, RoomMemberEventContent}, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, @@ -120,6 +121,8 @@ mod KEYS { pub const STRIPPED_ROOM_INFOS: &str = "stripped_room_infos"; pub const STRIPPED_MEMBERS: &str = "stripped_members"; pub const STRIPPED_ROOM_STATE: &str = "stripped_room_state"; + pub const STRIPPED_JOINED_USER_IDS: &str = "stripped_joined_user_ids"; + pub const STRIPPED_INVITED_USER_IDS: &str = "stripped_invited_user_ids"; pub const ROOM_USER_RECEIPTS: &str = "room_user_receipts"; pub const ROOM_EVENT_RECEIPTS: &str = "room_event_receipts"; @@ -183,6 +186,8 @@ impl IndexeddbStore { db.create_object_store(KEYS::STRIPPED_ROOM_INFOS)?; db.create_object_store(KEYS::STRIPPED_MEMBERS)?; db.create_object_store(KEYS::STRIPPED_ROOM_STATE)?; + db.create_object_store(KEYS::STRIPPED_JOINED_USER_IDS)?; + db.create_object_store(KEYS::STRIPPED_INVITED_USER_IDS)?; db.create_object_store(KEYS::ROOM_USER_RECEIPTS)?; db.create_object_store(KEYS::ROOM_EVENT_RECEIPTS)?; @@ -369,7 +374,6 @@ impl IndexeddbStore { (!changes.room_infos.is_empty(), KEYS::ROOM_INFOS), (!changes.receipts.is_empty(), KEYS::ROOM_EVENT_RECEIPTS), (!changes.stripped_state.is_empty(), KEYS::STRIPPED_ROOM_STATE), - (!changes.stripped_members.is_empty(), KEYS::STRIPPED_MEMBERS), (!changes.stripped_room_infos.is_empty(), KEYS::STRIPPED_ROOM_INFOS), ] .iter() @@ -385,6 +389,14 @@ impl IndexeddbStore { ]) } + if !changes.stripped_members.is_empty() { + stores.extend([ + KEYS::STRIPPED_MEMBERS, + KEYS::STRIPPED_INVITED_USER_IDS, + KEYS::STRIPPED_JOINED_USER_IDS, + ]) + } + if !changes.receipts.is_empty() { stores.extend([KEYS::ROOM_EVENT_RECEIPTS, KEYS::ROOM_USER_RECEIPTS]) } @@ -486,10 +498,38 @@ impl IndexeddbStore { if !changes.stripped_members.is_empty() { let store = tx.object_store(KEYS::STRIPPED_MEMBERS)?; + let joined = tx.object_store(KEYS::STRIPPED_JOINED_USER_IDS)?; + let invited = tx.object_store(KEYS::STRIPPED_INVITED_USER_IDS)?; for (room, events) in &changes.stripped_members { for event in events.values() { - let key = self.encode_key(KEYS::STRIPPED_MEMBERS, (room, &event.state_key)); - store.put_key_val(&key, &self.serialize_event(&event)?)?; + let key = (room, &event.state_key); + + match event.content.membership { + MembershipState::Join => { + joined.put_key_val_owned( + &self.encode_key(KEYS::STRIPPED_JOINED_USER_IDS, key), + &self.serialize_event(&event.state_key)?, + )?; + invited + .delete(&self.encode_key(KEYS::STRIPPED_INVITED_USER_IDS, key))?; + } + MembershipState::Invite => { + invited.put_key_val_owned( + &self.encode_key(KEYS::STRIPPED_INVITED_USER_IDS, key), + &self.serialize_event(&event.state_key)?, + )?; + joined.delete(&self.encode_key(KEYS::STRIPPED_JOINED_USER_IDS, key))?; + } + _ => { + joined.delete(&self.encode_key(KEYS::STRIPPED_JOINED_USER_IDS, key))?; + invited + .delete(&self.encode_key(KEYS::STRIPPED_INVITED_USER_IDS, key))?; + } + } + store.put_key_val( + &self.encode_key(KEYS::STRIPPED_MEMBERS, key), + &self.serialize_event(&event)?, + )?; } } } @@ -508,14 +548,14 @@ impl IndexeddbStore { } if !changes.members.is_empty() { + let profiles_store = tx.object_store(KEYS::PROFILES)?; + let joined = tx.object_store(KEYS::JOINED_USER_IDS)?; + let invited = tx.object_store(KEYS::INVITED_USER_IDS)?; + let members = tx.object_store(KEYS::MEMBERS)?; + for (room, events) in &changes.members { let profile_changes = changes.profiles.get(room); - let profiles_store = tx.object_store(KEYS::PROFILES)?; - let joined = tx.object_store(KEYS::JOINED_USER_IDS)?; - let invited = tx.object_store(KEYS::INVITED_USER_IDS)?; - let members = tx.object_store(KEYS::MEMBERS)?; - for event in events.values() { let key = (room, &event.state_key); @@ -862,14 +902,30 @@ impl IndexeddbStore { &self, room_id: &RoomId, state_key: &UserId, - ) -> Result> { - self.inner + ) -> Result> { + if let Some(e) = self + .inner .transaction_on_one_with_mode(KEYS::MEMBERS, IdbTransactionMode::Readonly)? .object_store(KEYS::MEMBERS)? .get(&self.encode_key(KEYS::MEMBERS, (room_id, state_key)))? .await? .map(|f| self.deserialize_event(f)) - .transpose() + .transpose()? + { + Ok(Some(MemberEvent::Original(e))) + } else if let Some(e) = self + .inner + .transaction_on_one_with_mode(KEYS::STRIPPED_MEMBERS, IdbTransactionMode::Readonly)? + .object_store(KEYS::STRIPPED_MEMBERS)? + .get(&self.encode_key(KEYS::STRIPPED_MEMBERS, (room_id, state_key)))? + .await? + .map(|f| self.deserialize_event(f)) + .transpose()? + { + Ok(Some(MemberEvent::Stripped(e))) + } else { + Ok(None) + } } pub async fn get_user_ids_stream(&self, room_id: &RoomId) -> Result> { @@ -905,6 +961,51 @@ impl IndexeddbStore { .collect::>()) } + pub async fn get_stripped_user_ids_stream(&self, room_id: &RoomId) -> Result> { + Ok([ + self.get_stripped_invited_user_ids(room_id).await?, + self.get_stripped_joined_user_ids(room_id).await?, + ] + .concat()) + } + + pub async fn get_stripped_invited_user_ids( + &self, + room_id: &RoomId, + ) -> Result> { + let range = self.encode_to_range(KEYS::STRIPPED_INVITED_USER_IDS, room_id)?; + let entries = self + .inner + .transaction_on_one_with_mode( + KEYS::STRIPPED_INVITED_USER_IDS, + IdbTransactionMode::Readonly, + )? + .object_store(KEYS::STRIPPED_INVITED_USER_IDS)? + .get_all_with_key(&range)? + .await? + .iter() + .filter_map(|f| self.deserialize_event::(f).ok()) + .collect::>(); + + Ok(entries) + } + + pub async fn get_stripped_joined_user_ids(&self, room_id: &RoomId) -> Result> { + let range = self.encode_to_range(KEYS::STRIPPED_JOINED_USER_IDS, room_id)?; + Ok(self + .inner + .transaction_on_one_with_mode( + KEYS::STRIPPED_JOINED_USER_IDS, + IdbTransactionMode::Readonly, + )? + .object_store(KEYS::STRIPPED_JOINED_USER_IDS)? + .get_all_with_key(&range)? + .await? + .iter() + .filter_map(|f| self.deserialize_event::(f).ok()) + .collect::>()) + } + pub async fn get_room_infos(&self) -> Result> { let entries: Vec<_> = self .inner @@ -1242,20 +1343,32 @@ impl StateStore for IndexeddbStore { &self, room_id: &RoomId, state_key: &UserId, - ) -> StoreResult> { + ) -> StoreResult> { self.get_member_event(room_id, state_key).await.map_err(|e| e.into()) } async fn get_user_ids(&self, room_id: &RoomId) -> StoreResult> { - self.get_user_ids_stream(room_id).await.map_err(|e| e.into()) + let ids: Vec = self.get_user_ids_stream(room_id).await?; + if !ids.is_empty() { + return Ok(ids); + } + self.get_stripped_user_ids_stream(room_id).await.map_err(|e| e.into()) } async fn get_invited_user_ids(&self, room_id: &RoomId) -> StoreResult> { - self.get_invited_user_ids(room_id).await.map_err(|e| e.into()) + let ids: Vec = self.get_invited_user_ids(room_id).await?; + if !ids.is_empty() { + return Ok(ids); + } + self.get_stripped_invited_user_ids(room_id).await.map_err(|e| e.into()) } async fn get_joined_user_ids(&self, room_id: &RoomId) -> StoreResult> { - self.get_joined_user_ids(room_id).await.map_err(|e| e.into()) + let ids: Vec = self.get_joined_user_ids(room_id).await?; + if !ids.is_empty() { + return Ok(ids); + } + self.get_stripped_joined_user_ids(room_id).await.map_err(|e| e.into()) } async fn get_room_infos(&self) -> StoreResult> { diff --git a/crates/matrix-sdk-sled/src/state_store.rs b/crates/matrix-sdk-sled/src/state_store.rs index ebbfb7d7a..eeeb1c1d8 100644 --- a/crates/matrix-sdk-sled/src/state_store.rs +++ b/crates/matrix-sdk-sled/src/state_store.rs @@ -25,19 +25,20 @@ use async_stream::stream; use async_trait::async_trait; use futures_core::stream::Stream; use futures_util::stream::{self, StreamExt, TryStreamExt}; -#[cfg(feature = "experimental-timeline")] -use matrix_sdk_base::{deserialized_responses::SyncRoomEvent, store::BoxStream}; use matrix_sdk_base::{ + deserialized_responses::MemberEvent, media::{MediaRequest, UniqueKey}, store::{Result as StoreResult, StateChanges, StateStore, StoreError}, RoomInfo, }; +#[cfg(feature = "experimental-timeline")] +use matrix_sdk_base::{deserialized_responses::SyncRoomEvent, store::BoxStream}; use matrix_sdk_store_encryption::{Error as KeyEncryptionError, StoreCipher}; use ruma::{ events::{ presence::PresenceEvent, receipt::Receipt, - room::member::{MembershipState, OriginalSyncRoomMemberEvent, RoomMemberEventContent}, + room::member::{MembershipState, RoomMemberEventContent}, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType, }, @@ -134,6 +135,8 @@ const ROOM_STATE: &str = "room-state"; const ROOM_USER_RECEIPT: &str = "room-user-receipt"; const ROOM: &str = "room"; const SESSION: &str = "session"; +const STRIPPED_INVITED_USER_ID: &str = "stripped-invited-user-id"; +const STRIPPED_JOINED_USER_ID: &str = "stripped-joined-user-id"; const STRIPPED_ROOM_INFO: &str = "stripped-room-info"; const STRIPPED_ROOM_MEMBER: &str = "stripped-room-member"; const STRIPPED_ROOM_STATE: &str = "stripped-room-state"; @@ -159,6 +162,8 @@ pub struct SledStore { room_info: Tree, room_state: Tree, room_account_data: Tree, + stripped_joined_user_ids: Tree, + stripped_invited_user_ids: Tree, stripped_room_infos: Tree, stripped_room_state: Tree, stripped_members: Tree, @@ -205,6 +210,8 @@ impl SledStore { let presence = db.open_tree(PRESENCE)?; let room_account_data = db.open_tree(ROOM_ACCOUNT_DATA)?; + let stripped_joined_user_ids = db.open_tree(STRIPPED_JOINED_USER_ID)?; + let stripped_invited_user_ids = db.open_tree(STRIPPED_INVITED_USER_ID)?; let stripped_room_infos = db.open_tree(STRIPPED_ROOM_INFO)?; let stripped_members = db.open_tree(STRIPPED_ROOM_MEMBER)?; let stripped_room_state = db.open_tree(STRIPPED_ROOM_STATE)?; @@ -238,6 +245,8 @@ impl SledStore { presence, room_state, room_info, + stripped_joined_user_ids, + stripped_invited_user_ids, stripped_room_infos, stripped_members, stripped_room_state, @@ -374,9 +383,8 @@ impl SledStore { pub async fn save_changes(&self, changes: &StateChanges) -> Result<()> { let now = Instant::now(); + // room state & memberships let ret: Result<(), TransactionError> = ( - &self.session, - &self.account_data, &self.members, &self.profiles, &self.display_names, @@ -385,15 +393,14 @@ impl SledStore { &self.room_info, &self.room_state, &self.room_account_data, - &self.presence, + &self.stripped_joined_user_ids, + &self.stripped_invited_user_ids, &self.stripped_room_infos, &self.stripped_members, &self.stripped_room_state, ) .transaction( |( - session, - account_data, members, profiles, display_names, @@ -402,15 +409,12 @@ impl SledStore { rooms, state, room_account_data, - presence, + stripped_joined, + stripped_invited, striped_rooms, stripped_members, stripped_state, )| { - if let Some(s) = &changes.sync_token { - session.insert("sync_token".encode(), s.as_str())?; - } - for (room, events) in &changes.members { let profile_changes = changes.profiles.get(room); @@ -466,14 +470,6 @@ impl SledStore { } } - for (event_type, event) in &changes.account_data { - account_data.insert( - self.encode_key(ACCOUNT_DATA, event_type), - self.serialize_event(&event) - .map_err(ConflictableTransactionError::Abort)?, - )?; - } - for (room, events) in &changes.room_account_data { for (event_type, event) in events { room_account_data.insert( @@ -504,14 +500,6 @@ impl SledStore { )?; } - for (sender, event) in &changes.presence { - presence.insert( - self.encode_key(PRESENCE, sender), - self.serialize_event(&event) - .map_err(ConflictableTransactionError::Abort)?, - )?; - } - for (room_id, info) in &changes.stripped_room_infos { striped_rooms.insert( self.encode_key(STRIPPED_ROOM_INFO, room_id), @@ -522,11 +510,34 @@ impl SledStore { for (room, events) in &changes.stripped_members { for event in events.values() { + let key = (room, &event.state_key); + + match event.content.membership { + MembershipState::Join => { + stripped_joined.insert( + self.encode_key(STRIPPED_JOINED_USER_ID, &key), + event.state_key.as_str(), + )?; + stripped_invited + .remove(self.encode_key(STRIPPED_INVITED_USER_ID, &key))?; + } + MembershipState::Invite => { + stripped_invited.insert( + self.encode_key(STRIPPED_INVITED_USER_ID, &key), + event.state_key.as_str(), + )?; + stripped_joined + .remove(self.encode_key(STRIPPED_JOINED_USER_ID, &key))?; + } + _ => { + stripped_joined + .remove(self.encode_key(STRIPPED_JOINED_USER_ID, &key))?; + stripped_invited + .remove(self.encode_key(STRIPPED_INVITED_USER_ID, &key))?; + } + } stripped_members.insert( - self.encode_key( - STRIPPED_ROOM_MEMBER, - (room, event.state_key.to_string()), - ), + self.encode_key(STRIPPED_ROOM_MEMBER, &key), self.serialize_event(&event) .map_err(ConflictableTransactionError::Abort)?, )?; @@ -555,8 +566,8 @@ impl SledStore { ret?; let ret: Result<(), TransactionError> = - (&self.room_user_receipts, &self.room_event_receipts).transaction( - |(room_user_receipts, room_event_receipts)| { + (&self.room_user_receipts, &self.room_event_receipts, &self.presence).transaction( + |(room_user_receipts, room_event_receipts, presence)| { for (room, content) in &changes.receipts { for (event_id, receipts) in &content.0 { for (receipt_type, receipts) in receipts { @@ -594,6 +605,14 @@ impl SledStore { } } + for (sender, event) in &changes.presence { + presence.insert( + self.encode_key(PRESENCE, sender), + self.serialize_event(&event) + .map_err(ConflictableTransactionError::Abort)?, + )?; + } + Ok(()) }, ); @@ -603,6 +622,26 @@ impl SledStore { #[cfg(feature = "experimental-timeline")] self.save_room_timeline(changes).await?; + // user state + let ret: Result<(), TransactionError> = (&self.session, &self.account_data) + .transaction(|(session, account_data)| { + if let Some(s) = &changes.sync_token { + session.insert("sync_token".encode(), s.as_str())?; + } + + for (event_type, event) in &changes.account_data { + account_data.insert( + self.encode_key(ACCOUNT_DATA, event_type), + self.serialize_event(&event) + .map_err(ConflictableTransactionError::Abort)?, + )?; + } + + Ok(()) + }); + + ret?; + self.inner.flush_async().await?; tracing::info!("Saved changes in {:?}", now.elapsed()); @@ -662,11 +701,25 @@ impl SledStore { &self, room_id: &RoomId, state_key: &UserId, - ) -> Result> { + ) -> Result> { let db = self.clone(); let key = self.encode_key(MEMBER, (room_id, state_key)); - spawn_blocking(move || db.members.get(key)?.map(|v| db.deserialize_event(&v)).transpose()) - .await? + let stripped_key = self.encode_key(STRIPPED_ROOM_MEMBER, (room_id, state_key)); + spawn_blocking(move || { + if let Some(e) = db.members.get(key)?.map(|v| db.deserialize_event(&v)).transpose()? { + Ok(Some(MemberEvent::Original(e))) + } else if let Some(e) = db + .stripped_members + .get(stripped_key)? + .map(|v| db.deserialize_event(&v)) + .transpose()? + { + Ok(Some(MemberEvent::Stripped(e))) + } else { + Ok(None) + } + }) + .await? } pub async fn get_user_ids_stream( @@ -678,6 +731,15 @@ impl SledStore { .await? .chain(self.get_invited_user_ids(room_id).await?)) } + pub async fn get_stripped_user_ids_stream( + &self, + room_id: &RoomId, + ) -> StoreResult>> { + Ok(self + .get_stripped_joined_user_ids(room_id) + .await? + .chain(self.get_stripped_invited_user_ids(room_id).await?)) + } pub async fn get_invited_user_ids( &self, @@ -715,6 +777,42 @@ impl SledStore { .map_err(|e| StoreError::Backend(anyhow!(e))) } + pub async fn get_stripped_invited_user_ids( + &self, + room_id: &RoomId, + ) -> StoreResult>> { + let db = self.clone(); + let key = self.encode_key(STRIPPED_INVITED_USER_ID, room_id); + spawn_blocking(move || { + stream::iter(db.stripped_invited_user_ids.scan_prefix(key).map(|u| { + UserId::parse(String::from_utf8_lossy( + &u.map_err(|e| StoreError::Backend(anyhow!(e)))?.1, + )) + .map_err(StoreError::Identifier) + })) + }) + .await + .map_err(|e| StoreError::Backend(anyhow!(e))) + } + + pub async fn get_stripped_joined_user_ids( + &self, + room_id: &RoomId, + ) -> StoreResult>> { + let db = self.clone(); + let key = self.encode_key(STRIPPED_JOINED_USER_ID, room_id); + spawn_blocking(move || { + stream::iter(db.stripped_joined_user_ids.scan_prefix(key).map(|u| { + UserId::parse(String::from_utf8_lossy( + &u.map_err(|e| StoreError::Backend(anyhow!(e)))?.1, + )) + .map_err(StoreError::Identifier) + })) + }) + .await + .map_err(|e| StoreError::Backend(anyhow!(e))) + } + pub async fn get_room_infos(&self) -> Result>> { let db = self.clone(); spawn_blocking(move || { @@ -1303,20 +1401,32 @@ impl StateStore for SledStore { &self, room_id: &RoomId, state_key: &UserId, - ) -> StoreResult> { + ) -> StoreResult> { self.get_member_event(room_id, state_key).await.map_err(Into::into) } async fn get_user_ids(&self, room_id: &RoomId) -> StoreResult> { - self.get_user_ids_stream(room_id).await?.try_collect().await + let v: Vec = self.get_user_ids_stream(room_id).await?.try_collect().await?; + if !v.is_empty() { + return Ok(v); + } + self.get_stripped_user_ids_stream(room_id).await?.try_collect().await } async fn get_invited_user_ids(&self, room_id: &RoomId) -> StoreResult> { - self.get_invited_user_ids(room_id).await?.try_collect().await + let v: Vec = self.get_invited_user_ids(room_id).await?.try_collect().await?; + if !v.is_empty() { + return Ok(v); + } + self.get_stripped_invited_user_ids(room_id).await?.try_collect().await } async fn get_joined_user_ids(&self, room_id: &RoomId) -> StoreResult> { - self.get_joined_user_ids(room_id).await?.try_collect().await + let v: Vec = self.get_joined_user_ids(room_id).await?.try_collect().await?; + if !v.is_empty() { + return Ok(v); + } + self.get_stripped_joined_user_ids(room_id).await?.try_collect().await } async fn get_room_infos(&self) -> StoreResult> { diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index dbd6cdfe9..634c85aa8 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2249,7 +2249,10 @@ pub(crate) mod tests { use std::{collections::BTreeMap, convert::TryInto, io::Cursor, str::FromStr, time::Duration}; - use matrix_sdk_base::media::{MediaFormat, MediaRequest, MediaThumbnailSize}; + use matrix_sdk_base::{ + media::{MediaFormat, MediaRequest, MediaThumbnailSize}, + DisplayName, + }; #[cfg(feature = "experimental-timeline")] use matrix_sdk_common::deserialized_responses::SyncRoomEvent; use matrix_sdk_test::{test_json, EventBuilder, EventsJson}; @@ -3366,7 +3369,10 @@ pub(crate) mod tests { let _response = client.sync_once(sync_settings).await.unwrap(); let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - assert_eq!("example2", room.display_name().await.unwrap()); + assert_eq!( + DisplayName::Calculated("example2".to_owned()), + room.display_name().await.unwrap() + ); } #[async_test] @@ -3444,7 +3450,7 @@ pub(crate) mod tests { assert_eq!(client.rooms().len(), 1); let room = client.get_joined_room(room_id!("!SVkFJHzfwvuaIEawgC:localhost")).unwrap(); - assert_eq!("tutorial".to_owned(), room.display_name().await.unwrap()); + assert_eq!(DisplayName::Aliased("tutorial".to_owned()), room.display_name().await.unwrap()); let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_owned())) .with_status(200) @@ -3458,7 +3464,10 @@ pub(crate) mod tests { assert_eq!(client.rooms().len(), 1); let invited_room = client.get_invited_room(room_id!("!696r7674:example.com")).unwrap(); - assert_eq!("My Room Name".to_owned(), invited_room.display_name().await.unwrap()); + assert_eq!( + DisplayName::Named("My Room Name".to_owned()), + invited_room.display_name().await.unwrap() + ); } #[async_test]