Makes it so that groups/communities no longer exist from a user-POV. E.g. we remove: * All API endpoints (including Client-Server, Server-Server, and admin). * Documented configuration options (and the experimental flag, which is now unused). * Special handling during room upgrades. * The `groups` section of the `/sync` response.tags/v1.61.0rc1
@@ -0,0 +1 @@ | |||
Remove support for the non-standard groups/communities feature from Synapse. |
@@ -2521,16 +2521,6 @@ push: | |||
# "events_default": 1 | |||
# Uncomment to allow non-server-admin users to create groups on this server | |||
# | |||
#enable_group_creation: true | |||
# If enabled, non server admins can only create groups with local parts | |||
# starting with this prefix | |||
# | |||
#group_creation_prefix: "unofficial_" | |||
# User Directory configuration | |||
# | |||
@@ -3145,25 +3145,6 @@ Example configuration: | |||
encryption_enabled_by_default_for_room_type: invite | |||
``` | |||
--- | |||
Config option: `enable_group_creation` | |||
Set to true to allow non-server-admin users to create groups on this server | |||
Example configuration: | |||
```yaml | |||
enable_group_creation: true | |||
``` | |||
Config option: `group_creation_prefix` | |||
If enabled/present, non-server admins can only create groups with local parts | |||
starting with this prefix. | |||
Example configuration: | |||
```yaml | |||
group_creation_prefix: "unofficial_" | |||
``` | |||
Config option: `user_directory` | |||
This setting defines options related to the user directory. | |||
@@ -31,11 +31,6 @@ MAX_ALIAS_LENGTH = 255 | |||
# the maximum length for a user id is 255 characters | |||
MAX_USERID_LENGTH = 255 | |||
# The maximum length for a group id is 255 characters | |||
MAX_GROUPID_LENGTH = 255 | |||
MAX_GROUP_CATEGORYID_LENGTH = 255 | |||
MAX_GROUP_ROLEID_LENGTH = 255 | |||
class Membership: | |||
@@ -69,7 +69,6 @@ from synapse.rest.admin import register_servlets_for_media_repo | |||
from synapse.rest.client import ( | |||
account_data, | |||
events, | |||
groups, | |||
initial_sync, | |||
login, | |||
presence, | |||
@@ -323,9 +322,6 @@ class GenericWorkerServer(HomeServer): | |||
presence.register_servlets(self, resource) | |||
if self.config.experimental.groups_enabled: | |||
groups.register_servlets(self, resource) | |||
resources.update({CLIENT_API_PREFIX: resource}) | |||
resources.update(build_synapse_client_resource_tree(self)) | |||
@@ -73,9 +73,6 @@ class ExperimentalConfig(Config): | |||
# MSC3720 (Account status endpoint) | |||
self.msc3720_enabled: bool = experimental.get("msc3720_enabled", False) | |||
# The deprecated groups feature. | |||
self.groups_enabled: bool = experimental.get("groups_enabled", False) | |||
# MSC2654: Unread counts | |||
self.msc2654_enabled: bool = experimental.get("msc2654_enabled", False) | |||
@@ -25,15 +25,3 @@ class GroupsConfig(Config): | |||
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", "") | |||
def generate_config_section(self, **kwargs: Any) -> str: | |||
return """\ | |||
# Uncomment to allow non-server-admin users to create groups on this server | |||
# | |||
#enable_group_creation: true | |||
# If enabled, non server admins can only create groups with local parts | |||
# starting with this prefix | |||
# | |||
#group_creation_prefix: "unofficial_" | |||
""" |
@@ -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. | |||
@@ -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, | |||
) |
@@ -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, | |||
) |
@@ -1081,17 +1081,6 @@ class RoomMemberHandler(metaclass=abc.ABCMeta): | |||
# Transfer alias mappings in the room directory | |||
await self.store.update_aliases_for_room(old_room_id, room_id) | |||
# Check if any groups we own contain the predecessor room | |||
local_group_ids = await self.store.get_local_groups_for_room(old_room_id) | |||
for group_id in local_group_ids: | |||
# Add new the new room to those groups | |||
await self.store.add_room_to_group( | |||
group_id, room_id, old_room is not None and old_room["is_public"] | |||
) | |||
# Remove the old room from those groups | |||
await self.store.remove_room_from_group(group_id, old_room_id) | |||
async def copy_user_state_on_room_upgrade( | |||
self, old_room_id: str, new_room_id: str, user_ids: Iterable[str] | |||
) -> None: | |||
@@ -166,16 +166,6 @@ class KnockedSyncResult: | |||
return True | |||
@attr.s(slots=True, frozen=True, auto_attribs=True) | |||
class GroupsSyncResult: | |||
join: JsonDict | |||
invite: JsonDict | |||
leave: JsonDict | |||
def __bool__(self) -> bool: | |||
return bool(self.join or self.invite or self.leave) | |||
@attr.s(slots=True, auto_attribs=True) | |||
class _RoomChanges: | |||
"""The set of room entries to include in the sync, plus the set of joined | |||
@@ -206,7 +196,6 @@ class SyncResult: | |||
for this device | |||
device_unused_fallback_key_types: List of key types that have an unused fallback | |||
key | |||
groups: Group updates, if any | |||
""" | |||
next_batch: StreamToken | |||
@@ -220,7 +209,6 @@ class SyncResult: | |||
device_lists: DeviceListUpdates | |||
device_one_time_keys_count: JsonDict | |||
device_unused_fallback_key_types: List[str] | |||
groups: Optional[GroupsSyncResult] | |||
def __bool__(self) -> bool: | |||
"""Make the result appear empty if there are no updates. This is used | |||
@@ -236,7 +224,6 @@ class SyncResult: | |||
or self.account_data | |||
or self.to_device | |||
or self.device_lists | |||
or self.groups | |||
) | |||
@@ -1157,10 +1144,6 @@ class SyncHandler: | |||
await self.store.get_e2e_unused_fallback_key_types(user_id, device_id) | |||
) | |||
if self.hs_config.experimental.groups_enabled: | |||
logger.debug("Fetching group data") | |||
await self._generate_sync_entry_for_groups(sync_result_builder) | |||
num_events = 0 | |||
# debug for https://github.com/matrix-org/synapse/issues/9424 | |||
@@ -1184,57 +1167,11 @@ class SyncHandler: | |||
archived=sync_result_builder.archived, | |||
to_device=sync_result_builder.to_device, | |||
device_lists=device_lists, | |||
groups=sync_result_builder.groups, | |||
device_one_time_keys_count=one_time_key_counts, | |||
device_unused_fallback_key_types=unused_fallback_key_types, | |||
next_batch=sync_result_builder.now_token, | |||
) | |||
@measure_func("_generate_sync_entry_for_groups") | |||
async def _generate_sync_entry_for_groups( | |||
self, sync_result_builder: "SyncResultBuilder" | |||
) -> None: | |||
user_id = sync_result_builder.sync_config.user.to_string() | |||
since_token = sync_result_builder.since_token | |||
now_token = sync_result_builder.now_token | |||
if since_token and since_token.groups_key: | |||
results = await self.store.get_groups_changes_for_user( | |||
user_id, since_token.groups_key, now_token.groups_key | |||
) | |||
else: | |||
results = await self.store.get_all_groups_for_user( | |||
user_id, now_token.groups_key | |||
) | |||
invited = {} | |||
joined = {} | |||
left = {} | |||
for result in results: | |||
membership = result["membership"] | |||
group_id = result["group_id"] | |||
gtype = result["type"] | |||
content = result["content"] | |||
if membership == "join": | |||
if gtype == "membership": | |||
# TODO: Add profile | |||
content.pop("membership", None) | |||
joined[group_id] = content["content"] | |||
else: | |||
joined.setdefault(group_id, {})[gtype] = content | |||
elif membership == "invite": | |||
if gtype == "membership": | |||
content.pop("membership", None) | |||
invited[group_id] = content["content"] | |||
else: | |||
if gtype == "membership": | |||
left[group_id] = content["content"] | |||
sync_result_builder.groups = GroupsSyncResult( | |||
join=joined, invite=invited, leave=left | |||
) | |||
@measure_func("_generate_sync_entry_for_device_list") | |||
async def _generate_sync_entry_for_device_list( | |||
self, | |||
@@ -2333,7 +2270,6 @@ class SyncResultBuilder: | |||
invited | |||
knocked | |||
archived | |||
groups | |||
to_device | |||
""" | |||
@@ -2349,7 +2285,6 @@ class SyncResultBuilder: | |||
invited: List[InvitedSyncResult] = attr.Factory(list) | |||
knocked: List[KnockedSyncResult] = attr.Factory(list) | |||
archived: List[ArchivedSyncResult] = attr.Factory(list) | |||
groups: Optional[GroupsSyncResult] = None | |||
to_device: List[JsonDict] = attr.Factory(list) | |||
def calculate_user_changes(self) -> Tuple[Set[str], Set[str]]: | |||
@@ -26,7 +26,6 @@ from synapse.rest.client import ( | |||
directory, | |||
events, | |||
filter, | |||
groups, | |||
initial_sync, | |||
keys, | |||
knock, | |||
@@ -118,8 +117,6 @@ class ClientRestResource(JsonResource): | |||
thirdparty.register_servlets(hs, client_resource) | |||
sendtodevice.register_servlets(hs, client_resource) | |||
user_directory.register_servlets(hs, client_resource) | |||
if hs.config.experimental.groups_enabled: | |||
groups.register_servlets(hs, client_resource) | |||
room_upgrade_rest_servlet.register_servlets(hs, client_resource) | |||
room_batch.register_servlets(hs, client_resource) | |||
capabilities.register_servlets(hs, client_resource) | |||
@@ -47,7 +47,6 @@ from synapse.rest.admin.federation import ( | |||
DestinationRestServlet, | |||
ListDestinationsRestServlet, | |||
) | |||
from synapse.rest.admin.groups import DeleteGroupAdminRestServlet | |||
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo | |||
from synapse.rest.admin.registration_tokens import ( | |||
ListRegistrationTokensRestServlet, | |||
@@ -293,8 +292,6 @@ def register_servlets_for_client_rest_resource( | |||
ResetPasswordRestServlet(hs).register(http_server) | |||
SearchUsersRestServlet(hs).register(http_server) | |||
UserRegisterServlet(hs).register(http_server) | |||
if hs.config.experimental.groups_enabled: | |||
DeleteGroupAdminRestServlet(hs).register(http_server) | |||
AccountValidityRenewServlet(hs).register(http_server) | |||
# Load the media repo ones if we're using them. Otherwise load the servlets which | |||
@@ -1,50 +0,0 @@ | |||
# Copyright 2019 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. | |||
import logging | |||
from http import HTTPStatus | |||
from typing import TYPE_CHECKING, Tuple | |||
from synapse.api.errors import SynapseError | |||
from synapse.http.servlet import RestServlet | |||
from synapse.http.site import SynapseRequest | |||
from synapse.rest.admin._base import admin_patterns, assert_user_is_admin | |||
from synapse.types import JsonDict | |||
if TYPE_CHECKING: | |||
from synapse.server import HomeServer | |||
logger = logging.getLogger(__name__) | |||
class DeleteGroupAdminRestServlet(RestServlet): | |||
"""Allows deleting of local groups""" | |||
PATTERNS = admin_patterns("/delete_group/(?P<group_id>[^/]*)$") | |||
def __init__(self, hs: "HomeServer"): | |||
self.group_server = hs.get_groups_server_handler() | |||
self.is_mine_id = hs.is_mine_id | |||
self.auth = hs.get_auth() | |||
async def on_POST( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
await assert_user_is_admin(self.auth, requester.user) | |||
if not self.is_mine_id(group_id): | |||
raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only delete local groups") | |||
await self.group_server.delete_group(group_id, requester.user.to_string()) | |||
return HTTPStatus.OK, {} |
@@ -1,962 +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 functools import wraps | |||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Tuple | |||
from twisted.web.server import Request | |||
from synapse.api.constants import ( | |||
MAX_GROUP_CATEGORYID_LENGTH, | |||
MAX_GROUP_ROLEID_LENGTH, | |||
MAX_GROUPID_LENGTH, | |||
) | |||
from synapse.api.errors import Codes, SynapseError | |||
from synapse.handlers.groups_local import GroupsLocalHandler | |||
from synapse.http.server import HttpServer | |||
from synapse.http.servlet import ( | |||
RestServlet, | |||
assert_params_in_dict, | |||
parse_json_object_from_request, | |||
) | |||
from synapse.http.site import SynapseRequest | |||
from synapse.types import GroupID, JsonDict | |||
from ._base import client_patterns | |||
if TYPE_CHECKING: | |||
from synapse.server import HomeServer | |||
logger = logging.getLogger(__name__) | |||
def _validate_group_id( | |||
f: Callable[..., Awaitable[Tuple[int, JsonDict]]] | |||
) -> Callable[..., Awaitable[Tuple[int, JsonDict]]]: | |||
"""Wrapper to validate the form of the group ID. | |||
Can be applied to any on_FOO methods that accepts a group ID as a URL parameter. | |||
""" | |||
@wraps(f) | |||
def wrapper( | |||
self: RestServlet, request: Request, group_id: str, *args: Any, **kwargs: Any | |||
) -> Awaitable[Tuple[int, JsonDict]]: | |||
if not GroupID.is_valid(group_id): | |||
raise SynapseError(400, "%s is not a legal group ID" % (group_id,)) | |||
return f(self, request, group_id, *args, **kwargs) | |||
return wrapper | |||
class GroupServlet(RestServlet): | |||
"""Get the group profile""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/profile$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
group_description = await self.groups_handler.get_group_profile( | |||
group_id, requester_user_id | |||
) | |||
return 200, group_description | |||
@_validate_group_id | |||
async def on_POST( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
assert_params_in_dict( | |||
content, ("name", "avatar_url", "short_description", "long_description") | |||
) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot create group profiles." | |||
await self.groups_handler.update_group_profile( | |||
group_id, requester_user_id, content | |||
) | |||
return 200, {} | |||
class GroupSummaryServlet(RestServlet): | |||
"""Get the full group summary""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/summary$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
get_group_summary = await self.groups_handler.get_group_summary( | |||
group_id, requester_user_id | |||
) | |||
return 200, get_group_summary | |||
class GroupSummaryRoomsCatServlet(RestServlet): | |||
"""Update/delete a rooms entry in the summary. | |||
Matches both: | |||
- /groups/:group/summary/rooms/:room_id | |||
- /groups/:group/summary/categories/:category/rooms/:room_id | |||
""" | |||
PATTERNS = client_patterns( | |||
"/groups/(?P<group_id>[^/]*)/summary" | |||
"(/categories/(?P<category_id>[^/]+))?" | |||
"/rooms/(?P<room_id>[^/]*)$" | |||
) | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, | |||
request: SynapseRequest, | |||
group_id: str, | |||
category_id: Optional[str], | |||
room_id: str, | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
if category_id == "": | |||
raise SynapseError(400, "category_id cannot be empty", Codes.INVALID_PARAM) | |||
if category_id and 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, | |||
) | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group summaries." | |||
resp = await self.groups_handler.update_group_summary_room( | |||
group_id, | |||
requester_user_id, | |||
room_id=room_id, | |||
category_id=category_id, | |||
content=content, | |||
) | |||
return 200, resp | |||
@_validate_group_id | |||
async def on_DELETE( | |||
self, request: SynapseRequest, group_id: str, category_id: str, room_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group profiles." | |||
resp = await self.groups_handler.delete_group_summary_room( | |||
group_id, requester_user_id, room_id=room_id, category_id=category_id | |||
) | |||
return 200, resp | |||
class GroupCategoryServlet(RestServlet): | |||
"""Get/add/update/delete a group category""" | |||
PATTERNS = client_patterns( | |||
"/groups/(?P<group_id>[^/]*)/categories/(?P<category_id>[^/]+)$" | |||
) | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str, category_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
category = await self.groups_handler.get_group_category( | |||
group_id, requester_user_id, category_id=category_id | |||
) | |||
return 200, category | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str, category_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
if not category_id: | |||
raise SynapseError(400, "category_id cannot be empty", 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, | |||
) | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group categories." | |||
resp = await self.groups_handler.update_group_category( | |||
group_id, requester_user_id, category_id=category_id, content=content | |||
) | |||
return 200, resp | |||
@_validate_group_id | |||
async def on_DELETE( | |||
self, request: SynapseRequest, group_id: str, category_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group categories." | |||
resp = await self.groups_handler.delete_group_category( | |||
group_id, requester_user_id, category_id=category_id | |||
) | |||
return 200, resp | |||
class GroupCategoriesServlet(RestServlet): | |||
"""Get all group categories""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/categories/$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
category = await self.groups_handler.get_group_categories( | |||
group_id, requester_user_id | |||
) | |||
return 200, category | |||
class GroupRoleServlet(RestServlet): | |||
"""Get/add/update/delete a group role""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/roles/(?P<role_id>[^/]+)$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str, role_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
category = await self.groups_handler.get_group_role( | |||
group_id, requester_user_id, role_id=role_id | |||
) | |||
return 200, category | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str, role_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
if not role_id: | |||
raise SynapseError(400, "role_id cannot be empty", 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, | |||
) | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group roles." | |||
resp = await self.groups_handler.update_group_role( | |||
group_id, requester_user_id, role_id=role_id, content=content | |||
) | |||
return 200, resp | |||
@_validate_group_id | |||
async def on_DELETE( | |||
self, request: SynapseRequest, group_id: str, role_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group roles." | |||
resp = await self.groups_handler.delete_group_role( | |||
group_id, requester_user_id, role_id=role_id | |||
) | |||
return 200, resp | |||
class GroupRolesServlet(RestServlet): | |||
"""Get all group roles""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/roles/$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
category = await self.groups_handler.get_group_roles( | |||
group_id, requester_user_id | |||
) | |||
return 200, category | |||
class GroupSummaryUsersRoleServlet(RestServlet): | |||
"""Update/delete a user's entry in the summary. | |||
Matches both: | |||
- /groups/:group/summary/users/:room_id | |||
- /groups/:group/summary/roles/:role/users/:user_id | |||
""" | |||
PATTERNS = client_patterns( | |||
"/groups/(?P<group_id>[^/]*)/summary" | |||
"(/roles/(?P<role_id>[^/]+))?" | |||
"/users/(?P<user_id>[^/]*)$" | |||
) | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, | |||
request: SynapseRequest, | |||
group_id: str, | |||
role_id: Optional[str], | |||
user_id: str, | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
if role_id == "": | |||
raise SynapseError(400, "role_id cannot be empty", Codes.INVALID_PARAM) | |||
if role_id and 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, | |||
) | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group summaries." | |||
resp = await self.groups_handler.update_group_summary_user( | |||
group_id, | |||
requester_user_id, | |||
user_id=user_id, | |||
role_id=role_id, | |||
content=content, | |||
) | |||
return 200, resp | |||
@_validate_group_id | |||
async def on_DELETE( | |||
self, request: SynapseRequest, group_id: str, role_id: str, user_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group summaries." | |||
resp = await self.groups_handler.delete_group_summary_user( | |||
group_id, requester_user_id, user_id=user_id, role_id=role_id | |||
) | |||
return 200, resp | |||
class GroupRoomServlet(RestServlet): | |||
"""Get all rooms in a group""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/rooms$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
result = await self.groups_handler.get_rooms_in_group( | |||
group_id, requester_user_id | |||
) | |||
return 200, result | |||
class GroupUsersServlet(RestServlet): | |||
"""Get all users in a group""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/users$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
result = await self.groups_handler.get_users_in_group( | |||
group_id, requester_user_id | |||
) | |||
return 200, result | |||
class GroupInvitedUsersServlet(RestServlet): | |||
"""Get users invited to a group""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/invited_users$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_GET( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
result = await self.groups_handler.get_invited_users_in_group( | |||
group_id, requester_user_id | |||
) | |||
return 200, result | |||
class GroupSettingJoinPolicyServlet(RestServlet): | |||
"""Set group join policy""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/settings/m.join_policy$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group join policy." | |||
result = await self.groups_handler.set_group_join_policy( | |||
group_id, requester_user_id, content | |||
) | |||
return 200, result | |||
class GroupCreateServlet(RestServlet): | |||
"""Create a group""" | |||
PATTERNS = client_patterns("/create_group$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
self.server_name = hs.hostname | |||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
# TODO: Create group on remote server | |||
content = parse_json_object_from_request(request) | |||
localpart = content.pop("localpart") | |||
group_id = GroupID(localpart, self.server_name).to_string() | |||
if not localpart: | |||
raise SynapseError(400, "Group ID cannot be empty", Codes.INVALID_PARAM) | |||
if len(group_id) > MAX_GROUPID_LENGTH: | |||
raise SynapseError( | |||
400, | |||
"Group ID may not be longer than %s characters" % (MAX_GROUPID_LENGTH,), | |||
Codes.INVALID_PARAM, | |||
) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot create groups." | |||
result = await self.groups_handler.create_group( | |||
group_id, requester_user_id, content | |||
) | |||
return 200, result | |||
class GroupAdminRoomsServlet(RestServlet): | |||
"""Add a room to the group""" | |||
PATTERNS = client_patterns( | |||
"/groups/(?P<group_id>[^/]*)/admin/rooms/(?P<room_id>[^/]*)$" | |||
) | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str, room_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify rooms in a group." | |||
result = await self.groups_handler.add_room_to_group( | |||
group_id, requester_user_id, room_id, content | |||
) | |||
return 200, result | |||
@_validate_group_id | |||
async def on_DELETE( | |||
self, request: SynapseRequest, group_id: str, room_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group categories." | |||
result = await self.groups_handler.remove_room_from_group( | |||
group_id, requester_user_id, room_id | |||
) | |||
return 200, result | |||
class GroupAdminRoomsConfigServlet(RestServlet): | |||
"""Update the config of a room in a group""" | |||
PATTERNS = client_patterns( | |||
"/groups/(?P<group_id>[^/]*)/admin/rooms/(?P<room_id>[^/]*)" | |||
"/config/(?P<config_key>[^/]*)$" | |||
) | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str, room_id: str, config_key: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot modify group categories." | |||
result = await self.groups_handler.update_room_in_group( | |||
group_id, requester_user_id, room_id, config_key, content | |||
) | |||
return 200, result | |||
class GroupAdminUsersInviteServlet(RestServlet): | |||
"""Invite a user to the group""" | |||
PATTERNS = client_patterns( | |||
"/groups/(?P<group_id>[^/]*)/admin/users/invite/(?P<user_id>[^/]*)$" | |||
) | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
self.store = hs.get_datastores().main | |||
self.is_mine_id = hs.is_mine_id | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str, user_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
config = content.get("config", {}) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot invite users to a group." | |||
result = await self.groups_handler.invite( | |||
group_id, user_id, requester_user_id, config | |||
) | |||
return 200, result | |||
class GroupAdminUsersKickServlet(RestServlet): | |||
"""Kick a user from the group""" | |||
PATTERNS = client_patterns( | |||
"/groups/(?P<group_id>[^/]*)/admin/users/remove/(?P<user_id>[^/]*)$" | |||
) | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str, user_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot kick users from a group." | |||
result = await self.groups_handler.remove_user_from_group( | |||
group_id, user_id, requester_user_id, content | |||
) | |||
return 200, result | |||
class GroupSelfLeaveServlet(RestServlet): | |||
"""Leave a joined group""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/self/leave$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot leave a group for a users." | |||
result = await self.groups_handler.remove_user_from_group( | |||
group_id, requester_user_id, requester_user_id, content | |||
) | |||
return 200, result | |||
class GroupSelfJoinServlet(RestServlet): | |||
"""Attempt to join a group, or knock""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/self/join$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot join a user to a group." | |||
result = await self.groups_handler.join_group( | |||
group_id, requester_user_id, content | |||
) | |||
return 200, result | |||
class GroupSelfAcceptInviteServlet(RestServlet): | |||
"""Accept a group invite""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/self/accept_invite$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
assert isinstance( | |||
self.groups_handler, GroupsLocalHandler | |||
), "Workers cannot accept an invite to a group." | |||
result = await self.groups_handler.accept_invite( | |||
group_id, requester_user_id, content | |||
) | |||
return 200, result | |||
class GroupSelfUpdatePublicityServlet(RestServlet): | |||
"""Update whether we publicise a users membership of a group""" | |||
PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/self/update_publicity$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.store = hs.get_datastores().main | |||
@_validate_group_id | |||
async def on_PUT( | |||
self, request: SynapseRequest, group_id: str | |||
) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request) | |||
requester_user_id = requester.user.to_string() | |||
content = parse_json_object_from_request(request) | |||
publicise = content["publicise"] | |||
await self.store.update_group_publicity(group_id, requester_user_id, publicise) | |||
return 200, {} | |||
class PublicisedGroupsForUserServlet(RestServlet): | |||
"""Get the list of groups a user is advertising""" | |||
PATTERNS = client_patterns("/publicised_groups/(?P<user_id>[^/]*)$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.store = hs.get_datastores().main | |||
self.groups_handler = hs.get_groups_local_handler() | |||
async def on_GET( | |||
self, request: SynapseRequest, user_id: str | |||
) -> Tuple[int, JsonDict]: | |||
await self.auth.get_user_by_req(request, allow_guest=True) | |||
result = await self.groups_handler.get_publicised_groups_for_user(user_id) | |||
return 200, result | |||
class PublicisedGroupsForUsersServlet(RestServlet): | |||
"""Get the list of groups a user is advertising""" | |||
PATTERNS = client_patterns("/publicised_groups$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.store = hs.get_datastores().main | |||
self.groups_handler = hs.get_groups_local_handler() | |||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: | |||
await self.auth.get_user_by_req(request, allow_guest=True) | |||
content = parse_json_object_from_request(request) | |||
user_ids = content["user_ids"] | |||
result = await self.groups_handler.bulk_get_publicised_groups(user_ids) | |||
return 200, result | |||
class GroupsForUserServlet(RestServlet): | |||
"""Get all groups the logged in user is joined to""" | |||
PATTERNS = client_patterns("/joined_groups$") | |||
def __init__(self, hs: "HomeServer"): | |||
super().__init__() | |||
self.auth = hs.get_auth() | |||
self.clock = hs.get_clock() | |||
self.groups_handler = hs.get_groups_local_handler() | |||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: | |||
requester = await self.auth.get_user_by_req(request, allow_guest=True) | |||
requester_user_id = requester.user.to_string() | |||
result = await self.groups_handler.get_joined_groups(requester_user_id) | |||
return 200, result | |||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: | |||
GroupServlet(hs).register(http_server) | |||
GroupSummaryServlet(hs).register(http_server) | |||
GroupInvitedUsersServlet(hs).register(http_server) | |||
GroupUsersServlet(hs).register(http_server) | |||
GroupRoomServlet(hs).register(http_server) | |||
GroupSettingJoinPolicyServlet(hs).register(http_server) | |||
GroupCreateServlet(hs).register(http_server) | |||
GroupAdminRoomsServlet(hs).register(http_server) | |||
GroupAdminRoomsConfigServlet(hs).register(http_server) | |||
GroupAdminUsersInviteServlet(hs).register(http_server) | |||
GroupAdminUsersKickServlet(hs).register(http_server) | |||
GroupSelfLeaveServlet(hs).register(http_server) | |||
GroupSelfJoinServlet(hs).register(http_server) | |||
GroupSelfAcceptInviteServlet(hs).register(http_server) | |||
GroupsForUserServlet(hs).register(http_server) | |||
GroupCategoryServlet(hs).register(http_server) | |||
GroupCategoriesServlet(hs).register(http_server) | |||
GroupSummaryRoomsCatServlet(hs).register(http_server) | |||
GroupRoleServlet(hs).register(http_server) | |||
GroupRolesServlet(hs).register(http_server) | |||
GroupSelfUpdatePublicityServlet(hs).register(http_server) | |||
GroupSummaryUsersRoleServlet(hs).register(http_server) | |||
PublicisedGroupsForUserServlet(hs).register(http_server) | |||
PublicisedGroupsForUsersServlet(hs).register(http_server) |
@@ -298,14 +298,6 @@ class SyncRestServlet(RestServlet): | |||
if archived: | |||
response["rooms"][Membership.LEAVE] = archived | |||
if sync_result.groups is not None: | |||
if sync_result.groups.join: | |||
response["groups"][Membership.JOIN] = sync_result.groups.join | |||
if sync_result.groups.invite: | |||
response["groups"][Membership.INVITE] = sync_result.groups.invite | |||
if sync_result.groups.leave: | |||
response["groups"][Membership.LEAVE] = sync_result.groups.leave | |||
return response | |||
@staticmethod | |||
@@ -14,7 +14,6 @@ | |||
import urllib.parse | |||
from http import HTTPStatus | |||
from typing import List | |||
from parameterized import parameterized | |||
@@ -23,7 +22,7 @@ from twisted.test.proto_helpers import MemoryReactor | |||
import synapse.rest.admin | |||
from synapse.http.server import JsonResource | |||
from synapse.rest.admin import VersionServlet | |||
from synapse.rest.client import groups, login, room | |||
from synapse.rest.client import login, room | |||
from synapse.server import HomeServer | |||
from synapse.util import Clock | |||
@@ -49,93 +48,6 @@ class VersionTestCase(unittest.HomeserverTestCase): | |||
) | |||
class DeleteGroupTestCase(unittest.HomeserverTestCase): | |||
servlets = [ | |||
synapse.rest.admin.register_servlets_for_client_rest_resource, | |||
login.register_servlets, | |||
groups.register_servlets, | |||
] | |||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: | |||
self.admin_user = self.register_user("admin", "pass", admin=True) | |||
self.admin_user_tok = self.login("admin", "pass") | |||
self.other_user = self.register_user("user", "pass") | |||
self.other_user_token = self.login("user", "pass") | |||
@unittest.override_config({"experimental_features": {"groups_enabled": True}}) | |||
def test_delete_group(self) -> None: | |||
# Create a new group | |||
channel = self.make_request( | |||
"POST", | |||
b"/create_group", | |||
access_token=self.admin_user_tok, | |||
content={"localpart": "test"}, | |||
) | |||
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body) | |||
group_id = channel.json_body["group_id"] | |||
self._check_group(group_id, expect_code=HTTPStatus.OK) | |||
# Invite/join another user | |||
url = "/groups/%s/admin/users/invite/%s" % (group_id, self.other_user) | |||
channel = self.make_request( | |||
"PUT", url.encode("ascii"), access_token=self.admin_user_tok, content={} | |||
) | |||
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body) | |||
url = "/groups/%s/self/accept_invite" % (group_id,) | |||
channel = self.make_request( | |||
"PUT", url.encode("ascii"), access_token=self.other_user_token, content={} | |||
) | |||
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body) | |||
# Check other user knows they're in the group | |||
self.assertIn(group_id, self._get_groups_user_is_in(self.admin_user_tok)) | |||
self.assertIn(group_id, self._get_groups_user_is_in(self.other_user_token)) | |||
# Now delete the group | |||
url = "/_synapse/admin/v1/delete_group/" + group_id | |||
channel = self.make_request( | |||
"POST", | |||
url.encode("ascii"), | |||
access_token=self.admin_user_tok, | |||
content={"localpart": "test"}, | |||
) | |||
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body) | |||
# Check group returns HTTPStatus.NOT_FOUND | |||
self._check_group(group_id, expect_code=HTTPStatus.NOT_FOUND) | |||
# Check users don't think they're in the group | |||
self.assertNotIn(group_id, self._get_groups_user_is_in(self.admin_user_tok)) | |||
self.assertNotIn(group_id, self._get_groups_user_is_in(self.other_user_token)) | |||
def _check_group(self, group_id: str, expect_code: int) -> None: | |||
"""Assert that trying to fetch the given group results in the given | |||
HTTP status code | |||
""" | |||
url = "/groups/%s/profile" % (group_id,) | |||
channel = self.make_request( | |||
"GET", url.encode("ascii"), access_token=self.admin_user_tok | |||
) | |||
self.assertEqual(expect_code, channel.code, msg=channel.json_body) | |||
def _get_groups_user_is_in(self, access_token: str) -> List[str]: | |||
"""Returns the list of groups the user is in (given their access token)""" | |||
channel = self.make_request("GET", b"/joined_groups", access_token=access_token) | |||
self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body) | |||
return channel.json_body["groups"] | |||
class QuarantineMediaTestCase(unittest.HomeserverTestCase): | |||
"""Test /quarantine_media admin API.""" | |||
@@ -1,56 +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 synapse.rest.client import groups, room | |||
from tests import unittest | |||
from tests.unittest import override_config | |||
class GroupsTestCase(unittest.HomeserverTestCase): | |||
user_id = "@alice:test" | |||
room_creator_user_id = "@bob:test" | |||
servlets = [room.register_servlets, groups.register_servlets] | |||
@override_config({"enable_group_creation": True}) | |||
def test_rooms_limited_by_visibility(self) -> None: | |||
group_id = "+spqr:test" | |||
# Alice creates a group | |||
channel = self.make_request("POST", "/create_group", {"localpart": "spqr"}) | |||
self.assertEqual(channel.code, 200, msg=channel.text_body) | |||
self.assertEqual(channel.json_body, {"group_id": group_id}) | |||
# Bob creates a private room | |||
room_id = self.helper.create_room_as(self.room_creator_user_id, is_public=False) | |||
self.helper.auth_user_id = self.room_creator_user_id | |||
self.helper.send_state( | |||
room_id, "m.room.name", {"name": "bob's secret room"}, tok=None | |||
) | |||
self.helper.auth_user_id = self.user_id | |||
# Alice adds the room to her group. | |||
channel = self.make_request( | |||
"PUT", f"/groups/{group_id}/admin/rooms/{room_id}", {} | |||
) | |||
self.assertEqual(channel.code, 200, msg=channel.text_body) | |||
self.assertEqual(channel.json_body, {}) | |||
# Alice now tries to retrieve the room list of the space. | |||
channel = self.make_request("GET", f"/groups/{group_id}/rooms") | |||
self.assertEqual(channel.code, 200, msg=channel.text_body) | |||
self.assertEqual( | |||
channel.json_body, {"chunk": [], "total_room_count_estimate": 0} | |||
) |