From 842c87f9aae446123f6811e694ce987ff07563d0 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 21 Apr 2020 13:19:02 +0200 Subject: [PATCH 1/8] Add room details admin endpoint Signed-off-by: Manuel Stahl --- changelog.d/7317.feature | 1 + docs/admin_api/rooms.md | 31 ++++++++++++++++ synapse/rest/admin/__init__.py | 2 ++ synapse/rest/admin/rooms.py | 39 +++++++++++++++++++- synapse/storage/data_stores/main/room.py | 46 ++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7317.feature diff --git a/changelog.d/7317.feature b/changelog.d/7317.feature new file mode 100644 index 000000000000..23c063f28019 --- /dev/null +++ b/changelog.d/7317.feature @@ -0,0 +1 @@ +Add room details admin endpoint. Contributed by Awesome Technologies Innovationslabor GmbH. diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 26fe8b8679a0..697b9b671c8c 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -264,3 +264,34 @@ Response: Once the `next_token` parameter is no longer present, we know we've reached the end of the list. + +# DRAFT: Room Details API + +The Room Details admin API allows server admins to get all details of a room, +including a list of all room members. + +This API is still a draft and details might change! + +## Usage + +A standard request: + +``` +GET /_synapse/admin/v1/rooms/ + +{} +``` + +Response: + +``` +{ + "room_id": "!OGEhHVWSdvArJzumhm:matrix.org", + "name": "Matrix HQ", + "canonical_alias": "#matrix:matrix.org", + "join_rules": "invite", + "guest_access": "can_join", + "topic": "A room topic", + "members": ["@foo:matrix.org", "@bar:matrix.org"] +} +``` diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index ed70d448a141..6b85148a32fb 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -32,6 +32,7 @@ from synapse.rest.admin.rooms import ( JoinRoomAliasServlet, ListRoomRestServlet, + RoomRestServlet, ShutdownRoomRestServlet, ) from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet @@ -193,6 +194,7 @@ def register_servlets(hs, http_server): """ register_servlets_for_client_rest_resource(hs, http_server) ListRoomRestServlet(hs).register(http_server) + RoomRestServlet(hs).register(http_server) JoinRoomAliasServlet(hs).register(http_server) PurgeRoomServlet(hs).register(http_server) SendServerNoticeServlet(hs).register(http_server) diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index d1bdb641115d..0bb595fbe803 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -169,7 +169,7 @@ class ListRoomRestServlet(RestServlet): in a dictionary containing room information. Supports pagination. """ - PATTERNS = admin_patterns("/rooms") + PATTERNS = admin_patterns("/rooms$") def __init__(self, hs): self.store = hs.get_datastore() @@ -253,6 +253,43 @@ async def on_GET(self, request): return 200, response +class RoomRestServlet(RestServlet): + """Get room details. + This needs user to have administrator access in Synapse. + TODO: Return power level for each user + TODO: Add on_POST to allow room creation without joining the room + + GET /_synapse/admin/v1/rooms/ + + returns: + 200 OK with users if success otherwise an error. + """ + + PATTERNS = admin_patterns("/rooms/(?P[^/]+)$") + + def __init__(self, hs): + self.hs = hs + self.auth = hs.get_auth() + self.store = hs.get_datastore() + + async def on_GET(self, request, room_id): + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + ret = await self.store.get_room_stats_state(room_id) + if not ret: + raise NotFoundError("Room not found") + + stats_curr = await self.store.get_room_stats_current(room_id) + if stats_curr: + ret.update(stats_curr) + + members = await self.store.get_users_in_room(room_id) + ret["members"] = members if members else [] + + return 200, ret + + class JoinRoomAliasServlet(RestServlet): PATTERNS = admin_patterns("/join/(?P[^/]*)") diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 147eba1df70e..0e57dbcacbfd 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -98,6 +98,52 @@ def get_room(self, room_id): allow_none=True, ) + def get_room_stats_state(self, room_id): + """Retrieve room stats state. + + Args: + room_id (str): The ID of the room to retrieve. + Returns: + A dict containing the room information, or None if the room is unknown. + """ + return self.db.simple_select_one( + table="room_stats_state", + keyvalues={"room_id": room_id}, + retcols=( + "room_id", + "name", + "canonical_alias", + "encryption", + "is_federatable", + "join_rules", + "guest_access", + "history_visibility" "topic", + ), + desc="get_room_stats_state", + allow_none=True, + ) + + def get_room_stats_current(self, room_id): + """Retrieve current room stats. + + Args: + room_id (str): The ID of the room to retrieve. + Returns: + A dict containing the room information, or None if the room is unknown. + """ + return self.db.simple_select_one( + table="room_stats_state", + keyvalues={"room_id": room_id}, + retcols=( + "room_id", + "joined_members", + "local_users_in_room", + "current_state_events", + ), + desc="get_room_stats_current", + allow_none=True, + ) + def get_public_room_ids(self): return self.db.simple_select_onecol( table="rooms", From b05ba4dd7608a7d547714a7dfcd7111f3a867336 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Thu, 30 Apr 2020 23:01:18 +0200 Subject: [PATCH 2/8] Fix retcols --- synapse/storage/data_stores/main/room.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 0e57dbcacbfd..128f13094331 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -117,7 +117,8 @@ def get_room_stats_state(self, room_id): "is_federatable", "join_rules", "guest_access", - "history_visibility" "topic", + "history_visibility", + "topic", ), desc="get_room_stats_state", allow_none=True, From da6d9676b8ac769c23685504e149fcfab143a9c0 Mon Sep 17 00:00:00 2001 From: Manuel Stahl <37705355+awesome-manuel@users.noreply.github.com> Date: Fri, 1 May 2020 18:27:41 +0200 Subject: [PATCH 3/8] Update synapse/rest/admin/rooms.py Co-authored-by: Patrick Cloke --- synapse/rest/admin/rooms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 0bb595fbe803..45a9ae3743c1 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -255,7 +255,8 @@ async def on_GET(self, request): class RoomRestServlet(RestServlet): """Get room details. - This needs user to have administrator access in Synapse. + + This needs the requesting user to have administrator access in Synapse. TODO: Return power level for each user TODO: Add on_POST to allow room creation without joining the room From 7056da7d1af5d29fe7b6e5253500e536f8a86335 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Fri, 1 May 2020 20:11:37 +0200 Subject: [PATCH 4/8] Add all columns from the room list endpoint --- docs/admin_api/rooms.md | 36 ++++++++++++-- synapse/rest/admin/rooms.py | 14 +----- synapse/storage/data_stores/main/room.py | 60 +++++++++--------------- tests/rest/admin/test_room.py | 41 ++++++++++++++++ tests/storage/test_room.py | 11 +++++ 5 files changed, 106 insertions(+), 56 deletions(-) diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 697b9b671c8c..0c5a28fd72bc 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -272,6 +272,24 @@ including a list of all room members. This API is still a draft and details might change! +The following fields are possible in the JSON response body: + +* `room_id` - The ID of the room. +* `name` - The name of the room. +* `canonical_alias` - The canonical (main) alias address of the room. +* `joined_members` - How many users are currently in the room. +* `joined_local_members` - How many local users are currently in the room. +* `version` - The version of the room as a string. +* `creator` - The `user_id` of the room creator. +* `encryption` - Algorithm of end-to-end encryption of messages. Is `null` if encryption is not active. +* `federatable` - Whether users on other servers can join this room. +* `public` - Whether the room is visible in room directory. +* `join_rules` - The type of rules used for users wishing to join this room. One of: ["public", "knock", "invite", "private"]. +* `guest_access` - Whether guests can join the room. One of: ["can_join", "forbidden"]. +* `history_visibility` - Who can see the room history. One of: ["invited", "joined", "shared", "world_readable"]. +* `state_events` - Total number of state_events of a room. Complexity of the room. +* `members` - An array of user ids. + ## Usage A standard request: @@ -286,12 +304,20 @@ Response: ``` { - "room_id": "!OGEhHVWSdvArJzumhm:matrix.org", - "name": "Matrix HQ", - "canonical_alias": "#matrix:matrix.org", + "room_id": "!mscvqgqpHYjBGDxNym:matrix.org", + "name": "Music Theory", + "canonical_alias": "#musictheory:matrix.org", + "joined_members": 127 + "joined_local_members": 2, + "version": "1", + "creator": "@foo:matrix.org", + "encryption": null, + "federatable": true, + "public": true, "join_rules": "invite", - "guest_access": "can_join", - "topic": "A room topic", + "guest_access": null, + "history_visibility": "shared", + "state_events": 93534 "members": ["@foo:matrix.org", "@bar:matrix.org"] } ``` diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 45a9ae3743c1..608e967c86e2 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -256,14 +256,8 @@ async def on_GET(self, request): class RoomRestServlet(RestServlet): """Get room details. - This needs the requesting user to have administrator access in Synapse. TODO: Return power level for each user TODO: Add on_POST to allow room creation without joining the room - - GET /_synapse/admin/v1/rooms/ - - returns: - 200 OK with users if success otherwise an error. """ PATTERNS = admin_patterns("/rooms/(?P[^/]+)$") @@ -277,16 +271,12 @@ async def on_GET(self, request, room_id): requester = await self.auth.get_user_by_req(request) await assert_user_is_admin(self.auth, requester.user) - ret = await self.store.get_room_stats_state(room_id) + ret = await self.store.get_room_with_stats(room_id) if not ret: raise NotFoundError("Room not found") - stats_curr = await self.store.get_room_stats_current(room_id) - if stats_curr: - ret.update(stats_curr) - members = await self.store.get_users_in_room(room_id) - ret["members"] = members if members else [] + ret["members"] = members return 200, ret diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 128f13094331..734b3fb77f71 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -98,52 +98,34 @@ def get_room(self, room_id): allow_none=True, ) - def get_room_stats_state(self, room_id): - """Retrieve room stats state. + def get_room_with_stats(self, room_id: str): + """Retrieve room with statistics. Args: - room_id (str): The ID of the room to retrieve. + room_id: The ID of the room to retrieve. Returns: A dict containing the room information, or None if the room is unknown. """ - return self.db.simple_select_one( - table="room_stats_state", - keyvalues={"room_id": room_id}, - retcols=( - "room_id", - "name", - "canonical_alias", - "encryption", - "is_federatable", - "join_rules", - "guest_access", - "history_visibility", - "topic", - ), - desc="get_room_stats_state", - allow_none=True, - ) - def get_room_stats_current(self, room_id): - """Retrieve current room stats. + def get_room_with_stats_txn(txn, room_id): + sql = """ + SELECT room_id, state.name, state.canonical_alias, curr.joined_members, + curr.local_users_in_room AS joined_local_members, rooms.room_version AS version, + rooms.creator, state.encryption, state.is_federatable AS federatable, + rooms.is_public AS public, state.join_rules, state.guest_access, + state.history_visibility, curr.current_state_events AS state_events + FROM rooms + LEFT JOIN room_stats_state state USING (room_id) + LEFT JOIN room_stats_current curr USING (room_id) + WHERE room_id = ? + """ + txn.execute(sql, [room_id]) + res = self.db.cursor_to_dict(txn)[0] + res["federatable"] = bool(res["federatable"]) + res["public"] = bool(res["public"]) + return res - Args: - room_id (str): The ID of the room to retrieve. - Returns: - A dict containing the room information, or None if the room is unknown. - """ - return self.db.simple_select_one( - table="room_stats_state", - keyvalues={"room_id": room_id}, - retcols=( - "room_id", - "joined_members", - "local_users_in_room", - "current_state_events", - ), - desc="get_room_stats_current", - allow_none=True, - ) + return self.db.runInteraction("get_room_with_stats", get_room_with_stats_txn, room_id) def get_public_room_ids(self): return self.db.simple_select_onecol( diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 249c93722f47..54cd24bf645d 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -701,6 +701,47 @@ def _search_test( _search_test(None, "bar") _search_test(None, "", expected_http_code=400) + def test_single_room(self): + """Test that a single room can be requested correctly""" + # Create two test rooms + room_id_1 = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + room_id_2 = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok) + + room_name_1 = "something" + room_name_2 = "else" + + # Set the name for each room + self.helper.send_state( + room_id_1, "m.room.name", {"name": room_name_1}, tok=self.admin_user_tok, + ) + self.helper.send_state( + room_id_2, "m.room.name", {"name": room_name_2}, tok=self.admin_user_tok, + ) + + url = "/_synapse/admin/v1/rooms/%s" % (room_id_1,) + request, channel = self.make_request( + "GET", url.encode("ascii"), access_token=self.admin_user_tok, + ) + self.render(request) + self.assertEqual(200, channel.code, msg=channel.json_body) + + self.assertIn("room_id", channel.json_body) + self.assertIn("name", channel.json_body) + self.assertIn("canonical_alias", channel.json_body) + self.assertIn("joined_members", channel.json_body) + self.assertIn("joined_local_members", channel.json_body) + self.assertIn("version", channel.json_body) + self.assertIn("creator", channel.json_body) + self.assertIn("encryption", channel.json_body) + self.assertIn("federatable", channel.json_body) + self.assertIn("public", channel.json_body) + self.assertIn("join_rules", channel.json_body) + self.assertIn("guest_access", channel.json_body) + self.assertIn("history_visibility", channel.json_body) + self.assertIn("state_events", channel.json_body) + + self.assertEqual(room_id_1, channel.json_body["room_id"]) + class JoinAliasRoomTestCase(unittest.HomeserverTestCase): diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py index 086adeb8fd39..3b78d488965b 100644 --- a/tests/storage/test_room.py +++ b/tests/storage/test_room.py @@ -55,6 +55,17 @@ def test_get_room(self): (yield self.store.get_room(self.room.to_string())), ) + @defer.inlineCallbacks + def test_get_room_with_stats(self): + self.assertDictContainsSubset( + { + "room_id": self.room.to_string(), + "creator": self.u_creator.to_string(), + "public": True, + }, + (yield self.store.get_room_with_stats(self.room.to_string())), + ) + class RoomEventsStoreTestCase(unittest.TestCase): @defer.inlineCallbacks From 0b14e50c0f283d0564af3ed53e1aadd0fffa4188 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 5 May 2020 13:58:00 +0200 Subject: [PATCH 5/8] Remove members from room endpoint --- docs/admin_api/rooms.md | 2 -- synapse/rest/admin/rooms.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 0c5a28fd72bc..32a6b1b89696 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -288,7 +288,6 @@ The following fields are possible in the JSON response body: * `guest_access` - Whether guests can join the room. One of: ["can_join", "forbidden"]. * `history_visibility` - Who can see the room history. One of: ["invited", "joined", "shared", "world_readable"]. * `state_events` - Total number of state_events of a room. Complexity of the room. -* `members` - An array of user ids. ## Usage @@ -318,6 +317,5 @@ Response: "guest_access": null, "history_visibility": "shared", "state_events": 93534 - "members": ["@foo:matrix.org", "@bar:matrix.org"] } ``` diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 608e967c86e2..ea80f71e74d3 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -275,9 +275,6 @@ async def on_GET(self, request, room_id): if not ret: raise NotFoundError("Room not found") - members = await self.store.get_users_in_room(room_id) - ret["members"] = members - return 200, ret From c55d802a0d863dd68df6ccb290c00903b9b4fb18 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 5 May 2020 14:07:34 +0200 Subject: [PATCH 6/8] Fixes --- synapse/rest/admin/rooms.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index ea80f71e74d3..7d4000198866 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -26,6 +26,7 @@ ) from synapse.rest.admin._base import ( admin_patterns, + assert_requester_is_admin, assert_user_is_admin, historical_admin_path_patterns, ) @@ -256,7 +257,6 @@ async def on_GET(self, request): class RoomRestServlet(RestServlet): """Get room details. - TODO: Return power level for each user TODO: Add on_POST to allow room creation without joining the room """ @@ -268,8 +268,7 @@ def __init__(self, hs): self.store = hs.get_datastore() async def on_GET(self, request, room_id): - requester = await self.auth.get_user_by_req(request) - await assert_user_is_admin(self.auth, requester.user) + await assert_requester_is_admin(self.auth, request) ret = await self.store.get_room_with_stats(room_id) if not ret: From 172c566b008e6d5afc31d02eb8b1cc4ee8f87f18 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 5 May 2020 16:16:33 +0200 Subject: [PATCH 7/8] Fix coding style --- synapse/storage/data_stores/main/room.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py index 734b3fb77f71..cafa664c1632 100644 --- a/synapse/storage/data_stores/main/room.py +++ b/synapse/storage/data_stores/main/room.py @@ -125,7 +125,9 @@ def get_room_with_stats_txn(txn, room_id): res["public"] = bool(res["public"]) return res - return self.db.runInteraction("get_room_with_stats", get_room_with_stats_txn, room_id) + return self.db.runInteraction( + "get_room_with_stats", get_room_with_stats_txn, room_id + ) def get_public_room_ids(self): return self.db.simple_select_onecol( From b118fb9570d42eec2ec92e818f17f313cc3ff90e Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Thu, 7 May 2020 10:14:35 +0200 Subject: [PATCH 8/8] Fix rooms.md --- docs/admin_api/rooms.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 32a6b1b89696..624e7745baa7 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -267,8 +267,7 @@ end of the list. # DRAFT: Room Details API -The Room Details admin API allows server admins to get all details of a room, -including a list of all room members. +The Room Details admin API allows server admins to get all details of a room. This API is still a draft and details might change!