From 2d9f9430341ad3abf753dc0d83f3a94bbb13210a Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Thu, 4 Jun 2026 22:04:41 +0300 Subject: [PATCH] Collect lazy loaded members for profile updates in sync response from events --- synapse/handlers/sync.py | 45 ++++++++++++++++++----- tests/handlers/test_sync.py | 71 +++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 937f382370..36319e4e61 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -2139,9 +2139,11 @@ class SyncHandler: async def _generate_initial_sync_entry_for_profile_updates( self, + *, user_id: str, sync_result_builder: "SyncResultBuilder", profile_fields: list[str], + include_users: set[str] | None, ) -> None: """ Build an initial sync entry for profile updates and attach it to the @@ -2153,17 +2155,16 @@ class SyncHandler: user_id: The Matrix ID of the user to generate the sync entry for. sync_result_builder: profile_fields: The list of field IDs to filter for. + include_users: List of users profiles to include in the sync response, + for when we have calculated a list of users in our lazy loading + sync and want to only return those. """ # Currently, limited to only local profiles, so filter remote servers out user_ids = await self.store.get_local_users_who_share_room_with_user(user_id) - sync_config = sync_result_builder.sync_config - lazy_load_members = sync_config.filter_collection.lazy_load_members() - if lazy_load_members: - # Only include members we've collected for lazy loading - cache_key = (sync_config.user.to_string(), sync_config.device_id) - cache = self.get_lazy_loaded_members_cache(cache_key) - user_ids = {user_id for user_id in user_ids if cache.get(user_id)} + if include_users: + # Filter down to selected included users + user_ids = {user_id for user_id in user_ids if user_id in include_users} if not user_ids: return @@ -2213,9 +2214,33 @@ class SyncHandler: since_token = sync_result_builder.since_token now_token = sync_result_builder.now_token + sync_config = sync_result_builder.sync_config + lazy_load_members = sync_config.filter_collection.lazy_load_members() + include_users = None + if lazy_load_members: + # Collect members from the existing `sync_result_builder` data + include_users = set() + # invited + for invited in sync_result_builder.invited: + include_users.add(invited.invite.sender) + # joined + for joined in sync_result_builder.joined: + for timeline_event in joined.timeline.events: + include_users.add(timeline_event.event.sender) + # knocked + for knocked in sync_result_builder.knocked: + include_users.add(knocked.knock.sender) + # archived + for archived in sync_result_builder.archived: + for timeline_event in archived.timeline.events: + include_users.add(timeline_event.event.sender) + if since_token is None: await self._generate_initial_sync_entry_for_profile_updates( - user_id, sync_result_builder, profile_fields + user_id=user_id, + sync_result_builder=sync_result_builder, + profile_fields=profile_fields, + include_users=include_users, ) return @@ -2227,6 +2252,10 @@ class SyncHandler: to_id=now_token.profile_updates_key, field_names=profile_fields, ) + if include_users: + # Filter down to selected included users + updates = [update for update in updates if update.user_id in include_users] + if not updates: return diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index ae5b87a4ad..88ed55f873 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -1372,6 +1372,77 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): '{"text": "On holiday", "emoji": "\\ud83c\\udfd6"}', ) + @override_config({"experimental_features": {"msc4429_enabled": True}}) + def test_incremental_sync_sends_down_only_interesting_profile_updates_when_lazy_loading( + self, + ) -> None: + third_user = self.register_user("third_user", "password") + third_tok = self.login("third_user", "password") + second_room = self.helper.create_room_as(self.user, tok=self.tok) + self.helper.join( + room=second_room, + user=third_user, + tok=third_tok, + ) + + requester = create_requester(self.user) + 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={ + "org.matrix.msc4429.profile_fields": { + "ids": ["m.status", "displayname", "avatar_url"] + } + }, + ), + ), + request_key=generate_request_key(), + ) + ) + self.get_success( + self.profile_handler.set_field( + target_user=UserID.from_string(self.other_user), + requester=create_requester(self.other_user), + field_name="m.status", + new_value=json.dumps({"text": "On holiday", "emoji": "🏖"}), + ) + ) + self.helper.send_messages(room_id=second_room, num_events=10, tok=third_tok) + 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={ + "org.matrix.msc4429.profile_fields": { + "ids": ["m.status", "displayname", "avatar_url"] + }, + "room": { + "state": { + "lazy_load_members": True, + }, + }, + }, + ), + ), + request_key=generate_request_key(), + ) + ) + self.assertCountEqual( + incremental_result.profile_updates.keys(), + [ + "@third_user:test", + "@user:test", + ], + ) + @override_config({"experimental_features": {"msc4429_enabled": True}}) def test_incremental_sync_sends_down_null_profile_if_user_no_longer_sharing_rooms( self,