diff --git a/changelog.d/20003.feature b/changelog.d/20003.feature index add6b613c8..6a7216fd58 100644 --- a/changelog.d/20003.feature +++ b/changelog.d/20003.feature @@ -1,2 +1,6 @@ Add optional support for [MSC4262: Profile Updates for Sliding Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4262). -Currently defaults to not enabled, and is limited to local users only for the sync results. \ No newline at end of file +Currently defaults to not enabled, and is limited to local users only for the sync results. + +Additionally, optional support for [MSC4429: Profile Updates for Legacy Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4262) +now includes removed fields in the `removed_profile_fields` response key, in addition to setting the field to a `null` +value. The latter behaviour will be removed in a future Synapse release. \ No newline at end of file diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 8723056ae0..56b88da2c6 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -29,6 +29,7 @@ from typing import ( Any, Mapping, Sequence, + cast, ) import attr @@ -40,6 +41,7 @@ from synapse.api.constants import ( EventContentFields, EventTypes, Membership, + ProfileFields, ProfileUpdateAction, StickyEvent, ) @@ -237,6 +239,37 @@ class _RoomChanges: newly_left_rooms: list[str] +@attr.s(slots=True, auto_attribs=True) +class ProfilesResult: + """ + Updates to profiles to add to the sync response. Includes per user updated or + existing fields in `profile_updates`, and field removals in + `removed_profile_fields`. + """ + + # user ID -> {profile field -> value | null if unset } + profile_updates: dict[str, dict[str, JsonValue | dict[str, JsonValue]] | None] = ( + attr.ib(factory=dict) + ) + # user ID -> [field name] + removed_profile_fields: dict[str, list[str]] = attr.ib(factory=dict) + + def __bool__(self) -> bool: + return bool(self.profile_updates.keys()) or bool( + self.removed_profile_fields.keys() + ) + + def to_response(self) -> dict[str, dict[str, JsonValue | dict[str, JsonValue]]]: + response: dict[str, JsonDict] = {} + for user_id, updates in self.profile_updates.items(): + response[user_id] = {"profile_updates": updates} + for user_id, removals in self.removed_profile_fields.items(): + if response.get(user_id) is None: + response[user_id] = {} + response[user_id]["removed_profile_fields"] = removals + return response + + @attr.s(slots=True, frozen=True, auto_attribs=True) class SyncResult: """ @@ -244,7 +277,7 @@ class SyncResult: next_batch: Token for the next sync presence: List of presence events for the user. account_data: List of account_data events for the user. - profile_updates: Map of user_id to profile field updates for that user. + profiles: The ProfilesResult object containing updates and removals. joined: JoinedSyncResult for each joined room. invited: InvitedSyncResult for each invited room. knocked: KnockedSyncResult for each knocked on room. @@ -260,8 +293,7 @@ class SyncResult: next_batch: StreamToken presence: list[UserPresenceState] account_data: list[JsonDict] - # user ID -> {profile field -> value | null if unset } - profile_updates: dict[str, dict[str, JsonValue | dict[str, JsonValue]] | None] + profiles: ProfilesResult joined: list[JoinedSyncResult] invited: list[InvitedSyncResult] knocked: list[KnockedSyncResult] @@ -283,7 +315,7 @@ class SyncResult: or self.knocked or self.archived or self.account_data - or self.profile_updates + or self.profiles or self.to_device or self.device_lists ) @@ -299,7 +331,7 @@ class SyncResult: next_batch=next_batch, presence=[], account_data=[], - profile_updates={}, + profiles=ProfilesResult(), joined=[], invited=[], knocked=[], @@ -1954,7 +1986,7 @@ class SyncHandler: return SyncResult( presence=sync_result_builder.presence, account_data=sync_result_builder.account_data, - profile_updates=sync_result_builder.profile_updates, + profiles=sync_result_builder.profiles, joined=sync_result_builder.joined, invited=sync_result_builder.invited, knocked=sync_result_builder.knocked, @@ -2274,7 +2306,9 @@ class SyncHandler: profile_updates[other_user_id] = per_user_updates if profile_updates: - sync_result_builder.profile_updates = profile_updates + sync_result_builder.profiles = ProfilesResult( + profile_updates=profile_updates, + ) async def _generate_sync_entry_for_profile_updates( self, sync_result_builder: "SyncResultBuilder" @@ -2350,6 +2384,11 @@ class SyncHandler: for update in updates if update.action == ProfileUpdateAction.JOINED_ROOM.value } + delete_field_user_ids = { + update.user_id + for update in updates + if update.action == ProfileUpdateAction.DELETE.value + } users = set() updated_users = { update.user_id @@ -2364,7 +2403,7 @@ class SyncHandler: # Add any newly joined users users.update(joined_room_user_ids) - if not users and not left_room_user_ids: + if not users and not left_room_user_ids and not delete_field_user_ids: return # Serialise the profile updates into the sync response format. @@ -2373,6 +2412,19 @@ class SyncHandler: str, dict[str, JsonValue | dict[str, JsonValue]] | None ] = {} + # Note: there's a small race condition here where a profile update may + # occur between fetching `now_token` above and reaching this step. In + # that case, the profile information will be newer than `now_token`. + # This is fine, as users will generally always want the latest profile + # information. However, it does mean that on the next sync, the same + # profile update will come down a second time. + # + # Hopefully clients can just filter these out. + profile_data_by_user = await self.store.get_profile_data_for_users( + # Get profiles for both updates and deletes in one go + users.union(delete_field_user_ids) + ) + # Process field updates and users who have events in the sync response if users: updated_user_fields: dict[str, set[str]] = {} @@ -2393,16 +2445,6 @@ class SyncHandler: update.affected_fields & profile_fields ) - # Note: there's a small race condition here where a profile update may - # occur between fetching `now_token` above and reaching this step. In - # that case, the profile information will be newer than `now_token`. - # This is fine, as users will generally always want the latest profile - # information. However, it does mean that on the next sync, the same - # profile update will come down a second time. - # - # Hopefully clients can just filter these out. - profile_data_by_user = await self.store.get_profile_data_for_users(users) - # Note, we've already collected field updates above via `updates`, # outside of events in the timeline when lazy loading. When lazy loading, # we're already always sending the fields that have changed, regardless @@ -2476,16 +2518,61 @@ class SyncHandler: if per_user_updates: profile_updates[other_user_id] = per_user_updates + # Process deleted fields + removed_profile_fields: dict[str, list[str]] = {} + for update in updates: + if ( + update.action != ProfileUpdateAction.DELETE.value + or not update.affected_fields + or update.user_id in left_room_user_ids + ): + continue + profile_data = profile_data_by_user.get(update.user_id) + for field_name in update.affected_fields: + if field_name not in profile_fields: + # Only include fields the client is interested in + continue + if profile_data is not None: + if field_name in ( + ProfileFields.AVATAR_URL, + ProfileFields.DISPLAYNAME, + ): + if profile_data.get(field_name) is not None: + # Displayname/avatar_url are not unset anymore + continue + elif field_name in profile_data.keys(): + # This field has re-appeared to the profile, skip + continue + if removed_profile_fields.get(update.user_id) is None: + removed_profile_fields[update.user_id] = [] + + # Ensure we only add the field once + if field_name in removed_profile_fields[update.user_id]: + continue + removed_profile_fields[update.user_id].append(field_name) + + # FIXME: Initial MSC4429 behaviour of including removed profile fields + # in the main `profile_updates` key. We need to keep this around for + # a few releases so that clients can adapt. + # See https://github.com/matrix-org/matrix-spec-proposals/pull/4429#discussion_r3512513534 + # Removing will be tracked via https://github.com/element-hq/synapse/issues/19981 + if profile_updates.get(update.user_id) is None: + profile_updates[update.user_id] = {} + # Typing fix as mypy thinks this may be None + entry = cast(JsonDict, profile_updates[update.user_id]) + entry[field_name] = None + # Process left rooms if left_room_user_ids: for other_user_id in left_room_user_ids: # Return an empty dictionary to the client profile_updates[other_user_id] = None - # FIXME: handle profile field deletions like we do for sliding sync - if profile_updates: - sync_result_builder.profile_updates = profile_updates + sync_result_builder.profiles = ProfilesResult( + profile_updates=profile_updates, + removed_profile_fields=removed_profile_fields, + ) async def _generate_sync_entry_for_presence( self, @@ -3503,7 +3590,7 @@ class SyncResultBuilder: # The following mirror the fields in a sync response presence account_data - profile_updates + profiles joined invited knocked @@ -3522,9 +3609,7 @@ class SyncResultBuilder: presence: list[UserPresenceState] = attr.Factory(list) account_data: list[JsonDict] = attr.Factory(list) - profile_updates: dict[str, dict[str, JsonValue | dict[str, JsonValue]] | None] = ( - attr.Factory(dict) - ) + profiles: ProfilesResult = attr.Factory(ProfilesResult) joined: list[JoinedSyncResult] = attr.Factory(list) invited: list[InvitedSyncResult] = attr.Factory(list) knocked: list[KnockedSyncResult] = attr.Factory(list) diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py index 1514af8b79..25cc89f020 100644 --- a/synapse/rest/client/sync.py +++ b/synapse/rest/client/sync.py @@ -358,14 +358,11 @@ class SyncRestServlet(RestServlet): if sync_result.to_device: response["to_device"] = {"events": sync_result.to_device} - if self._msc4429_enabled and sync_result.profile_updates: + if self._msc4429_enabled and sync_result.profiles: # FIXME: See issue https://github.com/element-hq/synapse/issues/19981 # for concerns around the current implementation of the profile # updates stream. - response["org.matrix.msc4429.users"] = { - user_id: {"profile_updates": updates} - for user_id, updates in sync_result.profile_updates.items() - } + response["org.matrix.msc4429.users"] = sync_result.profiles.to_response() if sync_result.device_lists.changed: response["device_lists"]["changed"] = list(sync_result.device_lists.changed) diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index bfb687a6c7..71fecd7197 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -1208,7 +1208,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) - self.assertEqual(initial_result.profile_updates, {}) + self.assertEqual(initial_result.profiles.profile_updates, {}) + self.assertEqual(initial_result.profiles.removed_profile_fields, {}) @override_config({"include_profile_updates_in_sync": True}) def test_initial_sync_no_profile_updates_if_not_filtered_for(self) -> None: @@ -1234,7 +1235,11 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) ) self.assertEqual( - initial_result.profile_updates, + initial_result.profiles.profile_updates, + {}, + ) + self.assertEqual( + initial_result.profiles.removed_profile_fields, {}, ) @@ -1277,17 +1282,18 @@ 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 + profile_updates = initial_result.profiles.profile_updates + assert profile_updates[self.user] is not None + assert profile_updates["@other_user:test"] is not None self.assertEqual( - initial_result.profile_updates["@other_user:test"]["m.status"], + profile_updates["@other_user:test"]["m.status"], {"text": "On holiday", "emoji": "🏖"}, ) self.assertFalse( - "displayname" in initial_result.profile_updates["@other_user:test"].keys(), + "displayname" in profile_updates["@other_user:test"].keys(), ) self.assertCountEqual( - initial_result.profile_updates.keys(), + profile_updates.keys(), [ self.user, "@other_user:test", @@ -1341,7 +1347,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) - self.assertIsNone(initial_result.profile_updates.get(third_user)) + self.assertIsNone(initial_result.profiles.profile_updates.get(third_user)) @override_config({"include_profile_updates_in_sync": True}) def test_initial_sync_lazy_loading_responds_with_only_profiles_with_events( @@ -1414,7 +1420,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): # Only third_user is returned, as lazy loading filters out the events from # the other users self.assertCountEqual( - initial_result.profile_updates.keys(), + initial_result.profiles.profile_updates.keys(), [ "@third_user:test", ], @@ -1479,22 +1485,106 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) - assert incremental_result.profile_updates["@other_user:test"] is not None + profile_updates = incremental_result.profiles.profile_updates + assert profile_updates["@other_user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@other_user:test"]["m.status"], + profile_updates["@other_user:test"]["m.status"], {"text": "On holiday", "emoji": "🏖"}, ) # We only send diffs in incremental sync for profile field updates self.assertFalse( - "displayname" - in incremental_result.profile_updates["@other_user:test"].keys(), + "displayname" in profile_updates["@other_user:test"].keys(), ) # The client didn't ask for this field self.assertFalse( - "uninterestingfield" - in incremental_result.profile_updates["@other_user:test"].keys(), + "uninterestingfield" in profile_updates["@other_user:test"].keys(), ) + @override_config({"include_profile_updates_in_sync": True}) + def test_incremental_sync_sends_down_profile_field_removals( + self, + ) -> None: + """Test that with MSC4429 enabled the incremental sync response correctly + responds with field removals. + + Note that initially MSC4429 defined field removals being in the + `profile_updates` key, with the field having a null value if it was removed. + This got implemented and released in Element Web, thus we kept the behaviour + in the pr adding profile updates over legacy sync + (https://github.com/element-hq/synapse/pull/19556). + + The latest suggestion in MSC4429 is to do something similar that sliding sync + MSC4262 has, ie have a dedicated key for field removals, see: + https://github.com/matrix-org/matrix-spec-proposals/pull/4429#pullrequestreview-4616853235 + + To ensure clients have time to adapt, we should include field removals in both + keys for a few Synapse releases. + """ + 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="otherfield", + new_value="value", + ) + ) + self.get_success( + self.profile_handler.delete_profile_field( + target_user=UserID.from_string(self.other_user), + requester=create_requester(self.other_user), + field_name="m.status", + ) + ) + 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"] + } + }, + ), + ), + request_key=generate_request_key(), + ) + ) + profile_updates = incremental_result.profiles.profile_updates + assert profile_updates["@other_user:test"] is not None + # Legacy behaviour - field is null + self.assertIsNone(profile_updates["@other_user:test"]["m.status"]) + # Latest behaviour, in `removed_fields` + field_removals = incremental_result.profiles.removed_profile_fields + self.assertEqual( + field_removals["@other_user:test"], + ["m.status"], + ) + # Client didn't ask for this field + self.assertFalse("otherfield" in field_removals["@other_user:test"]) + self.assertFalse("otherfield" in profile_updates["@other_user:test"].keys()) + @override_config({"include_profile_updates_in_sync": True}) def test_incremental_sync_does_not_filter_profile_updates_when_lazy_loading( self, @@ -1592,49 +1682,46 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) ) + profile_updates = incremental_result.profiles.profile_updates # Ensure our federated user is filtered out, even though they have an # event in the joined room timeline - self.assertFalse( - "@federateduser:federatedhs" in incremental_result.profile_updates.keys() - ) + self.assertFalse("@federateduser:federatedhs" in profile_updates.keys()) # Lazy loading only filters initial sync profile updates. Incremental syncs # should include all tracked profile updates for the syncing user. self.assertCountEqual( - incremental_result.profile_updates.keys(), + profile_updates.keys(), [ "@other_user:test", "@third_user:test", ], ) - assert incremental_result.profile_updates["@other_user:test"] is not None + assert profile_updates["@other_user:test"] is not None # This is a field update, so should be here self.assertEqual( - incremental_result.profile_updates["@other_user:test"]["m.status"], + profile_updates["@other_user:test"]["m.status"], {"text": "On holiday", "emoji": "🏖"}, ) # We don't have events for this user in this response, so their full profile # is not included self.assertFalse( - "displayname" - in incremental_result.profile_updates["@other_user:test"].keys(), + "displayname" in profile_updates["@other_user:test"].keys(), ) - assert incremental_result.profile_updates["@third_user:test"] is not None + assert profile_updates["@third_user:test"] is not None # This user has events in the timeline, thus the fields the client asked for # are included self.assertEqual( - incremental_result.profile_updates["@third_user:test"]["m.status"], + profile_updates["@third_user:test"]["m.status"], {"text": "On fire", "emoji": "🔥"}, ) self.assertFalse( - "uninterestingfield" - in incremental_result.profile_updates["@third_user:test"].keys(), + "uninterestingfield" in profile_updates["@third_user:test"].keys(), ) self.assertEqual( - incremental_result.profile_updates["@third_user:test"]["displayname"], + profile_updates["@third_user:test"]["displayname"], "third_user", ) @@ -1693,7 +1780,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): # Ensure our federated user is filtered out, even though they have an # event in the joined room timeline self.assertFalse( - "@federateduser1:federatedhs" in initial_result.profile_updates.keys() + "@federateduser1:federatedhs" + in initial_result.profiles.profile_updates.keys() ) if not is_initial: # Join another federated user to the room, causing a membership event into @@ -1725,7 +1813,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): # event in the joined room timeline self.assertFalse( "@federateduser2:federatedhs" - in incremental_result.profile_updates.keys() + in incremental_result.profiles.profile_updates.keys() ) @parameterized.expand( @@ -1782,9 +1870,10 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) ) if is_initial: - assert initial_result.profile_updates["@third_user:test"] is not None + profile_updates = initial_result.profiles.profile_updates + assert profile_updates["@third_user:test"] is not None self.assertEqual( - initial_result.profile_updates["@third_user:test"]["field"], + profile_updates["@third_user:test"]["field"], "Content", ) else: @@ -1810,9 +1899,10 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) - assert incremental_result.profile_updates["@third_user:test"] is not None + profile_updates = incremental_result.profiles.profile_updates + assert profile_updates["@third_user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@third_user:test"]["field"], + profile_updates["@third_user:test"]["field"], "Content", ) @@ -1867,11 +1957,10 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) ) if is_initial: - assert initial_result.profile_updates["@other_user:test"] is not None + profile_updates = initial_result.profiles.profile_updates + assert profile_updates["@other_user:test"] is not None self.assertEqual( - initial_result.profile_updates["@other_user:test"][ - "falseyvaluefield" - ], + profile_updates["@other_user:test"]["falseyvaluefield"], value, ) else: @@ -1897,13 +1986,10 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) - assert ( - incremental_result.profile_updates["@other_user:test"] is not None - ) + profile_updates = incremental_result.profiles.profile_updates + assert profile_updates["@other_user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@other_user:test"][ - "falseyvaluefield" - ], + profile_updates["@other_user:test"]["falseyvaluefield"], value, ) @@ -1968,16 +2054,17 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + profile_updates = incremental_result.profiles.profile_updates # Lazy loading incremental sync should include profiles from events self.assertCountEqual( - incremental_result.profile_updates.keys(), + profile_updates.keys(), [ "@other_user:test", ], ) - assert incremental_result.profile_updates["@other_user:test"] is not None + assert profile_updates["@other_user:test"] is not None self.assertEqual( - set(incremental_result.profile_updates["@other_user:test"].keys()), + set(profile_updates["@other_user:test"].keys()), {"avatar_url", "displayname"}, ) @@ -2009,8 +2096,9 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + profile_updates = incremental_result.profiles.profile_updates self.assertCountEqual( - incremental_result.profile_updates.keys(), + profile_updates.keys(), [], ) # However, if we again add an event, we do expect any fields the client didn't @@ -2046,15 +2134,16 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + profile_updates = incremental_result.profiles.profile_updates self.assertCountEqual( - incremental_result.profile_updates.keys(), + profile_updates.keys(), [ "@other_user:test", ], ) - assert incremental_result.profile_updates["@other_user:test"] is not None + assert profile_updates["@other_user:test"] is not None self.assertEqual( - set(incremental_result.profile_updates["@other_user:test"].keys()), + set(profile_updates["@other_user:test"].keys()), {"sooninterestingfield"}, ) @@ -2105,7 +2194,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) ) self.assertIsNone( - incremental_result.profile_updates["@other_user:test"], + incremental_result.profiles.profile_updates["@other_user:test"], ) @override_config({"include_profile_updates_in_sync": True}) @@ -2187,20 +2276,21 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) - assert incremental_result.profile_updates["@third_user:test"] is not None + profile_updates = incremental_result.profiles.profile_updates + assert profile_updates["@third_user:test"] is not None self.assertCountEqual( - incremental_result.profile_updates.keys(), + profile_updates.keys(), [third_user], ) self.assertEqual( - incremental_result.profile_updates["@third_user:test"]["displayname"], + profile_updates["@third_user:test"]["displayname"], "third_user", ) self.assertIsNone( - incremental_result.profile_updates["@third_user:test"]["avatar_url"], + profile_updates["@third_user:test"]["avatar_url"], ) self.assertFalse( - "m.status" in incremental_result.profile_updates["@third_user:test"].keys(), + "m.status" in profile_updates["@third_user:test"].keys(), ) @parameterized.expand( @@ -2258,14 +2348,15 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) - assert incremental_result.profile_updates["@user:test"] is not None + profile_updates = incremental_result.profiles.profile_updates + assert profile_updates["@user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@user:test"]["m.status"], + profile_updates["@user:test"]["m.status"], {"text": "On holiday", "emoji": "🏖"}, ) # We didn't ask for displayname self.assertFalse( - "displayname" in incremental_result.profile_updates["@user:test"].keys(), + "displayname" in profile_updates["@user:test"].keys(), ) @parameterized.expand([[True, False], [True, True], [False, False], [False, True]]) @@ -2337,7 +2428,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) # We expect there to be the users profile self.assertIsNotNone( - incremental_result.profile_updates["@third_user:test"], + incremental_result.profiles.profile_updates["@third_user:test"], ) next_token = incremental_result.next_batch # Leave the room @@ -2365,7 +2456,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) # We expect there to be a null profile self.assertIsNone( - incremental_result.profile_updates["@third_user:test"], + incremental_result.profiles.profile_updates["@third_user:test"], ) next_token = incremental_result.next_batch # Join the room @@ -2393,7 +2484,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) # We expect there to be the users profile self.assertIsNotNone( - incremental_result.profile_updates["@third_user:test"], + incremental_result.profiles.profile_updates["@third_user:test"], ) next_token = incremental_result.next_batch # Leave the room @@ -2420,7 +2511,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) # The end result should be null profile self.assertIsNone( - incremental_result.profile_updates["@third_user:test"], + incremental_result.profiles.profile_updates["@third_user:test"], ) @parameterized.expand( @@ -2469,7 +2560,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) ) self.assertFalse( - "@other_user:test" in initial_result.profile_updates, + "@other_user:test" in initial_result.profiles.profile_updates, ) # Update the field @@ -2505,12 +2596,13 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + profile_updates = incremental_result.profiles.profile_updates # We should have the field change in our sync response. # It will also be added to the lazy loading cache, so the same field value # isn't sent again immediately. - assert incremental_result.profile_updates["@other_user:test"] is not None + assert profile_updates["@other_user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@other_user:test"]["field"], + profile_updates["@other_user:test"]["field"], value, ) @@ -2547,11 +2639,12 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + profile_updates = incremental_result.profiles.profile_updates # Even though the field was added to the lazy loading members cache, # it should come through as an update, as the field value changed. - assert incremental_result.profile_updates["@other_user:test"] is not None + assert profile_updates["@other_user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@other_user:test"]["field"], + profile_updates["@other_user:test"]["field"], new_value, ) @@ -2586,7 +2679,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): ) ) self.assertFalse( - "@other_user:test" in initial_result.profile_updates, + "@other_user:test" in initial_result.profiles.profile_updates, ) # Update the field @@ -2615,12 +2708,13 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + profile_updates = incremental_result.profiles.profile_updates # We should have the field change in our sync response. # It will also be added to the lazy loading cache, so the same field value # isn't sent again immediately. - assert incremental_result.profile_updates["@other_user:test"] is not None + assert profile_updates["@other_user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@other_user:test"]["field"], + profile_updates["@other_user:test"]["field"], "value", ) @@ -2650,11 +2744,12 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + profile_updates = incremental_result.profiles.profile_updates # Even though the field was added to the lazy loading members cache, # it should come through as an update, as the field value changed. - assert incremental_result.profile_updates["@other_user:test"] is not None + assert profile_updates["@other_user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@other_user:test"]["field"], + profile_updates["@other_user:test"]["field"], "new value", ) @@ -2684,11 +2779,12 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase): request_key=generate_request_key(), ) ) + profile_updates = incremental_result.profiles.profile_updates # Even though we've quite recently sent down this value, we should still # see it again as it is a change - assert incremental_result.profile_updates["@other_user:test"] is not None + assert profile_updates["@other_user:test"] is not None self.assertEqual( - incremental_result.profile_updates["@other_user:test"]["field"], + profile_updates["@other_user:test"]["field"], "value", )