diff --git a/changelog.d/6939.feature b/changelog.d/6939.feature
new file mode 100644
index 0000000000..40fe7fc9a9
--- /dev/null
+++ b/changelog.d/6939.feature
@@ -0,0 +1 @@
+Implement `GET /_matrix/client/r0/rooms/{roomId}/aliases` endpoint as per [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432).
diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py
index f718388884..3f8c792149 100644
--- a/synapse/handlers/directory.py
+++ b/synapse/handlers/directory.py
@@ -16,6 +16,7 @@
import logging
import string
+from typing import List
from twisted.internet import defer
@@ -28,7 +29,7 @@ from synapse.api.errors import (
StoreError,
SynapseError,
)
-from synapse.types import RoomAlias, UserID, get_domain_from_id
+from synapse.types import Requester, RoomAlias, UserID, get_domain_from_id
from ._base import BaseHandler
@@ -452,3 +453,17 @@ class DirectoryHandler(BaseHandler):
yield self.store.set_room_is_public_appservice(
room_id, appservice_id, network_id, visibility == "public"
)
+
+ async def get_aliases_for_room(
+ self, requester: Requester, room_id: str
+ ) -> List[str]:
+ """
+ Get a list of the aliases that currently point to this room on this server
+ """
+ # allow access to server admins and current members of the room
+ is_admin = await self.auth.is_server_admin(requester.user)
+ if not is_admin:
+ await self.auth.check_joined_room(room_id, requester.user.to_string())
+
+ aliases = await self.store.get_aliases_for_room(room_id)
+ return aliases
diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py
index 6f31584c51..143dc738c6 100644
--- a/synapse/rest/client/v1/room.py
+++ b/synapse/rest/client/v1/room.py
@@ -45,6 +45,10 @@ from synapse.storage.state import StateFilter
from synapse.streams.config import PaginationConfig
from synapse.types import RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID
+MYPY = False
+if MYPY:
+ import synapse.server
+
logger = logging.getLogger(__name__)
@@ -843,6 +847,24 @@ class RoomTypingRestServlet(RestServlet):
return 200, {}
+class RoomAliasListServlet(RestServlet):
+ PATTERNS = client_patterns("/rooms/(?P<room_id>[^/]*)/aliases", unstable=False)
+
+ def __init__(self, hs: "synapse.server.HomeServer"):
+ super().__init__()
+ self.auth = hs.get_auth()
+ self.directory_handler = hs.get_handlers().directory_handler
+
+ async def on_GET(self, request, room_id):
+ requester = await self.auth.get_user_by_req(request)
+
+ alias_list = await self.directory_handler.get_aliases_for_room(
+ requester, room_id
+ )
+
+ return 200, {"aliases": alias_list}
+
+
class SearchRestServlet(RestServlet):
PATTERNS = client_patterns("/search$", v1=True)
@@ -931,6 +953,7 @@ def register_servlets(hs, http_server):
JoinedRoomsRestServlet(hs).register(http_server)
RoomEventServlet(hs).register(http_server)
RoomEventContextServlet(hs).register(http_server)
+ RoomAliasListServlet(hs).register(http_server)
def register_deprecated_servlets(hs, http_server):
diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py
index fb681a1db9..fb08a45d27 100644
--- a/tests/rest/client/v1/test_rooms.py
+++ b/tests/rest/client/v1/test_rooms.py
@@ -28,8 +28,9 @@ from twisted.internet import defer
import synapse.rest.admin
from synapse.api.constants import EventContentFields, EventTypes, Membership
from synapse.handlers.pagination import PurgeStatus
-from synapse.rest.client.v1 import login, profile, room
+from synapse.rest.client.v1 import directory, login, profile, room
from synapse.rest.client.v2_alpha import account
+from synapse.types import JsonDict, RoomAlias
from synapse.util.stringutils import random_string
from tests import unittest
@@ -1726,3 +1727,70 @@ class ContextTestCase(unittest.HomeserverTestCase):
self.assertEqual(len(events_after), 2, events_after)
self.assertDictEqual(events_after[0].get("content"), {}, events_after[0])
self.assertEqual(events_after[1].get("content"), {}, events_after[1])
+
+
+class DirectoryTestCase(unittest.HomeserverTestCase):
+
+ servlets = [
+ synapse.rest.admin.register_servlets_for_client_rest_resource,
+ directory.register_servlets,
+ login.register_servlets,
+ room.register_servlets,
+ ]
+
+ def prepare(self, reactor, clock, homeserver):
+ self.room_owner = self.register_user("room_owner", "test")
+ self.room_owner_tok = self.login("room_owner", "test")
+
+ self.room_id = self.helper.create_room_as(
+ self.room_owner, tok=self.room_owner_tok
+ )
+
+ def test_no_aliases(self):
+ res = self._get_aliases(self.room_owner_tok)
+ self.assertEqual(res["aliases"], [])
+
+ def test_not_in_room(self):
+ self.register_user("user", "test")
+ user_tok = self.login("user", "test")
+ res = self._get_aliases(user_tok, expected_code=403)
+ self.assertEqual(res["errcode"], "M_FORBIDDEN")
+
+ def test_with_aliases(self):
+ alias1 = self._random_alias()
+ alias2 = self._random_alias()
+
+ self._set_alias_via_directory(alias1)
+ self._set_alias_via_directory(alias2)
+
+ res = self._get_aliases(self.room_owner_tok)
+ self.assertEqual(set(res["aliases"]), {alias1, alias2})
+
+ def _get_aliases(self, access_token: str, expected_code: int = 200) -> JsonDict:
+ """Calls the endpoint under test. returns the json response object."""
+ request, channel = self.make_request(
+ "GET",
+ "/_matrix/client/r0/rooms/%s/aliases" % (self.room_id,),
+ access_token=access_token,
+ )
+ self.render(request)
+ self.assertEqual(channel.code, expected_code, channel.result)
+ res = channel.json_body
+ self.assertIsInstance(res, dict)
+ if expected_code == 200:
+ self.assertIsInstance(res["aliases"], list)
+ return res
+
+ def _random_alias(self) -> str:
+ return RoomAlias(random_string(5), self.hs.hostname).to_string()
+
+ def _set_alias_via_directory(self, alias: str, expected_code: int = 200):
+ url = "/_matrix/client/r0/directory/room/" + alias
+ data = {"room_id": self.room_id}
+ request_data = json.dumps(data)
+
+ request, channel = self.make_request(
+ "PUT", url, request_data, access_token=self.room_owner_tok
+ )
+ self.render(request)
+ self.assertEqual(channel.code, expected_code, channel.result)
diff --git a/tests/unittest.py b/tests/unittest.py
index 98bf27d39c..8816a4d152 100644
--- a/tests/unittest.py
+++ b/tests/unittest.py
@@ -21,6 +21,7 @@ import hmac
import inspect
import logging
import time
+from typing import Optional, Tuple, Type, TypeVar, Union
from mock import Mock
@@ -42,7 +43,13 @@ from synapse.server import HomeServer
from synapse.types import Requester, UserID, create_requester
from synapse.util.ratelimitutils import FederationRateLimiter
-from tests.server import get_clock, make_request, render, setup_test_homeserver
+from tests.server import (
+ FakeChannel,
+ get_clock,
+ make_request,
+ render,
+ setup_test_homeserver,
+)
from tests.test_utils.logging_setup import setup_logging
from tests.utils import default_config, setupdb
@@ -71,6 +78,9 @@ def around(target):
return _around
+T = TypeVar("T")
+
+
class TestCase(unittest.TestCase):
"""A subclass of twisted.trial's TestCase which looks for 'loglevel'
attributes on both itself and its individual test methods, to override the
@@ -334,14 +344,14 @@ class HomeserverTestCase(TestCase):
def make_request(
self,
- method,
- path,
- content=b"",
- access_token=None,
- request=SynapseRequest,
- shorthand=True,
- federation_auth_origin=None,
- ):
+ method: Union[bytes, str],
+ path: Union[bytes, str],
+ content: Union[bytes, dict] = b"",
+ access_token: Optional[str] = None,
+ request: Type[T] = SynapseRequest,
+ shorthand: bool = True,
+ federation_auth_origin: str = None,
+ ) -> Tuple[T, FakeChannel]:
"""
Create a SynapseRequest at the path using the method and containing the
given content.
|