Adjust MSC4429 profile updates over legacy removed fields in response

Due to MSC4429 being in flux and clients having implemented field removals early, the previously merged implementation pushed field removals by setting the value to `null`. The MSC will require delivering the fields in a separate `removed_profile_fields` key as `None` is a valid field value, see https://github.com/matrix-org/matrix-spec-proposals/pull/4429#discussion_r3512513534

Thus, as we moved storing field deletes to `ProfileUpdateAction.DELETE`, we need to adjust the legacy sync to properly identify deletes. At the same time have implemented the new `removed_profile_fields` key, so we have it ready for simplifying the legacy sync response in the future.
This commit is contained in:
Jason Robinson
2026-08-18 15:13:27 +03:00
parent d7f093c3c8
commit 84946f8d30
4 changed files with 289 additions and 107 deletions
+5 -1
View File
@@ -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.
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.
+110 -25
View File
@@ -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)
+2 -5
View File
@@ -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)
+172 -76
View File
@@ -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",
)