diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 41a15ee020..844f3ea8b7 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -3096,6 +3096,7 @@ class SyncHandler: sync_result_builder.forced_newly_joined_room_ids ) newly_left_rooms: list[str] = [] + left_announcement_room_ids: set[str] = set() room_entries: list[RoomSyncResultBuilder] = [] invited: list[InvitedSyncResult] = [] knocked: list[KnockedSyncResult] = [] @@ -3200,6 +3201,7 @@ class SyncHandler: knocked.append(knock_room_sync) if room_id in sync_result_builder.announcement_room_ids: + left_announcement_room_ids.add(room_id) continue # Always include leave/ban events. Just take the last one. @@ -3311,7 +3313,9 @@ class SyncHandler: room_entries.extend( await self._get_announcement_room_entries( - sync_result_builder, sync_result_builder.announcement_room_ids + sync_result_builder, + sync_result_builder.announcement_room_ids, + left_room_ids=left_announcement_room_ids, ) ) @@ -3327,6 +3331,7 @@ class SyncHandler: self, sync_result_builder: "SyncResultBuilder", room_ids: AbstractSet[str], + left_room_ids: AbstractSet[str] = frozenset(), ) -> list["RoomSyncResultBuilder"]: """Returns entries for the given announcement rooms that have an announcement to show, i.e. a non-state event. @@ -3336,9 +3341,15 @@ class SyncHandler: most recent events hold an announcement. On an initial sync, a room is included if its most recent events hold an announcement. + Rooms in `left_room_ids` are included even without an announcement, so + that the client replaces the user's real membership with the synthetic + join and gets the room's current state. + Args: sync_result_builder room_ids: The announcement rooms to consider. + left_room_ids: Rooms among `room_ids` that the user left or was + removed from since the `since_token`. """ since_token = sync_result_builder.since_token now_token = sync_result_builder.now_token @@ -3357,7 +3368,7 @@ class SyncHandler: room_id for room_id, (events, _, _) in room_to_events.items() if not all(event.is_state() for event in events) - } + } | (left_room_ids & room_ids) if since_token and sync_result_builder.full_state: recent_room_to_events = await self.store.get_room_events_stream_for_rooms( diff --git a/tests/rest/client/test_sync.py b/tests/rest/client/test_sync.py index e5725ddbb7..0504180cc2 100644 --- a/tests/rest/client/test_sync.py +++ b/tests/rest/client/test_sync.py @@ -2242,6 +2242,9 @@ class ServerAnnouncementsSyncTestCase(unittest.HomeserverTestCase): def _timeline_ids(self, room: JsonDict) -> list[str]: return [event["event_id"] for event in room["timeline"]["events"]] + def _timeline_types(self, room: JsonDict) -> list[str]: + return [event["type"] for event in room["timeline"]["events"]] + def _state_types(self, room: JsonDict) -> set[str]: return {event["type"] for event in room["state"]["events"]} @@ -2342,8 +2345,9 @@ class ServerAnnouncementsSyncTestCase(unittest.HomeserverTestCase): ) def test_removed_member(self) -> None: - """A member removed from the room by the config does not get their leave, - then gets later announcements as a non-member, on initial sync too. + """A member removed from the room by the config switches straight to the + synthetic join and the room's current power levels, then gets later + announcements as a non-member, on initial sync too. """ since = self._sync(self.former_tok)["next_batch"] @@ -2352,7 +2356,14 @@ class ServerAnnouncementsSyncTestCase(unittest.HomeserverTestCase): ] self.get_success(self.handler.reconcile_rooms()) response = self._sync(self.former_tok, since) - self._assert_not_delivered(response) + room = self._assert_delivered(response, self.former) + self.assertNotIn(EventTypes.Message, self._timeline_types(room)) + power_levels = [ + event + for event in room["state"]["events"] + room["timeline"]["events"] + if event["type"] == EventTypes.PowerLevels + ][-1] + self.assertNotIn(self.former, power_levels["content"]["users"]) event_id = self._announce("hello") room = self._assert_delivered( @@ -2380,6 +2391,17 @@ class ServerAnnouncementsSyncTestCase(unittest.HomeserverTestCase): room = self._assert_delivered(response, self.former) self.assertEqual(self._timeline_ids(room)[-2:], event_ids) + def test_member_leaving(self) -> None: + """A member who leaves the room on their own switches straight to the + synthetic join, even when no announcement is sent. + """ + since = self._sync(self.former_tok)["next_batch"] + + self.helper.leave(self.room_id, self.former, tok=self.former_tok) + + room = self._assert_delivered(self._sync(self.former_tok, since), self.former) + self.assertEqual(self._timeline_ids(room), []) + def test_initial_sync_after_leaving(self) -> None: """A former member who left on their own gets a single, synthetic join on initial sync, even when their real leave is among the recent events.