Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bdfeb90
Client side API's should still use stripped events with MSC4311
MadLittleMods Apr 23, 2026
43a11f5
Add changelog
MadLittleMods Apr 23, 2026
7f25120
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods May 1, 2026
6026aaa
Non-working: Use full PDU's for `invite_room_state` in federation
MadLittleMods May 2, 2026
e0eb224
Iteration that uses current state
MadLittleMods May 2, 2026
3464ec8
Use `get_stripped_room_state_ids_from_event_context`
MadLittleMods May 2, 2026
f96c008
Invite event should be stripped when included in `invite_state`
MadLittleMods May 4, 2026
a088aa8
Sanitize `invite_room_state` received over federation
MadLittleMods May 4, 2026
336b686
Fix test lints
MadLittleMods May 4, 2026
22f4f20
Updated comment
MadLittleMods May 4, 2026
cce5dc7
Merge branch 'develop' into madlittlemods/remove-flawed-msc4311-parti…
MadLittleMods May 21, 2026
7e379e7
Make the TODO more obvious
MadLittleMods May 21, 2026
92d0d8b
Placeholder arg docstring
MadLittleMods May 21, 2026
76b4905
Better logic/reasoning
MadLittleMods May 22, 2026
5103f1b
Better comment flow
MadLittleMods May 22, 2026
5c1f4ca
Use `FIXME(MSC4311)` prefix
MadLittleMods May 22, 2026
374c4c5
Mark down create event after we verify it's valid
MadLittleMods May 22, 2026
ffe5c4b
Small comment to explain (we also explain above)
MadLittleMods May 22, 2026
2b900b4
Better `invite_room_state` scrutiny
MadLittleMods May 22, 2026
5e12cb8
Fix `events` typo
MadLittleMods May 22, 2026
ff533df
Event ID context
MadLittleMods May 22, 2026
de78d9e
Simplify logic to just use `hs.config.api.room_prejoin_state` instead…
MadLittleMods May 22, 2026
4598a43
`_parse_stripped_room_state`
MadLittleMods May 23, 2026
fbff685
`_minimal_parse_stripped_room_state`
MadLittleMods May 23, 2026
75a53ef
Strip the knock itself we add down `/sync`
MadLittleMods May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/19723.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remove flawed [MSC4311](https://github.com/matrix-org/matrix-spec-proposals/pull/4311) partial implementation: Client-side API's like `/sync` should still use stripped events.
21 changes: 21 additions & 0 deletions rust/src/room_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,25 @@ pub struct RoomVersion {
/// This is similar to how doubly-linked lists can potentially not refer to previous items correctly
/// without verifying the list's integrity, but doing it on every insert is too expensive.
pub msc4242_state_dags: bool,
/// Whether the `m.room.create` event is required in
/// `invite_room_state`/`knock_room_state` when receiving invites/knocks over the
/// federation API's.
///
/// Also determines whether we expect full PDU's in the
/// `invite_room_state`/`knock_room_state` in the federation API. The client API
/// still uses stripped state.
///
/// According to MSC4311:
/// > If any of the events are not a PDU, not for the room ID specified, or fail
/// > signature checks, or the `m.room.create` event is missing, the receiving
/// > server MAY respond to invites with a `400 M_MISSING_PARAM` standard Matrix
/// > error (new to the endpoint). For invites to room version 12+ rooms, servers
/// > SHOULD rather than MAY respond to such requests with `400 M_MISSING_PARAM`.
///
/// This does *not* determine whether we should include the `m.room.create` event in
/// stripped state or use full PDU's in stripped state over federation. We should
/// always do this.
msc4311_stripped_state: bool,
}

impl RoomVersion {
Expand All @@ -187,6 +206,7 @@ impl RoomVersion {
msc4291_room_ids_as_hashes: false,
strict_event_byte_limits_room_versions: false,
msc4242_state_dags: false,
msc4311_stripped_state: false,
};

pub const V2: RoomVersion = RoomVersion {
Expand Down Expand Up @@ -304,6 +324,7 @@ impl RoomVersion {
state_res: StateResolutionVersions::V2_1,
msc4289_creator_power_enabled: true,
msc4291_room_ids_as_hashes: true,
msc4311_stripped_state: true,
..Self::V11
};

Expand Down
18 changes: 9 additions & 9 deletions synapse/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,15 +1020,6 @@ 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() == ""
):
return event.get_pdu_json()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the flawed MSC4311 partial implementation

return {
"type": event.type,
"state_key": event.state_key,
Expand Down Expand Up @@ -1062,3 +1053,12 @@ def parse_stripped_state_event(raw_stripped_event: Any) -> StrippedStateEvent |
)

return None


def serialize_stripped_state_event(stripped_event: StrippedStateEvent) -> JsonDict:
return {
"type": stripped_event.type,
"state_key": stripped_event.state_key,
"sender": stripped_event.sender,
"content": stripped_event.content,
}
67 changes: 56 additions & 11 deletions synapse/federation/federation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
RoomVersions,
)
from synapse.events import EventBase, builder, make_event_from_dict
from synapse.events.snapshot import EventContext
from synapse.federation.federation_base import (
FederationBase,
InvalidEventSignatureError,
Expand All @@ -71,7 +72,12 @@
from synapse.http.types import QueryParams
from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace
from synapse.metrics import SERVER_NAME_LABEL
from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id
from synapse.types import (
JsonDict,
StrCollection,
UserID,
get_domain_from_id,
)
from synapse.util.async_helpers import concurrently_execute
from synapse.util.caches.expiringcache import ExpiringCache
from synapse.util.duration import Duration
Expand Down Expand Up @@ -138,6 +144,7 @@ def __init__(self, hs: "HomeServer"):

self.server_name = hs.hostname
self.signing_key = hs.signing_key
self._room_prejoin_state_types = hs.config.api.room_prejoin_state

# Cache mapping `event_id` to a tuple of the event itself and the `pull_origin`
# (which server we pulled the event from)
Expand Down Expand Up @@ -1309,12 +1316,12 @@ async def send_invite(
self,
destination: str,
room_id: str,
event_id: str,
pdu: EventBase,
context: EventContext,
) -> 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, context, room_version)

pdu_dict = content["event"]

Expand All @@ -1335,11 +1342,21 @@ async def send_invite(
return pdu

async def _do_send_invite(
self, destination: str, pdu: EventBase, room_version: RoomVersion
self,
destination: str,
pdu: EventBase,
context: EventContext,
room_version: RoomVersion,
) -> JsonDict:
"""Actually sends the invite, first trying v2 API and falling back to
v1 API if necessary.

Args:
destination:
pdu: Invite event
context:
room_version:

Returns:
The event as a dict as returned by the remote server

Expand All @@ -1350,6 +1367,18 @@ async def _do_send_invite(
"""
time_now = self._clock.time_msec()

# MSC4311: For the federation API, format events in `invite_room_state` as full
# PDU's
#
# Find the full events based on the state at the time of the invite
state_ids = await self.store.get_stripped_room_state_ids_from_event_context(
context, self._room_prejoin_state_types
)
state_events = await self.store.get_events(state_ids)
assert set(state_ids) == set(state_events.keys()), (
"We should have all events available that were set as stripped state."
)

try:
return await self.transport_layer.send_invite_v2(
destination=destination,
Expand All @@ -1358,10 +1387,19 @@ 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": [
# Use full PDU's according to MSC4311
state_event.get_pdu_json(time_now)
for state_event in state_events.values()
],
},
)
except HttpResponseException as e:
# TODO: MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx
# error by the sending server over the Client-Server API. This is done
# because there's nothing the client can materially do differently to make
# the request succeed.

