diff options
author | Erik Johnston <erik@matrix.org> | 2024-06-18 14:06:08 +0100 |
---|---|---|
committer | Erik Johnston <erik@matrix.org> | 2024-06-18 14:06:08 +0100 |
commit | edc36df4094678448ee92339f048c197e5c9f057 (patch) | |
tree | 1f861eaf774aa072094e2ca143bdfd72bab7a1ba /synapse/rest/client | |
parent | Merge remote-tracking branch 'origin/release-v1.109' into matrix-org-hotfixes (diff) | |
parent | Merge branch 'master' into develop (diff) | |
download | synapse-edc36df4094678448ee92339f048c197e5c9f057.tar.xz |
Merge remote-tracking branch 'origin/develop' into matrix-org-hotfixes
Diffstat (limited to 'synapse/rest/client')
-rw-r--r-- | synapse/rest/client/account.py | 6 | ||||
-rw-r--r-- | synapse/rest/client/devices.py | 4 | ||||
-rw-r--r-- | synapse/rest/client/directory.py | 2 | ||||
-rw-r--r-- | synapse/rest/client/keys.py | 92 | ||||
-rw-r--r-- | synapse/rest/client/knock.py | 8 | ||||
-rw-r--r-- | synapse/rest/client/media.py | 2 | ||||
-rw-r--r-- | synapse/rest/client/models.py | 99 | ||||
-rw-r--r-- | synapse/rest/client/reporting.py | 12 | ||||
-rw-r--r-- | synapse/rest/client/room.py | 11 | ||||
-rw-r--r-- | synapse/rest/client/sync.py | 230 |
10 files changed, 299 insertions, 167 deletions
diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 6ac07d354c..8daa449f9e 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -56,14 +56,14 @@ from synapse.http.servlet import ( from synapse.http.site import SynapseRequest from synapse.metrics import threepid_send_requests from synapse.push.mailer import Mailer -from synapse.rest.client.models import ( +from synapse.types import JsonDict +from synapse.types.rest import RequestBodyModel +from synapse.types.rest.client import ( AuthenticationData, ClientSecretStr, EmailRequestTokenBody, MsisdnRequestTokenBody, ) -from synapse.rest.models import RequestBodyModel -from synapse.types import JsonDict from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.stringutils import assert_valid_client_secret, random_string from synapse.util.threepids import check_3pid_allowed, validate_email diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py index b1b803549e..8313d687b7 100644 --- a/synapse/rest/client/devices.py +++ b/synapse/rest/client/devices.py @@ -42,9 +42,9 @@ from synapse.http.servlet import ( ) from synapse.http.site import SynapseRequest from synapse.rest.client._base import client_patterns, interactive_auth_handler -from synapse.rest.client.models import AuthenticationData -from synapse.rest.models import RequestBodyModel from synapse.types import JsonDict +from synapse.types.rest import RequestBodyModel +from synapse.types.rest.client import AuthenticationData if TYPE_CHECKING: from synapse.server import HomeServer diff --git a/synapse/rest/client/directory.py b/synapse/rest/client/directory.py index 8099fdf3e4..11fdd0f7c6 100644 --- a/synapse/rest/client/directory.py +++ b/synapse/rest/client/directory.py @@ -41,8 +41,8 @@ from synapse.http.servlet import ( ) from synapse.http.site import SynapseRequest from synapse.rest.client._base import client_patterns -from synapse.rest.models import RequestBodyModel from synapse.types import JsonDict, RoomAlias +from synapse.types.rest import RequestBodyModel if TYPE_CHECKING: from synapse.server import HomeServer diff --git a/synapse/rest/client/keys.py b/synapse/rest/client/keys.py index a0017257ce..67de634eab 100644 --- a/synapse/rest/client/keys.py +++ b/synapse/rest/client/keys.py @@ -36,7 +36,6 @@ from synapse.http.servlet import ( ) from synapse.http.site import SynapseRequest from synapse.logging.opentracing import log_kv, set_tag -from synapse.replication.http.devices import ReplicationUploadKeysForUserRestServlet from synapse.rest.client._base import client_patterns, interactive_auth_handler from synapse.types import JsonDict, StreamToken from synapse.util.cancellation import cancellable @@ -105,13 +104,8 @@ class KeyUploadServlet(RestServlet): self.auth = hs.get_auth() self.e2e_keys_handler = hs.get_e2e_keys_handler() self.device_handler = hs.get_device_handler() - - if hs.config.worker.worker_app is None: - # if main process - self.key_uploader = self.e2e_keys_handler.upload_keys_for_user - else: - # then a worker - self.key_uploader = ReplicationUploadKeysForUserRestServlet.make_client(hs) + self._clock = hs.get_clock() + self._store = hs.get_datastores().main async def on_POST( self, request: SynapseRequest, device_id: Optional[str] @@ -151,9 +145,10 @@ class KeyUploadServlet(RestServlet): 400, "To upload keys, you must pass device_id when authenticating" ) - result = await self.key_uploader( + result = await self.e2e_keys_handler.upload_keys_for_user( user_id=user_id, device_id=device_id, keys=body ) + return 200, result @@ -387,44 +382,35 @@ class SigningKeyUploadServlet(RestServlet): master_key_updatable_without_uia, ) = await self.e2e_keys_handler.check_cross_signing_setup(user_id) - # Before MSC3967 we required UIA both when setting up cross signing for the - # first time and when resetting the device signing key. With MSC3967 we only - # require UIA when resetting cross-signing, and not when setting up the first - # time. Because there is no UIA in MSC3861, for now we throw an error if the - # user tries to reset the device signing key when MSC3861 is enabled, but allow - # first-time setup. - if self.hs.config.experimental.msc3861.enabled: - # The auth service has to explicitly mark the master key as replaceable - # without UIA to reset the device signing key with MSC3861. - if is_cross_signing_setup and not master_key_updatable_without_uia: - config = self.hs.config.experimental.msc3861 - if config.account_management_url is not None: - url = f"{config.account_management_url}?action=org.matrix.cross_signing_reset" - else: - url = config.issuer - - raise SynapseError( - HTTPStatus.NOT_IMPLEMENTED, - "To reset your end-to-end encryption cross-signing identity, " - f"you first need to approve it at {url} and then try again.", - Codes.UNRECOGNIZED, - ) - # But first-time setup is fine - - elif self.hs.config.experimental.msc3967_enabled: - # MSC3967 allows this endpoint to 200 OK for idempotency. Resending exactly the same - # keys should just 200 OK without doing a UIA prompt. - keys_are_different = await self.e2e_keys_handler.has_different_keys( - user_id, body - ) - if not keys_are_different: - # FIXME: we do not fallthrough to upload_signing_keys_for_user because confusingly - # if we do, we 500 as it looks like it tries to INSERT the same key twice, causing a - # unique key constraint violation. This sounds like a bug? - return 200, {} - # the keys are different, is x-signing set up? If no, then the keys don't exist which is - # why they are different. If yes, then we need to UIA to change them. - if is_cross_signing_setup: + # Resending exactly the same keys should just 200 OK without doing a UIA prompt. + keys_are_different = await self.e2e_keys_handler.has_different_keys( + user_id, body + ) + if not keys_are_different: + return 200, {} + + # The keys are different; is x-signing set up? If no, then this is first-time + # setup, and that is allowed without UIA, per MSC3967. + # If yes, then we need to authenticate the change. + if is_cross_signing_setup: + # With MSC3861, UIA is not possible. Instead, the auth service has to + # explicitly mark the master key as replaceable. + if self.hs.config.experimental.msc3861.enabled: + if not master_key_updatable_without_uia: + config = self.hs.config.experimental.msc3861 + if config.account_management_url is not None: + url = f"{config.account_management_url}?action=org.matrix.cross_signing_reset" + else: + url = config.issuer + + raise SynapseError( + HTTPStatus.NOT_IMPLEMENTED, + "To reset your end-to-end encryption cross-signing identity, " + f"you first need to approve it at {url} and then try again.", + Codes.UNRECOGNIZED, + ) + else: + # Without MSC3861, we require UIA. await self.auth_handler.validate_user_via_ui_auth( requester, request, @@ -433,18 +419,6 @@ class SigningKeyUploadServlet(RestServlet): # Do not allow skipping of UIA auth. can_skip_ui_auth=False, ) - # Otherwise we don't require UIA since we are setting up cross signing for first time - else: - # Previous behaviour is to always require UIA but allow it to be skipped - await self.auth_handler.validate_user_via_ui_auth( - requester, - request, - body, - "add a device signing key to your account", - # Allow skipping of UI auth since this is frequently called directly - # after login and it is silly to ask users to re-auth immediately. - can_skip_ui_auth=True, - ) result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) return 200, result diff --git a/synapse/rest/client/knock.py b/synapse/rest/client/knock.py index ff52a9bf8c..e31687fc13 100644 --- a/synapse/rest/client/knock.py +++ b/synapse/rest/client/knock.py @@ -53,6 +53,7 @@ class KnockRoomAliasServlet(RestServlet): super().__init__() self.room_member_handler = hs.get_room_member_handler() self.auth = hs.get_auth() + self._support_via = hs.config.experimental.msc4156_enabled async def on_POST( self, @@ -74,6 +75,13 @@ class KnockRoomAliasServlet(RestServlet): remote_room_hosts = parse_strings_from_args( args, "server_name", required=False ) + if self._support_via: + remote_room_hosts = parse_strings_from_args( + args, + "org.matrix.msc4156.via", + default=remote_room_hosts, + required=False, + ) elif RoomAlias.is_valid(room_identifier): handler = self.room_member_handler room_alias = RoomAlias.from_string(room_identifier) diff --git a/synapse/rest/client/media.py b/synapse/rest/client/media.py index 172d240783..0c089163c1 100644 --- a/synapse/rest/client/media.py +++ b/synapse/rest/client/media.py @@ -174,6 +174,7 @@ class UnstableThumbnailResource(RestServlet): respond_404(request) return + ip_address = request.getClientAddress().host remote_resp_function = ( self.thumbnailer.select_or_generate_remote_thumbnail if self.dynamic_thumbnails @@ -188,6 +189,7 @@ class UnstableThumbnailResource(RestServlet): method, m_type, max_timeout_ms, + ip_address, ) self.media_repo.mark_recently_accessed(server_name, media_id) diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py deleted file mode 100644 index fc1aed2889..0000000000 --- a/synapse/rest/client/models.py +++ /dev/null @@ -1,99 +0,0 @@ -# -# This file is licensed under the Affero General Public License (AGPL) version 3. -# -# Copyright 2022 The Matrix.org Foundation C.I.C. -# Copyright (C) 2023 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>. -# -# Originally licensed under the Apache License, Version 2.0: -# <http://www.apache.org/licenses/LICENSE-2.0>. -# -# [This file includes modifications made by New Vector Limited] -# -# -from typing import TYPE_CHECKING, Dict, Optional - -from synapse._pydantic_compat import HAS_PYDANTIC_V2 - -if TYPE_CHECKING or HAS_PYDANTIC_V2: - from pydantic.v1 import Extra, StrictInt, StrictStr, constr, validator -else: - from pydantic import Extra, StrictInt, StrictStr, constr, validator - -from synapse.rest.models import RequestBodyModel -from synapse.util.threepids import validate_email - - -class AuthenticationData(RequestBodyModel): - """ - Data used during user-interactive authentication. - - (The name "Authentication Data" is taken directly from the spec.) - - Additional keys will be present, depending on the `type` field. Use - `.dict(exclude_unset=True)` to access them. - """ - - class Config: - extra = Extra.allow - - session: Optional[StrictStr] = None - type: Optional[StrictStr] = None - - -if TYPE_CHECKING: - ClientSecretStr = StrictStr -else: - # See also assert_valid_client_secret() - ClientSecretStr = constr( - regex="[0-9a-zA-Z.=_-]", # noqa: F722 - min_length=1, - max_length=255, - strict=True, - ) - - -class ThreepidRequestTokenBody(RequestBodyModel): - client_secret: ClientSecretStr - id_server: Optional[StrictStr] - id_access_token: Optional[StrictStr] - next_link: Optional[StrictStr] - send_attempt: StrictInt - - @validator("id_access_token", always=True) - def token_required_for_identity_server( - cls, token: Optional[str], values: Dict[str, object] - ) -> Optional[str]: - if values.get("id_server") is not None and token is None: - raise ValueError("id_access_token is required if an id_server is supplied.") - return token - - -class EmailRequestTokenBody(ThreepidRequestTokenBody): - email: StrictStr - - # Canonicalise the email address. The addresses are all stored canonicalised - # in the database. This allows the user to reset his password without having to - # know the exact spelling (eg. upper and lower case) of address in the database. - # Without this, an email stored in the database as "foo@bar.com" would cause - # user requests for "FOO@bar.com" to raise a Not Found error. - _email_validator = validator("email", allow_reuse=True)(validate_email) - - -if TYPE_CHECKING: - ISO3116_1_Alpha_2 = StrictStr -else: - # Per spec: two-letter uppercase ISO-3166-1-alpha-2 - ISO3116_1_Alpha_2 = constr(regex="[A-Z]{2}", strict=True) - - -class MsisdnRequestTokenBody(ThreepidRequestTokenBody): - country: ISO3116_1_Alpha_2 - phone_number: StrictStr diff --git a/synapse/rest/client/reporting.py b/synapse/rest/client/reporting.py index 555f565618..4eee53e5a8 100644 --- a/synapse/rest/client/reporting.py +++ b/synapse/rest/client/reporting.py @@ -32,8 +32,8 @@ from synapse.http.servlet import ( parse_json_object_from_request, ) from synapse.http.site import SynapseRequest -from synapse.rest.models import RequestBodyModel from synapse.types import JsonDict +from synapse.types.rest import RequestBodyModel from ._base import client_patterns @@ -107,7 +107,15 @@ class ReportEventRestServlet(RestServlet): class ReportRoomRestServlet(RestServlet): - # https://github.com/matrix-org/matrix-spec-proposals/pull/4151 + """This endpoint lets clients report a room for abuse. + + Whilst MSC4151 is not yet merged, this unstable endpoint is enabled on matrix.org + for content moderation purposes, and therefore backwards compatibility should be + carefully considered when changing anything on this endpoint. + + More details on the MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4151 + """ + PATTERNS = client_patterns( "/org.matrix.msc4151/rooms/(?P<room_id>[^/]*)/report$", releases=[], diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py index fb4d44211e..c98241f6ce 100644 --- a/synapse/rest/client/room.py +++ b/synapse/rest/client/room.py @@ -292,6 +292,9 @@ class RoomStateEventRestServlet(RestServlet): try: if event_type == EventTypes.Member: membership = content.get("membership", None) + if not isinstance(membership, str): + raise SynapseError(400, "Invalid membership (must be a string)") + event_id, _ = await self.room_member_handler.update_membership( requester, target=UserID.from_string(state_key), @@ -414,6 +417,7 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet): super().__init__(hs) super(ResolveRoomIdMixin, self).__init__(hs) # ensure the Mixin is set up self.auth = hs.get_auth() + self._support_via = hs.config.experimental.msc4156_enabled def register(self, http_server: HttpServer) -> None: # /join/$room_identifier[/$txn_id] @@ -432,6 +436,13 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet): # twisted.web.server.Request.args is incorrectly defined as Optional[Any] args: Dict[bytes, List[bytes]] = request.args # type: ignore remote_room_hosts = parse_strings_from_args(args, "server_name", required=False) + if self._support_via: + remote_room_hosts = parse_strings_from_args( + args, + "org.matrix.msc4156.via", + default=remote_room_hosts, + required=False, + ) room_id, remote_room_hosts = await self.resolve_room_id( room_identifier, remote_room_hosts, diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py index 27ea943e31..1b0ac20d94 100644 --- a/synapse/rest/client/sync.py +++ b/synapse/rest/client/sync.py @@ -33,6 +33,7 @@ from synapse.events.utils import ( format_event_raw, ) from synapse.handlers.presence import format_user_presence_state +from synapse.handlers.sliding_sync import SlidingSyncConfig, SlidingSyncResult from synapse.handlers.sync import ( ArchivedSyncResult, InvitedSyncResult, @@ -43,10 +44,17 @@ from synapse.handlers.sync import ( SyncVersion, ) from synapse.http.server import HttpServer -from synapse.http.servlet import RestServlet, parse_boolean, parse_integer, parse_string +from synapse.http.servlet import ( + RestServlet, + parse_and_validate_json_object_from_request, + parse_boolean, + parse_integer, + parse_string, +) from synapse.http.site import SynapseRequest from synapse.logging.opentracing import trace_with_opname from synapse.types import JsonDict, Requester, StreamToken +from synapse.types.rest.client import SlidingSyncBody from synapse.util import json_decoder from synapse.util.caches.lrucache import LruCache @@ -735,8 +743,228 @@ class SlidingSyncE2eeRestServlet(RestServlet): return 200, response +class SlidingSyncRestServlet(RestServlet): + """ + API endpoint for MSC3575 Sliding Sync `/sync`. Allows for clients to request a + subset (sliding window) of rooms, state, and timeline events (just what they need) + in order to bootstrap quickly and subscribe to only what the client cares about. + Because the client can specify what it cares about, we can respond quickly and skip + all of the work we would normally have to do with a sync v2 response. + + Request query parameters: + timeout: How long to wait for new events in milliseconds. + pos: Stream position token when asking for incremental deltas. + + Request body:: + { + // Sliding Window API + "lists": { + "foo-list": { + "ranges": [ [0, 99] ], + "sort": [ "by_notification_level", "by_recency", "by_name" ], + "required_state": [ + ["m.room.join_rules", ""], + ["m.room.history_visibility", ""], + ["m.space.child", "*"] + ], + "timeline_limit": 10, + "filters": { + "is_dm": true + }, + "bump_event_types": [ "m.room.message", "m.room.encrypted" ], + } + }, + // Room Subscriptions API + "room_subscriptions": { + "!sub1:bar": { + "required_state": [ ["*","*"] ], + "timeline_limit": 10, + "include_old_rooms": { + "timeline_limit": 1, + "required_state": [ ["m.room.tombstone", ""], ["m.room.create", ""] ], + } + } + }, + // Extensions API + "extensions": {} + } + + Response JSON:: + { + "next_pos": "s58_224_0_13_10_1_1_16_0_1", + "lists": { + "foo-list": { + "count": 1337, + "ops": [{ + "op": "SYNC", + "range": [0, 99], + "room_ids": [ + "!foo:bar", + // ... 99 more room IDs + ] + }] + } + }, + // Aggregated rooms from lists and room subscriptions + "rooms": { + // Room from room subscription + "!sub1:bar": { + "name": "Alice and Bob", + "avatar": "mxc://...", + "initial": true, + "required_state": [ + {"sender":"@alice:example.com","type":"m.room.create", "state_key":"", "content":{"creator":"@alice:example.com"}}, + {"sender":"@alice:example.com","type":"m.room.join_rules", "state_key":"", "content":{"join_rule":"invite"}}, + {"sender":"@alice:example.com","type":"m.room.history_visibility", "state_key":"", "content":{"history_visibility":"joined"}}, + {"sender":"@alice:example.com","type":"m.room.member", "state_key":"@alice:example.com", "content":{"membership":"join"}} + ], + "timeline": [ + {"sender":"@alice:example.com","type":"m.room.create", "state_key":"", "content":{"creator":"@alice:example.com"}}, + {"sender":"@alice:example.com","type":"m.room.join_rules", "state_key":"", "content":{"join_rule":"invite"}}, + {"sender":"@alice:example.com","type":"m.room.history_visibility", "state_key":"", "content":{"history_visibility":"joined"}}, + {"sender":"@alice:example.com","type":"m.room.member", "state_key":"@alice:example.com", "content":{"membership":"join"}}, + {"sender":"@alice:example.com","type":"m.room.message", "content":{"body":"A"}}, + {"sender":"@alice:example.com","type":"m.room.message", "content":{"body":"B"}}, + ], + "prev_batch": "t111_222_333", + "joined_count": 41, + "invited_count": 1, + "notification_count": 1, + "highlight_count": 0 + }, + // rooms from list + "!foo:bar": { + "name": "The calculated room name", + "avatar": "mxc://...", + "initial": true, + "required_state": [ + {"sender":"@alice:example.com","type":"m.room.join_rules", "state_key":"", "content":{"join_rule":"invite"}}, + {"sender":"@alice:example.com","type":"m.room.history_visibility", "state_key":"", "content":{"history_visibility":"joined"}}, + {"sender":"@alice:example.com","type":"m.space.child", "state_key":"!foo:example.com", "content":{"via":["example.com"]}}, + {"sender":"@alice:example.com","type":"m.space.child", "state_key":"!bar:example.com", "content":{"via":["example.com"]}}, + {"sender":"@alice:example.com","type":"m.space.child", "state_key":"!baz:example.com", "content":{"via":["example.com"]}} + ], + "timeline": [ + {"sender":"@alice:example.com","type":"m.room.join_rules", "state_key":"", "content":{"join_rule":"invite"}}, + {"sender":"@alice:example.com","type":"m.room.message", "content":{"body":"A"}}, + {"sender":"@alice:example.com","type":"m.room.message", "content":{"body":"B"}}, + {"sender":"@alice:example.com","type":"m.room.message", "content":{"body":"C"}}, + {"sender":"@alice:example.com","type":"m.room.message", "content":{"body":"D"}}, + ], + "prev_batch": "t111_222_333", + "joined_count": 4, + "invited_count": 0, + "notification_count": 54, + "highlight_count": 3 + }, + // ... 99 more items + }, + "extensions": {} + } + """ + + PATTERNS = client_patterns( + "/org.matrix.msc3575/sync$", releases=[], v1=False, unstable=True + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.auth = hs.get_auth() + self.store = hs.get_datastores().main + self.filtering = hs.get_filtering() + self.sliding_sync_handler = hs.get_sliding_sync_handler() + + # TODO: Update this to `on_GET` once we figure out how we want to handle params + async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request, allow_guest=True) + user = requester.user + device_id = requester.device_id + + timeout = parse_integer(request, "timeout", default=0) + # Position in the stream + from_token_string = parse_string(request, "pos") + + from_token = None + if from_token_string is not None: + from_token = await StreamToken.from_string(self.store, from_token_string) + + # TODO: We currently don't know whether we're going to use sticky params or + # maybe some filters like sync v2 where they are built up once and referenced + # by filter ID. For now, we will just prototype with always passing everything + # in. + body = parse_and_validate_json_object_from_request(request, SlidingSyncBody) + logger.info("Sliding sync request: %r", body) + + sync_config = SlidingSyncConfig( + user=user, + device_id=device_id, + # FIXME: Currently, we're just manually copying the fields from the + # `SlidingSyncBody` into the config. How can we gurantee into the future + # that we don't forget any? I would like something more structured like + # `copy_attributes(from=body, to=config)` + lists=body.lists, + room_subscriptions=body.room_subscriptions, + extensions=body.extensions, + ) + + sliding_sync_results = await self.sliding_sync_handler.wait_for_sync_for_user( + requester, + sync_config, + from_token, + timeout, + ) + + # The client may have disconnected by now; don't bother to serialize the + # response if so. + if request._disconnected: + logger.info("Client has disconnected; not serializing response.") + return 200, {} + + response_content = await self.encode_response(sliding_sync_results) + + return 200, response_content + + # TODO: Is there a better way to encode things? + async def encode_response( + self, + sliding_sync_result: SlidingSyncResult, + ) -> JsonDict: + response: JsonDict = defaultdict(dict) + + response["next_pos"] = await sliding_sync_result.next_pos.to_string(self.store) + serialized_lists = self.encode_lists(sliding_sync_result.lists) + if serialized_lists: + response["lists"] = serialized_lists + response["rooms"] = {} # TODO: sliding_sync_result.rooms + response["extensions"] = {} # TODO: sliding_sync_result.extensions + + return response + + def encode_lists( + self, lists: Dict[str, SlidingSyncResult.SlidingWindowList] + ) -> JsonDict: + def encode_operation( + operation: SlidingSyncResult.SlidingWindowList.Operation, + ) -> JsonDict: + return { + "op": operation.op.value, + "range": operation.range, + "room_ids": operation.room_ids, + } + + serialized_lists = {} + for list_key, list_result in lists.items(): + serialized_lists[list_key] = { + "count": list_result.count, + "ops": [encode_operation(op) for op in list_result.ops], + } + + return serialized_lists + + def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: SyncRestServlet(hs).register(http_server) if hs.config.experimental.msc3575_enabled: + SlidingSyncRestServlet(hs).register(http_server) SlidingSyncE2eeRestServlet(hs).register(http_server) |