mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 11:17:51 +00:00
Remember the server a local user knocked through (MSC4233)
When a local user knocks on a room this server is not in, the knock goes through a server the client named in `?server_name`, and we then forget it. Room IDs are not routable and `/leave` takes no `via`, so nothing later can reach the room again. MSC4233 asks the knocking server to remember that server. `send_knock` now returns the server that answered alongside the response, and `do_knock` records it in the new `local_knock_via_servers` table with the knock's event ID. There is one row per (user, room); a new knock on the same room overwrites it. Lookups key on the knock event ID too, so a row left by an earlier knock never vouches for a later one. The row is written after the knock is persisted, outside its transaction. A crash between the two leaves a knock with no remembered route, which lands on the fallback MSC4233 already specifies: generate the leave locally. Rows go with the room on purge. The `msc4233_enabled` flag is added here and read by the next two commits; the tracking issue number is still a placeholder. Co-authored-by: Quentin Gliech <quenting@element.io> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca28da9663
commit
a356f964ac
8 files changed
+124
-10
No files matched your search
@@ -0,0 +1 @@
|
||||
Add experimental support for [MSC4233](https://github.com/matrix-org/matrix-spec-proposals/pull/4233), so that a user who knocks on a remote room can withdraw the knock over federation, and is told when the room rejects it.
|
||||
@@ -263,6 +263,11 @@ class ExperimentalConfig(Config):
|
||||
# TODO: Remove the flag and the legacy rules after 2026-12-01.
|
||||
self.msc4210_enabled: bool = experimental.get("msc4210_enabled", True)
|
||||
|
||||
# MSC4233: Remembering which server a user knocked through, so that
|
||||
# knocks can be rescinded and denied over federation.
|
||||
# Tracked in: https://github.com/element-hq/synapse/issues/NNNNN
|
||||
self.msc4233_enabled: bool = experimental.get("msc4233_enabled", False)
|
||||
|
||||
# MSC4076: Add `disable_badge_count`` to pusher configuration
|
||||
self.msc4076_enabled: bool = experimental.get("msc4076_enabled", False)
|
||||
|
||||
|
||||
@@ -136,6 +136,15 @@ class SendJoinResult:
|
||||
state_dag: list[EventBase] | None
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class SendKnockResult:
|
||||
# The response body from the remote server, of the form
|
||||
# {"knock_room_state": [<state event dict>, ...]}.
|
||||
response: JsonDict
|
||||
# The server which fulfilled the knock (i.e. answered our /send_knock).
|
||||
origin: str
|
||||
|
||||
|
||||
class FederationClient(FederationBase):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
@@ -1588,7 +1597,9 @@ class FederationClient(FederationBase):
|
||||
# content.
|
||||
return resp[1]
|
||||
|
||||
async def send_knock(self, destinations: list[str], pdu: EventBase) -> JsonDict:
|
||||
async def send_knock(
|
||||
self, destinations: list[str], pdu: EventBase
|
||||
) -> "SendKnockResult":
|
||||
"""Attempts to send a knock event to a given list of servers. Iterates
|
||||
through the list until one attempt succeeds.
|
||||
|
||||
@@ -1601,20 +1612,18 @@ class FederationClient(FederationBase):
|
||||
pdu: The event to be sent.
|
||||
|
||||
Returns:
|
||||
The remote homeserver return some state from the room. The response
|
||||
dictionary is in the form:
|
||||
|
||||
{"knock_room_state": [<state event dict>, ...]}
|
||||
|
||||
The list of state events may be empty.
|
||||
A SendKnockResult holding the remote homeserver's response (some
|
||||
state from the room, possibly empty) and the name of the server
|
||||
that fulfilled the knock.
|
||||
|
||||
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)
|
||||
async def send_request(destination: str) -> "SendKnockResult":
|
||||
response = await self._do_send_knock(destination, pdu)
|
||||
return SendKnockResult(response=response, origin=destination)
|
||||
|
||||
return await self._try_destination_list(
|
||||
"send_knock", destinations, send_request
|
||||
|
||||
@@ -982,7 +982,8 @@ class FederationHandler:
|
||||
|
||||
# Send the signed event back to the room, and potentially receive some
|
||||
# further information about the room in the form of partial state events
|
||||
knock_response = await self.federation_client.send_knock(target_hosts, event)
|
||||
send_knock_result = await self.federation_client.send_knock(target_hosts, event)
|
||||
knock_response = send_knock_result.response
|
||||
|
||||
# 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
|
||||
@@ -1040,6 +1041,20 @@ class FederationHandler:
|
||||
stream_id = await self._federation_event_handler.persist_events_and_notify(
|
||||
event.room_id, [(event, context)]
|
||||
)
|
||||
|
||||
# Remember which server fulfilled the knock, so that we can route a
|
||||
# rescission of the knock through it later, and know which server to
|
||||
# trust for an out-of-band retraction of the knock (MSC4233). A crash
|
||||
# between persisting the knock and this write leaves the knock without
|
||||
# a remembered route, which MSC4233 already covers: we fall back to
|
||||
# generating the leave locally.
|
||||
await self.store.store_local_knock_via_server(
|
||||
user_id=knockee,
|
||||
room_id=event.room_id,
|
||||
via_server=send_knock_result.origin,
|
||||
knock_event_id=event.event_id,
|
||||
)
|
||||
|
||||
return event.event_id, stream_id
|
||||
|
||||
async def _handle_queued_pdus(
|
||||
|
||||
@@ -95,6 +95,7 @@ purge_room_tables_with_room_id_column = (
|
||||
"appservice_room_list",
|
||||
"e2e_room_keys",
|
||||
"event_push_summary",
|
||||
"local_knock_via_servers",
|
||||
"pusher_throttle",
|
||||
"room_account_data",
|
||||
"room_tags",
|
||||
|
||||
@@ -686,6 +686,54 @@ class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
|
||||
|
||||
return results
|
||||
|
||||
async def store_local_knock_via_server(
|
||||
self, user_id: str, room_id: str, via_server: str, knock_event_id: str
|
||||
) -> None:
|
||||
"""Record the remote server that fulfilled a local user's knock on a
|
||||
remote room, so that a later rescission can be routed through it
|
||||
(MSC4233). Overwrites any previous record for the user/room pair.
|
||||
|
||||
Args:
|
||||
user_id: The ID of the knocking (local) user.
|
||||
room_id: The ID of the room that was knocked on.
|
||||
via_server: The remote server that answered our /send_knock.
|
||||
knock_event_id: The event ID of the knock membership event.
|
||||
"""
|
||||
await self.db_pool.simple_upsert(
|
||||
table="local_knock_via_servers",
|
||||
keyvalues={"user_id": user_id, "room_id": room_id},
|
||||
values={"via_server": via_server, "knock_event_id": knock_event_id},
|
||||
desc="store_local_knock_via_server",
|
||||
)
|
||||
|
||||
async def get_local_knock_via_server(
|
||||
self, user_id: str, room_id: str, knock_event_id: str
|
||||
) -> str | None:
|
||||
"""Retrieve the remote server that fulfilled a local user's knock on a
|
||||
remote room, if we have a record of one (MSC4233).
|
||||
|
||||
Args:
|
||||
user_id: The ID of the knocking (local) user.
|
||||
room_id: The ID of the room that was knocked on.
|
||||
knock_event_id: The event ID of the knock the caller is acting on.
|
||||
A record left over from an earlier knock on the same room does
|
||||
not vouch for it.
|
||||
|
||||
Returns:
|
||||
The server name, or None if no route is recorded for that knock.
|
||||
"""
|
||||
return await self.db_pool.simple_select_one_onecol(
|
||||
table="local_knock_via_servers",
|
||||
keyvalues={
|
||||
"user_id": user_id,
|
||||
"room_id": room_id,
|
||||
"knock_event_id": knock_event_id,
|
||||
},
|
||||
retcol="via_server",
|
||||
allow_none=True,
|
||||
desc="get_local_knock_via_server",
|
||||
)
|
||||
|
||||
async def get_users_server_still_shares_room_with(
|
||||
self, user_ids: Collection[str]
|
||||
) -> set[str]:
|
||||
|
||||
@@ -184,6 +184,8 @@ Changes in SCHEMA_VERSION = 95
|
||||
sent from a different user.
|
||||
- `event_edges.is_state` is no longer read from (it has not been written to since SCHEMA_VERSION 72).
|
||||
- Add `server_announcement_rooms` table.
|
||||
- MSC4233: Add the `local_knock_via_servers` table, recording which server a
|
||||
local user's knock was fulfilled through.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
--
|
||||
-- This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
--
|
||||
-- Copyright (C) 2026 New Vector, Ltd
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU Affero General Public License as
|
||||
-- published by the Free Software Foundation, either version 3 of the
|
||||
-- License, or (at your option) any later version.
|
||||
--
|
||||
-- See the GNU Affero General Public License for more details:
|
||||
-- <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
|
||||
-- Records the remote server that fulfilled a local user's knock on a remote
|
||||
-- room (i.e. the server that answered our /send_knock request), so that we can
|
||||
-- route a subsequent rescission of the knock through the same server, and
|
||||
-- so that we know which server to trust for out-of-band retractions of the
|
||||
-- knock (MSC4233).
|
||||
--
|
||||
-- At most one row per local user and knocked room: a later knock on the same
|
||||
-- room overwrites the previous one. Rows are not deleted when the knock ends,
|
||||
-- so the table grows with the number of rooms local users have ever knocked
|
||||
-- on; MSC4233 only requires the record to survive as long as the knock is the
|
||||
-- user's membership.
|
||||
CREATE TABLE local_knock_via_servers (
|
||||
user_id TEXT NOT NULL,
|
||||
room_id TEXT NOT NULL,
|
||||
-- The server the knock was fulfilled through.
|
||||
via_server TEXT NOT NULL,
|
||||
-- The event ID of the knock membership event.
|
||||
knock_event_id TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, room_id)
|
||||
);
|
||||
Reference in new issue
Block a user