Skip to content

Commit 9104a9f

Browse files
authoredJun 19, 2024··
Filter added to Admin-API GET /rooms (#17276)
1 parent a412a58 commit 9104a9f

File tree

5 files changed

+131
-15
lines changed

5 files changed

+131
-15
lines changed
 

‎changelog.d/17276.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Filter for public and empty rooms added to Admin-API [List Room API](https://element-hq.github.io/synapse/latest/admin_api/rooms.html#list-room-api).

‎docs/admin_api/rooms.md

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ The following query parameters are available:
3636
- the room's name,
3737
- the local part of the room's canonical alias, or
3838
- the complete (local and server part) room's id (case sensitive).
39+
* `public_rooms` - Optional flag to filter public rooms. If `true`, only public rooms are queried. If `false`, public rooms are excluded from
40+
the query. When the flag is absent (the default), **both** public and non-public rooms are included in the search results.
41+
* `empty_rooms` - Optional flag to filter empty rooms. A room is empty if joined_members is zero. If `true`, only empty rooms are queried. If `false`, empty rooms are excluded from
42+
the query. When the flag is absent (the default), **both** empty and non-empty rooms are included in the search results.
3943

4044
Defaults to no filtering.
4145

‎synapse/rest/admin/rooms.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
ResolveRoomIdMixin,
3636
RestServlet,
3737
assert_params_in_dict,
38+
parse_boolean,
3839
parse_enum,
3940
parse_integer,
4041
parse_json,
@@ -242,13 +243,23 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
242243
errcode=Codes.INVALID_PARAM,
243244
)
244245

246+
public_rooms = parse_boolean(request, "public_rooms")
247+
empty_rooms = parse_boolean(request, "empty_rooms")
248+
245249
direction = parse_enum(request, "dir", Direction, default=Direction.FORWARDS)
246250
reverse_order = True if direction == Direction.BACKWARDS else False
247251

248252
# Return list of rooms according to parameters
249253
rooms, total_rooms = await self.store.get_rooms_paginate(
250-
start, limit, order_by, reverse_order, search_term
254+
start,
255+
limit,
256+
order_by,
257+
reverse_order,
258+
search_term,
259+
public_rooms,
260+
empty_rooms,
251261
)
262+
252263
response = {
253264
# next_token should be opaque, so return a value the client can parse
254265
"offset": start,

‎synapse/storage/databases/main/room.py

+37-14
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,8 @@ async def get_rooms_paginate(
606606
order_by: str,
607607
reverse_order: bool,
608608
search_term: Optional[str],
609+
public_rooms: Optional[bool],
610+
empty_rooms: Optional[bool],
609611
) -> Tuple[List[Dict[str, Any]], int]:
610612
"""Function to retrieve a paginated list of rooms as json.
611613
@@ -617,30 +619,49 @@ async def get_rooms_paginate(
617619
search_term: a string to filter room names,
618620
canonical alias and room ids by.
619621
Room ID must match exactly. Canonical alias must match a substring of the local part.
622+
public_rooms: Optional flag to filter public and non-public rooms. If true, public rooms are queried.
623+
if false, public rooms are excluded from the query. When it is
624+
none (the default), both public rooms and none-public-rooms are queried.
625+
empty_rooms: Optional flag to filter empty and non-empty rooms.
626+
A room is empty if joined_members is zero.
627+
If true, empty rooms are queried.
628+
if false, empty rooms are excluded from the query. When it is
629+
none (the default), both empty rooms and none-empty rooms are queried.
620630
Returns:
621631
A list of room dicts and an integer representing the total number of
622632
rooms that exist given this query
623633
"""
624634
# Filter room names by a string
625-
where_statement = ""
626-
search_pattern: List[object] = []
635+
filter_ = []
636+
where_args = []
627637
if search_term:
628-
where_statement = """
629-
WHERE LOWER(state.name) LIKE ?
630-
OR LOWER(state.canonical_alias) LIKE ?
631-
OR state.room_id = ?
632-
"""
638+
filter_ = [
639+
"LOWER(state.name) LIKE ? OR "
640+
"LOWER(state.canonical_alias) LIKE ? OR "
641+
"state.room_id = ?"
642+
]
633643

634644
# Our postgres db driver converts ? -> %s in SQL strings as that's the
635645
# placeholder for postgres.
636646
# HOWEVER, if you put a % into your SQL then everything goes wibbly.
637647
# To get around this, we're going to surround search_term with %'s
638648
# before giving it to the database in python instead
639-
search_pattern = [
640-
"%" + search_term.lower() + "%",
641-
"#%" + search_term.lower() + "%:%",
649+
where_args = [
650+
f"%{search_term.lower()}%",
651+
f"#%{search_term.lower()}%:%",
642652
search_term,
643653
]
654+
if public_rooms is not None:
655+
filter_arg = "1" if public_rooms else "0"
656+
filter_.append(f"rooms.is_public = '{filter_arg}'")
657+
658+
if empty_rooms is not None:
659+
if empty_rooms:
660+
filter_.append("curr.joined_members = 0")
661+
else:
662+
filter_.append("curr.joined_members <> 0")
663+
664+
where_clause = "WHERE " + " AND ".join(filter_) if len(filter_) > 0 else ""
644665

645666
# Set ordering
646667
if RoomSortOrder(order_by) == RoomSortOrder.SIZE:
@@ -717,7 +738,7 @@ async def get_rooms_paginate(
717738
LIMIT ?
718739
OFFSET ?
719740
""".format(
720-
where=where_statement,
741+
where=where_clause,
721742
order_by=order_by_column,
722743
direction="ASC" if order_by_asc else "DESC",
723744
)
@@ -726,18 +747,20 @@ async def get_rooms_paginate(
726747
count_sql = """
727748
SELECT count(*) FROM (
728749
SELECT room_id FROM room_stats_state state
750+
INNER JOIN room_stats_current curr USING (room_id)
751+
INNER JOIN rooms USING (room_id)
729752
{where}
730753
) AS get_room_ids
731754
""".format(
732-
where=where_statement,
755+
where=where_clause,
733756
)
734757

735758
def _get_rooms_paginate_txn(
736759
txn: LoggingTransaction,
737760
) -> Tuple[List[Dict[str, Any]], int]:
738761
# Add the search term into the WHERE clause
739762
# and execute the data query
740-
txn.execute(info_sql, search_pattern + [limit, start])
763+
txn.execute(info_sql, where_args + [limit, start])
741764

742765
# Refactor room query data into a structured dictionary
743766
rooms = []
@@ -767,7 +790,7 @@ def _get_rooms_paginate_txn(
767790
# Execute the count query
768791

769792
# Add the search term into the WHERE clause if present
770-
txn.execute(count_sql, search_pattern)
793+
txn.execute(count_sql, where_args)
771794

772795
room_count = cast(Tuple[int], txn.fetchone())
773796
return rooms, room_count[0]

‎tests/rest/admin/test_room.py

+77
Original file line numberDiff line numberDiff line change
@@ -1795,6 +1795,83 @@ def test_search_term_non_ascii(self) -> None:
17951795
self.assertEqual(room_id, channel.json_body["rooms"][0].get("room_id"))
17961796
self.assertEqual("ж", channel.json_body["rooms"][0].get("name"))
17971797

1798+
def test_filter_public_rooms(self) -> None:
1799+
self.helper.create_room_as(
1800+
self.admin_user, tok=self.admin_user_tok, is_public=True
1801+
)
1802+
self.helper.create_room_as(
1803+
self.admin_user, tok=self.admin_user_tok, is_public=True
1804+
)
1805+
self.helper.create_room_as(
1806+
self.admin_user, tok=self.admin_user_tok, is_public=False
1807+
)
1808+
1809+
response = self.make_request(
1810+
"GET",
1811+
"/_synapse/admin/v1/rooms",
1812+
access_token=self.admin_user_tok,
1813+
)
1814+
self.assertEqual(200, response.code, msg=response.json_body)
1815+
self.assertEqual(3, response.json_body["total_rooms"])
1816+
self.assertEqual(3, len(response.json_body["rooms"]))
1817+
1818+
response = self.make_request(
1819+
"GET",
1820+
"/_synapse/admin/v1/rooms?public_rooms=true",
1821+
access_token=self.admin_user_tok,
1822+
)
1823+
self.assertEqual(200, response.code, msg=response.json_body)
1824+
self.assertEqual(2, response.json_body["total_rooms"])
1825+
self.assertEqual(2, len(response.json_body["rooms"]))
1826+
1827+
response = self.make_request(
1828+
"GET",
1829+
"/_synapse/admin/v1/rooms?public_rooms=false",
1830+
access_token=self.admin_user_tok,
1831+
)
1832+
self.assertEqual(200, response.code, msg=response.json_body)
1833+
self.assertEqual(1, response.json_body["total_rooms"])
1834+
self.assertEqual(1, len(response.json_body["rooms"]))
1835+
1836+
def test_filter_empty_rooms(self) -> None:
1837+
self.helper.create_room_as(
1838+
self.admin_user, tok=self.admin_user_tok, is_public=True
1839+
)
1840+
self.helper.create_room_as(
1841+
self.admin_user, tok=self.admin_user_tok, is_public=True
1842+
)
1843+
room_id = self.helper.create_room_as(
1844+
self.admin_user, tok=self.admin_user_tok, is_public=False
1845+
)
1846+
self.helper.leave(room_id, self.admin_user, tok=self.admin_user_tok)
1847+
1848+
response = self.make_request(
1849+
"GET",
1850+
"/_synapse/admin/v1/rooms",
1851+
access_token=self.admin_user_tok,
1852+
)
1853+
self.assertEqual(200, response.code, msg=response.json_body)
1854+
self.assertEqual(3, response.json_body["total_rooms"])
1855+
self.assertEqual(3, len(response.json_body["rooms"]))
1856+
1857+
response = self.make_request(
1858+
"GET",
1859+
"/_synapse/admin/v1/rooms?empty_rooms=false",
1860+
access_token=self.admin_user_tok,
1861+
)
1862+
self.assertEqual(200, response.code, msg=response.json_body)
1863+
self.assertEqual(2, response.json_body["total_rooms"])
1864+
self.assertEqual(2, len(response.json_body["rooms"]))
1865+
1866+
response = self.make_request(
1867+
"GET",
1868+
"/_synapse/admin/v1/rooms?empty_rooms=true",
1869+
access_token=self.admin_user_tok,
1870+
)
1871+
self.assertEqual(200, response.code, msg=response.json_body)
1872+
self.assertEqual(1, response.json_body["total_rooms"])
1873+
self.assertEqual(1, len(response.json_body["rooms"]))
1874+
17981875
def test_single_room(self) -> None:
17991876
"""Test that a single room can be requested correctly"""
18001877
# Create two test rooms

0 commit comments

Comments
 (0)
Please sign in to comment.