diff --git a/changelog.d/19958.bugfix b/changelog.d/19958.bugfix index 03e3f597bc..f2376eabe4 100644 --- a/changelog.d/19958.bugfix +++ b/changelog.d/19958.bugfix @@ -1 +1 @@ -Add a new `auto_accept_invites.enabled_for_accepted_knocks` option which, when enabled, automatically joins a user to a room when an invite arrives for a knock they have pending in that room (i.e. their knock was accepted), instead of requiring the user to accept the resulting invite by hand. Also fix the `on_new_event` module callback breaking event persistence when a rejected event was received over federation. +Add a new `auto_accept_invites.enabled_for_accepted_knocks` option which, when enabled, automatically joins a user to a room when an invite arrives for a knock they have pending in that room (i.e. their knock was accepted), instead of requiring the user to accept the resulting invite by hand; the server designates one of the user's devices to download any MSC4268 room key bundle. Also fix the `on_new_event` module callback breaking event persistence when a rejected event was received over federation. diff --git a/synapse/events/auto_accept_invites.py b/synapse/events/auto_accept_invites.py index 5eae169133..2fcaa306d5 100644 --- a/synapse/events/auto_accept_invites.py +++ b/synapse/events/auto_accept_invites.py @@ -26,9 +26,14 @@ from synapse.api.constants import AccountDataTypes, EventTypes, Membership from synapse.api.errors import SynapseError from synapse.config.auto_accept_invites import AutoAcceptInvitesConfig from synapse.module_api import EventBase, ModuleApi, run_as_background_process +from synapse.types import create_requester logger = logging.getLogger(__name__) +# MSC4509: to-device message designating one of a user's devices as the eager +# downloader of an MSC4268 room key bundle after a server-initiated join. +UNSTABLE_KEY_BUNDLE_CLAIM_TYPE = "org.matrix.msc4509.key_bundle_claim" + class InviteAutoAccepter: def __init__(self, config: AutoAcceptInvitesConfig, api: ModuleApi): @@ -251,3 +256,54 @@ class InviteAutoAccepter: if join_event is not None: break + + if join_event is not None: + # The join was made by the server, so it lands on all of the + # user's devices in the same sync instant. Designate one device to + # eagerly download any MSC4268 room key bundle for the room, lest + # every device does (MSC4509). Best effort: the hint is advisory, + # and without it clients fall back to claiming the bundle lazily. + try: + await self._send_key_bundle_claim_hint(target, room_id) + except Exception as e: + logger.warning( + "Failed to send key bundle claim hint to %s for %s: %s", + target, + room_id, + e, + ) + + async def _send_key_bundle_claim_hint(self, user_id: str, room_id: str) -> None: + """Send an `org.matrix.msc4509.key_bundle_claim` to-device message to + the user's most recently active device, designating it as the one + device which should eagerly download an MSC4268 room key bundle for + the room the user was just joined to. + + Args: + user_id: the (local) user who was joined to the room + room_id: the room they were joined to + """ + # This is bundled with Synapse (rather than a true module), so reach + # into the homeserver for the device machinery: the module API doesn't + # expose device listing or to-device sending. + hs = self._api._hs + devices = await hs.get_device_handler().get_devices_by_user(user_id) + if not devices: + return + + # Pick the device the user is most likely to be actively using. The + # worst a stale pick costs is the bundle being claimed lazily instead. + device = max(devices, key=lambda d: d.get("last_seen_ts") or 0) + device_id = device["device_id"] + + logger.info( + "Designating device %s of %s to claim any key bundle for %s", + device_id, + user_id, + room_id, + ) + await hs.get_device_message_handler().send_device_message( + create_requester(user_id), + UNSTABLE_KEY_BUNDLE_CLAIM_TYPE, + {user_id: {device_id: {"room_id": room_id}}}, + ) diff --git a/tests/events/test_auto_accept_invites.py b/tests/events/test_auto_accept_invites.py index 03d8918d89..31f0ad4cfc 100644 --- a/tests/events/test_auto_accept_invites.py +++ b/tests/events/test_auto_accept_invites.py @@ -32,7 +32,10 @@ from synapse.api.constants import EventTypes from synapse.api.errors import SynapseError from synapse.config._base import RootConfig from synapse.config.auto_accept_invites import AutoAcceptInvitesConfig -from synapse.events.auto_accept_invites import InviteAutoAccepter +from synapse.events.auto_accept_invites import ( + UNSTABLE_KEY_BUNDLE_CLAIM_TYPE, + InviteAutoAccepter, +) from synapse.handlers.sync import JoinedSyncResult, SyncRequestKey from synapse.module_api import ModuleApi from synapse.rest import admin @@ -123,6 +126,27 @@ class AutoAcceptInvitesTestCase(FederatingHomeserverTestCase): self.assertEqual(len(join_updates), 1) self.assertEqual(join_updates[0].room_id, room_id) + # The join was server-initiated, so the knocker's device should have + # been designated (via a to-device message) to eagerly claim any + # MSC4268 room key bundle for the room. + devices = self.get_success( + self.hs.get_datastores().main.get_devices_by_user(knocker_id) + ) + self.assertEqual(len(devices), 1, devices) + device_id = next(iter(devices)) + to_stream_id = self.hs.get_datastores().main.get_to_device_stream_token() + messages, _ = self.get_success( + self.hs.get_datastores().main.get_messages_for_device( + knocker_id, device_id, 0, to_stream_id + ) + ) + claim_messages = [ + m for m in messages if m["type"] == UNSTABLE_KEY_BUNDLE_CLAIM_TYPE + ] + self.assertEqual(len(claim_messages), 1, messages) + self.assertEqual(claim_messages[0]["content"], {"room_id": room_id}) + self.assertEqual(claim_messages[0]["sender"], knocker_id) + def test_no_auto_join_on_accepted_knock_by_default(self) -> None: """With `enabled_for_accepted_knocks` off (the default), an accepted knock stays an ordinary invite."""