From 2f8683f42f2355a32ab97a1978c2e6862b69d344 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 22 Jul 2026 12:15:58 +0300 Subject: [PATCH] Handle room join/leave for profiles extension --- synapse/handlers/sliding_sync/extensions.py | 102 ++++++++++-------- .../sliding_sync/test_extension_profiles.py | 96 +++++++++++++++++ 2 files changed, 155 insertions(+), 43 deletions(-) diff --git a/synapse/handlers/sliding_sync/extensions.py b/synapse/handlers/sliding_sync/extensions.py index bfed95099f..0e787f36c5 100644 --- a/synapse/handlers/sliding_sync/extensions.py +++ b/synapse/handlers/sliding_sync/extensions.py @@ -1179,6 +1179,19 @@ class SlidingSyncExtensionHandler: field_names_empty_means_all_fields=False if fields else True, ) profile_user_ids = set() + left_room_user_ids = { + update.user_id + for update in updates + if update.action == ProfileUpdateAction.LEFT_ROOM.value + } + joined_room_user_ids = { + update.user_id + for update in updates + if update.action == ProfileUpdateAction.JOINED_ROOM.value + } + # Add any newly joined users + profile_user_ids.update(joined_room_user_ids) + updated_users = { update.user_id for update in updates @@ -1201,54 +1214,57 @@ class SlidingSyncExtensionHandler: continue updated_user_fields.setdefault(update.user_id, set()).add(update.field_name) - profile_data_by_user = await self.store.get_profile_data_for_users( - profile_user_ids - ) + profile_data_by_user = await self.store.get_profile_data_for_users( + profile_user_ids + ) - # TODO lazy loading - is_lazy = False + # TODO lazy loading + is_lazy = False - # Serialise the profile updates into the sync response format. - for profile_user_id in profile_user_ids: - profile_data = profile_data_by_user.get(profile_user_id) - if profile_data is None: - # No profile data for this user, just return a blank dictionary - # in incremental sync, telling the clients to remove all profile - # information for this user. - response[profile_user_id] = None - continue + # Serialise the profile updates into the sync response format. + for profile_user_id in profile_user_ids: + profile_data = profile_data_by_user.get(profile_user_id) + if profile_data is None: + # No profile data for this user, just return a blank dictionary + # in incremental sync, telling the clients to remove all profile + # information for this user. + response[profile_user_id] = None + continue - per_user_updates: dict[str, JsonValue | dict[str, JsonValue]] = {} - if is_lazy: - # TODO lazy cache + per_user_updates: dict[str, JsonValue | dict[str, JsonValue]] = {} + # Include the fields the client asked for, or all, if not specified + if fields: + user_fields = set(profile_data.keys()).intersection(fields) + else: + user_fields = set(profile_data.keys()) + if is_lazy: + # TODO lazy cache + for field_name in user_fields: + per_user_updates[field_name] = profile_data.get(field_name) + else: + # Include only the diff, unless the user recently joined, + # then send all the fields the client asked for. + # We don't use a cache here as for non-lazy sync we always + # send changes and/or fields the client asked for, if relevant + # as above joined condition. + user_fields = ( + user_fields + if profile_user_id in joined_room_user_ids + else set(updated_user_fields.get(profile_user_id, [])) + ) + for field_name in user_fields: + per_user_updates[field_name] = profile_data[field_name] - # Include the fields the client asked for, or all, if not specified - if fields: - user_fields = set(profile_data.keys()).intersection(fields) - else: - user_fields = set(profile_data.keys()) - for field_name in user_fields: - per_user_updates[field_name] = profile_data.get(field_name) - else: - # Include only the diff, unless the user recently joined, - # then send all the fields the client asked for. - # We don't use a cache here as for non-lazy sync we always - # send changes and/or fields the client asked for, if relevant - # as above joined condition. - user_fields = ( - fields - # TODO joined_room_user_ids - if profile_user_id in [] - else set(updated_user_fields.get(profile_user_id, [])) - ) - user_fields = set(profile_data.keys()).intersection(user_fields) - for field_name in user_fields: - per_user_updates[field_name] = profile_data[field_name] + if per_user_updates: + response[profile_user_id] = { + "updated": per_user_updates, + } - if per_user_updates: - response[profile_user_id] = { - "updated": per_user_updates, - } + # Process left rooms + if left_room_user_ids: + for other_user_id in left_room_user_ids: + # Return a null response to the client + response[other_user_id] = None return SlidingSyncResult.Extensions.ProfilesExtension( users=response, diff --git a/tests/rest/client/sliding_sync/test_extension_profiles.py b/tests/rest/client/sliding_sync/test_extension_profiles.py index e7405e3c33..056e9e3951 100644 --- a/tests/rest/client/sliding_sync/test_extension_profiles.py +++ b/tests/rest/client/sliding_sync/test_extension_profiles.py @@ -434,3 +434,99 @@ class SlidingSyncProfilesTestCase(SlidingSyncBase): } }, ) + + @parameterized.expand( + [ + [True, False], + [True, True], + [False, False], + [False, True], + ] + ) + @override_config({"include_profile_updates_in_sync": True}) + def test_null_profile_returned_if_user_left_all_rooms( + self, request_fields: bool, is_lazy: bool + ) -> None: + """ + Test that profile extension response returns a null for the user in + incremental sync. + """ + # TODO handle is_lazy + # Make an initial Sliding Sync request with the profiles extension enabled + profiles_config: dict = { + "enabled": True, + } + if request_fields: + profiles_config["fields"] = ["field"] + sync_body = { + "lists": {}, + "extensions": { + "org.matrix.msc4262.profiles": profiles_config, + }, + } + response_body, from_token = self.do_sync(sync_body, tok=self.tok) + + self.helper.leave(self.joined_room, self.other_user, tok=self.other_tok) + + # Make an incremental Sliding Sync request + response_body, _ = self.do_sync(sync_body, since=from_token, tok=self.tok) + # We should see a null profile + self.assertIsNone( + response_body["extensions"]["org.matrix.msc4262.profiles"]["users"][ + "@other_user:test" + ], + ) + + @parameterized.expand( + [ + True, + False, + ] + ) + @override_config({"include_profile_updates_in_sync": True}) + def test_all_fields_returned_in_incremental_non_lazy_sync_if_someone_joined( + self, request_fields: bool + ) -> None: + """ + Test that profile extension response returns all profile fields in + incremental non-lazy sync, if someone joined the room.. + """ + # Make an initial Sliding Sync request with the profiles extension enabled + profiles_config: dict = { + "enabled": True, + } + if request_fields: + profiles_config["fields"] = ["displayname"] + sync_body = { + "lists": {}, + "extensions": { + "org.matrix.msc4262.profiles": profiles_config, + }, + } + response_body, from_token = self.do_sync(sync_body, tok=self.tok) + + third_user = self.register_user("third_user", "third_user") + third_tok = self.login(third_user, "third_user") + self.helper.join(self.joined_room, third_user, tok=third_tok) + + # Make an incremental Sliding Sync request + response_body, _ = self.do_sync(sync_body, since=from_token, tok=self.tok) + + expectation = { + "updated": { + "avatar_url": None, + "displayname": "third_user", + } + } + if request_fields: + expectation = { + "updated": { + "displayname": "third_user", + }, + } + self.assertEqual( + response_body["extensions"]["org.matrix.msc4262.profiles"]["users"][ + "@third_user:test" + ], + expectation, + )