From e34fd1228df8f20ae56896c3cbf2b15efcfaa06a Mon Sep 17 00:00:00 2001
From: Shay <hillerys@element.io>
Date: Wed, 8 Jan 2025 07:38:26 -0800
Subject: [PATCH] Add the ability to filter by state event type on admin room
 state endpoint (#18035)

Adds a query param `type` to `/_synapse/admin/v1/rooms/{room_id}/state`
that filters the state event query by state event type.

---------

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
---
 changelog.d/18035.feature     |  1 +
 docs/admin_api/rooms.md       |  7 ++++++
 synapse/rest/admin/rooms.py   | 14 ++++++++++-
 tests/rest/admin/test_room.py | 46 +++++++++++++++++++++++++++++++++++
 4 files changed, 67 insertions(+), 1 deletion(-)
 create mode 100644 changelog.d/18035.feature

diff --git a/changelog.d/18035.feature b/changelog.d/18035.feature
new file mode 100644
index 0000000000..99b68a9e45
--- /dev/null
+++ b/changelog.d/18035.feature
@@ -0,0 +1 @@
+Add a unit test for the `type` parameter of the [Room State Admin API](https://element-hq.github.io/synapse/develop/admin_api/rooms.html#room-state-api).
\ No newline at end of file
diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md
index 8e3a367e90..bfc2cd4376 100644
--- a/docs/admin_api/rooms.md
+++ b/docs/admin_api/rooms.md
@@ -385,6 +385,13 @@ The API is:
 GET /_synapse/admin/v1/rooms/<room_id>/state
 ```
 
+**Parameters**
+
+The following query parameter is available:
+
+* `type` - The type of room state event to filter by, eg "m.room.create". If provided, only state events
+    of this type will be returned (regardless of their `state_key` value).
+
 A response body like the following is returned:
 
 ```json
diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py
index 01f9de9ffa..3097cb1a9d 100644
--- a/synapse/rest/admin/rooms.py
+++ b/synapse/rest/admin/rooms.py
@@ -23,6 +23,7 @@ from http import HTTPStatus
 from typing import TYPE_CHECKING, List, Optional, Tuple, cast
 
 import attr
+from immutabledict import immutabledict
 
 from synapse.api.constants import Direction, EventTypes, JoinRules, Membership
 from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
@@ -463,7 +464,18 @@ class RoomStateRestServlet(RestServlet):
         if not room:
             raise NotFoundError("Room not found")
 
-        event_ids = await self._storage_controllers.state.get_current_state_ids(room_id)
+        state_filter = None
+        type = parse_string(request, "type")
+
+        if type:
+            state_filter = StateFilter(
+                types=immutabledict({type: None}),
+                include_others=False,
+            )
+
+        event_ids = await self._storage_controllers.state.get_current_state_ids(
+            room_id, state_filter
+        )
         events = await self.store.get_events(event_ids.values())
         now = self.clock.time_msec()
         room_state = await self._event_serializer.serialize_events(events.values(), now)
diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py
index 99df591529..1817d67a00 100644
--- a/tests/rest/admin/test_room.py
+++ b/tests/rest/admin/test_room.py
@@ -2035,6 +2035,52 @@ class RoomTestCase(unittest.HomeserverTestCase):
         # the create_room already does the right thing, so no need to verify that we got
         # the state events it created.
 
+    def test_room_state_param(self) -> None:
+        """Test that filtering by state event type works when requesting state"""
+        room_id = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
+
+        channel = self.make_request(
+            "GET",
+            f"/_synapse/admin/v1/rooms/{room_id}/state?type=m.room.member",
+            access_token=self.admin_user_tok,
+        )
+        self.assertEqual(200, channel.code)
+        state = channel.json_body["state"]
+        # only one member has joined so there should be one membership event
+        self.assertEqual(1, len(state))
+        event = state[0]
+        self.assertEqual(event["type"], "m.room.member")
+        self.assertEqual(event["state_key"], self.admin_user)
+
+    def test_room_state_param_empty(self) -> None:
+        """Test that passing an empty string as state filter param returns no state events"""
+        room_id = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
+
+        channel = self.make_request(
+            "GET",
+            f"/_synapse/admin/v1/rooms/{room_id}/state?type=",
+            access_token=self.admin_user_tok,
+        )
+        self.assertEqual(200, channel.code)
+        state = channel.json_body["state"]
+        self.assertEqual(5, len(state))
+
+    def test_room_state_param_not_in_room(self) -> None:
+        """
+        Test that passing a state filter param for a state event not in the room
+        returns no state events
+        """
+        room_id = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
+
+        channel = self.make_request(
+            "GET",
+            f"/_synapse/admin/v1/rooms/{room_id}/state?type=m.room.custom",
+            access_token=self.admin_user_tok,
+        )
+        self.assertEqual(200, channel.code)
+        state = channel.json_body["state"]
+        self.assertEqual(0, len(state))
+
     def _set_canonical_alias(
         self, room_id: str, test_alias: str, admin_user_tok: str
     ) -> None:
-- 
GitLab