mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-28 05:04:38 +00:00
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:
co-authored by
Copilot Autofix powered by AI
Olivier 'reivilibre'
Olivier 'reivilibre
parent
3db77e80a5
commit
4bb07152c8
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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, ...]:
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user