diff --git a/changelog.d/18040.feature b/changelog.d/18040.feature new file mode 100644 index 00000000000..76c41cd08f0 --- /dev/null +++ b/changelog.d/18040.feature @@ -0,0 +1 @@ +Add a query param `participant` to `/members` admin api to filter for room participants. \ No newline at end of file diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 8e3a367e90a..442ad3b15ec 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -358,6 +358,13 @@ The API is: GET /_synapse/admin/v1/rooms//members ``` +**Parameters** + +The following query parameters are available: + +* `participant` - Optional. If provided and set to true, only returns currently joined members who have + also posted to the room. Defaults to false. + A response body like the following is returned: ```json diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 01f9de9ffa5..794c8118720 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -434,7 +434,12 @@ async def on_GET( if not room: raise NotFoundError("Room not found") - members = await self.store.get_users_in_room(room_id) + participant = parse_boolean(request, "participant", False) + + if participant: + members = await self.store.get_participants_in_room(room_id) + else: + members = await self.store.get_users_in_room(room_id) ret = {"members": members, "total": len(members)} return HTTPStatus.OK, ret diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 50ed6a28bf0..d386dd8499a 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -1606,6 +1606,34 @@ def _get_rooms_for_user_by_join_date_txn( from_ts, ) + async def get_participants_in_room(self, room_id: str) -> Sequence[str]: + """ + Return a list of all currently joined room members who have posted + in the given room. + + Args: + room_id: room ID of the room to search in + """ + + def _get_participants_in_room_txn( + txn: LoggingTransaction, room_id: str + ) -> List[str]: + sql = """ + SELECT DISTINCT c.state_key + FROM current_state_events AS c + INNER JOIN events AS e USING(room_id) + WHERE room_id = ? + AND c.membership = 'join' + AND e.type = 'm.room.message' + AND c.state_key = e.sender + """ + txn.execute(sql, (room_id,)) + return [r[0] for r in txn] + + return await self.db_pool.runInteraction( + "_get_participants_in_room_txn", _get_participants_in_room_txn, room_id + ) + class RoomMemberBackgroundUpdateStore(SQLBaseStore): def __init__( diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 95ed7364510..617c15071a3 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -2018,6 +2018,82 @@ def test_room_members(self) -> None: ) self.assertEqual(channel.json_body["total"], 3) + def test_room_members_participants_param(self) -> None: + """ + Test that /_synapse/admin/v1/rooms/{room_id}/members with a query param value of + participant=true returns a list of currently joined users who have posted, and + that the list does not contain room members who have not posted + """ + + participant = self.register_user("participant", "pass") + participant_tok = self.login("participant", "pass") + room_id = self.helper.create_room_as(participant, tok=participant_tok) + + participant2 = self.register_user("participant2", "pass") + participant2_tok = self.login("participant2", "pass") + self.helper.join(room_id, participant2, tok=participant2_tok) + + # have some lurkers join the room + for i in range(5): + user = self.register_user(f"user{i}", "pass") + user_tok = self.login(f"user{i}", "pass") + self.helper.join(room_id, user, tok=user_tok) + + # double-check the room membership total, should be 7 + channel = self.make_request( + "GET", + f"/_synapse/admin/v1/rooms/{room_id}/members", + access_token=self.admin_user_tok, + ) + self.assertEqual(200, channel.code) + self.assertEqual(channel.json_body["total"], 7) + + # participants send messages + self.helper.send_event( + room_id, + "m.room.message", + content={ + "msgtype": "m.text", + "body": "nefarious activity", + }, + tok=participant_tok, + ) + + self.helper.send_event( + room_id, + "m.room.message", + content={ + "msgtype": "m.text", + "body": "more nefarious activity", + }, + tok=participant2_tok, + ) + + channel = self.make_request( + "GET", + f"/_synapse/admin/v1/rooms/{room_id}/members?participant=true", + access_token=self.admin_user_tok, + ) + self.assertEqual(200, channel.code) + participants = channel.json_body["members"] + self.assertEqual(channel.json_body["total"], 2) + self.assertIn(participant, participants) + self.assertIn(participant2, participants) + + # have participants leave room + self.helper.leave(room_id, participant, tok=participant_tok) + self.helper.leave(room_id, participant2, tok=participant2_tok) + + # there should no longer be any active participants + channel = self.make_request( + "GET", + f"/_synapse/admin/v1/rooms/{room_id}/members?participant=true", + access_token=self.admin_user_tok, + ) + self.assertEqual(200, channel.code) + participants = channel.json_body["total"] + self.assertEqual(participants, 0) + def test_room_state(self) -> None: """Test that room state can be requested correctly""" # Create two test rooms