# # This file is licensed under the Affero General Public License (AGPL) version 3. # # Copyright (C) 2024 New Vector, Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # See the GNU Affero General Public License for more details: # . # import logging from parameterized import parameterized, parameterized_class from twisted.test.proto_helpers import MemoryReactor import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes, Membership from synapse.api.room_versions import RoomVersions from synapse.rest.client import login, room, sync from synapse.server import HomeServer from synapse.util import Clock from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase from tests.test_utils.event_injection import create_event logger = logging.getLogger(__name__) # FIXME: This can be removed once we bump `SCHEMA_COMPAT_VERSION` and run the # foreground update for # `sliding_sync_joined_rooms`/`sliding_sync_membership_snapshots` (tracked by # https://github.com/element-hq/synapse/issues/17623) @parameterized_class( ("use_new_tables",), [ (True,), (False,), ], class_name_func=lambda cls, num, params_dict: f"{cls.__name__}_{'new' if params_dict['use_new_tables'] else 'fallback'}", ) class SlidingSyncRoomsMetaTestCase(SlidingSyncBase): """ Test rooms meta info like name, avatar, joined_count, invited_count, is_dm, bump_stamp in the Sliding Sync API. """ servlets = [ synapse.rest.admin.register_servlets, login.register_servlets, room.register_servlets, sync.register_servlets, ] def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: self.store = hs.get_datastores().main self.storage_controllers = hs.get_storage_controllers() self.state_handler = self.hs.get_state_handler() persistence = self.hs.get_storage_controllers().persistence assert persistence is not None self.persistence = persistence super().prepare(reactor, clock, hs) def test_rooms_meta_when_joined_initial(self) -> None: """ Test that the `rooms` `name` and `avatar` are included in the initial sync response and reflect the current state of the room when the user is joined to the room. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "name": "my super room", }, ) # Set the room avatar URL self.helper.send_state( room_id1, EventTypes.RoomAvatar, {"url": "mxc://DUMMY_MEDIA_ID"}, tok=user2_tok, ) self.helper.join(room_id1, user1_id, tok=user1_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 0, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # Reflect the current state of the room self.assertEqual(response_body["rooms"][room_id1]["initial"], True) self.assertEqual( response_body["rooms"][room_id1]["name"], "my super room", response_body["rooms"][room_id1], ) self.assertEqual( response_body["rooms"][room_id1]["avatar"], "mxc://DUMMY_MEDIA_ID", response_body["rooms"][room_id1], ) self.assertEqual( response_body["rooms"][room_id1]["joined_count"], 2, ) self.assertEqual( response_body["rooms"][room_id1]["invited_count"], 0, ) self.assertIsNone( response_body["rooms"][room_id1].get("is_dm"), ) def test_rooms_meta_when_joined_incremental_no_change(self) -> None: """ Test that the `rooms` `name` and `avatar` aren't included in an incremental sync response if they haven't changed. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "name": "my super room", }, ) # Set the room avatar URL self.helper.send_state( room_id1, EventTypes.RoomAvatar, {"url": "mxc://DUMMY_MEDIA_ID"}, tok=user2_tok, ) self.helper.join(room_id1, user1_id, tok=user1_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], # This needs to be set to one so the `RoomResult` isn't empty and # the room comes down incremental sync when we send a new message. "timeline_limit": 1, } } } response_body, from_token = self.do_sync(sync_body, tok=user1_tok) # Send a message to make the room come down sync self.helper.send(room_id1, "message in room1", tok=user2_tok) # Incremental sync response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok) # We should only see changed meta info (nothing changed so we shouldn't see any # of these fields) self.assertNotIn( "initial", response_body["rooms"][room_id1], ) self.assertNotIn( "name", response_body["rooms"][room_id1], ) self.assertNotIn( "avatar", response_body["rooms"][room_id1], ) self.assertNotIn( "joined_count", response_body["rooms"][room_id1], ) self.assertNotIn( "invited_count", response_body["rooms"][room_id1], ) self.assertIsNone( response_body["rooms"][room_id1].get("is_dm"), ) @parameterized.expand( [ ("in_required_state", True), ("not_in_required_state", False), ] ) def test_rooms_meta_when_joined_incremental_with_state_change( self, test_description: str, include_changed_state_in_required_state: bool ) -> None: """ Test that the `rooms` `name` and `avatar` are included in an incremental sync response if they changed. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "name": "my super room", }, ) # Set the room avatar URL self.helper.send_state( room_id1, EventTypes.RoomAvatar, {"url": "mxc://DUMMY_MEDIA_ID"}, tok=user2_tok, ) self.helper.join(room_id1, user1_id, tok=user1_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": ( [[EventTypes.Name, ""], [EventTypes.RoomAvatar, ""]] # Conditionally include the changed state in the # `required_state` to make sure whether we request it or not, # the new room name still flows down to the client. if include_changed_state_in_required_state else [] ), "timeline_limit": 0, } } } response_body, from_token = self.do_sync(sync_body, tok=user1_tok) # Update the room name self.helper.send_state( room_id1, EventTypes.Name, {EventContentFields.ROOM_NAME: "my super duper room"}, tok=user2_tok, ) # Update the room avatar URL self.helper.send_state( room_id1, EventTypes.RoomAvatar, {"url": "mxc://DUMMY_MEDIA_ID_UPDATED"}, tok=user2_tok, ) # Incremental sync response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok) # We should only see changed meta info (the room name and avatar) self.assertNotIn( "initial", response_body["rooms"][room_id1], ) self.assertEqual( response_body["rooms"][room_id1]["name"], "my super duper room", response_body["rooms"][room_id1], ) self.assertEqual( response_body["rooms"][room_id1]["avatar"], "mxc://DUMMY_MEDIA_ID_UPDATED", response_body["rooms"][room_id1], ) self.assertNotIn( "joined_count", response_body["rooms"][room_id1], ) self.assertNotIn( "invited_count", response_body["rooms"][room_id1], ) self.assertIsNone( response_body["rooms"][room_id1].get("is_dm"), ) def test_rooms_meta_when_invited(self) -> None: """ Test that the `rooms` `name` and `avatar` are included in the response and reflect the current state of the room when the user is invited to the room. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "name": "my super room", }, ) # Set the room avatar URL self.helper.send_state( room_id1, EventTypes.RoomAvatar, {"url": "mxc://DUMMY_MEDIA_ID"}, tok=user2_tok, ) # User1 is invited to the room self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok) # Update the room name after user1 has left self.helper.send_state( room_id1, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # Update the room avatar URL after user1 has left self.helper.send_state( room_id1, EventTypes.RoomAvatar, {"url": "mxc://UPDATED_DUMMY_MEDIA_ID"}, tok=user2_tok, ) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 0, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # This should still reflect the current state of the room even when the user is # invited. self.assertEqual(response_body["rooms"][room_id1]["initial"], True) self.assertEqual( response_body["rooms"][room_id1]["name"], "my super duper room", response_body["rooms"][room_id1], ) self.assertEqual( response_body["rooms"][room_id1]["avatar"], "mxc://UPDATED_DUMMY_MEDIA_ID", response_body["rooms"][room_id1], ) # We don't give extra room information to invitees self.assertNotIn( "joined_count", response_body["rooms"][room_id1], ) self.assertNotIn( "invited_count", response_body["rooms"][room_id1], ) self.assertIsNone( response_body["rooms"][room_id1].get("is_dm"), ) def test_rooms_meta_when_banned(self) -> None: """ Test that the `rooms` `name` and `avatar` reflect the state of the room when the user was banned (do not leak current state). """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "name": "my super room", }, ) # Set the room avatar URL self.helper.send_state( room_id1, EventTypes.RoomAvatar, {"url": "mxc://DUMMY_MEDIA_ID"}, tok=user2_tok, ) self.helper.join(room_id1, user1_id, tok=user1_tok) self.helper.ban(room_id1, src=user2_id, targ=user1_id, tok=user2_tok) # Update the room name after user1 has left self.helper.send_state( room_id1, EventTypes.Name, {"name": "my super duper room"}, tok=user2_tok, ) # Update the room avatar URL after user1 has left self.helper.send_state( room_id1, EventTypes.RoomAvatar, {"url": "mxc://UPDATED_DUMMY_MEDIA_ID"}, tok=user2_tok, ) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 0, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # Reflect the state of the room at the time of leaving self.assertEqual(response_body["rooms"][room_id1]["initial"], True) self.assertEqual( response_body["rooms"][room_id1]["name"], "my super room", response_body["rooms"][room_id1], ) self.assertEqual( response_body["rooms"][room_id1]["avatar"], "mxc://DUMMY_MEDIA_ID", response_body["rooms"][room_id1], ) # FIXME: We possibly want to return joined and invited counts for rooms # you're banned form self.assertNotIn( "joined_count", response_body["rooms"][room_id1], ) self.assertNotIn( "invited_count", response_body["rooms"][room_id1], ) self.assertIsNone( response_body["rooms"][room_id1].get("is_dm"), ) def test_rooms_meta_heroes(self) -> None: """ Test that the `rooms` `heroes` are included in the response when the room doesn't have a room name set. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user3_id = self.register_user("user3", "pass") _user3_tok = self.login(user3_id, "pass") room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ "name": "my super room", }, ) self.helper.join(room_id1, user1_id, tok=user1_tok) # User3 is invited self.helper.invite(room_id1, src=user2_id, targ=user3_id, tok=user2_tok) room_id2 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ # No room name set so that `heroes` is populated # # "name": "my super room2", }, ) self.helper.join(room_id2, user1_id, tok=user1_tok) # User3 is invited self.helper.invite(room_id2, src=user2_id, targ=user3_id, tok=user2_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 0, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # Room1 has a name so we shouldn't see any `heroes` which the client would use # the calculate the room name themselves. self.assertEqual(response_body["rooms"][room_id1]["initial"], True) self.assertEqual( response_body["rooms"][room_id1]["name"], "my super room", response_body["rooms"][room_id1], ) self.assertIsNone(response_body["rooms"][room_id1].get("heroes")) self.assertEqual( response_body["rooms"][room_id1]["joined_count"], 2, ) self.assertEqual( response_body["rooms"][room_id1]["invited_count"], 1, ) # Room2 doesn't have a name so we should see `heroes` populated self.assertEqual(response_body["rooms"][room_id2]["initial"], True) self.assertIsNone(response_body["rooms"][room_id2].get("name")) self.assertCountEqual( [ hero["user_id"] for hero in response_body["rooms"][room_id2].get("heroes", []) ], # Heroes shouldn't include the user themselves (we shouldn't see user1) [user2_id, user3_id], ) self.assertEqual( response_body["rooms"][room_id2]["joined_count"], 2, ) self.assertEqual( response_body["rooms"][room_id2]["invited_count"], 1, ) # We didn't request any state so we shouldn't see any `required_state` self.assertIsNone(response_body["rooms"][room_id1].get("required_state")) self.assertIsNone(response_body["rooms"][room_id2].get("required_state")) def test_rooms_meta_heroes_max(self) -> None: """ Test that the `rooms` `heroes` only includes the first 5 users (not including yourself). """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user3_id = self.register_user("user3", "pass") user3_tok = self.login(user3_id, "pass") user4_id = self.register_user("user4", "pass") user4_tok = self.login(user4_id, "pass") user5_id = self.register_user("user5", "pass") user5_tok = self.login(user5_id, "pass") user6_id = self.register_user("user6", "pass") user6_tok = self.login(user6_id, "pass") user7_id = self.register_user("user7", "pass") user7_tok = self.login(user7_id, "pass") room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ # No room name set so that `heroes` is populated # # "name": "my super room", }, ) self.helper.join(room_id1, user1_id, tok=user1_tok) self.helper.join(room_id1, user3_id, tok=user3_tok) self.helper.join(room_id1, user4_id, tok=user4_tok) self.helper.join(room_id1, user5_id, tok=user5_tok) self.helper.join(room_id1, user6_id, tok=user6_tok) self.helper.join(room_id1, user7_id, tok=user7_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 0, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # Room2 doesn't have a name so we should see `heroes` populated self.assertEqual(response_body["rooms"][room_id1]["initial"], True) self.assertIsNone(response_body["rooms"][room_id1].get("name")) self.assertCountEqual( [ hero["user_id"] for hero in response_body["rooms"][room_id1].get("heroes", []) ], # Heroes should be the first 5 users in the room (excluding the user # themselves, we shouldn't see `user1`) [user2_id, user3_id, user4_id, user5_id, user6_id], ) self.assertEqual( response_body["rooms"][room_id1]["joined_count"], 7, ) self.assertEqual( response_body["rooms"][room_id1]["invited_count"], 0, ) # We didn't request any state so we shouldn't see any `required_state` self.assertIsNone(response_body["rooms"][room_id1].get("required_state")) def test_rooms_meta_heroes_when_banned(self) -> None: """ Test that the `rooms` `heroes` are included in the response when the room doesn't have a room name set but doesn't leak information past their ban. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user3_id = self.register_user("user3", "pass") _user3_tok = self.login(user3_id, "pass") user4_id = self.register_user("user4", "pass") user4_tok = self.login(user4_id, "pass") user5_id = self.register_user("user5", "pass") _user5_tok = self.login(user5_id, "pass") room_id1 = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ # No room name set so that `heroes` is populated # # "name": "my super room", }, ) # User1 joins the room self.helper.join(room_id1, user1_id, tok=user1_tok) # User3 is invited self.helper.invite(room_id1, src=user2_id, targ=user3_id, tok=user2_tok) # User1 is banned from the room self.helper.ban(room_id1, src=user2_id, targ=user1_id, tok=user2_tok) # User4 joins the room after user1 is banned self.helper.join(room_id1, user4_id, tok=user4_tok) # User5 is invited after user1 is banned self.helper.invite(room_id1, src=user2_id, targ=user5_id, tok=user2_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 0, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # Room doesn't have a name so we should see `heroes` populated self.assertEqual(response_body["rooms"][room_id1]["initial"], True) self.assertIsNone(response_body["rooms"][room_id1].get("name")) self.assertCountEqual( [ hero["user_id"] for hero in response_body["rooms"][room_id1].get("heroes", []) ], # Heroes shouldn't include the user themselves (we shouldn't see user1). We # also shouldn't see user4 since they joined after user1 was banned. # # FIXME: The actual result should be `[user2_id, user3_id]` but we currently # don't support this for rooms where the user has left/been banned. [], ) # FIXME: We possibly want to return joined and invited counts for rooms # you're banned form self.assertNotIn( "joined_count", response_body["rooms"][room_id1], ) self.assertNotIn( "invited_count", response_body["rooms"][room_id1], ) def test_rooms_meta_heroes_incremental_sync_no_change(self) -> None: """ Test that the `rooms` `heroes` aren't included in an incremental sync response if they haven't changed. (when the room doesn't have a room name set) """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user3_id = self.register_user("user3", "pass") _user3_tok = self.login(user3_id, "pass") room_id = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ # No room name set so that `heroes` is populated # # "name": "my super room2", }, ) self.helper.join(room_id, user1_id, tok=user1_tok) # User3 is invited self.helper.invite(room_id, src=user2_id, targ=user3_id, tok=user2_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], # This needs to be set to one so the `RoomResult` isn't empty and # the room comes down incremental sync when we send a new message. "timeline_limit": 1, } } } response_body, from_token = self.do_sync(sync_body, tok=user1_tok) # Send a message to make the room come down sync self.helper.send(room_id, "message in room", tok=user2_tok) # Incremental sync response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok) # This is an incremental sync and the second time we have seen this room so it # isn't `initial` self.assertNotIn( "initial", response_body["rooms"][room_id], ) # Room shouldn't have a room name because we're testing the `heroes` field which # will only has a chance to appear if the room doesn't have a name. self.assertNotIn( "name", response_body["rooms"][room_id], ) # No change to heroes self.assertNotIn( "heroes", response_body["rooms"][room_id], ) # No change to member counts self.assertNotIn( "joined_count", response_body["rooms"][room_id], ) self.assertNotIn( "invited_count", response_body["rooms"][room_id], ) # We didn't request any state so we shouldn't see any `required_state` self.assertNotIn( "required_state", response_body["rooms"][room_id], ) def test_rooms_meta_heroes_incremental_sync_with_membership_change(self) -> None: """ Test that the `rooms` `heroes` are included in an incremental sync response if the membership has changed. (when the room doesn't have a room name set) """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") user3_id = self.register_user("user3", "pass") user3_tok = self.login(user3_id, "pass") room_id = self.helper.create_room_as( user2_id, tok=user2_tok, extra_content={ # No room name set so that `heroes` is populated # # "name": "my super room2", }, ) self.helper.join(room_id, user1_id, tok=user1_tok) # User3 is invited self.helper.invite(room_id, src=user2_id, targ=user3_id, tok=user2_tok) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 0, } } } response_body, from_token = self.do_sync(sync_body, tok=user1_tok) # User3 joins (membership change) self.helper.join(room_id, user3_id, tok=user3_tok) # Incremental sync response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok) # This is an incremental sync and the second time we have seen this room so it # isn't `initial` self.assertNotIn( "initial", response_body["rooms"][room_id], ) # Room shouldn't have a room name because we're testing the `heroes` field which # will only has a chance to appear if the room doesn't have a name. self.assertNotIn( "name", response_body["rooms"][room_id], ) # Membership change so we should see heroes and membership counts self.assertCountEqual( [ hero["user_id"] for hero in response_body["rooms"][room_id].get("heroes", []) ], # Heroes shouldn't include the user themselves (we shouldn't see user1) [user2_id, user3_id], ) self.assertEqual( response_body["rooms"][room_id]["joined_count"], 3, ) self.assertEqual( response_body["rooms"][room_id]["invited_count"], 0, ) # We didn't request any state so we shouldn't see any `required_state` self.assertNotIn( "required_state", response_body["rooms"][room_id], ) def test_rooms_bump_stamp(self) -> None: """ Test that `bump_stamp` is present and pointing to relevant events. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") room_id1 = self.helper.create_room_as( user1_id, tok=user1_tok, ) event_response1 = message_response = self.helper.send( room_id1, "message in room1", tok=user1_tok ) event_pos1 = self.get_success( self.store.get_position_for_event(event_response1["event_id"]) ) room_id2 = self.helper.create_room_as( user1_id, tok=user1_tok, ) send_response2 = self.helper.send(room_id2, "message in room2", tok=user1_tok) event_pos2 = self.get_success( self.store.get_position_for_event(send_response2["event_id"]) ) # Send a reaction in room1 but it shouldn't affect the `bump_stamp` # because reactions are not part of the `DEFAULT_BUMP_EVENT_TYPES` self.helper.send_event( room_id1, type=EventTypes.Reaction, content={ "m.relates_to": { "event_id": message_response["event_id"], "key": "👍", "rel_type": "m.annotation", } }, tok=user1_tok, ) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 100, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # Make sure it has the foo-list we requested self.assertListEqual( list(response_body["lists"].keys()), ["foo-list"], response_body["lists"].keys(), ) # Make sure the list includes the rooms in the right order self.assertEqual( len(response_body["lists"]["foo-list"]["ops"]), 1, response_body["lists"]["foo-list"], ) op = response_body["lists"]["foo-list"]["ops"][0] self.assertEqual(op["op"], "SYNC") self.assertEqual(op["range"], [0, 1]) # Note that we don't sort the rooms when the range includes all of the rooms, so # we just assert that the rooms are included self.assertIncludes(set(op["room_ids"]), {room_id1, room_id2}, exact=True) # The `bump_stamp` for room1 should point at the latest message (not the # reaction since it's not one of the `DEFAULT_BUMP_EVENT_TYPES`) self.assertEqual( response_body["rooms"][room_id1]["bump_stamp"], event_pos1.stream, response_body["rooms"][room_id1], ) # The `bump_stamp` for room2 should point at the latest message self.assertEqual( response_body["rooms"][room_id2]["bump_stamp"], event_pos2.stream, response_body["rooms"][room_id2], ) def test_rooms_bump_stamp_backfill(self) -> None: """ Test that `bump_stamp` ignores backfilled events, i.e. events with a negative stream ordering. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") # Create a remote room creator = "@user:other" room_id = "!foo:other" room_version = RoomVersions.V10 shared_kwargs = { "room_id": room_id, "room_version": room_version.identifier, } create_tuple = self.get_success( create_event( self.hs, prev_event_ids=[], type=EventTypes.Create, state_key="", content={ # The `ROOM_CREATOR` field could be removed if we used a room # version > 10 (in favor of relying on `sender`) EventContentFields.ROOM_CREATOR: creator, EventContentFields.ROOM_VERSION: room_version.identifier, }, sender=creator, **shared_kwargs, ) ) creator_tuple = self.get_success( create_event( self.hs, prev_event_ids=[create_tuple[0].event_id], auth_event_ids=[create_tuple[0].event_id], type=EventTypes.Member, state_key=creator, content={"membership": Membership.JOIN}, sender=creator, **shared_kwargs, ) ) # We add a message event as a valid "bump type" msg_tuple = self.get_success( create_event( self.hs, prev_event_ids=[creator_tuple[0].event_id], auth_event_ids=[create_tuple[0].event_id], type=EventTypes.Message, content={"body": "foo", "msgtype": "m.text"}, sender=creator, **shared_kwargs, ) ) invite_tuple = self.get_success( create_event( self.hs, prev_event_ids=[msg_tuple[0].event_id], auth_event_ids=[create_tuple[0].event_id, creator_tuple[0].event_id], type=EventTypes.Member, state_key=user1_id, content={"membership": Membership.INVITE}, sender=creator, **shared_kwargs, ) ) remote_events_and_contexts = [ create_tuple, creator_tuple, msg_tuple, invite_tuple, ] # Ensure the local HS knows the room version self.get_success(self.store.store_room(room_id, creator, False, room_version)) # Persist these events as backfilled events. for event, context in remote_events_and_contexts: self.get_success( self.persistence.persist_event(event, context, backfilled=True) ) # Now we join the local user to the room. We want to make this feel as close to # the real `process_remote_join()` as possible but we'd like to avoid some of # the auth checks that would be done in the real code. # # FIXME: The test was originally written using this less-real # `persist_event(...)` shortcut but it would be nice to use the real remote join # process in a `FederatingHomeserverTestCase`. flawed_join_tuple = self.get_success( create_event( self.hs, prev_event_ids=[invite_tuple[0].event_id], # This doesn't work correctly to create an `EventContext` that includes # both of these state events. I assume it's because we're working on our # local homeserver which has the remote state set as `outlier`. We have # to create our own EventContext below to get this right. auth_event_ids=[create_tuple[0].event_id, invite_tuple[0].event_id], type=EventTypes.Member, state_key=user1_id, content={"membership": Membership.JOIN}, sender=user1_id, **shared_kwargs, ) ) # We have to create our own context to get the state set correctly. If we use # the `EventContext` from the `flawed_join_tuple`, the `current_state_events` # table will only have the join event in it which should never happen in our # real server. join_event = flawed_join_tuple[0] join_context = self.get_success( self.state_handler.compute_event_context( join_event, state_ids_before_event={ (e.type, e.state_key): e.event_id for e in [create_tuple[0], invite_tuple[0]] }, partial_state=False, ) ) self.get_success(self.persistence.persist_event(join_event, join_context)) # Doing an SS request should return a positive `bump_stamp`, even though # the only event that matches the bump types has as negative stream # ordering. sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 5, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) self.assertGreater(response_body["rooms"][room_id]["bump_stamp"], 0) def test_rooms_bump_stamp_no_change_incremental(self) -> None: """Test that the bump stamp is omitted if there has been no change""" user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") room_id1 = self.helper.create_room_as( user1_id, tok=user1_tok, ) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 100, } } } response_body, from_token = self.do_sync(sync_body, tok=user1_tok) # Initial sync so we expect to see a bump stamp self.assertIn("bump_stamp", response_body["rooms"][room_id1]) # Send an event that is not in the bump events list self.helper.send_event( room_id1, type="org.matrix.test", content={}, tok=user1_tok ) response_body, from_token = self.do_sync( sync_body, since=from_token, tok=user1_tok ) # There hasn't been a change to the bump stamps, so we ignore it self.assertNotIn("bump_stamp", response_body["rooms"][room_id1]) def test_rooms_bump_stamp_change_incremental(self) -> None: """Test that the bump stamp is included if there has been a change, even if its not in the timeline""" user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") room_id1 = self.helper.create_room_as( user1_id, tok=user1_tok, ) # Make the Sliding Sync request sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 2, } } } response_body, from_token = self.do_sync(sync_body, tok=user1_tok) # Initial sync so we expect to see a bump stamp self.assertIn("bump_stamp", response_body["rooms"][room_id1]) first_bump_stamp = response_body["rooms"][room_id1]["bump_stamp"] # Send a bump event at the start. self.helper.send(room_id1, "test", tok=user1_tok) # Send events that are not in the bump events list to fill the timeline for _ in range(5): self.helper.send_event( room_id1, type="org.matrix.test", content={}, tok=user1_tok ) response_body, from_token = self.do_sync( sync_body, since=from_token, tok=user1_tok ) # There was a bump event in the timeline gap, so we should see the bump # stamp be updated. self.assertIn("bump_stamp", response_body["rooms"][room_id1]) second_bump_stamp = response_body["rooms"][room_id1]["bump_stamp"] self.assertGreater(second_bump_stamp, first_bump_stamp) def test_rooms_bump_stamp_invites(self) -> None: """ Test that `bump_stamp` is present and points to the membership event, and not later events, for non-joined rooms """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") room_id = self.helper.create_room_as( user2_id, tok=user2_tok, ) # Invite user1 to the room invite_response = self.helper.invite(room_id, user2_id, user1_id, tok=user2_tok) # More messages happen after the invite self.helper.send(room_id, "message in room1", tok=user2_tok) # We expect the bump_stamp to match the invite. invite_pos = self.get_success( self.store.get_position_for_event(invite_response["event_id"]) ) # Doing an SS request should return a `bump_stamp` of the invite event, # rather than the message that was sent after. sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 5, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) self.assertEqual( response_body["rooms"][room_id]["bump_stamp"], invite_pos.stream ) def test_rooms_meta_is_dm(self) -> None: """ Test `rooms` `is_dm` is correctly set for DM rooms. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") user2_id = self.register_user("user2", "pass") user2_tok = self.login(user2_id, "pass") # Create a DM room joined_dm_room_id = self._create_dm_room( inviter_user_id=user1_id, inviter_tok=user1_tok, invitee_user_id=user2_id, invitee_tok=user2_tok, should_join_room=True, ) invited_dm_room_id = self._create_dm_room( inviter_user_id=user1_id, inviter_tok=user1_tok, invitee_user_id=user2_id, invitee_tok=user2_tok, should_join_room=False, ) # Create a normal room room_id = self.helper.create_room_as(user2_id, tok=user2_tok) self.helper.join(room_id, user1_id, tok=user1_tok) # Create a room that user1 is invited to invite_room_id = self.helper.create_room_as(user2_id, tok=user2_tok) self.helper.invite(invite_room_id, src=user2_id, targ=user1_id, tok=user2_tok) sync_body = { "lists": { "foo-list": { "ranges": [[0, 99]], "required_state": [], "timeline_limit": 0, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok) # Ensure DM's are correctly marked self.assertDictEqual( { room_id: room.get("is_dm") for room_id, room in response_body["rooms"].items() }, { invite_room_id: None, room_id: None, invited_dm_room_id: True, joined_dm_room_id: True, }, ) def test_old_room_with_unknown_room_version(self) -> None: """Test that an old room with unknown room version does not break sync.""" user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") # We first create a standard room, then we'll change the room version in # the DB. room_id = self.helper.create_room_as( user1_id, tok=user1_tok, ) # Poke the database and update the room version to an unknown one. self.get_success( self.hs.get_datastores().main.db_pool.simple_update( "rooms", keyvalues={"room_id": room_id}, updatevalues={"room_version": "unknown-room-version"}, desc="updated-room-version", ) ) # Invalidate method so that it returns the currently updated version # instead of the cached version. self.hs.get_datastores().main.get_room_version_id.invalidate((room_id,)) # For old unknown room versions we won't have an entry in this table # (due to us skipping unknown room versions in the background update). self.get_success( self.store.db_pool.simple_delete( table="sliding_sync_joined_rooms", keyvalues={"room_id": room_id}, desc="delete_sliding_room", ) ) # Also invalidate some caches to ensure we pull things from the DB. self.store._events_stream_cache._entity_to_key.pop(room_id) self.store._get_max_event_pos.invalidate((room_id,)) sync_body = { "lists": { "foo-list": { "ranges": [[0, 1]], "required_state": [], "timeline_limit": 5, } } } response_body, _ = self.do_sync(sync_body, tok=user1_tok)