From d74902e45f88c8c2bd1304530aefe88466a29c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 9 Oct 2024 16:20:42 +0200 Subject: [PATCH] feat(knocking): add code to process knocked rooms separately during sync --- crates/matrix-sdk-base/src/client.rs | 26 +++++ crates/matrix-sdk-base/src/debug.rs | 19 +++- .../matrix-sdk-base/src/sliding_sync/mod.rs | 97 +++++++++++++------ crates/matrix-sdk-base/src/sync.rs | 17 +++- crates/matrix-sdk/src/sync.rs | 34 ++++++- crates/matrix-sdk/tests/integration/client.rs | 17 +++- testing/matrix-sdk-test/src/test_json/sync.rs | 29 +++++- 7 files changed, 201 insertions(+), 38 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index e39587455ab..5e0c3af13e7 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1140,6 +1140,32 @@ impl BaseClient { new_rooms.invite.insert(room_id, new_info); } + for (room_id, new_info) in response.rooms.knock { + let room = self.store.get_or_create_room( + &room_id, + RoomState::Knocked, + self.room_info_notable_update_sender.clone(), + ); + + let mut room_info = room.clone_info(); + room_info.mark_as_invited(); + room_info.mark_state_fully_synced(); + + self.handle_invited_state( + &room, + &new_info.knock_state.events, + &push_rules, + &mut room_info, + &mut changes, + &mut notifications, + ) + .await?; + + changes.add_room(room_info); + + new_rooms.knocked.insert(room_id, new_info); + } + account_data_processor.apply(&mut changes, &self.store).await; changes.presence = response diff --git a/crates/matrix-sdk-base/src/debug.rs b/crates/matrix-sdk-base/src/debug.rs index d207eccef10..95035c1c4f7 100644 --- a/crates/matrix-sdk-base/src/debug.rs +++ b/crates/matrix-sdk-base/src/debug.rs @@ -17,7 +17,10 @@ use std::fmt; pub use matrix_sdk_common::debug::*; -use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw}; +use ruma::{ + api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom}, + serde::Raw, +}; /// A wrapper around a slice of `Raw` events that implements `Debug` in a way /// that only prints the event type of each item. @@ -46,6 +49,20 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> { } } +/// A wrapper around a knocked on room as found in `/sync` responses that +/// implements `Debug` in a way that only prints the event ID and event type for +/// the raw events contained in `knock_state`. +pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom); + +#[cfg(not(tarpaulin_include))] +impl<'a> fmt::Debug for DebugKnockedRoom<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KnockedRoom") + .field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events)) + .finish() + } +} + pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw]); #[cfg(not(tarpaulin_include))] diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 334336e3ba7..ffe15f9f7cd 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -27,8 +27,11 @@ use ruma::api::client::sync::sync_events::v5; #[cfg(feature = "e2e-encryption")] use ruma::events::AnyToDeviceEvent; use ruma::{ - api::client::sync::sync_events::v3::{self, InvitedRoom}, - events::{AnyRoomAccountDataEvent, AnySyncStateEvent}, + api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom}, + events::{ + room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent, + AnySyncStateEvent, + }, serde::Raw, JsOption, OwnedRoomId, RoomId, UInt, }; @@ -47,6 +50,7 @@ use crate::{ normal::{RoomHero, RoomInfoNotableUpdateReasons}, RoomState, }, + ruma::assign, store::{ambiguity_map::AmbiguityCache, StateChanges, Store}, sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse}, Room, RoomInfo, @@ -168,7 +172,7 @@ impl BaseClient { let mut rooms_account_data = extensions.account_data.rooms.clone(); for (room_id, response_room_data) in rooms { - let (room_info, joined_room, left_room, invited_room) = self + let (room_info, joined_room, left_room, invited_room, knocked_room) = self .process_sliding_sync_room( room_id, response_room_data, @@ -196,6 +200,10 @@ impl BaseClient { if let Some(invited_room) = invited_room { new_rooms.invite.insert(room_id.clone(), invited_room); } + + if let Some(knocked_room) = knocked_room { + new_rooms.knocked.insert(room_id.clone(), knocked_room); + } } // Handle read receipts and typing notifications independently of the rooms: @@ -347,8 +355,13 @@ impl BaseClient { notifications: &mut BTreeMap>, ambiguity_cache: &mut AmbiguityCache, with_msc4186: bool, - ) -> Result<(RoomInfo, Option, Option, Option)> - { + ) -> Result<( + RoomInfo, + Option, + Option, + Option, + Option, + )> { // This method may change `room_data` (see the terrible hack describes below) // with `timestamp` and `invite_state. We don't want to change the `room_data` // from outside this method, hence `Cow` is perfectly suited here. @@ -403,13 +416,14 @@ impl BaseClient { } #[allow(unused_mut)] // Required for some feature flag combinations - let (mut room, mut room_info, invited_room) = self.process_sliding_sync_room_membership( - room_data.as_ref(), - &state_events, - store, - room_id, - room_info_notable_updates, - ); + let (mut room, mut room_info, invited_room, knocked_room) = self + .process_sliding_sync_room_membership( + room_data.as_ref(), + &state_events, + store, + room_id, + room_info_notable_updates, + ); room_info.mark_state_partially_synced(); @@ -428,6 +442,7 @@ impl BaseClient { let push_rules = self.get_push_rules(account_data_processor).await?; + // This will be used for both invited and knocked rooms if let Some(invite_state) = &room_data.invite_state { self.handle_invited_state( &room, @@ -512,6 +527,7 @@ impl BaseClient { )), None, None, + None, )) } @@ -525,12 +541,12 @@ impl BaseClient { ambiguity_changes, )), None, + None, )), - RoomState::Invited => Ok((room_info, None, None, invited_room)), + RoomState::Invited => Ok((room_info, None, None, invited_room, None)), - // TODO: implement special logic for retrieving the knocked room info - RoomState::Knocked => Ok((room_info, None, None, None)), + RoomState::Knocked => Ok((room_info, None, None, None, knocked_room)), } } @@ -546,7 +562,12 @@ impl BaseClient { store: &Store, room_id: &RoomId, room_info_notable_updates: &mut BTreeMap, - ) -> (Room, RoomInfo, Option) { + ) -> (Room, RoomInfo, Option, Option) { + let user_id = self + .session_meta() + .expect("Sliding sync shouldn't run without an authenticated user.") + .user_id + .to_owned(); if let Some(invite_state) = &room_data.invite_state { let room = store.get_or_create_room( room_id, @@ -555,20 +576,35 @@ impl BaseClient { ); let mut room_info = room.clone_info(); - // We don't actually know what events are inside invite_state. In theory, they - // might not contain an m.room.member event, or they might set the - // membership to something other than invite. This would be very - // weird behaviour by the server, because invite_state is supposed - // to contain an m.room.member. We will call handle_invited_state, which will - // reflect any information found in the real events inside - // invite_state, but we default to considering this room invited - // simply because invite_state exists. This is needed in the normal - // case, because the sliding sync server tries to send minimal state, - // meaning that we normally actually just receive {"type": "m.room.member"} with - // no content at all. - room_info.mark_as_invited(); + // We need to find the membership event since it could be for either an invited + // or knocked room + let membership_event_content = invite_state.iter().find_map(|raw| { + raw.deserialize().ok().iter().find_map(|e| { + if let AnyStrippedStateEvent::RoomMember(membership_event) = e { + if membership_event.sender == user_id { + return Some(membership_event.content.clone()); + } + } + None + }) + }); - (room, room_info, Some(InvitedRoom::from(v3::InviteState::from(invite_state.clone())))) + if let Some(membership_event_content) = membership_event_content { + if membership_event_content.membership == MembershipState::Knock { + // If we have a `Knock` membership state, set the room as such + room_info.mark_as_knocked(); + let knock_state = + assign!(v3::KnockState::default(), { events: invite_state.clone() }); + let knocked_room = + assign!(KnockedRoom::default(), { knock_state: knock_state }); + return (room, room_info, None, Some(knocked_room)); + } + } + + // Otherwise assume it's an invited room + room_info.mark_as_invited(); + let invited_room = InvitedRoom::from(v3::InviteState::from(invite_state.clone())); + (room, room_info, Some(invited_room), None) } else { let room = store.get_or_create_room( room_id, @@ -594,7 +630,7 @@ impl BaseClient { room_info_notable_updates, ); - (room, room_info, None) + (room, room_info, None, None) } } @@ -1014,6 +1050,7 @@ mod tests { assert!(!sync_resp.rooms.join.contains_key(room_id)); assert!(!sync_resp.rooms.leave.contains_key(room_id)); assert!(sync_resp.rooms.invite.contains_key(room_id)); + assert!(!sync_resp.rooms.knocked.contains_key(room_id)); } #[async_test] diff --git a/crates/matrix-sdk-base/src/sync.rs b/crates/matrix-sdk-base/src/sync.rs index d9015e72ea7..4d47cd904ce 100644 --- a/crates/matrix-sdk-base/src/sync.rs +++ b/crates/matrix-sdk-base/src/sync.rs @@ -19,7 +19,7 @@ use std::{collections::BTreeMap, fmt}; use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent}; use ruma::{ api::client::sync::sync_events::{ - v3::InvitedRoom as InvitedRoomUpdate, + v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom}, UnreadNotificationsCount as RumaUnreadNotificationsCount, }, events::{ @@ -33,7 +33,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use crate::{ - debug::{DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId}, + debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId}, deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent}, store::Store, }; @@ -77,6 +77,8 @@ pub struct RoomUpdates { pub join: BTreeMap, /// The rooms that the user has been invited to. pub invite: BTreeMap, + /// The rooms that the user has knocked on. + pub knocked: BTreeMap, } impl RoomUpdates { @@ -89,6 +91,7 @@ impl RoomUpdates { .keys() .chain(self.join.keys()) .chain(self.invite.keys()) + .chain(self.knocked.keys()) .filter_map(|room_id| store.room(room_id)) { let _ = room.compute_display_name().await; @@ -103,6 +106,7 @@ impl fmt::Debug for RoomUpdates { .field("leave", &self.leave) .field("join", &self.join) .field("invite", &DebugInvitedRoomUpdates(&self.invite)) + .field("knocked", &DebugKnockedRoomUpdates(&self.knocked)) .finish() } } @@ -250,6 +254,15 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> { } } +struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap); + +#[cfg(not(tarpaulin_include))] +impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish() + } +} + /// A notification triggered by a sync response. #[derive(Clone)] pub struct Notification { diff --git a/crates/matrix-sdk/src/sync.rs b/crates/matrix-sdk/src/sync.rs index a9f490aa976..5a824ebf845 100644 --- a/crates/matrix-sdk/src/sync.rs +++ b/crates/matrix-sdk/src/sync.rs @@ -22,11 +22,14 @@ use std::{ pub use matrix_sdk_base::sync::*; use matrix_sdk_base::{ - debug::{DebugInvitedRoom, DebugListOfRawEventsNoId}, + debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEventsNoId}, sync::SyncResponse as BaseSyncResponse, }; use ruma::{ - api::client::sync::sync_events::{self, v3::InvitedRoom}, + api::client::sync::sync_events::{ + self, + v3::{InvitedRoom, KnockedRoom}, + }, events::{presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyToDeviceEvent}, serde::Raw, time::Instant, @@ -100,6 +103,13 @@ pub enum RoomUpdate { /// Updates to the room. updates: InvitedRoom, }, + /// Updates to a room the user knocked on. + Knocked { + /// Room object with general information on the room. + room: Room, + /// Updates to the room. + updates: KnockedRoom, + }, } #[cfg(not(tarpaulin_include))] @@ -117,6 +127,11 @@ impl fmt::Debug for RoomUpdate { .field("room", room) .field("updates", &DebugInvitedRoom(updates)) .finish(), + Self::Knocked { room, updates } => f + .debug_struct("Knocked") + .field("room", room) + .field("updates", &DebugKnockedRoom(updates)) + .finish(), } } } @@ -225,6 +240,21 @@ impl Client { self.handle_sync_events(HandlerKind::StrippedState, Some(&room), invite_state).await?; } + for (room_id, room_info) in &rooms.knocked { + let Some(room) = self.get_room(room_id) else { + error!(?room_id, "Can't call event handler, room not found"); + continue; + }; + + self.send_room_update(room_id, || RoomUpdate::Knocked { + room: room.clone(), + updates: room_info.clone(), + }); + + let knock_state = &room_info.knock_state.events; + self.handle_sync_events(HandlerKind::StrippedState, Some(&room), knock_state).await?; + } + debug!("Ran event handlers in {:?}", now.elapsed()); let now = Instant::now(); diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index 3dd739d751f..6e044c28567 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -15,7 +15,10 @@ use matrix_sdk_test::{ async_test, sync_state_event, test_json::{ self, - sync::{MIXED_INVITED_ROOM_ID, MIXED_JOINED_ROOM_ID, MIXED_LEFT_ROOM_ID, MIXED_SYNC}, + sync::{ + MIXED_INVITED_ROOM_ID, MIXED_JOINED_ROOM_ID, MIXED_KNOCKED_ROOM_ID, MIXED_LEFT_ROOM_ID, + MIXED_SYNC, + }, sync_events::PINNED_EVENTS, TAG, }, @@ -340,7 +343,7 @@ async fn test_subscribe_all_room_updates() { client.sync_once(sync_settings).await.unwrap(); let room_updates = rx.recv().now_or_never().unwrap().unwrap(); - assert_let!(RoomUpdates { leave, join, invite } = room_updates); + assert_let!(RoomUpdates { leave, join, invite, knocked } = room_updates); // Check the left room updates. { @@ -383,6 +386,16 @@ async fn test_subscribe_all_room_updates() { assert_eq!(room_id, *MIXED_INVITED_ROOM_ID); assert_eq!(update.invite_state.events.len(), 2); } + + // Check the knocked room updates. + { + assert_eq!(knocked.len(), 1); + + let (room_id, update) = knocked.iter().next().unwrap(); + + assert_eq!(room_id, *MIXED_KNOCKED_ROOM_ID); + assert_eq!(update.knock_state.events.len(), 2); + } } // Check that the `Room::is_encrypted()` is properly deduplicated, meaning we diff --git a/testing/matrix-sdk-test/src/test_json/sync.rs b/testing/matrix-sdk-test/src/test_json/sync.rs index 02a4ea12a83..cbd32dbc46b 100644 --- a/testing/matrix-sdk-test/src/test_json/sync.rs +++ b/testing/matrix-sdk-test/src/test_json/sync.rs @@ -1240,8 +1240,11 @@ pub static MIXED_LEFT_ROOM_ID: Lazy<&RoomId> = /// In the [`MIXED_SYNC`], the room id of the invited room. pub static MIXED_INVITED_ROOM_ID: Lazy<&RoomId> = Lazy::new(|| room_id!("!SVkFJHzfwvuaIEawgE:localhost")); +/// In the [`MIXED_SYNC`], the room id of the knocked room. +pub static MIXED_KNOCKED_ROOM_ID: Lazy<&RoomId> = + Lazy::new(|| room_id!("!SVkFJHzfwvuaIEawgF:localhost")); -/// A sync that contains updates to joined/invited/left rooms. +/// A sync that contains updates to joined/invited/knocked/left rooms. pub static MIXED_SYNC: Lazy = Lazy::new(|| { json!({ "account_data": { @@ -1357,6 +1360,30 @@ pub static MIXED_SYNC: Lazy = Lazy::new(|| { } } }, + "knock": { + *MIXED_KNOCKED_ROOM_ID: { + "knock_state": { + "events": [ + { + "sender": "@alice:example.com", + "type": "m.room.name", + "state_key": "", + "content": { + "name": "My Room Name" + } + }, + { + "sender": "@alice:example.com", + "type": "m.room.member", + "state_key": "@bob:example.com", + "content": { + "membership": "knock" + } + } + ] + } + } + }, "leave": { *MIXED_LEFT_ROOM_ID: { "timeline": {