diff --git a/changelog.d/18070.feature b/changelog.d/18070.feature new file mode 100644 index 0000000000..ecf468f0b6 --- /dev/null +++ b/changelog.d/18070.feature @@ -0,0 +1 @@ +Support for [MSC4235](https://github.com/matrix-org/matrix-spec-proposals/pull/4235): via query param for hierarchy endpoint. Contributed by Krishan (@kfiven). diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 85eca9c05b..1ae47d3c16 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -561,6 +561,9 @@ class ExperimentalConfig(Config): # MSC4076: Add `disable_badge_count`` to pusher configuration self.msc4076_enabled: bool = experimental.get("msc4076_enabled", False) + # MSC4235: Add `via` param to hierarchy endpoint + self.msc4235_enabled: bool = experimental.get("msc4235_enabled", False) + # MSC4263: Preventing MXID enumeration via key queries self.msc4263_limit_key_queries_to_users_who_share_rooms = experimental.get( "msc4263_limit_key_queries_to_users_who_share_rooms", diff --git a/synapse/handlers/room_summary.py b/synapse/handlers/room_summary.py index 91b131d09b..1f322ac263 100644 --- a/synapse/handlers/room_summary.py +++ b/synapse/handlers/room_summary.py @@ -111,7 +111,15 @@ class RoomSummaryHandler: # If a user tries to fetch the same page multiple times in quick succession, # only process the first attempt and return its result to subsequent requests. self._pagination_response_cache: ResponseCache[ - Tuple[str, str, bool, Optional[int], Optional[int], Optional[str]] + Tuple[ + str, + str, + bool, + Optional[int], + Optional[int], + Optional[str], + Optional[Tuple[str, ...]], + ] ] = ResponseCache( hs.get_clock(), "get_room_hierarchy", @@ -126,6 +134,7 @@ class RoomSummaryHandler: max_depth: Optional[int] = None, limit: Optional[int] = None, from_token: Optional[str] = None, + remote_room_hosts: Optional[Tuple[str, ...]] = None, ) -> JsonDict: """ Implementation of the room hierarchy C-S API. @@ -143,6 +152,9 @@ class RoomSummaryHandler: limit: An optional limit on the number of rooms to return per page. Must be a positive integer. from_token: An optional pagination token. + remote_room_hosts: An optional list of remote homeserver server names. If defined, + each host will be used to try and fetch the room hierarchy. Must be a tuple so + that it can be hashed by the `RoomSummaryHandler._pagination_response_cache`. Returns: The JSON hierarchy dictionary. @@ -162,6 +174,7 @@ class RoomSummaryHandler: max_depth, limit, from_token, + remote_room_hosts, ), self._get_room_hierarchy, requester.user.to_string(), @@ -170,6 +183,7 @@ class RoomSummaryHandler: max_depth, limit, from_token, + remote_room_hosts, ) async def _get_room_hierarchy( @@ -180,6 +194,7 @@ class RoomSummaryHandler: max_depth: Optional[int] = None, limit: Optional[int] = None, from_token: Optional[str] = None, + remote_room_hosts: Optional[Tuple[str, ...]] = None, ) -> JsonDict: """See docstring for SpaceSummaryHandler.get_room_hierarchy.""" @@ -199,7 +214,7 @@ class RoomSummaryHandler: if not local_room: room_hierarchy = await self._summarize_remote_room_hierarchy( - _RoomQueueEntry(requested_room_id, ()), + _RoomQueueEntry(requested_room_id, remote_room_hosts or ()), False, ) root_room_entry = room_hierarchy[0] @@ -240,7 +255,7 @@ class RoomSummaryHandler: processed_rooms = set(pagination_session["processed_rooms"]) else: # The queue of rooms to process, the next room is last on the stack. - room_queue = [_RoomQueueEntry(requested_room_id, ())] + room_queue = [_RoomQueueEntry(requested_room_id, remote_room_hosts or ())] # Rooms we have already processed. processed_rooms = set() diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py index c64ce1f9c7..4600a87778 100644 --- a/synapse/rest/client/room.py +++ b/synapse/rest/client/room.py @@ -1538,6 +1538,7 @@ class RoomHierarchyRestServlet(RestServlet): super().__init__() self._auth = hs.get_auth() self._room_summary_handler = hs.get_room_summary_handler() + self.msc4235_enabled = hs.config.experimental.msc4235_enabled async def on_GET( self, request: SynapseRequest, room_id: str @@ -1547,6 +1548,15 @@ class RoomHierarchyRestServlet(RestServlet): max_depth = parse_integer(request, "max_depth") limit = parse_integer(request, "limit") + # twisted.web.server.Request.args is incorrectly defined as Optional[Any] + remote_room_hosts = None + if self.msc4235_enabled: + args: Dict[bytes, List[bytes]] = request.args # type: ignore + via_param = parse_strings_from_args( + args, "org.matrix.msc4235.via", required=False + ) + remote_room_hosts = tuple(via_param or []) + return 200, await self._room_summary_handler.get_room_hierarchy( requester, room_id, @@ -1554,6 +1564,7 @@ class RoomHierarchyRestServlet(RestServlet): max_depth=max_depth, limit=limit, from_token=parse_string(request, "from"), + remote_room_hosts=remote_room_hosts, ) diff --git a/tests/handlers/test_room_summary.py b/tests/handlers/test_room_summary.py index b55fa1a8fd..bf18c1e72a 100644 --- a/tests/handlers/test_room_summary.py +++ b/tests/handlers/test_room_summary.py @@ -1080,6 +1080,62 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase): self.assertEqual(federation_requests, 2) self._assert_hierarchy(result, expected) + def test_fed_remote_room_hosts(self) -> None: + """ + Test if requested room is available over federation using via's. + """ + fed_hostname = self.hs.hostname + "2" + fed_space = "#fed_space:" + fed_hostname + fed_subroom = "#fed_sub_room:" + fed_hostname + + remote_room_hosts = tuple(fed_hostname) + + requested_room_entry = _RoomEntry( + fed_space, + { + "room_id": fed_space, + "world_readable": True, + "join_rule": "public", + "room_type": RoomTypes.SPACE, + }, + [ + { + "type": EventTypes.SpaceChild, + "room_id": fed_space, + "state_key": fed_subroom, + "content": {"via": [fed_hostname]}, + } + ], + ) + child_room = { + "room_id": fed_subroom, + "world_readable": True, + "join_rule": "public", + } + + async def summarize_remote_room_hierarchy( + _self: Any, room: Any, suggested_only: bool + ) -> Tuple[Optional[_RoomEntry], Dict[str, JsonDict], Set[str]]: + return requested_room_entry, {fed_subroom: child_room}, set() + + expected = [ + (fed_space, [fed_subroom]), + (fed_subroom, ()), + ] + + with mock.patch( + "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy", + new=summarize_remote_room_hierarchy, + ): + result = self.get_success( + self.handler.get_room_hierarchy( + create_requester(self.user), + fed_space, + remote_room_hosts=remote_room_hosts, + ) + ) + self._assert_hierarchy(result, expected) + class RoomSummaryTestCase(unittest.HomeserverTestCase): servlets = [