# If an error is received that is due to an unrecognised endpoint,
# fallback to the v1 endpoint if the room uses old-style event IDs.
# Otherwise, consider it a legitimate error and raise.
Expand All @@ -1379,12 +1417,19 @@ async def _do_send_invite(
# Didn't work, try v1 API.
# Note the v1 API returns a tuple of `(200, content)`

_, content = await self.transport_layer.send_invite_v1(
destination=destination,
room_id=pdu.room_id,
event_id=pdu.event_id,
content=pdu.get_pdu_json(time_now),
)
try:
_, content = await self.transport_layer.send_invite_v1(
destination=destination,
room_id=pdu.room_id,
event_id=pdu.event_id,
content=pdu.get_pdu_json(time_now),
)
except HttpResponseException as e:
# TODO: MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx
# error by the sending server over the Client-Server API. This is done
# because there's nothing the client can materially do differently to make
# the request succeed.
raise e
return content

async def send_leave(self, destinations: Iterable[str], pdu: EventBase) -> None:
Expand Down
69 changes: 58 additions & 11 deletions synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,8 +767,27 @@ async def on_make_join_request(
return {"event": pdu.get_templated_pdu_json(), "room_version": room_version}

async def on_invite_request(
self, origin: str, content: JsonDict, room_version_id: str
self,
*,
origin: str,
expected_room_id: str,
expected_event_id: str,
event_json: JsonDict,
room_version_id: str,
) -> dict[str, Any]:
"""
Args:
origin:
expected_room_id: The room ID specified in the
`/_matrix/federation/v1/invite/{roomId}/{eventId}` request that we expect to
match in the actual event itself.
expected_event_id: The event ID specified in the
`/_matrix/federation/v1/invite/{roomId}/{eventId}` request that we expect to
match in the actual event itself.
event_json:
room_version_id:
"""

room_version = KNOWN_ROOM_VERSIONS.get(room_version_id)
if not room_version:
raise SynapseError(
Expand All @@ -777,9 +796,21 @@ async def on_invite_request(
Codes.UNSUPPORTED_ROOM_VERSION,
)

pdu = event_from_pdu_json(content, room_version)
pdu = event_from_pdu_json(event_json, room_version)
origin_host, _ = parse_server_name(origin)
await self.check_server_matches_acl(origin_host, pdu.room_id)
if pdu.event_id != expected_event_id:
raise SynapseError(
400,
Codes.INVALID_PARAM,
"Invite event ID must match event ID specified in the federation `/invite` request",
)
if pdu.room_id != expected_room_id:
raise SynapseError(
400,
Codes.INVALID_PARAM,
"The room_id specified in the invite event must match room ID specified in the federation `/invite` request",
)
if await self._spam_checker_module_callbacks.should_drop_federated_event(pdu):
logger.info(
"Federated event contains spam, dropping %s",
Expand All @@ -792,7 +823,11 @@ async def on_invite_request(
errmsg = f"event id {pdu.event_id}: {e}"
logger.warning("%s", errmsg)
raise SynapseError(403, errmsg, Codes.FORBIDDEN)
ret_pdu = await self.handler.on_invite_request(origin, pdu, room_version)
ret_pdu = await self.handler.on_invite_request(
origin=origin,
event=pdu,
room_version=room_version,
)
time_now = self._clock.time_msec()
return {"event": ret_pdu.get_pdu_json(time_now)}

Expand Down Expand Up @@ -952,19 +987,31 @@ async def on_send_knock_request(
Returns:
The stripped room state.
"""
time_now = self._clock.time_msec()

_, context = await self._on_send_membership_event(
origin, content, Membership.KNOCK, room_id
)

# 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(
context, self._room_prejoin_state_types
)
# MSC4311: For the federation API, format events in `knock_room_state` as full
# PDU's
#
# Find the full events based on the state at the time of the knock
state_ids = await self.store.get_stripped_room_state_ids_from_event_context(
context, self._room_prejoin_state_types
)
state_events = await self.store.get_events(state_ids)
assert set(state_ids) == set(state_events.keys()), (
"We should have all events available that were set as stripped state."
)
return {"knock_room_state": stripped_room_state}

return {
"knock_room_state": [
# Use full PDU's according to MSC4311
state_event.get_pdu_json(time_now)
for state_event in state_events.values()
]
}

async def _on_send_membership_event(
self, origin: str, content: JsonDict, membership_type: str, room_id: str
Expand Down
18 changes: 11 additions & 7 deletions synapse/federation/transport/server/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,11 @@ async def on_PUT(
# state resolution algorithm, and we don't use that for processing
# invites
result = await self.handler.on_invite_request(
origin, content, room_version_id=RoomVersions.V1.identifier
origin=origin,
expected_room_id=room_id,
expected_event_id=event_id,
event_json=content,
room_version_id=RoomVersions.V1.identifier,
)

# V1 federation API is defined to return a content of `[200, {...}]`
Expand All @@ -512,9 +516,6 @@ async def on_PUT(
room_id: str,
event_id: str,
) -> tuple[int, JsonDict]:
# TODO(paul): assert that room_id/event_id parsed from path actually
# match those given in content
Comment on lines -515 to -516
Copy link
Copy Markdown
Contributor Author

@MadLittleMods MadLittleMods May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed as we actually do this now in FederationServer.on_invite_request(...) (added in this PR)


room_version = content["room_version"]
event = content["event"]
invite_room_state = content.get("invite_room_state", [])
Expand All @@ -523,12 +524,15 @@ async def on_PUT(
invite_room_state = []

# Synapse expects invite_room_state to be in unsigned, as it is in v1
# API

# API. We will sanitize this inside `on_invite_request(...)`
event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state

result = await self.handler.on_invite_request(
origin, event, room_version_id=room_version
origin=origin,
expected_room_id=room_id,
expected_event_id=event_id,
event_json=event,
room_version_id=room_version,
)

# We only store invite_room_state for internal use, so remove it before
Expand Down
Loading
Loading