diff --git a/changelog.d/9167.feature b/changelog.d/9167.feature
new file mode 100644
index 0000000000..6961620946
--- /dev/null
+++ b/changelog.d/9167.feature
@@ -0,0 +1 @@
+Add server admin endpoints to join users to legacy groups and manage their flair.
\ No newline at end of file
diff --git a/changelog.d/9168.feature b/changelog.d/9168.feature
new file mode 100644
index 0000000000..8be1950eee
--- /dev/null
+++ b/changelog.d/9168.feature
@@ -0,0 +1 @@
+Add an admin API for retrieving the current room state of a room.
\ No newline at end of file
diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md
index 9e560003a9..d68003853b 100644
--- a/docs/admin_api/rooms.md
+++ b/docs/admin_api/rooms.md
@@ -367,6 +367,36 @@ Response:
}
```
+# Room State API
+
+The Room State admin API allows server admins to get a list of all state events in a room.
+
+The response includes the following fields:
+
+* `state` - The current state of the room at the time of request.
+
+## Usage
+
+A standard request:
+
+```
+GET /_synapse/admin/v1/rooms/<room_id>/state
+
+{}
+```
+
+Response:
+
+```json
+{
+ "state": [
+ {"type": "m.room.create", "state_key": "", "etc": true},
+ {"type": "m.room.power_levels", "state_key": "", "etc": true},
+ {"type": "m.room.name", "state_key": "", "etc": true}
+ ]
+}
+```
+
# Delete Room API
The Delete Room admin API allows server admins to remove rooms from server
diff --git a/synapse/handlers/groups_local.py b/synapse/handlers/groups_local.py
index df29edeb83..a2f16f77df 100644
--- a/synapse/handlers/groups_local.py
+++ b/synapse/handlers/groups_local.py
@@ -365,6 +365,32 @@ class GroupsLocalHandler(GroupsLocalWorkerHandler):
return {}
+ async def force_join_user_to_group(self, group_id, user_id):
+ """Forces a user to join a group.
+ """
+ if not self.is_mine_id(group_id):
+ raise SynapseError(400, "Can only affect local groups")
+
+ if not self.is_mine_id(user_id):
+ raise SynapseError(400, "Can only affect local users")
+
+ # Bypass the group server to avoid business logic regarding whether or not
+ # the user can actually join.
+ await self.store.add_user_to_group(group_id, user_id)
+
+ token = await self.store.register_user_group_membership(
+ group_id,
+ user_id,
+ membership="join",
+ is_admin=False,
+ local_attestation=None,
+ remote_attestation=None,
+ is_publicised=False,
+ )
+ self.notifier.on_new_event("groups_key", token, users=[user_id])
+
+ return {}
+
async def accept_invite(self, group_id, user_id, content):
"""Accept an invite to a group
"""
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 9dfeab09cd..d3f8dc550d 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -174,7 +174,7 @@ class MessageHandler:
raise NotFoundError("Can't find event for token %s" % (at_token,))
visible_events = await filter_events_for_client(
- self.storage, user_id, last_events, filter_send_to_client=False
+ self.storage, user_id, last_events, filter_send_to_client=False,
)
event = last_events[0]
diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py
index 6f7dc06503..a20a459874 100644
--- a/synapse/rest/admin/__init__.py
+++ b/synapse/rest/admin/__init__.py
@@ -31,7 +31,11 @@ from synapse.rest.admin.event_reports import (
EventReportDetailRestServlet,
EventReportsRestServlet,
)
-from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
+from synapse.rest.admin.groups import (
+ DeleteGroupAdminRestServlet,
+ ForceJoinGroupAdminRestServlet,
+ UpdatePublicityGroupAdminRestServlet,
+)
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet
from synapse.rest.admin.rooms import (
@@ -41,6 +45,7 @@ from synapse.rest.admin.rooms import (
MakeRoomAdminRestServlet,
RoomMembersRestServlet,
RoomRestServlet,
+ RoomStateRestServlet,
ShutdownRoomRestServlet,
)
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
@@ -209,6 +214,7 @@ def register_servlets(hs, http_server):
"""
register_servlets_for_client_rest_resource(hs, http_server)
ListRoomRestServlet(hs).register(http_server)
+ RoomStateRestServlet(hs).register(http_server)
RoomRestServlet(hs).register(http_server)
RoomMembersRestServlet(hs).register(http_server)
DeleteRoomRestServlet(hs).register(http_server)
@@ -244,6 +250,8 @@ def register_servlets_for_client_rest_resource(hs, http_server):
ShutdownRoomRestServlet(hs).register(http_server)
UserRegisterServlet(hs).register(http_server)
DeleteGroupAdminRestServlet(hs).register(http_server)
+ ForceJoinGroupAdminRestServlet(hs).register(http_server)
+ UpdatePublicityGroupAdminRestServlet(hs).register(http_server)
AccountValidityRenewServlet(hs).register(http_server)
# Load the media repo ones if we're using them. Otherwise load the servlets which
diff --git a/synapse/rest/admin/groups.py b/synapse/rest/admin/groups.py
index d0c86b204a..6af36e32b7 100644
--- a/synapse/rest/admin/groups.py
+++ b/synapse/rest/admin/groups.py
@@ -15,7 +15,11 @@
import logging
from synapse.api.errors import SynapseError
-from synapse.http.servlet import RestServlet
+from synapse.http.servlet import (
+ RestServlet,
+ assert_params_in_dict,
+ parse_json_object_from_request,
+)
from synapse.rest.admin._base import admin_patterns, assert_user_is_admin
logger = logging.getLogger(__name__)
@@ -41,3 +45,57 @@ class DeleteGroupAdminRestServlet(RestServlet):
await self.group_server.delete_group(group_id, requester.user.to_string())
return 200, {}
+
+
+class ForceJoinGroupAdminRestServlet(RestServlet):
+ """Allows a server admin to force-join a local user to a local group.
+ """
+
+ PATTERNS = admin_patterns("/group/(?P<group_id>[^/]*)/force_join$")
+
+ def __init__(self, hs):
+ self.groups_handler = hs.get_groups_local_handler()
+ self.is_mine_id = hs.is_mine_id
+ self.auth = hs.get_auth()
+
+ async def on_POST(self, request, group_id):
+ requester = await self.auth.get_user_by_req(request)
+ await assert_user_is_admin(self.auth, requester.user)
+
+ if not self.is_mine_id(group_id):
+ raise SynapseError(400, "Can only affect local groups")
+
+ body = parse_json_object_from_request(request, allow_empty_body=False)
+ assert_params_in_dict(body, ["user_id"])
+ target_user_id = body["user_id"]
+ await self.groups_handler.force_join_user_to_group(group_id, target_user_id)
+
+ return 200, {}
+
+
+class UpdatePublicityGroupAdminRestServlet(RestServlet):
+ """Allows a server admin to update a user's publicity (flair) for a given group.
+ """
+
+ PATTERNS = admin_patterns("/group/(?P<group_id>[^/]*)/update_publicity$")
+
+ def __init__(self, hs):
+ self.store = hs.get_datastore()
+ self.is_mine_id = hs.is_mine_id
+ self.auth = hs.get_auth()
+
+ async def on_POST(self, request, group_id):
+ requester = await self.auth.get_user_by_req(request)
+ await assert_user_is_admin(self.auth, requester.user)
+
+ body = parse_json_object_from_request(request, allow_empty_body=False)
+ assert_params_in_dict(body, ["user_id"])
+ target_user_id = body["user_id"]
+ if not self.is_mine_id(target_user_id):
+ raise SynapseError(400, "Can only affect local users")
+
+ # Logic copied from `/self/update_publicity` endpoint.
+ publicise = body["publicise"]
+ await self.store.update_group_publicity(group_id, target_user_id, publicise)
+
+ return 200, {}
diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py
index ab7cc9102a..d903129dd2 100644
--- a/synapse/rest/admin/rooms.py
+++ b/synapse/rest/admin/rooms.py
@@ -292,6 +292,45 @@ class RoomMembersRestServlet(RestServlet):
return 200, ret
+class RoomStateRestServlet(RestServlet):
+ """
+ Get full state within a room.
+ """
+
+ PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/state")
+
+ def __init__(self, hs: "HomeServer"):
+ self.hs = hs
+ self.auth = hs.get_auth()
+ self.store = hs.get_datastore()
+ self.clock = hs.get_clock()
+ self._event_serializer = hs.get_event_client_serializer()
+
+ async def on_GET(
+ self, request: SynapseRequest, room_id: str
+ ) -> Tuple[int, JsonDict]:
+ requester = await self.auth.get_user_by_req(request)
+ await assert_user_is_admin(self.auth, requester.user)
+
+ ret = await self.store.get_room(room_id)
+ if not ret:
+ raise NotFoundError("Room not found")
+
+ event_ids = await self.store.get_current_state_ids(room_id)
+ 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,
+ # We don't bother bundling aggregations in when asked for state
+ # events, as clients won't use them.
+ bundle_aggregations=False,
+ )
+ ret = {"state": room_state}
+
+ return 200, ret
+
+
class JoinRoomAliasServlet(RestServlet):
PATTERNS = admin_patterns("/join/(?P<room_identifier>[^/]*)")
diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py
index a0f32c5512..7c47aa7e0a 100644
--- a/tests/rest/admin/test_room.py
+++ b/tests/rest/admin/test_room.py
@@ -1180,6 +1180,21 @@ class RoomTestCase(unittest.HomeserverTestCase):
)
self.assertEqual(channel.json_body["total"], 3)
+ def test_room_state(self):
+ """Test that room state can be requested correctly"""
+ # Create two test rooms
+ room_id = self.helper.create_room_as(self.admin_user, tok=self.admin_user_tok)
+
+ url = "/_synapse/admin/v1/rooms/%s/state" % (room_id,)
+ channel = self.make_request(
+ "GET", url.encode("ascii"), access_token=self.admin_user_tok,
+ )
+ self.assertEqual(200, channel.code, msg=channel.json_body)
+ self.assertIn("state", channel.json_body)
+ # testing that the state events match is painful and not done here. We assume that
+ # the create_room already does the right thing, so no need to verify that we got
+ # the state events it created.
+
class JoinAliasRoomTestCase(unittest.HomeserverTestCase):
|