Skip to main content

Module rooms

Module rooms 

Source
Expand description

Room and membership decisions over an open messenger connection. The builder supplies user ids, mxids, and display names, and enforces entitlements before it calls in. Nothing here authenticates, bills, or wakes a socket.

Structs§

CreateRoomRequest
InviteTarget
The invitee of apply_invite: the resolved internal user id and the effective label to stamp into their invite event as displayname.
KickBanBody
MembersQuery
MembershipPowerAction
The pieces apply_membership_power_action needs beyond the caller and the connection — grouped so the function itself stays under a plain four-parameter signature instead of needing #[allow(clippy::too_many_arguments)]. pub(super) alongside apply_membership_power_action — see that function’s own doc for why.
RoomCreate
Inputs for apply_create_room. Identity, entitlement, and the HTTP body parse stay with the builder. Invitees are already integer user ids.
RoomInvitee
Caller-supplied invitee. The mxid is already stored on matrix_users; displayname is whatever the builder wants stamped into the member event.
UserIdBody

Enums§

JoinDecision
RoomCreation
TargetStateRule
What state a kick/ban/unban target must currently be in for the action to proceed. pub(super) — also routes::matrix::moderation’s (P11) own room-power fallback path reuses this exact rule set, so a kick/ban/unban reached through /api/matrix-admin/.../moderate never drifts from the same action reached through this module’s own /kick//ban//unban.

Functions§

