From b3ac42904349968dea534797683592c8283b9d23 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 Aug 2025 19:11:40 -0600 Subject: [PATCH] Use PDUs for stripped state over federation As per MSC4311 updates. --- synapse/events/utils.py | 13 +++++-------- synapse/federation/federation_client.py | 16 +++++++++++++--- synapse/federation/federation_server.py | 2 +- synapse/handlers/federation.py | 3 ++- synapse/handlers/message.py | 11 ++++++++++- synapse/storage/databases/main/events_worker.py | 5 ++++- 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index cae27136cec..63c497b5f6f 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -870,7 +870,7 @@ def maybe_upsert_event_field( return upsert_okay -def strip_event(event: EventBase) -> JsonDict: +def strip_event(event: EventBase, for_federation: Optional[bool] = False) -> JsonDict: """ Used for "stripped state" events which provide a simplified view of the state of a room intended to help a potential joiner identify the room (relevant when the user @@ -879,13 +879,10 @@ def strip_event(event: EventBase) -> JsonDict: Stripped state events can only have the `sender`, `type`, `state_key` and `content` properties present. """ - # MSC4311: Ensure the create event is available on invites and knocks. - # TODO: Implement the rest of MSC4311 - if ( - event.room_version.msc4291_room_ids_as_hashes - and event.type == EventTypes.Create - and event.get_state_key() == "" - ): + # MSC4311 makes all stripped state events fully-formed PDUs over federation, + # especially the `m.room.create` event. + # TODO: Implement the validation component of MSC4311 + if for_federation: return event.get_pdu_json() return { diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 542d9650d47..e51a0982648 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -1336,10 +1336,16 @@ async def send_invite( room_id: str, event_id: str, pdu: EventBase, + stripped_state: List[JsonDict], ) -> EventBase: room_version = await self.store.get_room_version(room_id) - content = await self._do_send_invite(destination, pdu, room_version) + content = await self._do_send_invite( + destination, + pdu, + room_version, + stripped_state, + ) pdu_dict = content["event"] @@ -1360,7 +1366,11 @@ async def send_invite( return pdu async def _do_send_invite( - self, destination: str, pdu: EventBase, room_version: RoomVersion + self, + destination: str, + pdu: EventBase, + room_version: RoomVersion, + stripped_state: List[JsonDict], ) -> JsonDict: """Actually sends the invite, first trying v2 API and falling back to v1 API if necessary. @@ -1383,7 +1393,7 @@ async def _do_send_invite( content={ "event": pdu.get_pdu_json(time_now), "room_version": room_version.identifier, - "invite_room_state": pdu.unsigned.get("invite_room_state", []), + "invite_room_state": stripped_state, }, ) except HttpResponseException as e: diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 127518e1f7d..86ac9b6de5f 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -907,7 +907,7 @@ async def on_send_knock_request( # related to the room while the knock request is pending. stripped_room_state = ( await self.store.get_stripped_room_state_from_event_context( - context, self._room_prejoin_state_types + context, self._room_prejoin_state_types, for_federation=True, ) ) return {"knock_room_state": stripped_room_state} diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 34aae7ef3ce..00dae4e3e81 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -547,7 +547,7 @@ async def try_backfill(domains: StrCollection) -> bool: return False - async def send_invite(self, target_host: str, event: EventBase) -> EventBase: + async def send_invite(self, target_host: str, event: EventBase, stripped_state: List[JsonDict]) -> EventBase: """Sends the invite to the remote server for signing. Invites must be signed by the invitee's server before distribution. @@ -558,6 +558,7 @@ async def send_invite(self, target_host: str, event: EventBase) -> EventBase: room_id=event.room_id, event_id=event.event_id, pdu=event, + stripped_state=stripped_state, ) except RequestSendFailed: raise SynapseError(502, f"Can't connect to server {target_host}") diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index fff46b640bd..b6bedeb6934 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1973,8 +1973,17 @@ async def persist_and_notify_client_events( # way? If we have been invited by a remote server, we need # to get them to sign the event. + # As of MSC4311, "stripped" state events are formatted differently + # over federation. + stripped_state_fed = await self.store.get_stripped_room_state_from_event_context( + context, + self.room_prejoin_state_types, + membership_user_id=event.sender, + for_federation=True, + ) + returned_invite = await federation_handler.send_invite( - invitee.domain, event + invitee.domain, event, stripped_state_fed, ) event.unsigned.pop("room_state", None) diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index 7f015aa22cb..17653c3b547 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -1094,6 +1094,7 @@ async def get_stripped_room_state_from_event_context( context: EventContext, state_keys_to_include: StateFilter, membership_user_id: Optional[str] = None, + for_federation: Optional[bool] = False, ) -> List[JsonDict]: """ Retrieve the stripped state from a room, given an event context to retrieve state @@ -1110,6 +1111,8 @@ async def get_stripped_room_state_from_event_context( events of. This is useful when generating the stripped state of a room for invites. We want to send membership events of the inviter, so that the invitee can display the inviter's profile information if the room lacks any. + for_federation: When True, the stripped state events will be returned as PDUs + as per MSC4311. When False, the stripped client format is used. Returns: A list of dictionaries, each representing a stripped state event from the room. @@ -1134,7 +1137,7 @@ async def get_stripped_room_state_from_event_context( state_to_include = await self.get_events(selected_state_ids.values()) - return [strip_event(e) for e in state_to_include.values()] + return [strip_event(e, for_federation) for e in state_to_include.values()] def _maybe_start_fetch_thread(self) -> None: """Starts an event fetch thread if we are not yet at the maximum number."""