Filter added to Admin-API GET /rooms (#17276)

This commit is contained in:
Alexander Fechler 2024-06-19 12:45:48 +02:00 committed by GitHub
parent a412a5829d
commit 9104a9f0d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 131 additions and 15 deletions

View File

@ -0,0 +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).

View File

@ -36,6 +36,10 @@ The following query parameters are available:
- the room's name, - the room's name,
- the local part of the room's canonical alias, or - the local part of the room's canonical alias, or
- the complete (local and server part) room's id (case sensitive). - the complete (local and server part) room's id (case sensitive).
* `public_rooms` - Optional flag to filter public rooms. If `true`, only public rooms are queried. If `false`, public rooms are excluded from
the query. When the flag is absent (the default), **both** public and non-public rooms are included in the search results.
* `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
the query. When the flag is absent (the default), **both** empty and non-empty rooms are included in the search results.
Defaults to no filtering. Defaults to no filtering.

View File

@ -35,6 +35,7 @@ from synapse.http.servlet import (
ResolveRoomIdMixin, ResolveRoomIdMixin,
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
parse_boolean,
parse_enum, parse_enum,
parse_integer, parse_integer,
parse_json, parse_json,
@ -242,13 +243,23 @@ class ListRoomRestServlet(RestServlet):
errcode=Codes.INVALID_PARAM, errcode=Codes.INVALID_PARAM,
) )
public_rooms = parse_boolean(request, "public_rooms")
empty_rooms = parse_boolean(request, "empty_rooms")
direction = parse_enum(request, "dir", Direction, default=Direction.FORWARDS) direction = parse_enum(request, "dir", Direction, default=Direction.FORWARDS)
reverse_order = True if direction == Direction.BACKWARDS else False reverse_order = True if direction == Direction.BACKWARDS else False
# Return list of rooms according to parameters # Return list of rooms according to parameters
rooms, total_rooms = await self.store.get_rooms_paginate( rooms, total_rooms = await self.store.get_rooms_paginate(
start, limit, order_by, reverse_order, search_term start,
limit,
order_by,
reverse_order,
search_term,
public_rooms,
empty_rooms,
) )
response = { response = {
# next_token should be opaque, so return a value the client can parse # next_token should be opaque, so return a value the client can parse
"offset": start, "offset": start,

View File

@ -606,6 +606,8 @@ class RoomWorkerStore(CacheInvalidationWorkerStore):
order_by: str, order_by: str,
reverse_order: bool, reverse_order: bool,
search_term: Optional[str], search_term: Optional[str],
public_rooms: Optional[bool],
empty_rooms: Optional[bool],
) -> Tuple[List[Dict[str, Any]], int]: ) -> Tuple[List[Dict[str, Any]], int]:
"""Function to retrieve a paginated list of rooms as json. """Function to retrieve a paginated list of rooms as json.
@ -617,30 +619,49 @@ class RoomWorkerStore(CacheInvalidationWorkerStore):
search_term: a string to filter room names, search_term: a string to filter room names,
canonical alias and room ids by. canonical alias and room ids by.
Room ID must match exactly. Canonical alias must match a substring of the local part. Room ID must match exactly. Canonical alias must match a substring of the local part.
public_rooms: Optional flag to filter public and non-public rooms. If true, public rooms are queried.
if false, public rooms are excluded from the query. When it is
none (the default), both public rooms and none-public-rooms are queried.
empty_rooms: Optional flag to filter empty and non-empty rooms.
A room is empty if joined_members is zero.
If true, empty rooms are queried.
if false, empty rooms are excluded from the query. When it is
none (the default), both empty rooms and none-empty rooms are queried.
Returns: Returns:
A list of room dicts and an integer representing the total number of A list of room dicts and an integer representing the total number of
rooms that exist given this query rooms that exist given this query
""" """
# Filter room names by a string # Filter room names by a string
where_statement = "" filter_ = []
search_pattern: List[object] = [] where_args = []
if search_term: if search_term:
where_statement = """ filter_ = [
WHERE LOWER(state.name) LIKE ? "LOWER(state.name) LIKE ? OR "
OR LOWER(state.canonical_alias) LIKE ? "LOWER(state.canonical_alias) LIKE ? OR "
OR state.room_id = ? "state.room_id = ?"
""" ]
# Our postgres db driver converts ? -> %s in SQL strings as that's the # Our postgres db driver converts ? -> %s in SQL strings as that's the
# placeholder for postgres. # placeholder for postgres.
# HOWEVER, if you put a % into your SQL then everything goes wibbly. # HOWEVER, if you put a % into your SQL then everything goes wibbly.
# To get around this, we're going to surround search_term with %'s # To get around this, we're going to surround search_term with %'s
# before giving it to the database in python instead # before giving it to the database in python instead
search_pattern = [ where_args = [
"%" + search_term.lower() + "%", f"%{search_term.lower()}%",
"#%" + search_term.lower() + "%:%", f"#%{search_term.lower()}%:%",
search_term, search_term,
] ]
if public_rooms is not None:
filter_arg = "1" if public_rooms else "0"
filter_.append(f"rooms.is_public = '{filter_arg}'")
if empty_rooms is not None:
if empty_rooms:
filter_.append("curr.joined_members = 0")
else:
filter_.append("curr.joined_members <> 0")
where_clause = "WHERE " + " AND ".join(filter_) if len(filter_) > 0 else ""
# Set ordering # Set ordering
if RoomSortOrder(order_by) == RoomSortOrder.SIZE: if RoomSortOrder(order_by) == RoomSortOrder.SIZE:
@ -717,7 +738,7 @@ class RoomWorkerStore(CacheInvalidationWorkerStore):
LIMIT ? LIMIT ?
OFFSET ? OFFSET ?
""".format( """.format(
where=where_statement, where=where_clause,
order_by=order_by_column, order_by=order_by_column,
direction="ASC" if order_by_asc else "DESC", direction="ASC" if order_by_asc else "DESC",
) )
@ -726,10 +747,12 @@ class RoomWorkerStore(CacheInvalidationWorkerStore):
count_sql = """ count_sql = """
SELECT count(*) FROM ( SELECT count(*) FROM (
SELECT room_id FROM room_stats_state state SELECT room_id FROM room_stats_state state
INNER JOIN room_stats_current curr USING (room_id)
INNER JOIN rooms USING (room_id)
{where} {where}
) AS get_room_ids ) AS get_room_ids
""".format( """.format(
where=where_statement, where=where_clause,
) )
def _get_rooms_paginate_txn( def _get_rooms_paginate_txn(
@ -737,7 +760,7 @@ class RoomWorkerStore(CacheInvalidationWorkerStore):
) -> Tuple[List[Dict[str, Any]], int]: ) -> Tuple[List[Dict[str, Any]], int]:
# Add the search term into the WHERE clause # Add the search term into the WHERE clause
# and execute the data query # and execute the data query
txn.execute(info_sql, search_pattern + [limit, start]) txn.execute(info_sql, where_args + [limit, start])
# Refactor room query data into a structured dictionary # Refactor room query data into a structured dictionary
rooms = [] rooms = []
@ -767,7 +790,7 @@ class RoomWorkerStore(CacheInvalidationWorkerStore):
# Execute the count query # Execute the count query
# Add the search term into the WHERE clause if present # Add the search term into the WHERE clause if present
txn.execute(count_sql, search_pattern) txn.execute(count_sql, where_args)
room_count = cast(Tuple[int], txn.fetchone()) room_count = cast(Tuple[int], txn.fetchone())
return rooms, room_count[0] return rooms, room_count[0]

View File

@ -1795,6 +1795,83 @@ class RoomTestCase(unittest.HomeserverTestCase):
self.assertEqual(room_id, channel.json_body["rooms"][0].get("room_id")) self.assertEqual(room_id, channel.json_body["rooms"][0].get("room_id"))
self.assertEqual("ж", channel.json_body["rooms"][0].get("name")) self.assertEqual("ж", channel.json_body["rooms"][0].get("name"))
def test_filter_public_rooms(self) -> None:
self.helper.create_room_as(
self.admin_user, tok=self.admin_user_tok, is_public=True
)
self.helper.create_room_as(
self.admin_user, tok=self.admin_user_tok, is_public=True
)
self.helper.create_room_as(
self.admin_user, tok=self.admin_user_tok, is_public=False
)
response = self.make_request(
"GET",
"/_synapse/admin/v1/rooms",
access_token=self.admin_user_tok,
)
self.assertEqual(200, response.code, msg=response.json_body)
self.assertEqual(3, response.json_body["total_rooms"])
self.assertEqual(3, len(response.json_body["rooms"]))
response = self.make_request(
"GET",
"/_synapse/admin/v1/rooms?public_rooms=true",
access_token=self.admin_user_tok,
)
self.assertEqual(200, response.code, msg=response.json_body)
self.assertEqual(2, response.json_body["total_rooms"])
self.assertEqual(2, len(response.json_body["rooms"]))
response = self.make_request(
"GET",
"/_synapse/admin/v1/rooms?public_rooms=false",
access_token=self.admin_user_tok,
)
self.assertEqual(200, response.code, msg=response.json_body)
self.assertEqual(1, response.json_body["total_rooms"])
self.assertEqual(1, len(response.json_body["rooms"]))
def test_filter_empty_rooms(self) -> None:
self.helper.create_room_as(
self.admin_user, tok=self.admin_user_tok, is_public=True
)
self.helper.create_room_as(
self.admin_user, tok=self.admin_user_tok, is_public=True
)
room_id = self.helper.create_room_as(
self.admin_user, tok=self.admin_user_tok, is_public=False
)
self.helper.leave(room_id, self.admin_user, tok=self.admin_user_tok)
response = self.make_request(
"GET",
"/_synapse/admin/v1/rooms",
access_token=self.admin_user_tok,
)
self.assertEqual(200, response.code, msg=response.json_body)
self.assertEqual(3, response.json_body["total_rooms"])
self.assertEqual(3, len(response.json_body["rooms"]))
response = self.make_request(
"GET",
"/_synapse/admin/v1/rooms?empty_rooms=false",
access_token=self.admin_user_tok,
)
self.assertEqual(200, response.code, msg=response.json_body)
self.assertEqual(2, response.json_body["total_rooms"])
self.assertEqual(2, len(response.json_body["rooms"]))
response = self.make_request(
"GET",
"/_synapse/admin/v1/rooms?empty_rooms=true",
access_token=self.admin_user_tok,
)
self.assertEqual(200, response.code, msg=response.json_body)
self.assertEqual(1, response.json_body["total_rooms"])
self.assertEqual(1, len(response.json_body["rooms"]))
def test_single_room(self) -> None: def test_single_room(self) -> None:
"""Test that a single room can be requested correctly""" """Test that a single room can be requested correctly"""
# Create two test rooms # Create two test rooms