1use std::collections::HashSet;
7
8use rusqlite::Connection;
9
10use crate::error::MatrixError;
11use crate::store::{HistoryVisibility, JoinRule, Membership, PowerAction, Room, RoomKind};
12
13#[derive(serde::Deserialize, Default)]
18pub struct CreateRoomRequest {
19 #[serde(default)]
20 pub visibility: Option<String>,
21 #[serde(default)]
22 pub is_direct: bool,
23 #[serde(default)]
24 pub invite: Vec<String>,
25 #[serde(default)]
26 pub name: Option<String>,
27 #[serde(default)]
28 pub topic: Option<String>,
29 #[serde(default)]
30 pub power_level_content_override: Option<serde_json::Value>,
31 #[serde(default)]
35 pub room_alias_name: Option<String>,
36 #[serde(default)]
38 pub creation_content: Option<serde_json::Value>,
39 }
46
47
48#[derive(serde::Deserialize)]
49pub struct UserIdBody {
50 pub user_id: String,
51}
52
53
54#[derive(serde::Deserialize)]
55pub struct KickBanBody {
56 pub user_id: String,
57 #[serde(default)]
58 pub reason: Option<String>,
59}
60
61
62#[derive(serde::Deserialize, Default)]
63pub struct MembersQuery {
64 pub membership: Option<String>,
65 pub at: Option<String>,
67}
68
69
70pub fn derive_room_kind(is_direct: bool, invite_count: usize, visibility_public: bool) -> Result<RoomKind, MatrixError> {
79 if is_direct {
80 return if invite_count == 1 {
81 Ok(RoomKind::Dm)
82 } else {
83 Err(MatrixError::invalid_param("is_direct requires exactly one invite target"))
84 };
85 }
86 if visibility_public {
87 return Ok(RoomKind::Channel);
88 }
89 Ok(RoomKind::Group)
90}
91
92
93pub fn with_displayname(mut content: serde_json::Value, displayname: &str) -> serde_json::Value {
101 if !displayname.is_empty() {
102 content["displayname"] = serde_json::Value::String(displayname.to_string());
103 }
104 content
105}
106
107
108pub fn join_member_content(displayname: &str) -> serde_json::Value {
112 with_displayname(serde_json::json!({ "membership": "join" }), displayname)
113}
114
115
116pub fn invite_member_content(kind: RoomKind, displayname: &str) -> serde_json::Value {
123 let mut content = with_displayname(serde_json::json!({ "membership": "invite" }), displayname);
124 if kind == RoomKind::Dm {
125 content["is_direct"] = serde_json::Value::Bool(true);
126 }
127 content
128}
129
130
131pub fn stamp_own_member_displayname(content: &str, displayname: &str) -> Result<String, MatrixError> {
138 let mut value: serde_json::Value = serde_json::from_str(content)?;
139 let Some(fields) = value.as_object_mut() else {
140 return Err(MatrixError::invalid_param("m.room.member content must be an object"));
141 };
142 if displayname.is_empty() {
143 fields.remove("displayname");
144 } else {
145 fields.insert("displayname".to_string(), serde_json::Value::String(displayname.to_string()));
146 }
147 Ok(value.to_string())
148}
149
150
151pub fn bootstrap_member_events(kind: RoomKind, creator: (i64, &str, &str), invitees: &[(String, String)]) -> Vec<crate::store::NewStateEvent> {
157 let (creator_user_id, creator_mxid, creator_label) = creator;
158 let mut events = vec![state_event("m.room.member", creator_mxid, creator_user_id, join_member_content(creator_label))];
159 for (mxid, label) in invitees {
160 events.push(state_event("m.room.member", mxid, creator_user_id, invite_member_content(kind, label)));
161 }
162 events
163}
164
165
166pub fn room_kind_settings(kind: RoomKind) -> (JoinRule, HistoryVisibility, bool) {
174 match kind {
175 RoomKind::Dm => (JoinRule::Invite, HistoryVisibility::Shared, true),
176 RoomKind::Group => (JoinRule::Invite, HistoryVisibility::Shared, true),
177 RoomKind::Channel => (JoinRule::Public, HistoryVisibility::WorldReadable, false),
178 }
179}
180
181
182pub fn validate_power_level_override(kind: RoomKind, override_value: &Option<serde_json::Value>) -> Result<Option<i64>, MatrixError> {
188 let Some(value) = override_value else { return Ok(None) };
189 let obj = value.as_object().ok_or_else(|| MatrixError::invalid_param("power_level_content_override must be an object"))?;
190 if obj.keys().any(|k| k != "invite") {
191 return Err(MatrixError::invalid_param("power_level_content_override only accepts the 'invite' key"));
192 }
193 let Some(invite_value) = obj.get("invite") else { return Ok(None) };
194 if kind != RoomKind::Group {
195 return Err(MatrixError::invalid_param("power_level_content_override is only accepted for group rooms"));
196 }
197 match invite_value.as_i64() {
198 Some(0) => Ok(Some(0)),
199 Some(50) => Ok(Some(50)),
200 _ => Err(MatrixError::invalid_param("power_level_content_override.invite must be 0 or 50")),
201 }
202}
203
204
205pub fn invite_power_level(kind: RoomKind, override_value: Option<i64>) -> i64 {
210 match kind {
211 RoomKind::Dm | RoomKind::Channel => 50,
212 RoomKind::Group => override_value.unwrap_or(50),
213 }
214}
215
216
217pub fn power_levels_content(creator_mxid: &str, kind: RoomKind, invite_level: i64) -> serde_json::Value {
224 let events_default = if kind == RoomKind::Channel { 50 } else { 0 };
225 serde_json::json!({
226 "users": { creator_mxid: 100 },
227 "users_default": 0,
228 "events_default": events_default,
229 "state_default": 50,
230 "ban": 50,
231 "kick": 50,
232 "redact": 50,
233 "invite": invite_level,
234 })
235}
236
237
238pub fn check_encryption_state_change(event_type: &str, join_rule: JoinRule, already_encrypted: bool) -> Result<(), MatrixError> {
243 let _ = join_rule;
244 if event_type != "m.room.encryption" {
245 return Ok(());
246 }
247 if already_encrypted {
248 return Err(MatrixError::forbidden("encryption cannot be changed once set"));
249 }
250 Ok(())
251}
252
253
254pub fn check_state_event_type_allowed(event_type: &str, state_key: &str, caller_mxid: &str, content_str: &str) -> Result<(), MatrixError> {
268 if event_type == "m.room.create" {
269 return Err(MatrixError::forbidden("m.room.create cannot be modified"));
270 }
271 if event_type == "m.space.child" || event_type == "m.space.parent" {
272 let value: serde_json::Value = serde_json::from_str(content_str)?;
273 crate::spaces::validate_space_state(event_type, &value)?;
274 }
275 if event_type == "m.room.member" {
276 let membership: Option<String> = serde_json::from_str::<serde_json::Value>(content_str)
277 .ok()
278 .and_then(|v| v.get("membership").and_then(|m| m.as_str()).map(str::to_string));
279 let is_own_join_update = state_key == caller_mxid && membership.as_deref() == Some("join");
280 if !is_own_join_update {
281 return Err(MatrixError::forbidden(
282 "m.room.member only changes via join/leave/invite/kick/ban, except your own profile update while still joined",
283 ));
284 }
285 }
286 Ok(())
287}
288
289
290pub fn dm_pair_key(a: i64, b: i64) -> String {
295 let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
296 format!("{lo}:{hi}")
297}
298
299
300pub fn state_event(event_type: &str, state_key: &str, sender_user_id: i64, content: serde_json::Value) -> crate::store::NewStateEvent {
303 crate::store::NewStateEvent {
304 event_id: crate::store::new_event_id(),
305 sender_user_id,
306 event_type: event_type.to_string(),
307 state_key: state_key.to_string(),
308 content: content.to_string(),
309 }
310}
311
312
313pub fn power_levels_of(conn: &Connection, room_id: &str) -> Result<serde_json::Value, MatrixError> {
321 match crate::store::current_state_event(conn, room_id, "m.room.power_levels", "")? {
322 Some(event) => Ok(serde_json::from_str(&event.content)?),
323 None => Ok(serde_json::json!({})),
324 }
325}
326
327
328pub fn require_member(membership: Option<Membership>) -> Result<(), MatrixError> {
330 match membership {
331 Some(Membership::Join) => Ok(()),
332 _ => Err(MatrixError::forbidden("not a member of this room")),
333 }
334}
335
336
337pub fn require_power(power_levels: &serde_json::Value, mxid: &str, action: PowerAction) -> Result<(), MatrixError> {
338 if crate::store::can(power_levels, action, mxid) {
339 Ok(())
340 } else {
341 Err(MatrixError::forbidden("insufficient power level for this action"))
342 }
343}
344
345
346pub fn require_power_over_target(power_levels: &serde_json::Value, caller_mxid: &str, target_mxid: &str, action: PowerAction, self_leave: bool) -> Result<(), MatrixError> {
352 if crate::store::can_act_on(power_levels, action, caller_mxid, target_mxid, self_leave) {
353 Ok(())
354 } else {
355 Err(MatrixError::forbidden("insufficient power level for this action"))
356 }
357}
358
359
360pub fn require_pub_read(conn: &Connection, room: &Room, user_id: i64) -> Result<(), MatrixError> {
363 let membership = crate::store::room_member(conn, &room.id, user_id)?.map(|m| m.membership);
364 if matches!(membership, Some(Membership::Join)) {
365 return Ok(());
366 }
367 if room.join_rule == JoinRule::Public && room.history_visibility == HistoryVisibility::WorldReadable {
368 return Ok(());
369 }
370 Err(MatrixError::forbidden("no read access to this room"))
371}
372
373
374pub fn member_and_invited_ids(conn: &Connection, room_id: &str) -> rusqlite::Result<HashSet<i64>> {
381 let mut ids = HashSet::new();
382 for member in crate::store::room_members(conn, room_id, Some(Membership::Join))? {
383 ids.insert(member.user_id);
384 }
385 for member in crate::store::room_members(conn, room_id, Some(Membership::Invite))? {
386 ids.insert(member.user_id);
387 }
388 Ok(ids)
389}
390
391
392pub fn find_reusable_dm_room(conn: &Connection, pair_key: &str, user_a: i64, user_b: i64) -> rusqlite::Result<Option<String>> {
400 let Some(existing) = crate::store::room_by_dm_pair_key(conn, pair_key)? else {
401 return Ok(None);
402 };
403 let members = crate::store::room_members(conn, &existing.id, None)?;
404 let alive = |uid: i64| members.iter().any(|m| m.user_id == uid && matches!(m.membership, Membership::Join | Membership::Invite));
405 if alive(user_a) && alive(user_b) {
406 return Ok(Some(existing.id));
407 }
408 crate::store::clear_dm_pair_key(conn, &existing.id)?;
409 Ok(None)
410}
411
412
413#[derive(Debug, Clone, Copy)]
421pub struct InviteTarget<'a> {
422 pub user_id: i64,
423 pub displayname: &'a str,
424}
425
426pub fn apply_invite(
438 conn: &mut Connection,
439 room_id: &str,
440 caller_user_id: i64,
441 caller_mxid: &str,
442 target: InviteTarget<'_>,
443 now: &str,
444 origin_ts: i64,
445) -> Result<HashSet<i64>, MatrixError> {
446 let target_user_id = target.user_id;
447 let room = crate::store::get_room(conn, room_id)?.ok_or_else(|| MatrixError::not_found("no such room"))?;
448 let caller_membership = crate::store::room_member(conn, room_id, caller_user_id)?.map(|m| m.membership);
449 require_member(caller_membership)?;
450 let power_levels = power_levels_of(conn, room_id)?;
451 require_power(&power_levels, caller_mxid, PowerAction::Invite)?;
452
453 let target_membership = crate::store::room_member(conn, room_id, target_user_id)?.map(|m| m.membership);
454 match target_membership {
455 Some(Membership::Join) | Some(Membership::Invite) => return Err(MatrixError::invalid_param("user is already invited or joined")),
456 Some(Membership::Ban) => return Err(MatrixError::forbidden("user is banned from this room")),
457 Some(Membership::Leave) | None => {}
458 }
459
460 let target_mxid = crate::store::mxid_of(conn, target_user_id)?.ok_or_else(MatrixError::internal)?;
461 crate::store::apply_state_event(
462 conn,
463 &crate::store::StateEventWrite {
464 event_id: &crate::store::new_event_id(),
465 room_id,
466 sender_user_id: caller_user_id,
467 event_type: "m.room.member",
468 state_key: &target_mxid,
469 content: &invite_member_content(room.kind, target.displayname).to_string(),
470 origin_server_ts: origin_ts,
471 now,
472 },
473 )?;
474
475 let mut ids = member_and_invited_ids(conn, room_id)?;
476 ids.insert(target_user_id);
477 Ok(ids)
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486pub enum TargetStateRule {
487 MustBeActiveMember,
489 Any,
492 MustBeBanned,
494}
495
496
497pub fn check_target_state_rule(rule: TargetStateRule, membership: Option<Membership>) -> Result<(), MatrixError> {
498 match rule {
499 TargetStateRule::MustBeActiveMember => {
500 if matches!(membership, Some(Membership::Join) | Some(Membership::Invite)) {
501 Ok(())
502 } else {
503 Err(MatrixError::not_found("user is not a member of this room"))
504 }
505 }
506 TargetStateRule::MustBeBanned => {
507 if membership == Some(Membership::Ban) {
508 Ok(())
509 } else {
510 Err(MatrixError::invalid_param("user is not banned"))
511 }
512 }
513 TargetStateRule::Any => Ok(()),
514 }
515}
516
517
518pub struct MembershipPowerAction<'a> {
524 pub room_id: &'a str,
525 pub action: PowerAction,
526 pub target_user_id: i64,
527 pub target_rule: TargetStateRule,
528 pub new_membership: Membership,
529 pub reason: Option<&'a str>,
530}
531
532pub fn apply_membership_power_action(
547 conn: &mut Connection,
548 caller_user_id: i64,
549 caller_mxid: &str,
550 action: MembershipPowerAction<'_>,
551 now: &str,
552 origin_ts: i64,
553) -> Result<HashSet<i64>, MatrixError> {
554 let room_id = action.room_id;
555 let caller_membership = crate::store::room_member(conn, room_id, caller_user_id)?.map(|m| m.membership);
556 require_member(caller_membership)?;
557 let power_levels = power_levels_of(conn, room_id)?;
558
559 let target_mxid = crate::store::mxid_of(conn, action.target_user_id)?.ok_or_else(MatrixError::internal)?;
560 let self_leave = action.new_membership == Membership::Leave;
561 require_power_over_target(&power_levels, caller_mxid, &target_mxid, action.action, self_leave)?;
562
563 let target_membership = crate::store::room_member(conn, room_id, action.target_user_id)?.map(|m| m.membership);
564 check_target_state_rule(action.target_rule, target_membership)?;
565
566 let mut content = serde_json::json!({ "membership": action.new_membership.as_str() });
567 if let Some(reason) = action.reason {
568 content["reason"] = serde_json::Value::String(reason.to_string());
569 }
570 crate::store::apply_state_event(conn, &crate::store::StateEventWrite { event_id: &crate::store::new_event_id(), room_id, sender_user_id: caller_user_id, event_type: "m.room.member", state_key: &target_mxid, content: &content.to_string(), origin_server_ts: origin_ts, now })?;
571
572 let mut ids = member_and_invited_ids(conn, room_id)?;
573 ids.insert(action.target_user_id);
574 Ok(ids)
575}
576
577pub fn apply_join(
583 conn: &mut Connection,
584 room_id: &str,
585 caller_user_id: i64,
586 caller_mxid: &str,
587 displayname: &str,
588 now: &str,
589 origin_ts: i64,
590) -> Result<HashSet<i64>, MatrixError> {
591 crate::store::apply_state_event(
592 conn,
593 &crate::store::StateEventWrite {
594 event_id: &crate::store::new_event_id(),
595 room_id,
596 sender_user_id: caller_user_id,
597 event_type: "m.room.member",
598 state_key: caller_mxid,
599 content: &join_member_content(displayname).to_string(),
600 origin_server_ts: origin_ts,
601 now,
602 },
603 )?;
604 Ok(member_and_invited_ids(conn, room_id)?)
605}
606
607
608pub fn apply_leave(conn: &mut Connection, room_id: &str, caller_user_id: i64, caller_mxid: &str, now: &str, origin_ts: i64) -> Result<HashSet<i64>, MatrixError> {
617 let caller_membership = crate::store::room_member(conn, room_id, caller_user_id)?.map(|m| m.membership);
618 if !matches!(caller_membership, Some(Membership::Join) | Some(Membership::Invite)) {
619 return Err(MatrixError::forbidden("not a member or invitee of this room"));
620 }
621 let wake_ids = member_and_invited_ids(conn, room_id)?;
622 crate::store::apply_state_event(
623 conn,
624 &crate::store::StateEventWrite {
625 event_id: &crate::store::new_event_id(),
626 room_id,
627 sender_user_id: caller_user_id,
628 event_type: "m.room.member",
629 state_key: caller_mxid,
630 content: &serde_json::json!({ "membership": "leave" }).to_string(),
631 origin_server_ts: origin_ts,
632 now,
633 },
634 )?;
635 Ok(wake_ids)
636}
637
638
639pub struct RoomInvitee<'a> {
642 pub user_id: i64,
643 pub displayname: &'a str,
644}
645
646pub struct RoomCreate<'a> {
649 pub creator_user_id: i64,
650 pub creator_mxid: &'a str,
651 pub creator_displayname: &'a str,
652 pub is_direct: bool,
653 pub invitees: &'a [RoomInvitee<'a>],
654 pub visibility_public: bool,
655 pub power_level_content_override: Option<serde_json::Value>,
656 pub name: Option<&'a str>,
657 pub topic: Option<&'a str>,
658 pub room_type: Option<&'a str>,
660 pub predecessor: Option<(&'a str, &'a str)>,
662}
663
664pub enum RoomCreation {
665 Reused(String),
666 Created { room_id: String, notify_user_ids: HashSet<i64> },
667}
668
669pub fn apply_create_room(
672 conn: &mut Connection,
673 req: RoomCreate<'_>,
674 now: &str,
675 origin_ts: i64,
676) -> Result<RoomCreation, MatrixError> {
677 if req.invitees.iter().any(|invitee| invitee.user_id == req.creator_user_id) {
678 return Err(MatrixError::invalid_param("cannot invite yourself"));
679 }
680 let kind = derive_room_kind(req.is_direct, req.invitees.len(), req.visibility_public)?;
681 let invite_override = validate_power_level_override(kind, &req.power_level_content_override)?;
682 if kind == RoomKind::Dm {
683 let peer_id = req.invitees[0].user_id;
684 let pair_key = dm_pair_key(req.creator_user_id, peer_id);
685 if let Some(existing) = find_reusable_dm_room(conn, &pair_key, req.creator_user_id, peer_id)? {
686 return Ok(RoomCreation::Reused(existing));
687 }
688 }
689
690 let room_id = crate::store::new_room_id();
691 let (join_rule, history_visibility, is_encrypted) = room_kind_settings(kind);
692 let invite_level = invite_power_level(kind, invite_override);
693 let mut invitees = Vec::with_capacity(req.invitees.len());
694 for invitee in req.invitees {
695 let mxid = crate::store::mxid_of(conn, invitee.user_id)?.ok_or_else(MatrixError::internal)?;
696 invitees.push((mxid, invitee.displayname.to_string()));
697 }
698 let mut state_events = vec![state_event(
699 "m.room.create",
700 "",
701 req.creator_user_id,
702 {
703 let mut create = serde_json::json!({ "room_version": crate::store::MATRIX_ROOM_VERSION });
704 if let Some(t) = req.room_type {
705 create["type"] = serde_json::json!(t);
706 }
707 if let Some((room, event)) = req.predecessor {
708 create["predecessor"] = serde_json::json!({ "room_id": room, "event_id": event });
709 }
710 create
711 },
712 )];
713 state_events.extend(bootstrap_member_events(
714 kind,
715 (req.creator_user_id, req.creator_mxid, req.creator_displayname),
716 &invitees,
717 ));
718 state_events.push(state_event(
719 "m.room.power_levels",
720 "",
721 req.creator_user_id,
722 power_levels_content(req.creator_mxid, kind, invite_level),
723 ));
724 state_events.push(state_event(
725 "m.room.join_rules",
726 "",
727 req.creator_user_id,
728 serde_json::json!({ "join_rule": join_rule.as_str() }),
729 ));
730 state_events.push(state_event(
731 "m.room.history_visibility",
732 "",
733 req.creator_user_id,
734 serde_json::json!({ "history_visibility": history_visibility.as_str() }),
735 ));
736 if let Some(name) = req.name {
737 state_events.push(state_event(
738 "m.room.name",
739 "",
740 req.creator_user_id,
741 serde_json::json!({ "name": name }),
742 ));
743 }
744 if let Some(topic) = req.topic {
745 state_events.push(state_event(
746 "m.room.topic",
747 "",
748 req.creator_user_id,
749 serde_json::json!({ "topic": topic }),
750 ));
751 }
752 if is_encrypted {
753 state_events.push(state_event(
754 "m.room.encryption",
755 "",
756 req.creator_user_id,
757 serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2" }),
758 ));
759 }
760 let dm_pair = (kind == RoomKind::Dm).then(|| dm_pair_key(req.creator_user_id, req.invitees[0].user_id));
761 let bootstrap = crate::store::RoomBootstrap {
762 room_id: &room_id,
763 kind,
764 creator_user_id: req.creator_user_id,
765 created_at: now,
766 is_encrypted,
767 join_rule,
768 history_visibility,
769 dm_pair_key: dm_pair.as_deref(),
770 legacy_dm_id: None,
771 };
772 crate::store::create_room_with_state(conn, bootstrap, &state_events, origin_ts)?;
773 let mut notify_user_ids: HashSet<i64> = req.invitees.iter().map(|invitee| invitee.user_id).collect();
774 notify_user_ids.insert(req.creator_user_id);
775 Ok(RoomCreation::Created { room_id, notify_user_ids })
776}
777
778pub fn apply_upgrade(conn: &mut Connection, old_room: &str, caller_user_id: i64, caller_mxid: &str, caller_label: &str, now: &str, origin_ts: i64) -> Result<(String, HashSet<i64>), MatrixError> {
784 let room = crate::store::get_room(conn, old_room)?.ok_or_else(|| MatrixError::not_found("no such room"))?;
785 let member = crate::store::room_member(conn, old_room, caller_user_id)?;
786 require_member(member.as_ref().map(|m| m.membership))?;
787 let powerful = room.creator_user_id == caller_user_id || member.and_then(|m| m.power_level).unwrap_or(0) >= 100;
788 if !powerful {
789 return Err(MatrixError::forbidden("only the room's creator or an admin can upgrade it"));
790 }
791 if room.kind == RoomKind::Dm {
792 return Err(MatrixError::forbidden("a direct room is not upgraded"));
793 }
794 if crate::store::current_state_event(conn, old_room, "m.room.tombstone", "")?.is_some() {
795 return Err(MatrixError::forbidden("this room has already been upgraded"));
796 }
797 let content_of = |conn: &Connection, ty: &str| -> Result<Option<serde_json::Value>, MatrixError> {
798 Ok(match crate::store::current_state_event(conn, old_room, ty, "")? {
799 Some(e) => Some(serde_json::from_str(&e.content)?),
800 None => None,
801 })
802 };
803 let name = content_of(conn, "m.room.name")?.and_then(|c| c.get("name").and_then(|v| v.as_str()).map(str::to_string));
804 let topic = content_of(conn, "m.room.topic")?.and_then(|c| c.get("topic").and_then(|v| v.as_str()).map(str::to_string));
805 let room_type = content_of(conn, "m.room.create")?.and_then(|c| c.get("type").and_then(|v| v.as_str()).map(str::to_string)).filter(|t| t == "m.space");
806 let last: String = conn
807 .query_row("SELECT event_id FROM events WHERE room_id = ?1 ORDER BY stream_id DESC LIMIT 1", [old_room], |r| r.get(0))
808 .map_err(|_| MatrixError::internal())?;
809 let creation = apply_create_room(
810 conn,
811 RoomCreate {
812 creator_user_id: caller_user_id,
813 creator_mxid: caller_mxid,
814 creator_displayname: caller_label,
815 is_direct: false,
816 invitees: &[],
817 visibility_public: room.kind == RoomKind::Channel,
818 power_level_content_override: None,
819 name: name.as_deref(),
820 topic: topic.as_deref(),
821 room_type: room_type.as_deref(),
822 predecessor: Some((old_room, &last)),
823 },
824 now,
825 origin_ts,
826 )?;
827 let RoomCreation::Created { room_id: new_room, mut notify_user_ids } = creation else { return Err(MatrixError::internal()) };
828 for ty in ["m.room.avatar", "m.room.guest_access", "m.room.join_rules", "m.room.power_levels"] {
830 if let Some(c) = content_of(conn, ty)? {
831 let _ = apply_put_state(conn, &new_room, caller_user_id, caller_mxid, ty, "", &c.to_string(), now, origin_ts);
832 }
833 }
834 if room_type.is_some() {
835 for e in crate::store::current_state_all(conn, old_room)? {
836 if e.event_type == "m.space.child" {
837 if let Some(k) = &e.state_key {
838 let _ = apply_put_state(conn, &new_room, caller_user_id, caller_mxid, "m.space.child", k, &e.content, now, origin_ts);
839 }
840 }
841 }
842 }
843 let _ = conn.execute("UPDATE room_aliases SET room_id = ?1 WHERE room_id = ?2", rusqlite::params![new_room, old_room]);
845 let (_, wake) = apply_put_state(
846 conn,
847 old_room,
848 caller_user_id,
849 caller_mxid,
850 "m.room.tombstone",
851 "",
852 &serde_json::json!({ "body": "This room has been replaced", "replacement_room": new_room }).to_string(),
853 now,
854 origin_ts,
855 )?;
856 notify_user_ids.extend(wake);
857 Ok((new_room, notify_user_ids))
858}
859
860pub enum JoinDecision {
861 AlreadyJoined,
862 Joined(HashSet<i64>),
863}
864
865pub fn decide_and_apply_join(
868 conn: &mut Connection,
869 room_id: &str,
870 caller_user_id: i64,
871 caller_mxid: &str,
872 displayname: &str,
873 now: &str,
874 origin_ts: i64,
875) -> Result<JoinDecision, MatrixError> {
876 let room = crate::store::get_room(conn, room_id)?.ok_or_else(|| MatrixError::not_found("no such room"))?;
877 let current = crate::store::room_member(conn, room_id, caller_user_id)?.map(|member| member.membership);
878 match current {
879 Some(Membership::Join) => return Ok(JoinDecision::AlreadyJoined),
880 Some(Membership::Ban) => return Err(MatrixError::forbidden("banned from this room")),
881 Some(Membership::Invite) => {}
882 Some(Membership::Leave) | None => {
883 if room.join_rule != JoinRule::Public && !crate::spaces::restricted_allows(conn, room_id, caller_user_id)? {
884 return Err(MatrixError::forbidden("no invitation to this room"));
885 }
886 }
887 }
888 let ids = apply_join(conn, room_id, caller_user_id, caller_mxid, displayname, now, origin_ts)?;
889 Ok(JoinDecision::Joined(ids))
890}
891
892pub fn apply_put_state(
896 conn: &mut Connection,
897 room_id: &str,
898 sender_user_id: i64,
899 sender_mxid: &str,
900 event_type: &str,
901 state_key: &str,
902 content: &str,
903 now: &str,
904 origin_ts: i64,
905) -> Result<(String, HashSet<i64>), MatrixError> {
906 if content.len() > crate::store::MATRIX_EVENT_CONTENT_MAX_BYTES {
907 return Err(MatrixError::invalid_param("event content too large"));
908 }
909 let room = crate::store::get_room(conn, room_id)?.ok_or_else(|| MatrixError::not_found("no such room"))?;
910 let caller_membership = crate::store::room_member(conn, room_id, sender_user_id)?.map(|member| member.membership);
911 require_member(caller_membership)?;
912 check_state_event_type_allowed(event_type, state_key, sender_mxid, content)?;
913 let already_encrypted = crate::store::current_state_event(conn, room_id, "m.room.encryption", "")?.is_some();
914 check_encryption_state_change(event_type, room.join_rule, already_encrypted)?;
915 let power_levels = power_levels_of(conn, room_id)?;
916 if crate::store::user_level(&power_levels, sender_mxid) < crate::store::event_level(&power_levels, event_type, true) {
917 return Err(MatrixError::forbidden("insufficient power level to set this state event"));
918 }
919 if event_type == "m.room.power_levels" {
920 let new_power_levels: serde_json::Value = serde_json::from_str(content)?;
921 crate::store::validate_power_levels_change(&power_levels, &new_power_levels, sender_mxid).map_err(MatrixError::forbidden)?;
922 }
923 let event = crate::store::apply_state_event(
924 conn,
925 &crate::store::StateEventWrite {
926 event_id: &crate::store::new_event_id(),
927 room_id,
928 sender_user_id,
929 event_type,
930 state_key,
931 content,
932 origin_server_ts: origin_ts,
933 now,
934 },
935 )?;
936 let notify = member_and_invited_ids(conn, room_id)?;
937 Ok((event.event_id, notify))
938}
939
940pub fn apply_forget(conn: &Connection, room_id: &str, user_id: i64) -> Result<(), MatrixError> {
942 let deleted = crate::store::forget_membership(conn, room_id, user_id)?;
943 if deleted == 0 {
944 return Err(MatrixError::forbidden("must have left the room before forgetting it"));
945 }
946 Ok(())
947}
948
949
950pub fn migrate_plaintext_rooms_to_encrypted(
956 conn: &mut Connection,
957 now: &str,
958 origin_ts: i64,
959) -> Result<usize, MatrixError> {
960 let plaintext: Vec<(String, i64, String)> = {
961 let mut stmt = conn
962 .prepare("SELECT id, creator_user_id, history_visibility FROM rooms WHERE is_encrypted = 0 AND kind != 'channel'")
964 .map_err(|e| MatrixError::unknown(e.to_string()))?;
965 let rows = stmt
966 .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
967 .map_err(|e| MatrixError::unknown(e.to_string()))?;
968 rows.collect::<Result<Vec<_>, _>>()
969 .map_err(|e| MatrixError::unknown(e.to_string()))?
970 };
971 let mut changed = 0usize;
972 for (room_id, creator_user_id, hv) in plaintext {
973 let already = crate::store::current_state_event(conn, &room_id, "m.room.encryption", "")?.is_some();
974 if !already {
975 let event_id = crate::store::new_event_id();
976 crate::store::apply_state_event(
977 conn,
978 &crate::store::StateEventWrite {
979 event_id: &event_id,
980 room_id: &room_id,
981 sender_user_id: creator_user_id,
982 event_type: "m.room.encryption",
983 state_key: "",
984 content: r#"{"algorithm":"m.megolm.v1.aes-sha2"}"#,
985 origin_server_ts: origin_ts,
986 now,
987 },
988 )?;
989 }
990 if hv == HistoryVisibility::WorldReadable.as_str() {
991 let event_id = crate::store::new_event_id();
992 crate::store::apply_state_event(
993 conn,
994 &crate::store::StateEventWrite {
995 event_id: &event_id,
996 room_id: &room_id,
997 sender_user_id: creator_user_id,
998 event_type: "m.room.history_visibility",
999 state_key: "",
1000 content: r#"{"history_visibility":"shared"}"#,
1001 origin_server_ts: origin_ts,
1002 now,
1003 },
1004 )?;
1005 conn.execute(
1006 "UPDATE rooms SET history_visibility = ?1 WHERE id = ?2",
1007 rusqlite::params![HistoryVisibility::Shared.as_str(), room_id],
1008 )
1009 .map_err(|e| MatrixError::unknown(e.to_string()))?;
1010 }
1011 conn.execute(
1012 "UPDATE rooms SET is_encrypted = 1 WHERE id = ?1",
1013 rusqlite::params![room_id],
1014 )
1015 .map_err(|e| MatrixError::unknown(e.to_string()))?;
1016 changed += 1;
1017 }
1018 Ok(changed)
1019}
1020
1021pub fn drop_legacy_dm_scaffold_if_empty(conn: &Connection) -> Result<bool, MatrixError> {
1023 let count: i64 = conn
1024 .query_row(
1025 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='legacy_dm_message_map'",
1026 [],
1027 |row| row.get(0),
1028 )
1029 .map_err(|e| MatrixError::unknown(e.to_string()))?;
1030 if count == 0 {
1031 return Ok(false);
1032 }
1033 let rows: i64 = conn
1034 .query_row("SELECT COUNT(*) FROM legacy_dm_message_map", [], |row| row.get(0))
1035 .map_err(|e| MatrixError::unknown(e.to_string()))?;
1036 if rows > 0 {
1037 return Err(MatrixError::forbidden("legacy_dm_message_map still has rows; refuse drop"));
1038 }
1039 conn.execute_batch("DROP TABLE IF EXISTS legacy_dm_message_map;")
1040 .map_err(|e| MatrixError::unknown(e.to_string()))?;
1041 Ok(true)
1042}
1043
1044#[cfg(test)]
1045mod messenger_model_tests {
1046 use super::*;
1047 use crate::store::{self, HistoryVisibility, JoinRule, RoomKind};
1048 use rusqlite::Connection;
1049
1050 const T0: &str = "2026-10-06T00:00:00+00:00";
1051
1052 fn test_conn() -> Connection {
1053 let conn = Connection::open_in_memory().expect("in-memory sqlite");
1054 store::create_matrix_schema(&conn).expect("schema");
1055 conn
1056 }
1057
1058 #[test]
1059 fn dm_and_group_are_encrypted_channel_is_public_plaintext() {
1060 let (jr, hv, enc) = room_kind_settings(RoomKind::Dm);
1061 assert_eq!(jr, JoinRule::Invite);
1062 assert_eq!(hv, HistoryVisibility::Shared);
1063 assert!(enc);
1064
1065 let (jr, hv, enc) = room_kind_settings(RoomKind::Group);
1066 assert_eq!(jr, JoinRule::Invite);
1067 assert_eq!(hv, HistoryVisibility::Shared);
1068 assert!(enc);
1069
1070 let (jr, hv, enc) = room_kind_settings(RoomKind::Channel);
1071 assert_eq!(jr, JoinRule::Public);
1072 assert_eq!(hv, HistoryVisibility::WorldReadable);
1073 assert!(!enc, "public channels are plaintext in the public store");
1074 }
1075
1076 #[test]
1077 fn encryption_may_be_enabled_on_a_public_room_but_never_changed() {
1078 check_encryption_state_change("m.room.encryption", JoinRule::Public, false).expect("enable ok");
1079 let err = check_encryption_state_change("m.room.encryption", JoinRule::Public, true).unwrap_err();
1080 assert!(format!("{err:?}").contains("changed") || format!("{err:?}").to_lowercase().contains("forbidden"));
1081 }
1082
1083 #[test]
1084 fn public_channel_join_without_invite_private_group_requires_invite() {
1085 let mut conn = test_conn();
1086 store::ensure_matrix_user(&conn, 1, "alice000000000000000000000000001", T0).expect("alice");
1087 let bob_mxid = store::ensure_matrix_user(&conn, 2, "bob00000000000000000000000000002", T0).expect("bob");
1088
1089 let channel = "!chan:example.org";
1090 let (jr, hv, enc) = room_kind_settings(RoomKind::Channel);
1091 store::create_room(&conn, channel, RoomKind::Channel, 1, T0, enc, jr, hv, None, None).expect("channel");
1092 match decide_and_apply_join(&mut conn, channel, 2, &bob_mxid, "bob", T0, 1_000).expect("join") {
1093 JoinDecision::Joined(_) => {}
1094 JoinDecision::AlreadyJoined => panic!("expected fresh join"),
1095 }
1096
1097 let group = "!grp:example.org";
1098 let (jr, hv, enc) = room_kind_settings(RoomKind::Group);
1099 store::create_room(&conn, group, RoomKind::Group, 1, T0, enc, jr, hv, None, None).expect("group");
1100 match decide_and_apply_join(&mut conn, group, 2, &bob_mxid, "bob", T0, 2_000) {
1101 Err(err) => {
1102 let msg = format!("{err:?}").to_lowercase();
1103 assert!(msg.contains("invitation") || msg.contains("forbidden"), "{msg}");
1104 }
1105 Ok(_) => panic!("stranger must not join a private group without invite"),
1106 }
1107 }
1108
1109 #[test]
1110 fn migration_encrypts_plaintext_group_but_leaves_public_channel_alone() {
1111 let mut conn = test_conn();
1112 store::ensure_matrix_user(&conn, 1, "alice000000000000000000000000001", T0).expect("alice");
1113 let channel = "!oldchan:example.org";
1114 store::create_room(&conn, channel, RoomKind::Channel, 1, T0, false, JoinRule::Public, HistoryVisibility::WorldReadable, None, None).expect("channel");
1115 let group = "!oldgrp:example.org";
1116 store::create_room(&conn, group, RoomKind::Group, 1, T0, false, JoinRule::Invite, HistoryVisibility::Shared, None, None).expect("group");
1117 let n = migrate_plaintext_rooms_to_encrypted(&mut conn, T0, 1_000).expect("migrate");
1118 assert_eq!(n, 1, "only the group is rewritten");
1119 let room = store::get_room(&conn, group).expect("get").expect("exists");
1120 assert!(room.is_encrypted);
1121 let chan = store::get_room(&conn, channel).expect("get").expect("exists");
1122 assert!(!chan.is_encrypted, "public channel stays plaintext");
1123 assert_eq!(chan.history_visibility, HistoryVisibility::WorldReadable);
1124 assert!(store::current_state_event(&conn, channel, "m.room.encryption", "").expect("state").is_none());
1125 let n2 = migrate_plaintext_rooms_to_encrypted(&mut conn, T0, 2_000).expect("idempotent");
1126 assert_eq!(n2, 0);
1127 }
1128
1129 #[test]
1130 fn drop_legacy_dm_scaffold_when_empty() {
1131 let conn = test_conn();
1132 conn.execute_batch(
1134 "CREATE TABLE legacy_dm_message_map (
1135 legacy_message_id INTEGER PRIMARY KEY,
1136 event_id TEXT NOT NULL
1137 );",
1138 )
1139 .expect("old table");
1140 assert!(drop_legacy_dm_scaffold_if_empty(&conn).expect("drop"));
1141 assert!(!drop_legacy_dm_scaffold_if_empty(&conn).expect("already gone"));
1142 }
1143}