diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 2d520e616..36adf7662 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -17,7 +17,7 @@ use std::ops::Deref; use std::{ collections::{BTreeMap, BTreeSet}, - fmt, + fmt, iter, sync::Arc, }; @@ -484,10 +484,16 @@ impl BaseClient { changes.stripped_state.insert(room_info.room_id().to_owned(), state_events); } + /// Process the events provided during a sync. + /// + /// events must be exactly the same list of events that are in raw_events, + /// but deserialised. We demand them here to avoid deserialising + /// multiple times. #[instrument(skip_all, fields(room_id = ?room_info.room_id))] pub(crate) async fn handle_state( &self, - events: &[Raw], + raw_events: &[Raw], + events: &[AnySyncStateEvent], room_info: &mut RoomInfo, changes: &mut StateChanges, ambiguity_cache: &mut AmbiguityCache, @@ -496,16 +502,8 @@ impl BaseClient { let mut user_ids = BTreeSet::new(); let mut profiles = BTreeMap::new(); - for raw_event in events { - let event = match raw_event.deserialize() { - Ok(ev) => ev, - Err(e) => { - warn!("Couldn't deserialize state event: {e}"); - continue; - } - }; - - room_info.handle_state_event(&event); + for (raw_event, event) in iter::zip(raw_events, events) { + room_info.handle_state_event(event); if let AnySyncStateEvent::RoomMember(member) = &event { ambiguity_cache.handle_event(changes, &room_info.room_id, member).await?; @@ -720,9 +718,12 @@ impl BaseClient { room_info.set_prev_batch(new_info.timeline.prev_batch.as_deref()); room_info.mark_state_fully_synced(); + let deserialized_events = Self::deserialize_events(&new_info.state.events); + let mut user_ids = self .handle_state( &new_info.state.events, + &deserialized_events, &mut room_info, &mut changes, &mut ambiguity_cache, @@ -806,9 +807,12 @@ impl BaseClient { room_info.mark_as_left(); room_info.mark_state_partially_synced(); + let deserialized_events = Self::deserialize_events(&new_info.state.events); + let mut user_ids = self .handle_state( &new_info.state.events, + &deserialized_events, &mut room_info, &mut changes, &mut ambiguity_cache, @@ -1237,6 +1241,21 @@ impl BaseClient { pub fn subscribe_to_ignore_user_list_changes(&self) -> Subscriber<()> { self.ignore_user_list_changes_tx.subscribe() } + + pub(crate) fn deserialize_events( + raw_events: &[Raw], + ) -> Vec { + raw_events + .iter() + .filter_map(|raw_event| match raw_event.deserialize() { + Ok(ev) => Some(ev), + Err(e) => { + warn!("Couldn't deserialize state event: {e}"); + None + } + }) + .collect() + } } impl Default for BaseClient { diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index c475f1822..7e342b1ee 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -25,10 +25,15 @@ use ruma::{ ignored_user_list::IgnoredUserListEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, room::{ - create::RoomCreateEventContent, encryption::RoomEncryptionEventContent, - guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule, - member::RoomMemberEventContent, name::RoomNameEventContent, - redaction::OriginalSyncRoomRedactionEvent, tombstone::RoomTombstoneEventContent, + create::RoomCreateEventContent, + encryption::RoomEncryptionEventContent, + guest_access::GuestAccess, + history_visibility::HistoryVisibility, + join_rules::JoinRule, + member::{MembershipState, RoomMemberEventContent}, + name::RoomNameEventContent, + redaction::OriginalSyncRoomRedactionEvent, + tombstone::RoomTombstoneEventContent, }, tag::Tags, AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, @@ -87,6 +92,21 @@ pub enum RoomState { Invited, } +impl From<&MembershipState> for RoomState { + fn from(membership_state: &MembershipState) -> Self { + // We consider Ban, Knock and Leave to be Left, because they all mean we are not + // in the room. + match membership_state { + MembershipState::Ban => Self::Left, + MembershipState::Invite => Self::Invited, + MembershipState::Join => Self::Joined, + MembershipState::Knock => Self::Left, + MembershipState::Leave => Self::Left, + _ => panic!("Unexpected MembershipState: {}", membership_state), + } + } +} + impl Room { pub(crate) fn new( own_user_id: &UserId, @@ -680,6 +700,11 @@ impl RoomInfo { self.room_state = RoomState::Invited; } + /// Set the membership RoomState of this Room + pub fn set_state(&mut self, room_state: RoomState) { + self.room_state = room_state; + } + /// Mark this Room as having all the members synced. pub fn mark_members_synced(&mut self) { self.members_synced = true; diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index a1bac0c7c..7bbb2eb49 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -20,6 +20,7 @@ use ruma::{ v3::{self, InvitedRoom, RoomSummary}, v4::{self, AccountData}, }, + events::AnySyncStateEvent, RoomId, }; use tracing::{debug, info, instrument}; @@ -191,15 +192,30 @@ impl BaseClient { ambiguity_cache: &mut AmbiguityCache, account_data: &AccountData, ) -> Result<(RoomInfo, Option, Option)> { + let required_state = Self::deserialize_events(&room_data.required_state); + // Find or create the room in the store - let (room, mut room_info, invited_room) = - self.process_sliding_sync_room_membership(room_data, store, room_id, changes).await; + let (room, mut room_info, invited_room) = self + .process_sliding_sync_room_membership( + room_data, + &required_state, + store, + room_id, + changes, + ) + .await; room_info.mark_state_partially_synced(); - let mut user_ids = if !room_data.required_state.is_empty() { - self.handle_state(&room_data.required_state, &mut room_info, changes, ambiguity_cache) - .await? + let mut user_ids = if !required_state.is_empty() { + self.handle_state( + &room_data.required_state, + &required_state, + &mut room_info, + changes, + ambiguity_cache, + ) + .await? } else { Default::default() }; @@ -269,10 +285,12 @@ impl BaseClient { /// Look through the sliding sync data for this room, find/create it in the /// store, and process any invite information. /// If any invite_state exists, we take it to mean that we are invited to - /// this room https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#room-list-parameters + /// this room, unless that state contains membership events that specify + /// otherwise. https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#room-list-parameters async fn process_sliding_sync_room_membership( &self, room_data: &v4::SlidingSyncRoom, + required_state: &[AnySyncStateEvent], store: &Store, room_id: &RoomId, changes: &mut StateChanges, @@ -285,13 +303,13 @@ impl BaseClient { // 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. Later, 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. + // 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(); self.handle_invited_state(invite_state.as_slice(), &mut room_info, changes); @@ -305,15 +323,44 @@ impl BaseClient { let room = store.get_or_create_room(room_id, RoomState::Joined).await; let mut room_info = room.clone_info(); - // We default to considering this room joined if it's not an invite, but we - // expect to find a m.room.member event in the required_state, so - // this should get fixed to the real correct value when we call - // self.handle_state below. + // We default to considering this room joined if it's not an invite. If it's + // actually left (and we remembered to request membership events in + // our sync request), then we can find this out from the events in + // required_state by calling handle_own_room_membership. room_info.mark_as_joined(); + // We don't need to do this in a v2 sync, because the membership of a room can + // be figured out by whether the room is in the "join", "leave" etc. + // property. In sliding sync we only have invite_state and + // required_state, so we must process required_state looking for + // relevant membership events. + self.handle_own_room_membership(required_state, &mut room_info).await; + (room, room_info, None) } } + + /// Find any m.room.member events that refer to the current user, and update + /// the state in room_info to reflect the "membership" property. + pub(crate) async fn handle_own_room_membership( + &self, + required_state: &[AnySyncStateEvent], + room_info: &mut RoomInfo, + ) { + for event in required_state { + if let AnySyncStateEvent::RoomMember(member) = &event { + // If this event updates the current user's membership, record that in the + // room_info. + if let Some(meta) = self.session_meta() { + if member.sender() == meta.user_id + && member.state_key() == meta.user_id.as_str() + { + room_info.set_state(member.membership().into()); + } + } + } + } + } } fn process_room_properties(room_data: &v4::SlidingSyncRoom, room_info: &mut RoomInfo) { @@ -345,9 +392,11 @@ mod test { device_id, event_id, events::{ room::{ - avatar::RoomAvatarEventContent, canonical_alias::RoomCanonicalAliasEventContent, + avatar::RoomAvatarEventContent, + canonical_alias::RoomCanonicalAliasEventContent, + member::{MembershipState, RoomMemberEventContent}, }, - StateEventContent, + AnySyncStateEvent, StateEventContent, }, mxc_uri, room_alias_id, room_id, serde::Raw, @@ -412,10 +461,11 @@ mod test { // Given a logged-in client let client = logged_in_client().await; let room_id = room_id!("!r:e.uk"); + let user_id = user_id!("@w:e.uk"); // When I send sliding sync response containing a room with a name let mut room = v4::SlidingSyncRoom::new(); - set_room_invited(&mut room); + set_room_invited(&mut room, user_id); room.name = Some("little room".to_owned()); let response = response_with_room(room_id, room).await; client.process_sliding_sync(&response).await.expect("Failed to process sync"); @@ -425,6 +475,41 @@ mod test { assert_eq!(client_room.name(), Some("little room".to_owned())); } + #[async_test] + async fn can_be_reinvited_to_a_left_room() { + // See https://github.com/matrix-org/matrix-rust-sdk/issues/1834 + + // Given a logged-in client + let client = logged_in_client().await; + let room_id = room_id!("!r:e.uk"); + let user_id = user_id!("@u:e.uk"); + + // When I join... + let mut room = v4::SlidingSyncRoom::new(); + set_room_joined(&mut room, user_id); + let response = response_with_room(room_id, room).await; + client.process_sliding_sync(&response).await.expect("Failed to process sync"); + // (sanity: state is join) + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined); + + // And then leave... + let mut room = v4::SlidingSyncRoom::new(); + set_room_left(&mut room, user_id); + let response = response_with_room(room_id, room).await; + client.process_sliding_sync(&response).await.expect("Failed to process sync"); + // (sanity: state is left) + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left); + + // And then get invited back + let mut room = v4::SlidingSyncRoom::new(); + set_room_invited(&mut room, user_id); + let response = response_with_room(room_id, room).await; + client.process_sliding_sync(&response).await.expect("Failed to process sync"); + + // Then the room is in the invite state + assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited); + } + #[async_test] async fn avatar_is_found_when_processing_sliding_sync_response() { // Given a logged-in client @@ -450,10 +535,11 @@ mod test { // Given a logged-in client let client = logged_in_client().await; let room_id = room_id!("!r:e.uk"); + let user_id = user_id!("@u:e.uk"); // When I send sliding sync response containing an invited room let mut room = v4::SlidingSyncRoom::new(); - set_room_invited(&mut room); + set_room_invited(&mut room, user_id); let response = response_with_room(room_id, room).await; let sync_resp = client.process_sliding_sync(&response).await.expect("Failed to process sync"); @@ -477,7 +563,7 @@ mod test { // When I send sliding sync response containing an invited room with an avatar let mut room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id); - set_room_invited(&mut room); + set_room_invited(&mut room, user_id); let response = response_with_room(room_id, room).await; client.process_sliding_sync(&response).await.expect("Failed to process sync"); @@ -499,7 +585,7 @@ mod test { // When I send sliding sync response containing an invited room with an avatar let mut room = room_with_canonical_alias(room_alias_id, user_id); - set_room_invited(&mut room); + set_room_invited(&mut room, user_id); let response = response_with_room(room_id, room).await; client.process_sliding_sync(&response).await.expect("Failed to process sync"); @@ -556,7 +642,7 @@ mod test { room } - fn set_room_invited(room: &mut v4::SlidingSyncRoom) { + fn set_room_invited(room: &mut v4::SlidingSyncRoom, user_id: &UserId) { // MSC3575 shows an almost-empty event to indicate that we are invited to a // room. Just the type is supplied. @@ -567,6 +653,22 @@ mod test { .cast(); room.invite_state = Some(vec![evt]); + + // We expect that there will also be an invite event in the required_state, + // assuming you've asked for this type of event. + room.required_state.push(make_membership_event(user_id, MembershipState::Invite)); + } + + fn set_room_joined(room: &mut v4::SlidingSyncRoom, user_id: &UserId) { + room.required_state.push(make_membership_event(user_id, MembershipState::Join)); + } + + fn set_room_left(room: &mut v4::SlidingSyncRoom, user_id: &UserId) { + room.required_state.push(make_membership_event(user_id, MembershipState::Leave)); + } + + fn make_membership_event(user_id: &UserId, state: MembershipState) -> Raw { + make_state_event(user_id, user_id.as_str(), RoomMemberEventContent::new(state), None) } fn make_state_event(