From dd55f5b73d62ca42a3dbc5398183888ee05f3a40 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 12 Mar 2021 16:57:59 +0000 Subject: [PATCH 01/11] Very basic impl of a space summary API --- synapse/api/constants.py | 6 ++ synapse/config/experimental.py | 6 ++ synapse/handlers/space_summary.py | 161 ++++++++++++++++++++++++++++++ synapse/rest/client/v1/room.py | 36 ++++++- synapse/server.py | 5 + 5 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 synapse/handlers/space_summary.py diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 691f8f9adf07..ed050c810459 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -100,6 +100,9 @@ class EventTypes: Dummy = "org.matrix.dummy_event" + MSC1772_SPACE_CHILD = "org.matrix.msc1772.space.child" + MSC1772_SPACE_PARENT = "org.matrix.msc1772.space.parent" + class EduTypes: Presence = "m.presence" @@ -160,6 +163,9 @@ class EventContentFields: # cf https://github.com/matrix-org/matrix-doc/pull/2228 SELF_DESTRUCT_AFTER = "org.matrix.self_destruct_after" + # cf https://github.com/matrix-org/matrix-doc/pull/1772 + MSC1772_ROOM_TYPE = "org.matrix.msc1772.type" + class RoomEncryptionAlgorithms: MEGOLM_V1_AES_SHA2 = "m.megolm.v1.aes-sha2" diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index b1c1c51e4dcc..db2155fc091a 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -27,3 +27,9 @@ def read_config(self, config: JsonDict, **kwargs): # MSC2858 (multiple SSO identity providers) self.msc2858_enabled = experimental.get("msc2858_enabled", False) # type: bool + + # MSC2946 (space summaries) + self.msc2946_enabled = experimental.get("msc2946_enabled", False) # type: bool + + # MSC1772 (spaces) + self.msc1772_enabled = experimental.get("msc1772_enabled", False) # type: bool diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py new file mode 100644 index 000000000000..a9e13145b362 --- /dev/null +++ b/synapse/handlers/space_summary.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from collections import deque +from typing import TYPE_CHECKING, List, Set, Tuple + +from synapse.api.constants import EventContentFields, EventTypes, HistoryVisibility +from synapse.api.errors import AuthError +from synapse.events.utils import format_event_for_client_v2 +from synapse.types import JsonDict + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + +# number of rooms to return. We'll stop once we hit this limit. +# TODO: allow clients to reduce this with a request param. +# TODO: increase it, probably. It's deliberately low to start with so that +# we can think about whether we need pagination. +ROOMS_LIMIT = 5 + +# number of events to return per room. +# TODO: allow clients to reduce this with a request param. +EVENTS_PER_ROOM_LIMIT = 5 + + +class SpaceSummaryHandler: + def __init__(self, hs: "HomeServer"): + self._clock = hs.get_clock() + self._auth = hs.get_auth() + self._room_list_handler = hs.get_room_list_handler() + self._state_handler = hs.get_state_handler() + self._store = hs.get_datastore() + self._msc1772 = hs.config.experimental.msc1772_enabled + self._event_serializer = hs.get_event_client_serializer() + + async def get_space_summary(self, requester: str, room_id: str) -> JsonDict: + """ + Implementation of the space summary API + + Args: + requester: user id of the user making this request + room_id: room id to start the summary at + + Returns: + summary dict to return + """ + + # the queue of rooms to process + room_queue = deque((room_id,)) + + processed_rooms = set() # type: Set[str] + returned_edges = set() # type: Set[str] + + rooms_result = [] # type: List[JsonDict] + events_result = [] # type: List[JsonDict] + + now = self._clock.time_msec() + + while room_queue and len(rooms_result) < ROOMS_LIMIT: + room_id = room_queue.popleft() + processed_rooms.add(room_id) + try: + await self._auth.check_user_in_room_or_world_readable( + room_id, requester + ) + except AuthError: + logger.debug( + "user %s cannot view room %s, omitting from summary", + requester, + room_id, + ) + continue + + stats = await self._store.get_room_with_stats(room_id) + assert stats is not None, "unable to retrieve stats for %s" % (room_id,) + current_state_ids = await self._store.get_current_state_ids(room_id) + create_event = await self._store.get_event( + current_state_ids[(EventTypes.Create, "")] + ) + + room_type = None + if self._msc1772: + room_type = create_event.content.get( + EventContentFields.MSC1772_ROOM_TYPE + ) + + # TODO: include num_refs? + entry = { + "room_id": stats["room_id"], + "name": stats["name"], + "topic": stats["topic"], + "canonical_alias": stats["canonical_alias"], + "num_joined_members": stats["joined_members"], + "avatar_url": stats["avatar"], + "world_readable": ( + stats["history_visibility"] == HistoryVisibility.WORLD_READABLE + ), + "guest_can_join": stats["guest_access"] == "can_join", + "room_type": room_type, + } + + # Filter out Nones – rather omit the field altogether + room_entry = {k: v for k, v in entry.items() if v is not None} + rooms_result.append(room_entry) + + # TODO: get reverse links? + + edge_event_types = () # type: Tuple[str, ...] + if self._msc1772: + edge_event_types += ( + EventTypes.MSC1772_SPACE_CHILD, + EventTypes.MSC1772_SPACE_PARENT, + ) + + events = await self._store.get_events_as_list( + [ + event_id + for key, event_id in current_state_ids.items() + if key[0] in edge_event_types and event_id not in returned_edges + ] + ) + + events_for_this_room = 0 + for edge_event in events: + if not edge_event.content.get("via"): + # possibly redacted; ignore + continue + + if events_for_this_room < EVENTS_PER_ROOM_LIMIT: + events_result.append( + await self._event_serializer.serialize_event( + edge_event, + time_now=now, + event_format=format_event_for_client_v2, + ) + ) + returned_edges.add(edge_event.event_id) + events_for_this_room += 1 + + # if we haven't yet visited the target of this link, add it to the + # queue. + edge_room_id = edge_event.state_key + if edge_room_id not in processed_rooms: + room_queue.append(edge_room_id) + + return {"rooms": rooms_result, "events": events_result} diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 9a1df30c2999..e96cd6277454 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -21,6 +21,9 @@ from typing import TYPE_CHECKING, List, Optional from urllib import parse as urlparse +from twisted.web.iweb import IRequest +from twisted.web.server import Request + from synapse.api.constants import EventTypes, Membership from synapse.api.errors import ( AuthError, @@ -981,7 +984,35 @@ def register_txn_path(servlet, regex_string, http_server, with_get=False): ) -def register_servlets(hs, http_server, is_worker=False): +class RoomSpaceSummaryRestServlet(RestServlet): + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/org.matrix.msc2946" + "/rooms/(?P[^/]*)/spaces$" + ), + ) + + def __init__(self, hs: "synapse.server.HomeServer"): + super().__init__() + self._auth = hs.get_auth() + self._space_summary_handler = hs.get_space_summary_handler() + + async def on_POST(self, request: Request, room_id: str): + requester = await self._auth.get_user_by_req(request, allow_guest=True) + + # first of all, check that the user is in the room in question (or it's + # world-readable) + await self._auth.check_user_in_room_or_world_readable( + room_id, requester.user.to_string() + ) + + data = await self._space_summary_handler.get_space_summary( + requester.user.to_string(), room_id + ) + return 200, data + + +def register_servlets(hs: "synapse.server.HomeServer", http_server, is_worker=False): RoomStateEventRestServlet(hs).register(http_server) RoomMemberListRestServlet(hs).register(http_server) JoinedRoomMemberListRestServlet(hs).register(http_server) @@ -995,6 +1026,9 @@ def register_servlets(hs, http_server, is_worker=False): RoomTypingRestServlet(hs).register(http_server) RoomEventContextServlet(hs).register(http_server) + if hs.config.experimental.msc2946_enabled: + RoomSpaceSummaryRestServlet(hs).register(http_server) + # Some servlets only get registered for the main process. if not is_worker: RoomCreateRestServlet(hs).register(http_server) diff --git a/synapse/server.py b/synapse/server.py index 369cc88026b8..16eaac4c1ad7 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -100,6 +100,7 @@ from synapse.handlers.room_member_worker import RoomMemberWorkerHandler from synapse.handlers.search import SearchHandler from synapse.handlers.set_password import SetPasswordHandler +from synapse.handlers.space_summary import SpaceSummaryHandler from synapse.handlers.sso import SsoHandler from synapse.handlers.stats import StatsHandler from synapse.handlers.sync import SyncHandler @@ -725,6 +726,10 @@ def get_module_api(self) -> ModuleApi: def get_account_data_handler(self) -> AccountDataHandler: return AccountDataHandler(self) + @cache_in_self + def get_space_summary_handler(self) -> SpaceSummaryHandler: + return SpaceSummaryHandler(self) + @cache_in_self def get_external_cache(self) -> ExternalCache: return ExternalCache(self) From f8f32c1a470026d4be2471fe2092f8873b438ed9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 12 Mar 2021 18:54:43 +0000 Subject: [PATCH 02/11] only go *down* the hierarchy for now --- synapse/handlers/space_summary.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index a9e13145b362..eb53aa51e95a 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -64,7 +64,6 @@ async def get_space_summary(self, requester: str, room_id: str) -> JsonDict: room_queue = deque((room_id,)) processed_rooms = set() # type: Set[str] - returned_edges = set() # type: Set[str] rooms_result = [] # type: List[JsonDict] events_result = [] # type: List[JsonDict] @@ -99,7 +98,6 @@ async def get_space_summary(self, requester: str, room_id: str) -> JsonDict: EventContentFields.MSC1772_ROOM_TYPE ) - # TODO: include num_refs? entry = { "room_id": stats["room_id"], "name": stats["name"], @@ -118,20 +116,21 @@ async def get_space_summary(self, requester: str, room_id: str) -> JsonDict: room_entry = {k: v for k, v in entry.items() if v is not None} rooms_result.append(room_entry) - # TODO: get reverse links? + if room_type != "org.matrix.msc1772.space": + continue + + # look for child rooms/spaces. + # TODO: add a param so that the client can request parent spaces instead edge_event_types = () # type: Tuple[str, ...] if self._msc1772: - edge_event_types += ( - EventTypes.MSC1772_SPACE_CHILD, - EventTypes.MSC1772_SPACE_PARENT, - ) + edge_event_types += (EventTypes.MSC1772_SPACE_CHILD,) events = await self._store.get_events_as_list( [ event_id for key, event_id in current_state_ids.items() - if key[0] in edge_event_types and event_id not in returned_edges + if key[0] in edge_event_types ] ) @@ -149,7 +148,6 @@ async def get_space_summary(self, requester: str, room_id: str) -> JsonDict: event_format=format_event_for_client_v2, ) ) - returned_edges.add(edge_event.event_id) events_for_this_room += 1 # if we haven't yet visited the target of this link, add it to the From 5ed35cbdba4e794f2d34c8fa433fbaa3ce49af7c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 15 Mar 2021 19:15:07 +0000 Subject: [PATCH 03/11] More space summary work - `suggested_only` - `max_rooms_per_space` - GET interface - bit of a refactor --- synapse/handlers/space_summary.py | 185 +++++++++++++++++++----------- synapse/rest/client/v1/room.py | 45 ++++++-- 2 files changed, 151 insertions(+), 79 deletions(-) diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index eb53aa51e95a..40d4b0d67b8a 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -13,12 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import itertools import logging from collections import deque -from typing import TYPE_CHECKING, List, Set, Tuple +from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple from synapse.api.constants import EventContentFields, EventTypes, HistoryVisibility from synapse.api.errors import AuthError +from synapse.events import EventBase from synapse.events.utils import format_event_for_client_v2 from synapse.types import JsonDict @@ -33,9 +35,8 @@ # we can think about whether we need pagination. ROOMS_LIMIT = 5 -# number of events to return per room. -# TODO: allow clients to reduce this with a request param. -EVENTS_PER_ROOM_LIMIT = 5 +# max number of events to return per room. +ROOMS_PER_SPACE_LIMIT = 5 class SpaceSummaryHandler: @@ -48,17 +49,34 @@ def __init__(self, hs: "HomeServer"): self._msc1772 = hs.config.experimental.msc1772_enabled self._event_serializer = hs.get_event_client_serializer() - async def get_space_summary(self, requester: str, room_id: str) -> JsonDict: + async def get_space_summary( + self, + requester: str, + room_id: str, + suggested_only: False, + max_rooms_per_space: Optional[int], + ) -> JsonDict: """ Implementation of the space summary API Args: requester: user id of the user making this request - room_id: room id to start the summary at + + room_id: room id to start the summary at + + suggested_only: whether we should only return children with the "suggested" + flag set. + + max_rooms_per_space: an optional limit on the number of child rooms we will + return. This does not apply to the root room (ie, room_id), and + is overridden by ROOMS_PER_SPACE_LIMIT. Returns: summary dict to return """ + # first of all, check that the user is in the room in question (or it's + # world-readable) + await self._auth.check_user_in_room_or_world_readable(room_id, requester) # the queue of rooms to process room_queue = deque((room_id,)) @@ -72,88 +90,115 @@ async def get_space_summary(self, requester: str, room_id: str) -> JsonDict: while room_queue and len(rooms_result) < ROOMS_LIMIT: room_id = room_queue.popleft() + logger.debug("Processing room %s", room_id) processed_rooms.add(room_id) + try: await self._auth.check_user_in_room_or_world_readable( room_id, requester ) except AuthError: - logger.debug( + logger.info( "user %s cannot view room %s, omitting from summary", requester, room_id, ) continue - stats = await self._store.get_room_with_stats(room_id) - assert stats is not None, "unable to retrieve stats for %s" % (room_id,) - current_state_ids = await self._store.get_current_state_ids(room_id) - create_event = await self._store.get_event( - current_state_ids[(EventTypes.Create, "")] - ) - - room_type = None - if self._msc1772: - room_type = create_event.content.get( - EventContentFields.MSC1772_ROOM_TYPE - ) - - entry = { - "room_id": stats["room_id"], - "name": stats["name"], - "topic": stats["topic"], - "canonical_alias": stats["canonical_alias"], - "num_joined_members": stats["joined_members"], - "avatar_url": stats["avatar"], - "world_readable": ( - stats["history_visibility"] == HistoryVisibility.WORLD_READABLE - ), - "guest_can_join": stats["guest_access"] == "can_join", - "room_type": room_type, - } - - # Filter out Nones – rather omit the field altogether - room_entry = {k: v for k, v in entry.items() if v is not None} + room_entry = await self._build_room_entry(room_id) rooms_result.append(room_entry) - if room_type != "org.matrix.msc1772.space": - continue - # look for child rooms/spaces. - # TODO: add a param so that the client can request parent spaces instead - - edge_event_types = () # type: Tuple[str, ...] - if self._msc1772: - edge_event_types += (EventTypes.MSC1772_SPACE_CHILD,) - - events = await self._store.get_events_as_list( - [ - event_id - for key, event_id in current_state_ids.items() - if key[0] in edge_event_types - ] - ) - - events_for_this_room = 0 - for edge_event in events: - if not edge_event.content.get("via"): - # possibly redacted; ignore - continue - - if events_for_this_room < EVENTS_PER_ROOM_LIMIT: - events_result.append( - await self._event_serializer.serialize_event( - edge_event, - time_now=now, - event_format=format_event_for_client_v2, - ) - ) - events_for_this_room += 1 + child_events = await self._get_child_events(room_id) + + if suggested_only: + # we only care about suggested children + child_events = filter(_is_suggested_child_event, child_events) - # if we haven't yet visited the target of this link, add it to the - # queue. + # if this is not the first room, and the client has specified a limit, + # apply it + if max_rooms_per_space is not None and len(processed_rooms) > 1: + max_rooms = min(ROOMS_PER_SPACE_LIMIT, max_rooms_per_space) + else: + max_rooms = ROOMS_PER_SPACE_LIMIT + + for edge_event in itertools.islice(child_events, max_rooms): edge_room_id = edge_event.state_key + + events_result.append( + await self._event_serializer.serialize_event( + edge_event, + time_now=now, + event_format=format_event_for_client_v2, + ) + ) + + # if we haven't yet visited the target of this link, add it to the queue if edge_room_id not in processed_rooms: room_queue.append(edge_room_id) return {"rooms": rooms_result, "events": events_result} + + async def _build_room_entry(self, room_id: str) -> JsonDict: + """Generate en entry suitable for the 'rooms' list in the summary response""" + stats = await self._store.get_room_with_stats(room_id) + + # currently this should be impossible because we call + # check_user_in_room_or_world_readable on the room before we get here, so + # there should always be an entry + assert stats is not None, "unable to retrieve stats for %s" % (room_id,) + + current_state_ids = await self._store.get_current_state_ids(room_id) + create_event = await self._store.get_event( + current_state_ids[(EventTypes.Create, "")] + ) + + room_type = None + if self._msc1772: + room_type = create_event.content.get(EventContentFields.MSC1772_ROOM_TYPE) + + entry = { + "room_id": stats["room_id"], + "name": stats["name"], + "topic": stats["topic"], + "canonical_alias": stats["canonical_alias"], + "num_joined_members": stats["joined_members"], + "avatar_url": stats["avatar"], + "world_readable": ( + stats["history_visibility"] == HistoryVisibility.WORLD_READABLE + ), + "guest_can_join": stats["guest_access"] == "can_join", + "room_type": room_type, + } + + # Filter out Nones – rather omit the field altogether + room_entry = {k: v for k, v in entry.items() if v is not None} + + return room_entry + + async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: + # look for child rooms/spaces. + current_state_ids = await self._store.get_current_state_ids(room_id) + + edge_event_types = () # type: Tuple[str, ...] + if self._msc1772: + edge_event_types += (EventTypes.MSC1772_SPACE_CHILD,) + + events = await self._store.get_events_as_list( + [ + event_id + for key, event_id in current_state_ids.items() + if key[0] in edge_event_types + ] + ) + + # filter out any events without a "via" (which implies it has been redacted) + return (e for e in events if e.content.get("via")) + + +def _is_suggested_child_event(edge_event: EventBase) -> bool: + suggested = edge_event.content.get("suggested") + if isinstance(suggested, bool) and suggested: + return True + logger.debug("Ignorning not-suggested child %s", edge_event.state_key) + return False diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index e96cd6277454..2cc7bcb9d223 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -38,6 +38,7 @@ from synapse.http.servlet import ( RestServlet, assert_params_in_dict, + parse_boolean, parse_integer, parse_json_object_from_request, parse_string, @@ -47,7 +48,14 @@ from synapse.rest.client.v2_alpha._base import client_patterns from synapse.storage.state import StateFilter from synapse.streams.config import PaginationConfig -from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID +from synapse.types import ( + Requester, + RoomAlias, + RoomID, + StreamToken, + ThirdPartyInstanceID, + UserID, +) from synapse.util import json_decoder from synapse.util.stringutils import parse_and_validate_server_name, random_string @@ -997,19 +1005,38 @@ def __init__(self, hs: "synapse.server.HomeServer"): self._auth = hs.get_auth() self._space_summary_handler = hs.get_space_summary_handler() - async def on_POST(self, request: Request, room_id: str): + async def on_GET(self, request: Request, room_id: str): requester = await self._auth.get_user_by_req(request, allow_guest=True) - # first of all, check that the user is in the room in question (or it's - # world-readable) - await self._auth.check_user_in_room_or_world_readable( - room_id, requester.user.to_string() + return 200, await self._space_summary_handler.get_space_summary( + requester.user.to_string(), + room_id, + suggested_only=parse_boolean(request, "suggested_only", default=False), + max_rooms_per_space=parse_integer(request, "max_rooms_per_space"), ) - data = await self._space_summary_handler.get_space_summary( - requester.user.to_string(), room_id + async def on_POST(self, request: Request, room_id: str): + requester = await self._auth.get_user_by_req(request, allow_guest=True) + content = parse_json_object_from_request(request) + + suggested_only = content.get("suggested_only", False) + if not isinstance(suggested_only, bool): + raise SynapseError( + 400, "'suggested_only' must be a boolean", Codes.BAD_JSON + ) + + max_rooms_per_space = content.get("max_rooms_per_space") + if max_rooms_per_space is not None and not isinstance(max_rooms_per_space, int): + raise SynapseError( + 400, "'max_rooms_per_space' must be an integer", Codes.BAD_JSON + ) + + return 200, await self._space_summary_handler.get_space_summary( + requester.user.to_string(), + room_id, + suggested_only=suggested_only, + max_rooms_per_space=max_rooms_per_space, ) - return 200, data def register_servlets(hs: "synapse.server.HomeServer", http_server, is_worker=False): From 7c8b2b5aaccae224575c5c14a6426bd0b2b8731e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 15 Mar 2021 19:18:29 +0000 Subject: [PATCH 04/11] bump limits up --- synapse/handlers/space_summary.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 40d4b0d67b8a..ba49ae00e056 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -31,12 +31,10 @@ # number of rooms to return. We'll stop once we hit this limit. # TODO: allow clients to reduce this with a request param. -# TODO: increase it, probably. It's deliberately low to start with so that -# we can think about whether we need pagination. -ROOMS_LIMIT = 5 +ROOMS_LIMIT = 50 # max number of events to return per room. -ROOMS_PER_SPACE_LIMIT = 5 +ROOMS_PER_SPACE_LIMIT = 50 class SpaceSummaryHandler: @@ -116,7 +114,7 @@ async def get_space_summary( child_events = filter(_is_suggested_child_event, child_events) # if this is not the first room, and the client has specified a limit, - # apply it + # apply it (the client limit does not apply to the root room) if max_rooms_per_space is not None and len(processed_rooms) > 1: max_rooms = min(ROOMS_PER_SPACE_LIMIT, max_rooms_per_space) else: From e66c0fd55a4d814e4edcf16b78e8cb825a91898d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Mar 2021 11:37:13 +0000 Subject: [PATCH 05/11] changelog --- changelog.d/9643.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/9643.feature diff --git a/changelog.d/9643.feature b/changelog.d/9643.feature new file mode 100644 index 000000000000..2f7ccedcfbb8 --- /dev/null +++ b/changelog.d/9643.feature @@ -0,0 +1 @@ +Add initial experimental support for a "space summary" API. From 7a0f7699ff51007363f59094e3c5a78be1c2f606 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Mar 2021 12:08:44 +0000 Subject: [PATCH 06/11] fix lint --- synapse/handlers/space_summary.py | 4 ++-- synapse/rest/client/v1/room.py | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index ba49ae00e056..3509cb793aeb 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -51,8 +51,8 @@ async def get_space_summary( self, requester: str, room_id: str, - suggested_only: False, - max_rooms_per_space: Optional[int], + suggested_only: bool = False, + max_rooms_per_space: Optional[int] = None, ) -> JsonDict: """ Implementation of the space summary API diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 2cc7bcb9d223..49d12d0bb78c 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -21,7 +21,6 @@ from typing import TYPE_CHECKING, List, Optional from urllib import parse as urlparse -from twisted.web.iweb import IRequest from twisted.web.server import Request from synapse.api.constants import EventTypes, Membership @@ -48,14 +47,7 @@ from synapse.rest.client.v2_alpha._base import client_patterns from synapse.storage.state import StateFilter from synapse.streams.config import PaginationConfig -from synapse.types import ( - Requester, - RoomAlias, - RoomID, - StreamToken, - ThirdPartyInstanceID, - UserID, -) +from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID from synapse.util import json_decoder from synapse.util.stringutils import parse_and_validate_server_name, random_string From 82906c5b640ba640fdafd21f0e701d91c74d0545 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 18 Mar 2021 14:29:26 +0000 Subject: [PATCH 07/11] Update synapse/rest/client/v1/room.py Co-authored-by: Patrick Cloke --- synapse/rest/client/v1/room.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 49d12d0bb78c..a730cf2511a6 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -997,7 +997,7 @@ def __init__(self, hs: "synapse.server.HomeServer"): self._auth = hs.get_auth() self._space_summary_handler = hs.get_space_summary_handler() - async def on_GET(self, request: Request, room_id: str): + async def on_GET(self, request: Request, room_id: str) -> Tuple[int, JsonDict]: requester = await self._auth.get_user_by_req(request, allow_guest=True) return 200, await self._space_summary_handler.get_space_summary( From f3a49a921518cd7aa13208b055be3c178591d9b2 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Mar 2021 14:35:36 +0000 Subject: [PATCH 08/11] Address review comments --- synapse/handlers/space_summary.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 3509cb793aeb..de03e6953a39 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -31,10 +31,10 @@ # number of rooms to return. We'll stop once we hit this limit. # TODO: allow clients to reduce this with a request param. -ROOMS_LIMIT = 50 +MAX_ROOMS = 50 # max number of events to return per room. -ROOMS_PER_SPACE_LIMIT = 50 +MAX_ROOMS_PER_SPACE = 50 class SpaceSummaryHandler: @@ -86,7 +86,7 @@ async def get_space_summary( now = self._clock.time_msec() - while room_queue and len(rooms_result) < ROOMS_LIMIT: + while room_queue and len(rooms_result) < MAX_ROOMS: room_id = room_queue.popleft() logger.debug("Processing room %s", room_id) processed_rooms.add(room_id) @@ -113,12 +113,14 @@ async def get_space_summary( # we only care about suggested children child_events = filter(_is_suggested_child_event, child_events) - # if this is not the first room, and the client has specified a limit, - # apply it (the client limit does not apply to the root room) + # The client-specified max_rooms_per_space limit doesn't apply to the + # room_id specified in the request, so we ignore it if this is the + # first room we are processing. Otherwise, apply any client-specified + # limit, capping to our built-in limit. if max_rooms_per_space is not None and len(processed_rooms) > 1: - max_rooms = min(ROOMS_PER_SPACE_LIMIT, max_rooms_per_space) + max_rooms = min(MAX_ROOMS_PER_SPACE, max_rooms_per_space) else: - max_rooms = ROOMS_PER_SPACE_LIMIT + max_rooms = MAX_ROOMS_PER_SPACE for edge_event in itertools.islice(child_events, max_rooms): edge_room_id = edge_event.state_key From 64dc92b5a22e943bc901e4776283c111aac61625 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Mar 2021 14:38:27 +0000 Subject: [PATCH 09/11] fix annotations/imports --- synapse/rest/client/v1/room.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index a730cf2511a6..ddfa5fa9a904 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -18,7 +18,7 @@ import logging import re -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, List, Optional, Tuple from urllib import parse as urlparse from twisted.web.server import Request @@ -47,7 +47,14 @@ from synapse.rest.client.v2_alpha._base import client_patterns from synapse.storage.state import StateFilter from synapse.streams.config import PaginationConfig -from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID +from synapse.types import ( + JsonDict, + RoomAlias, + RoomID, + StreamToken, + ThirdPartyInstanceID, + UserID, +) from synapse.util import json_decoder from synapse.util.stringutils import parse_and_validate_server_name, random_string @@ -1007,7 +1014,7 @@ async def on_GET(self, request: Request, room_id: str) -> Tuple[int, JsonDict]: max_rooms_per_space=parse_integer(request, "max_rooms_per_space"), ) - async def on_POST(self, request: Request, room_id: str): + async def on_POST(self, request: Request, room_id: str) -> Tuple[int, JsonDict]: requester = await self._auth.get_user_by_req(request, allow_guest=True) content = parse_json_object_from_request(request) From 33dd46101f6b09fe39a4be3749a0151f51d1a354 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Mar 2021 14:43:40 +0000 Subject: [PATCH 10/11] coalesce to a single `experimental` option --- synapse/config/experimental.py | 7 ++----- synapse/handlers/space_summary.py | 13 ++++--------- synapse/rest/client/v1/room.py | 2 +- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index db2155fc091a..5554bcea8ac8 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -28,8 +28,5 @@ def read_config(self, config: JsonDict, **kwargs): # MSC2858 (multiple SSO identity providers) self.msc2858_enabled = experimental.get("msc2858_enabled", False) # type: bool - # MSC2946 (space summaries) - self.msc2946_enabled = experimental.get("msc2946_enabled", False) # type: bool - - # MSC1772 (spaces) - self.msc1772_enabled = experimental.get("msc1772_enabled", False) # type: bool + # Spaces (MSC1772, MSC2946, etc) + self.spaces_enabled = experimental.get("spaces_enabled", False) # type: bool diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index de03e6953a39..c2e4e1c12219 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -44,7 +44,6 @@ def __init__(self, hs: "HomeServer"): self._room_list_handler = hs.get_room_list_handler() self._state_handler = hs.get_state_handler() self._store = hs.get_datastore() - self._msc1772 = hs.config.experimental.msc1772_enabled self._event_serializer = hs.get_event_client_serializer() async def get_space_summary( @@ -153,9 +152,8 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: current_state_ids[(EventTypes.Create, "")] ) - room_type = None - if self._msc1772: - room_type = create_event.content.get(EventContentFields.MSC1772_ROOM_TYPE) + # TODO: update once MSC1772 lands + room_type = create_event.content.get(EventContentFields.MSC1772_ROOM_TYPE) entry = { "room_id": stats["room_id"], @@ -180,15 +178,12 @@ async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: # look for child rooms/spaces. current_state_ids = await self._store.get_current_state_ids(room_id) - edge_event_types = () # type: Tuple[str, ...] - if self._msc1772: - edge_event_types += (EventTypes.MSC1772_SPACE_CHILD,) - events = await self._store.get_events_as_list( [ event_id for key, event_id in current_state_ids.items() - if key[0] in edge_event_types + # TODO: update once MSC1772 lands + if key[0] == EventTypes.MSC1772_SPACE_CHILD ] ) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index ddfa5fa9a904..4b2f632e5c05 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -1052,7 +1052,7 @@ def register_servlets(hs: "synapse.server.HomeServer", http_server, is_worker=Fa RoomTypingRestServlet(hs).register(http_server) RoomEventContextServlet(hs).register(http_server) - if hs.config.experimental.msc2946_enabled: + if hs.config.experimental.spaces_enabled: RoomSpaceSummaryRestServlet(hs).register(http_server) # Some servlets only get registered for the main process. From 5c6e78f6ea24bdb94d4e8d213d8c3ae86d77b5a8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 18 Mar 2021 17:06:21 +0000 Subject: [PATCH 11/11] fix lint --- synapse/handlers/space_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index c2e4e1c12219..513dc0c71a02 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -16,7 +16,7 @@ import itertools import logging from collections import deque -from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Iterable, List, Optional, Set from synapse.api.constants import EventContentFields, EventTypes, HistoryVisibility from synapse.api.errors import AuthError