diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index e39587455ab..2e3835a8cee 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -523,11 +523,22 @@ impl BaseClient { Ok(timeline) } + /// Handles the stripped state events in `invite_state`, modifying the + /// room's info and posting notifications as needed. + /// + /// * `room` - The [`Room`] to modify. + /// * `events` - The contents of `invite_state` in the form of list of pairs + /// of raw stripped state events with their deserialized counterpart. + /// * `push_rules` - The push rules for this room. + /// * `room_info` - The current room's info. + /// * `changes` - The accumulated list of changes to apply once the + /// processing is finished. + /// * `notifications` - Notifications to post for the current room. #[instrument(skip_all, fields(room_id = ?room_info.room_id))] pub(crate) async fn handle_invited_state( &self, room: &Room, - events: &[Raw], + events: &[(Raw, AnyStrippedStateEvent)], push_rules: &Ruleset, room_info: &mut RoomInfo, changes: &mut StateChanges, @@ -535,22 +546,12 @@ impl BaseClient { ) -> Result<()> { let mut state_events = BTreeMap::new(); - for raw_event in events { - match raw_event.deserialize() { - Ok(e) => { - room_info.handle_stripped_state_event(&e); - state_events - .entry(e.event_type()) - .or_insert_with(BTreeMap::new) - .insert(e.state_key().to_owned(), raw_event.clone()); - } - Err(err) => { - warn!( - room_id = ?room_info.room_id, - "Couldn't deserialize stripped state event: {err:?}", - ); - } - } + for (raw_event, event) in events { + room_info.handle_stripped_state_event(event); + state_events + .entry(event.event_type()) + .or_insert_with(BTreeMap::new) + .insert(event.state_key().to_owned(), raw_event.clone()); } changes.stripped_state.insert(room_info.room_id().to_owned(), state_events.clone()); @@ -1121,13 +1122,16 @@ impl BaseClient { self.room_info_notable_update_sender.clone(), ); + let invite_state = + Self::deserialize_stripped_state_events(&new_info.invite_state.events); + 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.invite_state.events, + &invite_state, &push_rules, &mut room_info, &mut changes, @@ -1140,6 +1144,34 @@ 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 knock_state = Self::deserialize_stripped_state_events(&new_info.knock_state.events); + + let mut room_info = room.clone_info(); + room_info.mark_as_knocked(); + room_info.mark_state_fully_synced(); + + self.handle_invited_state( + &room, + &knock_state, + &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 @@ -1582,6 +1614,21 @@ impl BaseClient { .collect() } + pub(crate) fn deserialize_stripped_state_events( + raw_events: &[Raw], + ) -> Vec<(Raw, AnyStrippedStateEvent)> { + raw_events + .iter() + .filter_map(|raw_event| match raw_event.deserialize() { + Ok(event) => Some((raw_event.clone(), event)), + Err(e) => { + warn!("Couldn't deserialize stripped state event: {e}"); + None + } + }) + .collect() + } + /// Returns a new receiver that gets future room info notable updates. /// /// Learn more by reading the [`RoomInfoNotableUpdate`] type. 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..819d1662eec 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -27,10 +27,13 @@ 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, + JsOption, OwnedRoomId, RoomId, UInt, UserId, }; use tracing::{debug, error, instrument, trace, warn}; @@ -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, @@ -167,13 +171,20 @@ impl BaseClient { let mut notifications = Default::default(); let mut rooms_account_data = extensions.account_data.rooms.clone(); + let user_id = self + .session_meta() + .expect("Sliding sync shouldn't run without an authenticated user.") + .user_id + .to_owned(); + 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, &mut rooms_account_data, &store, + &user_id, &account_data_processor, &mut changes, &mut room_info_notable_updates, @@ -196,6 +207,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: @@ -341,14 +356,20 @@ impl BaseClient { room_data: &http::response::Room, rooms_account_data: &mut BTreeMap>>, store: &Store, + user_id: &UserId, account_data_processor: &AccountDataProcessor, changes: &mut StateChanges, room_info_notable_updates: &mut BTreeMap, 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. @@ -402,14 +423,22 @@ impl BaseClient { } } + let stripped_state: Option, AnyStrippedStateEvent)>> = + room_data + .invite_state + .as_ref() + .map(|invite_state| Self::deserialize_stripped_state_events(invite_state)); + #[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( + &state_events, + stripped_state.as_ref(), + store, + user_id, + room_id, + room_info_notable_updates, + ); room_info.mark_state_partially_synced(); @@ -428,7 +457,8 @@ impl BaseClient { let push_rules = self.get_push_rules(account_data_processor).await?; - if let Some(invite_state) = &room_data.invite_state { + // This will be used for both invited and knocked rooms. + if let Some(invite_state) = &stripped_state { self.handle_invited_state( &room, invite_state, @@ -512,6 +542,7 @@ impl BaseClient { )), None, None, + None, )) } @@ -525,12 +556,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)), } } @@ -541,13 +572,14 @@ impl BaseClient { /// otherwise. https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#room-list-parameters fn process_sliding_sync_room_membership( &self, - room_data: &http::response::Room, state_events: &[AnySyncStateEvent], + stripped_state: Option<&Vec<(Raw, AnyStrippedStateEvent)>>, store: &Store, + user_id: &UserId, room_id: &RoomId, room_info_notable_updates: &mut BTreeMap, - ) -> (Room, RoomInfo, Option) { - if let Some(invite_state) = &room_data.invite_state { + ) -> (Room, RoomInfo, Option, Option) { + if let Some(stripped_state) = stripped_state { let room = store.get_or_create_room( room_id, RoomState::Invited, @@ -555,20 +587,34 @@ 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 = stripped_state.iter().find_map(|(_, event)| { + if let AnyStrippedStateEvent::RoomMember(membership_event) = event { + if membership_event.state_key == user_id { + return Some(membership_event.content.clone()); + } + } + None + }); + + 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 raw_events = stripped_state.iter().map(|(raw, _)| raw.clone()).collect(); + let knock_state = assign!(v3::KnockState::default(), { events: raw_events }); + let knocked_room = + assign!(KnockedRoom::default(), { knock_state: knock_state }); + return (room, room_info, None, Some(knocked_room)); + } + } - (room, room_info, Some(InvitedRoom::from(v3::InviteState::from(invite_state.clone())))) + // Otherwise assume it's an invited room + room_info.mark_as_invited(); + let raw_events = stripped_state.iter().map(|(raw, _)| raw.clone()).collect::>(); + let invited_room = InvitedRoom::from(v3::InviteState::from(raw_events)); + (room, room_info, Some(invited_room), None) } else { let room = store.get_or_create_room( room_id, @@ -594,7 +640,7 @@ impl BaseClient { room_info_notable_updates, ); - (room, room_info, None) + (room, room_info, None, None) } } @@ -958,6 +1004,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] @@ -1014,6 +1061,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] @@ -1042,6 +1090,78 @@ mod tests { assert_eq!(client_room.compute_display_name().await.unwrap().to_string(), "The Name"); } + #[async_test] + async fn test_receiving_a_knocked_room_membership_event_creates_a_knocked_room() { + // Given a logged-in client, + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + let user_id = client.session_meta().unwrap().user_id.to_owned(); + + // When the room is properly set as knocked with the current user id as state + // key, + let mut room = http::response::Room::new(); + set_room_knocked(&mut room, &user_id); + + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // The room is knocked. + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!(client_room.state(), RoomState::Knocked); + } + + #[async_test] + async fn test_receiving_a_knocked_room_membership_event_with_wrong_state_key_creates_an_invited_room( + ) { + // Given a logged-in client, + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + let user_id = user_id!("@w:e.uk"); + + // When the room is set as knocked with a random user id as state key, + let mut room = http::response::Room::new(); + set_room_knocked(&mut room, user_id); + + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // The room is invited since the membership event doesn't belong to the current + // user. + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!(client_room.state(), RoomState::Invited); + } + + #[async_test] + async fn test_receiving_an_unknown_room_membership_event_in_invite_state_creates_an_invited_room( + ) { + // Given a logged-in client, + let client = logged_in_base_client(None).await; + let room_id = room_id!("!r:e.uk"); + let user_id = client.session_meta().unwrap().user_id.to_owned(); + + // When the room has the wrong membership state in its invite_state + let mut room = http::response::Room::new(); + let event = Raw::new(&json!({ + "type": "m.room.member", + "sender": user_id, + "content": { + "is_direct": true, + "membership": "join", + }, + "state_key": user_id, + })) + .expect("Failed to make raw event") + .cast(); + room.invite_state = Some(vec![event]); + + let response = response_with_room(room_id, room); + client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync"); + + // The room is marked as invited. + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!(client_room.state(), RoomState::Invited); + } + #[async_test] async fn test_left_a_room_from_required_state_event() { // Given a logged-in client @@ -1072,6 +1192,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] @@ -1113,6 +1234,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)); } } @@ -2555,6 +2677,25 @@ mod tests { )); } + fn set_room_knocked(room: &mut http::response::Room, knocker: &UserId) { + // MSC3575 shows an almost-empty event to indicate that we are invited to a + // room. Just the type is supplied. + + let evt = Raw::new(&json!({ + "type": "m.room.member", + "sender": knocker, + "content": { + "is_direct": true, + "membership": "knock", + }, + "state_key": knocker, + })) + .expect("Failed to make raw event") + .cast(); + + room.invite_state = Some(vec![evt]); + } + fn set_room_joined(room: &mut http::response::Room, user_id: &UserId) { room.required_state.push(make_membership_event(user_id, MembershipState::Join)); } 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..8ad287bb292 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": "@bob:example.com", + "type": "m.room.member", + "state_key": "@bob:example.com", + "content": { + "membership": "knock" + } + } + ] + } + } + }, "leave": { *MIXED_LEFT_ROOM_ID: { "timeline": {