mirror of https://github.com/watcha-fr/synapse
Split `synapse.federation.transport.server` into multiple files. (#10590)
parent
2d9ca4ca77
commit
87b62f8bb2
@ -0,0 +1 @@ |
|||||||
|
Re-organize the `synapse.federation.transport.server` module to create smaller files. |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,332 @@ |
|||||||
|
# Copyright 2014-2021 The Matrix.org Foundation C.I.C. |
||||||
|
# Copyright 2020 Sorunome |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
import logging |
||||||
|
from typing import Dict, Iterable, List, Optional, Tuple, Type |
||||||
|
|
||||||
|
from typing_extensions import Literal |
||||||
|
|
||||||
|
from synapse.api.errors import FederationDeniedError, SynapseError |
||||||
|
from synapse.federation.transport.server._base import ( |
||||||
|
Authenticator, |
||||||
|
BaseFederationServlet, |
||||||
|
) |
||||||
|
from synapse.federation.transport.server.federation import FEDERATION_SERVLET_CLASSES |
||||||
|
from synapse.federation.transport.server.groups_local import GROUP_LOCAL_SERVLET_CLASSES |
||||||
|
from synapse.federation.transport.server.groups_server import ( |
||||||
|
GROUP_SERVER_SERVLET_CLASSES, |
||||||
|
) |
||||||
|
from synapse.http.server import HttpServer, JsonResource |
||||||
|
from synapse.http.servlet import ( |
||||||
|
parse_boolean_from_args, |
||||||
|
parse_integer_from_args, |
||||||
|
parse_string_from_args, |
||||||
|
) |
||||||
|
from synapse.server import HomeServer |
||||||
|
from synapse.types import JsonDict, ThirdPartyInstanceID |
||||||
|
from synapse.util.ratelimitutils import FederationRateLimiter |
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) |
||||||
|
|
||||||
|
|
||||||
|
class TransportLayerServer(JsonResource): |
||||||
|
"""Handles incoming federation HTTP requests""" |
||||||
|
|
||||||
|
def __init__(self, hs: HomeServer, servlet_groups: Optional[List[str]] = None): |
||||||
|
"""Initialize the TransportLayerServer |
||||||
|
|
||||||
|
Will by default register all servlets. For custom behaviour, pass in |
||||||
|
a list of servlet_groups to register. |
||||||
|
|
||||||
|
Args: |
||||||
|
hs: homeserver |
||||||
|
servlet_groups: List of servlet groups to register. |
||||||
|
Defaults to ``DEFAULT_SERVLET_GROUPS``. |
||||||
|
""" |
||||||
|
self.hs = hs |
||||||
|
self.clock = hs.get_clock() |
||||||
|
self.servlet_groups = servlet_groups |
||||||
|
|
||||||
|
super().__init__(hs, canonical_json=False) |
||||||
|
|
||||||
|
self.authenticator = Authenticator(hs) |
||||||
|
self.ratelimiter = hs.get_federation_ratelimiter() |
||||||
|
|
||||||
|
self.register_servlets() |
||||||
|
|
||||||
|
def register_servlets(self) -> None: |
||||||
|
register_servlets( |
||||||
|
self.hs, |
||||||
|
resource=self, |
||||||
|
ratelimiter=self.ratelimiter, |
||||||
|
authenticator=self.authenticator, |
||||||
|
servlet_groups=self.servlet_groups, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class PublicRoomList(BaseFederationServlet): |
||||||
|
""" |
||||||
|
Fetch the public room list for this server. |
||||||
|
|
||||||
|
This API returns information in the same format as /publicRooms on the |
||||||
|
client API, but will only ever include local public rooms and hence is |
||||||
|
intended for consumption by other homeservers. |
||||||
|
|
||||||
|
GET /publicRooms HTTP/1.1 |
||||||
|
|
||||||
|
HTTP/1.1 200 OK |
||||||
|
Content-Type: application/json |
||||||
|
|
||||||
|
{ |
||||||
|
"chunk": [ |
||||||
|
{ |
||||||
|
"aliases": [ |
||||||
|
"#test:localhost" |
||||||
|
], |
||||||
|
"guest_can_join": false, |
||||||
|
"name": "test room", |
||||||
|
"num_joined_members": 3, |
||||||
|
"room_id": "!whkydVegtvatLfXmPN:localhost", |
||||||
|
"world_readable": false |
||||||
|
} |
||||||
|
], |
||||||
|
"end": "END", |
||||||
|
"start": "START" |
||||||
|
} |
||||||
|
""" |
||||||
|
|
||||||
|
PATH = "/publicRooms" |
||||||
|
|
||||||
|
def __init__( |
||||||
|
self, |
||||||
|
hs: HomeServer, |
||||||
|
authenticator: Authenticator, |
||||||
|
ratelimiter: FederationRateLimiter, |
||||||
|
server_name: str, |
||||||
|
): |
||||||
|
super().__init__(hs, authenticator, ratelimiter, server_name) |
||||||
|
self.handler = hs.get_room_list_handler() |
||||||
|
self.allow_access = hs.config.allow_public_rooms_over_federation |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, origin: str, content: Literal[None], query: Dict[bytes, List[bytes]] |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
if not self.allow_access: |
||||||
|
raise FederationDeniedError(origin) |
||||||
|
|
||||||
|
limit = parse_integer_from_args(query, "limit", 0) |
||||||
|
since_token = parse_string_from_args(query, "since", None) |
||||||
|
include_all_networks = parse_boolean_from_args( |
||||||
|
query, "include_all_networks", default=False |
||||||
|
) |
||||||
|
third_party_instance_id = parse_string_from_args( |
||||||
|
query, "third_party_instance_id", None |
||||||
|
) |
||||||
|
|
||||||
|
if include_all_networks: |
||||||
|
network_tuple = None |
||||||
|
elif third_party_instance_id: |
||||||
|
network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) |
||||||
|
else: |
||||||
|
network_tuple = ThirdPartyInstanceID(None, None) |
||||||
|
|
||||||
|
if limit == 0: |
||||||
|
# zero is a special value which corresponds to no limit. |
||||||
|
limit = None |
||||||
|
|
||||||
|
data = await self.handler.get_local_public_room_list( |
||||||
|
limit, since_token, network_tuple=network_tuple, from_federation=True |
||||||
|
) |
||||||
|
return 200, data |
||||||
|
|
||||||
|
async def on_POST( |
||||||
|
self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
# This implements MSC2197 (Search Filtering over Federation) |
||||||
|
if not self.allow_access: |
||||||
|
raise FederationDeniedError(origin) |
||||||
|
|
||||||
|
limit: Optional[int] = int(content.get("limit", 100)) |
||||||
|
since_token = content.get("since", None) |
||||||
|
search_filter = content.get("filter", None) |
||||||
|
|
||||||
|
include_all_networks = content.get("include_all_networks", False) |
||||||
|
third_party_instance_id = content.get("third_party_instance_id", None) |
||||||
|
|
||||||
|
if include_all_networks: |
||||||
|
network_tuple = None |
||||||
|
if third_party_instance_id is not None: |
||||||
|
raise SynapseError( |
||||||
|
400, "Can't use include_all_networks with an explicit network" |
||||||
|
) |
||||||
|
elif third_party_instance_id is None: |
||||||
|
network_tuple = ThirdPartyInstanceID(None, None) |
||||||
|
else: |
||||||
|
network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) |
||||||
|
|
||||||
|
if search_filter is None: |
||||||
|
logger.warning("Nonefilter") |
||||||
|
|
||||||
|
if limit == 0: |
||||||
|
# zero is a special value which corresponds to no limit. |
||||||
|
limit = None |
||||||
|
|
||||||
|
data = await self.handler.get_local_public_room_list( |
||||||
|
limit=limit, |
||||||
|
since_token=since_token, |
||||||
|
search_filter=search_filter, |
||||||
|
network_tuple=network_tuple, |
||||||
|
from_federation=True, |
||||||
|
) |
||||||
|
|
||||||
|
return 200, data |
||||||
|
|
||||||
|
|
||||||
|
class FederationGroupsRenewAttestaionServlet(BaseFederationServlet): |
||||||
|
"""A group or user's server renews their attestation""" |
||||||
|
|
||||||
|
PATH = "/groups/(?P<group_id>[^/]*)/renew_attestation/(?P<user_id>[^/]*)" |
||||||
|
|
||||||
|
def __init__( |
||||||
|
self, |
||||||
|
hs: HomeServer, |
||||||
|
authenticator: Authenticator, |
||||||
|
ratelimiter: FederationRateLimiter, |
||||||
|
server_name: str, |
||||||
|
): |
||||||
|
super().__init__(hs, authenticator, ratelimiter, server_name) |
||||||
|
self.handler = hs.get_groups_attestation_renewer() |
||||||
|
|
||||||
|
async def on_POST( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
group_id: str, |
||||||
|
user_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
# We don't need to check auth here as we check the attestation signatures |
||||||
|
|
||||||
|
new_content = await self.handler.on_renew_attestation( |
||||||
|
group_id, user_id, content |
||||||
|
) |
||||||
|
|
||||||
|
return 200, new_content |
||||||
|
|
||||||
|
|
||||||
|
class OpenIdUserInfo(BaseFederationServlet): |
||||||
|
""" |
||||||
|
Exchange a bearer token for information about a user. |
||||||
|
|
||||||
|
The response format should be compatible with: |
||||||
|
http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse |
||||||
|
|
||||||
|
GET /openid/userinfo?access_token=ABDEFGH HTTP/1.1 |
||||||
|
|
||||||
|
HTTP/1.1 200 OK |
||||||
|
Content-Type: application/json |
||||||
|
|
||||||
|
{ |
||||||
|
"sub": "@userpart:example.org", |
||||||
|
} |
||||||
|
""" |
||||||
|
|
||||||
|
PATH = "/openid/userinfo" |
||||||
|
|
||||||
|
REQUIRE_AUTH = False |
||||||
|
|
||||||
|
def __init__( |
||||||
|
self, |
||||||
|
hs: HomeServer, |
||||||
|
authenticator: Authenticator, |
||||||
|
ratelimiter: FederationRateLimiter, |
||||||
|
server_name: str, |
||||||
|
): |
||||||
|
super().__init__(hs, authenticator, ratelimiter, server_name) |
||||||
|
self.handler = hs.get_federation_server() |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: Optional[str], |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
token = parse_string_from_args(query, "access_token") |
||||||
|
if token is None: |
||||||
|
return ( |
||||||
|
401, |
||||||
|
{"errcode": "M_MISSING_TOKEN", "error": "Access Token required"}, |
||||||
|
) |
||||||
|
|
||||||
|
user_id = await self.handler.on_openid_userinfo(token) |
||||||
|
|
||||||
|
if user_id is None: |
||||||
|
return ( |
||||||
|
401, |
||||||
|
{ |
||||||
|
"errcode": "M_UNKNOWN_TOKEN", |
||||||
|
"error": "Access Token unknown or expired", |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
return 200, {"sub": user_id} |
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SERVLET_GROUPS: Dict[str, Iterable[Type[BaseFederationServlet]]] = { |
||||||
|
"federation": FEDERATION_SERVLET_CLASSES, |
||||||
|
"room_list": (PublicRoomList,), |
||||||
|
"group_server": GROUP_SERVER_SERVLET_CLASSES, |
||||||
|
"group_local": GROUP_LOCAL_SERVLET_CLASSES, |
||||||
|
"group_attestation": (FederationGroupsRenewAttestaionServlet,), |
||||||
|
"openid": (OpenIdUserInfo,), |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
def register_servlets( |
||||||
|
hs: HomeServer, |
||||||
|
resource: HttpServer, |
||||||
|
authenticator: Authenticator, |
||||||
|
ratelimiter: FederationRateLimiter, |
||||||
|
servlet_groups: Optional[Iterable[str]] = None, |
||||||
|
): |
||||||
|
"""Initialize and register servlet classes. |
||||||
|
|
||||||
|
Will by default register all servlets. For custom behaviour, pass in |
||||||
|
a list of servlet_groups to register. |
||||||
|
|
||||||
|
Args: |
||||||
|
hs: homeserver |
||||||
|
resource: resource class to register to |
||||||
|
authenticator: authenticator to use |
||||||
|
ratelimiter: ratelimiter to use |
||||||
|
servlet_groups: List of servlet groups to register. |
||||||
|
Defaults to ``DEFAULT_SERVLET_GROUPS``. |
||||||
|
""" |
||||||
|
if not servlet_groups: |
||||||
|
servlet_groups = DEFAULT_SERVLET_GROUPS.keys() |
||||||
|
|
||||||
|
for servlet_group in servlet_groups: |
||||||
|
# Skip unknown servlet groups. |
||||||
|
if servlet_group not in DEFAULT_SERVLET_GROUPS: |
||||||
|
raise RuntimeError( |
||||||
|
f"Attempting to register unknown federation servlet: '{servlet_group}'" |
||||||
|
) |
||||||
|
|
||||||
|
for servletclass in DEFAULT_SERVLET_GROUPS[servlet_group]: |
||||||
|
servletclass( |
||||||
|
hs=hs, |
||||||
|
authenticator=authenticator, |
||||||
|
ratelimiter=ratelimiter, |
||||||
|
server_name=hs.hostname, |
||||||
|
).register(resource) |
@ -0,0 +1,328 @@ |
|||||||
|
# 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. |
||||||
|
|
||||||
|
import functools |
||||||
|
import logging |
||||||
|
import re |
||||||
|
|
||||||
|
from synapse.api.errors import Codes, FederationDeniedError, SynapseError |
||||||
|
from synapse.api.urls import FEDERATION_V1_PREFIX |
||||||
|
from synapse.http.servlet import parse_json_object_from_request |
||||||
|
from synapse.logging import opentracing |
||||||
|
from synapse.logging.context import run_in_background |
||||||
|
from synapse.logging.opentracing import ( |
||||||
|
SynapseTags, |
||||||
|
start_active_span, |
||||||
|
start_active_span_from_request, |
||||||
|
tags, |
||||||
|
whitelisted_homeserver, |
||||||
|
) |
||||||
|
from synapse.server import HomeServer |
||||||
|
from synapse.util.ratelimitutils import FederationRateLimiter |
||||||
|
from synapse.util.stringutils import parse_and_validate_server_name |
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) |
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationError(SynapseError): |
||||||
|
"""There was a problem authenticating the request""" |
||||||
|
|
||||||
|
|
||||||
|
class NoAuthenticationError(AuthenticationError): |
||||||
|
"""The request had no authentication information""" |
||||||
|
|
||||||
|
|
||||||
|
class Authenticator: |
||||||
|
def __init__(self, hs: HomeServer): |
||||||
|
self._clock = hs.get_clock() |
||||||
|
self.keyring = hs.get_keyring() |
||||||
|
self.server_name = hs.hostname |
||||||
|
self.store = hs.get_datastore() |
||||||
|
self.federation_domain_whitelist = hs.config.federation_domain_whitelist |
||||||
|
self.notifier = hs.get_notifier() |
||||||
|
|
||||||
|
self.replication_client = None |
||||||
|
if hs.config.worker.worker_app: |
||||||
|
self.replication_client = hs.get_tcp_replication() |
||||||
|
|
||||||
|
# A method just so we can pass 'self' as the authenticator to the Servlets |
||||||
|
async def authenticate_request(self, request, content): |
||||||
|
now = self._clock.time_msec() |
||||||
|
json_request = { |
||||||
|
"method": request.method.decode("ascii"), |
||||||
|
"uri": request.uri.decode("ascii"), |
||||||
|
"destination": self.server_name, |
||||||
|
"signatures": {}, |
||||||
|
} |
||||||
|
|
||||||
|
if content is not None: |
||||||
|
json_request["content"] = content |
||||||
|
|
||||||
|
origin = None |
||||||
|
|
||||||
|
auth_headers = request.requestHeaders.getRawHeaders(b"Authorization") |
||||||
|
|
||||||
|
if not auth_headers: |
||||||
|
raise NoAuthenticationError( |
||||||
|
401, "Missing Authorization headers", Codes.UNAUTHORIZED |
||||||
|
) |
||||||
|
|
||||||
|
for auth in auth_headers: |
||||||
|
if auth.startswith(b"X-Matrix"): |
||||||
|
(origin, key, sig) = _parse_auth_header(auth) |
||||||
|
json_request["origin"] = origin |
||||||
|
json_request["signatures"].setdefault(origin, {})[key] = sig |
||||||
|
|
||||||
|
if ( |
||||||
|
self.federation_domain_whitelist is not None |
||||||
|
and origin not in self.federation_domain_whitelist |
||||||
|
): |
||||||
|
raise FederationDeniedError(origin) |
||||||
|
|
||||||
|
if origin is None or not json_request["signatures"]: |
||||||
|
raise NoAuthenticationError( |
||||||
|
401, "Missing Authorization headers", Codes.UNAUTHORIZED |
||||||
|
) |
||||||
|
|
||||||
|
await self.keyring.verify_json_for_server( |
||||||
|
origin, |
||||||
|
json_request, |
||||||
|
now, |
||||||
|
) |
||||||
|
|
||||||
|
logger.debug("Request from %s", origin) |
||||||
|
request.requester = origin |
||||||
|
|
||||||
|
# If we get a valid signed request from the other side, its probably |
||||||
|
# alive |
||||||
|
retry_timings = await self.store.get_destination_retry_timings(origin) |
||||||
|
if retry_timings and retry_timings.retry_last_ts: |
||||||
|
run_in_background(self._reset_retry_timings, origin) |
||||||
|
|
||||||
|
return origin |
||||||
|
|
||||||
|
async def _reset_retry_timings(self, origin): |
||||||
|
try: |
||||||
|
logger.info("Marking origin %r as up", origin) |
||||||
|
await self.store.set_destination_retry_timings(origin, None, 0, 0) |
||||||
|
|
||||||
|
# Inform the relevant places that the remote server is back up. |
||||||
|
self.notifier.notify_remote_server_up(origin) |
||||||
|
if self.replication_client: |
||||||
|
# If we're on a worker we try and inform master about this. The |
||||||
|
# replication client doesn't hook into the notifier to avoid |
||||||
|
# infinite loops where we send a `REMOTE_SERVER_UP` command to |
||||||
|
# master, which then echoes it back to us which in turn pokes |
||||||
|
# the notifier. |
||||||
|
self.replication_client.send_remote_server_up(origin) |
||||||
|
|
||||||
|
except Exception: |
||||||
|
logger.exception("Error resetting retry timings on %s", origin) |
||||||
|
|
||||||
|
|
||||||
|
def _parse_auth_header(header_bytes): |
||||||
|
"""Parse an X-Matrix auth header |
||||||
|
|
||||||
|
Args: |
||||||
|
header_bytes (bytes): header value |
||||||
|
|
||||||
|
Returns: |
||||||
|
Tuple[str, str, str]: origin, key id, signature. |
||||||
|
|
||||||
|
Raises: |
||||||
|
AuthenticationError if the header could not be parsed |
||||||
|
""" |
||||||
|
try: |
||||||
|
header_str = header_bytes.decode("utf-8") |
||||||
|
params = header_str.split(" ")[1].split(",") |
||||||
|
param_dict = dict(kv.split("=") for kv in params) |
||||||
|
|
||||||
|
def strip_quotes(value): |
||||||
|
if value.startswith('"'): |
||||||
|
return value[1:-1] |
||||||
|
else: |
||||||
|
return value |
||||||
|
|
||||||
|
origin = strip_quotes(param_dict["origin"]) |
||||||
|
|
||||||
|
# ensure that the origin is a valid server name |
||||||
|
parse_and_validate_server_name(origin) |
||||||
|
|
||||||
|
key = strip_quotes(param_dict["key"]) |
||||||
|
sig = strip_quotes(param_dict["sig"]) |
||||||
|
return origin, key, sig |
||||||
|
except Exception as e: |
||||||
|
logger.warning( |
||||||
|
"Error parsing auth header '%s': %s", |
||||||
|
header_bytes.decode("ascii", "replace"), |
||||||
|
e, |
||||||
|
) |
||||||
|
raise AuthenticationError( |
||||||
|
400, "Malformed Authorization header", Codes.UNAUTHORIZED |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class BaseFederationServlet: |
||||||
|
"""Abstract base class for federation servlet classes. |
||||||
|
|
||||||
|
The servlet object should have a PATH attribute which takes the form of a regexp to |
||||||
|
match against the request path (excluding the /federation/v1 prefix). |
||||||
|
|
||||||
|
The servlet should also implement one or more of on_GET, on_POST, on_PUT, to match |
||||||
|
the appropriate HTTP method. These methods must be *asynchronous* and have the |
||||||
|
signature: |
||||||
|
|
||||||
|
on_<METHOD>(self, origin, content, query, **kwargs) |
||||||
|
|
||||||
|
With arguments: |
||||||
|
|
||||||
|
origin (unicode|None): The authenticated server_name of the calling server, |
||||||
|
unless REQUIRE_AUTH is set to False and authentication failed. |
||||||
|
|
||||||
|
content (unicode|None): decoded json body of the request. None if the |
||||||
|
request was a GET. |
||||||
|
|
||||||
|
query (dict[bytes, list[bytes]]): Query params from the request. url-decoded |
||||||
|
(ie, '+' and '%xx' are decoded) but note that it is *not* utf8-decoded |
||||||
|
yet. |
||||||
|
|
||||||
|
**kwargs (dict[unicode, unicode]): the dict mapping keys to path |
||||||
|
components as specified in the path match regexp. |
||||||
|
|
||||||
|
Returns: |
||||||
|
Optional[Tuple[int, object]]: either (response code, response object) to |
||||||
|
return a JSON response, or None if the request has already been handled. |
||||||
|
|
||||||
|
Raises: |
||||||
|
SynapseError: to return an error code |
||||||
|
|
||||||
|
Exception: other exceptions will be caught, logged, and a 500 will be |
||||||
|
returned. |
||||||
|
""" |
||||||
|
|
||||||
|
PATH = "" # Overridden in subclasses, the regex to match against the path. |
||||||
|
|
||||||
|
REQUIRE_AUTH = True |
||||||
|
|
||||||
|
PREFIX = FEDERATION_V1_PREFIX # Allows specifying the API version |
||||||
|
|
||||||
|
RATELIMIT = True # Whether to rate limit requests or not |
||||||
|
|
||||||
|
def __init__( |
||||||
|
self, |
||||||
|
hs: HomeServer, |
||||||
|
authenticator: Authenticator, |
||||||
|
ratelimiter: FederationRateLimiter, |
||||||
|
server_name: str, |
||||||
|
): |
||||||
|
self.hs = hs |
||||||
|
self.authenticator = authenticator |
||||||
|
self.ratelimiter = ratelimiter |
||||||
|
self.server_name = server_name |
||||||
|
|
||||||
|
def _wrap(self, func): |
||||||
|
authenticator = self.authenticator |
||||||
|
ratelimiter = self.ratelimiter |
||||||
|
|
||||||
|
@functools.wraps(func) |
||||||
|
async def new_func(request, *args, **kwargs): |
||||||
|
"""A callback which can be passed to HttpServer.RegisterPaths |
||||||
|
|
||||||
|
Args: |
||||||
|
request (twisted.web.http.Request): |
||||||
|
*args: unused? |
||||||
|
**kwargs (dict[unicode, unicode]): the dict mapping keys to path |
||||||
|
components as specified in the path match regexp. |
||||||
|
|
||||||
|
Returns: |
||||||
|
Tuple[int, object]|None: (response code, response object) as returned by |
||||||
|
the callback method. None if the request has already been handled. |
||||||
|
""" |
||||||
|
content = None |
||||||
|
if request.method in [b"PUT", b"POST"]: |
||||||
|
# TODO: Handle other method types? other content types? |
||||||
|
content = parse_json_object_from_request(request) |
||||||
|
|
||||||
|
try: |
||||||
|
origin = await authenticator.authenticate_request(request, content) |
||||||
|
except NoAuthenticationError: |
||||||
|
origin = None |
||||||
|
if self.REQUIRE_AUTH: |
||||||
|
logger.warning( |
||||||
|
"authenticate_request failed: missing authentication" |
||||||
|
) |
||||||
|
raise |
||||||
|
except Exception as e: |
||||||
|
logger.warning("authenticate_request failed: %s", e) |
||||||
|
raise |
||||||
|
|
||||||
|
request_tags = { |
||||||
|
SynapseTags.REQUEST_ID: request.get_request_id(), |
||||||
|
tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER, |
||||||
|
tags.HTTP_METHOD: request.get_method(), |
||||||
|
tags.HTTP_URL: request.get_redacted_uri(), |
||||||
|
tags.PEER_HOST_IPV6: request.getClientIP(), |
||||||
|
"authenticated_entity": origin, |
||||||
|
"servlet_name": request.request_metrics.name, |
||||||
|
} |
||||||
|
|
||||||
|
# Only accept the span context if the origin is authenticated |
||||||
|
# and whitelisted |
||||||
|
if origin and whitelisted_homeserver(origin): |
||||||
|
scope = start_active_span_from_request( |
||||||
|
request, "incoming-federation-request", tags=request_tags |
||||||
|
) |
||||||
|
else: |
||||||
|
scope = start_active_span( |
||||||
|
"incoming-federation-request", tags=request_tags |
||||||
|
) |
||||||
|
|
||||||
|
with scope: |
||||||
|
opentracing.inject_response_headers(request.responseHeaders) |
||||||
|
|
||||||
|
if origin and self.RATELIMIT: |
||||||
|
with ratelimiter.ratelimit(origin) as d: |
||||||
|
await d |
||||||
|
if request._disconnected: |
||||||
|
logger.warning( |
||||||
|
"client disconnected before we started processing " |
||||||
|
"request" |
||||||
|
) |
||||||
|
return -1, None |
||||||
|
response = await func( |
||||||
|
origin, content, request.args, *args, **kwargs |
||||||
|
) |
||||||
|
else: |
||||||
|
response = await func( |
||||||
|
origin, content, request.args, *args, **kwargs |
||||||
|
) |
||||||
|
|
||||||
|
return response |
||||||
|
|
||||||
|
return new_func |
||||||
|
|
||||||
|
def register(self, server): |
||||||
|
pattern = re.compile("^" + self.PREFIX + self.PATH + "$") |
||||||
|
|
||||||
|
for method in ("GET", "PUT", "POST"): |
||||||
|
code = getattr(self, "on_%s" % (method), None) |
||||||
|
if code is None: |
||||||
|
continue |
||||||
|
|
||||||
|
server.register_paths( |
||||||
|
method, |
||||||
|
(pattern,), |
||||||
|
self._wrap(code), |
||||||
|
self.__class__.__name__, |
||||||
|
) |
@ -0,0 +1,692 @@ |
|||||||
|
# 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. |
||||||
|
import logging |
||||||
|
from typing import Dict, List, Mapping, Optional, Sequence, Tuple, Type, Union |
||||||
|
|
||||||
|
from typing_extensions import Literal |
||||||
|
|
||||||
|
import synapse |
||||||
|
from synapse.api.errors import Codes, SynapseError |
||||||
|
from synapse.api.room_versions import RoomVersions |
||||||
|
from synapse.api.urls import FEDERATION_UNSTABLE_PREFIX, FEDERATION_V2_PREFIX |
||||||
|
from synapse.federation.transport.server._base import ( |
||||||
|
Authenticator, |
||||||
|
BaseFederationServlet, |
||||||
|
) |
||||||
|
from synapse.http.servlet import ( |
||||||
|
parse_boolean_from_args, |
||||||
|
parse_integer_from_args, |
||||||
|
parse_string_from_args, |
||||||
|
parse_strings_from_args, |
||||||
|
) |
||||||
|
from synapse.server import HomeServer |
||||||
|
from synapse.types import JsonDict |
||||||
|
from synapse.util.ratelimitutils import FederationRateLimiter |
||||||
|
from synapse.util.versionstring import get_version_string |
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) |
||||||
|
|
||||||
|
|
||||||
|
class BaseFederationServerServlet(BaseFederationServlet): |
||||||
|
"""Abstract base class for federation servlet classes which provides a federation 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_federation_server() |
||||||
|
|
||||||
|
|
||||||
|
class FederationSendServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/send/(?P<transaction_id>[^/]*)/?" |
||||||
|
|
||||||
|
# We ratelimit manually in the handler as we queue up the requests and we |
||||||
|
# don't want to fill up the ratelimiter with blocked requests. |
||||||
|
RATELIMIT = False |
||||||
|
|
||||||
|
# This is when someone is trying to send us a bunch of data. |
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
transaction_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
"""Called on PUT /send/<transaction_id>/ |
||||||
|
|
||||||
|
Args: |
||||||
|
transaction_id: The transaction_id associated with this request. This |
||||||
|
is *not* None. |
||||||
|
|
||||||
|
Returns: |
||||||
|
Tuple of `(code, response)`, where |
||||||
|
`response` is a python dict to be converted into JSON that is |
||||||
|
used as the response body. |
||||||
|
""" |
||||||
|
# Parse the request |
||||||
|
try: |
||||||
|
transaction_data = content |
||||||
|
|
||||||
|
logger.debug("Decoded %s: %s", transaction_id, str(transaction_data)) |
||||||
|
|
||||||
|
logger.info( |
||||||
|
"Received txn %s from %s. (PDUs: %d, EDUs: %d)", |
||||||
|
transaction_id, |
||||||
|
origin, |
||||||
|
len(transaction_data.get("pdus", [])), |
||||||
|
len(transaction_data.get("edus", [])), |
||||||
|
) |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
logger.exception(e) |
||||||
|
return 400, {"error": "Invalid transaction"} |
||||||
|
|
||||||
|
code, response = await self.handler.on_incoming_transaction( |
||||||
|
origin, transaction_id, self.server_name, transaction_data |
||||||
|
) |
||||||
|
|
||||||
|
return code, response |
||||||
|
|
||||||
|
|
||||||
|
class FederationEventServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/event/(?P<event_id>[^/]*)/?" |
||||||
|
|
||||||
|
# This is when someone asks for a data item for a given server data_id pair. |
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, Union[JsonDict, str]]: |
||||||
|
return await self.handler.on_pdu_request(origin, event_id) |
||||||
|
|
||||||
|
|
||||||
|
class FederationStateV1Servlet(BaseFederationServerServlet): |
||||||
|
PATH = "/state/(?P<room_id>[^/]*)/?" |
||||||
|
|
||||||
|
# This is when someone asks for all data for a given room. |
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
return await self.handler.on_room_state_request( |
||||||
|
origin, |
||||||
|
room_id, |
||||||
|
parse_string_from_args(query, "event_id", None, required=False), |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class FederationStateIdsServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/state_ids/(?P<room_id>[^/]*)/?" |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
return await self.handler.on_state_ids_request( |
||||||
|
origin, |
||||||
|
room_id, |
||||||
|
parse_string_from_args(query, "event_id", None, required=True), |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class FederationBackfillServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/backfill/(?P<room_id>[^/]*)/?" |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
versions = [x.decode("ascii") for x in query[b"v"]] |
||||||
|
limit = parse_integer_from_args(query, "limit", None) |
||||||
|
|
||||||
|
if not limit: |
||||||
|
return 400, {"error": "Did not include limit param"} |
||||||
|
|
||||||
|
return await self.handler.on_backfill_request(origin, room_id, versions, limit) |
||||||
|
|
||||||
|
|
||||||
|
class FederationQueryServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/query/(?P<query_type>[^/]*)" |
||||||
|
|
||||||
|
# This is when we receive a server-server Query |
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
query_type: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
args = {k.decode("utf8"): v[0].decode("utf-8") for k, v in query.items()} |
||||||
|
args["origin"] = origin |
||||||
|
return await self.handler.on_query_request(query_type, args) |
||||||
|
|
||||||
|
|
||||||
|
class FederationMakeJoinServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/make_join/(?P<room_id>[^/]*)/(?P<user_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
user_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
""" |
||||||
|
Args: |
||||||
|
origin: The authenticated server_name of the calling server |
||||||
|
|
||||||
|
content: (GETs don't have bodies) |
||||||
|
|
||||||
|
query: Query params from the request. |
||||||
|
|
||||||
|
**kwargs: the dict mapping keys to path components as specified in |
||||||
|
the path match regexp. |
||||||
|
|
||||||
|
Returns: |
||||||
|
Tuple of (response code, response object) |
||||||
|
""" |
||||||
|
supported_versions = parse_strings_from_args(query, "ver", encoding="utf-8") |
||||||
|
if supported_versions is None: |
||||||
|
supported_versions = ["1"] |
||||||
|
|
||||||
|
result = await self.handler.on_make_join_request( |
||||||
|
origin, room_id, user_id, supported_versions=supported_versions |
||||||
|
) |
||||||
|
return 200, result |
||||||
|
|
||||||
|
|
||||||
|
class FederationMakeLeaveServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/make_leave/(?P<room_id>[^/]*)/(?P<user_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
user_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
result = await self.handler.on_make_leave_request(origin, room_id, user_id) |
||||||
|
return 200, result |
||||||
|
|
||||||
|
|
||||||
|
class FederationV1SendLeaveServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/send_leave/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, Tuple[int, JsonDict]]: |
||||||
|
result = await self.handler.on_send_leave_request(origin, content, room_id) |
||||||
|
return 200, (200, result) |
||||||
|
|
||||||
|
|
||||||
|
class FederationV2SendLeaveServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/send_leave/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" |
||||||
|
|
||||||
|
PREFIX = FEDERATION_V2_PREFIX |
||||||
|
|
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
result = await self.handler.on_send_leave_request(origin, content, room_id) |
||||||
|
return 200, result |
||||||
|
|
||||||
|
|
||||||
|
class FederationMakeKnockServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/make_knock/(?P<room_id>[^/]*)/(?P<user_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
user_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
# Retrieve the room versions the remote homeserver claims to support |
||||||
|
supported_versions = parse_strings_from_args( |
||||||
|
query, "ver", required=True, encoding="utf-8" |
||||||
|
) |
||||||
|
|
||||||
|
result = await self.handler.on_make_knock_request( |
||||||
|
origin, room_id, user_id, supported_versions=supported_versions |
||||||
|
) |
||||||
|
return 200, result |
||||||
|
|
||||||
|
|
||||||
|
class FederationV1SendKnockServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/send_knock/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
result = await self.handler.on_send_knock_request(origin, content, room_id) |
||||||
|
return 200, result |
||||||
|
|
||||||
|
|
||||||
|
class FederationEventAuthServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/event_auth/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
return await self.handler.on_event_auth(origin, room_id, event_id) |
||||||
|
|
||||||
|
|
||||||
|
class FederationV1SendJoinServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/send_join/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, Tuple[int, JsonDict]]: |
||||||
|
# TODO(paul): assert that event_id parsed from path actually |
||||||
|
# match those given in content |
||||||
|
result = await self.handler.on_send_join_request(origin, content, room_id) |
||||||
|
return 200, (200, result) |
||||||
|
|
||||||
|
|
||||||
|
class FederationV2SendJoinServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/send_join/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" |
||||||
|
|
||||||
|
PREFIX = FEDERATION_V2_PREFIX |
||||||
|
|
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
# TODO(paul): assert that event_id parsed from path actually |
||||||
|
# match those given in content |
||||||
|
result = await self.handler.on_send_join_request(origin, content, room_id) |
||||||
|
return 200, result |
||||||
|
|
||||||
|
|
||||||
|
class FederationV1InviteServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/invite/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, Tuple[int, JsonDict]]: |
||||||
|
# We don't get a room version, so we have to assume its EITHER v1 or |
||||||
|
# v2. This is "fine" as the only difference between V1 and V2 is the |
||||||
|
# state resolution algorithm, and we don't use that for processing |
||||||
|
# invites |
||||||
|
result = await self.handler.on_invite_request( |
||||||
|
origin, content, room_version_id=RoomVersions.V1.identifier |
||||||
|
) |
||||||
|
|
||||||
|
# V1 federation API is defined to return a content of `[200, {...}]` |
||||||
|
# due to a historical bug. |
||||||
|
return 200, (200, result) |
||||||
|
|
||||||
|
|
||||||
|
class FederationV2InviteServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/invite/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" |
||||||
|
|
||||||
|
PREFIX = FEDERATION_V2_PREFIX |
||||||
|
|
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
event_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
# TODO(paul): assert that room_id/event_id parsed from path actually |
||||||
|
# match those given in content |
||||||
|
|
||||||
|
room_version = content["room_version"] |
||||||
|
event = content["event"] |
||||||
|
invite_room_state = content["invite_room_state"] |
||||||
|
|
||||||
|
# Synapse expects invite_room_state to be in unsigned, as it is in v1 |
||||||
|
# API |
||||||
|
|
||||||
|
event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state |
||||||
|
|
||||||
|
result = await self.handler.on_invite_request( |
||||||
|
origin, event, room_version_id=room_version |
||||||
|
) |
||||||
|
return 200, result |
||||||
|
|
||||||
|
|
||||||
|
class FederationThirdPartyInviteExchangeServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/exchange_third_party_invite/(?P<room_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_PUT( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
await self.handler.on_exchange_third_party_invite_request(content) |
||||||
|
return 200, {} |
||||||
|
|
||||||
|
|
||||||
|
class FederationClientKeysQueryServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/user/keys/query" |
||||||
|
|
||||||
|
async def on_POST( |
||||||
|
self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
return await self.handler.on_query_client_keys(origin, content) |
||||||
|
|
||||||
|
|
||||||
|
class FederationUserDevicesQueryServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/user/devices/(?P<user_id>[^/]*)" |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
user_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
return await self.handler.on_query_user_devices(origin, user_id) |
||||||
|
|
||||||
|
|
||||||
|
class FederationClientKeysClaimServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/user/keys/claim" |
||||||
|
|
||||||
|
async def on_POST( |
||||||
|
self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
response = await self.handler.on_claim_client_keys(origin, content) |
||||||
|
return 200, response |
||||||
|
|
||||||
|
|
||||||
|
class FederationGetMissingEventsServlet(BaseFederationServerServlet): |
||||||
|
# TODO(paul): Why does this path alone end with "/?" optional? |
||||||
|
PATH = "/get_missing_events/(?P<room_id>[^/]*)/?" |
||||||
|
|
||||||
|
async def on_POST( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
limit = int(content.get("limit", 10)) |
||||||
|
earliest_events = content.get("earliest_events", []) |
||||||
|
latest_events = content.get("latest_events", []) |
||||||
|
|
||||||
|
result = await self.handler.on_get_missing_events( |
||||||
|
origin, |
||||||
|
room_id=room_id, |
||||||
|
earliest_events=earliest_events, |
||||||
|
latest_events=latest_events, |
||||||
|
limit=limit, |
||||||
|
) |
||||||
|
|
||||||
|
return 200, result |
||||||
|
|
||||||
|
|
||||||
|
class On3pidBindServlet(BaseFederationServerServlet): |
||||||
|
PATH = "/3pid/onbind" |
||||||
|
|
||||||
|
REQUIRE_AUTH = False |
||||||
|
|
||||||
|
async def on_POST( |
||||||
|
self, origin: Optional[str], content: JsonDict, query: Dict[bytes, List[bytes]] |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
if "invites" in content: |
||||||
|
last_exception = None |
||||||
|
for invite in content["invites"]: |
||||||
|
try: |
||||||
|
if "signed" not in invite or "token" not in invite["signed"]: |
||||||
|
message = ( |
||||||
|
"Rejecting received notification of third-" |
||||||
|
"party invite without signed: %s" % (invite,) |
||||||
|
) |
||||||
|
logger.info(message) |
||||||
|
raise SynapseError(400, message) |
||||||
|
await self.handler.exchange_third_party_invite( |
||||||
|
invite["sender"], |
||||||
|
invite["mxid"], |
||||||
|
invite["room_id"], |
||||||
|
invite["signed"], |
||||||
|
) |
||||||
|
except Exception as e: |
||||||
|
last_exception = e |
||||||
|
if last_exception: |
||||||
|
raise last_exception |
||||||
|
return 200, {} |
||||||
|
|
||||||
|
|
||||||
|
class FederationVersionServlet(BaseFederationServlet): |
||||||
|
PATH = "/version" |
||||||
|
|
||||||
|
REQUIRE_AUTH = False |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: Optional[str], |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
return ( |
||||||
|
200, |
||||||
|
{"server": {"name": "Synapse", "version": get_version_string(synapse)}}, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class FederationSpaceSummaryServlet(BaseFederationServlet): |
||||||
|
PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" |
||||||
|
PATH = "/spaces/(?P<room_id>[^/]*)" |
||||||
|
|
||||||
|
def __init__( |
||||||
|
self, |
||||||
|
hs: HomeServer, |
||||||
|
authenticator: Authenticator, |
||||||
|
ratelimiter: FederationRateLimiter, |
||||||
|
server_name: str, |
||||||
|
): |
||||||
|
super().__init__(hs, authenticator, ratelimiter, server_name) |
||||||
|
self.handler = hs.get_space_summary_handler() |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Mapping[bytes, Sequence[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) |
||||||
|
max_rooms_per_space = parse_integer_from_args(query, "max_rooms_per_space") |
||||||
|
|
||||||
|
exclude_rooms = parse_strings_from_args(query, "exclude_rooms", default=[]) |
||||||
|
|
||||||
|
return 200, await self.handler.federation_space_summary( |
||||||
|
origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms |
||||||
|
) |
||||||
|
|
||||||
|
# TODO When switching to the stable endpoint, remove the POST handler. |
||||||
|
async def on_POST( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: JsonDict, |
||||||
|
query: Mapping[bytes, Sequence[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
suggested_only = content.get("suggested_only", False) |
||||||
|
if not isinstance(suggested_only, bool): |
||||||
|
raise SynapseError( |
||||||
|
400, "'suggested_only' must be a boolean", Codes.BAD_JSON |
||||||
|
) |
||||||
|
|
||||||
|
exclude_rooms = content.get("exclude_rooms", []) |
||||||
|
if not isinstance(exclude_rooms, list) or any( |
||||||
|
not isinstance(x, str) for x in exclude_rooms |
||||||
|
): |
||||||
|
raise SynapseError(400, "bad value for 'exclude_rooms'", Codes.BAD_JSON) |
||||||
|
|
||||||
|
max_rooms_per_space = content.get("max_rooms_per_space") |
||||||
|
if max_rooms_per_space is not None and not isinstance(max_rooms_per_space, int): |
||||||
|
raise SynapseError( |
||||||
|
400, "bad value for 'max_rooms_per_space'", Codes.BAD_JSON |
||||||
|
) |
||||||
|
|
||||||
|
return 200, await self.handler.federation_space_summary( |
||||||
|
origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class FederationRoomHierarchyServlet(BaseFederationServlet): |
||||||
|
PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" |
||||||
|
PATH = "/hierarchy/(?P<room_id>[^/]*)" |
||||||
|
|
||||||
|
def __init__( |
||||||
|
self, |
||||||
|
hs: HomeServer, |
||||||
|
authenticator: Authenticator, |
||||||
|
ratelimiter: FederationRateLimiter, |
||||||
|
server_name: str, |
||||||
|
): |
||||||
|
super().__init__(hs, authenticator, ratelimiter, server_name) |
||||||
|
self.handler = hs.get_space_summary_handler() |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Mapping[bytes, Sequence[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) |
||||||
|
return 200, await self.handler.get_federation_hierarchy( |
||||||
|
origin, room_id, suggested_only |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class RoomComplexityServlet(BaseFederationServlet): |
||||||
|
""" |
||||||
|
Indicates to other servers how complex (and therefore likely |
||||||
|
resource-intensive) a public room this server knows about is. |
||||||
|
""" |
||||||
|
|
||||||
|
PATH = "/rooms/(?P<room_id>[^/]*)/complexity" |
||||||
|
PREFIX = FEDERATION_UNSTABLE_PREFIX |
||||||
|
|
||||||
|
def __init__( |
||||||
|
self, |
||||||
|
hs: HomeServer, |
||||||
|
authenticator: Authenticator, |
||||||
|
ratelimiter: FederationRateLimiter, |
||||||
|
server_name: str, |
||||||
|
): |
||||||
|
super().__init__(hs, authenticator, ratelimiter, server_name) |
||||||
|
self._store = self.hs.get_datastore() |
||||||
|
|
||||||
|
async def on_GET( |
||||||
|
self, |
||||||
|
origin: str, |
||||||
|
content: Literal[None], |
||||||
|
query: Dict[bytes, List[bytes]], |
||||||
|
room_id: str, |
||||||
|
) -> Tuple[int, JsonDict]: |
||||||
|
is_public = await self._store.is_room_world_readable_or_publicly_joinable( |
||||||
|
room_id |
||||||
|
) |
||||||
|
|
||||||
|
if not is_public: |
||||||
|
raise SynapseError(404, "Room not found", errcode=Codes.INVALID_PARAM) |
||||||
|
|
||||||
|
complexity = await self._store.get_room_complexity(room_id) |
||||||
|
return 200, complexity |
||||||
|
|
||||||
|
|
||||||
|
FEDERATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( |
||||||
|
FederationSendServlet, |
||||||
|
FederationEventServlet, |
||||||
|
FederationStateV1Servlet, |
||||||
|
FederationStateIdsServlet, |
||||||
|
FederationBackfillServlet, |
||||||
|
FederationQueryServlet, |
||||||
|
FederationMakeJoinServlet, |
||||||
|
FederationMakeLeaveServlet, |
||||||
|
FederationEventServlet, |
||||||
|
FederationV1SendJoinServlet, |
||||||
|
FederationV2SendJoinServlet, |
||||||
|
FederationV1SendLeaveServlet, |
||||||
|
FederationV2SendLeaveServlet, |
||||||
|
FederationV1InviteServlet, |
||||||
|
FederationV2InviteServlet, |
||||||
|
FederationGetMissingEventsServlet, |
||||||
|
FederationEventAuthServlet, |
||||||
|
FederationClientKeysQueryServlet, |
||||||
|
FederationUserDevicesQueryServlet, |
||||||
|
FederationClientKeysClaimServlet, |
||||||
|
FederationThirdPartyInviteExchangeServlet, |
||||||
|
On3pidBindServlet, |
||||||
|
FederationVersionServlet, |
||||||
|
RoomComplexityServlet, |
||||||
|
FederationSpaceSummaryServlet, |
||||||
|
FederationRoomHierarchyServlet, |
||||||
|
FederationV1SendKnockServlet, |
||||||
|
FederationMakeKnockServlet, |
||||||
|
) |
@ -0,0 +1,113 @@ |
|||||||
|
# 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 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.server import HomeServer |
||||||
|
from synapse.types import JsonDict, get_domain_from_id |
||||||
|
from synapse.util.ratelimitutils import FederationRateLimiter |
||||||
|
|
||||||
|
|
||||||
|
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, |
||||||
|
) |
@ -0,0 +1,753 @@ |
|||||||
|
# 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 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.server import HomeServer |
||||||
|
from synapse.types import JsonDict, get_domain_from_id |
||||||
|
from synapse.util.ratelimitutils import FederationRateLimiter |
||||||
|
|
||||||
|
|
||||||
|
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, |
||||||
|
) |
Loading…
Reference in new issue