1use rusqlite::{params, Connection, OptionalExtension, Transaction};
47use std::collections::HashSet;
48use std::fmt;
49
50static SERVER_NAME_CELL: std::sync::OnceLock<String> = std::sync::OnceLock::new();
58
59pub fn set_matrix_server_name(name: impl Into<String>) -> Result<(), &'static str> {
62 let name = name.into();
63 let (host, port) = name.split_once(':').map_or((name.as_str(), None), |(h, p)| (h, Some(p)));
66 if host.is_empty() || host.contains('/') || host.contains(':') || port.is_some_and(|p| p.parse::<u16>().is_err()) {
67 return Err("server name must be a hostname, optionally with :port");
68 }
69 SERVER_NAME_CELL.set(name).map_err(|_| "server name already set")
70}
71
72static LOCAL_ALIASES: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
73
74
75pub fn set_local_aliases(names: impl IntoIterator<Item = String>) {
83 let list: Vec<String> = names.into_iter().map(|n| n.trim().to_ascii_lowercase()).filter(|n| !n.is_empty() && !n.contains(':') && !n.contains('/')).collect();
84 let _ = LOCAL_ALIASES.set(list);
85}
86
87pub fn is_local_server_name(name: &str) -> bool {
89 name == matrix_server_name() || LOCAL_ALIASES.get().is_some_and(|aliases| aliases.iter().any(|a| a.eq_ignore_ascii_case(name)))
90}
91
92pub fn matrix_server_name() -> &'static str {
94 SERVER_NAME_CELL.get_or_init(|| "example.org".to_string()).as_str()
95}
96
97pub const MATRIX_ROOM_VERSION: &str = "11";
102
103pub const MATRIX_EVENT_CONTENT_MAX_BYTES: usize = 16 * 1024;
109
110pub const RESERVED_LOCALPART_PREFIX: &str = "_bridge_";
115
116pub fn is_reserved_localpart(localpart: &str) -> bool {
120 localpart.starts_with(RESERVED_LOCALPART_PREFIX)
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum MatrixIdError {
126 MissingSigil,
128 MissingServerName,
130 ForeignServerName,
134}
135
136impl fmt::Display for MatrixIdError {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 match self {
139 MatrixIdError::MissingSigil => write!(f, "mxid is missing its '@' sigil"),
140 MatrixIdError::MissingServerName => write!(f, "mxid is missing a ':server_name' suffix"),
141 MatrixIdError::ForeignServerName => write!(f, "mxid is addressed to a foreign server name"),
142 }
143 }
144}
145
146pub fn mxid_for_public_id(public_id: &str) -> String {
149 format!("@{public_id}:{}", matrix_server_name())
150}
151
152pub fn public_id_from_mxid(mxid: &str) -> Result<&str, MatrixIdError> {
156 let rest = mxid.strip_prefix('@').ok_or(MatrixIdError::MissingSigil)?;
157 let (localpart, server_name) = rest.split_once(':').ok_or(MatrixIdError::MissingServerName)?;
158 if !is_local_server_name(server_name) {
159 return Err(MatrixIdError::ForeignServerName);
160 }
161 Ok(localpart)
162}
163
164fn random_id_component() -> String {
167 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
168 use base64::Engine;
169 use rand::Rng;
170 let bytes: [u8; 16] = rand::thread_rng().gen();
171 URL_SAFE_NO_PAD.encode(bytes)
172}
173
174pub fn new_room_id() -> String {
176 format!("!{}:{}", random_id_component(), matrix_server_name())
177}
178
179pub fn new_event_id() -> String {
182 format!("${}", random_id_component())
183}
184
185#[derive(Debug)]
192pub enum MatrixStoreError {
193 Db(rusqlite::Error),
194 Json(serde_json::Error),
197 ReservedLocalpart,
200 DuplicateAnnotation,
204 UnknownEventId(String),
206 UnknownMxid(String),
209 InvalidMembership(String),
212 InvalidRelationTarget(String),
217 WrongRoom(String),
222 UnredactableEvent(String),
226 F3Rejected(String),
229}
230
231impl From<rusqlite::Error> for MatrixStoreError {
232 fn from(e: rusqlite::Error) -> Self {
233 MatrixStoreError::Db(e)
234 }
235}
236
237impl From<serde_json::Error> for MatrixStoreError {
238 fn from(e: serde_json::Error) -> Self {
239 MatrixStoreError::Json(e)
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum RoomKind {
252 Dm,
253 Group,
254 Channel,
255}
256
257impl RoomKind {
258 pub fn as_str(self) -> &'static str {
259 match self {
260 RoomKind::Dm => "dm",
261 RoomKind::Group => "group",
262 RoomKind::Channel => "channel",
263 }
264 }
265
266 pub fn from_wire_name(s: &str) -> Option<Self> {
267 match s {
268 "dm" => Some(RoomKind::Dm),
269 "group" => Some(RoomKind::Group),
270 "channel" => Some(RoomKind::Channel),
271 _ => None,
272 }
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub enum JoinRule {
278 Invite,
279 Public,
280}
281
282impl JoinRule {
283 pub fn as_str(self) -> &'static str {
284 match self {
285 JoinRule::Invite => "invite",
286 JoinRule::Public => "public",
287 }
288 }
289
290 pub fn from_wire_name(s: &str) -> Option<Self> {
291 match s {
292 "invite" => Some(JoinRule::Invite),
293 "public" => Some(JoinRule::Public),
294 _ => None,
295 }
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum HistoryVisibility {
301 Shared,
302 WorldReadable,
303 Invited,
304 Joined,
305}
306
307impl HistoryVisibility {
308 pub fn as_str(self) -> &'static str {
309 match self {
310 HistoryVisibility::Shared => "shared",
311 HistoryVisibility::WorldReadable => "world_readable",
312 HistoryVisibility::Invited => "invited",
313 HistoryVisibility::Joined => "joined",
314 }
315 }
316
317 pub fn from_wire_name(s: &str) -> Option<Self> {
318 match s {
319 "shared" => Some(HistoryVisibility::Shared),
320 "world_readable" => Some(HistoryVisibility::WorldReadable),
321 "invited" => Some(HistoryVisibility::Invited),
322 "joined" => Some(HistoryVisibility::Joined),
323 _ => None,
324 }
325 }
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329pub enum Membership {
330 Join,
331 Invite,
332 Leave,
333 Ban,
334}
335
336impl Membership {
337 pub fn as_str(self) -> &'static str {
338 match self {
339 Membership::Join => "join",
340 Membership::Invite => "invite",
341 Membership::Leave => "leave",
342 Membership::Ban => "ban",
343 }
344 }
345
346 pub fn from_wire_name(s: &str) -> Option<Self> {
347 match s {
348 "join" => Some(Membership::Join),
349 "invite" => Some(Membership::Invite),
350 "leave" => Some(Membership::Leave),
351 "ban" => Some(Membership::Ban),
352 _ => None,
353 }
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub enum ReceiptType {
359 Read,
360 ReadPrivate,
361}
362
363impl ReceiptType {
364 pub fn as_str(self) -> &'static str {
365 match self {
366 ReceiptType::Read => "m.read",
367 ReceiptType::ReadPrivate => "m.read.private",
368 }
369 }
370
371 pub fn from_wire_name(s: &str) -> Option<Self> {
372 match s {
373 "m.read" => Some(ReceiptType::Read),
374 "m.read.private" => Some(ReceiptType::ReadPrivate),
375 _ => None,
376 }
377 }
378}
379
380fn decode_enum<T>(idx: usize, column: &'static str, raw: &str, parse: fn(&str) -> Option<T>) -> rusqlite::Result<T> {
386 parse(raw).ok_or_else(|| rusqlite::Error::InvalidColumnType(idx, column.to_string(), rusqlite::types::Type::Text))
387}
388
389pub fn create_matrix_schema(conn: &Connection) -> rusqlite::Result<()> {
406 conn.execute_batch(
407 r#"
408 -- Global stream ordering — see this module's doc comment on the
409 -- single-writer guarantee that makes `UPDATE ... RETURNING` safe.
410 CREATE TABLE IF NOT EXISTS stream_counter (
411 id INTEGER PRIMARY KEY CHECK (id = 1),
412 value INTEGER NOT NULL
413 );
414 INSERT OR IGNORE INTO stream_counter (id, value) VALUES (1, 0);
415
416 -- Federation F0: this server's signing keys and the verify keys
417 -- cached from remote servers. Secrets live only in the (encrypted) DB.
418 CREATE TABLE IF NOT EXISTS fed_signing_keys (
419 key_id TEXT PRIMARY KEY,
420 secret BLOB NOT NULL,
421 created_ms INTEGER NOT NULL,
422 retired_ms INTEGER
423 );
424 CREATE TABLE IF NOT EXISTS fed_remote_keys (
425 server_name TEXT NOT NULL,
426 key_id TEXT NOT NULL,
427 public_key TEXT NOT NULL,
428 valid_until_ms INTEGER NOT NULL,
429 fetched_ms INTEGER NOT NULL,
430 PRIMARY KEY (server_name, key_id)
431 );
432
433 -- user_id -> mxid, filled on first touch by ensure_matrix_user
434 -- (plan §2 manager decision: new table, not in the original DDL
435 -- text). public_id is immutable, so this mapping never changes.
436 CREATE TABLE IF NOT EXISTS matrix_users (
437 user_id INTEGER PRIMARY KEY,
438 mxid TEXT NOT NULL UNIQUE,
439 created_at TEXT NOT NULL,
440 nick TEXT
441 );
442 CREATE UNIQUE INDEX IF NOT EXISTS idx_matrix_users_nick_lower
443 ON matrix_users(LOWER(nick)) WHERE nick IS NOT NULL;
444
445 -- Nick belongs to a session, not to matrix_users and not to the
446 -- device. device_id is only a mark. One device may have many sessions.
447 -- matrix_users.nick stays for old databases and is not read.
448 CREATE TABLE IF NOT EXISTS messenger_sessions (
449 session_id TEXT PRIMARY KEY,
450 user_id INTEGER NOT NULL,
451 device_id TEXT NOT NULL,
452 nick TEXT NOT NULL
453 );
454 CREATE INDEX IF NOT EXISTS idx_messenger_sessions_user
455 ON messenger_sessions(user_id);
456 CREATE UNIQUE INDEX IF NOT EXISTS idx_messenger_sessions_nick_lower
457 ON messenger_sessions(LOWER(nick));
458
459 CREATE TABLE IF NOT EXISTS rooms (
460 id TEXT PRIMARY KEY,
461 kind TEXT NOT NULL,
462 room_version TEXT NOT NULL DEFAULT '11',
463 creator_user_id INTEGER NOT NULL,
464 created_at TEXT NOT NULL,
465 is_encrypted INTEGER NOT NULL DEFAULT 0,
466 join_rule TEXT NOT NULL DEFAULT 'invite',
467 history_visibility TEXT NOT NULL DEFAULT 'shared',
468 dm_pair_key TEXT UNIQUE,
469 legacy_dm_id INTEGER UNIQUE
470 );
471 CREATE INDEX IF NOT EXISTS idx_rooms_kind ON rooms(kind);
472
473 CREATE TABLE IF NOT EXISTS events (
474 stream_id INTEGER PRIMARY KEY,
475 event_id TEXT NOT NULL UNIQUE,
476 room_id TEXT NOT NULL REFERENCES rooms(id),
477 sender_user_id INTEGER NOT NULL,
478 event_type TEXT NOT NULL,
479 state_key TEXT,
480 content TEXT NOT NULL,
481 origin_server_ts INTEGER NOT NULL,
482 txn_id TEXT,
483 redacts TEXT REFERENCES events(event_id),
484 redacted_by TEXT REFERENCES events(event_id)
485 );
486 CREATE INDEX IF NOT EXISTS idx_events_room_stream ON events(room_id, stream_id);
487 CREATE INDEX IF NOT EXISTS idx_events_room_type_state ON events(room_id, event_type, state_key);
488 CREATE INDEX IF NOT EXISTS idx_events_sender ON events(sender_user_id, stream_id);
489
490 CREATE TABLE IF NOT EXISTS current_state (
491 room_id TEXT NOT NULL REFERENCES rooms(id),
492 event_type TEXT NOT NULL,
493 state_key TEXT NOT NULL,
494 event_id TEXT NOT NULL REFERENCES events(event_id),
495 PRIMARY KEY (room_id, event_type, state_key)
496 );
497
498 CREATE TABLE IF NOT EXISTS room_members (
499 room_id TEXT NOT NULL REFERENCES rooms(id),
500 user_id INTEGER NOT NULL,
501 membership TEXT NOT NULL,
502 power_level INTEGER,
503 updated_at TEXT NOT NULL,
504 PRIMARY KEY (room_id, user_id)
505 );
506 CREATE INDEX IF NOT EXISTS idx_room_members_user ON room_members(user_id, membership);
507
508 CREATE TABLE IF NOT EXISTS relations (
509 event_id TEXT PRIMARY KEY REFERENCES events(event_id),
510 room_id TEXT NOT NULL REFERENCES rooms(id),
511 rel_type TEXT NOT NULL,
512 target_id TEXT NOT NULL REFERENCES events(event_id),
513 agg_key TEXT
514 );
515 CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id, rel_type);
516
517 CREATE TABLE IF NOT EXISTS receipts (
518 room_id TEXT NOT NULL REFERENCES rooms(id),
519 user_id INTEGER NOT NULL,
520 receipt_type TEXT NOT NULL,
521 event_id TEXT NOT NULL REFERENCES events(event_id),
522 ts INTEGER NOT NULL,
523 stream_id INTEGER NOT NULL,
524 PRIMARY KEY (room_id, user_id, receipt_type)
525 );
526 CREATE INDEX IF NOT EXISTS idx_receipts_room_stream ON receipts(room_id, stream_id);
527
528 -- DEVIATION from plan §2's literal text: room_id has no
529 -- `REFERENCES rooms(id)` — see this function's doc comment.
530 CREATE TABLE IF NOT EXISTS account_data (
531 user_id INTEGER NOT NULL,
532 room_id TEXT NOT NULL DEFAULT '',
533 data_type TEXT NOT NULL,
534 content TEXT NOT NULL,
535 stream_id INTEGER NOT NULL,
536 PRIMARY KEY (user_id, room_id, data_type)
537 );
538 CREATE INDEX IF NOT EXISTS idx_account_data_user_stream ON account_data(user_id, stream_id);
539
540 CREATE TABLE IF NOT EXISTS txn_dedup (
541 user_id INTEGER NOT NULL,
542 device_id TEXT NOT NULL,
543 txn_id TEXT NOT NULL,
544 event_id TEXT REFERENCES events(event_id),
545 created_at TEXT NOT NULL,
546 PRIMARY KEY (user_id, device_id, txn_id)
547 );
548
549 CREATE TABLE IF NOT EXISTS filters (
550 id INTEGER PRIMARY KEY AUTOINCREMENT,
551 user_id INTEGER NOT NULL,
552 definition TEXT NOT NULL
553 );
554
555 -- legacy_dm_message_map removed (M2); drop_legacy_dm_scaffold_if_empty cleans old DBs
556 "#,
557 )?;
558 crate::public_channels::create_public_schema(conn)?;
560 crate::public_forum::create_forum_schema(conn)?;
561 crate::media::create_media_schema(conn)?;
562 crate::fed_rooms::create_fed_schema(conn)?;
563 crate::dag_schema::create_dag_schema(conn)?;
564 crate::http::extras::create_schema(conn)?;
565 crate::identities::create_identities_schema(conn)
566}
567
568pub fn parse_db_key(key_hex: &str) -> Result<[u8; 32], String> {
570 let h = key_hex.trim();
571 if h.len() != 64 || !h.bytes().all(|b| b.is_ascii_hexdigit()) {
572 return Err("database key must be 64 hex characters (32 bytes)".into());
573 }
574 let mut key = [0u8; 32];
575 for (i, b) in key.iter_mut().enumerate() {
576 *b = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).map_err(|_| "database key must be hex".to_string())?;
577 }
578 Ok(key)
579}
580
581pub fn messenger_db_config(path: &str, key_hex: &str) -> Result<tesserax_store::DbConfig, String> {
583 let key = parse_db_key(key_hex)?;
584 Ok(tesserax_store::DbConfig::encrypted_native(path, std::sync::Arc::new(tesserax_store::keysource::StaticKeySource(key))))
585}
586
587pub fn open_messenger_db(path: &str, key_hex: &str) -> Result<tesserax_store::Db, String> {
590 let cfg = messenger_db_config(path, key_hex)?;
591 let db = tesserax_store::Db::open(&cfg).map_err(|e| e.to_string())?;
592 db.blocking(|conn| ensure_schema(conn)).map_err(|e| e.to_string())?;
593 Ok(db)
594}
595
596pub fn open_read_pool(path: &str, key_hex: &str, size: usize) -> Result<tesserax_store::ReadPool, String> {
598 let cfg = messenger_db_config(path, key_hex)?;
599 tesserax_store::ReadPoolConfig::from_config(cfg).pool_size(size.max(1)).open().map_err(|e| e.to_string())
600}
601
602pub fn ensure_schema(conn: &Connection) -> rusqlite::Result<()> {
604 create_matrix_schema(conn)?;
605 crate::keys::create_matrix_keys_schema(conn)?;
606 crate::retention::create_retention_schema(conn)?;
607 crate::public_channels::create_public_schema(conn)?;
608 Ok(())
609}
610
611pub(crate) fn next_stream_id(tx: &Transaction) -> rusqlite::Result<i64> {
623 tx.query_row("UPDATE stream_counter SET value = value + 1 WHERE id = 1 RETURNING value", [], |row| row.get(0))
624}
625
626pub fn max_stream_id(conn: &Connection) -> rusqlite::Result<i64> {
630 conn.query_row("SELECT value FROM stream_counter WHERE id = 1", [], |row| row.get(0))
631}
632
633pub fn ensure_matrix_user(conn: &Connection, user_id: i64, public_id: &str, now: &str) -> Result<String, MatrixStoreError> {
641 if is_reserved_localpart(public_id) {
642 return Err(MatrixStoreError::ReservedLocalpart);
643 }
644 let mxid = mxid_for_public_id(public_id);
645 conn.execute(
646 "INSERT INTO matrix_users (user_id, mxid, created_at) VALUES (?1, ?2, ?3)
647 ON CONFLICT(user_id) DO NOTHING",
648 params![user_id, mxid, now],
649 )?;
650 Ok(mxid)
651}
652
653pub fn mxid_of(conn: &Connection, user_id: i64) -> rusqlite::Result<Option<String>> {
655 conn.query_row("SELECT mxid FROM matrix_users WHERE user_id = ?1", params![user_id], |row| row.get(0))
656 .optional()
657}
658
659pub fn user_id_of(conn: &Connection, mxid: &str) -> rusqlite::Result<Option<i64>> {
661 conn.query_row("SELECT user_id FROM matrix_users WHERE mxid = ?1", params![mxid], |row| row.get(0))
662 .optional()
663}
664
665#[derive(Debug, Clone, PartialEq)]
670pub struct Room {
671 pub id: String,
672 pub kind: RoomKind,
673 pub room_version: String,
674 pub creator_user_id: i64,
675 pub created_at: String,
676 pub is_encrypted: bool,
677 pub join_rule: JoinRule,
678 pub history_visibility: HistoryVisibility,
679 pub dm_pair_key: Option<String>,
680 pub legacy_dm_id: Option<i64>,
681}
682
683const ROOM_SELECT_COLUMNS: &str =
684 "id, kind, room_version, creator_user_id, created_at, is_encrypted, join_rule, history_visibility, dm_pair_key, legacy_dm_id";
685
686fn room_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Room> {
687 let kind_raw: String = row.get(1)?;
688 let join_rule_raw: String = row.get(6)?;
689 let history_visibility_raw: String = row.get(7)?;
690 Ok(Room {
691 id: row.get(0)?,
692 kind: decode_enum(1, "kind", &kind_raw, RoomKind::from_wire_name)?,
693 room_version: row.get(2)?,
694 creator_user_id: row.get(3)?,
695 created_at: row.get(4)?,
696 is_encrypted: row.get(5)?,
697 join_rule: decode_enum(6, "join_rule", &join_rule_raw, JoinRule::from_wire_name)?,
698 history_visibility: decode_enum(7, "history_visibility", &history_visibility_raw, HistoryVisibility::from_wire_name)?,
699 dm_pair_key: row.get(8)?,
700 legacy_dm_id: row.get(9)?,
701 })
702}
703
704#[allow(clippy::too_many_arguments)]
711fn insert_room_row(
712 conn: &Connection,
713 room_id: &str,
714 kind: RoomKind,
715 creator_user_id: i64,
716 created_at: &str,
717 is_encrypted: bool,
718 join_rule: JoinRule,
719 history_visibility: HistoryVisibility,
720 dm_pair_key: Option<&str>,
721 legacy_dm_id: Option<i64>,
722) -> rusqlite::Result<()> {
723 conn.execute(
724 "INSERT INTO rooms (id, kind, room_version, creator_user_id, created_at, is_encrypted, join_rule, history_visibility, dm_pair_key, legacy_dm_id)
725 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
726 params![
727 room_id,
728 kind.as_str(),
729 MATRIX_ROOM_VERSION,
730 creator_user_id,
731 created_at,
732 is_encrypted,
733 join_rule.as_str(),
734 history_visibility.as_str(),
735 dm_pair_key,
736 legacy_dm_id,
737 ],
738 )?;
739 Ok(())
740}
741
742#[allow(clippy::too_many_arguments)]
749pub fn create_room(
750 conn: &Connection,
751 room_id: &str,
752 kind: RoomKind,
753 creator_user_id: i64,
754 created_at: &str,
755 is_encrypted: bool,
756 join_rule: JoinRule,
757 history_visibility: HistoryVisibility,
758 dm_pair_key: Option<&str>,
759 legacy_dm_id: Option<i64>,
760) -> rusqlite::Result<()> {
761 insert_room_row(conn, room_id, kind, creator_user_id, created_at, is_encrypted, join_rule, history_visibility, dm_pair_key, legacy_dm_id)
762}
763
764pub fn get_room(conn: &Connection, room_id: &str) -> rusqlite::Result<Option<Room>> {
765 conn.query_row(&format!("SELECT {ROOM_SELECT_COLUMNS} FROM rooms WHERE id = ?1"), params![room_id], room_from_row)
766 .optional()
767}
768
769pub fn room_by_dm_pair_key(conn: &Connection, pair_key: &str) -> rusqlite::Result<Option<Room>> {
774 conn.query_row(&format!("SELECT {ROOM_SELECT_COLUMNS} FROM rooms WHERE dm_pair_key = ?1"), params![pair_key], room_from_row)
775 .optional()
776}
777
778pub fn room_by_legacy_dm_id(conn: &Connection, legacy_dm_id: i64) -> rusqlite::Result<Option<Room>> {
783 conn.query_row(&format!("SELECT {ROOM_SELECT_COLUMNS} FROM rooms WHERE legacy_dm_id = ?1"), params![legacy_dm_id], room_from_row)
784 .optional()
785}
786
787pub fn clear_dm_pair_key(conn: &Connection, room_id: &str) -> rusqlite::Result<()> {
793 conn.execute("UPDATE rooms SET dm_pair_key = NULL WHERE id = ?1", params![room_id])?;
794 Ok(())
795}
796
797#[derive(Debug, Clone, PartialEq)]
802pub struct MatrixEvent {
803 pub stream_id: i64,
804 pub event_id: String,
805 pub room_id: String,
806 pub sender_user_id: i64,
807 pub event_type: String,
808 pub state_key: Option<String>,
810 pub content: String,
811 pub origin_server_ts: i64,
812 pub txn_id: Option<String>,
813 pub redacts: Option<String>,
814 pub redacted_by: Option<String>,
815}
816
817const EVENT_SELECT_COLUMNS: &str =
818 "stream_id, event_id, room_id, sender_user_id, event_type, state_key, content, origin_server_ts, txn_id, redacts, redacted_by";
819
820const EVENT_SELECT_COLUMNS_ALIASED: &str = "e.stream_id, e.event_id, e.room_id, e.sender_user_id, e.event_type, e.state_key, e.content, e.origin_server_ts, e.txn_id, e.redacts, e.redacted_by";
821
822fn event_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<MatrixEvent> {
823 Ok(MatrixEvent {
824 stream_id: row.get(0)?,
825 event_id: row.get(1)?,
826 room_id: row.get(2)?,
827 sender_user_id: row.get(3)?,
828 event_type: row.get(4)?,
829 state_key: row.get(5)?,
830 content: row.get(6)?,
831 origin_server_ts: row.get(7)?,
832 txn_id: row.get(8)?,
833 redacts: row.get(9)?,
834 redacted_by: row.get(10)?,
835 })
836}
837
838fn collect_events(rows: &mut rusqlite::Rows<'_>) -> rusqlite::Result<Vec<MatrixEvent>> {
839 let mut out = Vec::new();
840 while let Some(row) = rows.next()? {
841 out.push(event_from_row(row)?);
842 }
843 Ok(out)
844}
845
846#[derive(Debug, Clone, Copy)]
850pub(crate) struct TimelineEventRow<'a> {
851 pub(crate) event_id: &'a str,
852 pub(crate) room_id: &'a str,
853 pub(crate) sender_user_id: i64,
854 pub(crate) event_type: &'a str,
855 pub(crate) content: &'a str,
856 pub(crate) origin_server_ts: i64,
857 pub(crate) txn_id: Option<&'a str>,
858}
859
860fn insert_timeline_event_in_tx(tx: &Transaction, row: &TimelineEventRow<'_>) -> Result<MatrixEvent, MatrixStoreError> {
869 #[cfg(feature = "f3-hash-ids")]
872 if let Some(p) = crate::f3::prepare_local(tx, row.room_id, row.sender_user_id, row.event_type, None, row.content, row.origin_server_ts)? {
873 let row = TimelineEventRow { event_id: &p.event_id, content: &p.content, ..*row };
874 return insert_timeline_event_raw_in_tx(tx, &row);
875 }
876 insert_timeline_event_raw_in_tx(tx, row)
877}
878
879pub(crate) fn insert_timeline_event_raw_in_tx(tx: &Transaction, row: &TimelineEventRow<'_>) -> Result<MatrixEvent, MatrixStoreError> {
881 let TimelineEventRow { event_id, room_id, sender_user_id, event_type, content, origin_server_ts, txn_id } = *row;
882 let stream_id = next_stream_id(tx)?;
883 tx.execute(
884 "INSERT INTO events (stream_id, event_id, room_id, sender_user_id, event_type, state_key, content, origin_server_ts, txn_id)
885 VALUES (?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, ?8)",
886 params![stream_id, event_id, room_id, sender_user_id, event_type, content, origin_server_ts, txn_id],
887 )?;
888 populate_relations(tx, event_id, room_id, sender_user_id, content)?;
889 Ok(MatrixEvent {
890 stream_id,
891 event_id: event_id.to_string(),
892 room_id: room_id.to_string(),
893 sender_user_id,
894 event_type: event_type.to_string(),
895 state_key: None,
896 content: content.to_string(),
897 origin_server_ts,
898 txn_id: txn_id.map(str::to_string),
899 redacts: None,
900 redacted_by: None,
901 })
902}
903
904pub fn insert_timeline_event(
913 conn: &mut Connection,
914 event_id: &str,
915 room_id: &str,
916 sender_user_id: i64,
917 event_type: &str,
918 content: &str,
919 origin_server_ts: i64,
920) -> Result<MatrixEvent, MatrixStoreError> {
921 let tx = conn.transaction()?;
922 let row = TimelineEventRow { event_id, room_id, sender_user_id, event_type, content, origin_server_ts, txn_id: None };
923 let event = insert_timeline_event_in_tx(&tx, &row)?;
924 tx.commit()?;
925 Ok(event)
926}
927
928#[derive(Debug, Clone, PartialEq)]
934pub enum DedupedWrite {
935 New(MatrixEvent),
936 Existing(MatrixEvent),
937}
938
939#[allow(clippy::too_many_arguments)]
949pub fn insert_timeline_event_deduped(
950 conn: &mut Connection,
951 device_id: &str,
952 txn_id: &str,
953 event_id: &str,
954 room_id: &str,
955 sender_user_id: i64,
956 event_type: &str,
957 content: &str,
958 origin_server_ts: i64,
959 now: &str,
960) -> Result<DedupedWrite, MatrixStoreError> {
961 let tx = conn.transaction()?;
962 if let TxnDedupEntry::Seen(existing_event_id) = txn_dedup_lookup(&tx, sender_user_id, device_id, txn_id)? {
963 let existing_event_id = existing_event_id.ok_or_else(|| MatrixStoreError::UnknownEventId(txn_id.to_string()))?;
964 let event = get_event(&tx, &existing_event_id)?.ok_or_else(|| MatrixStoreError::UnknownEventId(existing_event_id.clone()))?;
965 tx.commit()?;
966 return Ok(DedupedWrite::Existing(event));
967 }
968 let row = TimelineEventRow { event_id, room_id, sender_user_id, event_type, content, origin_server_ts, txn_id: Some(txn_id) };
969 let event = insert_timeline_event_in_tx(&tx, &row)?;
970 txn_dedup_record(&tx, sender_user_id, device_id, txn_id, Some(&event.event_id), now)?;
971 tx.commit()?;
972 Ok(DedupedWrite::New(event))
973}
974
975#[allow(clippy::too_many_arguments)]
983fn apply_state_event_in_tx(
984 tx: &Transaction,
985 event_id: &str,
986 room_id: &str,
987 sender_user_id: i64,
988 event_type: &str,
989 state_key: &str,
990 content: &str,
991 origin_server_ts: i64,
992 now: &str,
993) -> Result<MatrixEvent, MatrixStoreError> {
994 #[cfg(feature = "f3-hash-ids")]
997 if let Some(p) = crate::f3::prepare_local(tx, room_id, sender_user_id, event_type, Some(state_key), content, origin_server_ts)? {
998 return apply_state_event_raw_in_tx(tx, &p.event_id, room_id, sender_user_id, event_type, state_key, &p.content, origin_server_ts, now);
999 }
1000 apply_state_event_raw_in_tx(tx, event_id, room_id, sender_user_id, event_type, state_key, content, origin_server_ts, now)
1001}
1002
1003#[allow(clippy::too_many_arguments)]
1005pub(crate) fn apply_state_event_raw_in_tx(
1006 tx: &Transaction,
1007 event_id: &str,
1008 room_id: &str,
1009 sender_user_id: i64,
1010 event_type: &str,
1011 state_key: &str,
1012 content: &str,
1013 origin_server_ts: i64,
1014 now: &str,
1015) -> Result<MatrixEvent, MatrixStoreError> {
1016 let stream_id = next_stream_id(tx)?;
1017 tx.execute(
1018 "INSERT INTO events (stream_id, event_id, room_id, sender_user_id, event_type, state_key, content, origin_server_ts, txn_id)
1019 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL)",
1020 params![stream_id, event_id, room_id, sender_user_id, event_type, state_key, content, origin_server_ts],
1021 )?;
1022 tx.execute(
1023 "INSERT INTO current_state (room_id, event_type, state_key, event_id) VALUES (?1, ?2, ?3, ?4)
1024 ON CONFLICT(room_id, event_type, state_key) DO UPDATE SET event_id = excluded.event_id",
1025 params![room_id, event_type, state_key, event_id],
1026 )?;
1027 if event_type == "m.room.member" {
1028 refresh_room_member(tx, room_id, state_key, content, now)?;
1029 }
1030 if event_type == "m.room.power_levels" {
1031 refresh_power_levels(tx, room_id, content)?;
1032 }
1033 Ok(MatrixEvent {
1034 stream_id,
1035 event_id: event_id.to_string(),
1036 room_id: room_id.to_string(),
1037 sender_user_id,
1038 event_type: event_type.to_string(),
1039 state_key: Some(state_key.to_string()),
1040 content: content.to_string(),
1041 origin_server_ts,
1042 txn_id: None,
1043 redacts: None,
1044 redacted_by: None,
1045 })
1046}
1047
1048#[cfg_attr(not(feature = "f3-hash-ids"), allow(dead_code))]
1051#[allow(clippy::too_many_arguments)]
1052pub(crate) fn insert_past_state_row(tx: &Transaction, event_id: &str, room_id: &str, sender_user_id: i64, event_type: &str, state_key: &str, content: &str, origin_server_ts: i64) -> Result<(), MatrixStoreError> {
1053 let stream_id = next_stream_id(tx)?;
1054 tx.execute(
1055 "INSERT INTO events (stream_id, event_id, room_id, sender_user_id, event_type, state_key, content, origin_server_ts, txn_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL)",
1056 params![stream_id, event_id, room_id, sender_user_id, event_type, state_key, content, origin_server_ts],
1057 )?;
1058 Ok(())
1059}
1060
1061#[cfg_attr(not(feature = "f3-hash-ids"), allow(dead_code))]
1065pub(crate) fn set_current_state_slot(tx: &Transaction, room_id: &str, event_type: &str, state_key: &str, event_id: &str, now: &str) -> Result<(), MatrixStoreError> {
1066 let content: String = tx.query_row("SELECT content FROM events WHERE event_id = ?1", params![event_id], |r| r.get(0))?;
1067 tx.execute(
1068 "INSERT INTO current_state (room_id, event_type, state_key, event_id) VALUES (?1, ?2, ?3, ?4)
1069 ON CONFLICT(room_id, event_type, state_key) DO UPDATE SET event_id = excluded.event_id",
1070 params![room_id, event_type, state_key, event_id],
1071 )?;
1072 if event_type == "m.room.member" {
1073 refresh_room_member(tx, room_id, state_key, &content, now)?;
1074 }
1075 if event_type == "m.room.power_levels" {
1076 refresh_power_levels(tx, room_id, &content)?;
1077 }
1078 Ok(())
1079}
1080
1081#[derive(Debug, Clone, Copy)]
1086pub struct StateEventWrite<'a> {
1087 pub event_id: &'a str,
1088 pub room_id: &'a str,
1089 pub sender_user_id: i64,
1090 pub event_type: &'a str,
1091 pub state_key: &'a str,
1092 pub content: &'a str,
1093 pub origin_server_ts: i64,
1094 pub now: &'a str,
1095}
1096
1097pub fn apply_state_event(conn: &mut Connection, write: &StateEventWrite<'_>) -> Result<MatrixEvent, MatrixStoreError> {
1102 let tx = conn.transaction()?;
1103 let event = apply_state_event_in_tx(
1104 &tx,
1105 write.event_id,
1106 write.room_id,
1107 write.sender_user_id,
1108 write.event_type,
1109 write.state_key,
1110 write.content,
1111 write.origin_server_ts,
1112 write.now,
1113 )?;
1114 tx.commit()?;
1115 Ok(event)
1116}
1117
1118#[derive(Debug, Clone, Default, PartialEq, Eq)]
1120pub struct DisplaynameRefresh {
1121 pub rooms_updated: usize,
1123 pub affected_user_ids: HashSet<i64>,
1126}
1127
1128pub fn refresh_member_displayname(
1146 conn: &mut Connection,
1147 user_id: i64,
1148 displayname: &str,
1149 now: &str,
1150 origin_server_ts: i64,
1151) -> Result<DisplaynameRefresh, MatrixStoreError> {
1152 let mut outcome = DisplaynameRefresh::default();
1153 if displayname.is_empty() {
1154 return Ok(outcome);
1155 }
1156 let Some(mxid) = mxid_of(conn, user_id)? else {
1157 return Ok(outcome);
1158 };
1159
1160 let tx = conn.transaction()?;
1161 for membership in [Membership::Join, Membership::Invite] {
1162 for room_id in rooms_for_user(&tx, user_id, Some(membership))? {
1163 let Some(current) = current_state_event(&tx, &room_id, "m.room.member", &mxid)? else {
1164 continue;
1165 };
1166 let mut content: serde_json::Value = serde_json::from_str(¤t.content)?;
1167 if content.get("displayname").and_then(|v| v.as_str()) == Some(displayname) {
1168 continue;
1169 }
1170 let Some(fields) = content.as_object_mut() else {
1171 continue;
1172 };
1173 fields.insert("displayname".to_string(), serde_json::Value::String(displayname.to_string()));
1174
1175 let sender_user_id = if membership == Membership::Join { user_id } else { current.sender_user_id };
1176 apply_state_event_in_tx(
1177 &tx,
1178 &new_event_id(),
1179 &room_id,
1180 sender_user_id,
1181 "m.room.member",
1182 &mxid,
1183 &content.to_string(),
1184 origin_server_ts,
1185 now,
1186 )?;
1187 outcome.rooms_updated += 1;
1188 outcome.affected_user_ids.insert(user_id);
1189 for member in room_members(&tx, &room_id, None)? {
1190 if matches!(member.membership, Membership::Join | Membership::Invite) {
1191 outcome.affected_user_ids.insert(member.user_id);
1192 }
1193 }
1194 }
1195 }
1196 tx.commit()?;
1197 Ok(outcome)
1198}
1199
1200pub fn matrix_user_ids(conn: &Connection) -> rusqlite::Result<Vec<i64>> {
1204 let mut stmt = conn.prepare("SELECT user_id FROM matrix_users ORDER BY user_id")?;
1205 let rows = stmt.query_map([], |row| row.get(0))?;
1206 rows.collect()
1207}
1208
1209#[derive(Debug, Clone, PartialEq)]
1219pub struct NewStateEvent {
1220 pub event_id: String,
1221 pub sender_user_id: i64,
1222 pub event_type: String,
1223 pub state_key: String,
1224 pub content: String,
1225}
1226
1227#[derive(Debug, Clone, Copy)]
1233pub struct RoomBootstrap<'a> {
1234 pub room_id: &'a str,
1235 pub kind: RoomKind,
1236 pub creator_user_id: i64,
1237 pub created_at: &'a str,
1238 pub is_encrypted: bool,
1239 pub join_rule: JoinRule,
1240 pub history_visibility: HistoryVisibility,
1241 pub dm_pair_key: Option<&'a str>,
1242 pub legacy_dm_id: Option<i64>,
1243}
1244
1245pub fn create_room_with_state(
1256 conn: &mut Connection,
1257 bootstrap: RoomBootstrap<'_>,
1258 state_events: &[NewStateEvent],
1259 origin_server_ts: i64,
1260) -> Result<(Room, Vec<MatrixEvent>), MatrixStoreError> {
1261 let tx = conn.transaction()?;
1262 insert_room_row(
1263 &tx,
1264 bootstrap.room_id,
1265 bootstrap.kind,
1266 bootstrap.creator_user_id,
1267 bootstrap.created_at,
1268 bootstrap.is_encrypted,
1269 bootstrap.join_rule,
1270 bootstrap.history_visibility,
1271 bootstrap.dm_pair_key,
1272 bootstrap.legacy_dm_id,
1273 )?;
1274 #[cfg(feature = "f3-hash-ids")]
1277 if !(bootstrap.kind == RoomKind::Channel && !bootstrap.is_encrypted) {
1278 crate::f3::mark_room(&tx, bootstrap.room_id)?;
1279 }
1280
1281 let mut applied = Vec::with_capacity(state_events.len());
1282 for event in state_events {
1283 applied.push(apply_state_event_in_tx(
1284 &tx,
1285 &event.event_id,
1286 bootstrap.room_id,
1287 event.sender_user_id,
1288 &event.event_type,
1289 &event.state_key,
1290 &event.content,
1291 origin_server_ts,
1292 bootstrap.created_at,
1293 )?);
1294 }
1295 tx.commit()?;
1296
1297 Ok((
1298 Room {
1299 id: bootstrap.room_id.to_string(),
1300 kind: bootstrap.kind,
1301 room_version: MATRIX_ROOM_VERSION.to_string(),
1302 creator_user_id: bootstrap.creator_user_id,
1303 created_at: bootstrap.created_at.to_string(),
1304 is_encrypted: bootstrap.is_encrypted,
1305 join_rule: bootstrap.join_rule,
1306 history_visibility: bootstrap.history_visibility,
1307 dm_pair_key: bootstrap.dm_pair_key.map(str::to_string),
1308 legacy_dm_id: bootstrap.legacy_dm_id,
1309 },
1310 applied,
1311 ))
1312}
1313
1314pub(crate) fn refresh_room_member(tx: &Transaction, room_id: &str, state_key: &str, content: &str, now: &str) -> Result<(), MatrixStoreError> {
1322 let value: serde_json::Value = serde_json::from_str(content)?;
1323 let membership_str = value
1324 .get("membership")
1325 .and_then(|v| v.as_str())
1326 .ok_or_else(|| MatrixStoreError::InvalidMembership("missing 'membership' field".to_string()))?;
1327 let membership =
1328 Membership::from_wire_name(membership_str).ok_or_else(|| MatrixStoreError::InvalidMembership(membership_str.to_string()))?;
1329 let user_id = user_id_of(tx, state_key)?.ok_or_else(|| MatrixStoreError::UnknownMxid(state_key.to_string()))?;
1330 tx.execute(
1331 "INSERT INTO room_members (room_id, user_id, membership, power_level, updated_at)
1332 VALUES (?1, ?2, ?3, NULL, ?4)
1333 ON CONFLICT(room_id, user_id) DO UPDATE SET membership = excluded.membership, updated_at = excluded.updated_at",
1334 params![room_id, user_id, membership.as_str(), now],
1335 )?;
1336 Ok(())
1337}
1338
1339pub(crate) fn refresh_power_levels(tx: &Transaction, room_id: &str, content: &str) -> Result<(), MatrixStoreError> {
1348 let value: serde_json::Value = serde_json::from_str(content)?;
1349 tx.execute("UPDATE room_members SET power_level = NULL WHERE room_id = ?1", params![room_id])?;
1350 if let Some(users) = value.get("users").and_then(|v| v.as_object()) {
1351 for (mxid, level) in users {
1352 let Some(level) = level.as_i64() else { continue };
1353 let Some(user_id) = user_id_of(tx, mxid)? else { continue };
1354 tx.execute(
1355 "UPDATE room_members SET power_level = ?1 WHERE room_id = ?2 AND user_id = ?3",
1356 params![level, room_id, user_id],
1357 )?;
1358 }
1359 }
1360 Ok(())
1361}
1362
1363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1375pub enum PowerAction {
1376 Invite,
1377 Kick,
1378 Ban,
1379 Redact,
1380 StateDefault,
1381}
1382
1383impl PowerAction {
1384 fn field_and_default(self) -> (&'static str, i64) {
1385 match self {
1386 PowerAction::Invite => ("invite", 50),
1387 PowerAction::Kick => ("kick", 50),
1388 PowerAction::Ban => ("ban", 50),
1389 PowerAction::Redact => ("redact", 50),
1390 PowerAction::StateDefault => ("state_default", 50),
1391 }
1392 }
1393}
1394
1395pub fn user_level(power_levels: &serde_json::Value, mxid: &str) -> i64 {
1398 power_levels
1399 .get("users")
1400 .and_then(|users| users.get(mxid))
1401 .and_then(serde_json::Value::as_i64)
1402 .unwrap_or_else(|| power_levels.get("users_default").and_then(serde_json::Value::as_i64).unwrap_or(0))
1403}
1404
1405pub fn event_level(power_levels: &serde_json::Value, event_type: &str, is_state: bool) -> i64 {
1410 if let Some(level) = power_levels.get("events").and_then(|events| events.get(event_type)).and_then(serde_json::Value::as_i64) {
1411 return level;
1412 }
1413 let (key, default) = if is_state { ("state_default", 50) } else { ("events_default", 0) };
1414 power_levels.get(key).and_then(serde_json::Value::as_i64).unwrap_or(default)
1415}
1416
1417pub fn can(power_levels: &serde_json::Value, action: PowerAction, mxid: &str) -> bool {
1421 let (field, default) = action.field_and_default();
1422 let required = power_levels.get(field).and_then(serde_json::Value::as_i64).unwrap_or(default);
1423 user_level(power_levels, mxid) >= required
1424}
1425
1426pub fn can_act_on(power_levels: &serde_json::Value, action: PowerAction, sender_mxid: &str, target_mxid: &str, self_leave: bool) -> bool {
1439 if self_leave && sender_mxid == target_mxid {
1440 return true;
1441 }
1442 can(power_levels, action, sender_mxid) && user_level(power_levels, sender_mxid) > user_level(power_levels, target_mxid)
1443}
1444
1445fn power_levels_scalar_default(key: &str) -> i64 {
1450 match key {
1451 "events_default" | "users_default" => 0,
1452 _ => 50,
1453 }
1454}
1455
1456const POWER_LEVELS_SCALAR_KEYS: [&str; 7] = ["ban", "kick", "redact", "invite", "state_default", "events_default", "users_default"];
1461
1462fn reject_if_either_side_exceeds(old_value: Option<i64>, new_value: Option<i64>, sender_level: i64) -> Result<(), &'static str> {
1468 if old_value != new_value && (old_value.is_some_and(|v| v > sender_level) || new_value.is_some_and(|v| v > sender_level)) {
1469 return Err("cannot change a power-level field at or above your own level");
1470 }
1471 Ok(())
1472}
1473
1474pub fn validate_power_levels_change(old: &serde_json::Value, new: &serde_json::Value, sender_mxid: &str) -> Result<(), &'static str> {
1498 let sender_level = user_level(old, sender_mxid);
1499
1500 for key in POWER_LEVELS_SCALAR_KEYS {
1501 let default = power_levels_scalar_default(key);
1502 let old_value = old.get(key).and_then(serde_json::Value::as_i64).unwrap_or(default);
1503 let new_value = new.get(key).and_then(serde_json::Value::as_i64).unwrap_or(default);
1504 reject_if_either_side_exceeds(Some(old_value), Some(new_value), sender_level)?;
1505 }
1506
1507 let old_events = old.get("events").and_then(serde_json::Value::as_object);
1508 let new_events = new.get("events").and_then(serde_json::Value::as_object);
1509 let mut event_type_keys: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
1510 if let Some(map) = old_events {
1511 event_type_keys.extend(map.keys().map(String::as_str));
1512 }
1513 if let Some(map) = new_events {
1514 event_type_keys.extend(map.keys().map(String::as_str));
1515 }
1516 for event_type in event_type_keys {
1517 let old_value = old_events.and_then(|m| m.get(event_type)).and_then(serde_json::Value::as_i64);
1518 let new_value = new_events.and_then(|m| m.get(event_type)).and_then(serde_json::Value::as_i64);
1519 reject_if_either_side_exceeds(old_value, new_value, sender_level)?;
1520 }
1521
1522 let old_notif_room = old.get("notifications").and_then(|v| v.get("room")).and_then(serde_json::Value::as_i64);
1523 let new_notif_room = new.get("notifications").and_then(|v| v.get("room")).and_then(serde_json::Value::as_i64);
1524 reject_if_either_side_exceeds(old_notif_room, new_notif_room, sender_level)?;
1525
1526 let old_users = old.get("users").and_then(serde_json::Value::as_object);
1527 let new_users = new.get("users").and_then(serde_json::Value::as_object);
1528 let mut user_keys: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
1529 if let Some(map) = old_users {
1530 user_keys.extend(map.keys().map(String::as_str));
1531 }
1532 if let Some(map) = new_users {
1533 user_keys.extend(map.keys().map(String::as_str));
1534 }
1535 for target_mxid in user_keys {
1536 let old_effective = user_level(old, target_mxid);
1537 let new_effective = user_level(new, target_mxid);
1538 if old_effective == new_effective {
1539 continue;
1540 }
1541 if target_mxid != sender_mxid && old_effective >= sender_level {
1542 return Err("cannot change the level of a user at or above your own level");
1543 }
1544 if new_effective > sender_level {
1545 return Err("cannot set a user's level above your own");
1546 }
1547 }
1548
1549 Ok(())
1550}
1551
1552pub fn stripped_state_json(conn: &Connection, event: &MatrixEvent) -> Result<serde_json::Value, MatrixStoreError> {
1570 let sender = mxid_of(conn, event.sender_user_id)?.unwrap_or_default();
1571 let content: serde_json::Value = serde_json::from_str(&event.content)?;
1572 Ok(serde_json::json!({
1573 "content": content,
1574 "state_key": event.state_key.clone().unwrap_or_default(),
1575 "type": event.event_type,
1576 "sender": sender,
1577 }))
1578}
1579
1580pub fn stripped_invite_state(conn: &Connection, room_id: &str, inviter_user_id: i64) -> Result<Vec<serde_json::Value>, MatrixStoreError> {
1591 let mut out = Vec::new();
1592 for event_type in ["m.room.create", "m.room.join_rules", "m.room.encryption", "m.room.name"] {
1593 if let Some(event) = current_state_event(conn, room_id, event_type, "")? {
1594 out.push(stripped_state_json(conn, &event)?);
1595 }
1596 }
1597 if let Some(inviter_mxid) = mxid_of(conn, inviter_user_id)? {
1598 if let Some(event) = current_state_event(conn, room_id, "m.room.member", &inviter_mxid)? {
1599 out.push(stripped_state_json(conn, &event)?);
1600 }
1601 }
1602 Ok(out)
1603}
1604
1605pub fn get_event(conn: &Connection, event_id: &str) -> rusqlite::Result<Option<MatrixEvent>> {
1606 let closed = conn
1607 .query_row(&format!("SELECT {EVENT_SELECT_COLUMNS} FROM events WHERE event_id = ?1"), params![event_id], event_from_row)
1608 .optional()?;
1609 if closed.is_some() {
1610 return Ok(closed);
1611 }
1612 match crate::public_channels::get_event(conn, event_id) {
1614 Ok(found) => Ok(found),
1615 Err(rusqlite::Error::SqliteFailure(_, Some(msg))) if msg.contains("no such table") => Ok(None),
1616 Err(e) => Err(e),
1617 }
1618}
1619
1620pub fn current_state_event(conn: &Connection, room_id: &str, event_type: &str, state_key: &str) -> rusqlite::Result<Option<MatrixEvent>> {
1621 conn.query_row(
1622 &format!(
1623 "SELECT {EVENT_SELECT_COLUMNS_ALIASED} FROM current_state cs JOIN events e ON e.event_id = cs.event_id
1624 WHERE cs.room_id = ?1 AND cs.event_type = ?2 AND cs.state_key = ?3"
1625 ),
1626 params![room_id, event_type, state_key],
1627 event_from_row,
1628 )
1629 .optional()
1630}
1631
1632pub fn current_state_all(conn: &Connection, room_id: &str) -> rusqlite::Result<Vec<MatrixEvent>> {
1633 let mut stmt = conn.prepare(&format!(
1634 "SELECT {EVENT_SELECT_COLUMNS_ALIASED} FROM current_state cs JOIN events e ON e.event_id = cs.event_id WHERE cs.room_id = ?1"
1635 ))?;
1636 let mut rows = stmt.query(params![room_id])?;
1637 collect_events(&mut rows)
1638}
1639
1640pub fn state_events_of_type_at(conn: &Connection, room_id: &str, event_type: &str, at_stream_id: i64) -> rusqlite::Result<Vec<MatrixEvent>> {
1648 let mut stmt = conn.prepare(&format!(
1649 "SELECT {EVENT_SELECT_COLUMNS_ALIASED} FROM events e
1650 WHERE e.room_id = ?1 AND e.event_type = ?2 AND e.state_key IS NOT NULL AND e.stream_id <= ?3
1651 AND e.stream_id = (
1652 SELECT MAX(stream_id) FROM events e2
1653 WHERE e2.room_id = e.room_id AND e2.event_type = e.event_type AND e2.state_key = e.state_key AND e2.stream_id <= ?3
1654 )"
1655 ))?;
1656 let mut rows = stmt.query(params![room_id, event_type, at_stream_id])?;
1657 collect_events(&mut rows)
1658}
1659
1660pub fn member_state_changed_in_window(conn: &Connection, room_id: &str, since_exclusive: i64, upto_inclusive: i64) -> rusqlite::Result<Vec<MatrixEvent>> {
1670 let mut stmt = conn.prepare(&format!(
1671 "SELECT {EVENT_SELECT_COLUMNS_ALIASED} FROM events e
1672 WHERE e.room_id = ?1 AND e.event_type = 'm.room.member' AND e.state_key IS NOT NULL
1673 AND e.stream_id = (
1674 SELECT MAX(e2.stream_id) FROM events e2
1675 WHERE e2.room_id = e.room_id AND e2.event_type = 'm.room.member' AND e2.state_key = e.state_key AND e2.stream_id <= ?3
1676 )
1677 AND e.stream_id > ?2"
1678 ))?;
1679 let mut rows = stmt.query(params![room_id, since_exclusive, upto_inclusive])?;
1680 collect_events(&mut rows)
1681}
1682
1683pub fn non_member_state_changed_in_window(conn: &Connection, room_id: &str, since_exclusive: i64, upto_inclusive: i64) -> rusqlite::Result<Vec<MatrixEvent>> {
1692 let mut stmt = conn.prepare(&format!(
1693 "SELECT {EVENT_SELECT_COLUMNS_ALIASED} FROM events e
1694 WHERE e.room_id = ?1 AND e.event_type != 'm.room.member' AND e.state_key IS NOT NULL
1695 AND e.stream_id = (
1696 SELECT MAX(e2.stream_id) FROM events e2
1697 WHERE e2.room_id = e.room_id AND e2.event_type = e.event_type AND e2.state_key = e.state_key AND e2.stream_id <= ?3
1698 )
1699 AND e.stream_id > ?2"
1700 ))?;
1701 let mut rows = stmt.query(params![room_id, since_exclusive, upto_inclusive])?;
1702 collect_events(&mut rows)
1703}
1704
1705pub fn events_in_room_after(conn: &Connection, room_id: &str, since_stream: i64, limit: i64) -> rusqlite::Result<Vec<MatrixEvent>> {
1708 let mut stmt = conn.prepare(&format!(
1709 "SELECT {EVENT_SELECT_COLUMNS} FROM events WHERE room_id = ?1 AND stream_id > ?2 ORDER BY stream_id ASC LIMIT ?3"
1710 ))?;
1711 let mut rows = stmt.query(params![room_id, since_stream, limit])?;
1712 let mut out = collect_events(&mut rows)?;
1713 if crate::public_channels::is_public_room(conn, room_id)? {
1714 out.extend(crate::public_channels::events_after(conn, room_id, since_stream, limit)?);
1715 out.sort_by_key(|e| e.stream_id);
1716 out.truncate(limit.max(0) as usize);
1717 }
1718 Ok(out)
1719}
1720
1721pub fn events_in_room_before(conn: &Connection, room_id: &str, before_stream: i64, limit: i64) -> rusqlite::Result<Vec<MatrixEvent>> {
1725 let mut stmt = conn.prepare(&format!(
1726 "SELECT {EVENT_SELECT_COLUMNS} FROM events WHERE room_id = ?1 AND stream_id < ?2 ORDER BY stream_id DESC LIMIT ?3"
1727 ))?;
1728 let mut rows = stmt.query(params![room_id, before_stream, limit])?;
1729 let mut out = collect_events(&mut rows)?;
1730 if crate::public_channels::is_public_room(conn, room_id)? {
1731 out.extend(crate::public_channels::events_before(conn, room_id, before_stream, limit)?);
1732 out.sort_by_key(|e| std::cmp::Reverse(e.stream_id));
1733 out.truncate(limit.max(0) as usize);
1734 }
1735 Ok(out)
1736}
1737
1738fn populate_relations(tx: &Transaction, event_id: &str, room_id: &str, sender_user_id: i64, content: &str) -> Result<(), MatrixStoreError> {
1762 let Ok(value) = serde_json::from_str::<serde_json::Value>(content) else {
1763 return Ok(());
1764 };
1765 let Some(relates_to) = value.get("m.relates_to") else {
1766 return Ok(());
1767 };
1768
1769 let (rel_type, target_id, agg_key): (String, String, Option<String>) = if let Some(reply) = relates_to.get("m.in_reply_to") {
1770 match reply.get("event_id").and_then(|v| v.as_str()) {
1771 Some(target) => ("m.in_reply_to".to_string(), target.to_string(), None),
1772 None => return Ok(()),
1773 }
1774 } else {
1775 let rel_type = relates_to.get("rel_type").and_then(|v| v.as_str());
1776 let target = relates_to.get("event_id").and_then(|v| v.as_str());
1777 match (rel_type, target) {
1778 (Some(rt), Some(target)) => {
1779 let key = relates_to.get("key").and_then(|v| v.as_str()).map(str::to_string);
1780 (rt.to_string(), target.to_string(), key)
1781 }
1782 _ => return Ok(()),
1783 }
1784 };
1785
1786 let target_room: Option<String> = tx
1787 .query_row("SELECT room_id FROM events WHERE event_id = ?1", params![target_id], |row| row.get(0))
1788 .optional()?;
1789 match target_room {
1790 Some(ref found_room) if found_room == room_id => {}
1791 _ => return Err(MatrixStoreError::InvalidRelationTarget(target_id)),
1792 }
1793
1794 if rel_type == "m.annotation" {
1795 let duplicate: Option<i64> = tx
1796 .query_row(
1797 "SELECT 1 FROM relations r JOIN events e ON e.event_id = r.event_id
1798 WHERE r.target_id = ?1 AND r.rel_type = 'm.annotation' AND r.agg_key IS ?2 AND e.sender_user_id = ?3
1799 AND e.redacted_by IS NULL
1800 LIMIT 1",
1801 params![target_id, agg_key, sender_user_id],
1802 |row| row.get(0),
1803 )
1804 .optional()?;
1805 if duplicate.is_some() {
1806 return Err(MatrixStoreError::DuplicateAnnotation);
1807 }
1808 }
1809
1810 tx.execute(
1811 "INSERT INTO relations (event_id, room_id, rel_type, target_id, agg_key) VALUES (?1, ?2, ?3, ?4, ?5)",
1812 params![event_id, room_id, rel_type, target_id, agg_key],
1813 )?;
1814 Ok(())
1815}
1816
1817pub fn relations_of(
1823 conn: &Connection,
1824 target_event_id: &str,
1825 rel_type: Option<&str>,
1826 event_type: Option<&str>,
1827 before_stream: i64,
1828 limit: i64,
1829) -> rusqlite::Result<Vec<MatrixEvent>> {
1830 let mut sql = format!("SELECT {EVENT_SELECT_COLUMNS_ALIASED} FROM relations r JOIN events e ON e.event_id = r.event_id WHERE r.target_id = ? AND e.stream_id < ?");
1835 let mut values: Vec<&dyn rusqlite::ToSql> = vec![&target_event_id, &before_stream];
1836 if let Some(rt) = &rel_type {
1837 sql.push_str(" AND r.rel_type = ?");
1838 values.push(rt);
1839 }
1840 if let Some(et) = &event_type {
1841 sql.push_str(" AND e.event_type = ?");
1842 values.push(et);
1843 }
1844 sql.push_str(" ORDER BY e.stream_id DESC LIMIT ?");
1845 values.push(&limit);
1846
1847 let mut stmt = conn.prepare(&sql)?;
1848 let mut rows = stmt.query(values.as_slice())?;
1849 collect_events(&mut rows)
1850}
1851
1852fn redact_content_per_v11(event_type: &str, content: &str) -> Result<String, MatrixStoreError> {
1868 let value: serde_json::Value = serde_json::from_str(content)?;
1869 let obj = value.as_object().cloned().unwrap_or_default();
1870 let mut kept = serde_json::Map::new();
1871
1872 match event_type {
1873 "m.room.create" => kept = obj,
1874 "m.room.member" => {
1875 for key in ["membership", "join_authorised_via_users_server"] {
1876 if let Some(v) = obj.get(key) {
1877 kept.insert(key.to_string(), v.clone());
1878 }
1879 }
1880 if let Some(signed) = obj.get("third_party_invite").and_then(|v| v.get("signed")) {
1881 let mut third_party_invite = serde_json::Map::new();
1882 third_party_invite.insert("signed".to_string(), signed.clone());
1883 kept.insert("third_party_invite".to_string(), serde_json::Value::Object(third_party_invite));
1884 }
1885 }
1886 "m.room.join_rules" => {
1887 for key in ["join_rule", "allow"] {
1888 if let Some(v) = obj.get(key) {
1889 kept.insert(key.to_string(), v.clone());
1890 }
1891 }
1892 }
1893 "m.room.power_levels" => {
1894 for key in [
1895 "ban",
1896 "events",
1897 "events_default",
1898 "invite",
1899 "kick",
1900 "redact",
1901 "state_default",
1902 "users",
1903 "users_default",
1904 ] {
1905 if let Some(v) = obj.get(key) {
1906 kept.insert(key.to_string(), v.clone());
1907 }
1908 }
1909 }
1910 "m.room.history_visibility" => {
1911 if let Some(v) = obj.get("history_visibility") {
1912 kept.insert("history_visibility".to_string(), v.clone());
1913 }
1914 }
1915 _ => {}
1916 }
1917
1918 Ok(serde_json::Value::Object(kept).to_string())
1919}
1920
1921fn redact_event_in_tx(
1942 tx: &Transaction,
1943 room_id: &str,
1944 target_event_id: &str,
1945 redaction_event_id: &str,
1946 sender_user_id: i64,
1947 reason: Option<&str>,
1948 origin_server_ts: i64,
1949) -> Result<MatrixEvent, MatrixStoreError> {
1950 let target: Option<(String, String, String)> = tx
1951 .query_row(
1952 "SELECT event_type, content, room_id FROM events WHERE event_id = ?1",
1953 params![target_event_id],
1954 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1955 )
1956 .optional()?;
1957 let (target_type, target_content, target_room) = target.ok_or_else(|| MatrixStoreError::UnknownEventId(target_event_id.to_string()))?;
1958 if target_room != room_id {
1959 return Err(MatrixStoreError::WrongRoom(target_event_id.to_string()));
1960 }
1961 if target_type == "m.room.create" || target_type == "m.room.encryption" {
1962 return Err(MatrixStoreError::UnredactableEvent(target_type));
1963 }
1964
1965 let stream_id = next_stream_id(tx)?;
1966 let mut redaction_content = serde_json::json!({ "redacts": target_event_id });
1969 if let Some(r) = reason {
1970 redaction_content["reason"] = serde_json::Value::String(r.to_string());
1971 }
1972 let redaction_content = redaction_content.to_string();
1973 tx.execute(
1974 "INSERT INTO events (stream_id, event_id, room_id, sender_user_id, event_type, state_key, content, origin_server_ts, txn_id, redacts)
1975 VALUES (?1, ?2, ?3, ?4, 'm.room.redaction', NULL, ?5, ?6, NULL, ?7)",
1976 params![stream_id, redaction_event_id, room_id, sender_user_id, redaction_content, origin_server_ts, target_event_id],
1977 )?;
1978
1979 let stripped_content = redact_content_per_v11(&target_type, &target_content)?;
1980 tx.execute(
1981 "UPDATE events SET content = ?1, redacted_by = ?2 WHERE event_id = ?3",
1982 params![stripped_content, redaction_event_id, target_event_id],
1983 )?;
1984
1985 Ok(MatrixEvent {
1986 stream_id,
1987 event_id: redaction_event_id.to_string(),
1988 room_id: room_id.to_string(),
1989 sender_user_id,
1990 event_type: "m.room.redaction".to_string(),
1991 state_key: None,
1992 content: redaction_content,
1993 origin_server_ts,
1994 txn_id: None,
1995 redacts: Some(target_event_id.to_string()),
1996 redacted_by: None,
1997 })
1998}
1999
2000pub fn redact_event(
2001 conn: &mut Connection,
2002 room_id: &str,
2003 target_event_id: &str,
2004 redaction_event_id: &str,
2005 sender_user_id: i64,
2006 reason: Option<&str>,
2007 origin_server_ts: i64,
2008) -> Result<MatrixEvent, MatrixStoreError> {
2009 let tx = conn.transaction()?;
2010 let event = redact_event_in_tx(&tx, room_id, target_event_id, redaction_event_id, sender_user_id, reason, origin_server_ts)?;
2011 tx.commit()?;
2012 Ok(event)
2013}
2014
2015#[derive(Debug, Clone, Copy)]
2018pub struct Redaction<'a> {
2019 pub room_id: &'a str,
2020 pub target_event_id: &'a str,
2021 pub redaction_event_id: &'a str,
2022 pub sender_user_id: i64,
2023 pub reason: Option<&'a str>,
2024 pub origin_server_ts: i64,
2025}
2026
2027pub fn redact_event_marked(
2037 conn: &mut Connection,
2038 redaction: &Redaction<'_>,
2039 extra_content: &serde_json::Value,
2040) -> Result<MatrixEvent, MatrixStoreError> {
2041 let Redaction { room_id, target_event_id, redaction_event_id, sender_user_id, reason, origin_server_ts } = *redaction;
2042 let tx = conn.transaction()?;
2043 let mut event = redact_event_in_tx(&tx, room_id, target_event_id, redaction_event_id, sender_user_id, reason, origin_server_ts)?;
2044 let mut content: serde_json::Value = serde_json::from_str(&event.content)?;
2045 if let (Some(content_obj), Some(extra_obj)) = (content.as_object_mut(), extra_content.as_object()) {
2046 for (key, value) in extra_obj {
2047 content_obj.insert(key.clone(), value.clone());
2048 }
2049 }
2050 let content_str = content.to_string();
2051 tx.execute("UPDATE events SET content = ?1 WHERE event_id = ?2", params![content_str, redaction_event_id])?;
2052 event.content = content_str;
2053 tx.commit()?;
2054 Ok(event)
2055}
2056
2057#[allow(clippy::too_many_arguments)]
2062pub fn redact_event_deduped(
2063 conn: &mut Connection,
2064 device_id: &str,
2065 txn_id: &str,
2066 room_id: &str,
2067 target_event_id: &str,
2068 redaction_event_id: &str,
2069 sender_user_id: i64,
2070 reason: Option<&str>,
2071 origin_server_ts: i64,
2072 now: &str,
2073) -> Result<DedupedWrite, MatrixStoreError> {
2074 let tx = conn.transaction()?;
2075 if let TxnDedupEntry::Seen(existing_event_id) = txn_dedup_lookup(&tx, sender_user_id, device_id, txn_id)? {
2076 let existing_event_id = existing_event_id.ok_or_else(|| MatrixStoreError::UnknownEventId(txn_id.to_string()))?;
2077 let event = get_event(&tx, &existing_event_id)?.ok_or_else(|| MatrixStoreError::UnknownEventId(existing_event_id.clone()))?;
2078 tx.commit()?;
2079 return Ok(DedupedWrite::Existing(event));
2080 }
2081 let event = redact_event_in_tx(&tx, room_id, target_event_id, redaction_event_id, sender_user_id, reason, origin_server_ts)?;
2082 txn_dedup_record(&tx, sender_user_id, device_id, txn_id, Some(redaction_event_id), now)?;
2083 tx.commit()?;
2084 Ok(DedupedWrite::New(event))
2085}
2086
2087#[derive(Debug, Clone, PartialEq)]
2092pub struct RoomMember {
2093 pub room_id: String,
2094 pub user_id: i64,
2095 pub membership: Membership,
2096 pub power_level: Option<i64>,
2097 pub updated_at: String,
2098}
2099
2100fn room_member_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<RoomMember> {
2101 let membership_raw: String = row.get(2)?;
2102 Ok(RoomMember {
2103 room_id: row.get(0)?,
2104 user_id: row.get(1)?,
2105 membership: decode_enum(2, "membership", &membership_raw, Membership::from_wire_name)?,
2106 power_level: row.get(3)?,
2107 updated_at: row.get(4)?,
2108 })
2109}
2110
2111const ROOM_MEMBER_SELECT_COLUMNS: &str = "room_id, user_id, membership, power_level, updated_at";
2112
2113pub fn room_members(conn: &Connection, room_id: &str, membership: Option<Membership>) -> rusqlite::Result<Vec<RoomMember>> {
2115 match membership {
2116 Some(m) => {
2117 let mut stmt = conn.prepare(&format!(
2118 "SELECT {ROOM_MEMBER_SELECT_COLUMNS} FROM room_members WHERE room_id = ?1 AND membership = ?2"
2119 ))?;
2120 let rows = stmt.query_map(params![room_id, m.as_str()], room_member_from_row)?;
2121 rows.collect()
2122 }
2123 None => {
2124 let mut stmt = conn.prepare(&format!("SELECT {ROOM_MEMBER_SELECT_COLUMNS} FROM room_members WHERE room_id = ?1"))?;
2125 let rows = stmt.query_map(params![room_id], room_member_from_row)?;
2126 rows.collect()
2127 }
2128 }
2129}
2130
2131pub fn room_member(conn: &Connection, room_id: &str, user_id: i64) -> rusqlite::Result<Option<RoomMember>> {
2136 conn.query_row(
2137 &format!("SELECT {ROOM_MEMBER_SELECT_COLUMNS} FROM room_members WHERE room_id = ?1 AND user_id = ?2"),
2138 params![room_id, user_id],
2139 room_member_from_row,
2140 )
2141 .optional()
2142}
2143
2144pub fn forget_membership(conn: &Connection, room_id: &str, user_id: i64) -> rusqlite::Result<usize> {
2151 conn.execute(
2152 "DELETE FROM room_members WHERE room_id = ?1 AND user_id = ?2 AND membership = 'leave'",
2153 params![room_id, user_id],
2154 )
2155}
2156
2157pub fn room_heroes(conn: &Connection, room_id: &str, exclude_user_id: i64, limit: i64) -> rusqlite::Result<Vec<i64>> {
2161 let mut stmt = conn.prepare(
2162 "SELECT user_id FROM room_members
2163 WHERE room_id = ?1 AND user_id != ?2 AND membership IN ('join', 'invite')
2164 ORDER BY updated_at ASC LIMIT ?3",
2165 )?;
2166 let rows = stmt.query_map(params![room_id, exclude_user_id, limit], |row| row.get(0))?;
2167 rows.collect()
2168}
2169
2170pub fn rooms_for_user(conn: &Connection, user_id: i64, membership: Option<Membership>) -> rusqlite::Result<Vec<String>> {
2174 match membership {
2175 Some(m) => {
2176 let mut stmt = conn.prepare("SELECT room_id FROM room_members WHERE user_id = ?1 AND membership = ?2")?;
2177 let rows = stmt.query_map(params![user_id, m.as_str()], |row| row.get(0))?;
2178 rows.collect()
2179 }
2180 None => {
2181 let mut stmt = conn.prepare("SELECT room_id FROM room_members WHERE user_id = ?1")?;
2182 let rows = stmt.query_map(params![user_id], |row| row.get(0))?;
2183 rows.collect()
2184 }
2185 }
2186}
2187
2188pub fn rooms_changed_in_window(
2203 conn: &Connection,
2204 room_ids: &[String],
2205 caller_user_id: i64,
2206 since_exclusive: i64,
2207 upto_inclusive: i64,
2208) -> rusqlite::Result<HashSet<String>> {
2209 let mut changed = HashSet::new();
2210 if room_ids.is_empty() {
2211 return Ok(changed);
2212 }
2213 let placeholders = vec!["?"; room_ids.len()].join(",");
2214
2215 for table in ["events", "receipts", "pub_events"] {
2219 let sql = format!("SELECT DISTINCT room_id FROM {table} WHERE stream_id > ? AND stream_id <= ? AND room_id IN ({placeholders})");
2220 let mut stmt = conn.prepare(&sql)?;
2221 let mut bound: Vec<&dyn rusqlite::ToSql> = vec![&since_exclusive, &upto_inclusive];
2222 for room_id in room_ids {
2223 bound.push(room_id as &dyn rusqlite::ToSql);
2224 }
2225 let mut rows = stmt.query(bound.as_slice())?;
2226 while let Some(row) = rows.next()? {
2227 changed.insert(row.get::<_, String>(0)?);
2228 }
2229 }
2230
2231 let room_id_set: HashSet<&str> = room_ids.iter().map(String::as_str).collect();
2237 let mut stmt = conn.prepare("SELECT DISTINCT room_id FROM account_data WHERE user_id = ?1 AND stream_id > ?2 AND stream_id <= ?3")?;
2238 let mut rows = stmt.query(params![caller_user_id, since_exclusive, upto_inclusive])?;
2239 while let Some(row) = rows.next()? {
2240 let room_id: String = row.get(0)?;
2241 if room_id_set.contains(room_id.as_str()) {
2242 changed.insert(room_id);
2243 }
2244 }
2245
2246 Ok(changed)
2247}
2248
2249pub fn membership_at(conn: &Connection, room_id: &str, mxid: &str, at_stream_id: i64) -> rusqlite::Result<Option<Membership>> {
2256 let content: Option<String> = conn
2257 .query_row(
2258 "SELECT content FROM events
2259 WHERE room_id = ?1 AND event_type = 'm.room.member' AND state_key = ?2 AND stream_id <= ?3
2260 ORDER BY stream_id DESC LIMIT 1",
2261 params![room_id, mxid, at_stream_id],
2262 |row| row.get(0),
2263 )
2264 .optional()?;
2265 Ok(content
2266 .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
2267 .and_then(|value| value.get("membership").and_then(|m| m.as_str()).and_then(Membership::from_wire_name)))
2268}
2269
2270pub fn rooms_with_member_events_in_window(conn: &Connection, room_ids: &[String], from_exclusive: i64, to_inclusive: i64) -> rusqlite::Result<Vec<String>> {
2277 if room_ids.is_empty() {
2278 return Ok(Vec::new());
2279 }
2280 let placeholders = vec!["?"; room_ids.len()].join(",");
2281 let sql = format!(
2282 "SELECT DISTINCT room_id FROM events
2283 WHERE event_type = 'm.room.member' AND stream_id > ? AND stream_id <= ? AND room_id IN ({placeholders})"
2284 );
2285 let mut stmt = conn.prepare(&sql)?;
2286 let mut bound: Vec<&dyn rusqlite::ToSql> = vec![&from_exclusive, &to_inclusive];
2287 for room_id in room_ids {
2288 bound.push(room_id as &dyn rusqlite::ToSql);
2289 }
2290 let rows = stmt.query_map(bound.as_slice(), |row| row.get(0))?;
2291 rows.collect()
2292}
2293
2294pub fn member_state_keys_in_window(conn: &Connection, room_id: &str, from_exclusive: i64, to_inclusive: i64) -> rusqlite::Result<Vec<String>> {
2299 let mut stmt = conn.prepare(
2300 "SELECT DISTINCT state_key FROM events
2301 WHERE room_id = ?1 AND event_type = 'm.room.member' AND state_key IS NOT NULL AND stream_id > ?2 AND stream_id <= ?3",
2302 )?;
2303 let rows = stmt.query_map(params![room_id, from_exclusive, to_inclusive], |row| row.get(0))?;
2304 rows.collect()
2305}
2306
2307pub fn user_ids_with_leave_transition_in_rooms(
2319 conn: &Connection,
2320 room_ids: &[String],
2321 from_exclusive: i64,
2322 to_inclusive: i64,
2323) -> rusqlite::Result<Vec<i64>> {
2324 if room_ids.is_empty() {
2325 return Ok(Vec::new());
2326 }
2327 let placeholders = vec!["?"; room_ids.len()].join(",");
2328 let sql = format!(
2329 "SELECT DISTINCT e.state_key, e.content FROM events e
2330 WHERE e.event_type = 'm.room.member' AND e.state_key IS NOT NULL
2331 AND e.stream_id > ? AND e.stream_id <= ?
2332 AND e.room_id IN ({placeholders})"
2333 );
2334 let mut stmt = conn.prepare(&sql)?;
2335 let mut bound: Vec<&dyn rusqlite::ToSql> = vec![&from_exclusive, &to_inclusive];
2336 for room_id in room_ids {
2337 bound.push(room_id as &dyn rusqlite::ToSql);
2338 }
2339 let mut rows = stmt.query(bound.as_slice())?;
2340 let mut mxids = Vec::new();
2341 while let Some(row) = rows.next()? {
2342 let mxid: String = row.get(0)?;
2343 let content: String = row.get(1)?;
2344 if let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) {
2345 if matches!(value.get("membership").and_then(|m| m.as_str()), Some("leave") | Some("ban")) {
2346 mxids.push(mxid);
2347 }
2348 }
2349 }
2350 let mut user_ids = Vec::new();
2351 for mxid in mxids {
2352 if let Some(user_id) = user_id_of(conn, &mxid)? {
2353 user_ids.push(user_id);
2354 }
2355 }
2356 Ok(user_ids)
2357}
2358
2359pub fn private_room_messages_sent_since(conn: &Connection, sender_user_id: i64, since_ms: i64) -> rusqlite::Result<i64> {
2370 conn.query_row(
2371 "SELECT COUNT(*) FROM events e JOIN rooms r ON r.id = e.room_id
2372 WHERE e.sender_user_id = ?1 AND e.state_key IS NULL AND e.event_type != 'm.room.redaction'
2373 AND e.origin_server_ts >= ?2 AND r.join_rule = 'invite'",
2374 params![sender_user_id, since_ms],
2375 |row| row.get(0),
2376 )
2377}
2378
2379pub fn txn_dedup_count_since(conn: &Connection, user_id: i64, device_id: &str, since: &str) -> rusqlite::Result<i64> {
2390 conn.query_row(
2391 "SELECT COUNT(*) FROM txn_dedup WHERE user_id = ?1 AND device_id = ?2 AND created_at >= ?3",
2392 params![user_id, device_id, since],
2393 |row| row.get(0),
2394 )
2395}
2396
2397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2407pub enum HistoryWindow {
2408 Nothing,
2410 All,
2412 UpTo(i64),
2414}
2415
2416impl HistoryWindow {
2417 pub fn contains(self, stream_id: i64) -> bool {
2418 match self {
2419 HistoryWindow::Nothing => false,
2420 HistoryWindow::All => true,
2421 HistoryWindow::UpTo(upto) => stream_id <= upto,
2422 }
2423 }
2424}
2425
2426pub fn visible_upper_bound(conn: &Connection, room: &Room, caller_user_id: i64) -> Result<HistoryWindow, MatrixStoreError> {
2446 if room.history_visibility == HistoryVisibility::WorldReadable {
2447 return Ok(HistoryWindow::All);
2448 }
2449 let Some(mxid) = mxid_of(conn, caller_user_id)? else {
2450 return Ok(HistoryWindow::Nothing);
2451 };
2452 let Some(member_event) = current_state_event(conn, &room.id, "m.room.member", &mxid)? else {
2453 return Ok(HistoryWindow::Nothing);
2454 };
2455 let content: serde_json::Value = serde_json::from_str(&member_event.content)?;
2456 match content.get("membership").and_then(|v| v.as_str()) {
2457 Some("join") => Ok(HistoryWindow::All),
2458 Some("leave") | Some("ban") => Ok(HistoryWindow::UpTo(member_event.stream_id)),
2459 _ => Ok(HistoryWindow::Nothing),
2460 }
2461}
2462
2463#[derive(Debug, Clone, PartialEq)]
2468pub struct ReceiptRow {
2469 pub room_id: String,
2470 pub user_id: i64,
2471 pub receipt_type: ReceiptType,
2472 pub event_id: String,
2473 pub ts: i64,
2474 pub stream_id: i64,
2475}
2476
2477fn receipt_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ReceiptRow> {
2478 let receipt_type_raw: String = row.get(2)?;
2479 Ok(ReceiptRow {
2480 room_id: row.get(0)?,
2481 user_id: row.get(1)?,
2482 receipt_type: decode_enum(2, "receipt_type", &receipt_type_raw, ReceiptType::from_wire_name)?,
2483 event_id: row.get(3)?,
2484 ts: row.get(4)?,
2485 stream_id: row.get(5)?,
2486 })
2487}
2488
2489const RECEIPT_SELECT_COLUMNS: &str = "room_id, user_id, receipt_type, event_id, ts, stream_id";
2490
2491pub fn get_receipt(conn: &Connection, room_id: &str, user_id: i64, receipt_type: ReceiptType) -> rusqlite::Result<Option<ReceiptRow>> {
2492 conn.query_row(
2493 &format!("SELECT {RECEIPT_SELECT_COLUMNS} FROM receipts WHERE room_id = ?1 AND user_id = ?2 AND receipt_type = ?3"),
2494 params![room_id, user_id, receipt_type.as_str()],
2495 receipt_from_row,
2496 )
2497 .optional()
2498}
2499
2500pub fn receipts_changed_in_room(conn: &Connection, room_id: &str, since_exclusive: i64, upto_inclusive: i64) -> rusqlite::Result<Vec<ReceiptRow>> {
2506 let mut stmt = conn.prepare(&format!(
2507 "SELECT {RECEIPT_SELECT_COLUMNS} FROM receipts WHERE room_id = ?1 AND stream_id > ?2 AND stream_id <= ?3"
2508 ))?;
2509 let rows = stmt.query_map(params![room_id, since_exclusive, upto_inclusive], receipt_from_row)?;
2510 rows.collect()
2511}
2512
2513pub fn upsert_receipt(
2525 conn: &mut Connection,
2526 room_id: &str,
2527 user_id: i64,
2528 receipt_type: ReceiptType,
2529 event_id: &str,
2530 ts: i64,
2531) -> Result<i64, MatrixStoreError> {
2532 let tx = conn.transaction()?;
2533 let stream_id = upsert_receipt_in_tx(&tx, room_id, user_id, receipt_type, event_id, ts)?;
2534 tx.commit()?;
2535 Ok(stream_id)
2536}
2537
2538fn upsert_receipt_in_tx(
2547 tx: &Transaction,
2548 room_id: &str,
2549 user_id: i64,
2550 receipt_type: ReceiptType,
2551 event_id: &str,
2552 ts: i64,
2553) -> Result<i64, MatrixStoreError> {
2554 let target: Option<(i64, String)> = tx
2555 .query_row("SELECT stream_id, room_id FROM events WHERE event_id = ?1", params![event_id], |row| {
2556 Ok((row.get(0)?, row.get(1)?))
2557 })
2558 .optional()?;
2559 let Some((target_position, target_room)) = target else {
2560 if let Some(public) = crate::public_channels::get_event(tx, event_id)? {
2563 if public.room_id != room_id {
2564 return Err(MatrixStoreError::WrongRoom(event_id.to_string()));
2565 }
2566 return Ok(public.stream_id);
2567 }
2568 return Err(MatrixStoreError::UnknownEventId(event_id.to_string()));
2569 };
2570 if target_room != room_id {
2571 return Err(MatrixStoreError::WrongRoom(event_id.to_string()));
2572 }
2573
2574 let existing: Option<(String, i64)> = tx
2575 .query_row(
2576 "SELECT event_id, stream_id FROM receipts WHERE room_id = ?1 AND user_id = ?2 AND receipt_type = ?3",
2577 params![room_id, user_id, receipt_type.as_str()],
2578 |row| Ok((row.get(0)?, row.get(1)?)),
2579 )
2580 .optional()?;
2581
2582 if let Some((existing_event_id, existing_stream_id)) = &existing {
2583 let existing_position: i64 =
2584 tx.query_row("SELECT stream_id FROM events WHERE event_id = ?1", params![existing_event_id], |row| row.get(0))?;
2585 if target_position <= existing_position {
2586 return Ok(*existing_stream_id);
2587 }
2588 }
2589
2590 let stream_id = next_stream_id(tx)?;
2591 tx.execute(
2592 "INSERT INTO receipts (room_id, user_id, receipt_type, event_id, ts, stream_id)
2593 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
2594 ON CONFLICT(room_id, user_id, receipt_type) DO UPDATE SET
2595 event_id = excluded.event_id, ts = excluded.ts, stream_id = excluded.stream_id",
2596 params![room_id, user_id, receipt_type.as_str(), event_id, ts, stream_id],
2597 )?;
2598 Ok(stream_id)
2599}
2600
2601const NOTIFICATION_MESSAGE_TYPES_SQL: &str = "('m.room.message', 'm.room.encrypted', 'org.example.legacy_dm')";
2613
2614pub fn notification_count(conn: &Connection, room: &Room, user_id: i64) -> Result<i64, MatrixStoreError> {
2633 if room.kind == RoomKind::Channel && !room.is_encrypted {
2634 return Ok(0); }
2636 let upper_bound = match visible_upper_bound(conn, room, user_id)? {
2637 HistoryWindow::Nothing => return Ok(0),
2638 HistoryWindow::All => i64::MAX,
2639 HistoryWindow::UpTo(upper) => upper,
2640 };
2641
2642 let mut after_stream_id: i64 = 0;
2643 for receipt_type in [ReceiptType::Read, ReceiptType::ReadPrivate] {
2644 if let Some(receipt) = get_receipt(conn, &room.id, user_id, receipt_type)? {
2645 if let Some(target) = get_event(conn, &receipt.event_id)? {
2646 after_stream_id = after_stream_id.max(target.stream_id);
2647 }
2648 }
2649 }
2650
2651 let count = conn.query_row(
2652 &format!(
2653 "SELECT COUNT(*) FROM events
2654 WHERE room_id = ?1 AND state_key IS NULL AND sender_user_id != ?2
2655 AND stream_id > ?3 AND stream_id <= ?4
2656 AND event_type IN {NOTIFICATION_MESSAGE_TYPES_SQL}"
2657 ),
2658 params![room.id, user_id, after_stream_id, upper_bound],
2659 |row| row.get(0),
2660 )?;
2661 Ok(count)
2662}
2663
2664pub const GLOBAL_ACCOUNT_DATA_ROOM: &str = "";
2671
2672#[derive(Debug, Clone, PartialEq)]
2673pub struct AccountDataRow {
2674 pub user_id: i64,
2675 pub room_id: String,
2676 pub data_type: String,
2677 pub content: String,
2678 pub stream_id: i64,
2679}
2680
2681fn account_data_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<AccountDataRow> {
2682 Ok(AccountDataRow {
2683 user_id: row.get(0)?,
2684 room_id: row.get(1)?,
2685 data_type: row.get(2)?,
2686 content: row.get(3)?,
2687 stream_id: row.get(4)?,
2688 })
2689}
2690
2691const ACCOUNT_DATA_SELECT_COLUMNS: &str = "user_id, room_id, data_type, content, stream_id";
2692
2693pub fn upsert_account_data(conn: &mut Connection, user_id: i64, room_id: &str, data_type: &str, content: &str) -> Result<i64, MatrixStoreError> {
2697 let tx = conn.transaction()?;
2698 let stream_id = next_stream_id(&tx)?;
2699 tx.execute(
2700 "INSERT INTO account_data (user_id, room_id, data_type, content, stream_id) VALUES (?1, ?2, ?3, ?4, ?5)
2701 ON CONFLICT(user_id, room_id, data_type) DO UPDATE SET content = excluded.content, stream_id = excluded.stream_id",
2702 params![user_id, room_id, data_type, content, stream_id],
2703 )?;
2704 tx.commit()?;
2705 Ok(stream_id)
2706}
2707
2708pub fn get_account_data(conn: &Connection, user_id: i64, room_id: &str, data_type: &str) -> rusqlite::Result<Option<AccountDataRow>> {
2709 conn.query_row(
2710 &format!("SELECT {ACCOUNT_DATA_SELECT_COLUMNS} FROM account_data WHERE user_id = ?1 AND room_id = ?2 AND data_type = ?3"),
2711 params![user_id, room_id, data_type],
2712 account_data_from_row,
2713 )
2714 .optional()
2715}
2716
2717pub fn account_data_since(conn: &Connection, user_id: i64, room_id: &str, since_stream: i64) -> rusqlite::Result<Vec<AccountDataRow>> {
2722 let mut stmt = conn.prepare(&format!(
2723 "SELECT {ACCOUNT_DATA_SELECT_COLUMNS} FROM account_data WHERE user_id = ?1 AND room_id = ?2 AND stream_id > ?3 ORDER BY stream_id ASC"
2724 ))?;
2725 let rows = stmt.query_map(params![user_id, room_id, since_stream], account_data_from_row)?;
2726 rows.collect()
2727}
2728
2729#[derive(Debug, Clone, PartialEq, Eq)]
2735pub enum TxnDedupEntry {
2736 NotSeen,
2739 Seen(Option<String>),
2742}
2743
2744pub fn txn_dedup_lookup(conn: &Connection, user_id: i64, device_id: &str, txn_id: &str) -> rusqlite::Result<TxnDedupEntry> {
2747 let found: Option<Option<String>> = conn
2748 .query_row(
2749 "SELECT event_id FROM txn_dedup WHERE user_id = ?1 AND device_id = ?2 AND txn_id = ?3",
2750 params![user_id, device_id, txn_id],
2751 |row| row.get(0),
2752 )
2753 .optional()?;
2754 Ok(match found {
2755 None => TxnDedupEntry::NotSeen,
2756 Some(event_id) => TxnDedupEntry::Seen(event_id),
2757 })
2758}
2759
2760pub fn txn_dedup_record(conn: &Connection, user_id: i64, device_id: &str, txn_id: &str, event_id: Option<&str>, now: &str) -> rusqlite::Result<()> {
2765 conn.execute(
2766 "INSERT INTO txn_dedup (user_id, device_id, txn_id, event_id, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
2767 params![user_id, device_id, txn_id, event_id, now],
2768 )?;
2769 Ok(())
2770}
2771
2772pub fn txn_id_for_event(conn: &Connection, user_id: i64, device_id: &str, event_id: &str) -> rusqlite::Result<Option<String>> {
2780 conn.query_row(
2781 "SELECT txn_id FROM txn_dedup WHERE user_id = ?1 AND device_id = ?2 AND event_id = ?3",
2782 params![user_id, device_id, event_id],
2783 |row| row.get(0),
2784 )
2785 .optional()
2786}
2787
2788pub fn create_filter(conn: &Connection, user_id: i64, definition: &str) -> rusqlite::Result<i64> {
2794 conn.execute("INSERT INTO filters (user_id, definition) VALUES (?1, ?2)", params![user_id, definition])?;
2795 Ok(conn.last_insert_rowid())
2796}
2797
2798pub fn get_filter(conn: &Connection, user_id: i64, filter_id: i64) -> rusqlite::Result<Option<String>> {
2801 conn.query_row(
2802 "SELECT definition FROM filters WHERE id = ?1 AND user_id = ?2",
2803 params![filter_id, user_id],
2804 |row| row.get(0),
2805 )
2806 .optional()
2807}
2808
2809pub fn insert_legacy_dm_message_map(conn: &Connection, legacy_message_id: i64, event_id: &str) -> rusqlite::Result<()> {
2814 conn.execute(
2815 "INSERT INTO legacy_dm_message_map (legacy_message_id, event_id) VALUES (?1, ?2)",
2816 params![legacy_message_id, event_id],
2817 )?;
2818 Ok(())
2819}
2820
2821pub fn legacy_dm_message_event_id(conn: &Connection, legacy_message_id: i64) -> rusqlite::Result<Option<String>> {
2822 conn.query_row(
2823 "SELECT event_id FROM legacy_dm_message_map WHERE legacy_message_id = ?1",
2824 params![legacy_message_id],
2825 |row| row.get(0),
2826 )
2827 .optional()
2828}
2829
2830pub fn highest_mapped_legacy_message_id(conn: &Connection, room_id: &str) -> rusqlite::Result<Option<i64>> {
2843 conn.query_row(
2844 "SELECT MAX(m.legacy_message_id) FROM legacy_dm_message_map m
2845 JOIN events e ON e.event_id = m.event_id
2846 WHERE e.room_id = ?1",
2847 params![room_id],
2848 |row| row.get(0),
2849 )
2850}
2851
2852#[derive(Debug, Clone, PartialEq)]
2888pub struct LegacyDmMessageImport {
2889 pub legacy_message_id: i64,
2890 pub event_id: String,
2891 pub sender_user_id: i64,
2892 pub content: String,
2893 pub origin_server_ts: i64,
2894}
2895
2896#[derive(Debug, Clone, PartialEq)]
2902pub struct LegacyDmReadReceipt {
2903 pub reader_user_id: i64,
2904 pub up_to_legacy_message_id: i64,
2905 pub ts_ms: i64,
2906}
2907
2908#[derive(Debug, Clone, PartialEq)]
2913pub struct LegacyDmDirectHint {
2914 pub user_id: i64,
2915 pub peer_mxid: String,
2916}
2917
2918#[derive(Debug, Clone, Copy)]
2923pub struct DmMigrationExtras<'a> {
2924 pub messages: &'a [LegacyDmMessageImport],
2925 pub receipts: &'a [LegacyDmReadReceipt],
2926 pub direct_hints: &'a [LegacyDmDirectHint],
2927}
2928
2929#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2931pub struct DmMigrationCounts {
2932 pub messages_imported: usize,
2933 pub receipts_set: usize,
2934}
2935
2936fn merge_m_direct_in_tx(tx: &Transaction, user_id: i64, peer_mxid: &str, room_id: &str) -> Result<(), MatrixStoreError> {
2941 let existing: Option<String> = tx
2942 .query_row(
2943 "SELECT content FROM account_data WHERE user_id = ?1 AND room_id = ?2 AND data_type = 'm.direct'",
2944 params![user_id, GLOBAL_ACCOUNT_DATA_ROOM],
2945 |row| row.get(0),
2946 )
2947 .optional()?;
2948
2949 let mut direct: serde_json::Map<String, serde_json::Value> = match existing {
2950 Some(content) => serde_json::from_str(&content)?,
2951 None => serde_json::Map::new(),
2952 };
2953 let rooms_entry = direct.entry(peer_mxid.to_string()).or_insert_with(|| serde_json::Value::Array(Vec::new()));
2954 if !rooms_entry.is_array() {
2955 *rooms_entry = serde_json::Value::Array(Vec::new());
2956 }
2957 if let serde_json::Value::Array(list) = rooms_entry {
2958 if !list.iter().any(|v| v.as_str() == Some(room_id)) {
2959 list.push(serde_json::Value::String(room_id.to_string()));
2960 }
2961 }
2962
2963 let stream_id = next_stream_id(tx)?;
2964 tx.execute(
2965 "INSERT INTO account_data (user_id, room_id, data_type, content, stream_id) VALUES (?1, ?2, 'm.direct', ?3, ?4)
2966 ON CONFLICT(user_id, room_id, data_type) DO UPDATE SET content = excluded.content, stream_id = excluded.stream_id",
2967 params![user_id, GLOBAL_ACCOUNT_DATA_ROOM, serde_json::Value::Object(direct).to_string(), stream_id],
2968 )?;
2969 Ok(())
2970}
2971
2972pub fn migrate_dm_conversation(
2987 conn: &mut Connection,
2988 bootstrap: RoomBootstrap<'_>,
2989 state_events: &[NewStateEvent],
2990 bootstrap_origin_server_ts: i64,
2991 extras: DmMigrationExtras<'_>,
2992) -> Result<(Room, DmMigrationCounts), MatrixStoreError> {
2993 let tx = conn.transaction()?;
2994
2995 insert_room_row(
2996 &tx,
2997 bootstrap.room_id,
2998 bootstrap.kind,
2999 bootstrap.creator_user_id,
3000 bootstrap.created_at,
3001 bootstrap.is_encrypted,
3002 bootstrap.join_rule,
3003 bootstrap.history_visibility,
3004 bootstrap.dm_pair_key,
3005 bootstrap.legacy_dm_id,
3006 )?;
3007 for event in state_events {
3008 apply_state_event_in_tx(
3009 &tx,
3010 &event.event_id,
3011 bootstrap.room_id,
3012 event.sender_user_id,
3013 &event.event_type,
3014 &event.state_key,
3015 &event.content,
3016 bootstrap_origin_server_ts,
3017 bootstrap.created_at,
3018 )?;
3019 }
3020
3021 let mut event_id_by_legacy_id: std::collections::HashMap<i64, String> = std::collections::HashMap::new();
3022 for message in extras.messages {
3023 insert_timeline_event_in_tx(
3024 &tx,
3025 &TimelineEventRow {
3026 event_id: &message.event_id,
3027 room_id: bootstrap.room_id,
3028 sender_user_id: message.sender_user_id,
3029 event_type: "org.example.legacy_dm",
3030 content: &message.content,
3031 origin_server_ts: message.origin_server_ts,
3032 txn_id: None,
3033 },
3034 )?;
3035 insert_legacy_dm_message_map(&tx, message.legacy_message_id, &message.event_id)?;
3036 event_id_by_legacy_id.insert(message.legacy_message_id, message.event_id.clone());
3037 }
3038
3039 let mut receipts_set = 0usize;
3040 for receipt in extras.receipts {
3041 let Some(target_event_id) = event_id_by_legacy_id.get(&receipt.up_to_legacy_message_id) else { continue };
3042 let target_stream_id: i64 =
3043 tx.query_row("SELECT stream_id FROM events WHERE event_id = ?1", params![target_event_id], |row| row.get(0))?;
3044 tx.execute(
3045 "INSERT INTO receipts (room_id, user_id, receipt_type, event_id, ts, stream_id) VALUES (?1, ?2, 'm.read', ?3, ?4, ?5)",
3046 params![bootstrap.room_id, receipt.reader_user_id, target_event_id, receipt.ts_ms, target_stream_id],
3047 )?;
3048 receipts_set += 1;
3049 }
3050
3051 for hint in extras.direct_hints {
3052 merge_m_direct_in_tx(&tx, hint.user_id, &hint.peer_mxid, bootstrap.room_id)?;
3053 }
3054
3055 tx.commit()?;
3056
3057 Ok((
3058 Room {
3059 id: bootstrap.room_id.to_string(),
3060 kind: bootstrap.kind,
3061 room_version: MATRIX_ROOM_VERSION.to_string(),
3062 creator_user_id: bootstrap.creator_user_id,
3063 created_at: bootstrap.created_at.to_string(),
3064 is_encrypted: bootstrap.is_encrypted,
3065 join_rule: bootstrap.join_rule,
3066 history_visibility: bootstrap.history_visibility,
3067 dm_pair_key: bootstrap.dm_pair_key.map(str::to_string),
3068 legacy_dm_id: bootstrap.legacy_dm_id,
3069 },
3070 DmMigrationCounts { messages_imported: extras.messages.len(), receipts_set },
3071 ))
3072}
3073
3074pub fn catch_up_dm_conversation(
3102 conn: &mut Connection,
3103 room_id: &str,
3104 messages: &[LegacyDmMessageImport],
3105 receipts: &[LegacyDmReadReceipt],
3106) -> Result<DmMigrationCounts, MatrixStoreError> {
3107 let tx = conn.transaction()?;
3108 let counts = catch_up_dm_in_tx(&tx, room_id, messages, receipts)?;
3109 tx.commit()?;
3110 Ok(counts)
3111}
3112
3113fn catch_up_dm_in_tx(
3118 tx: &Transaction,
3119 room_id: &str,
3120 messages: &[LegacyDmMessageImport],
3121 receipts: &[LegacyDmReadReceipt],
3122) -> Result<DmMigrationCounts, MatrixStoreError> {
3123 for message in messages {
3124 insert_timeline_event_in_tx(
3125 tx,
3126 &TimelineEventRow {
3127 event_id: &message.event_id,
3128 room_id,
3129 sender_user_id: message.sender_user_id,
3130 event_type: "org.example.legacy_dm",
3131 content: &message.content,
3132 origin_server_ts: message.origin_server_ts,
3133 txn_id: None,
3134 },
3135 )?;
3136 insert_legacy_dm_message_map(tx, message.legacy_message_id, &message.event_id)?;
3137 }
3138
3139 let mut receipts_set = 0usize;
3140 for receipt in receipts {
3141 let Some(target_event_id) = legacy_dm_message_event_id(tx, receipt.up_to_legacy_message_id)? else { continue };
3142 upsert_receipt_in_tx(tx, room_id, receipt.reader_user_id, ReceiptType::Read, &target_event_id, receipt.ts_ms)?;
3143 receipts_set += 1;
3144 }
3145
3146 Ok(DmMigrationCounts { messages_imported: messages.len(), receipts_set })
3147}
3148
3149#[derive(Debug, Clone, Copy)]
3153pub struct DmAdoption<'a> {
3154 pub room_id: &'a str,
3156 pub legacy_dm_id: i64,
3158 pub key_events: &'a [NewStateEvent],
3163 pub key_events_origin_server_ts: i64,
3164 pub now: &'a str,
3166}
3167
3168pub fn adopt_dm_room_for_legacy(
3182 conn: &mut Connection,
3183 adoption: DmAdoption<'_>,
3184 messages: &[LegacyDmMessageImport],
3185 receipts: &[LegacyDmReadReceipt],
3186) -> Result<Option<DmMigrationCounts>, MatrixStoreError> {
3187 let tx = conn.transaction()?;
3188
3189 let bound = tx.execute(
3190 "UPDATE rooms SET legacy_dm_id = ?1 WHERE id = ?2 AND kind = 'dm' AND legacy_dm_id IS NULL",
3191 params![adoption.legacy_dm_id, adoption.room_id],
3192 )?;
3193 if bound == 0 {
3194 return Ok(None);
3195 }
3196
3197 for event in adoption.key_events {
3198 if current_state_event(&tx, adoption.room_id, &event.event_type, &event.state_key)?.is_some() {
3199 continue;
3200 }
3201 apply_state_event_in_tx(
3202 &tx,
3203 &event.event_id,
3204 adoption.room_id,
3205 event.sender_user_id,
3206 &event.event_type,
3207 &event.state_key,
3208 &event.content,
3209 adoption.key_events_origin_server_ts,
3210 adoption.now,
3211 )?;
3212 }
3213
3214 let counts = catch_up_dm_in_tx(&tx, adoption.room_id, messages, receipts)?;
3215 tx.commit()?;
3216 Ok(Some(counts))
3217}
3218
3219#[derive(Debug, Clone, PartialEq)]
3231pub struct PublicRoomSummary {
3232 pub room_id: String,
3233 pub name: Option<String>,
3234 pub topic: Option<String>,
3235 pub num_joined_members: i64,
3236 pub world_readable: bool,
3237}
3238
3239pub fn public_rooms_page(
3260 conn: &Connection,
3261 after_room_id: Option<&str>,
3262 limit: usize,
3263 search_term: Option<&str>,
3264) -> Result<(Vec<PublicRoomSummary>, bool, i64), MatrixStoreError> {
3265 let mut stmt = conn.prepare(
3266 "SELECT r.id, r.history_visibility,
3267 (SELECT COUNT(*) FROM room_members WHERE room_id = r.id AND membership = 'join')
3268 FROM rooms r WHERE r.join_rule = 'public' ORDER BY r.id ASC",
3269 )?;
3270 let mut rows = stmt.query([])?;
3271 let mut all = Vec::new();
3272 while let Some(row) = rows.next()? {
3273 let room_id: String = row.get(0)?;
3274 let history_visibility_raw: String = row.get(1)?;
3275 let num_joined_members: i64 = row.get(2)?;
3276 let world_readable = history_visibility_raw == HistoryVisibility::WorldReadable.as_str();
3277
3278 let name = current_state_event(conn, &room_id, "m.room.name", "")?
3279 .and_then(|e| serde_json::from_str::<serde_json::Value>(&e.content).ok())
3280 .and_then(|v| v.get("name").and_then(|n| n.as_str()).map(str::to_string));
3281 let topic = current_state_event(conn, &room_id, "m.room.topic", "")?
3282 .and_then(|e| serde_json::from_str::<serde_json::Value>(&e.content).ok())
3283 .and_then(|v| v.get("topic").and_then(|t| t.as_str()).map(str::to_string));
3284
3285 all.push(PublicRoomSummary { room_id, name, topic, num_joined_members, world_readable });
3286 }
3287 drop(rows);
3288 drop(stmt);
3289
3290 let total_room_count_estimate = all.len() as i64;
3291
3292 let filtered: Vec<PublicRoomSummary> = match search_term {
3293 Some(term) if !term.is_empty() => {
3294 let term_lower = term.to_lowercase();
3295 all.into_iter().filter(|room| room.name.as_deref().is_some_and(|n| n.to_lowercase().contains(&term_lower))).collect()
3296 }
3297 _ => all,
3298 };
3299
3300 let start = match after_room_id {
3301 Some(cursor) => filtered.iter().position(|r| r.room_id == cursor).map_or(0, |idx| idx + 1),
3302 None => 0,
3303 };
3304 let has_more = filtered.len() > start + limit;
3305 let page: Vec<PublicRoomSummary> = filtered.into_iter().skip(start).take(limit).collect();
3306 Ok((page, has_more, total_room_count_estimate))
3307}
3308
3309#[cfg(test)]
3310mod tests {
3311 use super::*;
3312
3313 const T0: &str = "2026-09-24T00:00:00+00:00";
3314 const ROOM: &str = "!testroom:example.org";
3315
3316 fn test_conn() -> Connection {
3317 let conn = Connection::open_in_memory().expect("in-memory sqlite");
3318 create_matrix_schema(&conn).expect("schema");
3319 conn
3320 }
3321
3322 fn ensure_legacy_dm_map_table(conn: &Connection) {
3323 conn.execute_batch(
3324 "CREATE TABLE IF NOT EXISTS legacy_dm_message_map (
3325 legacy_message_id INTEGER PRIMARY KEY,
3326 event_id TEXT NOT NULL UNIQUE REFERENCES events(event_id)
3327 );",
3328 )
3329 .expect("legacy map table for remaining unit tests");
3330 }
3331
3332 fn make_room(conn: &Connection) {
3333 create_room(conn, ROOM, RoomKind::Group, 1, T0, false, JoinRule::Invite, HistoryVisibility::Shared, None, None).expect("create room");
3334 }
3335
3336 #[test]
3339 fn next_stream_id_is_strictly_monotonic_across_every_table() {
3340 let mut conn = test_conn();
3341 make_room(&conn);
3342
3343 let event = insert_timeline_event(&mut conn, "$event1", ROOM, 1, "m.room.message", "{}", 1000).expect("insert event");
3344 let account_data_stream = upsert_account_data(&mut conn, 1, GLOBAL_ACCOUNT_DATA_ROOM, "m.direct", "{}").expect("account data");
3345 let receipt_stream = upsert_receipt(&mut conn, ROOM, 1, ReceiptType::Read, "$event1", 1500).expect("receipt");
3346
3347 assert!(event.stream_id < account_data_stream, "event {} should precede account data {}", event.stream_id, account_data_stream);
3348 assert!(
3349 account_data_stream < receipt_stream,
3350 "account data {account_data_stream} should precede receipt {receipt_stream}"
3351 );
3352 }
3353
3354 #[test]
3357 fn apply_state_event_replaces_current_state_but_keeps_history_in_events() {
3358 let mut conn = test_conn();
3359 make_room(&conn);
3360
3361 apply_state_event(&mut conn, &StateEventWrite { event_id: "$e1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.name", state_key: "", content: r#"{"name":"first"}"#, origin_server_ts: 1000, now: T0 }).expect("apply first");
3362 apply_state_event(&mut conn, &StateEventWrite { event_id: "$e2", room_id: ROOM, sender_user_id: 1, event_type: "m.room.name", state_key: "", content: r#"{"name":"second"}"#, origin_server_ts: 2000, now: T0 }).expect("apply second");
3363
3364 let current = current_state_event(&conn, ROOM, "m.room.name", "").expect("query").expect("row exists");
3365 assert_eq!(current.event_id, "$e2");
3366 assert_eq!(current.content, r#"{"name":"second"}"#);
3367
3368 let e1 = get_event(&conn, "$e1").expect("get e1").expect("row exists");
3369 let e2 = get_event(&conn, "$e2").expect("get e2").expect("row exists");
3370 assert_eq!(e1.content, r#"{"name":"first"}"#);
3371 assert_eq!(e2.content, r#"{"name":"second"}"#);
3372 }
3373
3374 #[test]
3377 fn redact_event_strips_content_per_v11_allow_list_by_type() {
3378 let cases: Vec<(&str, &str, serde_json::Value)> = vec![
3379 (
3380 "m.room.member",
3381 r#"{"membership":"join","join_authorised_via_users_server":"@x:example.org","avatar_url":"mxc://x","displayname":"Bob","third_party_invite":{"display_name":"bob@example.com","signed":{"mxid":"@bob:example.org"}}}"#,
3382 serde_json::json!({
3383 "membership": "join",
3384 "join_authorised_via_users_server": "@x:example.org",
3385 "third_party_invite": {"signed": {"mxid": "@bob:example.org"}}
3386 }),
3387 ),
3388 (
3389 "m.room.power_levels",
3390 r#"{"ban":50,"events":{},"events_default":0,"invite":50,"kick":50,"redact":50,"state_default":50,"users":{"@a:x":100},"users_default":0,"extra":"drop me"}"#,
3391 serde_json::json!({
3392 "ban": 50, "events": {}, "events_default": 0, "invite": 50, "kick": 50,
3393 "redact": 50, "state_default": 50, "users": {"@a:x": 100}, "users_default": 0
3394 }),
3395 ),
3396 (
3397 "m.room.history_visibility",
3398 r#"{"history_visibility":"shared","extra":"drop"}"#,
3399 serde_json::json!({"history_visibility": "shared"}),
3400 ),
3401 (
3406 "m.room.message",
3407 r#"{"body":"hi","msgtype":"m.text"}"#,
3408 serde_json::json!({}),
3409 ),
3410 ];
3411
3412 for (event_type, content, expected) in cases {
3413 let mut conn = test_conn();
3414 make_room(&conn);
3415 insert_timeline_event(&mut conn, "$target", ROOM, 1, event_type, content, 1000).expect("insert target");
3416 redact_event(&mut conn, ROOM, "$target", "$redaction", 1, None, 2000).expect("redact");
3417
3418 let target = get_event(&conn, "$target").expect("get target").expect("row exists");
3419 let got: serde_json::Value = serde_json::from_str(&target.content).expect("parse stripped content");
3420 assert_eq!(got, expected, "event_type={event_type}");
3421 assert_eq!(target.redacted_by.as_deref(), Some("$redaction"), "event_type={event_type}");
3422 }
3423 }
3424
3425 #[test]
3426 fn redaction_content_carries_the_target_as_redacts_for_every_write_path() {
3427 let mut conn = test_conn();
3428 make_room(&conn);
3429 insert_timeline_event(&mut conn, "$t1", ROOM, 1, "m.room.message", "{}", 1000).expect("insert t1");
3430 insert_timeline_event(&mut conn, "$t2", ROOM, 1, "m.room.message", "{}", 1100).expect("insert t2");
3431 insert_timeline_event(&mut conn, "$t3", ROOM, 1, "m.room.message", "{}", 1200).expect("insert t3");
3432
3433 let plain = redact_event(&mut conn, ROOM, "$t1", "$r1", 1, None, 2000).expect("plain redact");
3434 assert_eq!(serde_json::from_str::<serde_json::Value>(&plain.content).expect("json"), serde_json::json!({ "redacts": "$t1" }));
3435 assert_eq!(plain.redacts.as_deref(), Some("$t1"));
3436
3437 let marked = redact_event_marked(
3438 &mut conn,
3439 &Redaction { room_id: ROOM, target_event_id: "$t2", redaction_event_id: "$r2", sender_user_id: 1, reason: Some("spam"), origin_server_ts: 2100 },
3440 &serde_json::json!({ "org.example.site_moderation": true }),
3441 )
3442 .expect("marked redact");
3443 assert_eq!(
3444 serde_json::from_str::<serde_json::Value>(&marked.content).expect("json"),
3445 serde_json::json!({ "redacts": "$t2", "reason": "spam", "org.example.site_moderation": true })
3446 );
3447
3448 let deduped = redact_event_deduped(&mut conn, "DEV1", "txn-1", ROOM, "$t3", "$r3", 1, None, 2200, T0).expect("deduped redact");
3449 let DedupedWrite::New(event) = deduped else { panic!("first write is new") };
3450 let stored = get_event(&conn, &event.event_id).expect("get").expect("row exists");
3451 assert_eq!(serde_json::from_str::<serde_json::Value>(&stored.content).expect("json")["redacts"], "$t3");
3452 }
3453
3454 #[test]
3455 fn redact_content_per_v11_keeps_everything_for_m_room_create() {
3456 let content = r#"{"room_version":"11","creator":"@a:example.org"}"#;
3457 let stripped = redact_content_per_v11("m.room.create", content).expect("strip");
3458 let got: serde_json::Value = serde_json::from_str(&stripped).expect("parse");
3459 assert_eq!(got, serde_json::json!({"room_version": "11", "creator": "@a:example.org"}));
3460 }
3461
3462 #[test]
3465 fn txn_dedup_returns_the_same_event_id_on_a_repeated_txn_id() {
3466 let mut conn = test_conn();
3467 make_room(&conn);
3468
3469 assert_eq!(txn_dedup_lookup(&conn, 1, "DEV1", "txn-1").expect("lookup 1"), TxnDedupEntry::NotSeen);
3470
3471 let event = {
3472 let tx = conn.transaction().expect("tx");
3473 let row = TimelineEventRow {
3474 event_id: "$e1",
3475 room_id: ROOM,
3476 sender_user_id: 1,
3477 event_type: "m.room.message",
3478 content: "{}",
3479 origin_server_ts: 1000,
3480 txn_id: Some("txn-1"),
3481 };
3482 let event = insert_timeline_event_in_tx(&tx, &row).expect("insert");
3483 tx.commit().expect("commit");
3484 event
3485 };
3486 txn_dedup_record(&conn, 1, "DEV1", "txn-1", Some(&event.event_id), T0).expect("record");
3487
3488 assert_eq!(
3489 txn_dedup_lookup(&conn, 1, "DEV1", "txn-1").expect("lookup 2"),
3490 TxnDedupEntry::Seen(Some(event.event_id.clone()))
3491 );
3492
3493 let count: i64 = conn.query_row("SELECT COUNT(*) FROM events", [], |row| row.get(0)).expect("count");
3494 assert_eq!(count, 1, "a repeated txn_id must never create a second events row");
3495 }
3496
3497 #[test]
3500 fn relations_index_populated_from_cleartext_relates_to_even_when_content_is_ciphertext() {
3501 let mut conn = test_conn();
3502 make_room(&conn);
3503 insert_timeline_event(&mut conn, "$target", ROOM, 1, "m.room.message", "{}", 1000).expect("target");
3504
3505 let content = r#"{"algorithm":"m.megolm.v1.aes-sha2","ciphertext":"opaque-base64==","sender_key":"opaque","m.relates_to":{"rel_type":"m.annotation","event_id":"$target","key":"a"}}"#;
3506 let reaction = insert_timeline_event(&mut conn, "$reaction", ROOM, 2, "m.room.encrypted", content, 2000).expect("insert reaction");
3507
3508 let (rel_type, target_id, agg_key): (String, String, Option<String>) = conn
3509 .query_row(
3510 "SELECT rel_type, target_id, agg_key FROM relations WHERE event_id = ?1",
3511 params![reaction.event_id],
3512 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
3513 )
3514 .expect("relation row exists");
3515 assert_eq!(rel_type, "m.annotation");
3516 assert_eq!(target_id, "$target");
3517 assert_eq!(agg_key.as_deref(), Some("a"));
3518 }
3519
3520 #[test]
3523 fn account_data_global_row_is_unique() {
3524 let mut conn = test_conn();
3525 upsert_account_data(&mut conn, 1, GLOBAL_ACCOUNT_DATA_ROOM, "m.direct", r#"{"v":1}"#).expect("first upsert");
3526 upsert_account_data(&mut conn, 1, GLOBAL_ACCOUNT_DATA_ROOM, "m.direct", r#"{"v":2}"#).expect("second upsert");
3527
3528 let count: i64 = conn
3529 .query_row(
3530 "SELECT COUNT(*) FROM account_data WHERE user_id = 1 AND room_id = ''",
3531 [],
3532 |row| row.get(0),
3533 )
3534 .expect("count");
3535 assert_eq!(count, 1, "two global upserts of the same type must leave one row");
3536
3537 let row = get_account_data(&conn, 1, GLOBAL_ACCOUNT_DATA_ROOM, "m.direct").expect("get").expect("row exists");
3538 assert_eq!(row.content, r#"{"v":2}"#);
3539 }
3540
3541 #[test]
3544 fn receipt_never_moves_backwards() {
3545 let mut conn = test_conn();
3546 make_room(&conn);
3547 let e1 = insert_timeline_event(&mut conn, "$e1", ROOM, 1, "m.room.message", "{}", 1000).expect("e1");
3548 let e2 = insert_timeline_event(&mut conn, "$e2", ROOM, 1, "m.room.message", "{}", 2000).expect("e2");
3549
3550 upsert_receipt(&mut conn, ROOM, 9, ReceiptType::Read, &e2.event_id, 5000).expect("advance to e2");
3551 upsert_receipt(&mut conn, ROOM, 9, ReceiptType::Read, &e1.event_id, 6000).expect("attempted backward move is a no-op");
3552
3553 let receipt = get_receipt(&conn, ROOM, 9, ReceiptType::Read).expect("get").expect("row exists");
3554 assert_eq!(receipt.event_id, e2.event_id, "a receipt must never move back to an earlier event");
3555 }
3556
3557 #[test]
3560 fn receipt_further_ahead_of_the_two_types_wins_for_notification_count() {
3561 let mut conn = test_conn();
3562 make_room(&conn);
3563 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000001", T0).expect("alice");
3564 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 900, now: T0 }).expect("alice joins");
3565
3566 insert_timeline_event(&mut conn, "$e1", ROOM, 2, "m.room.message", "{}", 1000).expect("e1");
3567 let e2 = insert_timeline_event(&mut conn, "$e2", ROOM, 2, "m.room.message", "{}", 2000).expect("e2");
3568 insert_timeline_event(&mut conn, "$e3", ROOM, 2, "m.room.message", "{}", 3000).expect("e3");
3569 let e4 = insert_timeline_event(&mut conn, "$e4", ROOM, 2, "m.room.message", "{}", 4000).expect("e4");
3570 let e5 = insert_timeline_event(&mut conn, "$e5", ROOM, 2, "m.room.message", "{}", 5000).expect("e5");
3571
3572 upsert_receipt(&mut conn, ROOM, 1, ReceiptType::Read, &e2.event_id, 2500).expect("read receipt");
3576 upsert_receipt(&mut conn, ROOM, 1, ReceiptType::ReadPrivate, &e4.event_id, 4500).expect("private receipt further ahead");
3577
3578 let room = get_room(&conn, ROOM).expect("get room").expect("room exists");
3579 assert_eq!(notification_count(&conn, &room, 1).expect("count"), 1, "only $e5 is after the further-ahead receipt ($e4)");
3580
3581 upsert_receipt(&mut conn, ROOM, 1, ReceiptType::Read, &e5.event_id, 5500).expect("read receipt advances past e5");
3584 assert_eq!(notification_count(&conn, &room, 1).expect("count"), 0, "m.read now covers every message");
3585 }
3586
3587 #[test]
3588 fn notification_count_ignores_own_state_and_reactions() {
3589 let mut conn = test_conn();
3590 make_room(&conn);
3591 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000001", T0).expect("alice");
3592 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 900, now: T0 }).expect("alice joins");
3593
3594 insert_timeline_event(&mut conn, "$bob-msg", ROOM, 2, "m.room.message", "{}", 1000).expect("bob message");
3596 insert_timeline_event(&mut conn, "$bob-enc", ROOM, 2, "m.room.encrypted", "{}", 1100).expect("bob encrypted");
3597 insert_timeline_event(&mut conn, "$bob-legacy", ROOM, 2, "org.example.legacy_dm", "{}", 1200).expect("bob legacy dm");
3598
3599 insert_timeline_event(&mut conn, "$alice-msg", ROOM, 1, "m.room.message", "{}", 1300).expect("alice's own message");
3601 apply_state_event(&mut conn, &StateEventWrite { event_id: "$name", room_id: ROOM, sender_user_id: 2, event_type: "m.room.name", state_key: "", content: r#"{"name":"x"}"#, origin_server_ts: 1400, now: T0 }).expect("state event");
3602 insert_timeline_event(&mut conn, "$reaction", ROOM, 2, "m.reaction", "{}", 1500).expect("reaction");
3603
3604 let room = get_room(&conn, ROOM).expect("get room").expect("room exists");
3605 assert_eq!(notification_count(&conn, &room, 1).expect("count"), 3, "only bob's 3 message-like sends count");
3606 }
3607
3608 #[test]
3609 fn notification_count_is_zero_for_a_non_member() {
3610 let mut conn = test_conn();
3611 make_room(&conn);
3612 insert_timeline_event(&mut conn, "$e1", ROOM, 2, "m.room.message", "{}", 1000).expect("e1");
3613
3614 let room = get_room(&conn, ROOM).expect("get room").expect("room exists");
3615 assert_eq!(notification_count(&conn, &room, 999).expect("count"), 0, "a caller with no membership row at all sees nothing");
3616 }
3617
3618 #[test]
3621 fn duplicate_annotation_is_refused() {
3622 let mut conn = test_conn();
3623 make_room(&conn);
3624 insert_timeline_event(&mut conn, "$target", ROOM, 1, "m.room.message", "{}", 1000).expect("target");
3625
3626 let content = r#"{"m.relates_to":{"rel_type":"m.annotation","event_id":"$target","key":"a"}}"#;
3627 insert_timeline_event(&mut conn, "$react1", ROOM, 5, "m.reaction", content, 2000).expect("first reaction");
3628
3629 let err = insert_timeline_event(&mut conn, "$react2", ROOM, 5, "m.reaction", content, 3000).unwrap_err();
3630 assert!(matches!(err, MatrixStoreError::DuplicateAnnotation));
3631
3632 assert_eq!(get_event(&conn, "$react2").expect("get"), None);
3634
3635 let other = insert_timeline_event(&mut conn, "$react3", ROOM, 6, "m.reaction", content, 4000).expect("different sender reacts");
3638 assert_eq!(other.event_id, "$react3");
3639 }
3640
3641 #[test]
3644 fn reannotation_after_redaction_is_allowed() {
3645 let mut conn = test_conn();
3646 make_room(&conn);
3647 insert_timeline_event(&mut conn, "$target", ROOM, 1, "m.room.message", "{}", 1000).expect("target");
3648
3649 let content = r#"{"m.relates_to":{"rel_type":"m.annotation","event_id":"$target","key":"a"}}"#;
3650 let first = insert_timeline_event(&mut conn, "$react1", ROOM, 5, "m.reaction", content, 2000).expect("first reaction");
3651
3652 redact_event(&mut conn, ROOM, &first.event_id, "$redaction", 1, None, 2500).expect("redact the reaction");
3653
3654 let second = insert_timeline_event(&mut conn, "$react2", ROOM, 5, "m.reaction", content, 3000)
3657 .expect("re-annotation after redaction must succeed");
3658 assert_eq!(second.event_id, "$react2");
3659 }
3660
3661 #[test]
3662 fn relation_target_must_exist() {
3663 let mut conn = test_conn();
3664 make_room(&conn);
3665
3666 let content = r#"{"m.relates_to":{"rel_type":"m.annotation","event_id":"$missing","key":"a"}}"#;
3667 let err = insert_timeline_event(&mut conn, "$react1", ROOM, 5, "m.reaction", content, 2000).unwrap_err();
3668 assert!(matches!(err, MatrixStoreError::InvalidRelationTarget(ref id) if id == "$missing"));
3669 }
3670
3671 #[test]
3672 fn relation_target_must_be_in_the_same_room() {
3673 let mut conn = test_conn();
3674 make_room(&conn);
3675 let other_room = format!("!other:{}", matrix_server_name());
3676 create_room(&conn, &other_room, RoomKind::Group, 1, T0, false, JoinRule::Invite, HistoryVisibility::Shared, None, None)
3677 .expect("other room");
3678 insert_timeline_event(&mut conn, "$target", &other_room, 1, "m.room.message", "{}", 1000).expect("target in other room");
3679
3680 let content = r#"{"m.relates_to":{"rel_type":"m.annotation","event_id":"$target","key":"a"}}"#;
3681 let err = insert_timeline_event(&mut conn, "$react1", ROOM, 5, "m.reaction", content, 2000).unwrap_err();
3682 assert!(matches!(err, MatrixStoreError::InvalidRelationTarget(ref id) if id == "$target"));
3683 }
3684
3685 #[test]
3686 fn redact_event_refuses_a_target_in_a_different_room() {
3687 let mut conn = test_conn();
3688 make_room(&conn);
3689 let other_room = format!("!other:{}", matrix_server_name());
3690 create_room(&conn, &other_room, RoomKind::Group, 1, T0, false, JoinRule::Invite, HistoryVisibility::Shared, None, None)
3691 .expect("other room");
3692 insert_timeline_event(&mut conn, "$target", &other_room, 1, "m.room.message", "{}", 1000).expect("target in other room");
3693
3694 let err = redact_event(&mut conn, ROOM, "$target", "$redaction", 1, None, 2000).unwrap_err();
3695 assert!(matches!(err, MatrixStoreError::WrongRoom(ref id) if id == "$target"));
3696 }
3697
3698 #[test]
3699 fn redact_event_refuses_create_and_encryption_events() {
3700 for event_type in ["m.room.create", "m.room.encryption"] {
3701 let mut conn = test_conn();
3702 make_room(&conn);
3703 apply_state_event(&mut conn, &StateEventWrite { event_id: "$target", room_id: ROOM, sender_user_id: 1, event_type, state_key: "", content: r#"{"a":1}"#, origin_server_ts: 1000, now: T0 }).expect("apply state event");
3704
3705 let err = redact_event(&mut conn, ROOM, "$target", "$redaction", 1, None, 2000).unwrap_err();
3706 assert!(
3707 matches!(err, MatrixStoreError::UnredactableEvent(ref t) if t == event_type),
3708 "event_type={event_type}"
3709 );
3710 }
3711 }
3712
3713 #[test]
3714 fn upsert_receipt_refuses_a_target_in_a_different_room() {
3715 let mut conn = test_conn();
3716 make_room(&conn);
3717 let other_room = format!("!other:{}", matrix_server_name());
3718 create_room(&conn, &other_room, RoomKind::Group, 1, T0, false, JoinRule::Invite, HistoryVisibility::Shared, None, None)
3719 .expect("other room");
3720 let event = insert_timeline_event(&mut conn, "$e1", &other_room, 1, "m.room.message", "{}", 1000).expect("event in other room");
3721
3722 let err = upsert_receipt(&mut conn, ROOM, 9, ReceiptType::Read, &event.event_id, 5000).unwrap_err();
3723 assert!(matches!(err, MatrixStoreError::WrongRoom(ref id) if id == &event.event_id));
3724 }
3725
3726 #[test]
3729 fn mxid_parse_refuses_foreign_server() {
3730 assert_eq!(public_id_from_mxid("@abc123:example.org"), Ok("abc123"));
3731 assert_eq!(public_id_from_mxid("@abc123:otherserver.example"), Err(MatrixIdError::ForeignServerName));
3732 let aliases = ["chat.example", "m4a.example.net", "m4a.example.org"];
3734 set_local_aliases(aliases.iter().map(|s| s.to_string()));
3735 for name in aliases {
3736 assert_eq!(public_id_from_mxid(&format!("@abc123:{name}")), Ok("abc123"));
3737 }
3738 assert_eq!(public_id_from_mxid("@abc123:evil.example"), Err(MatrixIdError::ForeignServerName));
3739 assert_eq!(mxid_for_public_id("abc123"), format!("@abc123:{}", matrix_server_name()), "minting never uses an alias");
3740 assert_eq!(public_id_from_mxid("abc123:example.org"), Err(MatrixIdError::MissingSigil));
3741 assert_eq!(public_id_from_mxid("@abc123"), Err(MatrixIdError::MissingServerName));
3742 }
3743
3744 #[test]
3747 fn ensure_matrix_user_is_idempotent_and_refuses_a_reserved_localpart() {
3748 let conn = test_conn();
3749 let mxid = ensure_matrix_user(&conn, 1, "abc123", T0).expect("first ensure");
3750 assert_eq!(mxid, "@abc123:example.org");
3751 let mxid_again = ensure_matrix_user(&conn, 1, "abc123", T0).expect("second ensure is a no-op");
3752 assert_eq!(mxid_again, mxid);
3753 assert_eq!(mxid_of(&conn, 1).expect("mxid_of"), Some(mxid.clone()));
3754 assert_eq!(user_id_of(&conn, &mxid).expect("user_id_of"), Some(1));
3755
3756 let err = ensure_matrix_user(&conn, 2, "_bridge_evil", T0).unwrap_err();
3757 assert!(matches!(err, MatrixStoreError::ReservedLocalpart));
3758 }
3759
3760 #[test]
3761 fn apply_state_event_member_refreshes_room_members_and_power_levels() {
3762 let mut conn = test_conn();
3763 make_room(&conn);
3764 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000001", T0).expect("alice");
3765 let bob = ensure_matrix_user(&conn, 2, "bob000000000000000000000000002", T0).expect("bob");
3766
3767 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 1000, now: T0 }).expect("alice joins");
3768 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m2", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &bob, content: r#"{"membership":"invite"}"#, origin_server_ts: 1100, now: T0 }).expect("bob invited");
3769
3770 let joined = room_members(&conn, ROOM, Some(Membership::Join)).expect("joined members");
3771 assert_eq!(joined.len(), 1);
3772 assert_eq!(joined[0].user_id, 1);
3773 assert_eq!(joined[0].power_level, None);
3774
3775 let power_levels_content = serde_json::json!({"users": {alice.clone(): 100}, "users_default": 0}).to_string();
3776 apply_state_event(&mut conn, &StateEventWrite { event_id: "$pl1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.power_levels", state_key: "", content: &power_levels_content, origin_server_ts: 1200, now: T0 }).expect("power levels");
3777
3778 let alice_row = room_members(&conn, ROOM, None)
3779 .expect("all members")
3780 .into_iter()
3781 .find(|m| m.user_id == 1)
3782 .expect("alice row");
3783 assert_eq!(alice_row.power_level, Some(100));
3784
3785 let bob_rooms = rooms_for_user(&conn, 2, Some(Membership::Invite)).expect("bob's invited rooms");
3786 assert_eq!(bob_rooms, vec![ROOM.to_string()]);
3787 }
3788
3789 #[test]
3790 fn events_in_room_after_and_before_page_in_the_documented_order() {
3791 let mut conn = test_conn();
3792 make_room(&conn);
3793 let e1 = insert_timeline_event(&mut conn, "$e1", ROOM, 1, "m.room.message", "{}", 1000).expect("e1");
3794 let e2 = insert_timeline_event(&mut conn, "$e2", ROOM, 1, "m.room.message", "{}", 2000).expect("e2");
3795 let e3 = insert_timeline_event(&mut conn, "$e3", ROOM, 1, "m.room.message", "{}", 3000).expect("e3");
3796
3797 let after = events_in_room_after(&conn, ROOM, e1.stream_id, 10).expect("after");
3798 assert_eq!(after.iter().map(|e| e.event_id.clone()).collect::<Vec<_>>(), vec![e2.event_id.clone(), e3.event_id.clone()]);
3799
3800 let before = events_in_room_before(&conn, ROOM, e3.stream_id, 10).expect("before");
3801 assert_eq!(before.iter().map(|e| e.event_id.clone()).collect::<Vec<_>>(), vec![e2.event_id.clone(), e1.event_id.clone()]);
3802
3803 assert_eq!(max_stream_id(&conn).expect("max"), e3.stream_id);
3804 }
3805
3806 #[test]
3807 fn txn_dedup_lookup_of_a_to_device_send_has_no_event_id() {
3808 let conn = test_conn();
3809 assert_eq!(txn_dedup_lookup(&conn, 1, "DEV1", "txn-td").expect("lookup"), TxnDedupEntry::NotSeen);
3810 txn_dedup_record(&conn, 1, "DEV1", "txn-td", None, T0).expect("record to-device send");
3811 assert_eq!(txn_dedup_lookup(&conn, 1, "DEV1", "txn-td").expect("lookup again"), TxnDedupEntry::Seen(None));
3812 }
3813
3814 #[test]
3815 fn filters_create_and_get_are_scoped_to_their_owner() {
3816 let conn = test_conn();
3817 let filter_id = create_filter(&conn, 1, r#"{"room":{"timeline":{"limit":20}}}"#).expect("create");
3818 assert_eq!(get_filter(&conn, 1, filter_id).expect("owner reads it"), Some(r#"{"room":{"timeline":{"limit":20}}}"#.to_string()));
3819 assert_eq!(get_filter(&conn, 2, filter_id).expect("a different user cannot"), None);
3820 }
3821
3822 #[test]
3823 fn legacy_dm_message_map_insert_and_get_round_trip() {
3824 let mut conn = test_conn();
3825 make_room(&conn);
3826 ensure_legacy_dm_map_table(&conn);
3827 let event = insert_timeline_event(&mut conn, "$legacy1", ROOM, 1, "org.example.legacy_dm", "{}", 1000).expect("insert");
3828 insert_legacy_dm_message_map(&conn, 42, &event.event_id).expect("map insert");
3829 assert_eq!(legacy_dm_message_event_id(&conn, 42).expect("map get"), Some(event.event_id));
3830 assert_eq!(legacy_dm_message_event_id(&conn, 999).expect("missing"), None);
3831 }
3832
3833 #[test]
3836 fn power_level_defaults_apply_when_fields_missing() {
3837 let pl = serde_json::json!({});
3838 assert_eq!(user_level(&pl, "@nobody:example.org"), 0, "users_default defaults to 0");
3839 assert_eq!(event_level(&pl, "m.room.message", false), 0, "events_default defaults to 0");
3840 assert_eq!(event_level(&pl, "m.room.name", true), 50, "state_default defaults to 50");
3841 for action in [PowerAction::Invite, PowerAction::Kick, PowerAction::Ban, PowerAction::Redact, PowerAction::StateDefault] {
3842 assert!(!can(&pl, action, "@nobody:example.org"), "level 0 must not reach the default 50 threshold for {action:?}");
3843 }
3844
3845 let pl_with_creator = serde_json::json!({ "users": { "@creator:example.org": 100 } });
3846 assert!(can(&pl_with_creator, PowerAction::Ban, "@creator:example.org"));
3847 assert!(can(&pl_with_creator, PowerAction::StateDefault, "@creator:example.org"));
3848 }
3849
3850 #[test]
3851 fn event_level_uses_the_events_type_override_before_falling_back_to_a_default() {
3852 let pl = serde_json::json!({ "events": { "m.room.name": 60 }, "events_default": 0, "state_default": 50 });
3853 assert_eq!(event_level(&pl, "m.room.name", true), 60, "an explicit events[type] override wins");
3854 assert_eq!(event_level(&pl, "m.room.topic", true), 50, "an unlisted state type falls back to state_default");
3855 assert_eq!(event_level(&pl, "m.room.message", false), 0, "an unlisted timeline type falls back to events_default");
3856 }
3857
3858 #[test]
3861 fn can_act_on_requires_strictly_greater_level_except_self_leave() {
3862 let pl = serde_json::json!({
3863 "users": { "@owner:example.org": 100, "@admin:example.org": 50, "@peer:example.org": 50 },
3864 "kick": 50,
3865 "ban": 50,
3866 });
3867
3868 assert!(!can_act_on(&pl, PowerAction::Kick, "@admin:example.org", "@owner:example.org", false));
3871 assert!(!can_act_on(&pl, PowerAction::Kick, "@admin:example.org", "@peer:example.org", false));
3873 assert!(can_act_on(&pl, PowerAction::Kick, "@owner:example.org", "@admin:example.org", false));
3875 assert!(can_act_on(&pl, PowerAction::Kick, "@admin:example.org", "@admin:example.org", true));
3878 assert!(!can_act_on(&pl, PowerAction::Ban, "@admin:example.org", "@admin:example.org", false));
3880 }
3881
3882 #[test]
3883 fn validate_power_levels_change_refuses_raising_self_above_own_level() {
3884 let old = serde_json::json!({ "users": { "@admin:example.org": 50 } });
3885 let new = serde_json::json!({ "users": { "@admin:example.org": 100 } });
3886 assert!(validate_power_levels_change(&old, &new, "@admin:example.org").is_err());
3887 }
3888
3889 #[test]
3890 fn validate_power_levels_change_refuses_demoting_a_peer_at_an_equal_level() {
3891 let old = serde_json::json!({ "users": { "@a:example.org": 50, "@b:example.org": 50 } });
3892 let new = serde_json::json!({ "users": { "@a:example.org": 50, "@b:example.org": 0 } });
3893 assert!(validate_power_levels_change(&old, &new, "@a:example.org").is_err());
3894 }
3895
3896 #[test]
3897 fn validate_power_levels_change_allows_demoting_self() {
3898 let old = serde_json::json!({ "users": { "@admin:example.org": 50 } });
3899 let new = serde_json::json!({ "users": { "@admin:example.org": 10 } });
3900 assert!(validate_power_levels_change(&old, &new, "@admin:example.org").is_ok());
3901 }
3902
3903 #[test]
3904 fn validate_power_levels_change_refuses_raising_events_default_above_own_level() {
3905 let old = serde_json::json!({ "users": { "@admin:example.org": 50 }, "events_default": 0 });
3906 let new = serde_json::json!({ "users": { "@admin:example.org": 50 }, "events_default": 60 });
3907 assert!(validate_power_levels_change(&old, &new, "@admin:example.org").is_err());
3908 }
3909
3910 #[test]
3911 fn validate_power_levels_change_allows_the_owner_changing_anything_up_to_their_own_level() {
3912 let old = serde_json::json!({ "users": { "@owner:example.org": 100, "@a:example.org": 50 } });
3913 let new = serde_json::json!({
3914 "users": { "@owner:example.org": 100, "@a:example.org": 90 },
3915 "ban": 100,
3916 "kick": 100,
3917 "events_default": 100,
3918 });
3919 assert!(validate_power_levels_change(&old, &new, "@owner:example.org").is_ok());
3920 }
3921
3922 #[test]
3923 fn validate_power_levels_change_refuses_a_scalar_field_change_above_own_level() {
3924 let old = serde_json::json!({ "users": { "@admin:example.org": 50 }, "ban": 50 });
3925 let new = serde_json::json!({ "users": { "@admin:example.org": 50 }, "ban": 75 });
3926 assert!(validate_power_levels_change(&old, &new, "@admin:example.org").is_err());
3927 }
3928
3929 #[test]
3930 fn validate_power_levels_change_ignores_unchanged_fields() {
3931 let old = serde_json::json!({ "users": { "@admin:example.org": 50 }, "ban": 50, "events": { "m.room.name": 40 } });
3932 let new = old.clone();
3933 assert!(validate_power_levels_change(&old, &new, "@admin:example.org").is_ok());
3934 }
3935
3936 fn bootstrap<'a>(room_id: &'a str, kind: RoomKind, creator: i64) -> RoomBootstrap<'a> {
3939 RoomBootstrap {
3940 room_id,
3941 kind,
3942 creator_user_id: creator,
3943 created_at: T0,
3944 is_encrypted: true,
3945 join_rule: if kind == RoomKind::Channel { JoinRule::Public } else { JoinRule::Invite },
3946 history_visibility: HistoryVisibility::Shared,
3947 dm_pair_key: None,
3948 legacy_dm_id: None,
3949 }
3950 }
3951
3952 fn state_event(event_id: &str, sender: i64, event_type: &str, state_key: &str, content: &str) -> NewStateEvent {
3953 NewStateEvent {
3954 event_id: event_id.to_string(),
3955 sender_user_id: sender,
3956 event_type: event_type.to_string(),
3957 state_key: state_key.to_string(),
3958 content: content.to_string(),
3959 }
3960 }
3961
3962 #[test]
3963 fn create_room_with_state_inserts_the_room_and_every_bootstrap_event_atomically() {
3964 let mut conn = test_conn();
3965 let creator_mxid = ensure_matrix_user(&conn, 1, "creator0000000000000000000001", T0).expect("creator");
3966 let room_id = "!batch:example.org";
3967
3968 let events = vec![
3969 state_event("$create", 1, "m.room.create", "", r#"{"room_version":"11"}"#),
3970 state_event("$m1", 1, "m.room.member", &creator_mxid, r#"{"membership":"join"}"#),
3971 state_event(
3972 "$pl",
3973 1,
3974 "m.room.power_levels",
3975 "",
3976 &serde_json::json!({"users": {creator_mxid.clone(): 100}, "users_default": 0}).to_string(),
3977 ),
3978 ];
3979
3980 let (room, applied) = create_room_with_state(&mut conn, bootstrap(room_id, RoomKind::Group, 1), &events, 1000).expect("create batch");
3981 assert_eq!(room.id, room_id);
3982 assert_eq!(applied.len(), 3);
3983 assert!(get_room(&conn, room_id).expect("get room").is_some());
3984 for event in &applied {
3985 assert!(get_event(&conn, &event.event_id).expect("get event").is_some());
3986 }
3987 let creator_row = room_member(&conn, room_id, 1).expect("member row").expect("row exists");
3988 assert_eq!(creator_row.power_level, Some(100), "power_levels applied after the member row existed");
3989 }
3990
3991 #[test]
3992 fn create_room_is_atomic_on_failure() {
3993 let mut conn = test_conn();
3994 let creator_mxid = ensure_matrix_user(&conn, 1, "creator0000000000000000000002", T0).expect("creator");
3995 let room_id = "!atomic:example.org";
3996
3997 let events = vec![
3998 state_event("$create", 1, "m.room.create", "", r#"{"room_version":"11"}"#),
3999 state_event("$m1", 1, "m.room.member", &creator_mxid, r#"{"membership":"join"}"#),
4000 state_event("$bad", 1, "m.room.member", "@ghost:example.org", r#"{"membership":"invite"}"#),
4003 ];
4004
4005 let err = create_room_with_state(&mut conn, bootstrap(room_id, RoomKind::Group, 1), &events, 1000).unwrap_err();
4006 assert!(matches!(err, MatrixStoreError::UnknownMxid(ref m) if m == "@ghost:example.org"));
4007
4008 assert_eq!(get_room(&conn, room_id).expect("get room"), None, "a failed batch must leave no room row");
4009 assert_eq!(get_event(&conn, "$create").expect("get"), None, "a failed batch must leave no event rows at all");
4010 assert_eq!(get_event(&conn, "$m1").expect("get"), None);
4011 }
4012
4013 #[test]
4016 fn dm_pair_key_reuse_is_freed_once_the_room_is_not_reused() {
4017 let conn = test_conn();
4018 create_room(&conn, "!dm1:example.org", RoomKind::Dm, 1, T0, true, JoinRule::Invite, HistoryVisibility::Shared, Some("1:2"), None)
4019 .expect("first dm room");
4020 assert_eq!(room_by_dm_pair_key(&conn, "1:2").expect("lookup").map(|r| r.id), Some("!dm1:example.org".to_string()));
4021
4022 clear_dm_pair_key(&conn, "!dm1:example.org").expect("clear");
4023 assert_eq!(room_by_dm_pair_key(&conn, "1:2").expect("lookup after clear"), None);
4024
4025 create_room(&conn, "!dm2:example.org", RoomKind::Dm, 1, T0, true, JoinRule::Invite, HistoryVisibility::Shared, Some("1:2"), None)
4027 .expect("second dm room reuses the freed pair key");
4028 assert_eq!(room_by_dm_pair_key(&conn, "1:2").expect("lookup").map(|r| r.id), Some("!dm2:example.org".to_string()));
4029 }
4030
4031 #[test]
4034 fn room_member_finds_the_one_row_forget_membership_deletes_only_when_left() {
4035 let mut conn = test_conn();
4036 make_room(&conn);
4037 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000099", T0).expect("alice");
4038 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 1000, now: T0 }).expect("join");
4039
4040 assert_eq!(room_member(&conn, ROOM, 1).expect("member").map(|m| m.membership), Some(Membership::Join));
4041 assert_eq!(room_member(&conn, ROOM, 999).expect("no such member"), None);
4042
4043 assert_eq!(forget_membership(&conn, ROOM, 1).expect("forget while joined"), 0);
4046 assert!(room_member(&conn, ROOM, 1).expect("still a member").is_some());
4047
4048 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m2", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"leave"}"#, origin_server_ts: 2000, now: T0 }).expect("leave");
4049 assert_eq!(forget_membership(&conn, ROOM, 1).expect("forget after leaving"), 1);
4050 assert_eq!(room_member(&conn, ROOM, 1).expect("gone"), None);
4051 }
4052
4053 #[test]
4056 fn state_events_of_type_at_excludes_state_keys_created_after_the_cutoff() {
4057 let mut conn = test_conn();
4058 make_room(&conn);
4059 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000098", T0).expect("alice");
4060 let bob = ensure_matrix_user(&conn, 2, "bob0000000000000000000000000098", T0).expect("bob");
4061
4062 let e1 = apply_state_event(&mut conn, &StateEventWrite { event_id: "$m1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 1000, now: T0 }).expect("alice joins");
4063 let cutoff = e1.stream_id;
4064 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m2", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &bob, content: r#"{"membership":"invite"}"#, origin_server_ts: 1100, now: T0 }).expect("bob invited later");
4065
4066 let at_cutoff = state_events_of_type_at(&conn, ROOM, "m.room.member", cutoff).expect("at cutoff");
4067 assert_eq!(at_cutoff.len(), 1, "bob's invite lands strictly after the cutoff and must be excluded");
4068 assert_eq!(at_cutoff[0].event_id, "$m1");
4069
4070 let after_both = state_events_of_type_at(&conn, ROOM, "m.room.member", cutoff + 1).expect("after both");
4071 assert_eq!(after_both.len(), 2);
4072 }
4073
4074 #[test]
4077 fn stripped_invite_state_includes_room_basics_and_the_inviters_own_member_event() {
4078 let mut conn = test_conn();
4079 make_room(&conn);
4080 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000097", T0).expect("alice");
4081 let bob = ensure_matrix_user(&conn, 2, "bob0000000000000000000000000097", T0).expect("bob");
4082
4083 apply_state_event(&mut conn, &StateEventWrite { event_id: "$create", room_id: ROOM, sender_user_id: 1, event_type: "m.room.create", state_key: "", content: r#"{"room_version":"11"}"#, origin_server_ts: 900, now: T0 }).expect("create");
4084 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 1000, now: T0 }).expect("alice joins");
4085 apply_state_event(&mut conn, &StateEventWrite { event_id: "$jr", room_id: ROOM, sender_user_id: 1, event_type: "m.room.join_rules", state_key: "", content: r#"{"join_rule":"invite"}"#, origin_server_ts: 1100, now: T0 }).expect("join rules");
4086 apply_state_event(&mut conn, &StateEventWrite { event_id: "$m2", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &bob, content: r#"{"membership":"invite"}"#, origin_server_ts: 1200, now: T0 }).expect("bob invited");
4087
4088 let stripped = stripped_invite_state(&conn, ROOM, 1).expect("stripped state");
4089 let types: Vec<&str> = stripped.iter().map(|v| v["type"].as_str().expect("type")).collect();
4090 assert!(types.contains(&"m.room.create"));
4091 assert!(types.contains(&"m.room.join_rules"));
4092 assert!(!types.contains(&"m.room.encryption"), "no encryption event exists in this room");
4093
4094 let inviter_member = stripped
4095 .iter()
4096 .find(|v| v["type"] == "m.room.member" && v["state_key"] == alice)
4097 .expect("the inviter's own member event is included");
4098 assert_eq!(inviter_member["sender"], alice);
4099 assert_eq!(inviter_member["content"]["membership"], "join");
4100 }
4101
4102 #[test]
4105 fn user_ids_with_leave_transition_in_rooms_finds_only_leave_and_ban_inside_the_window() {
4106 let mut conn = test_conn();
4107 make_room(&conn);
4108 let alice = ensure_matrix_user(&conn, 2, "alice00000000000000000000000097", T0).expect("alice");
4109 let bob = ensure_matrix_user(&conn, 3, "bob0000000000000000000000000097", T0).expect("bob");
4110 let carol = ensure_matrix_user(&conn, 4, "carol0000000000000000000000097a", T0).expect("carol");
4111
4112 apply_state_event(&mut conn, &StateEventWrite { event_id: "$a1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 1000, now: T0 }).expect("alice joins");
4113 let boundary = apply_state_event(&mut conn, &StateEventWrite { event_id: "$b1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &bob, content: r#"{"membership":"join"}"#, origin_server_ts: 1100, now: T0 })
4114 .expect("bob joins")
4115 .stream_id;
4116 apply_state_event(&mut conn, &StateEventWrite { event_id: "$a2", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"leave"}"#, origin_server_ts: 1200, now: T0 }).expect("alice leaves");
4117 apply_state_event(&mut conn, &StateEventWrite { event_id: "$b2", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &bob, content: r#"{"membership":"ban"}"#, origin_server_ts: 1300, now: T0 }).expect("bob banned");
4118 apply_state_event(&mut conn, &StateEventWrite { event_id: "$c1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &carol, content: r#"{"membership":"join"}"#, origin_server_ts: 1400, now: T0 }).expect("carol joins (not a departure)");
4119
4120 let mut left = user_ids_with_leave_transition_in_rooms(&conn, &[ROOM.to_string()], boundary, i64::MAX).expect("query");
4121 left.sort_unstable();
4122 assert_eq!(left, vec![2, 3], "alice (leave) and bob (ban) both count; carol's join does not");
4123
4124 let empty = user_ids_with_leave_transition_in_rooms(&conn, &[], 0, i64::MAX).expect("empty room set");
4125 assert!(empty.is_empty());
4126 }
4127
4128 #[test]
4131 fn membership_at_reads_the_state_as_of_a_stream_position() {
4132 let mut conn = test_conn();
4133 make_room(&conn);
4134 let alice = ensure_matrix_user(&conn, 2, "alice00000000000000000000000081", T0).expect("alice");
4135 let bob = ensure_matrix_user(&conn, 3, "bob0000000000000000000000000081", T0).expect("bob");
4136
4137 let alice_joined = apply_state_event(&mut conn, &StateEventWrite { event_id: "$a1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 1000, now: T0 }).expect("alice joins").stream_id;
4138 let bob_invited = apply_state_event(&mut conn, &StateEventWrite { event_id: "$b1", room_id: ROOM, sender_user_id: 1, event_type: "m.room.member", state_key: &bob, content: r#"{"membership":"invite"}"#, origin_server_ts: 1100, now: T0 }).expect("bob invited").stream_id;
4139 let bob_joined = apply_state_event(&mut conn, &StateEventWrite { event_id: "$b2", room_id: ROOM, sender_user_id: 3, event_type: "m.room.member", state_key: &bob, content: r#"{"membership":"join","displayname":"Bob"}"#, origin_server_ts: 1200, now: T0 }).expect("bob joins").stream_id;
4140
4141 assert_eq!(membership_at(&conn, ROOM, &bob, alice_joined).expect("query"), None, "bob had no member event yet");
4142 assert_eq!(membership_at(&conn, ROOM, &bob, bob_invited).expect("query"), Some(Membership::Invite));
4143 assert_eq!(membership_at(&conn, ROOM, &bob, bob_joined).expect("query"), Some(Membership::Join));
4144 assert_eq!(membership_at(&conn, ROOM, &alice, bob_invited).expect("query"), Some(Membership::Join));
4145
4146 let mut window = member_state_keys_in_window(&conn, ROOM, bob_invited, bob_joined).expect("window");
4147 window.sort();
4148 assert_eq!(window, vec![bob.clone()], "only bob has a member event after `bob_invited`");
4149 let mut all = member_state_keys_in_window(&conn, ROOM, 0, bob_joined).expect("whole history");
4150 all.sort();
4151 let mut expected = vec![alice, bob];
4152 expected.sort();
4153 assert_eq!(all, expected);
4154
4155 let rooms = [ROOM.to_string(), "!other:example.org".to_string()];
4156 assert_eq!(rooms_with_member_events_in_window(&conn, &rooms, bob_invited, bob_joined).expect("rooms"), vec![ROOM.to_string()]);
4157 assert!(rooms_with_member_events_in_window(&conn, &rooms, bob_joined, i64::MAX).expect("rooms after the last member event").is_empty());
4158 assert!(rooms_with_member_events_in_window(&conn, &[], 0, i64::MAX).expect("empty room set").is_empty());
4159 }
4160
4161 const ROOM_A: &str = "!roomA:example.org";
4164 const ROOM_B: &str = "!roomB:example.org";
4165 const ROOM_C: &str = "!roomC:example.org";
4166
4167 fn make_room_with_kind(conn: &Connection, room_id: &str, kind: RoomKind) {
4168 create_room(conn, room_id, kind, 1, T0, false, JoinRule::Invite, HistoryVisibility::Shared, None, None).expect("create room");
4169 }
4170
4171 fn member_content_in(conn: &Connection, room_id: &str, mxid: &str) -> serde_json::Value {
4172 let event = current_state_event(conn, room_id, "m.room.member", mxid).expect("query").expect("member event exists");
4173 serde_json::from_str(&event.content).expect("member content is json")
4174 }
4175
4176 fn member_event_count(conn: &Connection, room_id: &str, mxid: &str) -> i64 {
4177 conn.query_row(
4178 "SELECT COUNT(*) FROM events WHERE room_id = ?1 AND event_type = 'm.room.member' AND state_key = ?2",
4179 params![room_id, mxid],
4180 |row| row.get(0),
4181 )
4182 .expect("count member events")
4183 }
4184
4185 #[test]
4186 fn refresh_member_displayname_restamps_every_joined_room_and_skips_a_room_the_user_left() {
4187 let mut conn = test_conn();
4188 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000091", T0).expect("alice");
4189 let bob = ensure_matrix_user(&conn, 2, "bob0000000000000000000000000091", T0).expect("bob");
4190 for room in [ROOM_A, ROOM_B, ROOM_C] {
4191 make_room_with_kind(&conn, room, RoomKind::Group);
4192 apply_state_event(&mut conn, &StateEventWrite { event_id: &new_event_id(), room_id: room, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join","displayname":"old_alice"}"#, origin_server_ts: 1000, now: T0 })
4193 .expect("alice joins");
4194 apply_state_event(&mut conn, &StateEventWrite { event_id: &new_event_id(), room_id: room, sender_user_id: 2, event_type: "m.room.member", state_key: &bob, content: r#"{"membership":"join","displayname":"bob_nick"}"#, origin_server_ts: 1100, now: T0 })
4195 .expect("bob joins");
4196 }
4197 apply_state_event(&mut conn, &StateEventWrite { event_id: &new_event_id(), room_id: ROOM_C, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"leave"}"#, origin_server_ts: 1200, now: T0 }).expect("alice leaves C");
4198
4199 let refresh = refresh_member_displayname(&mut conn, 1, "new_alice", T0, 5000).expect("refresh");
4200
4201 assert_eq!(refresh.rooms_updated, 2, "the two rooms alice is still joined in");
4202 for room in [ROOM_A, ROOM_B] {
4203 let content = member_content_in(&conn, room, &alice);
4204 assert_eq!(content["membership"], "join");
4205 assert_eq!(content["displayname"], "new_alice");
4206 assert_eq!(member_event_count(&conn, room, &alice), 2, "a NEW member event lands; the old one stays in history");
4207 let event = current_state_event(&conn, room, "m.room.member", &alice).expect("query").expect("exists");
4208 assert_eq!(event.sender_user_id, 1, "a join refresh is sent by the user themself");
4209 assert_eq!(event.origin_server_ts, 5000);
4210 assert_eq!(member_content_in(&conn, room, &bob)["displayname"], "bob_nick", "another member's event is never touched");
4211 }
4212 assert_eq!(member_content_in(&conn, ROOM_C, &alice), serde_json::json!({ "membership": "leave" }), "the left room gets no new event");
4213 assert_eq!(member_event_count(&conn, ROOM_C, &alice), 2, "join + leave, nothing more");
4214 assert_eq!(refresh.affected_user_ids, HashSet::from([1, 2]), "alice and bob are woken; room C's members are not part of it");
4215
4216 let repeat = refresh_member_displayname(&mut conn, 1, "new_alice", T0, 6000).expect("second pass");
4217 assert_eq!(repeat, DisplaynameRefresh::default(), "an up-to-date event is skipped, so a repeat pass writes nothing");
4218 assert_eq!(member_event_count(&conn, ROOM_A, &alice), 2);
4219 }
4220
4221 #[test]
4222 fn refresh_member_displayname_keeps_an_invite_events_sender_and_is_direct() {
4223 let mut conn = test_conn();
4224 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000092", T0).expect("alice");
4225 let bob = ensure_matrix_user(&conn, 2, "bob0000000000000000000000000092", T0).expect("bob");
4226 make_room_with_kind(&conn, ROOM_A, RoomKind::Dm);
4227 apply_state_event(&mut conn, &StateEventWrite { event_id: &new_event_id(), room_id: ROOM_A, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join","displayname":"alice_nick"}"#, origin_server_ts: 1000, now: T0 })
4228 .expect("alice joins");
4229 apply_state_event(
4230 &mut conn,
4231 &StateEventWrite {
4232 event_id: &new_event_id(),
4233 room_id: ROOM_A,
4234 sender_user_id: 1,
4235 event_type: "m.room.member",
4236 state_key: &bob,
4237 content: r#"{"membership":"invite","is_direct":true}"#,
4238 origin_server_ts: 1100,
4239 now: T0,
4240 },
4241 )
4242 .expect("alice invites bob");
4243
4244 let refresh = refresh_member_displayname(&mut conn, 2, "bob_nick", T0, 5000).expect("refresh");
4245
4246 assert_eq!(refresh.rooms_updated, 1);
4247 let content = member_content_in(&conn, ROOM_A, &bob);
4248 assert_eq!(content, serde_json::json!({ "membership": "invite", "is_direct": true, "displayname": "bob_nick" }));
4249 let event = current_state_event(&conn, ROOM_A, "m.room.member", &bob).expect("query").expect("exists");
4250 assert_eq!(event.sender_user_id, 1, "the inviter stays the sender: stripped invite state reads the inviter off it");
4251 assert_eq!(room_member(&conn, ROOM_A, 2).expect("query").expect("row").membership, Membership::Invite, "still an invitation");
4252 assert_eq!(refresh.affected_user_ids, HashSet::from([1, 2]));
4253 }
4254
4255 #[test]
4256 fn refresh_member_displayname_is_a_noop_without_a_matrix_user_or_a_label() {
4257 let mut conn = test_conn();
4258 let alice = ensure_matrix_user(&conn, 1, "alice00000000000000000000000093", T0).expect("alice");
4259 make_room_with_kind(&conn, ROOM_A, RoomKind::Group);
4260 apply_state_event(&mut conn, &StateEventWrite { event_id: &new_event_id(), room_id: ROOM_A, sender_user_id: 1, event_type: "m.room.member", state_key: &alice, content: r#"{"membership":"join"}"#, origin_server_ts: 1000, now: T0 }).expect("alice joins");
4261
4262 assert_eq!(refresh_member_displayname(&mut conn, 99, "ghost", T0, 5000).expect("unknown user"), DisplaynameRefresh::default());
4263 assert_eq!(refresh_member_displayname(&mut conn, 1, "", T0, 5000).expect("empty label"), DisplaynameRefresh::default());
4264 assert_eq!(member_event_count(&conn, ROOM_A, &alice), 1);
4265 assert_eq!(matrix_user_ids(&conn).expect("ids"), vec![1]);
4266 }
4267
4268 fn native_dm_room(conn: &Connection) {
4271 create_room(conn, ROOM, RoomKind::Dm, 1, T0, true, JoinRule::Invite, HistoryVisibility::Shared, Some("1:2"), None).expect("create dm room");
4272 }
4273
4274 fn legacy_import(legacy_message_id: i64, event_id: &str) -> LegacyDmMessageImport {
4275 LegacyDmMessageImport {
4276 legacy_message_id,
4277 event_id: event_id.to_string(),
4278 sender_user_id: 1,
4279 content: "{}".to_string(),
4280 origin_server_ts: 1000 + legacy_message_id,
4281 }
4282 }
4283
4284 fn adoption_of(legacy_dm_id: i64) -> DmAdoption<'static> {
4285 DmAdoption { room_id: ROOM, legacy_dm_id, key_events: &[], key_events_origin_server_ts: 500, now: T0 }
4286 }
4287
4288 #[test]
4289 fn adopt_dm_room_for_legacy_binds_the_room_and_imports_the_messages() {
4290 let mut conn = test_conn();
4291 ensure_legacy_dm_map_table(&conn);
4292 native_dm_room(&conn);
4293
4294 let counts = adopt_dm_room_for_legacy(&mut conn, adoption_of(7), &[legacy_import(1, "$l1"), legacy_import(2, "$l2")], &[])
4295 .expect("adopt")
4296 .expect("the room is adoptable");
4297 assert_eq!(counts.messages_imported, 2);
4298 assert_eq!(room_by_legacy_dm_id(&conn, 7).expect("query").expect("bound").id, ROOM);
4299 assert_eq!(highest_mapped_legacy_message_id(&conn, ROOM).expect("query"), Some(2));
4300 assert_eq!(get_event(&conn, "$l1").expect("query").expect("imported").room_id, ROOM);
4301 }
4302
4303 #[test]
4304 fn adopt_dm_room_for_legacy_writes_a_key_event_only_where_the_room_has_none() {
4305 let mut conn = test_conn();
4306 ensure_legacy_dm_map_table(&conn);
4307 native_dm_room(&conn);
4308 let existing = apply_state_event(&mut conn, &StateEventWrite { event_id: "$k-existing", room_id: ROOM, sender_user_id: 1, event_type: "org.example.legacy_dm_key", state_key: "@a:example.org", content: r#"{"public_key_b64":"AAAA"}"#, origin_server_ts: 900, now: T0 })
4309 .expect("existing key event");
4310 let key_events = [
4311 NewStateEvent {
4312 event_id: "$k-a".to_string(),
4313 sender_user_id: 1,
4314 event_type: "org.example.legacy_dm_key".to_string(),
4315 state_key: "@a:example.org".to_string(),
4316 content: r#"{"public_key_b64":"BBBB"}"#.to_string(),
4317 },
4318 NewStateEvent {
4319 event_id: "$k-b".to_string(),
4320 sender_user_id: 2,
4321 event_type: "org.example.legacy_dm_key".to_string(),
4322 state_key: "@b:example.org".to_string(),
4323 content: r#"{"public_key_b64":"CCCC"}"#.to_string(),
4324 },
4325 ];
4326 let adoption = DmAdoption { key_events: &key_events, ..adoption_of(7) };
4327 adopt_dm_room_for_legacy(&mut conn, adoption, &[], &[]).expect("adopt").expect("adoptable");
4328
4329 let a = current_state_event(&conn, ROOM, "org.example.legacy_dm_key", "@a:example.org").expect("query").expect("still present");
4330 assert_eq!(a.event_id, existing.event_id, "an existing key event is never overwritten");
4331 let b = current_state_event(&conn, ROOM, "org.example.legacy_dm_key", "@b:example.org").expect("query").expect("added");
4332 assert_eq!(b.event_id, "$k-b");
4333 }
4334
4335 #[test]
4336 fn adopt_dm_room_for_legacy_refuses_a_room_that_is_bound_or_not_a_dm() {
4337 let mut conn = test_conn();
4338 ensure_legacy_dm_map_table(&conn);
4339 make_room(&conn);
4340 assert!(adopt_dm_room_for_legacy(&mut conn, adoption_of(7), &[legacy_import(1, "$l1")], &[]).expect("adopt").is_none(), "a group room is never adopted");
4341 assert!(room_by_legacy_dm_id(&conn, 7).expect("query").is_none());
4342
4343 let mut bound = test_conn();
4344 create_room(&bound, ROOM, RoomKind::Dm, 1, T0, true, JoinRule::Invite, HistoryVisibility::Shared, Some("1:2"), Some(9)).expect("create bound room");
4345 assert!(adopt_dm_room_for_legacy(&mut bound, adoption_of(7), &[legacy_import(1, "$l1")], &[]).expect("adopt").is_none(), "an already-bound room is never re-bound");
4346 assert_eq!(room_by_legacy_dm_id(&bound, 9).expect("query").expect("still bound to 9").id, ROOM);
4347 assert!(get_event(&bound, "$l1").expect("query").is_none(), "a refused adoption writes nothing");
4348 }
4349
4350 #[test]
4351 fn adopt_dm_room_for_legacy_rolls_back_the_binding_when_an_import_fails() {
4352 let mut conn = test_conn();
4353 ensure_legacy_dm_map_table(&conn);
4354 native_dm_room(&conn);
4355
4356 let result = adopt_dm_room_for_legacy(&mut conn, adoption_of(7), &[legacy_import(1, "$dup"), legacy_import(2, "$dup")], &[]);
4359 assert!(result.is_err());
4360
4361 assert!(room_by_legacy_dm_id(&conn, 7).expect("query").is_none(), "the binding must roll back with the failed import");
4362 assert!(get_event(&conn, "$dup").expect("query").is_none(), "no imported event survives");
4363 assert_eq!(highest_mapped_legacy_message_id(&conn, ROOM).expect("query"), None);
4364 }
4365}