Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/19021.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add an [Admin API](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/index.html) to allow an admin
to search for room details about the parent space(s) (if any) of each room in a provided set of rooms, as well as any
other children of the found parent spaces.
53 changes: 53 additions & 0 deletions docs/admin_api/rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -1115,3 +1115,56 @@ Example response:
]
}
```

# Parent Space Search API
Comment thread
H-Shay marked this conversation as resolved.
Outdated

This API allows an admin to search for room details about the parent space(s) (if any) of each room in a provided set of rooms, as well as any other children of the found parent spaces. If no parent spaces are found, will return 404.

Request:

```
Comment thread
H-Shay marked this conversation as resolved.
Outdated
POST /_synapse/admin/v1/rooms/space/spaces_search
Comment thread
H-Shay marked this conversation as resolved.
Outdated
```

with a request body of:
```json
{
"rooms": ["!roomID1", "!roomID2"]
}
```

Response:

```json
{
"found_rooms":
[
{
"room_id": "!parentroomID",
"name": "parent space",
"join_rules": "public",
"is_space": true,
"topic": null,
"origin_server_ts": 234235,
"sender": "@user:somewhere.org",
"event_id": "$creationeventID",
"power_users": ["@user:somewhere.org"],
"deleted": false,
"aliases": []
},
{
"room_id": "!otherchildroomID",
"name": "other room in space",
"join_rules": "invite",
"is_space": false,
"topic": "dogs",
"origin_server_ts": 23523,
"sender": "@dogowner42:somewhere.org",
"event_id": "$wghhrhrehett",
"power_users": ["@dogowner42:somewhere.org", "@user:somewhere.org"],
"deleted": false,
"aliases": ["#hurrayfordogs:matrix.org"]
}
]
}
```
2 changes: 2 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
RoomRestV2Servlet,
RoomStateRestServlet,
RoomTimestampToEventRestServlet,
SpacesSearchServlet,
)
from synapse.rest.admin.scheduled_tasks import ScheduledTasksRestServlet
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
Expand Down Expand Up @@ -339,6 +340,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
ExperimentalFeaturesRestServlet(hs).register(http_server)
SuspendAccountRestServlet(hs).register(http_server)
ScheduledTasksRestServlet(hs).register(http_server)
SpacesSearchServlet(hs).register(http_server)


def register_servlets_for_client_rest_resource(
Expand Down
95 changes: 95 additions & 0 deletions synapse/rest/admin/rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,101 @@
logger = logging.getLogger(__name__)


class SpacesSearchServlet(RestServlet):
"""
For each room provided, get the details of all rooms which are parents to each room,
as well as any other rooms which are children of the found parent spaces
"""

PATTERNS = admin_patterns("/rooms/space/spaces_search")

def __init__(self, hs: "HomeServer"):
self._auth = hs.get_auth()
self._store = hs.get_datastores().main
self._storage_controllers = hs.get_storage_controllers()

async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
requester = await self._auth.get_user_by_req(request)
await assert_user_is_admin(self._auth, requester)

content = parse_json_object_from_request(request)
rooms = content.get("rooms")

if not rooms:
raise SynapseError(
HTTPStatus.BAD_REQUEST, "rooms must be provided", Codes.BAD_JSON
)
if not isinstance(rooms, list):
Comment thread
H-Shay marked this conversation as resolved.
Outdated
raise SynapseError(
HTTPStatus.BAD_REQUEST, "rooms must be a list", Codes.BAD_JSON
)

async def _fetch_room_details(room_id: str, is_space: bool) -> JsonDict:
details = await self._store.get_room_with_stats(room_id)
try:
creation_event = await self._store.get_create_event_for_room(room_id)
except NotFoundError:
# the room may have been deleted
creation_event = None
aliases = await self._store.get_aliases_for_room(room_id)

pl_event = await self._storage_controllers.state.get_current_state_event(
room_id, EventTypes.PowerLevels, ""
)
pl_users = []
if pl_event is not None:
users = pl_event.content.get("users", {})
for user, level in users.items():
if level >= 25:
pl_users.append(user)
Comment thread
MadLittleMods marked this conversation as resolved.
Outdated
if (
creation_event
and creation_event.room_version.msc4289_creator_power_enabled
):
additional_creators = creation_event.content.get(
"additional_creators", []
)
pl_users.extend(additional_creators)
pl_users.append(creation_event.sender)

return {
"room_id": room_id,
"name": details.name if details else None,
"join_rules": details.join_rules if details else None,
"is_space": is_space,
"topic": details.topic if details else None,
"origin_server_ts": creation_event.origin_server_ts
Comment thread
H-Shay marked this conversation as resolved.
Outdated
if creation_event
else None,
"sender": creation_event.sender if creation_event else None,
"event_id": creation_event.event_id if creation_event else None,
"power_users": pl_users,
"deleted": True if not details or not creation_event else False,
"aliases": aliases,
}

parents_and_children = await self._store.get_parents_and_siblings_of_rooms(
rooms
)
room_details_list = []
seen_parents = set()
for parent_id, child_id in parents_and_children:
if parent_id in seen_parents:
child_details = await _fetch_room_details(child_id, False)
room_details_list.append(child_details)
else:
child_details = await _fetch_room_details(child_id, False)
room_details_list.append(child_details)
parent_details = await _fetch_room_details(parent_id, True)
room_details_list.append(parent_details)
seen_parents.add(parent_id)

if not room_details_list:
return HTTPStatus.NOT_FOUND, {}
else:
return HTTPStatus.OK, {"found_rooms": room_details_list}


class RoomRestV2Servlet(RestServlet):
"""Delete a room from server asynchronously with a background task.

Expand Down
37 changes: 37 additions & 0 deletions synapse/storage/databases/main/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -2783,3 +2783,40 @@ def _clear_partial_state_room_txn(
WHERE stream_id <= ?
"""
txn.execute(sql, (device_lists_stream_id,))

async def get_parents_and_siblings_of_rooms(
self, room_ids: list[tuple]
) -> list[tuple]:
"""
For each room in the list, get any parents of the room, and any children of the
parent room

Args:
room_ids: a list of rooms to check for parent spaces
"""

def get_parents_and_children_of_rooms_txn(
txn: LoggingTransaction, room_ids: list[str]
) -> list[tuple]:
clause, param = make_in_list_sql_clause(
self.database_engine, "state_key", room_ids
)
sql = f"""
SELECT room_id AS parent_space_id, state_key AS room_id
FROM current_state_events
WHERE type = 'm.space.child'
AND room_id IN (
SELECT room_id
FROM current_state_events
WHERE {clause}
AND type = 'm.space.child'
)
"""
txn.execute(sql, param)
return txn.fetchall()

return await self.db_pool.runInteraction(
"get_parents_and_children_of_rooms_txn",
get_parents_and_children_of_rooms_txn,
room_ids,
)
Loading
Loading