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§
- Create
Room Request - Invite
Target - The invitee of
apply_invite: the resolved internal user id and the effective label to stamp into their invite event asdisplayname. - Kick
BanBody - Members
Query - Membership
Power Action - The pieces
apply_membership_power_actionneeds 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)alongsideapply_membership_power_action— see that function’s own doc for why. - Room
Create - Inputs for
apply_create_room. Identity, entitlement, and the HTTP body parse stay with the builder. Invitees are already integer user ids. - Room
Invitee - Caller-supplied invitee. The mxid is already stored on
matrix_users;displaynameis whatever the builder wants stamped into the member event. - User
IdBody
Enums§
- Join
Decision - Room
Creation - Target
State Rule - What state a kick/ban/unban target must currently be in for the action
to proceed.
pub(super)— alsoroutes::matrix::moderation’s (P11) own room-power fallback path reuses this exact rule set, so a kick/ban/unban reached through/api/matrix-admin/.../moderatenever 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
inviterow: Member + PowerCheck(invite), refuses an already-invited/joined or banned target), used by both the real handler (after resolvingtarget_user_idvia [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 asdisplayname, 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/leaveis “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 — sincemember_and_invited_idsalready 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
TargetStateRuleagainst the target’s current membership, then writes the target’s newm.room.memberstate event.target_user_idis 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.
contentis already final: the builder stampsdisplaynameon 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
joinand every invitee’sinvitem.room.memberstate events of a fresh room, each stamped with its own user’s effective label (inviteesis(mxid, label)per invited user). The creator’s event comes first:m.room.power_levelsmust follow the member events it refers to (seecrate::store::create_room_with_state). - check_
encryption_ state_ change PUT state/{eventType}/{stateKey}’sm.room.encryptionrule: 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_ruleis retained for call-site compatibility.- check_
state_ event_ type_ allowed PUT statemust never touchm.room.createorm.room.member(manager review, 2026-09-24 — a real gap: the genericPUT state/{type}/{key}route had no type-specific refusal at all, so a Member with enoughstate_defaultpower could rewritem.room.create’s content, or silently kick/invite/ban a target by PUTting theirm.room.memberevent 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 OWNm.room.memberevent while itsmembershipstaysjoin(a profile update; [put_state_inner] then forces itsdisplaynameto the nick’s effective label viastamp_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: truerequires EXACTLY ONEinvite[]entry (the peer; the creator is the other member) or refuses withM_INVALID_PARAM; otherwisevisibility == "public"->channel; otherwisegroup. - dm_
pair_ key (lo, hi)-ordered pair key for a DM between two internal user ids — the same trickdm_conversationsalready uses (dm_db.rs), reused here forrooms.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_keycurrently names a room, return its id when it is STILL a live DM for bothuser_aanduser_b(bothjoin/invite, neitherleaved out); otherwise free the pair key on that dead room (rooms.dm_pair_keyisUNIQUE, so a fresh room reusing this pair key needs the slot released first) and returnNone, 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
contentof the invitee’sm.room.memberinvite event, carrying the invitee’s own effective label asdisplayname. A DM invite also carriesis_direct: true(the Matrix convention): the invitee’s client reads it frominvite_stateto classify the room as a DM and record it inm.direct; without it the room would surface as a group. Every other kind carries the membership and thedisplaynameonly. - invite_
power_ level power_levels.invitefor a fresh room (plan §5):dm/channelare fixed at50;groupdefaults to50unlessvalidate_power_level_overridereturned an explicit override.pub(crate)— also used by the P12 legacy-DM migration.- join_
member_ content - The
contentof ajoinm.room.memberevent for a user whose effective label isdisplayname(seewith_displayname).pub(crate)— the legacy-DM migration’s bootstrap writes the same event. - member_
and_ invited_ ids - Every user id currently
joined ORinvited inroom_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)(notpub(super)) so bothroutes::matrix::messaging(P6) androutes::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.encryptionif missing, fliprooms.is_encrypted, and normalizehistory_visibilitytoshared(public vs private = join_rule only). Past timeline plaintext rows stay as historicalm.room.message; new sends must be encrypted. Idempotent. Returns how many rooms were changed. - power_
levels_ content - The
m.room.power_levelscontent for a fresh room (plan §5): the creator at100, every other field at the table’s fixed defaults exceptevents_default(raised to50for achannel— 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 nativecreateRoomcall would have produced. - power_
levels_ of m.room.power_levels’ content, or the empty object (every threshold then resolves to its Matrix default viacrate::store::user_level/crate::store::event_level) if the room has never had one applied.- require_
member - The plan §4 “Member” gate: a
room_membersrow withmembership='join'. - require_
power - require_
power_ over_ target - kick/ban/unban’s own gate (manager review, 2026-09-24): beyond the flat
require_powerthreshold, the caller’s level must be STRICTLY GREATER than the target’s current level — seecrate::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_encryptedfor 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 closedeventstable and never undercrate::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.memberupdate (PUT state, the profile-update exception incheck_state_event_type_allowed) withdisplaynameforced to the effective label: the nick is the source of truth (PUT /profile/{userId}/displaynameis 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 helpercreate_roomuses.- validate_
power_ level_ override power_level_content_overridevalidation (P5 correction): accepted with ONLY theinvitekey (value0or50), and only forgrouprooms (“members can add people,” §5). Any other key, an out-of-range value, or use on a non-group room is refused withM_INVALID_PARAM. Returns the override value to apply (Noneif the object was absent/empty).- with_
displayname contentwith the user’sdisplaynamestamped in. The server owns this field of everyjoin/invitemember event it writes (the Matrix convention): a client names a DM after the peer’s member-eventdisplaynameand lists members by it, falling back to the mxid localpart (the 32-hex public id) when it is absent. An emptydisplayname(no identity row behind the user) leaves the field out rather than stamping an empty name.