Add MSC4262 Profile Updates via Sliding Sync dummy response and config

This commit is contained in:
Jason Robinson
2026-08-11 14:57:25 +03:00
parent e1bf42508b
commit 63fe546d47
6 changed files with 244 additions and 1 deletions
@@ -80,6 +80,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 +198,16 @@ 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,
all_interested_room_ids=all_interested_room_ids,
to_token=to_token,
from_token=from_token,
)
(
to_device_response,
e2ee_response,
@@ -205,6 +216,7 @@ class SlidingSyncExtensionHandler:
typing_response,
thread_subs_response,
sticky_events_response,
profiles_response,
) = await gather_optional_coroutines(
to_device_coro,
e2ee_coro,
@@ -213,6 +225,7 @@ class SlidingSyncExtensionHandler:
typing_coro,
thread_subs_coro,
sticky_events_coro,
profiles_coro,
)
return SlidingSyncResult.Extensions(
@@ -223,6 +236,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 +1069,18 @@ class SlidingSyncExtensionHandler:
sticky_events_stream_id=sticky_events_to_id
),
)
async def get_profiles_extension_response(
self,
sync_config: SlidingSyncConfig,
profiles_request: SlidingSyncConfig.Extensions.ProfilesExtension,
all_interested_room_ids: set[str],
to_token: StreamToken,
from_token: SlidingSyncStreamToken | None,
) -> SlidingSyncResult.Extensions.ProfilesExtension | None:
if not profiles_request.enabled:
return None
return SlidingSyncResult.Extensions.ProfilesExtension(
users={},
)
+22 -1
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
@@ -1142,8 +1148,23 @@ class SlidingSyncRestServlet(RestServlet):
requester, extensions.sticky_events, ref_rooms_results
)
if extensions.profiles:
serialized_extensions[
"org.matrix.msc4262.profiles"
] = await self._serialise_profiles(
requester, extensions.profiles, ref_rooms_results
)
return serialized_extensions
async def _serialise_profiles(
self,
requester: Requester,
profiles: SlidingSyncResult.Extensions.ProfilesExtension,
ref_rooms_results: Mapping[str, SlidingSyncResult.RoomResult],
) -> JsonMapping:
return profiles.users
async def _serialise_sticky_events(
self,
requester: Requester,
+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]
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 | None = False
# Optionally filter on specific fields
fields: list[StrictStr] | None = []
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, ...]:
@@ -0,0 +1,136 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2024 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
import logging
from parameterized import parameterized, parameterized_class
from twisted.internet.testing import MemoryReactor
import synapse.rest.admin
from synapse.rest.client import login, profile, room, sync
from synapse.server import HomeServer
from synapse.util.clock import Clock
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
from tests.unittest import override_config
logger = logging.getLogger(__name__)
# FIXME: This can be removed once we bump `SCHEMA_COMPAT_VERSION` and run the
# foreground update for
# `sliding_sync_joined_rooms`/`sliding_sync_membership_snapshots` (tracked by
# https://github.com/element-hq/synapse/issues/17623)
@parameterized_class(
("use_new_tables",),
[
(True,),
(False,),
],
class_name_func=lambda cls,
num,
params_dict: f"{cls.__name__}_{'new' if params_dict['use_new_tables'] else 'fallback'}",
)
class SlidingSyncProfilesTestCase(SlidingSyncBase):
"""Tests for the profile updates sliding sync extension"""
servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
profile.register_servlets,
room.register_servlets,
sync.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.store = hs.get_datastores().main
super().prepare(reactor, clock, hs)
@parameterized.expand(
[
True,
False,
]
)
def test_no_data_when_not_enabled(self, is_initial: bool) -> None:
"""
Test that no profile extension response is returned
if the feature is not enabled.
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
# Make an initial Sliding Sync request with the profiles extension enabled
sync_body = {
"lists": {},
"extensions": {
"org.matrix.msc4262.profiles": {
"enabled": True,
},
},
}
response_body, from_token = self.do_sync(sync_body, tok=user1_tok)
self.assertIsNone(response_body["extensions"].get("org.matrix.msc4262.profiles"))
if not is_initial:
# Make an incremental Sliding Sync request
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
self.assertIsNone(response_body["extensions"].get("org.matrix.msc4262.profiles"))
@override_config({"include_profile_updates_in_sync": True})
def test_no_data_initial_sync(self) -> None:
"""
Test that enabling the profiles extension works during an initial sync,
even if there is no-data.
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
# Make an initial Sliding Sync request with the profiles extension enabled
sync_body = {
"lists": {},
"extensions": {
"org.matrix.msc4262.profiles": {
"enabled": True,
},
},
}
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
self.assertIsNone(response_body["extensions"].get("org.matrix.msc4262.profiles"))
@override_config({"include_profile_updates_in_sync": True})
def test_no_data_incremental_sync(self) -> None:
"""
Test that enabling profiles extension works during an incremental sync, even
if there is no-data.
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
sync_body = {
"lists": {},
"extensions": {
"org.matrix.msc4262.profiles": {
"enabled": True,
}
},
}
_, from_token = self.do_sync(sync_body, tok=user1_tok)
# Make an incremental Sliding Sync request with the profiles extension enabled
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
self.assertIsNone(response_body["extensions"].get("org.matrix.msc4262.profiles"))