summary refs log tree commit diff
path: root/synapse/federation
diff options
context:
space:
mode:
Diffstat (limited to 'synapse/federation')
-rw-r--r--synapse/federation/federation_server.py3
-rw-r--r--synapse/federation/sender/per_destination_queue.py9
-rw-r--r--synapse/federation/sender/transaction_manager.py6
-rw-r--r--synapse/federation/transport/client.py498
-rw-r--r--synapse/federation/transport/server/__init__.py48
-rw-r--r--synapse/federation/transport/server/federation.py11
-rw-r--r--synapse/federation/transport/server/groups_local.py115
-rw-r--r--synapse/federation/transport/server/groups_server.py755
8 files changed, 25 insertions, 1420 deletions
diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py
index b8232e5257..3ecede22d9 100644
--- a/synapse/federation/federation_server.py
+++ b/synapse/federation/federation_server.py
@@ -109,7 +109,6 @@ class FederationServer(FederationBase):
         super().__init__(hs)
 
         self.handler = hs.get_federation_handler()
-        self.storage = hs.get_storage()
         self._spam_checker = hs.get_spam_checker()
         self._federation_event_handler = hs.get_federation_event_handler()
         self.state = hs.get_state_handler()
@@ -1353,7 +1352,7 @@ class FederationHandlerRegistry:
         self._edu_type_to_instance[edu_type] = instance_names
 
     async def on_edu(self, edu_type: str, origin: str, content: dict) -> None:
-        if not self.config.server.use_presence and edu_type == EduTypes.Presence:
+        if not self.config.server.use_presence and edu_type == EduTypes.PRESENCE:
             return
 
         # Check if we have a handler on this instance
diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py
index d80f0ac5e8..333ca9a97f 100644
--- a/synapse/federation/sender/per_destination_queue.py
+++ b/synapse/federation/sender/per_destination_queue.py
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Dict, Hashable, Iterable, List, Optional, Tupl
 import attr
 from prometheus_client import Counter
 
