Support for profile updates over Sliding Sync (MSC4262) (#20003)

This PR implements support for profile updates over Sliding Sync:
https://github.com/matrix-org/matrix-spec-proposals/pull/4262. This pr
may be easier to review as a whole than commit by commit.

This builds on the legacy sync profile updates feature
https://github.com/element-hq/synapse/pull/19556, specifically the
profile updates stream it added.

Submitting for early review to get consensus on implementation. There
are some things we would like to add still, from spec, mainly:

* > Homeservers should only consider a profile field update "accepted"
by a client
> once the client returns with a new /sync request with the next /sync
token,
> NOT just after sending down the profile update. The client may never
receive
> response due to network conditions, or a bug in the client
implementation.
* > When a room enters this subset in this connection for the first
time, all requested
> fields from profiles of users in that room MAY be sent down. This
gives the client
> a base set of information for which future field updates can be
applied on top of.
> The homeserver MAY omit some fields and profiles if it believes that
the client has
> already received them, likewise repeat profiles MAY be sent down based
on homeserver
  > implementation.
* > Finally, if the list of fields expands to cover a new field ID,
those fields should
> be sent down for all users that are within the current room subset.
Future incremental
  > updates will then include changes to this field.
* Additionally, we would need to implement a lazy loading cache similar
to the legacy sync. (not part of MSC as such)

Depending on review these could either be added to this pr, or to keep
this pr from not growing too much, be added in a follow-up pr, as they
are more enhancement to this base sliding sync profile updates
functionality than a part of the core functionality.

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Olivier 'reivilibre' <olivier@librepush.net>
Co-authored-by: Olivier 'reivilibre <oliverw@element.io>
This commit is contained in:
Jason Robinson
2026-08-25 14:37:03 +00:00
committed by GitHub
co-authored by Copilot Autofix powered by AI Olivier 'reivilibre' Olivier 'reivilibre
parent 3db77e80a5
commit 4bb07152c8
21 changed files with 1874 additions and 98 deletions
+2
View File
@@ -0,0 +1,2 @@
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.
@@ -15,7 +15,7 @@ enable_registration_without_verification: true
bcrypt_rounds: 4
url_preview_enabled: true
url_preview_ip_range_blacklist: []
# MSC4429 Profile updates down legacy /sync
# MSC4429 and MSC4262 Profile updates down sync
include_profile_updates_in_sync: true
## Registration ##
@@ -339,7 +339,7 @@ include_profile_data_on_invite: false
### `include_profile_updates_in_sync`
*(boolean)* Use this option to include updates of other users' profiles in sync responses, for users who share rooms.
Requires an [MSC4429](https://github.com/matrix-org/matrix-spec-proposals/pull/4429) compatible client, and is currently limited to legacy sync and local users only.
For legacy sync clients, requires [MSC4429](https://github.com/matrix-org/matrix-spec-proposals/pull/4429) compatibility. For sliding sync clients, requires [MSC4262](https://github.com/matrix-org/matrix-spec-proposals/pull/4262) compatibility. Note, profile updates via sync are currently limited to local users only.
This feature is under development and should be used with caution on busy servers or servers which depend on `limit_profile_requests_to_users_who_share_rooms` for ensuring profile information doesn't leak across rooms. Defaults to `false`.
Example configuration:
+4
View File
@@ -251,6 +251,9 @@ pub struct UnstableFeatureMap {
/// MSC4169: Backwards-compatible redaction sending using `/send`
#[serde(rename = "com.beeper.msc4169")]
msc4169: bool,
/// MSC4262: Profile updates for simplified sliding sync.
#[serde(rename = "org.matrix.msc4262")]
msc4262: bool,
/// MSC4354: Sticky events
#[serde(rename = "org.matrix.msc4354")]
msc4354: bool,
@@ -320,6 +323,7 @@ pub fn synapse_config_to_global_unstable_feature_map(
msc4155: config.experimental.msc4155_enabled,
msc4306: config.experimental.msc4306_enabled,
msc4169: config.experimental.msc4169_enabled,
msc4262: config.server.include_profile_updates_in_sync,
msc4354: config.experimental.msc4354_enabled,
msc4380: true,
msc4429: config.server.include_profile_updates_in_sync,
+5 -3
View File
@@ -281,9 +281,11 @@ properties:
Use this option to include updates of other users' profiles in sync responses,
for users who share rooms.
Requires an [MSC4429](https://github.com/matrix-org/matrix-spec-proposals/pull/4429)
compatible client, and is currently limited to legacy sync and local users only.
For legacy sync clients, requires [MSC4429](https://github.com/matrix-org/matrix-spec-proposals/pull/4429)
compatibility. For sliding sync clients, requires
[MSC4262](https://github.com/matrix-org/matrix-spec-proposals/pull/4262) compatibility. Note, profile updates
via sync are currently limited to local users only.
This feature is under development and should be used with caution on busy servers or
servers which depend on `limit_profile_requests_to_users_who_share_rooms` for ensuring
profile information doesn't leak across rooms.
+5 -1
View File
@@ -441,6 +441,7 @@ class ProfileUpdateAction(str, enum.Enum):
normally includes. This update action currently has no meaning for sync responses
that are not incremental and non-lazy.
"""
LEFT_ROOM = "left_room"
"""
This profile update row action represents a user leaving a room.
@@ -450,9 +451,12 @@ class ProfileUpdateAction(str, enum.Enum):
profiles, so clients can clear their cache containing the users profile data
they are no longer interested in.
"""
UPDATE = "update"
"""
This profile update row action represents a user updating a profile field.
This profile update row action represents a user updating one or more
profile fields.
'Updating' could mean creating, changing the value of, or deleting a field.
Depending on the type of sync (initial/incremental, lazy/non-lazy), either the
diff of profile field updates or all the current profile fields are included
+1 -1
View File
@@ -585,7 +585,7 @@ class ServerConfig(Config):
" 'allow_public_rooms_over_federation' is set."
)
# Whether to support MSC4429 profile updates down legacy /sync
# Whether to support MSC4429 and MSC4262 Profile updates down sync
self.include_profile_updates_in_sync = config.get(
"include_profile_updates_in_sync",
False,
+8 -1
View File
@@ -106,7 +106,9 @@ class ProfileHandler:
self._worker_locks = hs.get_worker_locks_handler()
# Profile updates stream
self._msc4429_enabled = hs.config.server.include_profile_updates_in_sync
self._include_profile_updates_in_sync = (
hs.config.server.include_profile_updates_in_sync
)
self._is_events_writer = (
hs.get_instance_name() in hs.config.worker.writers.events
)
@@ -767,6 +769,9 @@ class ProfileHandler:
) -> None:
"""Delete a field from a user's profile.
This should only be called for custom profile fields,
not displayname or avatar_url.
Preconditions:
- This must NOT be called as part of deactivating the user, because we will
notify modules about the change whilst claiming it is not related
@@ -780,6 +785,8 @@ class ProfileHandler:
field_name: The name of the profile field to remove.
by_admin: Whether this change was made by an administrator.
"""
assert field_name not in (ProfileFields.DISPLAYNAME, ProfileFields.AVATAR_URL)
if not self.hs.is_mine(target_user):
raise SynapseError(400, "User is not hosted on this homeserver")
+405 -1
View File
@@ -25,20 +25,31 @@ from typing import (
from typing_extensions import TypeAlias, assert_never
from synapse.api.constants import AccountDataTypes, EduTypes, StickyEvent
from synapse.api.constants import (
AccountDataTypes,
EduTypes,
EventTypes,
ProfileFields,
ProfileUpdateAction,
StickyEvent,
)
from synapse.events.utils import FilteredEvent
from synapse.handlers.receipts import ReceiptEventSource
from synapse.logging.opentracing import trace
from synapse.storage.databases.main.receipts import ReceiptInRoom
from synapse.types import (
Absent,
AbsentType,
DeviceListUpdates,
JsonDict,
JsonMapping,
JsonValue,
MultiWriterStreamToken,
SlidingSyncStreamToken,
StrCollection,
StreamToken,
ThreadSubscriptionsToken,
UserID,
)
from synapse.types.handlers.sliding_sync import (
HaveSentRoomFlag,
@@ -47,6 +58,7 @@ from synapse.types.handlers.sliding_sync import (
PerConnectionState,
SlidingSyncConfig,
SlidingSyncResult,
StateValues,
)
from synapse.types.rest.client import SlidingSyncStickyEventsToken
from synapse.util.async_helpers import (
@@ -80,6 +92,7 @@ class SlidingSyncExtensionHandler:
self._storage_controllers = hs.get_storage_controllers()
self._enable_thread_subscriptions = hs.config.experimental.msc4306_enabled
self._enable_sticky_events = hs.config.experimental.msc4354_enabled
self._enable_profiles = hs.config.server.include_profile_updates_in_sync
@trace
async def get_extensions_response(
@@ -197,6 +210,18 @@ class SlidingSyncExtensionHandler:
from_token=from_token,
)
profiles_coro = None
if sync_config.extensions.profiles is not Absent and self._enable_profiles:
profiles_coro = self.get_profiles_extension_response(
sync_config=sync_config,
profiles_request=sync_config.extensions.profiles,
actual_room_ids=actual_room_ids,
to_token=to_token,
from_token=from_token,
actual_room_response_map=actual_room_response_map,
actual_lists=actual_lists,
)
(
to_device_response,
e2ee_response,
@@ -205,6 +230,7 @@ class SlidingSyncExtensionHandler:
typing_response,
thread_subs_response,
sticky_events_response,
profiles_response,
) = await gather_optional_coroutines(
to_device_coro,
e2ee_coro,
@@ -213,6 +239,7 @@ class SlidingSyncExtensionHandler:
typing_coro,
thread_subs_coro,
sticky_events_coro,
profiles_coro,
)
return SlidingSyncResult.Extensions(
@@ -223,6 +250,7 @@ class SlidingSyncExtensionHandler:
typing=typing_response,
thread_subscriptions=thread_subs_response,
sticky_events=sticky_events_response,
profiles=profiles_response,
)
def find_relevant_room_ids_for_extension(
@@ -1055,3 +1083,379 @@ class SlidingSyncExtensionHandler:
sticky_events_stream_id=sticky_events_to_id
),
)
async def _get_profile_ids_for_profiles_extension(
self,
user_id: str,
actual_room_ids: set[str],
sync_config: SlidingSyncConfig,
actual_room_response_map: Mapping[str, SlidingSyncResult.RoomResult],
actual_lists: Mapping[str, SlidingSyncResult.SlidingWindowList],
) -> tuple[set[str], set[str]]:
"""
Calculate target user profiles as candiates to include in the profile
extension sync response.
This function looks at both the sync config and the already calculated
rooms response, and pieces together the full set of user IDs to include
profiles for, based on sync config rooms being lazy loading or not.
For rooms with lazy loading, only profiles for those users who have sent events
into the timeline will be included, unless they would be included otherwise.
For other rooms, all members of the room will be included as candidates.
Note, this does not collect user IDs from the profile updates stream.
Args:
user_id: The full user ID syncing.
actual_room_ids: The actual room IDs in the the Sliding Sync response.
sync_config: The Sliding Sync config object.
actual_room_response_map: A calculated map of responses per room.
actual_lists: Sliding window API. A map of list key to list results in the
Sliding Sync response.
Returns:
Tuple containing two sets:
- first including all found user IDs,
- second containing user IDs calculated via lazy configured rooms.
"""
lazy_profile_user_ids = set()
non_lazy_profile_user_ids = set()
# Separate rooms into lazy and non-lazy based on sync config.
# Look at subscriptions first
lazy_rooms = (
{
room_id
for room_id, room_config in sync_config.room_subscriptions.items()
if (EventTypes.Member, StateValues.LAZY) in room_config.required_state
}
if sync_config.room_subscriptions
else set()
)
# Iterate lists to find lazy rooms
if sync_config.lists:
for list_name, list_data in sync_config.lists.items():
if (EventTypes.Member, StateValues.LAZY) in list_data.required_state:
for op in actual_lists[list_name].ops:
lazy_rooms.update(op.room_ids)
if lazy_rooms:
# For rooms configured as lazy, include users based on room response.
for room_id, room_data in actual_room_response_map.items():
if room_id not in lazy_rooms:
continue
# Include users from timeline events
for timeline_event in room_data.timeline_events:
lazy_profile_user_ids.add(timeline_event.event.sender)
# Include users from required state
for state_event in room_data.required_state:
if state_event.type == EventTypes.Member:
lazy_profile_user_ids.add(state_event.state_key)
# Include heroes
if room_data.heroes:
for hero in room_data.heroes:
lazy_profile_user_ids.add(hero.user_id)
non_lazy_rooms = actual_room_ids.difference(lazy_rooms)
# If we still have non-lazy rooms, get their members.
if non_lazy_rooms:
non_lazy_profile_user_ids = (
# TODO we should consider adding a limit to how many profiles
# of room members we push down the line. However, this produces
# a problem for clients in that they won't know which users
# just don't have any profile information, and which users were limited
# out. If we had an endpoint to fetch a list of profiles at once,
# we could have a hard limit here and clients could fetch the missing
# profiles separately for non-lazy initial sync cases.
await self.store.get_local_users_who_share_room_with_user(
user_id,
limit_to_rooms=non_lazy_rooms,
)
)
# Unify the two lists
profile_user_ids = lazy_profile_user_ids.union(non_lazy_profile_user_ids)
# Return a tuple containing the full list of user IDs and the lazy subset.
return (
profile_user_ids,
lazy_profile_user_ids,
)
async def _get_profiles_extension_initial_sync_response(
self,
user_id: UserID,
fields: set[str] | None,
profile_user_ids: set[str],
) -> dict[str, JsonDict]:
"""
Build an initial sync response for the profiles extension.
Args:
user_id: The syncing user UserID
fields: A set of fields to include in the response.
`None` means all fields.
profile_user_ids: Set of user IDs whose profiles are related to this sync response.
Returns:
A dictionary (in API response format) mapping users to their
profile updates in an `updated` dictionary.
{
"@user:example.org": {
"updated": {
"displayname": "Somebody",
"avatar_url": "mxc://example.org/123123123",
"org.example.field": "hiss",
...
}
},
...
}
"""
response: dict[str, JsonDict] = {}
# This doesn't return entries for the users with no profile data,
# which is good as we don't want to generate anything for users
# with no profile data in initial sync.
profile_data_by_user = await self.store.get_profile_data_for_users(
# Force our own user to be in the set, as we should
# always watch our own profile updates
profile_user_ids | {user_id.to_string()}
)
# Serialise the profile updates into the sync response format.
for profile_user_id, profile_data in profile_data_by_user.items():
per_user_updates: dict[str, JsonValue | dict[str, JsonValue]]
# Include the fields the client asked for, or all, if not specified
if fields is not None:
per_user_updates = {
k: v for k, v in profile_data.items() if k in fields
}
else:
per_user_updates = profile_data
if per_user_updates:
response[profile_user_id] = {
"updated": per_user_updates,
}
return response
async def get_profiles_extension_response(
self,
sync_config: SlidingSyncConfig,
profiles_request: SlidingSyncConfig.Extensions.ProfilesExtension,
actual_room_ids: set[str],
to_token: StreamToken,
from_token: SlidingSyncStreamToken | None,
actual_room_response_map: Mapping[str, SlidingSyncResult.RoomResult],
actual_lists: Mapping[str, SlidingSyncResult.SlidingWindowList],
) -> SlidingSyncResult.Extensions.ProfilesExtension | None:
"""
Generate a response for the profiles extension.
Args:
sync_config: The Sliding Sync config.
profiles_request: The profiles extension request.
actual_room_ids: The actual room IDs in the the Sliding Sync response.
to_token: The stream token to generate a response until.
from_token: The stream token to generate a response from.
actual_room_response_map: A calculated map of responses per room.
actual_lists: Sliding window API. A map of list key to list results in the
Sliding Sync response.
Returns:
- A SlidingSyncResult.Extensions.ProfilesExtension object containing
all the users who have profile updates.
- None if the extension is disabled.
"""
if not profiles_request.enabled:
return None
user_id = sync_config.user.to_string()
fields = (
set(profiles_request.fields)
if profiles_request.fields is not Absent
else None
)
response: dict[str, JsonDict | None] = {}
(
profile_user_ids,
lazy_profile_user_ids,
) = await self._get_profile_ids_for_profiles_extension(
user_id=user_id,
actual_room_ids=actual_room_ids,
sync_config=sync_config,
actual_room_response_map=actual_room_response_map,
actual_lists=actual_lists,
)
if from_token is None:
# Initial sync
return SlidingSyncResult.Extensions.ProfilesExtension(
users=await self._get_profiles_extension_initial_sync_response(
user_id=sync_config.user,
fields=fields,
profile_user_ids=profile_user_ids,
),
)
# Incremental sync
updates = await self.store.get_profile_updates_for_user_and_fields(
from_id=from_token.stream_token.profile_updates_key,
to_id=to_token.profile_updates_key,
user_id=user_id,
field_names=fields,
)
# Set of users that just joined their first room that we share with them
joined_room_user_ids: set[str] = set()
# Set of tracked users that have updated their profile
updated_user_ids: set[str] = set()
# Set of tracked users that just left their last room that we share with them
left_room_user_ids: set[str] = set()
# Process updates in stream order
# We need to be careful of users that have multiple types of updates
# within this sequence of stream rows.
for update in updates:
if update.action == ProfileUpdateAction.JOINED_ROOM:
joined_room_user_ids.add(update.user_id)
# If the user joins a shared room, that overrides
# the fact that they previously left the last shared room
left_room_user_ids.discard(update.user_id)
elif update.action == ProfileUpdateAction.UPDATE:
updated_user_ids.add(update.user_id)
elif update.action == ProfileUpdateAction.LEFT_ROOM:
left_room_user_ids.add(update.user_id)
# If the user leaves their last shared room, that overrides
# the fact that they previously joined a shared room
# and perhaps updated their profile whilst they were in it
joined_room_user_ids.discard(update.user_id)
updated_user_ids.discard(update.user_id)
# Add the users who joined a shared room or updated their profile to the set of
# users we will serialise profiles for
profile_user_ids.update(joined_room_user_ids)
profile_user_ids.update(updated_user_ids)
# Process left rooms
for other_user_id in left_room_user_ids:
# Return a null response to the client
# This tells the client that it will no longer receive updates for the user
response[other_user_id] = None
updated_user_fields: dict[str, set[str]] = {}
# Set fields from updates
for update in updates:
if (
update.action != ProfileUpdateAction.UPDATE
or not update.affected_fields
or update.user_id in left_room_user_ids
# Skip if not interested in this user
or update.user_id not in profile_user_ids
):
continue
interesting_changed_fields: set[str]
if fields is not None:
interesting_changed_fields = set(update.affected_fields) & fields
else:
interesting_changed_fields = set(update.affected_fields)
if not interesting_changed_fields:
# Skip the update as the client is not interested in these fields
continue
updated_user_fields.setdefault(update.user_id, set()).update(
interesting_changed_fields
)
profile_data_by_user = await self.store.get_profile_data_for_users(
profile_user_ids,
)
# Serialise the profile updates into the sync response format.
for profile_user_id in profile_user_ids:
if profile_user_id in left_room_user_ids:
continue
profile_data = profile_data_by_user.get(profile_user_id)
if profile_data is None:
# We don't have profile data for this user
# (This is different from having an empty profile)
# Return a null in incremental sync, telling the client to
# remove all profile information for this user.
response[profile_user_id] = None
continue
# Calculate which fields had updates
updated_fields: set[str] = updated_user_fields.get(profile_user_id, set())
# Calculate the full available field list
user_fields = set(profile_data.keys()).union(updated_fields)
# If the user joined the room or is included via lazy loading events,
# include all fields the client wants. This happens because when lazy
# a room, clients will not necessarily have the profile for the user that
# sent an event in the room, and thus we deliver all the fields. The same
# is true if another user joins the room - we need to deliver an initial
# state for clients to work on.
# For non-lazy-loaded users, include only updated fields. We assume clients
# with non-lazy loaded rooms have received the profiles for all the members
# in the room, and thus only need updates.
user_fields = (
user_fields
if profile_user_id in joined_room_user_ids
or profile_user_id in lazy_profile_user_ids
else updated_fields
)
# Filter down if the client only wants a subset
if fields:
user_fields = user_fields.intersection(fields)
if not user_fields:
continue
per_user_updates: dict[str, JsonValue | dict[str, JsonValue]] = {}
per_user_removals: set[str] = set()
for field_name in user_fields:
# For custom fields the lack of a field means it will be `Absent`,
# for displayname/avatar_url it will be `None`, due to way we store
# things differently.
# FIXME: I intend to simplify this by pushing the special-case logic
# for these 'original' profile fields into the storage layer instead.
absent_type = (
Absent
if field_name
not in (ProfileFields.DISPLAYNAME, ProfileFields.AVATAR_URL)
else None
)
field_value: JsonValue | dict[str, JsonValue] | AbsentType = (
profile_data.get(field_name, absent_type)
)
if (
# If the field isn't found on the profile and it is present in
# `updated_fields`, that means an existing field has been removed.
# We need the check against `updated_fields` as some profile fields
# are `None` by default, for example each and every user created
# by Synapse will have `avatar_url: None`, and we don't want to
# constantly send that to the clients.
field_value is absent_type and field_name in updated_fields
):
per_user_removals.add(field_name)
else:
per_user_updates[field_name] = cast(JsonValue, field_value)
if per_user_updates or per_user_removals:
entry: dict[str, JsonValue | JsonDict] = {}
response[profile_user_id] = entry
if per_user_updates:
entry["updated"] = per_user_updates
if per_user_removals:
entry["removed"] = list(per_user_removals)
return SlidingSyncResult.Extensions.ProfilesExtension(
users=response,
)
+36 -3
View File
@@ -55,7 +55,13 @@ from synapse.http.servlet import (
from synapse.http.site import SynapseRequest
from synapse.logging.opentracing import log_kv, set_tag, trace_with_opname
from synapse.rest.admin.experimental_features import ExperimentalFeature
from synapse.types import JsonDict, Requester, SlidingSyncStreamToken, StreamToken
from synapse.types import (
JsonDict,
JsonMapping,
Requester,
SlidingSyncStreamToken,
StreamToken,
)
from synapse.types.rest.client import SlidingSyncBody
from synapse.util.caches.lrucache import LruCache
from synapse.util.cancellation import cancellable
@@ -123,7 +129,9 @@ class SyncRestServlet(RestServlet):
self._event_serializer = hs.get_event_client_serializer()
self._msc2654_enabled = hs.config.experimental.msc2654_enabled
self._msc3773_enabled = hs.config.experimental.msc3773_enabled
self._msc4429_enabled = hs.config.server.include_profile_updates_in_sync
self._include_profile_updates_in_sync = (
hs.config.server.include_profile_updates_in_sync
)
self._json_filter_cache: LruCache[str, bool] = LruCache(
max_size=1000,
@@ -352,7 +360,7 @@ 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._include_profile_updates_in_sync and sync_result.profile_updates:
# FIXME: See issue https://github.com/element-hq/synapse/issues/19981
# for concerns around the current implementation of the profile
# updates stream.
@@ -1142,8 +1150,33 @@ class SlidingSyncRestServlet(RestServlet):
requester, extensions.sticky_events, ref_rooms_results
)
if extensions.profiles:
serialized_extensions[
"org.matrix.msc4262.profiles"
] = await self._serialise_profiles(
extensions.profiles,
)
return serialized_extensions
async def _serialise_profiles(
self,
profiles: SlidingSyncResult.Extensions.ProfilesExtension,
) -> JsonMapping:
"""
Serialise the profiles extension response.
Args:
profiles: The generated profiles response object.
Returns:
A dictionary containing the response `users` with the
generated profile updates.
"""
return {
"users": profiles.users,
}
async def _serialise_sticky_events(
self,
requester: Requester,
+4 -2
View File
@@ -269,7 +269,9 @@ class PersistEventsStore:
self._clock = hs.get_clock()
self._instance_name = hs.get_instance_name()
self._msc4354_enabled = hs.config.experimental.msc4354_enabled
self._msc4429_enabled = hs.config.server.include_profile_updates_in_sync
self._include_profile_updates_in_sync = (
hs.config.server.include_profile_updates_in_sync
)
self._ephemeral_messages_enabled = hs.config.server.enable_ephemeral_messages
self.is_mine_id = hs.is_mine_id
@@ -2121,7 +2123,7 @@ class PersistEventsStore:
txn, {m for m in members_to_cache_bust if not self.hs.is_mine_id(m)}
)
if self._msc4429_enabled:
if self._include_profile_updates_in_sync:
# Handle changes to the profile updates stream.
# We've already done a bunch of work calculating the changes needed
# for the sliding sync tables, so we may as well re-use that information
+44 -33
View File
@@ -86,7 +86,9 @@ class ProfileWorkerStore(SQLBaseStore):
"populate_full_user_id_profiles", self.populate_full_user_id_profiles
)
self._msc4429_enabled = hs.config.server.include_profile_updates_in_sync
self._include_profile_updates_in_sync = (
hs.config.server.include_profile_updates_in_sync
)
self._is_events_writer = self._instance_name in hs.config.worker.writers.events
self._profile_updates_id_gen: MultiWriterIdGenerator = MultiWriterIdGenerator(
db_conn=db_conn,
@@ -403,6 +405,7 @@ class ProfileWorkerStore(SQLBaseStore):
"get_updated_profile_updates", _get_updated_profile_updates_txn
)
# FIXME this function should be deleted, it's not used.
async def get_profile_updates_for_fields(
self,
*,
@@ -500,7 +503,7 @@ class ProfileWorkerStore(SQLBaseStore):
from_id: int,
to_id: int,
user_id: str,
field_names: Set[str],
field_names: Set[str] | None,
include_users: set[str] | None = None,
) -> list[ProfileUpdate]:
"""Get profile update markers for a user in a stream range.
@@ -515,15 +518,16 @@ class ProfileWorkerStore(SQLBaseStore):
to_id: The ending stream ID (inclusive).
user_id: The full user ID to filter on.
field_names: Set of field names to filter update actions against.
`None` means "include all fields".
include_users: If given, only include updates for these user IDs.
Returns:
A list of ProfileUpdates update rows.
A list of ProfileUpdate update rows, in stream order
"""
if from_id >= to_id:
return []
if len(field_names) == 0:
if field_names is not None and len(field_names) == 0:
return []
if include_users is not None and len(include_users) == 0:
@@ -533,22 +537,27 @@ class ProfileWorkerStore(SQLBaseStore):
def _get_profile_updates_for_user_and_fields_txn(
txn: LoggingTransaction,
) -> list[ProfileUpdate]:
wanted_field_in_elems_clause, wanted_field_in_elems_args = (
make_in_list_sql_clause(
# Build a `field_clause` that matches updates containing the fields we are interested in
if field_names is None:
# We are interested in all fields, so match any update with fields
field_clause = "pu.affected_fields IS NOT NULL"
field_args: list[str] = []
else:
wanted_field_in_elems_clause, field_args = make_in_list_sql_clause(
txn.database_engine, "field_names.value", field_names
)
)
if isinstance(txn.database_engine, PostgresEngine):
# Note that if we had a GIN index on `affected_fields`, this would defeat it.
# If we decide we want one, we should consider using the `?|` operator or its
# clearer-named `jsonb_exists_any` equivalent.
all_field_names_table_expression = "jsonb_array_elements_text(pu.affected_fields) AS field_names(value)"
else:
# json_each is a table-valued function that gives `value` as one of its column names
all_field_names_table_expression = (
"json_each(pu.affected_fields) AS field_names"
)
if isinstance(txn.database_engine, PostgresEngine):
# Note that if we had a GIN index on `affected_fields`, this would defeat it.
# If we decide we want one, we should consider using the `?|` operator or its
# clearer-named `jsonb_exists_any` equivalent.
all_field_names_table_expression = "jsonb_array_elements_text(pu.affected_fields) AS field_names(value)"
else:
# json_each is a table-valued function that gives `value` as one of its column names
all_field_names_table_expression = (
"json_each(pu.affected_fields) AS field_names"
)
field_clause = f"(EXISTS (SELECT 1 FROM {all_field_names_table_expression} WHERE {wanted_field_in_elems_clause}))"
user_clause = ""
user_args: list[str] = []
@@ -573,7 +582,7 @@ class ProfileWorkerStore(SQLBaseStore):
AND puf.user_id = ?
{user_clause}
AND (
(EXISTS (SELECT 1 FROM {all_field_names_table_expression} WHERE {wanted_field_in_elems_clause}))
{field_clause}
OR pu.action != ?
)
ORDER BY pu.stream_id ASC
@@ -583,7 +592,7 @@ class ProfileWorkerStore(SQLBaseStore):
to_id,
user_id,
*user_args,
*wanted_field_in_elems_args,
*field_args,
ProfileUpdateAction.UPDATE.value,
),
)
@@ -591,18 +600,20 @@ class ProfileWorkerStore(SQLBaseStore):
updates: list[ProfileUpdate] = []
for stream_id, updated_user_id, action, affected_fields_dbjson in rows:
if affected_fields_dbjson is not None:
# Get the field names that were affected by this update
affected_fields = frozenset(db_to_json(affected_fields_dbjson))
if field_names is not None:
# Only include the field names that we care about
affected_fields &= field_names
else:
affected_fields = None
updates.append(
ProfileUpdate(
stream_id=stream_id,
user_id=updated_user_id,
action=action,
affected_fields=(
# Get the field names that were affected by this update
# and intersect with the field names we care about
frozenset(db_to_json(affected_fields_dbjson)) & field_names
)
if affected_fields_dbjson is not None
else None,
affected_fields=affected_fields,
)
)
@@ -755,7 +766,7 @@ class ProfileWorkerStore(SQLBaseStore):
Returns:
The profile updates stream ID that was created in this transaction
"""
if self._msc4429_enabled:
if self._include_profile_updates_in_sync:
assert self._is_events_writer
self._check_profile_size(txn, user_id, field_name, new_value)
@@ -818,7 +829,7 @@ class ProfileWorkerStore(SQLBaseStore):
),
)
if not self._msc4429_enabled:
if not self._include_profile_updates_in_sync:
return None
# Record updates in the profile updates stream
@@ -847,7 +858,7 @@ class ProfileWorkerStore(SQLBaseStore):
users profile should be pushed to the client, should they need it
already even if the user hasn't actually joined the room.
"""
if not self._msc4429_enabled:
if not self._include_profile_updates_in_sync:
return
assert self._is_events_writer
@@ -884,7 +895,7 @@ class ProfileWorkerStore(SQLBaseStore):
txn: Transaction to use
user_id: User ID that made the profile update
action: The profile update action, either `update`, `left_room` or
`joined_room`
`joined_room`.
field_names: A list of fields that were set, if ProfileUpdateAction.UPDATE
user_rooms: Optionally, a set of rooms that the update concerns. If not
given, a database lookup will be done to fetch all the users rooms.
@@ -895,7 +906,7 @@ class ProfileWorkerStore(SQLBaseStore):
Returns:
The latest stream ID created in this transaction
"""
if not self._msc4429_enabled:
if not self._include_profile_updates_in_sync:
return None
if action == ProfileUpdateAction.UPDATE:
@@ -1010,7 +1021,7 @@ class ProfileWorkerStore(SQLBaseStore):
field_name: The name of the custom profile field.
"""
if self._msc4429_enabled:
if self._include_profile_updates_in_sync:
assert self._is_events_writer
def delete_profile_field(txn: LoggingTransaction) -> int | None:
@@ -1032,7 +1043,7 @@ class ProfileWorkerStore(SQLBaseStore):
(f'$."{field_name}"', user_id.localpart),
)
if not self._msc4429_enabled:
if not self._include_profile_updates_in_sync:
return None
stream_id = self.record_profile_updates_txn(
+16 -2
View File
@@ -1041,12 +1041,26 @@ 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`.
async def get_local_users_who_share_room_with_user(
self,
user_id: str,
limit_to_rooms: set[str] | None = None,
) -> set[str]:
"""
Returns the set of local users who share a room with `user_id`.
This also includes the `user_id` themselves.
Args:
user_id: The user ID to find the local users who share rooms.
limit_to_rooms: Optional set of rooms to limit to.
Returns:
Set of local user ID's who share a room with the given user.
"""
room_ids = await self.get_rooms_for_user(user_id)
if limit_to_rooms is not None:
room_ids = room_ids.intersection(limit_to_rooms)
user_who_share_room: set[str] = set()
for room_id in room_ids:
+1 -1
View File
@@ -175,7 +175,7 @@ Changes in SCHEMA_VERSION = 93
Changes in SCHEMA_VERSION = 94
- Add `recheck` column (boolean, default true) to the `redactions` table.
- MSC4242: Add state DAG tables.
- MSC4429: Track updates to user profile fields via a new stream.
- MSC4429/MSC4262: Track updates to user profile fields via a new stream.
"""
@@ -12,7 +12,7 @@
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
-- Track updates to profile fields.
-- For MSC4429 legacy /sync and others.
-- For MSC4429 and MSC4262 down sync and others.
-- See https://github.com/element-hq/synapse/issues/19981 for potential future directions of this table.
CREATE TABLE IF NOT EXISTS profile_updates (
stream_id BIGINT NOT NULL PRIMARY KEY,
+15
View File
@@ -442,6 +442,19 @@ class SlidingSyncResult:
def __bool__(self) -> bool:
return bool(self.room_id_to_sticky_events)
@attr.s(slots=True, frozen=True, auto_attribs=True)
class ProfilesExtension:
"""The Profile Updates extension (MSC4262)
Attributes:
users: map (user_id -> [profile_updates])
"""
users: Mapping[str, JsonMapping | None]
def __bool__(self) -> bool:
return bool(self.users)
to_device: ToDeviceExtension | None = None
e2ee: E2eeExtension | None = None
account_data: AccountDataExtension | None = None
@@ -449,6 +462,7 @@ class SlidingSyncResult:
typing: TypingExtension | None = None
thread_subscriptions: ThreadSubscriptionsExtension | None = None
sticky_events: StickyEventsExtension | None = None
profiles: ProfilesExtension | None = None
def __bool__(self) -> bool:
"""Are there any updates that should be returned immediately to
@@ -461,6 +475,7 @@ class SlidingSyncResult:
or self.typing
or self.thread_subscriptions
or self.sticky_events
or self.profiles
)
next_pos: SlidingSyncStreamToken
+15
View File
@@ -478,6 +478,18 @@ class SlidingSyncBody(RequestBodyModel):
limit: NonNegativeStrictInt = 100
since: SlidingSyncStickyEventsToken | AbsentType = Absent
class ProfilesExtension(RequestBodyModel):
"""The Profile Updates extension (MSC4262)
Attributes:
enabled
fields: List of fields to filter upon (optional)
"""
enabled: StrictBool = False
# Optionally filter on specific fields
fields: list[StrictStr] | AbsentType = Absent
to_device: ToDeviceExtension | None = None
e2ee: E2eeExtension | None = None
account_data: AccountDataExtension | None = None
@@ -489,6 +501,9 @@ class SlidingSyncBody(RequestBodyModel):
sticky_events: StickyEventsExtension | AbsentType = Field(
Absent, alias="org.matrix.msc4354.sticky_events"
)
profiles: ProfilesExtension | AbsentType = Field(
Absent, alias="org.matrix.msc4262.profiles"
)
conn_id: StrictStr | None = None
lists: (
+27
View File
@@ -391,6 +391,7 @@ T4 = TypeVar("T4")
T5 = TypeVar("T5")
T6 = TypeVar("T6")
T7 = TypeVar("T7")
T8 = TypeVar("T8")
@overload
@@ -544,6 +545,32 @@ async def gather_optional_coroutines(
]: ...
@overload
async def gather_optional_coroutines(
*coroutines: Unpack[
tuple[
Coroutine[Any, Any, T1] | None,
Coroutine[Any, Any, T2] | None,
Coroutine[Any, Any, T3] | None,
Coroutine[Any, Any, T4] | None,
Coroutine[Any, Any, T5] | None,
Coroutine[Any, Any, T6] | None,
Coroutine[Any, Any, T7] | None,
Coroutine[Any, Any, T8] | None,
]
],
) -> tuple[
T1 | None,
T2 | None,
T3 | None,
T4 | None,
T5 | None,
T6 | None,
T7 | None,
T8 | None,
]: ...
async def gather_optional_coroutines(
*coroutines: Unpack[tuple[Coroutine[Any, Any, T1] | None, ...]],
) -> tuple[T1 | None, ...]:
+13 -9
View File
@@ -199,13 +199,13 @@ class ProfileTestCase(unittest.HomeserverTestCase):
["m.status", '{"text": "Holiday", "emoji": "🏖"}'],
]
)
def test_update_profile_does_not_update_stream_on_set_field_if_msc4429_not_enabled(
def test_update_profile_does_not_update_stream_on_set_field_if_include_profile_updates_in_sync_not_enabled(
self,
field_name: str,
new_value: str,
) -> None:
"""Test that profile updates don't get recorded in the profile updates stream
if MSC4429 is not enabled."""
if `include_profile_updates_in_sync` is not enabled."""
self.get_success(
self.handler.set_field(
target_user=self.frank,
@@ -230,13 +230,13 @@ class ProfileTestCase(unittest.HomeserverTestCase):
["m.status", '{"text": "Holiday", "emoji": "🏖"}'],
]
)
def test_update_profile_does_not_notify_notifier_on_set_field_if_msc4429_not_enabled(
def test_update_profile_does_not_notify_notifier_on_set_field_if_include_profile_updates_in_sync_not_enabled(
self,
field_name: str,
new_value: str,
) -> None:
"""Test that profile updates do not cause the profile updates stream notifier
to wake up if MSC4429 is not enabled."""
to wake up if `include_profile_updates_in_sync` is not enabled."""
self.get_success(
self.handler.set_field(
target_user=self.frank,
@@ -265,7 +265,8 @@ class ProfileTestCase(unittest.HomeserverTestCase):
self, field_name: str, new_value: str
) -> None:
"""Test that profile updates do not cause the profile updates stream notifier
to wake up if the user is not in any rooms, if MSC4429 is enabled."""
to wake up if the user is not in any rooms, if `include_profile_updates_in_sync`
is enabled."""
self.get_success(
self.handler.set_field(
target_user=self.frank,
@@ -293,7 +294,7 @@ class ProfileTestCase(unittest.HomeserverTestCase):
self, field_name: str, new_value: str
) -> None:
"""Test that profile updates get recorded in the profile updates stream if
MSC4429 is enabled."""
`include_profile_updates_in_sync` is enabled."""
self.get_success(
self.handler.set_field(
target_user=self.frank,
@@ -320,6 +321,8 @@ class ProfileTestCase(unittest.HomeserverTestCase):
)
fields_updates = self.get_success(
# FIXME this function should be deleted, it's not used.
# Adapt this test to use the right one.
self.store.get_profile_updates_for_fields(
from_id=1,
to_id=2,
@@ -361,7 +364,7 @@ class ProfileTestCase(unittest.HomeserverTestCase):
self,
) -> None:
"""Test that profiles updates get recorded in the 'per user' profile updates
stream tracking table, if MSC4429 is enabled."""
stream tracking table, if `include_profile_updates_in_sync` is enabled."""
self.register_user("roger", "password")
roger_token = self.login("roger", "password")
self.register_user("millie", "password")
@@ -501,7 +504,8 @@ class ProfileTestCase(unittest.HomeserverTestCase):
self,
) -> None:
"""Test that previous profile update stream rows are removed for a user if
the user no longer shares rooms with another user, if MSC4429 is enabled.
the user no longer shares rooms with another user, if
`include_profile_updates_in_sync` is enabled.
This test ensures that when a user leaves a room, we clear all old profile
update rows of users who the user no longer shares rooms with, to avoid
@@ -668,7 +672,7 @@ class ProfileTestCase(unittest.HomeserverTestCase):
new_value: str,
) -> None:
"""Test that profile updates wake up the profile updates stream on profile
field updates, if MSC4429 is enabled."""
field updates, if `include_profile_updates_in_sync` is enabled."""
self.helper.create_room_as(
room_creator=self.frank.to_string(),
tok=self.frank_token,
+39 -38
View File
@@ -1187,8 +1187,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
)
def test_initial_sync_no_profile_updates_if_not_enabled(self) -> None:
"""Test that without MSC4429 enabled the initial sync response does not
contain any profile updates."""
"""Test that without `include_profile_updates_in_sync` enabled the initial sync
response does not contain any profile updates."""
self.get_success(
self.profile_handler.set_field(
target_user=UserID.from_string(self.other_user),
@@ -1212,8 +1212,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
@override_config({"include_profile_updates_in_sync": True})
def test_initial_sync_no_profile_updates_if_not_filtered_for(self) -> None:
"""Test that with MSC4429 enabled the initial sync response does not
contain any profile updates, if fields are not filtered for."""
"""Test that with `include_profile_updates_in_sync` enabled the initial sync
response does not contain any profile updates, if fields are not filtered for."""
self.get_success(
self.profile_handler.set_field(
target_user=UserID.from_string(self.other_user),
@@ -1240,9 +1240,9 @@ 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, for the fields the
client requests. This response should include our syncing user."""
"""Test that with `include_profile_updates_in_sync` enabled the initial sync
response does contain profile updates for users who share rooms, for the fields
the client requests. This response should include our syncing user."""
self.get_success(
self.profile_handler.set_field(
target_user=UserID.from_string(self.other_user),
@@ -1304,8 +1304,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
def test_initial_sync_does_not_include_untracked_users_profile_updates(
self, is_lazy: bool
) -> None:
"""Test that with MSC4429 enabled the initial sync response does not
contain profile updates for users who do not share rooms."""
"""Test that with `include_profile_updates_in_sync` enabled the initial sync
response does not contain profile updates for users who do not share rooms."""
third_user = self.register_user("third_user", "password")
self.get_success(
self.profile_handler.set_field(
@@ -1347,8 +1347,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
def test_initial_sync_lazy_loading_responds_with_only_profiles_with_events(
self,
) -> None:
"""Test that with MSC4429 enabled the initial sync lazy loading response does
contain profile updates for events in the timeline.
"""Test that with `include_profile_updates_in_sync` enabled the initial sync
lazy loading response does contain profile updates for events in the timeline.
This test ensures lazy loading sync only returns profiles that we also have
events for in the sync response. The second room in this test has the most
@@ -1424,8 +1424,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
def test_incremental_sync_sends_down_profile_update_diffs(
self,
) -> None:
"""Test that with MSC4429 enabled the incremental sync response does
contain profile update diffs."""
"""Test that with `include_profile_updates_in_sync` enabled the incremental
sync response does contain profile update diffs."""
requester = create_requester(self.user)
initial_result = self.get_success(
self.sync_handler.wait_for_sync_for_user(
@@ -1499,9 +1499,9 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
def test_incremental_sync_does_not_filter_profile_updates_when_lazy_loading(
self,
) -> None:
"""Test that with MSC4429 enabled the incremental sync lazy loading response
does contain profile updates even if the user would be filtered out by lazy
loading.
"""Test that with `include_profile_updates_in_sync` enabled the incremental
sync lazy loading response does contain profile updates even if the user would
be filtered out by lazy loading.
"""
third_user = self.register_user("third_user", "password")
third_tok = self.login("third_user", "password")
@@ -1652,7 +1652,7 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
is_initial: bool,
is_lazy: bool,
) -> None:
"""Test that with MSC4429 enabled any sync response
"""Test that with `include_profile_updates_in_sync` enabled any sync response
doesn't contain federated users even if there are timeline events from them.
"""
# Join a federated user to the room, causing a membership event into
@@ -1742,8 +1742,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
is_initial: bool,
is_lazy: bool,
) -> None:
"""Test that with MSC4429 enabled any sync response always contains the users
own updates.
"""Test that with `include_profile_updates_in_sync` enabled any sync response
always contains the users own updates.
This test is made with a user that is not in any rooms, to prove our code
to collect interested users from the profile updates always collect the user.
@@ -1830,8 +1830,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
is_initial: bool,
is_lazy: bool,
) -> None:
"""Test that with MSC4429 enabled a sync response correctly includes falsey
profile field values.
"""Test that with `include_profile_updates_in_sync` enabled a sync response
correctly includes falsey profile field values.
"""
requester = create_requester(self.user)
filter_json: dict[str, dict] = {
@@ -1911,8 +1911,9 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
def test_incremental_sync_lazy_loading_cache_filters_recently_sent_profiles_and_fields(
self,
) -> None:
"""Test that with MSC4429 enabled the incremental sync lazy loading response
filters out unchanged profiles or fields we have recently sent to the client.
"""Test that with `include_profile_updates_in_sync` enabled the incremental
sync lazy loading response filters out unchanged profiles or fields we have
recently sent to the client.
"""
requester = create_requester(self.user)
self.get_success(
@@ -2062,8 +2063,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
def test_incremental_sync_sends_down_null_profile_if_user_no_longer_sharing_rooms(
self,
) -> None:
"""Test that with MSC4429 enabled the incremental sync response
includes a 'null' for users who are no longer sharing rooms.
"""Test that with `include_profile_updates_in_sync` enabled the incremental
sync response includes a 'null' for users who are no longer sharing rooms.
"""
requester = create_requester(self.user)
initial_result = self.get_success(
@@ -2112,9 +2113,9 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
def test_incremental_sync_sends_down_all_requested_fields_for_users_who_have_joined(
self,
) -> None:
"""Test that with MSC4429 enabled the incremental sync response
includes all the requested fields of a user who has joined a room with the
syncing user.
"""Test that with `include_profile_updates_in_sync` enabled the incremental
sync response includes all the requested fields of a user who has joined a room
with the syncing user.
"""
requester = create_requester(self.user)
initial_result = self.get_success(
@@ -2211,8 +2212,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
)
@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."""
"""Test that with `include_profile_updates_in_sync` 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", "avatar_url"]}
@@ -2275,8 +2276,8 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
eager_sync: bool,
is_lazy: bool,
) -> None:
"""Test that with MSC4429 enabled the incremental sync response
correctly handles multiple join / leave / join / leave in a row.
"""Test that with `include_profile_updates_in_sync` enabled the incremental
sync response correctly handles multiple join / leave / join / leave in a row.
In the first variant we sync and check after each iteration of join/leave.
In the second variant we only sync at the end of all the join/leaves.
@@ -2440,9 +2441,9 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
value: str | bool | list | dict | int | float | None,
new_value: str | bool | list | dict | int | float | None,
) -> None:
"""Test that with MSC4429 enabled the incremental lazy sync response
includes all the profile update changes for the user, even if the profile
field has been recently sent and is in our lazy loading cache.
"""Test that with `include_profile_updates_in_sync` enabled the incremental
lazy sync response includes all the profile update changes for the user, even
if the profile field has been recently sent and is in our lazy loading cache.
Parameterize across different types of potential value types that profile
field updates could have to ensure robustness.
@@ -2559,9 +2560,9 @@ class SyncProfileUpdatesTestCase(tests.unittest.HomeserverTestCase):
def test_lazy_loading_cache_and_multiple_updates_to_the_same_field(
self,
) -> None:
"""Test that with MSC4429 enabled the incremental lazy sync response
includes an update to a field, even when the value changes back to a
value set and cached previously.
"""Test that with `include_profile_updates_in_sync` enabled the incremental
lazy sync response includes an update to a field, even when the value changes
back to a value set and cached previously.
"""
requester = create_requester(self.user)
filter_json = {
File diff suppressed because it is too large Load Diff