Persist the join first when the initial state has m.room.aliases

With `require_membership_for_aliases` (the default), `create_event`
checks that the sender of an `m.room.aliases` event has joined, by
loading their membership event from the database. Since the initial
events of a room are persisted in one batch, the creator's join is not
there yet, so a `/createRoom` with `m.room.aliases` in its initial
state failed with 403 "You must be in the room to create an alias for
it".

Persist the create event and the join on their own first in that case,
as for v1 rooms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVLsNh6akt4ifEizY4D1ta
This commit is contained in:
Erik JohnstonandClaude Opus 5.5 committed 2026-10-18 17:24:19 +00:00
1 parent b7b4e04de3
commit a486744648
2 files changed
+35 -10

No files matched your search

+18 -10
View File
@@ -1320,6 +1320,12 @@ class RoomCreationHandler:
if reason and isinstance(reason, str):
invite_content["reason"] = reason
raw_initial_state = config.get("initial_state", [])
initial_state = OrderedDict()
for val in raw_initial_state:
initial_state[(val["type"], val.get("state_key", ""))] = val["content"]
# Local invites go into the batch with the room's other initial events,
# in order, up to the first one that has to be sent on its own: an
# invite to a remote user, which needs a federation request first, or
@@ -1331,7 +1337,7 @@ class RoomCreationHandler:
# The invites to send on their own after the batch.
remaining_invites = invite_list
if not (
self._persist_create_and_join_alone(room_version)
self._persist_create_and_join_alone(room_version, initial_state)
or self._spam_checker_module_callbacks.has_user_may_invite_callbacks()
):
for index, invitee in enumerate(invite_list):
@@ -1352,12 +1358,6 @@ class RoomCreationHandler:
else:
remaining_invites = []
raw_initial_state = config.get("initial_state", [])
initial_state = OrderedDict()
for val in raw_initial_state:
initial_state[(val["type"], val.get("state_key", ""))] = val["content"]
(
last_stream_id,
last_sent_event_id,
@@ -1698,7 +1698,7 @@ class RoomCreationHandler:
current_state_group = events_and_context[-1][1].state_group
persist_create_and_join_alone = self._persist_create_and_join_alone(
room_version
room_version, initial_state
)
assert not (persist_create_and_join_alone and batched_invites)
@@ -1927,18 +1927,26 @@ class RoomCreationHandler:
len(invited_users),
)
def _persist_create_and_join_alone(self, room_version: RoomVersion) -> bool:
def _persist_create_and_join_alone(
self, room_version: RoomVersion, initial_state: StateMap[Any]
) -> bool:
"""Whether a new room's create event and the creator's join must each
be persisted on their own, before the next event is built.
Events in v1 and v2 rooms refer to their prev and auth events by hash,
which the builder reads from the database. `check_event_allowed`
callbacks are given the room state from the database, and rebuilding an
event they change reads its prev events from the database too.
event they change reads its prev events from the database too. An
`m.room.aliases` event in the initial state needs the sender's join in
the database when `require_membership_for_aliases` is set.
"""
return (
room_version.event_format == EventFormatVersions.ROOM_V1_V2
or self._third_party_event_rules.has_check_event_allowed_callbacks()
or (
self.event_creation_handler.require_membership_for_aliases
and any(etype == EventTypes.Aliases for etype, _ in initial_state)
)
)
def _validate_room_config(
+17
View File
@@ -427,6 +427,23 @@ class CreateRoomPersistBatchesTestCase(unittest.HomeserverTestCase):
)
self._check_room(room_id)
def test_aliases_in_initial_state(self) -> None:
"""An `m.room.aliases` event in the initial state needs the creator's
join in the database, so the create event and the join go first."""
self.batches.clear()
self.helper.create_room_as(
self.user_id,
tok=self.tok,
room_version="10",
extra_content={
"initial_state": [
{"type": EventTypes.Aliases, "state_key": "test", "content": {}}
]
},
)
self.assertEqual(self.batches[:2], [[EventTypes.Create], [EventTypes.Member]])
self.assertIn(EventTypes.Aliases, self.batches[2])
def test_check_event_allowed_callback(self) -> None:
"""With a `check_event_allowed` callback, the create event and the join
are persisted on their own. A callback that rewrites them leaves no