summary refs log tree commit diff
diff options
context:
space:
mode:
authorPatrick Cloke <clokep@users.noreply.github.com>2022-05-26 09:04:34 -0400
committerGitHub <noreply@github.com>2022-05-26 09:04:34 -0400
commit49f06866e4db2e19467a3733b2909ba397da265e (patch)
tree549f907d582a0bda1cd5f1c6139a1db4561e013b
parentBump pyjwt from 2.3.0 to 2.4.0 (#12865) (diff)
downloadsynapse-49f06866e4db2e19467a3733b2909ba397da265e.tar.xz
Remove backing code for groups/communities (#12558)
Including handlers, configuration code, appservice support, and
the GroupID construct.
-rw-r--r--changelog.d/12558.removal1
-rw-r--r--synapse/appservice/__init__.py43
-rw-r--r--synapse/config/_base.pyi2
-rw-r--r--synapse/config/groups.py27
-rw-r--r--synapse/config/homeserver.py2
-rw-r--r--synapse/groups/__init__.py0
-rw-r--r--synapse/groups/attestations.py218
-rw-r--r--synapse/groups/groups_server.py1019
-rw-r--r--synapse/handlers/groups_local.py503
-rw-r--r--synapse/server.py39
-rw-r--r--synapse/types.py23
-rw-r--r--tests/appservice/test_appservice.py2
-rw-r--r--tests/test_types.py21
13 files changed, 6 insertions, 1894 deletions
diff --git a/changelog.d/12558.removal b/changelog.d/12558.removal
new file mode 100644
index 0000000000..41f6fae5da
--- /dev/null
+++ b/changelog.d/12558.removal
@@ -0,0 +1 @@
+Remove support for the non-standard groups/communities feature from Synapse.
diff --git a/synapse/appservice/__init__.py b/synapse/appservice/__init__.py
index a610fb785d..ed92c2e910 100644
--- a/synapse/appservice/__init__.py
+++ b/synapse/appservice/__init__.py
@@ -23,13 +23,7 @@ from netaddr import IPSet
 
 from synapse.api.constants import EventTypes
 from synapse.events import EventBase
-from synapse.types import (
-    DeviceListUpdates,
-    GroupID,
-    JsonDict,
-    UserID,
-    get_domain_from_id,
-)
+from synapse.types import DeviceListUpdates, JsonDict, UserID
 from synapse.util.caches.descriptors import _CacheContext, cached
 
 if TYPE_CHECKING:
@@ -55,7 +49,6 @@ class ApplicationServiceState(Enum):
 @attr.s(slots=True, frozen=True, auto_attribs=True)
 class Namespace:
     exclusive: bool
-    group_id: Optional[str]
     regex: Pattern[str]
 
 
@@ -141,30 +134,13 @@ class ApplicationService:
                 exclusive = regex_obj.get("exclusive")
                 if not isinstance(exclusive, bool):
                     raise ValueError("Expected bool for 'exclusive' in ns '%s'" % ns)
-                group_id = regex_obj.get("group_id")
-                if group_id:
-                    if not isinstance(group_id, str):
-                        raise ValueError(
-                            "Expected string for 'group_id' in ns '%s'" % ns
-                        )
-                    try:
-                        GroupID.from_string(group_id)
-                    except Exception:
-                        raise ValueError(
-                            "Expected valid group ID for 'group_id' in ns '%s'" % ns
-                        )
-
-                    if get_domain_from_id(group_id) != self.server_name:
-                        raise ValueError(
-                            "Expected 'group_id' to be this host in ns '%s'" % ns
-                        )
 
                 regex = regex_obj.get("regex")
                 if not isinstance(regex, str):
                     raise ValueError("Expected string for 'regex' in ns '%s'" % ns)
 
                 # Pre-compile regex.
-                result[ns].append(Namespace(exclusive, group_id, re.compile(regex)))
+                result[ns].append(Namespace(exclusive, re.compile(regex)))
 
         return result
 
@@ -369,21 +345,6 @@ class ApplicationService:
             if namespace.exclusive
         ]
 
-    def get_groups_for_user(self, user_id: str) -> Iterable[str]:
-        """Get the groups that this user is associated with by this AS
-
-        Args:
-            user_id: The ID of the user.
-
-        Returns:
-            An iterable that yields group_id strings.
-        """
-        return (
-            namespace.group_id
-            for namespace in self.namespaces[ApplicationService.NS_USERS]
-            if namespace.group_id and namespace.regex.match(user_id)
-        )
-
     def is_rate_limited(self) -> bool:
         return self.rate_limited
 
diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi
index 71d6655fda..01ea2b4dab 100644
--- a/synapse/config/_base.pyi
+++ b/synapse/config/_base.pyi
@@ -32,7 +32,6 @@ from synapse.config import (
     emailconfig,
     experimental,
     federation,
-    groups,
     jwt,
     key,
     logger,
@@ -107,7 +106,6 @@ class RootConfig:
     push: push.PushConfig
     spamchecker: spam_checker.SpamCheckerConfig
     room: room.RoomConfig
-    groups: groups.GroupsConfig
     userdirectory: user_directory.UserDirectoryConfig
     consent: consent.ConsentConfig
     stats: stats.StatsConfig
diff --git a/synapse/config/groups.py b/synapse/config/groups.py
deleted file mode 100644
index baa051fdd4..0000000000
--- a/synapse/config/groups.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright 2017 New Vector Ltd
-#
-# 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 Any
-
-from synapse.types import JsonDict
-
-from ._base import Config
-
-
-class GroupsConfig(Config):
-    section = "groups"
-
-    def read_config(self, config: JsonDict, **kwargs: Any) -> None:
-        self.enable_group_creation = config.get("enable_group_creation", False)
-        self.group_creation_prefix = config.get("group_creation_prefix", "")
diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py
index a4ec706908..4d2b298a70 100644
--- a/synapse/config/homeserver.py
+++ b/synapse/config/homeserver.py
@@ -25,7 +25,6 @@ from .database import DatabaseConfig
 from .emailconfig import EmailConfig
 from .experimental import ExperimentalConfig
 from .federation import FederationConfig
-from .groups import GroupsConfig
 from .jwt import JWTConfig
 from .key import KeyConfig
 from .logger import LoggingConfig
@@ -89,7 +88,6 @@ class HomeServerConfig(RootConfig):
         PushConfig,
         SpamCheckerConfig,
         RoomConfig,
-        GroupsConfig,
         UserDirectoryConfig,
         ConsentConfig,
         StatsConfig,
diff --git a/synapse/groups/__init__.py b/synapse/groups/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
--- a/synapse/groups/__init__.py
+++ /dev/null
diff --git a/synapse/groups/attestations.py b/synapse/groups/attestations.py
deleted file mode 100644
index ed26d6a6ce..0000000000
--- a/synapse/groups/attestations.py
+++ /dev/null
@@ -1,218 +0,0 @@
-# Copyright 2017 Vector Creations Ltd
-#
-# 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.
-
-"""Attestations ensure that users and groups can't lie about their memberships.
-
-When a user joins a group the HS and GS swap attestations, which allow them
-both to independently prove to third parties their membership.These
-attestations have a validity period so need to be periodically renewed.
-
-If a user leaves (or gets kicked out of) a group, either side can still use
-their attestation to "prove" their membership, until the attestation expires.
-Therefore attestations shouldn't be relied on to prove membership in important
-cases, but can for less important situations, e.g. showing a users membership
-of groups on their profile, showing flairs, etc.
-
-An attestation is a signed blob of json that looks like:
-
-    {
-        "user_id": "@foo:a.example.com",
-        "group_id": "+bar:b.example.com",
-        "valid_until_ms": 1507994728530,
-        "signatures":{"matrix.org":{"ed25519:auto":"..."}}
-    }
-"""
-
-import logging
-import random
-from typing import TYPE_CHECKING, Optional, Tuple
-
-from signedjson.sign import sign_json
-
-from twisted.internet.defer import Deferred
-
-from synapse.api.errors import HttpResponseException, RequestSendFailed, SynapseError
-from synapse.metrics.background_process_metrics import run_as_background_process
-from synapse.types import JsonDict, get_domain_from_id
-
-if TYPE_CHECKING:
-    from synapse.server import HomeServer
-
-logger = logging.getLogger(__name__)
-
-
-# Default validity duration for new attestations we create
-DEFAULT_ATTESTATION_LENGTH_MS = 3 * 24 * 60 * 60 * 1000
-
-# We add some jitter to the validity duration of attestations so that if we
-# add lots of users at once we don't need to renew them all at once.
-# The jitter is a multiplier picked randomly between the first and second number
-DEFAULT_ATTESTATION_JITTER = (0.9, 1.3)
-
-# Start trying to update our attestations when they come this close to expiring
-UPDATE_ATTESTATION_TIME_MS = 1 * 24 * 60 * 60 * 1000
-
-
-class GroupAttestationSigning:
-    """Creates and verifies group attestations."""
-
-    def __init__(self, hs: "HomeServer"):
-        self.keyring = hs.get_keyring()
-        self.clock = hs.get_clock()
-        self.server_name = hs.hostname
-        self.signing_key = hs.signing_key
-
-    async def verify_attestation(
-        self,
-        attestation: JsonDict,
-        group_id: str,
-        user_id: str,
-        server_name: Optional[str] = None,
-    ) -> None:
-        """Verifies that the given attestation matches the given parameters.
-
-        An optional server_name can be supplied to explicitly set which server's
-        signature is expected. Otherwise assumes that either the group_id or user_id
-        is local and uses the other's server as the one to check.
-        """
-
-        if not server_name:
-            if get_domain_from_id(group_id) == self.server_name:
-                server_name = get_domain_from_id(user_id)
-            elif get_domain_from_id(user_id) == self.server_name:
-                server_name = get_domain_from_id(group_id)
-            else:
-                raise Exception("Expected either group_id or user_id to be local")
-
-        if user_id != attestation["user_id"]:
-            raise SynapseError(400, "Attestation has incorrect user_id")
-
-        if group_id != attestation["group_id"]:
-            raise SynapseError(400, "Attestation has incorrect group_id")
-        valid_until_ms = attestation["valid_until_ms"]
-
-        # TODO: We also want to check that *new* attestations that people give
-        # us to store are valid for at least a little while.
-        now = self.clock.time_msec()
-        if valid_until_ms < now:
-            raise SynapseError(400, "Attestation expired")
-
-        assert server_name is not None
-        await self.keyring.verify_json_for_server(
-            server_name,
-            attestation,
-            now,
-        )
-
-    def create_attestation(self, group_id: str, user_id: str) -> JsonDict:
-        """Create an attestation for the group_id and user_id with default
-        validity length.
-        """
-        validity_period = DEFAULT_ATTESTATION_LENGTH_MS * random.uniform(
-            *DEFAULT_ATTESTATION_JITTER
-        )
-        valid_until_ms = int(self.clock.time_msec() + validity_period)
-
-        return sign_json(
-            {
-                "group_id": group_id,
-                "user_id": user_id,
-                "valid_until_ms": valid_until_ms,
-            },
-            self.server_name,
-            self.signing_key,
-        )
-
-
-class GroupAttestionRenewer:
-    """Responsible for sending and receiving attestation updates."""
-
-    def __init__(self, hs: "HomeServer"):
-        self.clock = hs.get_clock()
-        self.store = hs.get_datastores().main
-        self.assestations = hs.get_groups_attestation_signing()
-        self.transport_client = hs.get_federation_transport_client()
-        self.is_mine_id = hs.is_mine_id
-        self.attestations = hs.get_groups_attestation_signing()
-
-        if not hs.config.worker.worker_app:
-            self._renew_attestations_loop = self.clock.looping_call(
-                self._start_renew_attestations, 30 * 60 * 1000
-            )
-
-    async def on_renew_attestation(
-        self, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """When a remote updates an attestation"""
-        attestation = content["attestation"]
-
-        if not self.is_mine_id(group_id) and not self.is_mine_id(user_id):
-            raise SynapseError(400, "Neither user not group are on this server")
-
-        await self.attestations.verify_attestation(
-            attestation, user_id=user_id, group_id=group_id
-        )
-
-        await self.store.update_remote_attestion(group_id, user_id, attestation)
-
-        return {}
-
-    def _start_renew_attestations(self) -> "Deferred[None]":
-        return run_as_background_process("renew_attestations", self._renew_attestations)
-
-    async def _renew_attestations(self) -> None:
-        """Called periodically to check if we need to update any of our attestations"""
-
-        now = self.clock.time_msec()
-
-        rows = await self.store.get_attestations_need_renewals(
-            now + UPDATE_ATTESTATION_TIME_MS
-        )
-
-        async def _renew_attestation(group_user: Tuple[str, str]) -> None:
-            group_id, user_id = group_user
-            try:
-                if not self.is_mine_id(group_id):
-                    destination = get_domain_from_id(group_id)
-                elif not self.is_mine_id(user_id):
-                    destination = get_domain_from_id(user_id)
-                else:
-                    logger.warning(
-                        "Incorrectly trying to do attestations for user: %r in %r",
-                        user_id,
-                        group_id,
-                    )
-                    await self.store.remove_attestation_renewal(group_id, user_id)
-                    return
-
-                attestation = self.attestations.create_attestation(group_id, user_id)
-
-                await self.transport_client.renew_group_attestation(
-                    destination, group_id, user_id, content={"attestation": attestation}
-                )
-
-                await self.store.update_attestation_renewal(
-                    group_id, user_id, attestation
-                )
-            except (RequestSendFailed, HttpResponseException) as e:
-                logger.warning(
-                    "Failed to renew attestation of %r in %r: %s", user_id, group_id, e
-                )
-            except Exception:
-                logger.exception(
-                    "Error renewing attestation of %r in %r", user_id, group_id
-                )
-
-        for row in rows:
-            await _renew_attestation((row["group_id"], row["user_id"]))
diff --git a/synapse/groups/groups_server.py b/synapse/groups/groups_server.py
deleted file mode 100644
index dfd24af695..0000000000
--- a/synapse/groups/groups_server.py
+++ /dev/null
@@ -1,1019 +0,0 @@
-# Copyright 2017 Vector Creations Ltd
-# Copyright 2018 New Vector Ltd
-# Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-#
-# 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.
-
-import logging
-from typing import TYPE_CHECKING, Optional
-
-from synapse.api.errors import Codes, SynapseError
-from synapse.handlers.groups_local import GroupsLocalHandler
-from synapse.handlers.profile import MAX_AVATAR_URL_LEN, MAX_DISPLAYNAME_LEN
-from synapse.types import GroupID, JsonDict, RoomID, UserID, get_domain_from_id
-from synapse.util.async_helpers import concurrently_execute
-
-if TYPE_CHECKING:
-    from synapse.server import HomeServer
-
-logger = logging.getLogger(__name__)
-
-
-# TODO: Allow users to "knock" or simply join depending on rules
-# TODO: Federation admin APIs
-# TODO: is_privileged flag to users and is_public to users and rooms
-# TODO: Audit log for admins (profile updates, membership changes, users who tried
-#       to join but were rejected, etc)
-# TODO: Flairs
-
-
-# Note that the maximum lengths are somewhat arbitrary.
-MAX_SHORT_DESC_LEN = 1000
-MAX_LONG_DESC_LEN = 10000
-
-
-class GroupsServerWorkerHandler:
-    def __init__(self, hs: "HomeServer"):
-        self.hs = hs
-        self.store = hs.get_datastores().main
-        self.room_list_handler = hs.get_room_list_handler()
-        self.auth = hs.get_auth()
-        self.clock = hs.get_clock()
-        self.keyring = hs.get_keyring()
-        self.is_mine_id = hs.is_mine_id
-        self.signing_key = hs.signing_key
-        self.server_name = hs.hostname
-        self.attestations = hs.get_groups_attestation_signing()
-        self.transport_client = hs.get_federation_transport_client()
-        self.profile_handler = hs.get_profile_handler()
-
-    async def check_group_is_ours(
-        self,
-        group_id: str,
-        requester_user_id: str,
-        and_exists: bool = False,
-        and_is_admin: Optional[str] = None,
-    ) -> Optional[dict]:
-        """Check that the group is ours, and optionally if it exists.
-
-        If group does exist then return group.
-
-        Args:
-            group_id: The group ID to check.
-            requester_user_id: The user ID of the requester.
-            and_exists: whether to also check if group exists
-            and_is_admin: whether to also check if given str is a user_id
-                that is an admin
-        """
-        if not self.is_mine_id(group_id):
-            raise SynapseError(400, "Group not on this server")
-
-        group = await self.store.get_group(group_id)
-        if and_exists and not group:
-            raise SynapseError(404, "Unknown group")
-
-        is_user_in_group = await self.store.is_user_in_group(
-            requester_user_id, group_id
-        )
-        if group and not is_user_in_group and not group["is_public"]:
-            raise SynapseError(404, "Unknown group")
-
-        if and_is_admin:
-            is_admin = await self.store.is_user_admin_in_group(group_id, and_is_admin)
-            if not is_admin:
-                raise SynapseError(403, "User is not admin in group")
-
-        return group
-
-    async def get_group_summary(
-        self, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get the summary for a group as seen by requester_user_id.
-
-        The group summary consists of the profile of the room, and a curated
-        list of users and rooms. These list *may* be organised by role/category.
-        The roles/categories are ordered, and so are the users/rooms within them.
-
-        A user/room may appear in multiple roles/categories.
-        """
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        is_user_in_group = await self.store.is_user_in_group(
-            requester_user_id, group_id
-        )
-
-        profile = await self.get_group_profile(group_id, requester_user_id)
-
-        users, roles = await self.store.get_users_for_summary_by_role(
-            group_id, include_private=is_user_in_group
-        )
-
-        # TODO: Add profiles to users
-
-        rooms, categories = await self.store.get_rooms_for_summary_by_category(
-            group_id, include_private=is_user_in_group
-        )
-
-        for room_entry in rooms:
-            room_id = room_entry["room_id"]
-            joined_users = await self.store.get_users_in_room(room_id)
-            entry = await self.room_list_handler.generate_room_entry(
-                room_id, len(joined_users), with_alias=False, allow_private=True
-            )
-            if entry is None:
-                continue
-            entry = dict(entry)  # so we don't change what's cached
-            entry.pop("room_id", None)
-
-            room_entry["profile"] = entry
-
-        rooms.sort(key=lambda e: e.get("order", 0))
-
-        for user in users:
-            user_id = user["user_id"]
-
-            if not self.is_mine_id(requester_user_id):
-                attestation = await self.store.get_remote_attestation(group_id, user_id)
-                if not attestation:
-                    continue
-
-                user["attestation"] = attestation
-            else:
-                user["attestation"] = self.attestations.create_attestation(
-                    group_id, user_id
-                )
-
-            user_profile = await self.profile_handler.get_profile_from_cache(user_id)
-            user.update(user_profile)
-
-        users.sort(key=lambda e: e.get("order", 0))
-
-        membership_info = await self.store.get_users_membership_info_in_group(
-            group_id, requester_user_id
-        )
-
-        return {
-            "profile": profile,
-            "users_section": {
-                "users": users,
-                "roles": roles,
-                "total_user_count_estimate": 0,  # TODO
-            },
-            "rooms_section": {
-                "rooms": rooms,
-                "categories": categories,
-                "total_room_count_estimate": 0,  # TODO
-            },
-            "user": membership_info,
-        }
-
-    async def get_group_categories(
-        self, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get all categories in a group (as seen by user)"""
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        categories = await self.store.get_group_categories(group_id=group_id)
-        return {"categories": categories}
-
-    async def get_group_category(
-        self, group_id: str, requester_user_id: str, category_id: str
-    ) -> JsonDict:
-        """Get a specific category in a group (as seen by user)"""
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        return await self.store.get_group_category(
-            group_id=group_id, category_id=category_id
-        )
-
-    async def get_group_roles(self, group_id: str, requester_user_id: str) -> JsonDict:
-        """Get all roles in a group (as seen by user)"""
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        roles = await self.store.get_group_roles(group_id=group_id)
-        return {"roles": roles}
-
-    async def get_group_role(
-        self, group_id: str, requester_user_id: str, role_id: str
-    ) -> JsonDict:
-        """Get a specific role in a group (as seen by user)"""
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        return await self.store.get_group_role(group_id=group_id, role_id=role_id)
-
-    async def get_group_profile(
-        self, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get the group profile as seen by requester_user_id"""
-
-        await self.check_group_is_ours(group_id, requester_user_id)
-
-        group = await self.store.get_group(group_id)
-
-        if group:
-            cols = [
-                "name",
-                "short_description",
-                "long_description",
-                "avatar_url",
-                "is_public",
-            ]
-            group_description = {key: group[key] for key in cols}
-            group_description["is_openly_joinable"] = group["join_policy"] == "open"
-
-            return group_description
-        else:
-            raise SynapseError(404, "Unknown group")
-
-    async def get_users_in_group(
-        self, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get the users in group as seen by requester_user_id.
-
-        The ordering is arbitrary at the moment
-        """
-
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        is_user_in_group = await self.store.is_user_in_group(
-            requester_user_id, group_id
-        )
-
-        user_results = await self.store.get_users_in_group(
-            group_id, include_private=is_user_in_group
-        )
-
-        chunk = []
-        for user_result in user_results:
-            g_user_id = user_result["user_id"]
-            is_public = user_result["is_public"]
-            is_privileged = user_result["is_admin"]
-
-            entry = {"user_id": g_user_id}
-
-            profile = await self.profile_handler.get_profile_from_cache(g_user_id)
-            entry.update(profile)
-
-            entry["is_public"] = bool(is_public)
-            entry["is_privileged"] = bool(is_privileged)
-
-            if not self.is_mine_id(g_user_id):
-                attestation = await self.store.get_remote_attestation(
-                    group_id, g_user_id
-                )
-                if not attestation:
-                    continue
-
-                entry["attestation"] = attestation
-            else:
-                entry["attestation"] = self.attestations.create_attestation(
-                    group_id, g_user_id
-                )
-
-            chunk.append(entry)
-
-        # TODO: If admin add lists of users whose attestations have timed out
-
-        return {"chunk": chunk, "total_user_count_estimate": len(user_results)}
-
-    async def get_invited_users_in_group(
-        self, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get the users that have been invited to a group as seen by requester_user_id.
-
-        The ordering is arbitrary at the moment
-        """
-
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        is_user_in_group = await self.store.is_user_in_group(
-            requester_user_id, group_id
-        )
-
-        if not is_user_in_group:
-            raise SynapseError(403, "User not in group")
-
-        invited_users = await self.store.get_invited_users_in_group(group_id)
-
-        user_profiles = []
-
-        for user_id in invited_users:
-            user_profile = {"user_id": user_id}
-            try:
-                profile = await self.profile_handler.get_profile_from_cache(user_id)
-                user_profile.update(profile)
-            except Exception as e:
-                logger.warning("Error getting profile for %s: %s", user_id, e)
-            user_profiles.append(user_profile)
-
-        return {"chunk": user_profiles, "total_user_count_estimate": len(invited_users)}
-
-    async def get_rooms_in_group(
-        self, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get the rooms in group as seen by requester_user_id
-
-        This returns rooms in order of decreasing number of joined users
-        """
-
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        is_user_in_group = await self.store.is_user_in_group(
-            requester_user_id, group_id
-        )
-
-        # Note! room_results["is_public"] is about whether the room is considered
-        # public from the group's point of view. (i.e. whether non-group members
-        # should be able to see the room is in the group).
-        # This is not the same as whether the room itself is public (in the sense
-        # of being visible in the room directory).
-        # As such, room_results["is_public"] itself is not sufficient to determine
-        # whether any given user is permitted to see the room's metadata.
-        room_results = await self.store.get_rooms_in_group(
-            group_id, include_private=is_user_in_group
-        )
-
-        chunk = []
-        for room_result in room_results:
-            room_id = room_result["room_id"]
-
-            joined_users = await self.store.get_users_in_room(room_id)
-
-            # check the user is actually allowed to see the room before showing it to them
-            allow_private = requester_user_id in joined_users
-
-            entry = await self.room_list_handler.generate_room_entry(
-                room_id,
-                len(joined_users),
-                with_alias=False,
-                allow_private=allow_private,
-            )
-
-            if not entry:
-                continue
-
-            entry["is_public"] = bool(room_result["is_public"])
-
-            chunk.append(entry)
-
-        chunk.sort(key=lambda e: -e["num_joined_members"])
-
-        return {"chunk": chunk, "total_room_count_estimate": len(chunk)}
-
-
-class GroupsServerHandler(GroupsServerWorkerHandler):
-    def __init__(self, hs: "HomeServer"):
-        super().__init__(hs)
-
-        # Ensure attestations get renewed
-        hs.get_groups_attestation_renewer()
-
-    async def update_group_summary_room(
-        self,
-        group_id: str,
-        requester_user_id: str,
-        room_id: str,
-        category_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Add/update a room to the group summary"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        RoomID.from_string(room_id)  # Ensure valid room id
-
-        order = content.get("order", None)
-
-        is_public = _parse_visibility_from_contents(content)
-
-        await self.store.add_room_to_summary(
-            group_id=group_id,
-            room_id=room_id,
-            category_id=category_id,
-            order=order,
-            is_public=is_public,
-        )
-
-        return {}
-
-    async def delete_group_summary_room(
-        self, group_id: str, requester_user_id: str, room_id: str, category_id: str
-    ) -> JsonDict:
-        """Remove a room from the summary"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        await self.store.remove_room_from_summary(
-            group_id=group_id, room_id=room_id, category_id=category_id
-        )
-
-        return {}
-
-    async def set_group_join_policy(
-        self, group_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Sets the group join policy.
-
-        Currently supported policies are:
-         - "invite": an invite must be received and accepted in order to join.
-         - "open": anyone can join.
-        """
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        join_policy = _parse_join_policy_from_contents(content)
-        if join_policy is None:
-            raise SynapseError(400, "No value specified for 'm.join_policy'")
-
-        await self.store.set_group_join_policy(group_id, join_policy=join_policy)
-
-        return {}
-
-    async def update_group_category(
-        self, group_id: str, requester_user_id: str, category_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Add/Update a group category"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        is_public = _parse_visibility_from_contents(content)
-        profile = content.get("profile")
-
-        await self.store.upsert_group_category(
-            group_id=group_id,
-            category_id=category_id,
-            is_public=is_public,
-            profile=profile,
-        )
-
-        return {}
-
-    async def delete_group_category(
-        self, group_id: str, requester_user_id: str, category_id: str
-    ) -> JsonDict:
-        """Delete a group category"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        await self.store.remove_group_category(
-            group_id=group_id, category_id=category_id
-        )
-
-        return {}
-
-    async def update_group_role(
-        self, group_id: str, requester_user_id: str, role_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Add/update a role in a group"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        is_public = _parse_visibility_from_contents(content)
-
-        profile = content.get("profile")
-
-        await self.store.upsert_group_role(
-            group_id=group_id, role_id=role_id, is_public=is_public, profile=profile
-        )
-
-        return {}
-
-    async def delete_group_role(
-        self, group_id: str, requester_user_id: str, role_id: str
-    ) -> JsonDict:
-        """Remove role from group"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        await self.store.remove_group_role(group_id=group_id, role_id=role_id)
-
-        return {}
-
-    async def update_group_summary_user(
-        self,
-        group_id: str,
-        requester_user_id: str,
-        user_id: str,
-        role_id: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Add/update a users entry in the group summary"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        order = content.get("order", None)
-
-        is_public = _parse_visibility_from_contents(content)
-
-        await self.store.add_user_to_summary(
-            group_id=group_id,
-            user_id=user_id,
-            role_id=role_id,
-            order=order,
-            is_public=is_public,
-        )
-
-        return {}
-
-    async def delete_group_summary_user(
-        self, group_id: str, requester_user_id: str, user_id: str, role_id: str
-    ) -> JsonDict:
-        """Remove a user from the group summary"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        await self.store.remove_user_from_summary(
-            group_id=group_id, user_id=user_id, role_id=role_id
-        )
-
-        return {}
-
-    async def update_group_profile(
-        self, group_id: str, requester_user_id: str, content: JsonDict
-    ) -> None:
-        """Update the group profile"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        profile = {}
-        for keyname, max_length in (
-            ("name", MAX_DISPLAYNAME_LEN),
-            ("avatar_url", MAX_AVATAR_URL_LEN),
-            ("short_description", MAX_SHORT_DESC_LEN),
-            ("long_description", MAX_LONG_DESC_LEN),
-        ):
-            if keyname in content:
-                value = content[keyname]
-                if not isinstance(value, str):
-                    raise SynapseError(
-                        400,
-                        "%r value is not a string" % (keyname,),
-                        errcode=Codes.INVALID_PARAM,
-                    )
-                if len(value) > max_length:
-                    raise SynapseError(
-                        400,
-                        "Invalid %s parameter" % (keyname,),
-                        errcode=Codes.INVALID_PARAM,
-                    )
-                profile[keyname] = value
-
-        await self.store.update_group_profile(group_id, profile)
-
-    async def add_room_to_group(
-        self, group_id: str, requester_user_id: str, room_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Add room to group"""
-        RoomID.from_string(room_id)  # Ensure valid room id
-
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        is_public = _parse_visibility_from_contents(content)
-
-        await self.store.add_room_to_group(group_id, room_id, is_public=is_public)
-
-        return {}
-
-    async def update_room_in_group(
-        self,
-        group_id: str,
-        requester_user_id: str,
-        room_id: str,
-        config_key: str,
-        content: JsonDict,
-    ) -> JsonDict:
-        """Update room in group"""
-        RoomID.from_string(room_id)  # Ensure valid room id
-
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        if config_key == "m.visibility":
-            is_public = _parse_visibility_dict(content)
-
-            await self.store.update_room_in_group_visibility(
-                group_id, room_id, is_public=is_public
-            )
-        else:
-            raise SynapseError(400, "Unknown config option")
-
-        return {}
-
-    async def remove_room_from_group(
-        self, group_id: str, requester_user_id: str, room_id: str
-    ) -> JsonDict:
-        """Remove room from group"""
-        await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-
-        await self.store.remove_room_from_group(group_id, room_id)
-
-        return {}
-
-    async def invite_to_group(
-        self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Invite user to group"""
-
-        group = await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id
-        )
-        if not group:
-            raise SynapseError(400, "Group does not exist", errcode=Codes.BAD_STATE)
-
-        # TODO: Check if user knocked
-
-        invited_users = await self.store.get_invited_users_in_group(group_id)
-        if user_id in invited_users:
-            raise SynapseError(
-                400, "User already invited to group", errcode=Codes.BAD_STATE
-            )
-
-        user_results = await self.store.get_users_in_group(
-            group_id, include_private=True
-        )
-        if user_id in (user_result["user_id"] for user_result in user_results):
-            raise SynapseError(400, "User already in group")
-
-        content = {
-            "profile": {"name": group["name"], "avatar_url": group["avatar_url"]},
-            "inviter": requester_user_id,
-        }
-
-        if self.hs.is_mine_id(user_id):
-            groups_local = self.hs.get_groups_local_handler()
-            assert isinstance(
-                groups_local, GroupsLocalHandler
-            ), "Workers cannot invites users to groups."
-            res = await groups_local.on_invite(group_id, user_id, content)
-            local_attestation = None
-        else:
-            local_attestation = self.attestations.create_attestation(group_id, user_id)
-            content.update({"attestation": local_attestation})
-
-            res = await self.transport_client.invite_to_group_notification(
-                get_domain_from_id(user_id), group_id, user_id, content
-            )
-
-            user_profile = res.get("user_profile", {})
-            await self.store.add_remote_profile_cache(
-                user_id,
-                displayname=user_profile.get("displayname"),
-                avatar_url=user_profile.get("avatar_url"),
-            )
-
-        if res["state"] == "join":
-            if not self.hs.is_mine_id(user_id):
-                remote_attestation = res["attestation"]
-
-                await self.attestations.verify_attestation(
-                    remote_attestation, user_id=user_id, group_id=group_id
-                )
-            else:
-                remote_attestation = None
-
-            await self.store.add_user_to_group(
-                group_id,
-                user_id,
-                is_admin=False,
-                is_public=False,  # TODO
-                local_attestation=local_attestation,
-                remote_attestation=remote_attestation,
-            )
-            return {"state": "join"}
-        elif res["state"] == "invite":
-            await self.store.add_group_invite(group_id, user_id)
-            return {"state": "invite"}
-        elif res["state"] == "reject":
-            return {"state": "reject"}
-        else:
-            raise SynapseError(502, "Unknown state returned by HS")
-
-    async def _add_user(
-        self, group_id: str, user_id: str, content: JsonDict
-    ) -> Optional[JsonDict]:
-        """Add a user to a group based on a content dict.
-
-        See accept_invite, join_group.
-        """
-        if not self.hs.is_mine_id(user_id):
-            local_attestation: Optional[
-                JsonDict
-            ] = self.attestations.create_attestation(group_id, user_id)
-
-            remote_attestation = content["attestation"]
-
-            await self.attestations.verify_attestation(
-                remote_attestation, user_id=user_id, group_id=group_id
-            )
-        else:
-            local_attestation = None
-            remote_attestation = None
-
-        is_public = _parse_visibility_from_contents(content)
-
-        await self.store.add_user_to_group(
-            group_id,
-            user_id,
-            is_admin=False,
-            is_public=is_public,
-            local_attestation=local_attestation,
-            remote_attestation=remote_attestation,
-        )
-
-        return local_attestation
-
-    async def accept_invite(
-        self, group_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """User tries to accept an invite to the group.
-
-        This is different from them asking to join, and so should error if no
-        invite exists (and they're not a member of the group)
-        """
-
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        is_invited = await self.store.is_user_invited_to_local_group(
-            group_id, requester_user_id
-        )
-        if not is_invited:
-            raise SynapseError(403, "User not invited to group")
-
-        local_attestation = await self._add_user(group_id, requester_user_id, content)
-
-        return {"state": "join", "attestation": local_attestation}
-
-    async def join_group(
-        self, group_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """User tries to join the group.
-
-        This will error if the group requires an invite/knock to join
-        """
-
-        group_info = await self.check_group_is_ours(
-            group_id, requester_user_id, and_exists=True
-        )
-        if not group_info:
-            raise SynapseError(404, "Group does not exist", errcode=Codes.NOT_FOUND)
-        if group_info["join_policy"] != "open":
-            raise SynapseError(403, "Group is not publicly joinable")
-
-        local_attestation = await self._add_user(group_id, requester_user_id, content)
-
-        return {"state": "join", "attestation": local_attestation}
-
-    async def remove_user_from_group(
-        self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Remove a user from the group; either a user is leaving or an admin
-        kicked them.
-        """
-
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        is_kick = False
-        if requester_user_id != user_id:
-            is_admin = await self.store.is_user_admin_in_group(
-                group_id, requester_user_id
-            )
-            if not is_admin:
-                raise SynapseError(403, "User is not admin in group")
-
-            is_kick = True
-
-        await self.store.remove_user_from_group(group_id, user_id)
-
-        if is_kick:
-            if self.hs.is_mine_id(user_id):
-                groups_local = self.hs.get_groups_local_handler()
-                assert isinstance(
-                    groups_local, GroupsLocalHandler
-                ), "Workers cannot remove users from groups."
-                await groups_local.user_removed_from_group(group_id, user_id, {})
-            else:
-                await self.transport_client.remove_user_from_group_notification(
-                    get_domain_from_id(user_id), group_id, user_id, {}
-                )
-
-        if not self.hs.is_mine_id(user_id):
-            await self.store.maybe_delete_remote_profile_cache(user_id)
-
-        # Delete group if the last user has left
-        users = await self.store.get_users_in_group(group_id, include_private=True)
-        if not users:
-            await self.store.delete_group(group_id)
-
-        return {}
-
-    async def create_group(
-        self, group_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        logger.info("Attempting to create group with ID: %r", group_id)
-
-        # parsing the id into a GroupID validates it.
-        group_id_obj = GroupID.from_string(group_id)
-
-        group = await self.check_group_is_ours(group_id, requester_user_id)
-        if group:
-            raise SynapseError(400, "Group already exists")
-
-        is_admin = await self.auth.is_server_admin(
-            UserID.from_string(requester_user_id)
-        )
-        if not is_admin:
-            if not self.hs.config.groups.enable_group_creation:
-                raise SynapseError(
-                    403, "Only a server admin can create groups on this server"
-                )
-            localpart = group_id_obj.localpart
-            if not localpart.startswith(self.hs.config.groups.group_creation_prefix):
-                raise SynapseError(
-                    400,
-                    "Can only create groups with prefix %r on this server"
-                    % (self.hs.config.groups.group_creation_prefix,),
-                )
-
-        profile = content.get("profile", {})
-        name = profile.get("name")
-        avatar_url = profile.get("avatar_url")
-        short_description = profile.get("short_description")
-        long_description = profile.get("long_description")
-        user_profile = content.get("user_profile", {})
-
-        await self.store.create_group(
-            group_id,
-            requester_user_id,
-            name=name,
-            avatar_url=avatar_url,
-            short_description=short_description,
-            long_description=long_description,
-        )
-
-        if not self.hs.is_mine_id(requester_user_id):
-            remote_attestation = content["attestation"]
-
-            await self.attestations.verify_attestation(
-                remote_attestation, user_id=requester_user_id, group_id=group_id
-            )
-
-            local_attestation: Optional[
-                JsonDict
-            ] = self.attestations.create_attestation(group_id, requester_user_id)
-        else:
-            local_attestation = None
-            remote_attestation = None
-
-        await self.store.add_user_to_group(
-            group_id,
-            requester_user_id,
-            is_admin=True,
-            is_public=True,  # TODO
-            local_attestation=local_attestation,
-            remote_attestation=remote_attestation,
-        )
-
-        if not self.hs.is_mine_id(requester_user_id):
-            await self.store.add_remote_profile_cache(
-                requester_user_id,
-                displayname=user_profile.get("displayname"),
-                avatar_url=user_profile.get("avatar_url"),
-            )
-
-        return {"group_id": group_id}
-
-    async def delete_group(self, group_id: str, requester_user_id: str) -> None:
-        """Deletes a group, kicking out all current members.
-
-        Only group admins or server admins can call this request
-
-        Args:
-            group_id: The group ID to delete.
-            requester_user_id: The user requesting to delete the group.
-        """
-
-        await self.check_group_is_ours(group_id, requester_user_id, and_exists=True)
-
-        # Only server admins or group admins can delete groups.
-
-        is_admin = await self.store.is_user_admin_in_group(group_id, requester_user_id)
-
-        if not is_admin:
-            is_admin = await self.auth.is_server_admin(
-                UserID.from_string(requester_user_id)
-            )
-
-        if not is_admin:
-            raise SynapseError(403, "User is not an admin")
-
-        # Before deleting the group lets kick everyone out of it
-        users = await self.store.get_users_in_group(group_id, include_private=True)
-
-        async def _kick_user_from_group(user_id: str) -> None:
-            if self.hs.is_mine_id(user_id):
-                groups_local = self.hs.get_groups_local_handler()
-                assert isinstance(
-                    groups_local, GroupsLocalHandler
-                ), "Workers cannot kick users from groups."
-                await groups_local.user_removed_from_group(group_id, user_id, {})
-            else:
-                await self.transport_client.remove_user_from_group_notification(
-                    get_domain_from_id(user_id), group_id, user_id, {}
-                )
-                await self.store.maybe_delete_remote_profile_cache(user_id)
-
-        # We kick users out in the order of:
-        #   1. Non-admins
-        #   2. Other admins
-        #   3. The requester
-        #
-        # This is so that if the deletion fails for some reason other admins or
-        # the requester still has auth to retry.
-        non_admins = []
-        admins = []
-        for u in users:
-            if u["user_id"] == requester_user_id:
-                continue
-            if u["is_admin"]:
-                admins.append(u["user_id"])
-            else:
-                non_admins.append(u["user_id"])
-
-        await concurrently_execute(_kick_user_from_group, non_admins, 10)
-        await concurrently_execute(_kick_user_from_group, admins, 10)
-        await _kick_user_from_group(requester_user_id)
-
-        await self.store.delete_group(group_id)
-
-
-def _parse_join_policy_from_contents(content: JsonDict) -> Optional[str]:
-    """Given a content for a request, return the specified join policy or None"""
-
-    join_policy_dict = content.get("m.join_policy")
-    if join_policy_dict:
-        return _parse_join_policy_dict(join_policy_dict)
-    else:
-        return None
-
-
-def _parse_join_policy_dict(join_policy_dict: JsonDict) -> str:
-    """Given a dict for the "m.join_policy" config return the join policy specified"""
-    join_policy_type = join_policy_dict.get("type")
-    if not join_policy_type:
-        return "invite"
-
-    if join_policy_type not in ("invite", "open"):
-        raise SynapseError(400, "Synapse only supports 'invite'/'open' join rule")
-    return join_policy_type
-
-
-def _parse_visibility_from_contents(content: JsonDict) -> bool:
-    """Given a content for a request parse out whether the entity should be
-    public or not
-    """
-
-    visibility = content.get("m.visibility")
-    if visibility:
-        return _parse_visibility_dict(visibility)
-    else:
-        is_public = True
-
-    return is_public
-
-
-def _parse_visibility_dict(visibility: JsonDict) -> bool:
-    """Given a dict for the "m.visibility" config return if the entity should
-    be public or not
-    """
-    vis_type = visibility.get("type")
-    if not vis_type:
-        return True
-
-    if vis_type not in ("public", "private"):
-        raise SynapseError(400, "Synapse only supports 'public'/'private' visibility")
-    return vis_type == "public"
diff --git a/synapse/handlers/groups_local.py b/synapse/handlers/groups_local.py
deleted file mode 100644
index e7a399787b..0000000000
--- a/synapse/handlers/groups_local.py
+++ /dev/null
@@ -1,503 +0,0 @@
-# Copyright 2017 Vector Creations Ltd
-# Copyright 2018 New Vector Ltd
-#
-# 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.
-
-import logging
-from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Iterable, List, Set
-
-from synapse.api.errors import HttpResponseException, RequestSendFailed, SynapseError
-from synapse.types import GroupID, JsonDict, get_domain_from_id
-
-if TYPE_CHECKING:
-    from synapse.server import HomeServer
-
-logger = logging.getLogger(__name__)
-
-
-def _create_rerouter(func_name: str) -> Callable[..., Awaitable[JsonDict]]:
-    """Returns an async function that looks at the group id and calls the function
-    on federation or the local group server if the group is local
-    """
-
-    async def f(
-        self: "GroupsLocalWorkerHandler", group_id: str, *args: Any, **kwargs: Any
-    ) -> JsonDict:
-        if not GroupID.is_valid(group_id):
-            raise SynapseError(400, "%s is not a legal group ID" % (group_id,))
-
-        if self.is_mine_id(group_id):
-            return await getattr(self.groups_server_handler, func_name)(
-                group_id, *args, **kwargs
-            )
-        else:
-            destination = get_domain_from_id(group_id)
-
-            try:
-                return await getattr(self.transport_client, func_name)(
-                    destination, group_id, *args, **kwargs
-                )
-            except HttpResponseException as e:
-                # Capture errors returned by the remote homeserver and
-                # re-throw specific errors as SynapseErrors. This is so
-                # when the remote end responds with things like 403 Not
-                # In Group, we can communicate that to the client instead
-                # of a 500.
-                raise e.to_synapse_error()
-            except RequestSendFailed:
-                raise SynapseError(502, "Failed to contact group server")
-
-    return f
-
-
-class GroupsLocalWorkerHandler:
-    def __init__(self, hs: "HomeServer"):
-        self.hs = hs
-        self.store = hs.get_datastores().main
-        self.room_list_handler = hs.get_room_list_handler()
-        self.groups_server_handler = hs.get_groups_server_handler()
-        self.transport_client = hs.get_federation_transport_client()
-        self.auth = hs.get_auth()
-        self.clock = hs.get_clock()
-        self.keyring = hs.get_keyring()
-        self.is_mine_id = hs.is_mine_id
-        self.signing_key = hs.signing_key
-        self.server_name = hs.hostname
-        self.notifier = hs.get_notifier()
-        self.attestations = hs.get_groups_attestation_signing()
-
-        self.profile_handler = hs.get_profile_handler()
-
-    # The following functions merely route the query to the local groups server
-    # or federation depending on if the group is local or remote
-
-    get_group_profile = _create_rerouter("get_group_profile")
-    get_rooms_in_group = _create_rerouter("get_rooms_in_group")
-    get_invited_users_in_group = _create_rerouter("get_invited_users_in_group")
-    get_group_category = _create_rerouter("get_group_category")
-    get_group_categories = _create_rerouter("get_group_categories")
-    get_group_role = _create_rerouter("get_group_role")
-    get_group_roles = _create_rerouter("get_group_roles")
-
-    async def get_group_summary(
-        self, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get the group summary for a group.
-
-        If the group is remote we check that the users have valid attestations.
-        """
-        if self.is_mine_id(group_id):
-            res = await self.groups_server_handler.get_group_summary(
-                group_id, requester_user_id
-            )
-        else:
-            try:
-                res = await self.transport_client.get_group_summary(
-                    get_domain_from_id(group_id), group_id, requester_user_id
-                )
-            except HttpResponseException as e:
-                raise e.to_synapse_error()
-            except RequestSendFailed:
-                raise SynapseError(502, "Failed to contact group server")
-
-            group_server_name = get_domain_from_id(group_id)
-
-            # Loop through the users and validate the attestations.
-            chunk = res["users_section"]["users"]
-            valid_users = []
-            for entry in chunk:
-                g_user_id = entry["user_id"]
-                attestation = entry.pop("attestation", {})
-                try:
-                    if get_domain_from_id(g_user_id) != group_server_name:
-                        await self.attestations.verify_attestation(
-                            attestation,
-                            group_id=group_id,
-                            user_id=g_user_id,
-                            server_name=get_domain_from_id(g_user_id),
-                        )
-                    valid_users.append(entry)
-                except Exception as e:
-                    logger.info("Failed to verify user is in group: %s", e)
-
-            res["users_section"]["users"] = valid_users
-
-            res["users_section"]["users"].sort(key=lambda e: e.get("order", 0))
-            res["rooms_section"]["rooms"].sort(key=lambda e: e.get("order", 0))
-
-        # Add `is_publicised` flag to indicate whether the user has publicised their
-        # membership of the group on their profile
-        result = await self.store.get_publicised_groups_for_user(requester_user_id)
-        is_publicised = group_id in result
-
-        res.setdefault("user", {})["is_publicised"] = is_publicised
-
-        return res
-
-    async def get_users_in_group(
-        self, group_id: str, requester_user_id: str
-    ) -> JsonDict:
-        """Get users in a group"""
-        if self.is_mine_id(group_id):
-            return await self.groups_server_handler.get_users_in_group(
-                group_id, requester_user_id
-            )
-
-        group_server_name = get_domain_from_id(group_id)
-
-        try:
-            res = await self.transport_client.get_users_in_group(
-                get_domain_from_id(group_id), group_id, requester_user_id
-            )
-        except HttpResponseException as e:
-            raise e.to_synapse_error()
-        except RequestSendFailed:
-            raise SynapseError(502, "Failed to contact group server")
-
-        chunk = res["chunk"]
-        valid_entries = []
-        for entry in chunk:
-            g_user_id = entry["user_id"]
-            attestation = entry.pop("attestation", {})
-            try:
-                if get_domain_from_id(g_user_id) != group_server_name:
-                    await self.attestations.verify_attestation(
-                        attestation,
-                        group_id=group_id,
-                        user_id=g_user_id,
-                        server_name=get_domain_from_id(g_user_id),
-                    )
-                valid_entries.append(entry)
-            except Exception as e:
-                logger.info("Failed to verify user is in group: %s", e)
-
-        res["chunk"] = valid_entries
-
-        return res
-
-    async def get_joined_groups(self, user_id: str) -> JsonDict:
-        group_ids = await self.store.get_joined_groups(user_id)
-        return {"groups": group_ids}
-
-    async def get_publicised_groups_for_user(self, user_id: str) -> JsonDict:
-        if self.hs.is_mine_id(user_id):
-            result = await self.store.get_publicised_groups_for_user(user_id)
-
-            # Check AS associated groups for this user - this depends on the
-            # RegExps in the AS registration file (under `users`)
-            for app_service in self.store.get_app_services():
-                result.extend(app_service.get_groups_for_user(user_id))
-
-            return {"groups": result}
-        else:
-            try:
-                bulk_result = await self.transport_client.bulk_get_publicised_groups(
-                    get_domain_from_id(user_id), [user_id]
-                )
-            except HttpResponseException as e:
-                raise e.to_synapse_error()
-            except RequestSendFailed:
-                raise SynapseError(502, "Failed to contact group server")
-
-            result = bulk_result.get("users", {}).get(user_id)
-            # TODO: Verify attestations
-            return {"groups": result}
-
-    async def bulk_get_publicised_groups(
-        self, user_ids: Iterable[str], proxy: bool = True
-    ) -> JsonDict:
-        destinations: Dict[str, Set[str]] = {}
-        local_users = set()
-
-        for user_id in user_ids:
-            if self.hs.is_mine_id(user_id):
-                local_users.add(user_id)
-            else:
-                destinations.setdefault(get_domain_from_id(user_id), set()).add(user_id)
-
-        if not proxy and destinations:
-            raise SynapseError(400, "Some user_ids are not local")
-
-        results = {}
-        failed_results: List[str] = []
-        for destination, dest_user_ids in destinations.items():
-            try:
-                r = await self.transport_client.bulk_get_publicised_groups(
-                    destination, list(dest_user_ids)
-                )
-                results.update(r["users"])
-            except Exception:
-                failed_results.extend(dest_user_ids)
-
-        for uid in local_users:
-            results[uid] = await self.store.get_publicised_groups_for_user(uid)
-
-            # Check AS associated groups for this user - this depends on the
-            # RegExps in the AS registration file (under `users`)
-            for app_service in self.store.get_app_services():
-                results[uid].extend(app_service.get_groups_for_user(uid))
-
-        return {"users": results}
-
-
-class GroupsLocalHandler(GroupsLocalWorkerHandler):
-    def __init__(self, hs: "HomeServer"):
-        super().__init__(hs)
-
-        # Ensure attestations get renewed
-        hs.get_groups_attestation_renewer()
-
-    # The following functions merely route the query to the local groups server
-    # or federation depending on if the group is local or remote
-
-    update_group_profile = _create_rerouter("update_group_profile")
-
-    add_room_to_group = _create_rerouter("add_room_to_group")
-    update_room_in_group = _create_rerouter("update_room_in_group")
-    remove_room_from_group = _create_rerouter("remove_room_from_group")
-
-    update_group_summary_room = _create_rerouter("update_group_summary_room")
-    delete_group_summary_room = _create_rerouter("delete_group_summary_room")
-
-    update_group_category = _create_rerouter("update_group_category")
-    delete_group_category = _create_rerouter("delete_group_category")
-
-    update_group_summary_user = _create_rerouter("update_group_summary_user")
-    delete_group_summary_user = _create_rerouter("delete_group_summary_user")
-
-    update_group_role = _create_rerouter("update_group_role")
-    delete_group_role = _create_rerouter("delete_group_role")
-
-    set_group_join_policy = _create_rerouter("set_group_join_policy")
-
-    async def create_group(
-        self, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Create a group"""
-
-        logger.info("Asking to create group with ID: %r", group_id)
-
-        if self.is_mine_id(group_id):
-            res = await self.groups_server_handler.create_group(
-                group_id, user_id, content
-            )
-            local_attestation = None
-            remote_attestation = None
-        else:
-            raise SynapseError(400, "Unable to create remote groups")
-
-        is_publicised = content.get("publicise", False)
-        token = await self.store.register_user_group_membership(
-            group_id,
-            user_id,
-            membership="join",
-            is_admin=True,
-            local_attestation=local_attestation,
-            remote_attestation=remote_attestation,
-            is_publicised=is_publicised,
-        )
-        self.notifier.on_new_event("groups_key", token, users=[user_id])
-
-        return res
-
-    async def join_group(
-        self, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Request to join a group"""
-        if self.is_mine_id(group_id):
-            await self.groups_server_handler.join_group(group_id, user_id, content)
-            local_attestation = None
-            remote_attestation = None
-        else:
-            local_attestation = self.attestations.create_attestation(group_id, user_id)
-            content["attestation"] = local_attestation
-
-            try:
-                res = await self.transport_client.join_group(
-                    get_domain_from_id(group_id), group_id, user_id, content
-                )
-            except HttpResponseException as e:
-                raise e.to_synapse_error()
-            except RequestSendFailed:
-                raise SynapseError(502, "Failed to contact group server")
-
-            remote_attestation = res["attestation"]
-
-            await self.attestations.verify_attestation(
-                remote_attestation,
-                group_id=group_id,
-                user_id=user_id,
-                server_name=get_domain_from_id(group_id),
-            )
-
-        # TODO: Check that the group is public and we're being added publicly
-        is_publicised = content.get("publicise", False)
-
-        token = await self.store.register_user_group_membership(
-            group_id,
-            user_id,
-            membership="join",
-            is_admin=False,
-            local_attestation=local_attestation,
-            remote_attestation=remote_attestation,
-            is_publicised=is_publicised,
-        )
-        self.notifier.on_new_event("groups_key", token, users=[user_id])
-
-        return {}
-
-    async def accept_invite(
-        self, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Accept an invite to a group"""
-        if self.is_mine_id(group_id):
-            await self.groups_server_handler.accept_invite(group_id, user_id, content)
-            local_attestation = None
-            remote_attestation = None
-        else:
-            local_attestation = self.attestations.create_attestation(group_id, user_id)
-            content["attestation"] = local_attestation
-
-            try:
-                res = await self.transport_client.accept_group_invite(
-                    get_domain_from_id(group_id), group_id, user_id, content
-                )
-            except HttpResponseException as e:
-                raise e.to_synapse_error()
-            except RequestSendFailed:
-                raise SynapseError(502, "Failed to contact group server")
-
-            remote_attestation = res["attestation"]
-
-            await self.attestations.verify_attestation(
-                remote_attestation,
-                group_id=group_id,
-                user_id=user_id,
-                server_name=get_domain_from_id(group_id),
-            )
-
-        # TODO: Check that the group is public and we're being added publicly
-        is_publicised = content.get("publicise", False)
-
-        token = await self.store.register_user_group_membership(
-            group_id,
-            user_id,
-            membership="join",
-            is_admin=False,
-            local_attestation=local_attestation,
-            remote_attestation=remote_attestation,
-            is_publicised=is_publicised,
-        )
-        self.notifier.on_new_event("groups_key", token, users=[user_id])
-
-        return {}
-
-    async def invite(
-        self, group_id: str, user_id: str, requester_user_id: str, config: JsonDict
-    ) -> JsonDict:
-        """Invite a user to a group"""
-        content = {"requester_user_id": requester_user_id, "config": config}
-        if self.is_mine_id(group_id):
-            res = await self.groups_server_handler.invite_to_group(
-                group_id, user_id, requester_user_id, content
-            )
-        else:
-            try:
-                res = await self.transport_client.invite_to_group(
-                    get_domain_from_id(group_id),
-                    group_id,
-                    user_id,
-                    requester_user_id,
-                    content,
-                )
-            except HttpResponseException as e:
-                raise e.to_synapse_error()
-            except RequestSendFailed:
-                raise SynapseError(502, "Failed to contact group server")
-
-        return res
-
-    async def on_invite(
-        self, group_id: str, user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """One of our users were invited to a group"""
-        # TODO: Support auto join and rejection
-
-        if not self.is_mine_id(user_id):
-            raise SynapseError(400, "User not on this server")
-
-        local_profile = {}
-        if "profile" in content:
-            if "name" in content["profile"]:
-                local_profile["name"] = content["profile"]["name"]
-            if "avatar_url" in content["profile"]:
-                local_profile["avatar_url"] = content["profile"]["avatar_url"]
-
-        token = await self.store.register_user_group_membership(
-            group_id,
-            user_id,
-            membership="invite",
-            content={"profile": local_profile, "inviter": content["inviter"]},
-        )
-        self.notifier.on_new_event("groups_key", token, users=[user_id])
-        try:
-            user_profile = await self.profile_handler.get_profile(user_id)
-        except Exception as e:
-            logger.warning("No profile for user %s: %s", user_id, e)
-            user_profile = {}
-
-        return {"state": "invite", "user_profile": user_profile}
-
-    async def remove_user_from_group(
-        self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict
-    ) -> JsonDict:
-        """Remove a user from a group"""
-        if user_id == requester_user_id:
-            token = await self.store.register_user_group_membership(
-                group_id, user_id, membership="leave"
-            )
-            self.notifier.on_new_event("groups_key", token, users=[user_id])
-
-            # TODO: Should probably remember that we tried to leave so that we can
-            # retry if the group server is currently down.
-
-        if self.is_mine_id(group_id):
-            res = await self.groups_server_handler.remove_user_from_group(
-                group_id, user_id, requester_user_id, content
-            )
-        else:
-            content["requester_user_id"] = requester_user_id
-            try:
-                res = await self.transport_client.remove_user_from_group(
-                    get_domain_from_id(group_id),
-                    group_id,
-                    requester_user_id,
-                    user_id,
-                    content,
-                )
-            except HttpResponseException as e:
-                raise e.to_synapse_error()
-            except RequestSendFailed:
-                raise SynapseError(502, "Failed to contact group server")
-
-        return res
-
-    async def user_removed_from_group(
-        self, group_id: str, user_id: str, content: JsonDict
-    ) -> None:
-        """One of our users was removed/kicked from a group"""
-        # TODO: Check if user in group
-        token = await self.store.register_user_group_membership(
-            group_id, user_id, membership="leave"
-        )
-        self.notifier.on_new_event("groups_key", token, users=[user_id])
diff --git a/synapse/server.py b/synapse/server.py
index ee60cce8eb..3fd23aaf52 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -21,17 +21,7 @@
 import abc
 import functools
 import logging
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Callable,
-    Dict,
-    List,
-    Optional,
-    TypeVar,
-    Union,
-    cast,
-)
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, TypeVar, cast
 
 from twisted.internet.interfaces import IOpenSSLContextFactory
 from twisted.internet.tcp import Port
@@ -60,8 +50,6 @@ from synapse.federation.federation_server import (
 from synapse.federation.send_queue import FederationRemoteSendQueue
 from synapse.federation.sender import AbstractFederationSender, FederationSender
 from synapse.federation.transport.client import TransportLayerClient
-from synapse.groups.attestations import GroupAttestationSigning, GroupAttestionRenewer
-from synapse.groups.groups_server import GroupsServerHandler, GroupsServerWorkerHandler
 from synapse.handlers.account import AccountHandler
 from synapse.handlers.account_data import AccountDataHandler
 from synapse.handlers.account_validity import AccountValidityHandler
@@ -79,7 +67,6 @@ from synapse.handlers.event_auth import EventAuthHandler
 from synapse.handlers.events import EventHandler, EventStreamHandler
 from synapse.handlers.federation import FederationHandler
 from synapse.handlers.federation_event import FederationEventHandler
-from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerHandler
 from synapse.handlers.identity import IdentityHandler
 from synapse.handlers.initial_sync import InitialSyncHandler
 from synapse.handlers.message import EventCreationHandler, MessageHandler
@@ -652,30 +639,6 @@ class HomeServer(metaclass=abc.ABCMeta):
         return UserDirectoryHandler(self)
 
     @cache_in_self
-    def get_groups_local_handler(
-        self,
-    ) -> Union[GroupsLocalWorkerHandler, GroupsLocalHandler]:
-        if self.config.worker.worker_app:
-            return GroupsLocalWorkerHandler(self)
-        else:
-            return GroupsLocalHandler(self)
-
-    @cache_in_self
-    def get_groups_server_handler(self):
-        if self.config.worker.worker_app:
-            return GroupsServerWorkerHandler(self)
-        else:
-            return GroupsServerHandler(self)
-
-    @cache_in_self
-    def get_groups_attestation_signing(self) -> GroupAttestationSigning:
-        return GroupAttestationSigning(self)
-
-    @cache_in_self
-    def get_groups_attestation_renewer(self) -> GroupAttestionRenewer:
-        return GroupAttestionRenewer(self)
-
-    @cache_in_self
     def get_stats_handler(self) -> StatsHandler:
         return StatsHandler(self)
 
diff --git a/synapse/types.py b/synapse/types.py
index 6f7128ddd6..091cc611ab 100644
--- a/synapse/types.py
+++ b/synapse/types.py
@@ -320,29 +320,6 @@ class EventID(DomainSpecificString):
     SIGIL = "$"
 
 
-@attr.s(slots=True, frozen=True, repr=False)
-class GroupID(DomainSpecificString):
-    """Structure representing a group ID."""
-
-    SIGIL = "+"
-
-    @classmethod
-    def from_string(cls: Type[DS], s: str) -> DS:
-        group_id: DS = super().from_string(s)  # type: ignore
-
-        if not group_id.localpart:
-            raise SynapseError(400, "Group ID cannot be empty", Codes.INVALID_PARAM)
-
-        if contains_invalid_mxid_characters(group_id.localpart):
-            raise SynapseError(
-                400,
-                "Group ID can only contain characters a-z, 0-9, or '=_-./'",
-                Codes.INVALID_PARAM,
-            )
-
-        return group_id
-
-
 mxid_localpart_allowed_characters = set(
     "_-./=" + string.ascii_lowercase + string.digits
 )
diff --git a/tests/appservice/test_appservice.py b/tests/appservice/test_appservice.py
index edc584d0cf..7135362f76 100644
--- a/tests/appservice/test_appservice.py
+++ b/tests/appservice/test_appservice.py
@@ -23,7 +23,7 @@ from tests.test_utils import simple_async_mock
 
 
 def _regex(regex: str, exclusive: bool = True) -> Namespace:
-    return Namespace(exclusive, None, re.compile(regex))
+    return Namespace(exclusive, re.compile(regex))
 
 
 class ApplicationServiceTestCase(unittest.TestCase):
diff --git a/tests/test_types.py b/tests/test_types.py
index 80888a744d..0b10dae848 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 from synapse.api.errors import SynapseError
-from synapse.types import GroupID, RoomAlias, UserID, map_username_to_mxid_localpart
+from synapse.types import RoomAlias, UserID, map_username_to_mxid_localpart
 
 from tests import unittest
 
@@ -62,25 +62,6 @@ class RoomAliasTestCase(unittest.HomeserverTestCase):
         self.assertFalse(RoomAlias.is_valid(id_string))
 
 
-class GroupIDTestCase(unittest.TestCase):
-    def test_parse(self):
-        group_id = GroupID.from_string("+group/=_-.123:my.domain")
-        self.assertEqual("group/=_-.123", group_id.localpart)
-        self.assertEqual("my.domain", group_id.domain)
-
-    def test_validate(self):
-        bad_ids = ["$badsigil:domain", "+:empty"] + [
-            "+group" + c + ":domain" for c in "A%?æ£"
-        ]
-        for id_string in bad_ids:
-            try:
-                GroupID.from_string(id_string)
-                self.fail("Parsing '%s' should raise exception" % id_string)
-            except SynapseError as exc:
-                self.assertEqual(400, exc.code)
-                self.assertEqual("M_INVALID_PARAM", exc.errcode)
-
-
 class MapUsernameTestCase(unittest.TestCase):
     def testPassThrough(self):
         self.assertEqual(map_username_to_mxid_localpart("test1234"), "test1234")