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