summary refs log tree commit diff
diff options
context:
space:
mode:
authorMichael Telatynski <7t3chguy@gmail.com>2021-08-16 15:49:12 +0100
committerGitHub <noreply@github.com>2021-08-16 14:49:12 +0000
commit0ace38b7b310fc1b4f88ac93d01ec900f33f7a07 (patch)
treebb160a73520626885425129aa088d1135dcc77f7
parentSplit `synapse.federation.transport.server` into multiple files. (#10590) (diff)
downloadsynapse-0ace38b7b310fc1b4f88ac93d01ec900f33f7a07.tar.xz
Experimental support for MSC3266 Room Summary API. (#10394)
-rw-r--r--changelog.d/10394.feature1
-rw-r--r--mypy.ini2
-rw-r--r--synapse/config/experimental.py3
-rw-r--r--synapse/federation/transport/server/federation.py4
-rw-r--r--synapse/handlers/room_summary.py (renamed from synapse/handlers/space_summary.py)87
-rw-r--r--synapse/http/servlet.py58
-rw-r--r--synapse/rest/admin/rooms.py45
-rw-r--r--synapse/rest/client/v1/room.py90
-rw-r--r--synapse/server.py6
-rw-r--r--tests/handlers/test_room_summary.py (renamed from tests/handlers/test_space_summary.py)108
10 files changed, 289 insertions, 115 deletions
diff --git a/changelog.d/10394.feature b/changelog.d/10394.feature
new file mode 100644
index 0000000000..c8bbc5a740
--- /dev/null
+++ b/changelog.d/10394.feature
@@ -0,0 +1 @@
+Initial local support for [MSC3266](https://github.com/matrix-org/synapse/pull/10394), Room Summary over the unstable `/rooms/{roomIdOrAlias}/summary` API.
diff --git a/mypy.ini b/mypy.ini
index 5d6cd557bc..e1b9405daa 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -86,7 +86,7 @@ files =
   tests/test_event_auth.py,
   tests/test_utils,
   tests/handlers/test_password_providers.py,
-  tests/handlers/test_space_summary.py,
+  tests/handlers/test_room_summary.py,
   tests/rest/client/v1/test_login.py,
   tests/rest/client/v2_alpha/test_auth.py,
   tests/util/test_itertools.py,
diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index 4c60ee8c28..b918fb15b0 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -38,3 +38,6 @@ class ExperimentalConfig(Config):
 
         # MSC3244 (room version capabilities)
         self.msc3244_enabled: bool = experimental.get("msc3244_enabled", False)
+
+        # MSC3266 (room summary api)
+        self.msc3266_enabled: bool = experimental.get("msc3266_enabled", False)
diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py
index 2806337846..7d81cc642c 100644
--- a/synapse/federation/transport/server/federation.py
+++ b/synapse/federation/transport/server/federation.py
@@ -547,7 +547,7 @@ class FederationSpaceSummaryServlet(BaseFederationServlet):
         server_name: str,
     ):
         super().__init__(hs, authenticator, ratelimiter, server_name)
-        self.handler = hs.get_space_summary_handler()
+        self.handler = hs.get_room_summary_handler()
 
     async def on_GET(
         self,
@@ -608,7 +608,7 @@ class FederationRoomHierarchyServlet(BaseFederationServlet):
         server_name: str,
     ):
         super().__init__(hs, authenticator, ratelimiter, server_name)
-        self.handler = hs.get_space_summary_handler()
+        self.handler = hs.get_room_summary_handler()
 
     async def on_GET(
         self,
diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/room_summary.py
index c74e90abbc..ac6cfc0da9 100644
--- a/synapse/handlers/space_summary.py
+++ b/synapse/handlers/room_summary.py
@@ -28,7 +28,7 @@ from synapse.api.constants import (
     Membership,
     RoomTypes,
 )
-from synapse.api.errors import AuthError, Codes, SynapseError
+from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
 from synapse.events import EventBase
 from synapse.events.utils import format_event_for_client_v2
 from synapse.types import JsonDict
@@ -75,7 +75,7 @@ class _PaginationSession:
     processed_rooms: Set[str]
 
 
-class SpaceSummaryHandler:
+class RoomSummaryHandler:
     # The time a pagination session remains valid for.
     _PAGINATION_SESSION_VALIDITY_PERIOD_MS = 5 * 60 * 1000
 
@@ -412,7 +412,7 @@ class SpaceSummaryHandler:
                         room_entry,
                         children_room_entries,
                         inaccessible_children,
-                    ) = await self._summarize_remote_room_hiearchy(
+                    ) = await self._summarize_remote_room_hierarchy(
                         queue_entry,
                         suggested_only,
                     )
@@ -724,7 +724,7 @@ class SpaceSummaryHandler:
 
         return results
 
-    async def _summarize_remote_room_hiearchy(
+    async def _summarize_remote_room_hierarchy(
         self, room: "_RoomQueueEntry", suggested_only: bool
     ) -> Tuple[Optional["_RoomEntry"], Dict[str, JsonDict], Set[str]]:
         """
@@ -781,25 +781,25 @@ class SpaceSummaryHandler:
         self, room_id: str, requester: Optional[str], origin: Optional[str] = None
     ) -> bool:
         """
-        Calculate whether the room should be shown in the spaces summary.
+        Calculate whether the room should be shown to the requester.
 
-        It should be included if:
+        It should return true if:
 
         * The requester is joined or can join the room (per MSC3173).
         * The origin server has any user that is joined or can join the room.
         * The history visibility is set to world readable.
 
         Args:
-            room_id: The room ID to summarize.
+            room_id: The room ID to check accessibility of.
             requester:
-                The user requesting the summary, if it is a local request. None
-                if this is a federation request.
+                The user making the request, if it is a local request.
+                None if this is a federation request.
             origin:
-                The server requesting the summary, if it is a federation request.
+                The server making the request, if it is a federation request.
                 None if this is a local request.
 
         Returns:
-             True if the room should be included in the spaces summary.
+             True if the room is accessible to the requesting user or server.
         """
         state_ids = await self._store.get_current_state_ids(room_id)
 
@@ -893,9 +893,9 @@ class SpaceSummaryHandler:
         self, requester: str, room_id: str, room: JsonDict
     ) -> bool:
         """
-        Calculate whether the room received over federation should be shown in the spaces summary.
+        Calculate whether the room received over federation should be shown to the requester.
 
-        It should be included if:
+        It should return true if:
 
         * The requester is joined or can join the room (per MSC3173).
         * The history visibility is set to world readable.
@@ -907,10 +907,10 @@ class SpaceSummaryHandler:
         Args:
             requester: The user requesting the summary.
             room_id: The room ID returned over federation.
-            room: The summary of the child room returned over federation.
+            room: The summary of the room returned over federation.
 
         Returns:
-            True if the room should be included in the spaces summary.
+            True if the room is accessible to the requesting user.
         """
         # The API doesn't return the room version so assume that a
         # join rule of knock is valid.
@@ -936,7 +936,7 @@ class SpaceSummaryHandler:
 
     async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDict:
         """
-        Generate en entry suitable for the 'rooms' list in the summary response.
+        Generate en entry summarising a single room.
 
         Args:
             room_id: The room ID to summarize.
@@ -1024,6 +1024,61 @@ class SpaceSummaryHandler:
         # and order to ensure we return stable results.
         return sorted(filter(_has_valid_via, events), key=_child_events_comparison_key)
 
+    async def get_room_summary(
+        self,
+        requester: Optional[str],
+        room_id: str,
+        remote_room_hosts: Optional[List[str]] = None,
+    ) -> JsonDict:
+        """
+        Implementation of the room summary C-S API from MSC3266
+
+        Args:
+            requester:  user id of the user making this request, will be None
+                for unauthenticated requests
+
+            room_id: room id to summarise.
+
+            remote_room_hosts: a list of homeservers to try fetching data through
+                if we don't know it ourselves
+
+        Returns:
+            summary dict to return
+        """
+        is_in_room = await self._store.is_host_joined(room_id, self._server_name)
+
+        if is_in_room:
+            room_entry = await self._summarize_local_room(
+                requester,
+                None,
+                room_id,
+                # Suggested-only doesn't matter since no children are requested.
+                suggested_only=False,
+                max_children=0,
+            )
+
+            if not room_entry:
+                raise NotFoundError("Room not found or is not accessible")
+
+            room_summary = room_entry.room
+
+            # If there was a requester, add their membership.
+            if requester:
+                (
+                    membership,
+                    _,
+                ) = await self._store.get_local_current_membership_for_user_in_room(
+                    requester, room_id
+                )
+
+                room_summary["membership"] = membership or "leave"
+        else:
+            # TODO federation API, descoped from initial unstable implementation
+            #      as MSC needs more maturing on that side.
+            raise SynapseError(400, "Federation is not currently supported.")
+
+        return room_summary
+
 
 @attr.s(frozen=True, slots=True, auto_attribs=True)
 class _RoomQueueEntry:
diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py
index 732a1e6aeb..a12fa30bfd 100644
--- a/synapse/http/servlet.py
+++ b/synapse/http/servlet.py
@@ -14,16 +14,28 @@
 
 """ This module contains base REST classes for constructing REST servlets. """
 import logging
-from typing import Iterable, List, Mapping, Optional, Sequence, overload
+from typing import (
+    TYPE_CHECKING,
+    Iterable,
+    List,
+    Mapping,
+    Optional,
+    Sequence,
+    Tuple,
+    overload,
+)
 
 from typing_extensions import Literal
 
 from twisted.web.server import Request
 
 from synapse.api.errors import Codes, SynapseError
-from synapse.types import JsonDict
+from synapse.types import JsonDict, RoomAlias, RoomID
 from synapse.util import json_decoder
 
+if TYPE_CHECKING:
+    from synapse.server import HomeServer
+
 logger = logging.getLogger(__name__)
 
 
@@ -663,3 +675,45 @@ class RestServlet:
 
         else:
             raise NotImplementedError("RestServlet must register something.")
+
+
+class ResolveRoomIdMixin:
+    def __init__(self, hs: "HomeServer"):
+        self.room_member_handler = hs.get_room_member_handler()
+
+    async def resolve_room_id(
+        self, room_identifier: str, remote_room_hosts: Optional[List[str]] = None
+    ) -> Tuple[str, Optional[List[str]]]:
+        """
+        Resolve a room identifier to a room ID, if necessary.
+
+        This also performanes checks to ensure the room ID is of the proper form.
+
+        Args:
+            room_identifier: The room ID or alias.
+            remote_room_hosts: The potential remote room hosts to use.
+
+        Returns:
+            The resolved room ID.
+
+        Raises:
+            SynapseError if the room ID is of the wrong form.
+        """
+        if RoomID.is_valid(room_identifier):
+            resolved_room_id = room_identifier
+        elif RoomAlias.is_valid(room_identifier):
+            room_alias = RoomAlias.from_string(room_identifier)
+            (
+                room_id,
+                remote_room_hosts,
+            ) = await self.room_member_handler.lookup_room_alias(room_alias)
+            resolved_room_id = room_id.to_string()
+        else:
+            raise SynapseError(
+                400, "%s was not legal room ID or room alias" % (room_identifier,)
+            )
+        if not resolved_room_id:
+            raise SynapseError(
+                400, "Unknown room ID or room alias %s" % room_identifier
+            )
+        return resolved_room_id, remote_room_hosts
diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py
index 40ee33646c..975c28b225 100644
--- a/synapse/rest/admin/rooms.py
+++ b/synapse/rest/admin/rooms.py
@@ -20,6 +20,7 @@ from synapse.api.constants import EventTypes, JoinRules, Membership
 from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
 from synapse.api.filtering import Filter
 from synapse.http.servlet import (
+    ResolveRoomIdMixin,
     RestServlet,
     assert_params_in_dict,
     parse_integer,
@@ -33,7 +34,7 @@ from synapse.rest.admin._base import (
     assert_user_is_admin,
 )
 from synapse.storage.databases.main.room import RoomSortOrder
-from synapse.types import JsonDict, RoomAlias, RoomID, UserID, create_requester
+from synapse.types import JsonDict, UserID, create_requester
 from synapse.util import json_decoder
 
 if TYPE_CHECKING:
@@ -45,48 +46,6 @@ if TYPE_CHECKING:
 logger = logging.getLogger(__name__)
 
 
-class ResolveRoomIdMixin:
-    def __init__(self, hs: "HomeServer"):
-        self.room_member_handler = hs.get_room_member_handler()
-
-    async def resolve_room_id(
-        self, room_identifier: str, remote_room_hosts: Optional[List[str]] = None
-    ) -> Tuple[str, Optional[List[str]]]:
-        """
-        Resolve a room identifier to a room ID, if necessary.
-
-        This also performanes checks to ensure the room ID is of the proper form.
-
-        Args:
-            room_identifier: The room ID or alias.
-            remote_room_hosts: The potential remote room hosts to use.
-
-        Returns:
-            The resolved room ID.
-
-        Raises:
-            SynapseError if the room ID is of the wrong form.
-        """
-        if RoomID.is_valid(room_identifier):
-            resolved_room_id = room_identifier
-        elif RoomAlias.is_valid(room_identifier):
-            room_alias = RoomAlias.from_string(room_identifier)
-            (
-                room_id,
-                remote_room_hosts,
-            ) = await self.room_member_handler.lookup_room_alias(room_alias)
-            resolved_room_id = room_id.to_string()
-        else:
-            raise SynapseError(
-                400, "%s was not legal room ID or room alias" % (room_identifier,)
-            )
-        if not resolved_room_id:
-            raise SynapseError(
-                400, "Unknown room ID or room alias %s" % room_identifier
-            )
-        return resolved_room_id, remote_room_hosts
-
-
 class ShutdownRoomRestServlet(RestServlet):
     """Shuts down a room by removing all local users from the room and blocking
     all future invites and joins to the room. Any local aliases will be repointed
diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py
index 2c3be23bc8..d3882a84e2 100644
--- a/synapse/rest/client/v1/room.py
+++ b/synapse/rest/client/v1/room.py
@@ -24,12 +24,14 @@ from synapse.api.errors import (
     AuthError,
     Codes,
     InvalidClientCredentialsError,
+    MissingClientTokenError,
     ShadowBanError,
     SynapseError,
 )
 from synapse.api.filtering import Filter
 from synapse.events.utils import format_event_for_client_v2
 from synapse.http.servlet import (
+    ResolveRoomIdMixin,
     RestServlet,
     assert_params_in_dict,
     parse_boolean,
@@ -44,14 +46,7 @@ from synapse.rest.client.transactions import HttpTransactionCache
 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 (
-    JsonDict,
-    RoomAlias,
-    RoomID,
-    StreamToken,
-    ThirdPartyInstanceID,
-    UserID,
-)
+from synapse.types import JsonDict, StreamToken, ThirdPartyInstanceID, UserID
 from synapse.util import json_decoder
 from synapse.util.stringutils import parse_and_validate_server_name, random_string
 
@@ -266,10 +261,10 @@ class RoomSendEventRestServlet(TransactionRestServlet):
 
 
 # TODO: Needs unit testing for room ID + alias joins
-class JoinRoomAliasServlet(TransactionRestServlet):
+class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet):
     def __init__(self, hs):
         super().__init__(hs)
-        self.room_member_handler = hs.get_room_member_handler()
+        super(ResolveRoomIdMixin, self).__init__(hs)  # ensure the Mixin is set up
         self.auth = hs.get_auth()
 
     def register(self, http_server):
@@ -292,24 +287,13 @@ class JoinRoomAliasServlet(TransactionRestServlet):
             # cheekily send invalid bodies.
             content = {}
 
-        if RoomID.is_valid(room_identifier):
-            room_id = room_identifier
-
-            # 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
-            )
-        elif RoomAlias.is_valid(room_identifier):
-            handler = self.room_member_handler
-            room_alias = RoomAlias.from_string(room_identifier)
-            room_id_obj, remote_room_hosts = await handler.lookup_room_alias(room_alias)
-            room_id = room_id_obj.to_string()
-        else:
-            raise SynapseError(
-                400, "%s was not legal room ID or room alias" % (room_identifier,)
-            )
+        # 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)
+        room_id, remote_room_hosts = await self.resolve_room_id(
+            room_identifier,
+            remote_room_hosts,
+        )
 
         await self.room_member_handler.update_membership(
             requester=requester,
@@ -1002,14 +986,14 @@ class RoomSpaceSummaryRestServlet(RestServlet):
     def __init__(self, hs: "HomeServer"):
         super().__init__()
         self._auth = hs.get_auth()
-        self._space_summary_handler = hs.get_space_summary_handler()
+        self._room_summary_handler = hs.get_room_summary_handler()
 
     async def on_GET(
         self, request: SynapseRequest, 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(
+        return 200, await self._room_summary_handler.get_space_summary(
             requester.user.to_string(),
             room_id,
             suggested_only=parse_boolean(request, "suggested_only", default=False),
@@ -1035,7 +1019,7 @@ class RoomSpaceSummaryRestServlet(RestServlet):
                 400, "'max_rooms_per_space' must be an integer", Codes.BAD_JSON
             )
 
-        return 200, await self._space_summary_handler.get_space_summary(
+        return 200, await self._room_summary_handler.get_space_summary(
             requester.user.to_string(),
             room_id,
             suggested_only=suggested_only,
@@ -1054,7 +1038,7 @@ class RoomHierarchyRestServlet(RestServlet):
     def __init__(self, hs: "HomeServer"):
         super().__init__()
         self._auth = hs.get_auth()
-        self._space_summary_handler = hs.get_space_summary_handler()
+        self._room_summary_handler = hs.get_room_summary_handler()
 
     async def on_GET(
         self, request: SynapseRequest, room_id: str
@@ -1073,7 +1057,7 @@ class RoomHierarchyRestServlet(RestServlet):
                 400, "'limit' must be a positive integer", Codes.BAD_JSON
             )
 
-        return 200, await self._space_summary_handler.get_room_hierarchy(
+        return 200, await self._room_summary_handler.get_room_hierarchy(
             requester.user.to_string(),
             room_id,
             suggested_only=parse_boolean(request, "suggested_only", default=False),
@@ -1083,6 +1067,44 @@ class RoomHierarchyRestServlet(RestServlet):
         )
 
 
+class RoomSummaryRestServlet(ResolveRoomIdMixin, RestServlet):
+    PATTERNS = (
+        re.compile(
+            "^/_matrix/client/unstable/im.nheko.summary"
+            "/rooms/(?P<room_identifier>[^/]*)/summary$"
+        ),
+    )
+
+    def __init__(self, hs: "HomeServer"):
+        super().__init__(hs)
+        self._auth = hs.get_auth()
+        self._room_summary_handler = hs.get_room_summary_handler()
+
+    async def on_GET(
+        self, request: SynapseRequest, room_identifier: str
+    ) -> Tuple[int, JsonDict]:
+        try:
+            requester = await self._auth.get_user_by_req(request, allow_guest=True)
+            requester_user_id: Optional[str] = requester.user.to_string()
+        except MissingClientTokenError:
+            # auth is optional
+            requester_user_id = None
+
+        # 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, "via", required=False)
+        room_id, remote_room_hosts = await self.resolve_room_id(
+            room_identifier,
+            remote_room_hosts,
+        )
+
+        return 200, await self._room_summary_handler.get_room_summary(
+            requester_user_id,
+            room_id,
+            remote_room_hosts,
+        )
+
+
 def register_servlets(hs: "HomeServer", http_server, is_worker=False):
     RoomStateEventRestServlet(hs).register(http_server)
     RoomMemberListRestServlet(hs).register(http_server)
@@ -1098,6 +1120,8 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False):
     RoomEventContextServlet(hs).register(http_server)
     RoomSpaceSummaryRestServlet(hs).register(http_server)
     RoomHierarchyRestServlet(hs).register(http_server)
+    if hs.config.experimental.msc3266_enabled:
+        RoomSummaryRestServlet(hs).register(http_server)
     RoomEventServlet(hs).register(http_server)
     JoinedRoomsRestServlet(hs).register(http_server)
     RoomAliasListServlet(hs).register(http_server)
diff --git a/synapse/server.py b/synapse/server.py
index 6c867f0f47..de6517663e 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -99,10 +99,10 @@ from synapse.handlers.room import (
 from synapse.handlers.room_list import RoomListHandler
 from synapse.handlers.room_member import RoomMemberHandler, RoomMemberMasterHandler
 from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
+from synapse.handlers.room_summary import RoomSummaryHandler
 from synapse.handlers.search import SearchHandler
 from synapse.handlers.send_email import SendEmailHandler
 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
@@ -772,8 +772,8 @@ class HomeServer(metaclass=abc.ABCMeta):
         return AccountDataHandler(self)
 
     @cache_in_self
-    def get_space_summary_handler(self) -> SpaceSummaryHandler:
-        return SpaceSummaryHandler(self)
+    def get_room_summary_handler(self) -> RoomSummaryHandler:
+        return RoomSummaryHandler(self)
 
     @cache_in_self
     def get_event_auth_handler(self) -> EventAuthHandler:
diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_room_summary.py
index bc8e131f4a..732d746e38 100644
--- a/tests/handlers/test_space_summary.py
+++ b/tests/handlers/test_room_summary.py
@@ -23,10 +23,10 @@ from synapse.api.constants import (
     RestrictedJoinRuleTypes,
     RoomTypes,
 )
-from synapse.api.errors import AuthError, SynapseError
+from synapse.api.errors import AuthError, NotFoundError, SynapseError
 from synapse.api.room_versions import RoomVersions
 from synapse.events import make_event_from_dict
-from synapse.handlers.space_summary import _child_events_comparison_key, _RoomEntry
+from synapse.handlers.room_summary import _child_events_comparison_key, _RoomEntry
 from synapse.rest import admin
 from synapse.rest.client.v1 import login, room
 from synapse.server import HomeServer
@@ -106,7 +106,7 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase):
 
     def prepare(self, reactor, clock, hs: HomeServer):
         self.hs = hs
-        self.handler = self.hs.get_space_summary_handler()
+        self.handler = self.hs.get_room_summary_handler()
 
         # Create a user.
         self.user = self.register_user("user", "pass")
@@ -624,14 +624,14 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase):
                 ),
             ]
 
-        async def summarize_remote_room_hiearchy(_self, room, suggested_only):
+        async def summarize_remote_room_hierarchy(_self, room, suggested_only):
             return requested_room_entry, {subroom: child_room}, set()
 
         # Add a room to the space which is on another server.
         self._add_child(self.space, subspace, self.token)
 
         with mock.patch(
-            "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room",
+            "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room",
             new=summarize_remote_room,
         ):
             result = self.get_success(
@@ -647,8 +647,8 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase):
         self._assert_rooms(result, expected)
 
         with mock.patch(
-            "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy",
-            new=summarize_remote_room_hiearchy,
+            "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy",
+            new=summarize_remote_room_hierarchy,
         ):
             result = self.get_success(
                 self.handler.get_room_hierarchy(self.user, self.space)
@@ -774,14 +774,14 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase):
                 for child_room in children_rooms
             ]
 
-        async def summarize_remote_room_hiearchy(_self, room, suggested_only):
+        async def summarize_remote_room_hierarchy(_self, room, suggested_only):
             return subspace_room_entry, dict(children_rooms), set()
 
         # Add a room to the space which is on another server.
         self._add_child(self.space, subspace, self.token)
 
         with mock.patch(
-            "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room",
+            "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room",
             new=summarize_remote_room,
         ):
             result = self.get_success(
@@ -814,8 +814,8 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase):
         self._assert_rooms(result, expected)
 
         with mock.patch(
-            "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy",
-            new=summarize_remote_room_hiearchy,
+            "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy",
+            new=summarize_remote_room_hierarchy,
         ):
             result = self.get_success(
                 self.handler.get_room_hierarchy(self.user, self.space)
@@ -850,14 +850,14 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase):
         ):
             return [fed_room_entry]
 
-        async def summarize_remote_room_hiearchy(_self, room, suggested_only):
+        async def summarize_remote_room_hierarchy(_self, room, suggested_only):
             return fed_room_entry, {}, set()
 
         # Add a room to the space which is on another server.
         self._add_child(self.space, fed_room, self.token)
 
         with mock.patch(
-            "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room",
+            "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room",
             new=summarize_remote_room,
         ):
             result = self.get_success(
@@ -872,10 +872,88 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase):
         self._assert_rooms(result, expected)
 
         with mock.patch(
-            "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy",
-            new=summarize_remote_room_hiearchy,
+            "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy",
+            new=summarize_remote_room_hierarchy,
         ):
             result = self.get_success(
                 self.handler.get_room_hierarchy(self.user, self.space)
             )
         self._assert_hierarchy(result, expected)
+
+
+class RoomSummaryTestCase(unittest.HomeserverTestCase):
+    servlets = [
+        admin.register_servlets_for_client_rest_resource,
+        room.register_servlets,
+        login.register_servlets,
+    ]
+
+    def prepare(self, reactor, clock, hs: HomeServer):
+        self.hs = hs
+        self.handler = self.hs.get_room_summary_handler()
+
+        # Create a user.
+        self.user = self.register_user("user", "pass")
+        self.token = self.login("user", "pass")
+
+        # Create a simple room.
+        self.room = self.helper.create_room_as(self.user, tok=self.token)
+        self.helper.send_state(
+            self.room,
+            event_type=EventTypes.JoinRules,
+            body={"join_rule": JoinRules.INVITE},
+            tok=self.token,
+        )
+
+    def test_own_room(self):
+        """Test a simple room created by the requester."""
+        result = self.get_success(self.handler.get_room_summary(self.user, self.room))
+        self.assertEqual(result.get("room_id"), self.room)
+
+    def test_visibility(self):
+        """A user not in a private room cannot get its summary."""
+        user2 = self.register_user("user2", "pass")
+        token2 = self.login("user2", "pass")
+
+        # The user cannot see the room.
+        self.get_failure(self.handler.get_room_summary(user2, self.room), NotFoundError)
+
+        # If the room is made world-readable it should return a result.
+        self.helper.send_state(
+            self.room,
+            event_type=EventTypes.RoomHistoryVisibility,
+            body={"history_visibility": HistoryVisibility.WORLD_READABLE},
+            tok=self.token,
+        )
+        result = self.get_success(self.handler.get_room_summary(user2, self.room))
+        self.assertEqual(result.get("room_id"), self.room)
+
+        # Make it not world-readable again and confirm it results in an error.
+        self.helper.send_state(
+            self.room,
+            event_type=EventTypes.RoomHistoryVisibility,
+            body={"history_visibility": HistoryVisibility.JOINED},
+            tok=self.token,
+        )
+        self.get_failure(self.handler.get_room_summary(user2, self.room), NotFoundError)
+
+        # If the room is made public it should return a result.
+        self.helper.send_state(
+            self.room,
+            event_type=EventTypes.JoinRules,
+            body={"join_rule": JoinRules.PUBLIC},
+            tok=self.token,
+        )
+        result = self.get_success(self.handler.get_room_summary(user2, self.room))
+        self.assertEqual(result.get("room_id"), self.room)
+
+        # Join the space, make it invite-only again and results should be returned.
+        self.helper.join(self.room, user2, tok=token2)
+        self.helper.send_state(
+            self.room,
+            event_type=EventTypes.JoinRules,
+            body={"join_rule": JoinRules.INVITE},
+            tok=self.token,
+        )
+        result = self.get_success(self.handler.get_room_summary(user2, self.room))
+        self.assertEqual(result.get("room_id"), self.room)