apply_create_room
Create a room, or reuse the live DM for the same pair. Returns the user ids the builder may notify. Does not wake anyone.
apply_forget
Forget a room the caller has already left.
apply_invite
The invite action’s whole DB-side decision + write (plan §4’s invite row: Member + PowerCheck(invite), refuses an already-invited/joined or banned target), used by both the real handler (after resolving target_user_id via [resolve_target_user], which needs the identity connection this function deliberately does NOT take) and this module’s own tests. Returns the wake set (every joined+invited member, including the new invitee).
apply_join
POST /join’s write for an already-invited or newly-public-eligible caller (idempotent-join and ban checks happen in the handler, before the policy hook — see [join_room]). Returns the post-join wake set. The join event carries the caller’s effective label as displayname, resolved by the caller BEFORE it takes the messenger connection this writes through.
apply_leave
POST /leave’s write (manager review, 2026-09-24: the plan’s plain “Member” gate table entry undersold this — Matrix’s own spec behaviour for /leave is “leave OR reject a pending invite,” so an INVITED, not yet joined, caller must be accepted too). Wake set is captured BEFORE the leave lands, so the leaver’s own other devices are included, and — since member_and_invited_ids already covers every joined+invited member — the inviter (who must be a joined member to have sent the invite at all) is naturally woken as well.
apply_membership_power_action
The shared core of kick/ban/unban (plan §4: Member + PowerCheck(kick/ ban)): checks the caller’s own membership+power, checks TargetStateRule against the target’s current membership, then writes the target’s new m.room.member state event. target_user_id is already resolved (identity-DB work happens in the handler, not here — see the module doc’s “testable cores” note). Returns the wake set (post-write joined+invited members plus the target, even though the target may no longer be one of those after this write).
apply_put_state
Write one state event. content is already final: the builder stamps displayname on an own-member profile update before calling in. Returns (event_id, user ids the builder may notify).
apply_upgrade
Replace a room by a new one (the way a legacy room moves to the current room version, hash ids included when the server makes them): the new room gets the old one’s kind, name, topic, type, avatar, join rules, history visibility, power levels and (for a space) its children, its create event names the old room as predecessor, and the old room gets a tombstone. Direct rooms are not upgraded (a pair has one direct room). Needs power 100 or the room’s creator.
bootstrap_member_events
The creator’s join and every invitee’s invite m.room.member state events of a fresh room, each stamped with its own user’s effective label (invitees is (mxid, label) per invited user). The creator’s event comes first: m.room.power_levels must follow the member events it refers to (see crate::store::create_room_with_state).
check_encryption_state_change
PUT state/{eventType}/{stateKey}’s m.room.encryption rule: refuse changing/removing an EXISTING encryption event. Enabling encryption on a still-unencrypted room (including a public channel) is allowed — all room kinds are E2E. join_rule is retained for call-site compatibility.
check_state_event_type_allowed
PUT state must never touch m.room.create or m.room.member (manager review, 2026-09-24 — a real gap: the generic PUT state/{type}/{key} route had no type-specific refusal at all, so a Member with enough state_default power could rewrite m.room.create’s content, or silently kick/invite/ban a target by PUTting their m.room.member event directly, bypassing every kick/ban/invite-specific rule this module otherwise enforces (target-level checks, txn-dedup, the right wake set, …). The ONE exception: a joined user updating their OWN m.room.member event while its membership stays join (a profile update; [put_state_inner] then forces its displayname to the nick’s effective label via stamp_own_member_displayname, so the client’s own name never sticks) — membership itself only ever changes through join/leave/invite/kick/ban.
check_target_state_rule
decide_and_apply_join
Join gate without an entitlement check. A banned user is refused. A private room still requires an invite. The builder stamps displayname.
derive_room_kind
Kind derivation (plan P5 correction): is_direct: true requires EXACTLY ONE invite[] entry (the peer; the creator is the other member) or refuses with M_INVALID_PARAM; otherwise visibility == "public" -> channel; otherwise group.
dm_pair_key
(lo, hi)-ordered pair key for a DM between two internal user ids — the same trick dm_conversations already uses (dm_db.rs), reused here for rooms.dm_pair_key. pub(crate) — also used by the P12 legacy-DM migration.
drop_legacy_dm_scaffold_if_empty
Drop empty legacy_dm_message_map (P12 scaffold). Refuses if any rows remain.
find_reusable_dm_room
The DM-reuse lookup (plan P5 correction): if pair_key currently names a room, return its id when it is STILL a live DM for both user_a and user_b (both join/invite, neither leaved out); otherwise free the pair key on that dead room (rooms.dm_pair_key is UNIQUE, so a fresh room reusing this pair key needs the slot released first) and return None, telling the caller to mint a new room. pub(crate) — the legacy-DM migration adopts a live native DM through the same lookup.
invite_member_content
The content of the invitee’s m.room.member invite event, carrying the invitee’s own effective label as displayname. A DM invite also carries is_direct: true (the Matrix convention): the invitee’s client reads it from invite_state to classify the room as a DM and record it in m.direct; without it the room would surface as a group. Every other kind carries the membership and the displayname only.
invite_power_level
power_levels.invite for a fresh room (plan §5): dm/channel are fixed at 50; group defaults to 50 unless validate_power_level_override returned an explicit override. pub(crate) — also used by the P12 legacy-DM migration.
join_member_content
The content of a join m.room.member event for a user whose effective label is displayname (see with_displayname). pub(crate) — the legacy-DM migration’s bootstrap writes the same event.
member_and_invited_ids
Every user id currently joined OR invited in room_id — the wake fan-out set for a write that affects the whole room (plan §3.1/§4: “wakes every joined AND invited member”). pub(crate) (not pub(super)) so both routes::matrix::messaging (P6) and routes::dm’s legacy-DM bridge (P15, outside this module tree) reuse the SAME wake-set computation rather than a second copy — see each module’s own doc.
migrate_plaintext_rooms_to_encrypted
Enable Megolm on every still-plaintext room: write m.room.encryption if missing, flip rooms.is_encrypted, and normalize history_visibility to shared (public vs private = join_rule only). Past timeline plaintext rows stay as historical m.room.message; new sends must be encrypted. Idempotent. Returns how many rooms were changed.
power_levels_content
The m.room.power_levels content for a fresh room (plan §5): the creator at 100, every other field at the table’s fixed defaults except events_default (raised to 50 for a channel — announcement-only). pub(crate) — reused verbatim by the P12 legacy-DM migration, so a migrated room’s power levels are byte-for-byte what a native createRoom call would have produced.
power_levels_of
m.room.power_levels’ content, or the empty object (every threshold then resolves to its Matrix default via crate::store::user_level/ crate::store::event_level) if the room has never had one applied.
require_member
The plan §4 “Member” gate: a room_members row with membership='join'.
require_power
require_power_over_target
kick/ban/unban’s own gate (manager review, 2026-09-24): beyond the flat require_power threshold, the caller’s level must be STRICTLY GREATER than the target’s current level — see crate::store::can_act_on’s own doc for why a level-50 admin must never be able to touch a level-100 owner or an equal-level peer.
require_pub_read
The plan §4 “PubRead” gate: Member, OR any signed-in caller when the room is a public, world-readable channel.
room_kind_settings
join_rule, history_visibility, is_encrypted for a fresh room. DM and group are E2E (shared). A Channel is PUBLIC PLAINTEXT (world_readable, not encrypted; project decision): its posts live in the separate public store (crate::public_channels), never in the closed events table and never under crate::retention. Rooms that were already created encrypted stay encrypted. pub(crate) — also the P12 legacy-DM migration’s bootstrap builder.
stamp_own_member_displayname
A joined user’s own m.room.member update (PUT state, the profile-update exception in check_state_event_type_allowed) with displayname forced to the effective label: the nick is the source of truth (PUT /profile/{userId}/displayname is refused for the same reason), so a client-chosen name is overwritten, and dropped when there is no label to stamp instead. Every other field of the client’s content is kept.
state_event
pub(crate) — also used by the P12 legacy-DM migration to build its own bootstrap state events through the same helper create_room uses.
validate_power_level_override
power_level_content_override validation (P5 correction): accepted with ONLY the invite key (value 0 or 50), and only for group rooms (“members can add people,” §5). Any other key, an out-of-range value, or use on a non-group room is refused with M_INVALID_PARAM. Returns the override value to apply (None if the object was absent/empty).
with_displayname
content with the user’s displayname stamped in. The server owns this field of every join/invite member event it writes (the Matrix convention): a client names a DM after the peer’s member-event displayname and lists members by it, falling back to the mxid localpart (the 32-hex public id) when it is absent. An empty displayname (no identity row behind the user) leaves the field out rather than stamping an empty name.