mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-07 01:20:16 +00:00
Compare commits
15 Commits
anoa/log_e
...
anoa/knock
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c781a1662 | ||
|
|
55163a0c80 | ||
|
|
a2ab56a068 | ||
|
|
9bdd420f9b | ||
|
|
f10ee502c7 | ||
|
|
fa7f378bba | ||
|
|
3dbe05d022 | ||
|
|
09e1942b04 | ||
|
|
a4dafd407d | ||
|
|
fa1de1d858 | ||
|
|
63c767ac10 | ||
|
|
c4ff5ddd00 | ||
|
|
6bd57f4bf3 | ||
|
|
f7d11de227 | ||
|
|
8b79ea03e5 |
1
changelog.d/6739.feature
Normal file
1
changelog.d/6739.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement "room knocking" as per MSC2403. Contributed by Sorunome and anoa.
|
||||
@@ -1395,6 +1395,17 @@ metrics_flags:
|
||||
# - "m.room.encryption"
|
||||
# - "m.room.name"
|
||||
|
||||
# A list of event types from a room that will be given to users when they
|
||||
# knock on the room. This allows clients to display information about the
|
||||
# room that they've knocked on, without actually being in the room yet.
|
||||
#
|
||||
#room_knock_state_types:
|
||||
# - "m.room.join_rules"
|
||||
# - "m.room.canonical_alias"
|
||||
# - "m.room.avatar"
|
||||
# - "m.room.encryption"
|
||||
# - "m.room.name"
|
||||
|
||||
|
||||
# A list of application service config files to use
|
||||
#
|
||||
|
||||
@@ -57,16 +57,19 @@ class RoomVersion:
|
||||
state_res = attr.ib() # int; one of the StateResolutionVersions
|
||||
enforce_key_validity = attr.ib() # bool
|
||||
|
||||
# bool: before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules
|
||||
# Before MSC2432, m.room.aliases had special auth rules and redaction rules
|
||||
special_case_aliases_auth = attr.ib(type=bool)
|
||||
# Strictly enforce canonicaljson, do not allow:
|
||||
# * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
|
||||
# * Floats
|
||||
# * NaN, Infinity, -Infinity
|
||||
strict_canonicaljson = attr.ib(type=bool)
|
||||
# bool: MSC2209: Check 'notifications' key while verifying
|
||||
# MSC2209: Check 'notifications' key while verifying
|
||||
# m.room.power_levels auth rules.
|
||||
limit_notifications_power_levels = attr.ib(type=bool)
|
||||
# MSC2403: Allows join_rules to be set to 'knock', changes auth rules to allow sending
|
||||
# m.room.membership event with membership 'knock'.
|
||||
allow_knocking = attr.ib(type=bool)
|
||||
|
||||
|
||||
class RoomVersions:
|
||||
@@ -79,6 +82,7 @@ class RoomVersions:
|
||||
special_case_aliases_auth=True,
|
||||
strict_canonicaljson=False,
|
||||
limit_notifications_power_levels=False,
|
||||
allow_knocking=False,
|
||||
)
|
||||
V2 = RoomVersion(
|
||||
"2",
|
||||
@@ -89,6 +93,7 @@ class RoomVersions:
|
||||
special_case_aliases_auth=True,
|
||||
strict_canonicaljson=False,
|
||||
limit_notifications_power_levels=False,
|
||||
allow_knocking=False,
|
||||
)
|
||||
V3 = RoomVersion(
|
||||
"3",
|
||||
@@ -99,6 +104,7 @@ class RoomVersions:
|
||||
special_case_aliases_auth=True,
|
||||
strict_canonicaljson=False,
|
||||
limit_notifications_power_levels=False,
|
||||
allow_knocking=False,
|
||||
)
|
||||
V4 = RoomVersion(
|
||||
"4",
|
||||
@@ -109,6 +115,7 @@ class RoomVersions:
|
||||
special_case_aliases_auth=True,
|
||||
strict_canonicaljson=False,
|
||||
limit_notifications_power_levels=False,
|
||||
allow_knocking=False,
|
||||
)
|
||||
V5 = RoomVersion(
|
||||
"5",
|
||||
@@ -119,6 +126,7 @@ class RoomVersions:
|
||||
special_case_aliases_auth=True,
|
||||
strict_canonicaljson=False,
|
||||
limit_notifications_power_levels=False,
|
||||
allow_knocking=False,
|
||||
)
|
||||
V6 = RoomVersion(
|
||||
"6",
|
||||
@@ -129,6 +137,18 @@ class RoomVersions:
|
||||
special_case_aliases_auth=False,
|
||||
strict_canonicaljson=True,
|
||||
limit_notifications_power_levels=True,
|
||||
allow_knocking=False,
|
||||
)
|
||||
MSC2403_DEV = RoomVersion(
|
||||
"xyz.amorgan.knock",
|
||||
RoomDisposition.UNSTABLE,
|
||||
EventFormatVersions.V3,
|
||||
StateResolutionVersions.V2,
|
||||
enforce_key_validity=True,
|
||||
special_case_aliases_auth=False,
|
||||
strict_canonicaljson=True,
|
||||
limit_notifications_power_levels=True,
|
||||
allow_knocking=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -141,5 +161,6 @@ KNOWN_ROOM_VERSIONS = {
|
||||
RoomVersions.V4,
|
||||
RoomVersions.V5,
|
||||
RoomVersions.V6,
|
||||
RoomVersions.MSC2403_DEV,
|
||||
)
|
||||
} # type: Dict[str, RoomVersion]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2020 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.
|
||||
@@ -21,15 +22,18 @@ class ApiConfig(Config):
|
||||
section = "api"
|
||||
|
||||
def read_config(self, config, **kwargs):
|
||||
default_room_state_types = [
|
||||
EventTypes.JoinRules,
|
||||
EventTypes.CanonicalAlias,
|
||||
EventTypes.RoomAvatar,
|
||||
EventTypes.RoomEncryption,
|
||||
EventTypes.Name,
|
||||
]
|
||||
self.room_invite_state_types = config.get(
|
||||
"room_invite_state_types",
|
||||
[
|
||||
EventTypes.JoinRules,
|
||||
EventTypes.CanonicalAlias,
|
||||
EventTypes.RoomAvatar,
|
||||
EventTypes.RoomEncryption,
|
||||
EventTypes.Name,
|
||||
],
|
||||
"room_invite_state_types", default_room_state_types
|
||||
)
|
||||
self.room_knock_state_types = config.get(
|
||||
"room_knock_state_types", default_room_state_types
|
||||
)
|
||||
|
||||
def generate_config_section(cls, **kwargs):
|
||||
@@ -44,6 +48,17 @@ class ApiConfig(Config):
|
||||
# - "{RoomAvatar}"
|
||||
# - "{RoomEncryption}"
|
||||
# - "{Name}"
|
||||
|
||||
# A list of event types from a room that will be given to users when they
|
||||
# knock on the room. This allows clients to display information about the
|
||||
# room that they've knocked on, without actually being in the room yet.
|
||||
#
|
||||
#room_knock_state_types:
|
||||
# - "{JoinRules}"
|
||||
# - "{CanonicalAlias}"
|
||||
# - "{RoomAvatar}"
|
||||
# - "{RoomEncryption}"
|
||||
# - "{Name}"
|
||||
""".format(
|
||||
**vars(EventTypes)
|
||||
)
|
||||
|
||||
@@ -161,6 +161,7 @@ def check(
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Auth events: %s", [a.event_id for a in auth_events.values()])
|
||||
|
||||
# 5. If type is m.room.membership
|
||||
if event.type == EventTypes.Member:
|
||||
_is_membership_change_allowed(event, auth_events)
|
||||
logger.debug("Allowing! %s", event)
|
||||
@@ -247,6 +248,7 @@ def _is_membership_change_allowed(
|
||||
|
||||
caller_in_room = caller and caller.membership == Membership.JOIN
|
||||
caller_invited = caller and caller.membership == Membership.INVITE
|
||||
caller_knocked = caller and caller.membership == Membership.KNOCK
|
||||
|
||||
# get info about the target
|
||||
key = (EventTypes.Member, target_user_id)
|
||||
@@ -289,9 +291,12 @@ def _is_membership_change_allowed(
|
||||
raise AuthError(403, "%s is banned from the room" % (target_user_id,))
|
||||
return
|
||||
|
||||
if Membership.JOIN != membership:
|
||||
# Require the user to be in the room for membership changes other than join/knocking
|
||||
if Membership.JOIN != membership and Membership.KNOCK != membership:
|
||||
# If the user has been invited or has knocked, they are allowed to change their
|
||||
# membership event to leave
|
||||
if (
|
||||
caller_invited
|
||||
(caller_invited or caller_knocked)
|
||||
and Membership.LEAVE == membership
|
||||
and target_user_id == event.user_id
|
||||
):
|
||||
@@ -324,7 +329,7 @@ def _is_membership_change_allowed(
|
||||
raise AuthError(403, "You are banned from this room")
|
||||
elif join_rule == JoinRules.PUBLIC:
|
||||
pass
|
||||
elif join_rule == JoinRules.INVITE:
|
||||
elif join_rule in [JoinRules.INVITE, JoinRules.KNOCK]:
|
||||
if not caller_in_room and not caller_invited:
|
||||
raise AuthError(403, "You are not invited to this room.")
|
||||
else:
|
||||
@@ -343,6 +348,15 @@ def _is_membership_change_allowed(
|
||||
elif Membership.BAN == membership:
|
||||
if user_level < ban_level or user_level <= target_level:
|
||||
raise AuthError(403, "You don't have permission to ban")
|
||||
elif Membership.KNOCK == membership:
|
||||
if join_rule != JoinRules.KNOCK:
|
||||
raise AuthError(403, "You don't have permission to knock")
|
||||
elif target_user_id != event.user_id:
|
||||
raise AuthError(403, "You cannot knock for other users")
|
||||
elif target_in_room:
|
||||
raise AuthError(403, "You cannot knock on a room you are already in")
|
||||
elif target_banned:
|
||||
raise AuthError(403, "You are banned from this room")
|
||||
else:
|
||||
raise AuthError(500, "Unknown membership %s" % membership)
|
||||
|
||||
@@ -699,7 +713,7 @@ def auth_types_for_event(event: EventBase) -> Set[Tuple[str, str]]:
|
||||
|
||||
if event.type == EventTypes.Member:
|
||||
membership = event.content["membership"]
|
||||
if membership in [Membership.JOIN, Membership.INVITE]:
|
||||
if membership in [Membership.JOIN, Membership.INVITE, Membership.KNOCK]:
|
||||
auth_types.add((EventTypes.JoinRules, ""))
|
||||
|
||||
auth_types.add((EventTypes.Member, event.state_key))
|
||||
|
||||
@@ -228,6 +228,7 @@ def format_event_for_client_v1(d):
|
||||
"replaces_state",
|
||||
"prev_content",
|
||||
"invite_room_state",
|
||||
"knock_room_state",
|
||||
)
|
||||
for key in copy_keys:
|
||||
if key in d["unsigned"]:
|
||||
@@ -311,6 +312,10 @@ def serialize_event(
|
||||
# If this is an invite for somebody else, then we don't care about the
|
||||
# invite_room_state as that's meant solely for the invitee. Other clients
|
||||
# will already have the state since they're in the room.
|
||||
#
|
||||
# Note that is_invite only seems to be set to False in the case of appservices
|
||||
# that are not interested in the target user of the invite.
|
||||
# TODO: Figure out what to do here w.r.t knocking
|
||||
if not is_invite:
|
||||
d["unsigned"].pop("invite_room_state", None)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyrignt 2020 Sorunome
|
||||
# Copyrignt 2020 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.
|
||||
@@ -539,7 +541,7 @@ class FederationClient(FederationBase):
|
||||
|
||||
RuntimeError: if no servers were reachable.
|
||||
"""
|
||||
valid_memberships = {Membership.JOIN, Membership.LEAVE}
|
||||
valid_memberships = {Membership.JOIN, Membership.LEAVE, Membership.KNOCK}
|
||||
if membership not in valid_memberships:
|
||||
raise RuntimeError(
|
||||
"make_membership_event called with membership='%s', must be one of %s"
|
||||
@@ -879,6 +881,62 @@ class FederationClient(FederationBase):
|
||||
# content.
|
||||
return resp[1]
|
||||
|
||||
async def send_knock(self, destinations: List[str], pdu: EventBase) -> JsonDict:
|
||||
"""Attempts to send a knock event to given a list of servers. Iterates
|
||||
through the list until one attempt succeeds.
|
||||
|
||||
Doing so will cause the remote server to add the event to the graph,
|
||||
and send the event out to the rest of the federation.
|
||||
|
||||
Args:
|
||||
destinations: A list of candidate homeservers which are likely to be
|
||||
participating in the room.
|
||||
pdu: The event to be sent.
|
||||
|
||||
Returns:
|
||||
The remote homeserver return some state from the room. The response
|
||||
dictionary is in the form:
|
||||
|
||||
{"knock_state_events": [<state event dict>, ...]}
|
||||
|
||||
The list of state events may be empty.
|
||||
|
||||
Raises:
|
||||
SynapseError: If the chosen remote server returns a 3xx/4xx code.
|
||||
RuntimeError: If no servers were reachable.
|
||||
"""
|
||||
|
||||
async def send_request(destination: str) -> JsonDict:
|
||||
return await self._do_send_knock(destination, pdu)
|
||||
|
||||
return await self._try_destination_list(
|
||||
"send_knock", destinations, send_request
|
||||
)
|
||||
|
||||
async def _do_send_knock(self, destination: str, pdu: EventBase) -> JsonDict:
|
||||
"""Send a knock event to a remote homeserver.
|
||||
|
||||
Args:
|
||||
destination: The homeserver to send to.
|
||||
pdu: The event to send.
|
||||
|
||||
Returns:
|
||||
The remote homeserver can optionally return some state from the room. The response
|
||||
dictionary is in the form:
|
||||
|
||||
{"knock_state_events": [<state event dict>, ...]}
|
||||
|
||||
The list of state events may be empty.
|
||||
"""
|
||||
time_now = self._clock.time_msec()
|
||||
|
||||
return await self.transport_layer.send_knock_v1(
|
||||
destination=destination,
|
||||
room_id=pdu.room_id,
|
||||
event_id=pdu.event_id,
|
||||
content=pdu.get_pdu_json(time_now),
|
||||
)
|
||||
|
||||
def get_public_rooms(
|
||||
self,
|
||||
remote_server: str,
|
||||
|
||||
@@ -118,6 +118,8 @@ class FederationServer(FederationBase):
|
||||
hs, "fed_txn_handler", timeout_ms=30000
|
||||
) # type: ResponseCache[Tuple[str, str]]
|
||||
|
||||
self._room_knock_state_types = hs.config.room_knock_state_types
|
||||
|
||||
self.transaction_actions = TransactionActions(self.store)
|
||||
|
||||
self.registry = hs.get_federation_registry()
|
||||
@@ -565,6 +567,40 @@ class FederationServer(FederationBase):
|
||||
await self.handler.on_send_leave_request(origin, pdu)
|
||||
return {}
|
||||
|
||||
async def on_make_knock_request(self, origin: str, room_id: str, user_id: str):
|
||||
origin_host, _ = parse_server_name(origin)
|
||||
await self.check_server_matches_acl(origin_host, room_id)
|
||||
pdu = await self.handler.on_make_knock_request(origin, room_id, user_id)
|
||||
|
||||
room_version = await self.store.get_room_version_id(room_id)
|
||||
|
||||
time_now = self._clock.time_msec()
|
||||
return {"event": pdu.get_pdu_json(time_now), "room_version": room_version}
|
||||
|
||||
async def on_send_knock_request(self, origin: str, content: JsonDict, room_id: str):
|
||||
logger.debug("on_send_knock_request: content: %s", content)
|
||||
|
||||
room_version = await self.store.get_room_version(room_id)
|
||||
pdu = event_from_pdu_json(content, room_version)
|
||||
|
||||
origin_host, _ = parse_server_name(origin)
|
||||
await self.check_server_matches_acl(origin_host, pdu.room_id)
|
||||
|
||||
logger.debug("on_send_knock_request: pdu sigs: %s", pdu.signatures)
|
||||
|
||||
pdu = await self._check_sigs_and_hash(room_version, pdu)
|
||||
|
||||
# Handle the event, and retrieve the EventContext
|
||||
event_context = await self.handler.on_send_knock_request(origin, pdu)
|
||||
|
||||
# Retrieve stripped state events from the room and send them back to the remote
|
||||
# server. This will allow the remote server's clients to display information
|
||||
# related to the room while the knock request is pending.
|
||||
stripped_room_state = await self.store.get_stripped_room_state_from_event_context(
|
||||
event_context, self._room_knock_state_types
|
||||
)
|
||||
return {"knock_state_events": stripped_room_state}
|
||||
|
||||
async def on_event_auth(
|
||||
self, origin: str, room_id: str, event_id: str
|
||||
) -> Tuple[int, Dict[str, Any]]:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2020 Sorunome
|
||||
# Copyright 2020 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.
|
||||
@@ -26,6 +28,7 @@ from synapse.api.urls import (
|
||||
FEDERATION_V2_PREFIX,
|
||||
)
|
||||
from synapse.logging.utils import log_function
|
||||
from synapse.types import JsonDict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -209,7 +212,7 @@ class TransportLayerClient:
|
||||
Fails with ``FederationDeniedError`` if the remote destination
|
||||
is not in our federation whitelist
|
||||
"""
|
||||
valid_memberships = {Membership.JOIN, Membership.LEAVE}
|
||||
valid_memberships = {Membership.JOIN, Membership.LEAVE, Membership.KNOCK}
|
||||
if membership not in valid_memberships:
|
||||
raise RuntimeError(
|
||||
"make_membership_event called with membership='%s', must be one of %s"
|
||||
@@ -293,6 +296,36 @@ class TransportLayerClient:
|
||||
|
||||
return response
|
||||
|
||||
@log_function
|
||||
async def send_knock_v1(
|
||||
self, destination: str, room_id: str, event_id: str, content: JsonDict,
|
||||
) -> JsonDict:
|
||||
"""
|
||||
Sends an signed knock membership event to a remote server. This is the second
|
||||
step knocking after make_knock.
|
||||
|
||||
Args:
|
||||
destination: The remote homeserver.
|
||||
room_id: The ID of the room to knock on.
|
||||
event_id: The ID of the knock membership event that we're sending.
|
||||
content: The knock membership event that we're sending. Note that this is not the
|
||||
`content` field of the membership event, but the entire signed membership event
|
||||
itself represented as a JSON dict.
|
||||
|
||||
Returns:
|
||||
The remote homeserver can optionally return some state from the room. The response
|
||||
dictionary is in the form:
|
||||
|
||||
{"knock_state_events": [<state event dict>, ...]}
|
||||
|
||||
The list of state events may be empty.
|
||||
"""
|
||||
path = _create_v1_path("/send_knock/%s/%s", room_id, event_id)
|
||||
|
||||
return await self.client.put_json(
|
||||
destination=destination, path=path, data=content
|
||||
)
|
||||
|
||||
@log_function
|
||||
async def send_invite_v1(self, destination, room_id, event_id, content):
|
||||
path = _create_v1_path("/invite/%s/%s", room_id, event_id)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2019-2020 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.
|
||||
@@ -542,6 +543,22 @@ class FederationV2SendLeaveServlet(BaseFederationServlet):
|
||||
return 200, content
|
||||
|
||||
|
||||
class FederationMakeKnockServlet(BaseFederationServlet):
|
||||
PATH = "/make_knock/(?P<context>[^/]*)/(?P<user_id>[^/]*)"
|
||||
|
||||
async def on_GET(self, origin, content, query, context, user_id):
|
||||
content = await self.handler.on_make_knock_request(origin, context, user_id)
|
||||
return 200, content
|
||||
|
||||
|
||||
class FederationV1MakeKnockServlet(BaseFederationServlet):
|
||||
PATH = "/send_knock/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)"
|
||||
|
||||
async def on_PUT(self, origin, content, query, room_id, event_id):
|
||||
content = await self.handler.on_send_knock_request(origin, content, room_id)
|
||||
return 200, content
|
||||
|
||||
|
||||
class FederationEventAuthServlet(BaseFederationServlet):
|
||||
PATH = "/event_auth/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
|
||||
|
||||
@@ -1389,11 +1406,13 @@ FEDERATION_SERVLET_CLASSES = (
|
||||
FederationQueryServlet,
|
||||
FederationMakeJoinServlet,
|
||||
FederationMakeLeaveServlet,
|
||||
FederationMakeKnockServlet,
|
||||
FederationEventServlet,
|
||||
FederationV1SendJoinServlet,
|
||||
FederationV2SendJoinServlet,
|
||||
FederationV1SendLeaveServlet,
|
||||
FederationV2SendLeaveServlet,
|
||||
FederationV1MakeKnockServlet,
|
||||
FederationV1InviteServlet,
|
||||
FederationV2InviteServlet,
|
||||
FederationGetMissingEventsServlet,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2019-2020 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.
|
||||
@@ -67,7 +68,7 @@ from synapse.replication.http.devices import ReplicationUserDevicesResyncRestSer
|
||||
from synapse.replication.http.federation import (
|
||||
ReplicationCleanRoomRestServlet,
|
||||
ReplicationFederationSendEventsRestServlet,
|
||||
ReplicationStoreRoomOnInviteRestServlet,
|
||||
ReplicationStoreRoomOnOutlierMembershipRestServlet,
|
||||
)
|
||||
from synapse.state import StateResolutionStore
|
||||
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
|
||||
@@ -152,12 +153,14 @@ class FederationHandler(BaseHandler):
|
||||
self._user_device_resync = ReplicationUserDevicesResyncRestServlet.make_client(
|
||||
hs
|
||||
)
|
||||
self._maybe_store_room_on_invite = ReplicationStoreRoomOnInviteRestServlet.make_client(
|
||||
self._maybe_store_room_on_outlier_membership = ReplicationStoreRoomOnOutlierMembershipRestServlet.make_client(
|
||||
hs
|
||||
)
|
||||
else:
|
||||
self._device_list_updater = hs.get_device_handler().device_list_updater
|
||||
self._maybe_store_room_on_invite = self.store.maybe_store_room_on_invite
|
||||
self._maybe_store_room_on_outlier_membership = (
|
||||
self.store.maybe_store_room_on_outlier_membership
|
||||
)
|
||||
|
||||
# When joining a room we need to queue any events for that room up.
|
||||
# For each room, a list of (pdu, origin) tuples.
|
||||
@@ -1436,6 +1439,69 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
run_in_background(self._handle_queued_pdus, room_queue)
|
||||
|
||||
@log_function
|
||||
async def do_knock(
|
||||
self, target_hosts: List[str], room_id: str, knockee: str, content: JsonDict,
|
||||
) -> Tuple[str, int]:
|
||||
"""Sends the knock to the remote server.
|
||||
|
||||
This first triggers a /make_knock request that returns a partial
|
||||
event that we can fill out and sign. This is then sent to the
|
||||
remote server via /send_knock.
|
||||
|
||||
Knock events must be signed by the knockee's server before distributing.
|
||||
|
||||
Args:
|
||||
target_hosts: A list of hosts that we want to try knocking through.
|
||||
room_id: The ID of the room to knock on.
|
||||
knockee: The ID of the user who is knocking.
|
||||
content: The content of the knock event.
|
||||
|
||||
Returns:
|
||||
A tuple of (event ID, stream ID).
|
||||
|
||||
Raises:
|
||||
SynapseError: If the chosen remote server returns a 3xx/4xx code.
|
||||
RuntimeError: If no servers were reachable.
|
||||
"""
|
||||
logger.debug("Knocking on room %s on behalf of user %s", room_id, knockee)
|
||||
|
||||
# Ask the remote server to create a valid knock event for us. Once received,
|
||||
# we sign the event
|
||||
origin, event, event_format_version = await self._make_and_verify_event(
|
||||
target_hosts, room_id, knockee, "knock", content,
|
||||
)
|
||||
|
||||
# Record the room ID and its version so that we have a record of the room
|
||||
await self._maybe_store_room_on_outlier_membership(
|
||||
room_id=event.room_id, room_version=event_format_version
|
||||
)
|
||||
|
||||
# Initially try the host that we successfully called /make_knock on
|
||||
try:
|
||||
target_hosts.remove(origin)
|
||||
target_hosts.insert(0, origin)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Send the signed event back to the room, and potentially receive some
|
||||
# further information about the room in the form of partial state events
|
||||
stripped_room_state = await self.federation_client.send_knock(
|
||||
target_hosts, event
|
||||
)
|
||||
|
||||
# Store any stripped room state events in the "unsigned" key of the event.
|
||||
# This is a bit of a hack and is cribbing off of invites. Basically we
|
||||
# store the room state here and retrieve it again when this event appears
|
||||
# in the invitee's sync stream. It is stripped out for all other local users.
|
||||
event.unsigned["knock_room_state"] = stripped_room_state["knock_state_events"]
|
||||
|
||||
context = await self.state_handler.compute_event_context(event)
|
||||
stream_id = await self.persist_events_and_notify(
|
||||
event.room_id, [(event, context)]
|
||||
)
|
||||
return event.event_id, stream_id
|
||||
|
||||
async def _handle_queued_pdus(self, room_queue):
|
||||
"""Process PDUs which got queued up while we were busy send_joining.
|
||||
|
||||
@@ -1617,7 +1683,7 @@ class FederationHandler(BaseHandler):
|
||||
# keep a record of the room version, if we don't yet know it.
|
||||
# (this may get overwritten if we later get a different room version in a
|
||||
# join dance).
|
||||
await self._maybe_store_room_on_invite(
|
||||
await self._maybe_store_room_on_outlier_membership(
|
||||
room_id=event.room_id, room_version=room_version
|
||||
)
|
||||
|
||||
@@ -1772,6 +1838,117 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
return None
|
||||
|
||||
@log_function
|
||||
async def on_make_knock_request(
|
||||
self, origin: str, room_id: str, user_id: str
|
||||
) -> EventBase:
|
||||
"""We've received a /make_knock/ request, so we create a partial
|
||||
knock event for the room and return that. We do *not* persist or
|
||||
process it until the other server has signed it and sent it back.
|
||||
|
||||
Args:
|
||||
origin: The (verified) server name of the requesting server.
|
||||
room_id: The room to create the knock event in.
|
||||
user_id: The user to create the knock for.
|
||||
|
||||
Returns:
|
||||
The partial knock event.
|
||||
"""
|
||||
if get_domain_from_id(user_id) != origin:
|
||||
logger.info(
|
||||
"Get /make_knock request for user %r from different origin %s, ignoring",
|
||||
user_id,
|
||||
origin,
|
||||
)
|
||||
raise SynapseError(403, "User not from origin", Codes.FORBIDDEN)
|
||||
|
||||
room_version = await self.store.get_room_version_id(room_id)
|
||||
builder = self.event_builder_factory.new(
|
||||
room_version,
|
||||
{
|
||||
"type": EventTypes.Member,
|
||||
"content": {"membership": Membership.KNOCK},
|
||||
"room_id": room_id,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
},
|
||||
)
|
||||
|
||||
event, context = await self.event_creation_handler.create_new_client_event(
|
||||
builder=builder
|
||||
)
|
||||
|
||||
event_allowed = await self.third_party_event_rules.check_event_allowed(
|
||||
event, context
|
||||
)
|
||||
if not event_allowed:
|
||||
logger.warning("Creation of knock %s forbidden by third-party rules", event)
|
||||
raise SynapseError(
|
||||
403, "This event is not allowed in this context", Codes.FORBIDDEN
|
||||
)
|
||||
|
||||
try:
|
||||
# The remote hasn't signed it yet, obviously. We'll do the full checks
|
||||
# when we get the event back in `on_send_knock_request`
|
||||
await self.auth.check_from_context(
|
||||
room_version, event, context, do_sig_check=False
|
||||
)
|
||||
except AuthError as e:
|
||||
logger.warning("Failed to create new knock %r because %s", event, e)
|
||||
raise e
|
||||
|
||||
return event
|
||||
|
||||
@log_function
|
||||
async def on_send_knock_request(self, origin: str, pdu: EventBase) -> EventContext:
|
||||
"""
|
||||
We have received a knock event for a room. Verify that event and send it into the room
|
||||
on the knocking homeserver's behalf.
|
||||
|
||||
Args:
|
||||
origin: The remote homeserver of the knocking user.
|
||||
pdu: The knocking member event that has been signed by the remote homeserver.
|
||||
|
||||
Returns:
|
||||
The context of the event after inserting it into the room graph.
|
||||
"""
|
||||
event = pdu
|
||||
|
||||
logger.debug(
|
||||
"on_send_knock_request: Got event: %s, signatures: %s",
|
||||
event.event_id,
|
||||
event.signatures,
|
||||
)
|
||||
|
||||
if get_domain_from_id(event.sender) != origin:
|
||||
logger.info(
|
||||
"Got /send_knock request for user %r from different origin %s",
|
||||
event.sender,
|
||||
origin,
|
||||
)
|
||||
raise SynapseError(403, "User not from origin", Codes.FORBIDDEN)
|
||||
|
||||
event.internal_metadata.outlier = False
|
||||
|
||||
context = await self._handle_new_event(origin, event)
|
||||
|
||||
event_allowed = await self.third_party_event_rules.check_event_allowed(
|
||||
event, context
|
||||
)
|
||||
if not event_allowed:
|
||||
logger.info("Sending of knock %s forbidden by third-party rules", event)
|
||||
raise SynapseError(
|
||||
403, "This event is not allowed in this context", Codes.FORBIDDEN
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"on_send_knock_request: After _handle_new_event: %s, sigs: %s",
|
||||
event.event_id,
|
||||
event.signatures,
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
async def get_state_for_pdu(self, room_id: str, event_id: str) -> List[EventBase]:
|
||||
"""Returns the state at the event. i.e. not including said event.
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
# Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
||||
# Copyrignt 2020 Sorunome
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -492,7 +493,7 @@ class EventCreationHandler:
|
||||
membership = builder.content.get("membership", None)
|
||||
target = UserID.from_string(builder.state_key)
|
||||
|
||||
if membership in {Membership.JOIN, Membership.INVITE}:
|
||||
if membership in {Membership.JOIN, Membership.INVITE, Membership.KNOCK}:
|
||||
# If event doesn't include a display name, add one.
|
||||
profile = self.profile_handler
|
||||
content = builder.content
|
||||
@@ -914,8 +915,8 @@ class EventCreationHandler:
|
||||
room_version = await self.store.get_room_version_id(event.room_id)
|
||||
|
||||
if event.internal_metadata.is_out_of_band_membership():
|
||||
# the only sort of out-of-band-membership events we expect to see here
|
||||
# are invite rejections we have generated ourselves.
|
||||
# the only sort of out-of-band-membership events we expect to see here are
|
||||
# invite rejections and rescinded knocks that we have generated ourselves.
|
||||
assert event.type == EventTypes.Member
|
||||
assert event.content["membership"] == Membership.LEAVE
|
||||
else:
|
||||
@@ -1138,9 +1139,6 @@ class EventCreationHandler:
|
||||
if original_event.room_id != event.room_id:
|
||||
raise SynapseError(400, "Cannot redact event from a different room")
|
||||
|
||||
if original_event.type == EventTypes.ServerACL:
|
||||
raise AuthError(403, "Redacting server ACL events is not permitted")
|
||||
|
||||
prev_state_ids = await context.get_prev_state_ids()
|
||||
auth_events_ids = self.auth.compute_auth_events(
|
||||
event, prev_state_ids, for_verification=True
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2016-2020 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.
|
||||
@@ -110,6 +111,20 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _remote_knock(
|
||||
self, remote_room_hosts: List[str], room_id: str, user: UserID, content: dict,
|
||||
) -> Tuple[str, int]:
|
||||
"""Try and knock on a room that this server is not in
|
||||
|
||||
Args:
|
||||
remote_room_hosts: List of servers that can be used to knock via.
|
||||
room_id: Room that we are trying to knock on.
|
||||
user: User who is trying to knock.
|
||||
content: A dict that should be used as the content of the knock event.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def remote_reject_invite(
|
||||
self,
|
||||
@@ -133,6 +148,28 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def generate_local_out_of_band_membership(
|
||||
self,
|
||||
previous_membership_event: EventBase,
|
||||
txn_id: Optional[str],
|
||||
requester: Requester,
|
||||
content: JsonDict,
|
||||
):
|
||||
"""Generate a local leave event for a room
|
||||
|
||||
Args:
|
||||
previous_membership_event: the previous membership event for this user
|
||||
txn_id: optional transaction ID supplied by the client
|
||||
requester: user making the request, according to the access token
|
||||
content: additional content to include in the leave event.
|
||||
Normally an empty dict.
|
||||
|
||||
Returns:
|
||||
A tuple containing (event_id, stream_id of the leave event)
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Notifies distributor on master process that the user has left the
|
||||
@@ -518,40 +555,77 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
invite = await self.store.get_invite_for_local_user_in_room(
|
||||
user_id=target.to_string(), room_id=room_id
|
||||
) # type: Optional[RoomsForUser]
|
||||
if not invite:
|
||||
if invite:
|
||||
logger.info(
|
||||
"%s rejects invite to %s from %s",
|
||||
target,
|
||||
room_id,
|
||||
invite.sender,
|
||||
)
|
||||
|
||||
if not self.hs.is_mine_id(invite.sender):
|
||||
# send the rejection to the inviter's HS (with fallback to
|
||||
# local event)
|
||||
return await self.remote_reject_invite(
|
||||
invite.event_id, txn_id, requester, content,
|
||||
)
|
||||
|
||||
# the inviter was on our server, but has now left. Carry on
|
||||
# with the normal rejection codepath, which will also send the
|
||||
# rejection out to any other servers we believe are still in the room.
|
||||
|
||||
# thanks to overzealous cleaning up of event_forward_extremities in
|
||||
# `delete_old_current_state_events`, it's possible to end up with no
|
||||
# forward extremities here. If that happens, let's just hang the
|
||||
# rejection off the invite event.
|
||||
#
|
||||
# see: https://github.com/matrix-org/synapse/issues/7139
|
||||
if len(latest_event_ids) == 0:
|
||||
latest_event_ids = [invite.event_id]
|
||||
|
||||
else:
|
||||
# or perhaps this is a remote room that a local user has knocked on
|
||||
knock = await self.store.get_knock_for_local_user_in_room(
|
||||
user_id=target.to_string(), room_id=room_id
|
||||
) # type: Optional[RoomsForUser]
|
||||
if knock:
|
||||
# TODO: We don't yet support rescinding knocks over federation
|
||||
# as we don't know which homeserver to send it to. An obvious
|
||||
# candidate is the remote homeserver we originally knocked through,
|
||||
# however we don't currently store that information.
|
||||
|
||||
# Just rescind the knock locally
|
||||
knock_event = await self.store.get_event(knock.event_id)
|
||||
return await self.generate_local_out_of_band_membership(
|
||||
knock_event, txn_id, requester, content
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"%s sent a leave request to %s, but that is not an active room "
|
||||
"on this server, and there is no pending invite",
|
||||
"on this server, and there is no pending knock",
|
||||
target,
|
||||
room_id,
|
||||
)
|
||||
|
||||
raise SynapseError(404, "Not a known room")
|
||||
|
||||
logger.info(
|
||||
"%s rejects invite to %s from %s", target, room_id, invite.sender
|
||||
elif effective_membership_state == Membership.KNOCK:
|
||||
if not is_host_in_room:
|
||||
# The knock needs to be sent over federation instead
|
||||
remote_room_hosts.append(room_id.split(":", 1)[1])
|
||||
|
||||
content["membership"] = Membership.KNOCK
|
||||
|
||||
profile = self.profile_handler
|
||||
if "displayname" not in content:
|
||||
content["displayname"] = await profile.get_displayname(target)
|
||||
if "avatar_url" not in content:
|
||||
content["avatar_url"] = await profile.get_avatar_url(target)
|
||||
|
||||
return await self._remote_knock(
|
||||
remote_room_hosts, room_id, target, content
|
||||
)
|
||||
|
||||
if not self.hs.is_mine_id(invite.sender):
|
||||
# send the rejection to the inviter's HS (with fallback to
|
||||
# local event)
|
||||
return await self.remote_reject_invite(
|
||||
invite.event_id, txn_id, requester, content,
|
||||
)
|
||||
|
||||
# the inviter was on our server, but has now left. Carry on
|
||||
# with the normal rejection codepath, which will also send the
|
||||
# rejection out to any other servers we believe are still in the room.
|
||||
|
||||
# thanks to overzealous cleaning up of event_forward_extremities in
|
||||
# `delete_old_current_state_events`, it's possible to end up with no
|
||||
# forward extremities here. If that happens, let's just hang the
|
||||
# rejection off the invite event.
|
||||
#
|
||||
# see: https://github.com/matrix-org/synapse/issues/7139
|
||||
if len(latest_event_ids) == 0:
|
||||
latest_event_ids = [invite.event_id]
|
||||
|
||||
return await self._local_membership_update(
|
||||
requester=requester,
|
||||
target=target,
|
||||
@@ -1104,32 +1178,34 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
#
|
||||
logger.warning("Failed to reject invite: %s", e)
|
||||
|
||||
return await self._locally_reject_invite(
|
||||
return await self.generate_local_out_of_band_membership(
|
||||
invite_event, txn_id, requester, content
|
||||
)
|
||||
|
||||
async def _locally_reject_invite(
|
||||
async def generate_local_out_of_band_membership(
|
||||
self,
|
||||
invite_event: EventBase,
|
||||
previous_membership_event: EventBase,
|
||||
txn_id: Optional[str],
|
||||
requester: Requester,
|
||||
content: JsonDict,
|
||||
) -> Tuple[str, int]:
|
||||
"""Generate a local invite rejection
|
||||
):
|
||||
"""Generate a local leave event for a room
|
||||
|
||||
This is called after we fail to reject an invite via a remote server. It
|
||||
generates an out-of-band membership event locally.
|
||||
This can be called after we e.g fail to reject an invite via a remote server.
|
||||
It generates an out-of-band membership event locally.
|
||||
|
||||
Args:
|
||||
invite_event: the invite to be rejected
|
||||
previous_membership_event: the previous membership event for this user
|
||||
txn_id: optional transaction ID supplied by the client
|
||||
requester: user making the rejection request, according to the access token
|
||||
content: additional content to include in the rejection event.
|
||||
requester: user making the request, according to the access token
|
||||
content: additional content to include in the leave event.
|
||||
Normally an empty dict.
|
||||
"""
|
||||
|
||||
room_id = invite_event.room_id
|
||||
target_user = invite_event.state_key
|
||||
Returns:
|
||||
A tuple containing (event_id, stream_id of the leave event)
|
||||
"""
|
||||
room_id = previous_membership_event.room_id
|
||||
target_user = previous_membership_event.state_key
|
||||
|
||||
content["membership"] = Membership.LEAVE
|
||||
|
||||
@@ -1141,12 +1217,12 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
"state_key": target_user,
|
||||
}
|
||||
|
||||
# the auth events for the new event are the same as that of the invite, plus
|
||||
# the invite itself.
|
||||
# the auth events for the new event are the same as that of the previous event, plus
|
||||
# the event itself.
|
||||
#
|
||||
# the prev_events are just the invite.
|
||||
prev_event_ids = [invite_event.event_id]
|
||||
auth_event_ids = invite_event.auth_event_ids() + prev_event_ids
|
||||
# the prev_events consist solely of the previous membership event.
|
||||
prev_event_ids = [previous_membership_event.event_id]
|
||||
auth_event_ids = previous_membership_event.auth_event_ids() + prev_event_ids
|
||||
|
||||
event, context = await self.event_creation_handler.create_event(
|
||||
requester,
|
||||
@@ -1166,6 +1242,32 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
|
||||
return result_event.event_id, result_event.internal_metadata.stream_ordering
|
||||
|
||||
async def _remote_knock(
|
||||
self, remote_room_hosts: List[str], room_id: str, user: UserID, content: dict,
|
||||
) -> Tuple[str, int]:
|
||||
"""Sends a knock to a room. Attempts to do so via one remote out of a given list.
|
||||
|
||||
Args:
|
||||
remote_room_hosts: A list of homeservers to try knocking through.
|
||||
room_id: The ID of the room to knock on.
|
||||
user: The user to knock on behalf of.
|
||||
content: The content of the knock event.
|
||||
|
||||
Returns:
|
||||
A tuple of (event ID, stream ID).
|
||||
"""
|
||||
# filter ourselves out of remote_room_hosts
|
||||
remote_room_hosts = [
|
||||
host for host in remote_room_hosts if host != self.hs.hostname
|
||||
]
|
||||
|
||||
if len(remote_room_hosts) == 0:
|
||||
raise SynapseError(404, "No known servers")
|
||||
|
||||
return await self.federation_handler.do_knock(
|
||||
remote_room_hosts, room_id, user.to_string(), content=content
|
||||
)
|
||||
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_left_room
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2020 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.
|
||||
@@ -17,13 +18,14 @@ import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.events import EventBase
|
||||
from synapse.handlers.room_member import RoomMemberHandler
|
||||
from synapse.replication.http.membership import (
|
||||
ReplicationRemoteJoinRestServlet as ReplRemoteJoin,
|
||||
ReplicationRemoteRejectInviteRestServlet as ReplRejectInvite,
|
||||
ReplicationUserJoinedLeftRoomRestServlet as ReplJoinedLeft,
|
||||
)
|
||||
from synapse.types import Requester, UserID
|
||||
from synapse.types import JsonDict, Requester, UserID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -79,6 +81,47 @@ class RoomMemberWorkerHandler(RoomMemberHandler):
|
||||
)
|
||||
return ret["event_id"], ret["stream_id"]
|
||||
|
||||
async def generate_local_out_of_band_membership(
|
||||
self,
|
||||
previous_membership_event: EventBase,
|
||||
txn_id: Optional[str],
|
||||
requester: Requester,
|
||||
content: JsonDict,
|
||||
):
|
||||
"""Generate a local leave event for a room
|
||||
|
||||
Args:
|
||||
previous_membership_event: the previous membership event for this user
|
||||
txn_id: optional transaction ID supplied by the client
|
||||
requester: user making the request, according to the access token
|
||||
content: additional content to include in the leave event.
|
||||
Normally an empty dict.
|
||||
|
||||
Returns:
|
||||
A tuple containing (event_id, stream_id of the leave event)
|
||||
"""
|
||||
ret = await self.generate_local_out_of_band_membership(
|
||||
previous_membership_event=previous_membership_event,
|
||||
txn_id=txn_id,
|
||||
requester=requester,
|
||||
content=content,
|
||||
)
|
||||
return ret["event_id"], ret["stream_id"]
|
||||
|
||||
async def _remote_knock(
|
||||
self, remote_room_hosts: List[str], room_id: str, user: UserID, content: dict,
|
||||
) -> Tuple[str, int]:
|
||||
"""Sends a knock to a room.
|
||||
|
||||
Implements RoomMemberHandler._remote_knock
|
||||
"""
|
||||
return await self._remote_knock(
|
||||
remote_room_hosts=remote_room_hosts,
|
||||
room_id=room_id,
|
||||
user=user,
|
||||
content=content,
|
||||
)
|
||||
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_left_room
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# 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.
|
||||
@@ -225,6 +226,8 @@ class StatsHandler:
|
||||
room_stats_delta["left_members"] -= 1
|
||||
elif prev_membership == Membership.BAN:
|
||||
room_stats_delta["banned_members"] -= 1
|
||||
elif prev_membership == Membership.KNOCK:
|
||||
room_stats_delta["knocked_members"] -= 1
|
||||
else:
|
||||
raise ValueError(
|
||||
"%r is not a valid prev_membership" % (prev_membership,)
|
||||
@@ -246,6 +249,8 @@ class StatsHandler:
|
||||
room_stats_delta["left_members"] += 1
|
||||
elif membership == Membership.BAN:
|
||||
room_stats_delta["banned_members"] += 1
|
||||
elif membership == Membership.KNOCK:
|
||||
room_stats_delta["knocked_members"] += 1
|
||||
else:
|
||||
raise ValueError("%r is not a valid membership" % (membership,))
|
||||
|
||||
|
||||
@@ -149,6 +149,16 @@ class InvitedSyncResult:
|
||||
return True
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class KnockedSyncResult:
|
||||
room_id = attr.ib(type=str)
|
||||
knock = attr.ib(type=EventBase)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Knocked rooms should always be reported to the client"""
|
||||
return True
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class GroupsSyncResult:
|
||||
join = attr.ib(type=JsonDict)
|
||||
@@ -182,6 +192,7 @@ class _RoomChanges:
|
||||
|
||||
room_entries = attr.ib(type=List["RoomSyncResultBuilder"])
|
||||
invited = attr.ib(type=List[InvitedSyncResult])
|
||||
knocked = attr.ib(type=List[KnockedSyncResult])
|
||||
newly_joined_rooms = attr.ib(type=List[str])
|
||||
newly_left_rooms = attr.ib(type=List[str])
|
||||
|
||||
@@ -195,6 +206,7 @@ class SyncResult:
|
||||
account_data: List of account_data events for the user.
|
||||
joined: JoinedSyncResult for each joined room.
|
||||
invited: InvitedSyncResult for each invited room.
|
||||
knocked: KnockedSyncResult for each knocked on room.
|
||||
archived: ArchivedSyncResult for each archived room.
|
||||
to_device: List of direct messages for the device.
|
||||
device_lists: List of user_ids whose devices have changed
|
||||
@@ -210,6 +222,7 @@ class SyncResult:
|
||||
account_data = attr.ib(type=List[JsonDict])
|
||||
joined = attr.ib(type=List[JoinedSyncResult])
|
||||
invited = attr.ib(type=List[InvitedSyncResult])
|
||||
knocked = attr.ib(type=List[KnockedSyncResult])
|
||||
archived = attr.ib(type=List[ArchivedSyncResult])
|
||||
to_device = attr.ib(type=List[JsonDict])
|
||||
device_lists = attr.ib(type=DeviceLists)
|
||||
@@ -226,6 +239,7 @@ class SyncResult:
|
||||
self.presence
|
||||
or self.joined
|
||||
or self.invited
|
||||
or self.knocked
|
||||
or self.archived
|
||||
or self.account_data
|
||||
or self.to_device
|
||||
@@ -997,7 +1011,7 @@ class SyncHandler:
|
||||
res = await self._generate_sync_entry_for_rooms(
|
||||
sync_result_builder, account_data_by_room
|
||||
)
|
||||
newly_joined_rooms, newly_joined_or_invited_users, _, _ = res
|
||||
newly_joined_rooms, newly_joined_or_invited_or_knocking_users, _, _ = res
|
||||
_, _, newly_left_rooms, newly_left_users = res
|
||||
|
||||
block_all_presence_data = (
|
||||
@@ -1006,7 +1020,9 @@ class SyncHandler:
|
||||
if self.hs_config.use_presence and not block_all_presence_data:
|
||||
logger.debug("Fetching presence data")
|
||||
await self._generate_sync_entry_for_presence(
|
||||
sync_result_builder, newly_joined_rooms, newly_joined_or_invited_users
|
||||
sync_result_builder,
|
||||
newly_joined_rooms,
|
||||
newly_joined_or_invited_or_knocking_users,
|
||||
)
|
||||
|
||||
logger.debug("Fetching to-device data")
|
||||
@@ -1015,7 +1031,7 @@ class SyncHandler:
|
||||
device_lists = await self._generate_sync_entry_for_device_list(
|
||||
sync_result_builder,
|
||||
newly_joined_rooms=newly_joined_rooms,
|
||||
newly_joined_or_invited_users=newly_joined_or_invited_users,
|
||||
newly_joined_or_invited_or_knocking_users=newly_joined_or_invited_or_knocking_users,
|
||||
newly_left_rooms=newly_left_rooms,
|
||||
newly_left_users=newly_left_users,
|
||||
)
|
||||
@@ -1049,6 +1065,7 @@ class SyncHandler:
|
||||
account_data=sync_result_builder.account_data,
|
||||
joined=sync_result_builder.joined,
|
||||
invited=sync_result_builder.invited,
|
||||
knocked=sync_result_builder.knocked,
|
||||
archived=sync_result_builder.archived,
|
||||
to_device=sync_result_builder.to_device,
|
||||
device_lists=device_lists,
|
||||
@@ -1108,7 +1125,7 @@ class SyncHandler:
|
||||
self,
|
||||
sync_result_builder: "SyncResultBuilder",
|
||||
newly_joined_rooms: Set[str],
|
||||
newly_joined_or_invited_users: Set[str],
|
||||
newly_joined_or_invited_or_knocking_users: Set[str],
|
||||
newly_left_rooms: Set[str],
|
||||
newly_left_users: Set[str],
|
||||
) -> DeviceLists:
|
||||
@@ -1117,8 +1134,9 @@ class SyncHandler:
|
||||
Args:
|
||||
sync_result_builder
|
||||
newly_joined_rooms: Set of rooms user has joined since previous sync
|
||||
newly_joined_or_invited_users: Set of users that have joined or
|
||||
been invited to a room since previous sync.
|
||||
newly_joined_or_invited_or_knocking_users: Set of users that have joined,
|
||||
been invited to a room or are knocking on a room since
|
||||
previous sync.
|
||||
newly_left_rooms: Set of rooms user has left since previous sync
|
||||
newly_left_users: Set of users that have left a room we're in since
|
||||
previous sync
|
||||
@@ -1129,7 +1147,9 @@ class SyncHandler:
|
||||
|
||||
# We're going to mutate these fields, so lets copy them rather than
|
||||
# assume they won't get used later.
|
||||
newly_joined_or_invited_users = set(newly_joined_or_invited_users)
|
||||
newly_joined_or_invited_or_knocking_users = set(
|
||||
newly_joined_or_invited_or_knocking_users
|
||||
)
|
||||
newly_left_users = set(newly_left_users)
|
||||
|
||||
if since_token and since_token.device_list_key:
|
||||
@@ -1168,11 +1188,11 @@ class SyncHandler:
|
||||
# Step 1b, check for newly joined rooms
|
||||
for room_id in newly_joined_rooms:
|
||||
joined_users = await self.state.get_current_users_in_room(room_id)
|
||||
newly_joined_or_invited_users.update(joined_users)
|
||||
newly_joined_or_invited_or_knocking_users.update(joined_users)
|
||||
|
||||
# TODO: Check that these users are actually new, i.e. either they
|
||||
# weren't in the previous sync *or* they left and rejoined.
|
||||
users_that_have_changed.update(newly_joined_or_invited_users)
|
||||
users_that_have_changed.update(newly_joined_or_invited_or_knocking_users)
|
||||
|
||||
user_signatures_changed = await self.store.get_users_whose_signatures_changed(
|
||||
user_id, since_token.device_list_key
|
||||
@@ -1417,6 +1437,7 @@ class SyncHandler:
|
||||
|
||||
room_entries = room_changes.room_entries
|
||||
invited = room_changes.invited
|
||||
knocked = room_changes.knocked
|
||||
newly_joined_rooms = room_changes.newly_joined_rooms
|
||||
newly_left_rooms = room_changes.newly_left_rooms
|
||||
|
||||
@@ -1437,9 +1458,10 @@ class SyncHandler:
|
||||
await concurrently_execute(handle_room_entries, room_entries, 10)
|
||||
|
||||
sync_result_builder.invited.extend(invited)
|
||||
sync_result_builder.knocked.extend(knocked)
|
||||
|
||||
# Now we want to get any newly joined or invited users
|
||||
newly_joined_or_invited_users = set()
|
||||
# Now we want to get any newly joined, invited or knocking users
|
||||
newly_joined_or_invited_or_knocking_users = set()
|
||||
newly_left_users = set()
|
||||
if since_token:
|
||||
for joined_sync in sync_result_builder.joined:
|
||||
@@ -1451,19 +1473,22 @@ class SyncHandler:
|
||||
if (
|
||||
event.membership == Membership.JOIN
|
||||
or event.membership == Membership.INVITE
|
||||
or event.membership == Membership.KNOCK
|
||||
):
|
||||
newly_joined_or_invited_users.add(event.state_key)
|
||||
newly_joined_or_invited_or_knocking_users.add(
|
||||
event.state_key
|
||||
)
|
||||
else:
|
||||
prev_content = event.unsigned.get("prev_content", {})
|
||||
prev_membership = prev_content.get("membership", None)
|
||||
if prev_membership == Membership.JOIN:
|
||||
newly_left_users.add(event.state_key)
|
||||
|
||||
newly_left_users -= newly_joined_or_invited_users
|
||||
newly_left_users -= newly_joined_or_invited_or_knocking_users
|
||||
|
||||
return (
|
||||
set(newly_joined_rooms),
|
||||
newly_joined_or_invited_users,
|
||||
newly_joined_or_invited_or_knocking_users,
|
||||
set(newly_left_rooms),
|
||||
newly_left_users,
|
||||
)
|
||||
@@ -1519,6 +1544,7 @@ class SyncHandler:
|
||||
newly_left_rooms = []
|
||||
room_entries = []
|
||||
invited = []
|
||||
knocked = []
|
||||
for room_id, events in mem_change_events_by_room_id.items():
|
||||
logger.debug(
|
||||
"Membership changes in %s: [%s]",
|
||||
@@ -1598,9 +1624,17 @@ class SyncHandler:
|
||||
should_invite = non_joins[-1].membership == Membership.INVITE
|
||||
if should_invite:
|
||||
if event.sender not in ignored_users:
|
||||
room_sync = InvitedSyncResult(room_id, invite=non_joins[-1])
|
||||
if room_sync:
|
||||
invited.append(room_sync)
|
||||
invite_room_sync = InvitedSyncResult(room_id, invite=non_joins[-1])
|
||||
if invite_room_sync:
|
||||
invited.append(invite_room_sync)
|
||||
|
||||
# Only bother if our latest membership in the room is knock (and we haven't
|
||||
# been accepted/rejected in the meantime).
|
||||
should_knock = non_joins[-1].membership == Membership.KNOCK
|
||||
if should_knock:
|
||||
knock_room_sync = KnockedSyncResult(room_id, knock=non_joins[-1])
|
||||
if knock_room_sync:
|
||||
knocked.append(knock_room_sync)
|
||||
|
||||
# Always include leave/ban events. Just take the last one.
|
||||
# TODO: How do we handle ban -> leave in same batch?
|
||||
@@ -1704,7 +1738,9 @@ class SyncHandler:
|
||||
)
|
||||
room_entries.append(entry)
|
||||
|
||||
return _RoomChanges(room_entries, invited, newly_joined_rooms, newly_left_rooms)
|
||||
return _RoomChanges(
|
||||
room_entries, invited, knocked, newly_joined_rooms, newly_left_rooms,
|
||||
)
|
||||
|
||||
async def _get_all_rooms(
|
||||
self, sync_result_builder: "SyncResultBuilder", ignored_users: FrozenSet[str]
|
||||
@@ -1724,6 +1760,7 @@ class SyncHandler:
|
||||
|
||||
membership_list = (
|
||||
Membership.INVITE,
|
||||
Membership.KNOCK,
|
||||
Membership.JOIN,
|
||||
Membership.LEAVE,
|
||||
Membership.BAN,
|
||||
@@ -1735,6 +1772,7 @@ class SyncHandler:
|
||||
|
||||
room_entries = []
|
||||
invited = []
|
||||
knocked = []
|
||||
|
||||
for event in room_list:
|
||||
if event.membership == Membership.JOIN:
|
||||
@@ -1754,8 +1792,11 @@ class SyncHandler:
|
||||
continue
|
||||
invite = await self.store.get_event(event.event_id)
|
||||
invited.append(InvitedSyncResult(room_id=event.room_id, invite=invite))
|
||||
elif event.membership == Membership.KNOCK:
|
||||
knock = await self.store.get_event(event.event_id)
|
||||
knocked.append(KnockedSyncResult(room_id=event.room_id, knock=knock))
|
||||
elif event.membership in (Membership.LEAVE, Membership.BAN):
|
||||
# Always send down rooms we were banned or kicked from.
|
||||
# Always send down rooms we were banned from or kicked from.
|
||||
if not sync_config.filter_collection.include_leave:
|
||||
if event.membership == Membership.LEAVE:
|
||||
if user_id == event.sender:
|
||||
@@ -1776,7 +1817,7 @@ class SyncHandler:
|
||||
)
|
||||
)
|
||||
|
||||
return _RoomChanges(room_entries, invited, [], [])
|
||||
return _RoomChanges(room_entries, invited, knocked, [], [])
|
||||
|
||||
async def _generate_room_entry(
|
||||
self,
|
||||
@@ -2065,6 +2106,7 @@ class SyncResultBuilder:
|
||||
account_data (list)
|
||||
joined (list[JoinedSyncResult])
|
||||
invited (list[InvitedSyncResult])
|
||||
knocked (list[KnockedSyncResult])
|
||||
archived (list[ArchivedSyncResult])
|
||||
groups (GroupsSyncResult|None)
|
||||
to_device (list)
|
||||
@@ -2080,6 +2122,7 @@ class SyncResultBuilder:
|
||||
account_data = attr.ib(type=List[JsonDict], default=attr.Factory(list))
|
||||
joined = attr.ib(type=List[JoinedSyncResult], default=attr.Factory(list))
|
||||
invited = attr.ib(type=List[InvitedSyncResult], default=attr.Factory(list))
|
||||
knocked = attr.ib(type=List[KnockedSyncResult], default=attr.Factory(list))
|
||||
archived = attr.ib(type=List[ArchivedSyncResult], default=attr.Factory(list))
|
||||
groups = attr.ib(type=Optional[GroupsSyncResult], default=None)
|
||||
to_device = attr.ib(type=List[JsonDict], default=attr.Factory(list))
|
||||
|
||||
@@ -254,20 +254,20 @@ class ReplicationCleanRoomRestServlet(ReplicationEndpoint):
|
||||
return 200, {}
|
||||
|
||||
|
||||
class ReplicationStoreRoomOnInviteRestServlet(ReplicationEndpoint):
|
||||
class ReplicationStoreRoomOnOutlierMembershipRestServlet(ReplicationEndpoint):
|
||||
"""Called to clean up any data in DB for a given room, ready for the
|
||||
server to join the room.
|
||||
|
||||
Request format:
|
||||
|
||||
POST /_synapse/replication/store_room_on_invite/:room_id/:txn_id
|
||||
POST /_synapse/replication/store_room_on_outlier_membership/:room_id/:txn_id
|
||||
|
||||
{
|
||||
"room_version": "1",
|
||||
}
|
||||
"""
|
||||
|
||||
NAME = "store_room_on_invite"
|
||||
NAME = "store_room_on_outlier_membership"
|
||||
PATH_ARGS = ("room_id",)
|
||||
|
||||
def __init__(self, hs):
|
||||
@@ -282,7 +282,7 @@ class ReplicationStoreRoomOnInviteRestServlet(ReplicationEndpoint):
|
||||
async def _handle_request(self, request, room_id):
|
||||
content = parse_json_object_from_request(request)
|
||||
room_version = KNOWN_ROOM_VERSIONS[content["room_version"]]
|
||||
await self.store.maybe_store_room_on_invite(room_id, room_version)
|
||||
await self.store.maybe_store_room_on_outlier_membership(room_id, room_version)
|
||||
return 200, {}
|
||||
|
||||
|
||||
@@ -291,4 +291,4 @@ def register_servlets(hs, http_server):
|
||||
ReplicationFederationSendEduRestServlet(hs).register(http_server)
|
||||
ReplicationGetQueryRestServlet(hs).register(http_server)
|
||||
ReplicationCleanRoomRestServlet(hs).register(http_server)
|
||||
ReplicationStoreRoomOnInviteRestServlet(hs).register(http_server)
|
||||
ReplicationStoreRoomOnOutlierMembershipRestServlet(hs).register(http_server)
|
||||
|
||||
@@ -39,6 +39,7 @@ from synapse.rest.client.v2_alpha import (
|
||||
filter,
|
||||
groups,
|
||||
keys,
|
||||
knock,
|
||||
notifications,
|
||||
openid,
|
||||
password_policy,
|
||||
@@ -121,6 +122,7 @@ class ClientRestResource(JsonResource):
|
||||
account_validity.register_servlets(hs, client_resource)
|
||||
relations.register_servlets(hs, client_resource)
|
||||
password_policy.register_servlets(hs, client_resource)
|
||||
knock.register_servlets(hs, client_resource)
|
||||
|
||||
# moving to /_synapse/admin
|
||||
admin.register_servlets_for_client_rest_resource(hs, client_resource)
|
||||
|
||||
103
synapse/rest/client/v2_alpha/knock.py
Normal file
103
synapse/rest/client/v2_alpha/knock.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 Sorunome
|
||||
# Copyright 2020 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 TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
from twisted.web.server import Request
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
from synapse.logging.opentracing import set_tag
|
||||
from synapse.rest.client.transactions import HttpTransactionCache
|
||||
from synapse.types import JsonDict, RoomAlias, RoomID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.app.homeserver import HomeServer
|
||||
|
||||
from ._base import client_patterns
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TransactionRestServlet(RestServlet):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super(TransactionRestServlet, self).__init__()
|
||||
self.txns = HttpTransactionCache(hs)
|
||||
|
||||
|
||||
class KnockRoomAliasServlet(TransactionRestServlet):
|
||||
"""
|
||||
POST /knock/{roomIdOrAlias}
|
||||
"""
|
||||
|
||||
PATTERNS = client_patterns("/knock/(?P<room_identifier>[^/]*)")
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_POST(
|
||||
self, request: Request, room_identifier: str, txn_id: Optional[str] = None,
|
||||
) -> Tuple[int, JsonDict]:
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
|
||||
content = parse_json_object_from_request(request)
|
||||
event_content = None
|
||||
if "reason" in content:
|
||||
event_content = {"reason": content["reason"]}
|
||||
|
||||
if RoomID.is_valid(room_identifier):
|
||||
room_id = room_identifier
|
||||
try:
|
||||
remote_room_hosts = [
|
||||
x.decode("ascii") for x in request.args[b"server_name"]
|
||||
] # type: Optional[List[str]]
|
||||
except Exception:
|
||||
remote_room_hosts = None
|
||||
elif RoomAlias.is_valid(room_identifier):
|
||||
handler = self.room_member_handler
|
||||
room_alias = RoomAlias.from_string(room_identifier)
|
||||
room_id_obj, remote_room_hosts = await handler.lookup_room_alias(room_alias)
|
||||
room_id = room_id_obj.to_string()
|
||||
else:
|
||||
raise SynapseError(
|
||||
400, "%s was not legal room ID or room alias" % (room_identifier,)
|
||||
)
|
||||
|
||||
await self.room_member_handler.update_membership(
|
||||
requester=requester,
|
||||
target=requester.user,
|
||||
room_id=room_id,
|
||||
action="knock",
|
||||
txn_id=txn_id,
|
||||
third_party_signed=None,
|
||||
remote_room_hosts=remote_room_hosts,
|
||||
content=event_content,
|
||||
)
|
||||
|
||||
return 200, {"room_id": room_id}
|
||||
|
||||
def on_PUT(self, request: Request, room_identifier: str, txn_id: str):
|
||||
set_tag("txn_id", txn_id)
|
||||
|
||||
return self.txns.fetch_or_execute_request(
|
||||
request, self.on_POST, request, room_identifier, txn_id
|
||||
)
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
KnockRoomAliasServlet(hs).register(http_server)
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
from synapse.api.constants import PresenceState
|
||||
from synapse.api.errors import Codes, StoreError, SynapseError
|
||||
@@ -24,7 +25,7 @@ from synapse.events.utils import (
|
||||
format_event_raw,
|
||||
)
|
||||
from synapse.handlers.presence import format_user_presence_state
|
||||
from synapse.handlers.sync import SyncConfig
|
||||
from synapse.handlers.sync import KnockedSyncResult, SyncConfig
|
||||
from synapse.http.servlet import RestServlet, parse_boolean, parse_integer, parse_string
|
||||
from synapse.types import StreamToken
|
||||
from synapse.util import json_decoder
|
||||
@@ -212,6 +213,10 @@ class SyncRestServlet(RestServlet):
|
||||
sync_result.invited, time_now, access_token_id, event_formatter
|
||||
)
|
||||
|
||||
knocked = await self.encode_knocked(
|
||||
sync_result.knocked, time_now, access_token_id, event_formatter
|
||||
)
|
||||
|
||||
archived = await self.encode_archived(
|
||||
sync_result.archived,
|
||||
time_now,
|
||||
@@ -229,7 +234,12 @@ class SyncRestServlet(RestServlet):
|
||||
"left": list(sync_result.device_lists.left),
|
||||
},
|
||||
"presence": SyncRestServlet.encode_presence(sync_result.presence, time_now),
|
||||
"rooms": {"join": joined, "invite": invited, "leave": archived},
|
||||
"rooms": {
|
||||
"join": joined,
|
||||
"invite": invited,
|
||||
"knock": knocked,
|
||||
"leave": archived,
|
||||
},
|
||||
"groups": {
|
||||
"join": sync_result.groups.join,
|
||||
"invite": sync_result.groups.invite,
|
||||
@@ -295,7 +305,7 @@ class SyncRestServlet(RestServlet):
|
||||
|
||||
Args:
|
||||
rooms(list[synapse.handlers.sync.InvitedSyncResult]): list of
|
||||
sync results for rooms this user is joined to
|
||||
sync results for rooms this user is invited to
|
||||
time_now(int): current time - used as a baseline for age
|
||||
calculations
|
||||
token_id(int): ID of the user's auth token - used for namespacing
|
||||
@@ -324,6 +334,52 @@ class SyncRestServlet(RestServlet):
|
||||
|
||||
return invited
|
||||
|
||||
async def encode_knocked(
|
||||
self,
|
||||
rooms: List[KnockedSyncResult],
|
||||
time_now: int,
|
||||
token_id: int,
|
||||
event_formatter: Callable[[Dict], Dict],
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Encode the rooms we've knocked on in a sync result.
|
||||
|
||||
Args:
|
||||
rooms: list of sync results for rooms this user is knocking on
|
||||
time_now: current time - used as a baseline for age calculations
|
||||
token_id: ID of the user's auth token - used for namespacing of transaction IDs
|
||||
event_formatter: function to convert from federation format to client format
|
||||
|
||||
Returns:
|
||||
The list of rooms the user has knocked on, in our response format.
|
||||
"""
|
||||
knocked = {}
|
||||
for room in rooms:
|
||||
knock = await self._event_serializer.serialize_event(
|
||||
room.knock, time_now, token_id=token_id, event_format=event_formatter,
|
||||
)
|
||||
|
||||
# Extract the `unsigned` key from the knock event.
|
||||
# This is where we (cheekily) store the knock state events
|
||||
unsigned = knock.setdefault("unsigned", {})
|
||||
|
||||
# Extract the stripped room state from the unsigned dict
|
||||
# This is for clients to get a little bit of information about
|
||||
# the room they've knocked on, without revealing any sensitive information
|
||||
knocked_state = list(unsigned.pop("knock_room_state", []))
|
||||
|
||||
# Append the actual knock membership event itself as well
|
||||
# TODO: I *believe* this is just for the client's sake of track its membership
|
||||
# state in each room, but I could be wrong. This certainly doesn't seem like it
|
||||
# could have any negative effects besides resource usage
|
||||
knocked_state.append(knock)
|
||||
|
||||
# Build the `knock_state` dictionary, which will contain the state of the
|
||||
# room that the client has knocked on
|
||||
knocked[room.room_id] = {"knock_state": {"events": knocked_state}}
|
||||
|
||||
return knocked
|
||||
|
||||
async def encode_archived(
|
||||
self, rooms, time_now, token_id, event_fields, event_formatter
|
||||
):
|
||||
|
||||
@@ -81,6 +81,8 @@ class VersionsRestServlet(RestServlet):
|
||||
"io.element.e2ee_forced.public": self.e2ee_forced_public,
|
||||
"io.element.e2ee_forced.private": self.e2ee_forced_private,
|
||||
"io.element.e2ee_forced.trusted_private": self.e2ee_forced_trusted_private,
|
||||
# Implements additional endpoints and features as described in MSC2403
|
||||
"xyz.amorgan.knock": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1240,13 +1240,16 @@ class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore):
|
||||
logger.error("store_room with room_id=%s failed: %s", room_id, e)
|
||||
raise StoreError(500, "Problem creating room.")
|
||||
|
||||
async def maybe_store_room_on_invite(self, room_id: str, room_version: RoomVersion):
|
||||
async def maybe_store_room_on_outlier_membership(
|
||||
self, room_id: str, room_version: RoomVersion
|
||||
):
|
||||
"""
|
||||
When we receive an invite over federation, store the version of the room if we
|
||||
don't already know the room version.
|
||||
When we receive an invite, a knock or any other event over federation that may relate
|
||||
to a room we are not in, store the version of the room if we don't already know the
|
||||
room version.
|
||||
"""
|
||||
await self.db_pool.simple_upsert(
|
||||
desc="maybe_store_room_on_invite",
|
||||
desc="maybe_store_room_on_outlier_membership",
|
||||
table="rooms",
|
||||
keyvalues={"room_id": room_id},
|
||||
values={},
|
||||
|
||||
@@ -272,6 +272,21 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||
user_id, [Membership.INVITE]
|
||||
)
|
||||
|
||||
@cached()
|
||||
async def get_knocked_rooms_for_local_user(self, user_id: str) -> RoomsForUser:
|
||||
"""Get all the rooms the *local* user currently has the membership of knock for.
|
||||
|
||||
Args:
|
||||
user_id: The user ID.
|
||||
|
||||
Returns:
|
||||
A list of RoomsForUser.
|
||||
"""
|
||||
|
||||
return await self.get_rooms_for_local_user_where_membership_is(
|
||||
user_id, [Membership.KNOCK]
|
||||
)
|
||||
|
||||
async def get_invite_for_local_user_in_room(
|
||||
self, user_id: str, room_id: str
|
||||
) -> Optional[RoomsForUser]:
|
||||
@@ -290,6 +305,24 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||
return invite
|
||||
return None
|
||||
|
||||
async def get_knock_for_local_user_in_room(
|
||||
self, user_id: str, room_id: str
|
||||
) -> Optional[RoomsForUser]:
|
||||
"""Gets the knock for the given *local* user and room.
|
||||
|
||||
Args:
|
||||
user_id: The user ID to find the knock of.
|
||||
room_id: The room to user knocked on.
|
||||
|
||||
Returns:
|
||||
Either a RoomsForUser or None if no knock was found.
|
||||
"""
|
||||
knocks = await self.get_knocked_rooms_for_local_user(user_id)
|
||||
for knock in knocks:
|
||||
if knock.room_id == room_id:
|
||||
return knock
|
||||
return None
|
||||
|
||||
async def get_rooms_for_local_user_where_membership_is(
|
||||
self, user_id: str, membership_list: Collection[str]
|
||||
) -> List[RoomsForUser]:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
ALTER TABLE room_stats_current ADD knock_members INT NOT NULL DEFAULT '0';
|
||||
ALTER TABLE room_stats_historical ADD knock_members BIGINT NOT NULL DEFAULT '0';
|
||||
@@ -41,6 +41,7 @@ ABSOLUTE_STATS_FIELDS = {
|
||||
"current_state_events",
|
||||
"joined_members",
|
||||
"invited_members",
|
||||
"knocked_members",
|
||||
"left_members",
|
||||
"banned_members",
|
||||
"local_users_in_room",
|
||||
|
||||
Reference in New Issue
Block a user