Rescind a knock through the remembered server

MSC4233 "Rescinding the knock": with the feature on, `/leave` on a
pending remote knock runs the `make_leave`/`send_leave` dance through
the server the knock went through, so the room sees the knock end.
Until now we only generated a local leave, and the knock stayed pending
in the room for ever.

The dance is the one we already do to reject an out-of-band invite, so
this reuses `do_remotely_reject_invite` and documents that it serves
both cases.

With no remembered server, or if the dance fails for any reason, we
fall back to the local leave. The handler catches every exception
because MSC4233 makes the local leave the answer to any failure of the
dance, DNS errors included.

Fixes #18030.

Co-authored-by: Quentin Gliech <quenting@element.io>

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
authored and Quentin Gliech committed 2026-10-30 10:20:36 +01:00
1 parent a356f964ac
commit dd5c807878
2 files changed
+46 -6

No files matched your search

+16
View File
@@ -1663,6 +1663,22 @@ class FederationHandler:
async def do_remotely_reject_invite(
self, target_hosts: Iterable[str], room_id: str, user_id: str, content: JsonDict
) -> tuple[EventBase, int]:
"""Leave a room we are not resident in, via the make_leave/send_leave dance.
Used both to reject an out-of-band invite and, under MSC4233, to
rescind an out-of-band knock through the server the knock was
fulfilled through: the two are the same federation dance and differ
only in which hosts we are given.
Args:
target_hosts: The servers to try, in order.
room_id: The room to leave.
user_id: The local user leaving.
content: The content for the leave event.
Returns:
The persisted leave event and its stream ID.
"""
origin, event, room_version = await self._make_and_verify_event(
target_hosts, room_id, user_id, "leave", content=content
)
+30 -6
View File
@@ -2098,13 +2098,37 @@ class RoomMemberMasterHandler(RoomMemberHandler):
Implements RoomMemberHandler.remote_rescind_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)
if self.hs.config.experimental.msc4233_enabled:
# MSC4233: route the rescission through the server the knock was
# fulfilled through, which we remembered at knock time.
via_server = await self.store.get_local_knock_via_server(
knock_event.state_key, knock_event.room_id, knock_event_id
)
if via_server is not None:
try:
(
event,
stream_id,
) = await self.federation_handler.do_remotely_reject_invite(
[via_server],
knock_event.room_id,
knock_event.state_key,
content,
)
return event.event_id, stream_id
except Exception as e:
# MSC4233 makes the local rescission below the fallback
# for any failure of the dance, from DNS errors upwards.
logger.warning(
"Failed to rescind knock on %s via %s: %s",
knock_event.room_id,
via_server,
e,
)
# Rescind the knock locally
return await self._generate_local_out_of_band_leave(
knock_event, txn_id, requester, content
)