Remove user-visible groups/communities code (#12553)
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.
This commit is contained in:
parent
759f9c09e1
commit
a8db8c6eba
|
@ -0,0 +1 @@
|
||||||
|
Remove support for the non-standard groups/communities feature from Synapse.
|
|
@ -2521,16 +2521,6 @@ push:
|
||||||
# "events_default": 1
|
# "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
|
# User Directory configuration
|
||||||
#
|
#
|
||||||
|
|
|
@ -3145,25 +3145,6 @@ Example configuration:
|
||||||
encryption_enabled_by_default_for_room_type: invite
|
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`
|
Config option: `user_directory`
|
||||||
|
|
||||||
This setting defines options related to the 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
|
# the maximum length for a user id is 255 characters
|
||||||
MAX_USERID_LENGTH = 255
|
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:
|
class Membership:
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,6 @@ from synapse.rest.admin import register_servlets_for_media_repo
|
||||||
from synapse.rest.client import (
|
from synapse.rest.client import (
|
||||||
account_data,
|
account_data,
|
||||||
events,
|
events,
|
||||||
groups,
|
|
||||||
initial_sync,
|
initial_sync,
|
||||||
login,
|
login,
|
||||||
presence,
|
presence,
|
||||||
|
@ -323,9 +322,6 @@ class GenericWorkerServer(HomeServer):
|
||||||
|
|
||||||
presence.register_servlets(self, resource)
|
presence.register_servlets(self, resource)
|
||||||
|
|
||||||
if self.config.experimental.groups_enabled:
|
|
||||||
groups.register_servlets(self, resource)
|
|
||||||
|
|
||||||
resources.update({CLIENT_API_PREFIX: resource})
|
resources.update({CLIENT_API_PREFIX: resource})
|
||||||
|
|
||||||
resources.update(build_synapse_client_resource_tree(self))
|
resources.update(build_synapse_client_resource_tree(self))
|
||||||
|
|
|
@ -73,9 +73,6 @@ class ExperimentalConfig(Config):
|
||||||
# MSC3720 (Account status endpoint)
|
# MSC3720 (Account status endpoint)
|
||||||
self.msc3720_enabled: bool = experimental.get("msc3720_enabled", False)
|
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
|
# MSC2654: Unread counts
|
||||||
self.msc2654_enabled: bool = experimental.get("msc2654_enabled", False)
|
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:
|
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
|
||||||
self.enable_group_creation = config.get("enable_group_creation", False)
|
self.enable_group_creation = config.get("enable_group_creation", False)
|
||||||
self.group_creation_prefix = config.get("group_creation_prefix", "")
|
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,
|
FederationAccountStatusServlet,
|
||||||
FederationTimestampLookupServlet,
|
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.server import HttpServer, JsonResource
|
||||||
from synapse.http.servlet import (
|
from synapse.http.servlet import (
|
||||||
parse_boolean_from_args,
|
parse_boolean_from_args,
|
||||||
|
@ -199,38 +195,6 @@ class PublicRoomList(BaseFederationServlet):
|
||||||
return 200, data
|
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):
|
class OpenIdUserInfo(BaseFederationServlet):
|
||||||
"""
|
"""
|
||||||
Exchange a bearer token for information about a user.
|
Exchange a bearer token for information about a user.
|
||||||
|
@ -292,16 +256,9 @@ class OpenIdUserInfo(BaseFederationServlet):
|
||||||
SERVLET_GROUPS: Dict[str, Iterable[Type[BaseFederationServlet]]] = {
|
SERVLET_GROUPS: Dict[str, Iterable[Type[BaseFederationServlet]]] = {
|
||||||
"federation": FEDERATION_SERVLET_CLASSES,
|
"federation": FEDERATION_SERVLET_CLASSES,
|
||||||
"room_list": (PublicRoomList,),
|
"room_list": (PublicRoomList,),
|
||||||
"group_server": GROUP_SERVER_SERVLET_CLASSES,
|
|
||||||
"group_local": GROUP_LOCAL_SERVLET_CLASSES,
|
|
||||||
"group_attestation": (FederationGroupsRenewAttestaionServlet,),
|
|
||||||
"openid": (OpenIdUserInfo,),
|
"openid": (OpenIdUserInfo,),
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_SERVLET_GROUPS = ("federation", "room_list", "openid")
|
|
||||||
|
|
||||||
GROUP_SERVLET_GROUPS = ("group_server", "group_local", "group_attestation")
|
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(
|
def register_servlets(
|
||||||
hs: "HomeServer",
|
hs: "HomeServer",
|
||||||
|
@ -324,10 +281,7 @@ def register_servlets(
|
||||||
Defaults to ``DEFAULT_SERVLET_GROUPS``.
|
Defaults to ``DEFAULT_SERVLET_GROUPS``.
|
||||||
"""
|
"""
|
||||||
if not servlet_groups:
|
if not servlet_groups:
|
||||||
servlet_groups = DEFAULT_SERVLET_GROUPS
|
servlet_groups = SERVLET_GROUPS.keys()
|
||||||
# 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
|
|
||||||
|
|
||||||
for servlet_group in servlet_groups:
|
for servlet_group in servlet_groups:
|
||||||
# Skip unknown 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
|
# Transfer alias mappings in the room directory
|
||||||
await self.store.update_aliases_for_room(old_room_id, room_id)
|
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(
|
async def copy_user_state_on_room_upgrade(
|
||||||
self, old_room_id: str, new_room_id: str, user_ids: Iterable[str]
|
self, old_room_id: str, new_room_id: str, user_ids: Iterable[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -166,16 +166,6 @@ class KnockedSyncResult:
|
||||||
return True
|
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)
|
@attr.s(slots=True, auto_attribs=True)
|
||||||
class _RoomChanges:
|
class _RoomChanges:
|
||||||
"""The set of room entries to include in the sync, plus the set of joined
|
"""The set of room entries to include in the sync, plus the set of joined
|
||||||
|
@ -206,7 +196,6 @@ class SyncResult:
|
||||||
for this device
|
for this device
|
||||||
device_unused_fallback_key_types: List of key types that have an unused fallback
|
device_unused_fallback_key_types: List of key types that have an unused fallback
|
||||||
key
|
key
|
||||||
groups: Group updates, if any
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
next_batch: StreamToken
|
next_batch: StreamToken
|
||||||
|
@ -220,7 +209,6 @@ class SyncResult:
|
||||||
device_lists: DeviceListUpdates
|
device_lists: DeviceListUpdates
|
||||||
device_one_time_keys_count: JsonDict
|
device_one_time_keys_count: JsonDict
|
||||||
device_unused_fallback_key_types: List[str]
|
device_unused_fallback_key_types: List[str]
|
||||||
groups: Optional[GroupsSyncResult]
|
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def __bool__(self) -> bool:
|
||||||
"""Make the result appear empty if there are no updates. This is used
|
"""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.account_data
|
||||||
or self.to_device
|
or self.to_device
|
||||||
or self.device_lists
|
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)
|
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
|
num_events = 0
|
||||||
|
|
||||||
# debug for https://github.com/matrix-org/synapse/issues/9424
|
# debug for https://github.com/matrix-org/synapse/issues/9424
|
||||||
|
@ -1184,57 +1167,11 @@ class SyncHandler:
|
||||||
archived=sync_result_builder.archived,
|
archived=sync_result_builder.archived,
|
||||||
to_device=sync_result_builder.to_device,
|
to_device=sync_result_builder.to_device,
|
||||||
device_lists=device_lists,
|
device_lists=device_lists,
|
||||||
groups=sync_result_builder.groups,
|
|
||||||
device_one_time_keys_count=one_time_key_counts,
|
device_one_time_keys_count=one_time_key_counts,
|
||||||
device_unused_fallback_key_types=unused_fallback_key_types,
|
device_unused_fallback_key_types=unused_fallback_key_types,
|
||||||
next_batch=sync_result_builder.now_token,
|
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")
|
@measure_func("_generate_sync_entry_for_device_list")
|
||||||
async def _generate_sync_entry_for_device_list(
|
async def _generate_sync_entry_for_device_list(
|
||||||
self,
|
self,
|
||||||
|
@ -2333,7 +2270,6 @@ class SyncResultBuilder:
|
||||||
invited
|
invited
|
||||||
knocked
|
knocked
|
||||||
archived
|
archived
|
||||||
groups
|
|
||||||
to_device
|
to_device
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2349,7 +2285,6 @@ class SyncResultBuilder:
|
||||||
invited: List[InvitedSyncResult] = attr.Factory(list)
|
invited: List[InvitedSyncResult] = attr.Factory(list)
|
||||||
knocked: List[KnockedSyncResult] = attr.Factory(list)
|
knocked: List[KnockedSyncResult] = attr.Factory(list)
|
||||||
archived: List[ArchivedSyncResult] = attr.Factory(list)
|
archived: List[ArchivedSyncResult] = attr.Factory(list)
|
||||||
groups: Optional[GroupsSyncResult] = None
|
|
||||||
to_device: List[JsonDict] = attr.Factory(list)
|
to_device: List[JsonDict] = attr.Factory(list)
|
||||||
|
|
||||||
def calculate_user_changes(self) -> Tuple[Set[str], Set[str]]:
|
def calculate_user_changes(self) -> Tuple[Set[str], Set[str]]:
|
||||||
|
|
|
@ -26,7 +26,6 @@ from synapse.rest.client import (
|
||||||
directory,
|
directory,
|
||||||
events,
|
events,
|
||||||
filter,
|
filter,
|
||||||
groups,
|
|
||||||
initial_sync,
|
initial_sync,
|
||||||
keys,
|
keys,
|
||||||
knock,
|
knock,
|
||||||
|
@ -118,8 +117,6 @@ class ClientRestResource(JsonResource):
|
||||||
thirdparty.register_servlets(hs, client_resource)
|
thirdparty.register_servlets(hs, client_resource)
|
||||||
sendtodevice.register_servlets(hs, client_resource)
|
sendtodevice.register_servlets(hs, client_resource)
|
||||||
user_directory.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_upgrade_rest_servlet.register_servlets(hs, client_resource)
|
||||||
room_batch.register_servlets(hs, client_resource)
|
room_batch.register_servlets(hs, client_resource)
|
||||||
capabilities.register_servlets(hs, client_resource)
|
capabilities.register_servlets(hs, client_resource)
|
||||||
|
|
|
@ -47,7 +47,6 @@ from synapse.rest.admin.federation import (
|
||||||
DestinationRestServlet,
|
DestinationRestServlet,
|
||||||
ListDestinationsRestServlet,
|
ListDestinationsRestServlet,
|
||||||
)
|
)
|
||||||
from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
|
|
||||||
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
|
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
|
||||||
from synapse.rest.admin.registration_tokens import (
|
from synapse.rest.admin.registration_tokens import (
|
||||||
ListRegistrationTokensRestServlet,
|
ListRegistrationTokensRestServlet,
|
||||||
|
@ -293,8 +292,6 @@ def register_servlets_for_client_rest_resource(
|
||||||
ResetPasswordRestServlet(hs).register(http_server)
|
ResetPasswordRestServlet(hs).register(http_server)
|
||||||
SearchUsersRestServlet(hs).register(http_server)
|
SearchUsersRestServlet(hs).register(http_server)
|
||||||
UserRegisterServlet(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)
|
AccountValidityRenewServlet(hs).register(http_server)
|
||||||
|
|
||||||
# Load the media repo ones if we're using them. Otherwise load the servlets which
|
# 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:
|
if archived:
|
||||||
response["rooms"][Membership.LEAVE] = 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
|
return response
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ from twisted.test.proto_helpers import MemoryReactor
|
||||||
import synapse.rest.admin
|
import synapse.rest.admin
|
||||||
from synapse.http.server import JsonResource
|
from synapse.http.server import JsonResource
|
||||||
from synapse.rest.admin import VersionServlet
|
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.server import HomeServer
|
||||||
from synapse.util import Clock
|
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):
|
class QuarantineMediaTestCase(unittest.HomeserverTestCase):
|
||||||
"""Test /quarantine_media admin API."""
|
"""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}
|
|
||||||
)
|
|
Loading…
Reference in New Issue