+from synapse.api.constants import EduTypes
 from synapse.api.errors import (
     FederationDeniedError,
     HttpResponseException,
@@ -223,7 +224,7 @@ class PerDestinationQueue:
         """Marks that the destination has new data to send, without starting a
         new transaction.
 
-        If a transaction loop is already in progress then a new transcation will
+        If a transaction loop is already in progress then a new transaction will
         be attempted when the current one finishes.
         """
 
@@ -542,7 +543,7 @@ class PerDestinationQueue:
         edu = Edu(
             origin=self._server_name,
             destination=self._destination,
-            edu_type="m.receipt",
+            edu_type=EduTypes.RECEIPT,
             content=self._pending_rrs,
         )
         self._pending_rrs = {}
@@ -592,7 +593,7 @@ class PerDestinationQueue:
             Edu(
                 origin=self._server_name,
                 destination=self._destination,
-                edu_type="m.direct_to_device",
+                edu_type=EduTypes.DIRECT_TO_DEVICE,
                 content=content,
             )
             for content in contents
@@ -670,7 +671,7 @@ class _TransactionQueueManager:
                 Edu(
                     origin=self.queue._server_name,
                     destination=self.queue._destination,
-                    edu_type="m.presence",
+                    edu_type=EduTypes.PRESENCE,
                     content={
                         "push": [
                             format_user_presence_state(
diff --git a/synapse/federation/sender/transaction_manager.py b/synapse/federation/sender/transaction_manager.py
index 0c1cad86ab..75081810fd 100644
--- a/synapse/federation/sender/transaction_manager.py
+++ b/synapse/federation/sender/transaction_manager.py
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, List
 
 from prometheus_client import Gauge
 
+from synapse.api.constants import EduTypes
 from synapse.api.errors import HttpResponseException
 from synapse.events import EventBase
 from synapse.federation.persistence import TransactionActions
@@ -126,7 +127,10 @@ class TransactionManager:
                 len(edus),
             )
             if issue_8631_logger.isEnabledFor(logging.DEBUG):
-                DEVICE_UPDATE_EDUS = {"m.device_list_update", "m.signing_key_update"}
+                DEVICE_UPDATE_EDUS = {
+                    EduTypes.DEVICE_LIST_UPDATE,
+                    EduTypes.SIGNING_KEY_UPDATE,
+                }
                 device_list_updates = [
                     edu.content for edu in edus if edu.edu_type in DEVICE_UPDATE_EDUS
                 ]
diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py
index 2686ee2e51..9e84bd677e 100644
--- a/synapse/federation/transport/client.py
+++ b/synapse/federation/transport/client.py
@@ -17,7 +17,6 @@ import logging
 import urllib
 from typing import (
     Any,
-    Awaitable,
     Callable,
     Collection,
     Dict,
@@ -49,11 +48,6 @@ from synapse.types import JsonDict
 
 logger = logging.getLogger(__name__)
 
-# Send join responses can be huge, so we set a separate limit here. The response
-# is parsed in a streaming manner, which helps alleviate the issue of memory
-# usage a bit.
-MAX_RESPONSE_SIZE_SEND_JOIN = 500 * 1024 * 1024
-
 
 class TransportLayerClient:
     """Sends federation HTTP requests to other servers"""
@@ -349,7 +343,6 @@ class TransportLayerClient:
             path=path,
             data=content,
             parser=SendJoinParser(room_version, v1_api=True),
-            max_response_size=MAX_RESPONSE_SIZE_SEND_JOIN,
         )
 
     async def send_join_v2(
@@ -372,7 +365,6 @@ class TransportLayerClient:
             args=query_params,
             data=content,
             parser=SendJoinParser(room_version, v1_api=False),
-            max_response_size=MAX_RESPONSE_SIZE_SEND_JOIN,
         )
 
     async def send_leave_v1(
@@ -688,488 +680,6 @@ class TransportLayerClient:
             timeout=timeout,
         )
 
-    async def get_group_profile(
-        self, destination: str, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get a group profile"""
-        path = _create_v1_path("/groups/%s/profile", group_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def update_group_profile(
-        self, destination: str, group_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Update a remote group profile
-
-        Args:
-            destination
-            group_id
-            requester_user_id
-            content: The new profile of the group
-        """
-        path = _create_v1_path("/groups/%s/profile", group_id)
-
-        return self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def get_group_summary(
-        self, destination: str, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get a group summary"""
-        path = _create_v1_path("/groups/%s/summary", group_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def get_rooms_in_group(
-        self, destination: str, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get all rooms in a group"""
-        path = _create_v1_path("/groups/%s/rooms", group_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def add_room_to_group(
-        self,
-        destination: str,
-        group_id: str,
-        requester_user_id: str,
-        room_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Add a room to a group"""
-        path = _create_v1_path("/groups/%s/room/%s", group_id, room_id)
-
-        return await self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def update_room_in_group(
-        self,
-        destination: str,
-        group_id: str,
-        requester_user_id: str,
-        room_id: str,
-        config_key: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Update room in group"""
-        path = _create_v1_path(
-            "/groups/%s/room/%s/config/%s", group_id, room_id, config_key
-        )
-
-        return await self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def remove_room_from_group(
-        self, destination: str, group_id: str, requester_user_id: str, room_id: str
-    ) -> JsonDict:
-        """Remove a room from a group"""
-        path = _create_v1_path("/groups/%s/room/%s", group_id, room_id)
-
-        return await self.client.delete_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def get_users_in_group(
-        self, destination: str, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get users in a group"""
-        path = _create_v1_path("/groups/%s/users", group_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def get_invited_users_in_group(
-        self, destination: str, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get users that have been invited to a group"""
-        path = _create_v1_path("/groups/%s/invited_users", group_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def accept_group_invite(
-        self, destination: str, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Accept a group invite"""
-        path = _create_v1_path("/groups/%s/users/%s/accept_invite", group_id, user_id)
-
-        return await self.client.post_json(
-            destination=destination, path=path, data=content, ignore_backoff=True
-        )
-
-    def join_group(
-        self, destination: str, group_id: str, user_id: str, content: JsonDict
-    ) -> Awaitable[JsonDict]:
-        """Attempts to join a group"""
-        path = _create_v1_path("/groups/%s/users/%s/join", group_id, user_id)
-
-        return self.client.post_json(
-            destination=destination, path=path, data=content, ignore_backoff=True
-        )
-
-    async def invite_to_group(
-        self,
-        destination: str,
-        group_id: str,
-        user_id: str,
-        requester_user_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Invite a user to a group"""
-        path = _create_v1_path("/groups/%s/users/%s/invite", group_id, user_id)
-
-        return await self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def invite_to_group_notification(
-        self, destination: str, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Sent by group server to inform a user's server that they have been
-        invited.
-        """
-
-        path = _create_v1_path("/groups/local/%s/users/%s/invite", group_id, user_id)
-
-        return await self.client.post_json(
-            destination=destination, path=path, data=content, ignore_backoff=True
-        )
-
-    async def remove_user_from_group(
-        self,
-        destination: str,
-        group_id: str,
-        requester_user_id: str,
-        user_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Remove a user from a group"""
-        path = _create_v1_path("/groups/%s/users/%s/remove", group_id, user_id)
-
-        return await self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def remove_user_from_group_notification(
-        self, destination: str, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Sent by group server to inform a user's server that they have been
-        kicked from the group.
-        """
-
-        path = _create_v1_path("/groups/local/%s/users/%s/remove", group_id, user_id)
-
-        return await self.client.post_json(
-            destination=destination, path=path, data=content, ignore_backoff=True
-        )
-
-    async def renew_group_attestation(
-        self, destination: str, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Sent by either a group server or a user's server to periodically update
-        the attestations
-        """
-
-        path = _create_v1_path("/groups/%s/renew_attestation/%s", group_id, user_id)
-
-        return await self.client.post_json(
-            destination=destination, path=path, data=content, ignore_backoff=True
-        )
-
-    async def update_group_summary_room(
-        self,
-        destination: str,
-        group_id: str,
-        user_id: str,
-        room_id: str,
-        category_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Update a room entry in a group summary"""
-        if category_id:
-            path = _create_v1_path(
-                "/groups/%s/summary/categories/%s/rooms/%s",
-                group_id,
-                category_id,
-                room_id,
-            )
-        else:
-            path = _create_v1_path("/groups/%s/summary/rooms/%s", group_id, room_id)
-
-        return await self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def delete_group_summary_room(
-        self,
-        destination: str,
-        group_id: str,
-        user_id: str,
-        room_id: str,
-        category_id: str,
-    ) -> JsonDict:
-        """Delete a room entry in a group summary"""
-        if category_id:
-            path = _create_v1_path(
-                "/groups/%s/summary/categories/%s/rooms/%s",
-                group_id,
-                category_id,
-                room_id,
-            )
-        else:
-            path = _create_v1_path("/groups/%s/summary/rooms/%s", group_id, room_id)
-
-        return await self.client.delete_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": user_id},
-            ignore_backoff=True,
-        )
-
-    async def get_group_categories(
-        self, destination: str, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get all categories in a group"""
-        path = _create_v1_path("/groups/%s/categories", group_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def get_group_category(
-        self, destination: str, group_id: str, requester_user_id: str, category_id: str
-    ) -> JsonDict:
-        """Get category info in a group"""
-        path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def update_group_category(
-        self,
-        destination: str,
-        group_id: str,
-        requester_user_id: str,
-        category_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Update a category in a group"""
-        path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id)
-
-        return await self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def delete_group_category(
-        self, destination: str, group_id: str, requester_user_id: str, category_id: str
-    ) -> JsonDict:
-        """Delete a category in a group"""
-        path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id)
-
-        return await self.client.delete_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def get_group_roles(
-        self, destination: str, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get all roles in a group"""
-        path = _create_v1_path("/groups/%s/roles", group_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def get_group_role(
-        self, destination: str, group_id: str, requester_user_id: str, role_id: str
-    ) -> JsonDict:
-        """Get a roles info"""
-        path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id)
-
-        return await self.client.get_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def update_group_role(
-        self,
-        destination: str,
-        group_id: str,
-        requester_user_id: str,
-        role_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Update a role in a group"""
-        path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id)
-
-        return await self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def delete_group_role(
-        self, destination: str, group_id: str, requester_user_id: str, role_id: str
-    ) -> JsonDict:
-        """Delete a role in a group"""
-        path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id)
-
-        return await self.client.delete_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def update_group_summary_user(
-        self,
-        destination: str,
-        group_id: str,
-        requester_user_id: str,
-        user_id: str,
-        role_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Update a users entry in a group"""
-        if role_id:
-            path = _create_v1_path(
-                "/groups/%s/summary/roles/%s/users/%s", group_id, role_id, user_id
-            )
-        else:
-            path = _create_v1_path("/groups/%s/summary/users/%s", group_id, user_id)
-
-        return await self.client.post_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def set_group_join_policy(
-        self, destination: str, group_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Sets the join policy for a group"""
-        path = _create_v1_path("/groups/%s/settings/m.join_policy", group_id)
-
-        return await self.client.put_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            data=content,
-            ignore_backoff=True,
-        )
-
-    async def delete_group_summary_user(
-        self,
-        destination: str,
-        group_id: str,
-        requester_user_id: str,
-        user_id: str,
-        role_id: str,
-    ) -> JsonDict:
-        """Delete a users entry in a group"""
-        if role_id:
-            path = _create_v1_path(
-                "/groups/%s/summary/roles/%s/users/%s", group_id, role_id, user_id
-            )
-        else:
-            path = _create_v1_path("/groups/%s/summary/users/%s", group_id, user_id)
-
-        return await self.client.delete_json(
-            destination=destination,
-            path=path,
-            args={"requester_user_id": requester_user_id},
-            ignore_backoff=True,
-        )
-
-    async def bulk_get_publicised_groups(
-        self, destination: str, user_ids: Iterable[str]
-    ) -> JsonDict:
-        """Get the groups a list of users are publicising"""
-
-        path = _create_v1_path("/get_groups_publicised")
-
-        content = {"user_ids": user_ids}
-
-        return await self.client.post_json(
-            destination=destination, path=path, data=content, ignore_backoff=True
-        )
-
     async def get_room_complexity(self, destination: str, room_id: str) -> JsonDict:
         """
         Args:
@@ -1360,6 +870,11 @@ class SendJoinParser(ByteParser[SendJoinResponse]):
 
     CONTENT_TYPE = "application/json"
 
+    # /send_join responses can be huge, so we override the size limit here. The response
+    # is parsed in a streaming manner, which helps alleviate the issue of memory
+    # usage a bit.
+    MAX_RESPONSE_SIZE = 500 * 1024 * 1024
+
     def __init__(self, room_version: RoomVersion, v1_api: bool):
         self._response = SendJoinResponse([], [], event_dict={})
         self._room_version = room_version
@@ -1430,6 +945,9 @@ class _StateParser(ByteParser[StateRequestResponse]):
 
     CONTENT_TYPE = "application/json"
 
+    # As with /send_join, /state responses can be huge.
+    MAX_RESPONSE_SIZE = 500 * 1024 * 1024
+
     def __init__(self, room_version: RoomVersion):
         self._response = StateRequestResponse([], [])
         self._room_version = room_version
diff --git a/synapse/federation/transport/server/__init__.py b/synapse/federation/transport/server/__init__.py
index 71b2f90eb9..50623cd385 100644
--- a/synapse/federation/transport/server/__init__.py
+++ b/synapse/federation/transport/server/__init__.py
@@ -27,10 +27,6 @@ from synapse.federation.transport.server.federation import (
     FederationAccountStatusServlet,
     FederationTimestampLookupServlet,
 )
-from synapse.federation.transport.server.groups_local import GROUP_LOCAL_SERVLET_CLASSES
-from synapse.federation.transport.server.groups_server import (
-    GROUP_SERVER_SERVLET_CLASSES,
-)
 from synapse.http.server import HttpServer, JsonResource
 from synapse.http.servlet import (
     parse_boolean_from_args,
@@ -199,38 +195,6 @@ class PublicRoomList(BaseFederationServlet):
         return 200, data
 
 
-class FederationGroupsRenewAttestaionServlet(BaseFederationServlet):
-    """A group or user's server renews their attestation"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/renew_attestation/(?P<user_id>[^/]*)"
-
-    def __init__(
-        self,
-        hs: "HomeServer",
-        authenticator: Authenticator,
-        ratelimiter: FederationRateLimiter,
-        server_name: str,
-    ):
-        super().__init__(hs, authenticator, ratelimiter, server_name)
-        self.handler = hs.get_groups_attestation_renewer()
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        user_id: str,
-    ) -> Tuple[int, JsonDict]:
-        # We don't need to check auth here as we check the attestation signatures
-
-        new_content = await self.handler.on_renew_attestation(
-            group_id, user_id, content
-        )
-
-        return 200, new_content
-
-
 class OpenIdUserInfo(BaseFederationServlet):
     """
     Exchange a bearer token for information about a user.
@@ -292,16 +256,9 @@ class OpenIdUserInfo(BaseFederationServlet):
 SERVLET_GROUPS: Dict[str, Iterable[Type[BaseFederationServlet]]] = {
     "federation": FEDERATION_SERVLET_CLASSES,
     "room_list": (PublicRoomList,),
-    "group_server": GROUP_SERVER_SERVLET_CLASSES,
-    "group_local": GROUP_LOCAL_SERVLET_CLASSES,
-    "group_attestation": (FederationGroupsRenewAttestaionServlet,),
     "openid": (OpenIdUserInfo,),
 }
 
-DEFAULT_SERVLET_GROUPS = ("federation", "room_list", "openid")
-
-GROUP_SERVLET_GROUPS = ("group_server", "group_local", "group_attestation")
-
 
 def register_servlets(
     hs: "HomeServer",
@@ -324,10 +281,7 @@ def register_servlets(
             Defaults to ``DEFAULT_SERVLET_GROUPS``.
     """
     if not servlet_groups:
-        servlet_groups = DEFAULT_SERVLET_GROUPS
-        # Only allow the groups servlets if the deprecated groups feature is enabled.
-        if hs.config.experimental.groups_enabled:
-            servlet_groups = servlet_groups + GROUP_SERVLET_GROUPS
+        servlet_groups = SERVLET_GROUPS.keys()
 
     for servlet_group in servlet_groups:
         # Skip unknown servlet groups.
diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py
index 6fbc7b5f15..7dfb890661 100644
--- a/synapse/federation/transport/server/federation.py
+++ b/synapse/federation/transport/server/federation.py
@@ -27,6 +27,7 @@ from typing import (
 from matrix_common.versionstring import get_distribution_version_string
 from typing_extensions import Literal
 
+from synapse.api.constants import EduTypes
 from synapse.api.errors import Codes, SynapseError
 from synapse.api.room_versions import RoomVersions
 from synapse.api.urls import FEDERATION_UNSTABLE_PREFIX, FEDERATION_V2_PREFIX
@@ -108,7 +109,10 @@ class FederationSendServlet(BaseFederationServerServlet):
             )
 
             if issue_8631_logger.isEnabledFor(logging.DEBUG):
-                DEVICE_UPDATE_EDUS = ["m.device_list_update", "m.signing_key_update"]
+                DEVICE_UPDATE_EDUS = [
+                    EduTypes.DEVICE_LIST_UPDATE,
+                    EduTypes.SIGNING_KEY_UPDATE,
+                ]
                 device_list_updates = [
                     edu.get("content", {})
                     for edu in transaction_data.get("edus", [])
@@ -650,10 +654,6 @@ class FederationRoomHierarchyServlet(BaseFederationServlet):
         )
 
 
-class FederationRoomHierarchyUnstableServlet(FederationRoomHierarchyServlet):
-    PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946"
-
-
 class RoomComplexityServlet(BaseFederationServlet):
     """
     Indicates to other servers how complex (and therefore likely
@@ -752,7 +752,6 @@ FEDERATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = (
     FederationVersionServlet,
     RoomComplexityServlet,
     FederationRoomHierarchyServlet,
-    FederationRoomHierarchyUnstableServlet,
     FederationV1SendKnockServlet,
     FederationMakeKnockServlet,
     FederationAccountStatusServlet,
diff --git a/synapse/federation/transport/server/groups_local.py b/synapse/federation/transport/server/groups_local.py
deleted file mode 100644
index 496472e1dc..0000000000
--- a/synapse/federation/transport/server/groups_local.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#  Copyright 2021 The Matrix.org Foundation C.I.C.
-#
-#  Licensed under the Apache License, Version 2.0 (the "License");
-#  you may not use this file except in compliance with the License.
-#  You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing, software
-#  distributed under the License is distributed on an "AS IS" BASIS,
-#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#  See the License for the specific language governing permissions and
-#  limitations under the License.
-from typing import TYPE_CHECKING, Dict, List, Tuple, Type
-
-from synapse.api.errors import SynapseError
-from synapse.federation.transport.server._base import (
-    Authenticator,
-    BaseFederationServlet,
-)
-from synapse.handlers.groups_local import GroupsLocalHandler
-from synapse.types import JsonDict, get_domain_from_id
-from synapse.util.ratelimitutils import FederationRateLimiter
-
-if TYPE_CHECKING:
-    from synapse.server import HomeServer
-
-
-class BaseGroupsLocalServlet(BaseFederationServlet):
-    """Abstract base class for federation servlet classes which provides a groups local handler.
-
-    See BaseFederationServlet for more information.
-    """
-
-    def __init__(
-        self,
-        hs: "HomeServer",
-        authenticator: Authenticator,
-        ratelimiter: FederationRateLimiter,
-        server_name: str,
-    ):
-        super().__init__(hs, authenticator, ratelimiter, server_name)
-        self.handler = hs.get_groups_local_handler()
-
-
-class FederationGroupsLocalInviteServlet(BaseGroupsLocalServlet):
-    """A group server has invited a local user"""
-
-    PATH = "/groups/local/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/invite"
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        user_id: str,
-    ) -> Tuple[int, JsonDict]:
-        if get_domain_from_id(group_id) != origin:
-            raise SynapseError(403, "group_id doesn't match origin")
-
-        assert isinstance(
-            self.handler, GroupsLocalHandler
-        ), "Workers cannot handle group invites."
-
-        new_content = await self.handler.on_invite(group_id, user_id, content)
-
-        return 200, new_content
-
-
-class FederationGroupsRemoveLocalUserServlet(BaseGroupsLocalServlet):
-    """A group server has removed a local user"""
-
-    PATH = "/groups/local/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/remove"
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        user_id: str,
-    ) -> Tuple[int, None]:
-        if get_domain_from_id(group_id) != origin:
-            raise SynapseError(403, "user_id doesn't match origin")
-
-        assert isinstance(
-            self.handler, GroupsLocalHandler
-        ), "Workers cannot handle group removals."
-
-        await self.handler.user_removed_from_group(group_id, user_id, content)
-
-        return 200, None
-
-
-class FederationGroupsBulkPublicisedServlet(BaseGroupsLocalServlet):
-    """Get roles in a group"""
-
-    PATH = "/get_groups_publicised"
-
-    async def on_POST(
-        self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]]
-    ) -> Tuple[int, JsonDict]:
-        resp = await self.handler.bulk_get_publicised_groups(
-            content["user_ids"], proxy=False
-        )
-
-        return 200, resp
-
-
-GROUP_LOCAL_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = (
-    FederationGroupsLocalInviteServlet,
-    FederationGroupsRemoveLocalUserServlet,
-    FederationGroupsBulkPublicisedServlet,
-)
diff --git a/synapse/federation/transport/server/groups_server.py b/synapse/federation/transport/server/groups_server.py
deleted file mode 100644
index 851b50152e..0000000000
--- a/synapse/federation/transport/server/groups_server.py
+++ /dev/null
@@ -1,755 +0,0 @@
-#  Copyright 2021 The Matrix.org Foundation C.I.C.
-#
-#  Licensed under the Apache License, Version 2.0 (the "License");
-#  you may not use this file except in compliance with the License.
-#  You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing, software
-#  distributed under the License is distributed on an "AS IS" BASIS,
-#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#  See the License for the specific language governing permissions and
-#  limitations under the License.
-from typing import TYPE_CHECKING, Dict, List, Tuple, Type
-
-from typing_extensions import Literal
-
-from synapse.api.constants import MAX_GROUP_CATEGORYID_LENGTH, MAX_GROUP_ROLEID_LENGTH
-from synapse.api.errors import Codes, SynapseError
-from synapse.federation.transport.server._base import (
-    Authenticator,
-    BaseFederationServlet,
-)
-from synapse.http.servlet import parse_string_from_args
-from synapse.types import JsonDict, get_domain_from_id
-from synapse.util.ratelimitutils import FederationRateLimiter
-
-if TYPE_CHECKING:
-    from synapse.server import HomeServer
-
-
-class BaseGroupsServerServlet(BaseFederationServlet):
-    """Abstract base class for federation servlet classes which provides a groups server handler.
-
-    See BaseFederationServlet for more information.
-    """
-
-    def __init__(
-        self,
-        hs: "HomeServer",
-        authenticator: Authenticator,
-        ratelimiter: FederationRateLimiter,
-        server_name: str,
-    ):
-        super().__init__(hs, authenticator, ratelimiter, server_name)
-        self.handler = hs.get_groups_server_handler()
-
-
-class FederationGroupsProfileServlet(BaseGroupsServerServlet):
-    """Get/set the basic profile of a group on behalf of a user"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/profile"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.get_group_profile(group_id, requester_user_id)
-
-        return 200, new_content
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.update_group_profile(
-            group_id, requester_user_id, content
-        )
-
-        return 200, new_content
-
-
-class FederationGroupsSummaryServlet(BaseGroupsServerServlet):
-    PATH = "/groups/(?P<group_id>[^/]*)/summary"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.get_group_summary(group_id, requester_user_id)
-
-        return 200, new_content
-
-
-class FederationGroupsRoomsServlet(BaseGroupsServerServlet):
-    """Get the rooms in a group on behalf of a user"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/rooms"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.get_rooms_in_group(group_id, requester_user_id)
-
-        return 200, new_content
-
-
-class FederationGroupsAddRoomsServlet(BaseGroupsServerServlet):
-    """Add/remove room from group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/room/(?P<room_id>[^/]*)"
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        room_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.add_room_to_group(
-            group_id, requester_user_id, room_id, content
-        )
-
-        return 200, new_content
-
-    async def on_DELETE(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        room_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.remove_room_from_group(
-            group_id, requester_user_id, room_id
-        )
-
-        return 200, new_content
-
-
-class FederationGroupsAddRoomsConfigServlet(BaseGroupsServerServlet):
-    """Update room config in group"""
-
-    PATH = (
-        "/groups/(?P<group_id>[^/]*)/room/(?P<room_id>[^/]*)"
-        "/config/(?P<config_key>[^/]*)"
-    )
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        room_id: str,
-        config_key: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        result = await self.handler.update_room_in_group(
-            group_id, requester_user_id, room_id, config_key, content
-        )
-
-        return 200, result
-
-
-class FederationGroupsUsersServlet(BaseGroupsServerServlet):
-    """Get the users in a group on behalf of a user"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/users"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.get_users_in_group(group_id, requester_user_id)
-
-        return 200, new_content
-
-
-class FederationGroupsInvitedUsersServlet(BaseGroupsServerServlet):
-    """Get the users that have been invited to a group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/invited_users"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.get_invited_users_in_group(
-            group_id, requester_user_id
-        )
-
-        return 200, new_content
-
-
-class FederationGroupsInviteServlet(BaseGroupsServerServlet):
-    """Ask a group server to invite someone to the group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/invite"
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        user_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.invite_to_group(
-            group_id, user_id, requester_user_id, content
-        )
-
-        return 200, new_content
-
-
-class FederationGroupsAcceptInviteServlet(BaseGroupsServerServlet):
-    """Accept an invitation from the group server"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/accept_invite"
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        user_id: str,
-    ) -> Tuple[int, JsonDict]:
-        if get_domain_from_id(user_id) != origin:
-            raise SynapseError(403, "user_id doesn't match origin")
-
-        new_content = await self.handler.accept_invite(group_id, user_id, content)
-
-        return 200, new_content
-
-
-class FederationGroupsJoinServlet(BaseGroupsServerServlet):
-    """Attempt to join a group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/join"
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        user_id: str,
-    ) -> Tuple[int, JsonDict]:
-        if get_domain_from_id(user_id) != origin:
-            raise SynapseError(403, "user_id doesn't match origin")
-
-        new_content = await self.handler.join_group(group_id, user_id, content)
-
-        return 200, new_content
-
-
-class FederationGroupsRemoveUserServlet(BaseGroupsServerServlet):
-    """Leave or kick a user from the group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/remove"
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        user_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.remove_user_from_group(
-            group_id, user_id, requester_user_id, content
-        )
-
-        return 200, new_content
-
-
-class FederationGroupsSummaryRoomsServlet(BaseGroupsServerServlet):
-    """Add/remove a room from the group summary, with optional category.
-
-    Matches both:
-        - /groups/:group/summary/rooms/:room_id
-        - /groups/:group/summary/categories/:category/rooms/:room_id
-    """
-
-    PATH = (
-        "/groups/(?P<group_id>[^/]*)/summary"
-        "(/categories/(?P<category_id>[^/]+))?"
-        "/rooms/(?P<room_id>[^/]*)"
-    )
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        category_id: str,
-        room_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        if category_id == "":
-            raise SynapseError(
-                400, "category_id cannot be empty string", Codes.INVALID_PARAM
-            )
-
-        if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH:
-            raise SynapseError(
-                400,
-                "category_id may not be longer than %s characters"
-                % (MAX_GROUP_CATEGORYID_LENGTH,),
-                Codes.INVALID_PARAM,
-            )
-
-        resp = await self.handler.update_group_summary_room(
-            group_id,
-            requester_user_id,
-            room_id=room_id,
-            category_id=category_id,
-            content=content,
-        )
-
-        return 200, resp
-
-    async def on_DELETE(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        category_id: str,
-        room_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        if category_id == "":
-            raise SynapseError(400, "category_id cannot be empty string")
-
-        resp = await self.handler.delete_group_summary_room(
-            group_id, requester_user_id, room_id=room_id, category_id=category_id
-        )
-
-        return 200, resp
-
-
-class FederationGroupsCategoriesServlet(BaseGroupsServerServlet):
-    """Get all categories for a group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/categories/?"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        resp = await self.handler.get_group_categories(group_id, requester_user_id)
-
-        return 200, resp
-
-
-class FederationGroupsCategoryServlet(BaseGroupsServerServlet):
-    """Add/remove/get a category in a group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/categories/(?P<category_id>[^/]+)"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        category_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        resp = await self.handler.get_group_category(
-            group_id, requester_user_id, category_id
-        )
-
-        return 200, resp
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        category_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        if category_id == "":
-            raise SynapseError(400, "category_id cannot be empty string")
-
-        if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH:
-            raise SynapseError(
-                400,
-                "category_id may not be longer than %s characters"
-                % (MAX_GROUP_CATEGORYID_LENGTH,),
-                Codes.INVALID_PARAM,
-            )
-
-        resp = await self.handler.upsert_group_category(
-            group_id, requester_user_id, category_id, content
-        )
-
-        return 200, resp
-
-    async def on_DELETE(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        category_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        if category_id == "":
-            raise SynapseError(400, "category_id cannot be empty string")
-
-        resp = await self.handler.delete_group_category(
-            group_id, requester_user_id, category_id
-        )
-
-        return 200, resp
-
-
-class FederationGroupsRolesServlet(BaseGroupsServerServlet):
-    """Get roles in a group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/roles/?"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        resp = await self.handler.get_group_roles(group_id, requester_user_id)
-
-        return 200, resp
-
-
-class FederationGroupsRoleServlet(BaseGroupsServerServlet):
-    """Add/remove/get a role in a group"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/roles/(?P<role_id>[^/]+)"
-
-    async def on_GET(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        role_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        resp = await self.handler.get_group_role(group_id, requester_user_id, role_id)
-
-        return 200, resp
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        role_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        if role_id == "":
-            raise SynapseError(
-                400, "role_id cannot be empty string", Codes.INVALID_PARAM
-            )
-
-        if len(role_id) > MAX_GROUP_ROLEID_LENGTH:
-            raise SynapseError(
-                400,
-                "role_id may not be longer than %s characters"
-                % (MAX_GROUP_ROLEID_LENGTH,),
-                Codes.INVALID_PARAM,
-            )
-
-        resp = await self.handler.update_group_role(
-            group_id, requester_user_id, role_id, content
-        )
-
-        return 200, resp
-
-    async def on_DELETE(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        role_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        if role_id == "":
-            raise SynapseError(400, "role_id cannot be empty string")
-
-        resp = await self.handler.delete_group_role(
-            group_id, requester_user_id, role_id
-        )
-
-        return 200, resp
-
-
-class FederationGroupsSummaryUsersServlet(BaseGroupsServerServlet):
-    """Add/remove a user from the group summary, with optional role.
-
-    Matches both:
-        - /groups/:group/summary/users/:user_id
-        - /groups/:group/summary/roles/:role/users/:user_id
-    """
-
-    PATH = (
-        "/groups/(?P<group_id>[^/]*)/summary"
-        "(/roles/(?P<role_id>[^/]+))?"
-        "/users/(?P<user_id>[^/]*)"
-    )
-
-    async def on_POST(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        role_id: str,
-        user_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        if role_id == "":
-            raise SynapseError(400, "role_id cannot be empty string")
-
-        if len(role_id) > MAX_GROUP_ROLEID_LENGTH:
-            raise SynapseError(
-                400,
-                "role_id may not be longer than %s characters"
-                % (MAX_GROUP_ROLEID_LENGTH,),
-                Codes.INVALID_PARAM,
-            )
-
-        resp = await self.handler.update_group_summary_user(
-            group_id,
-            requester_user_id,
-            user_id=user_id,
-            role_id=role_id,
-            content=content,
-        )
-
-        return 200, resp
-
-    async def on_DELETE(
-        self,
-        origin: str,
-        content: Literal[None],
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-        role_id: str,
-        user_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        if role_id == "":
-            raise SynapseError(400, "role_id cannot be empty string")
-
-        resp = await self.handler.delete_group_summary_user(
-            group_id, requester_user_id, user_id=user_id, role_id=role_id
-        )
-
-        return 200, resp
-
-
-class FederationGroupsSettingJoinPolicyServlet(BaseGroupsServerServlet):
-    """Sets whether a group is joinable without an invite or knock"""
-
-    PATH = "/groups/(?P<group_id>[^/]*)/settings/m.join_policy"
-
-    async def on_PUT(
-        self,
-        origin: str,
-        content: JsonDict,
-        query: Dict[bytes, List[bytes]],
-        group_id: str,
-    ) -> Tuple[int, JsonDict]:
-        requester_user_id = parse_string_from_args(
-            query, "requester_user_id", required=True
-        )
-        if get_domain_from_id(requester_user_id) != origin:
-            raise SynapseError(403, "requester_user_id doesn't match origin")
-
-        new_content = await self.handler.set_group_join_policy(
-            group_id, requester_user_id, content
-        )
-
-        return 200, new_content
-
-
-GROUP_SERVER_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = (
-    FederationGroupsProfileServlet,
-    FederationGroupsSummaryServlet,
-    FederationGroupsRoomsServlet,
-    FederationGroupsUsersServlet,
-    FederationGroupsInvitedUsersServlet,
-    FederationGroupsInviteServlet,
-    FederationGroupsAcceptInviteServlet,
-    FederationGroupsJoinServlet,
-    FederationGroupsRemoveUserServlet,
-    FederationGroupsSummaryRoomsServlet,
-    FederationGroupsCategoriesServlet,
-    FederationGroupsCategoryServlet,
-    FederationGroupsRolesServlet,
-    FederationGroupsRoleServlet,
-    FederationGroupsSummaryUsersServlet,
-    FederationGroupsAddRoomsServlet,
-    FederationGroupsAddRoomsConfigServlet,
-    FederationGroupsSettingJoinPolicyServlet,
-)