In Sliding Sync, find out which rooms we have left from state events

Look through m.room.member events when processing Sliding Sync
responses, finding those that refer to our user's membership, so that we
can recognise which rooms we have left.
This commit is contained in:
Andy Balaam
2023-06-09 15:43:57 +01:00
parent 8aa12c929a
commit 451398612a
2 changed files with 137 additions and 22 deletions

View File

@@ -26,8 +26,8 @@ use ruma::{
room::{
create::RoomCreateEventContent, encryption::RoomEncryptionEventContent,
guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule,
name::RoomNameEventContent, redaction::OriginalSyncRoomRedactionEvent,
tombstone::RoomTombstoneEventContent,
member::MembershipState, name::RoomNameEventContent,
redaction::OriginalSyncRoomRedactionEvent, tombstone::RoomTombstoneEventContent,
},
tag::Tags,
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
@@ -83,6 +83,19 @@ pub enum RoomState {
Invited,
}
impl From<&MembershipState> for RoomState {
fn from(membership_state: &MembershipState) -> Self {
match membership_state {
MembershipState::Ban => Self::Left, // TODO: is this right?
MembershipState::Invite => Self::Invited,
MembershipState::Join => Self::Joined,
MembershipState::Knock => Self::Left, // TODO: is this right?
MembershipState::Leave => Self::Left,
_ => panic!("Unexpected MembershipState: {}", membership_state),
}
}
}
impl Room {
pub(crate) fn new(
own_user_id: &UserId,
@@ -637,6 +650,10 @@ impl RoomInfo {
self.room_state = RoomState::Invited;
}
pub(crate) 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;

View File

@@ -20,9 +20,11 @@ use ruma::{
v3::{self, InvitedRoom, RoomSummary},
v4::{self, AccountData},
},
events::AnySyncStateEvent,
serde::Raw,
RoomId,
};
use tracing::{debug, info, instrument};
use tracing::{debug, info, instrument, warn};
use super::BaseClient;
#[cfg(feature = "e2e-encryption")]
@@ -269,7 +271,8 @@ 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,
@@ -285,13 +288,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 +308,55 @@ 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(&room_data.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: &[Raw<AnySyncStateEvent>],
room_info: &mut RoomInfo,
) {
// Note: we deserialise these same events inside handle_state(). We could refactor to do
// this only once.
for raw_event in required_state {
let event = match raw_event.deserialize() {
Ok(ev) => ev,
Err(e) => {
warn!("Couldn't deserialize state event: {e}");
continue;
}
};
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 +388,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 +457,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 +471,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 invite)
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 +531,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 +559,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 +581,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 +638,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 +649,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<AnySyncStateEvent> {
make_state_event(user_id, user_id.as_str(), RoomMemberEventContent::new(state), None)
}
fn make_state_event<C: StateEventContent, E>(