From eb5aa2129a8ec67476e74fc0e28b5f19dc2476c4 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 1 Jul 2026 11:22:57 +0300 Subject: [PATCH] Ensure both initial and incremental syncs include our own profile and updates Otherwise if the user has multiple clients doing incremental sync, they wont get their own profile updates to their other clients. For initial because it should probably match the incremental behaviour + it avoids clients needing to look up the profile separately potentially. --- synapse/handlers/profile.py | 11 ++-- synapse/handlers/sync.py | 5 -- synapse/storage/databases/main/profile.py | 2 +- synapse/storage/databases/main/roommember.py | 5 +- tests/handlers/test_profile.py | 6 ++ tests/handlers/test_sync.py | 68 +++++++++++++++++++- 6 files changed, 82 insertions(+), 15 deletions(-) diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index f27c0a475d..05e59c1818 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -142,13 +142,10 @@ class ProfileHandler: user_id.to_string() ) ) - # Remove ourselves from the user ID list - users_who_share_rooms.remove(user_id.to_string()) - if users_who_share_rooms: - await self.store.track_profile_updates_per_user( - stream_id=stream_id, - user_ids=users_who_share_rooms, - ) + await self.store.track_profile_updates_per_user( + stream_id=stream_id, + user_ids=users_who_share_rooms, + ) self._notifier.on_new_event( StreamKeyType.PROFILE_UPDATES, stream_id, rooms=room_ids diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index b30fc2adb7..17f0467deb 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -2203,11 +2203,6 @@ class SyncHandler: # Filter down to selected included users user_ids = {user_id for user_id in user_ids if user_id in include_users} - # Remove ourselves - # FIXME?: Should `get_local_users_who_share_room_with_user` even return - # ourselves? - user_ids.discard(user_id) - if not user_ids: return diff --git a/synapse/storage/databases/main/profile.py b/synapse/storage/databases/main/profile.py index 1475ee994c..b8a35314fd 100644 --- a/synapse/storage/databases/main/profile.py +++ b/synapse/storage/databases/main/profile.py @@ -670,7 +670,7 @@ class ProfileWorkerStore(SQLBaseStore): ) -> None: """ Create tracking rows for profile updater per target user interested in profile - updates for the user triggering one. + updates for the user triggering one, including themselves. Args: stream_id: Stream ID referencing a `profile_updates` stream ID. diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index a645e4f3a5..8c69266d51 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -983,7 +983,10 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore): return user_who_share_room async def get_local_users_who_share_room_with_user(self, user_id: str) -> set[str]: - """Returns the set of local users who share a room with `user_id`""" + """Returns the set of local users who share a room with `user_id`. + + This also includes the `user_id` themselves. + """ room_ids = await self.get_rooms_for_user(user_id) user_who_share_room: set[str] = set() diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index 82c1e7c435..0bf940d24b 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -415,6 +415,12 @@ class ProfileTestCase(unittest.HomeserverTestCase): action="joined_room", field_name=None, ), + ProfileUpdate( + stream_id=4, + user_id=self.frank.to_string(), + action="update", + field_name="m.status", + ), ], ) diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index f72cf54956..61cf0b0237 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -1241,7 +1241,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): @override_config({"include_profile_updates_in_sync": True}) def test_initial_sync_responds_with_tracked_profile_updates(self) -> None: """Test that with MSC4429 enabled the initial sync response does - contain profile updates for users who share rooms.""" + contain profile updates for users who share rooms, including our + syncing user.""" self.get_success( self.profile_handler.set_field( target_user=UserID.from_string(self.other_user), @@ -1269,6 +1270,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + assert initial_result.profile_updates[self.user] is not None assert initial_result.profile_updates["@other_user:test"] is not None self.assertEqual( initial_result.profile_updates["@other_user:test"]["m.status"], @@ -1277,6 +1279,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): self.assertCountEqual( initial_result.profile_updates.keys(), [ + self.user, "@other_user:test", ], ) @@ -1804,6 +1807,69 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): incremental_result.profile_updates["@third_user:test"]["avatar_url"], ) + @parameterized.expand( + [ + True, + False, + ] + ) + @override_config({"include_profile_updates_in_sync": True}) + def test_incremental_sync_includes_own_profile_updates(self, is_lazy: bool) -> None: + """Test that with MSC4429 enabled the incremental sync response includes + ones own profile updates.""" + requester = create_requester(self.user) + filter_json: dict[str, dict] = { + "org.matrix.msc4429.profile_fields": { + "ids": ["m.status", "displayname", "avatar_url"] + } + } + if is_lazy: + filter_json["room"] = { + "state": { + "lazy_load_members": True, + }, + } + initial_result = self.get_success( + self.sync_handler.wait_for_sync_for_user( + requester, + sync_config=generate_sync_config( + user_id=self.user, + filter_collection=FilterCollection( + hs=self.hs, + filter_json=filter_json, + ), + ), + request_key=generate_request_key(), + ) + ) + self.get_success( + self.profile_handler.set_field( + target_user=UserID.from_string(self.user), + requester=requester, + field_name="m.status", + new_value=json.dumps({"text": "On holiday", "emoji": "🏖"}), + ) + ) + incremental_result = self.get_success( + self.sync_handler.wait_for_sync_for_user( + requester, + since_token=initial_result.next_batch, + sync_config=generate_sync_config( + user_id=self.user, + filter_collection=FilterCollection( + hs=self.hs, + filter_json=filter_json, + ), + ), + request_key=generate_request_key(), + ) + ) + assert incremental_result.profile_updates["@user:test"] is not None + self.assertEqual( + incremental_result.profile_updates["@user:test"]["m.status"], + '{"text": "On holiday", "emoji": "\\ud83c\\udfd6"}', + ) + class SyncStateAfterTestCase(tests.unittest.HomeserverTestCase): """Tests Sync Handler state behavior when using `use_state_after."""