1use rusqlite::params;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4
5use crate::error::Result;
6use crate::storage::Db;
7
8#[derive(Debug, Clone)]
13pub struct StoredIdentity {
14 pub ed25519_secret: Vec<u8>,
15 pub created_at: i64,
16}
17
18pub fn save_identity(db: &Db, secret: &[u8], created_at: i64) -> Result<()> {
19 let conn = db.lock().unwrap();
20 conn.execute(
21 "INSERT OR REPLACE INTO identity (id, ed25519_secret, olm_account_data, created_at) VALUES (1, ?1, NULL, ?2)",
22 params![secret, created_at],
23 )?;
24 Ok(())
25}
26
27pub fn load_identity(db: &Db) -> Result<Option<StoredIdentity>> {
28 let conn = db.lock().unwrap();
29 let mut stmt = conn.prepare("SELECT ed25519_secret, created_at FROM identity WHERE id = 1")?;
30 let mut rows = stmt.query_map([], |row| {
31 Ok(StoredIdentity {
32 ed25519_secret: row.get(0)?,
33 created_at: row.get(1)?,
34 })
35 })?;
36 match rows.next() {
37 Some(row) => Ok(Some(row?)),
38 None => Ok(None),
39 }
40}
41
42pub fn get_display_name(db: &Db) -> Result<Option<String>> {
43 let conn = db.lock().unwrap();
44 let mut stmt = conn.prepare("SELECT display_name FROM identity WHERE id = 1")?;
45 let mut rows = stmt.query_map([], |row| row.get::<_, Option<String>>(0))?;
46 Ok(rows.next().and_then(|r| r.ok()).flatten())
47}
48
49pub fn set_display_name(db: &Db, name: Option<&str>) -> Result<()> {
50 let conn = db.lock().unwrap();
51 conn.execute(
52 "UPDATE identity SET display_name = ?1 WHERE id = 1",
53 params![name],
54 )?;
55 Ok(())
56}
57
58pub fn lookup_display_name(db: &Db, fingerprint: &str) -> Result<Option<String>> {
67 let conn = db.lock().unwrap();
68 let mut stmt = conn.prepare(
69 "SELECT display_name FROM room_members
70 WHERE fingerprint = ?1 AND display_name IS NOT NULL
71 ORDER BY last_seen DESC LIMIT 1",
72 )?;
73 let mut rows = stmt.query_map(params![fingerprint], |row| row.get::<_, Option<String>>(0))?;
74 Ok(rows.next().and_then(|r| r.ok()).flatten())
75}
76
77pub fn set_member_display_name(
78 db: &Db,
79 room_id: &str,
80 fingerprint: &str,
81 name: Option<&str>,
82) -> Result<()> {
83 let conn = db.lock().unwrap();
84 conn.execute(
85 "UPDATE room_members SET display_name = ?1 WHERE room_id = ?2 AND fingerprint = ?3",
86 params![name, room_id, fingerprint],
87 )?;
88 Ok(())
89}
90
91#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum RoomKind {
103 Direct,
104 #[default]
105 Group,
106}
107
108impl RoomKind {
109 pub fn as_str(&self) -> &'static str {
110 match self {
111 RoomKind::Direct => "direct",
112 RoomKind::Group => "group",
113 }
114 }
115
116 pub fn from_str(s: &str) -> Self {
117 match s {
118 "direct" => RoomKind::Direct,
119 _ => RoomKind::Group,
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
125pub struct StoredRoom {
126 pub id: String,
127 pub name: String,
128 pub creator_fingerprint: String,
129 pub encrypted: bool,
130 pub passphrase_salt: Option<Vec<u8>>,
131 pub created_at: i64,
132 pub last_active: Option<i64>,
133 pub kind: RoomKind,
136}
137
138pub fn derive_room_id(creator_fp: &str, name: &str, created_at: i64) -> String {
140 let mut hasher = Sha256::new();
141 hasher.update(creator_fp.as_bytes());
142 hasher.update(b"\0");
143 hasher.update(name.as_bytes());
144 hasher.update(b"\0");
145 hasher.update(created_at.to_be_bytes());
146 hex::encode(&hasher.finalize()[..16])
147}
148
149pub fn insert_room(db: &Db, room: &StoredRoom) -> Result<()> {
156 let conn = db.lock().unwrap();
157 conn.execute(
158 "INSERT INTO rooms (id, name, creator_fingerprint, encrypted, passphrase_salt, created_at, last_active, kind)
159 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
160 ON CONFLICT(id) DO UPDATE SET
161 name = excluded.name,
162 passphrase_salt = excluded.passphrase_salt,
163 last_active = excluded.last_active",
164 params![
165 room.id,
166 room.name,
167 room.creator_fingerprint,
168 room.encrypted as i64,
169 room.passphrase_salt,
170 room.created_at,
171 room.last_active,
172 room.kind.as_str(),
173 ],
174 )?;
175 Ok(())
176}
177
178pub fn get_room(db: &Db, room_id: &str) -> Result<Option<StoredRoom>> {
179 let conn = db.lock().unwrap();
180 let mut stmt = conn.prepare(
181 "SELECT id, name, creator_fingerprint, encrypted, passphrase_salt, created_at, last_active, kind
182 FROM rooms WHERE id = ?1",
183 )?;
184 let mut rows = stmt.query_map(params![room_id], |row| {
185 Ok(StoredRoom {
186 id: row.get(0)?,
187 name: row.get(1)?,
188 creator_fingerprint: row.get(2)?,
189 encrypted: row.get::<_, i64>(3)? != 0,
190 passphrase_salt: row.get(4)?,
191 created_at: row.get(5)?,
192 last_active: row.get(6)?,
193 kind: RoomKind::from_str(&row.get::<_, String>(7).unwrap_or_else(|_| "group".into())),
194 })
195 })?;
196 match rows.next() {
197 Some(row) => Ok(Some(row?)),
198 None => Ok(None),
199 }
200}
201
202pub fn list_rooms(db: &Db) -> Result<Vec<StoredRoom>> {
203 let conn = db.lock().unwrap();
204 let mut stmt = conn.prepare(
205 "SELECT id, name, creator_fingerprint, encrypted, passphrase_salt, created_at, last_active, kind
206 FROM rooms ORDER BY last_active DESC NULLS LAST, created_at DESC",
207 )?;
208 let rows = stmt.query_map([], |row| {
209 Ok(StoredRoom {
210 id: row.get(0)?,
211 name: row.get(1)?,
212 creator_fingerprint: row.get(2)?,
213 encrypted: row.get::<_, i64>(3)? != 0,
214 passphrase_salt: row.get(4)?,
215 created_at: row.get(5)?,
216 last_active: row.get(6)?,
217 kind: RoomKind::from_str(&row.get::<_, String>(7).unwrap_or_else(|_| "group".into())),
218 })
219 })?;
220 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
221}
222
223pub fn find_dm_with(db: &Db, our_fp: &str, partner_fp: &str) -> Result<Option<StoredRoom>> {
228 let conn = db.lock().unwrap();
229 let mut stmt = conn.prepare(
230 "SELECT r.id, r.name, r.creator_fingerprint, r.encrypted, r.passphrase_salt,
231 r.created_at, r.last_active, r.kind
232 FROM rooms r
233 WHERE r.kind = 'direct'
234 AND EXISTS (SELECT 1 FROM room_members m
235 WHERE m.room_id = r.id AND m.fingerprint = ?1)
236 AND EXISTS (SELECT 1 FROM room_members m
237 WHERE m.room_id = r.id AND m.fingerprint = ?2)
238 LIMIT 1",
239 )?;
240 let mut rows = stmt.query_map(params![our_fp, partner_fp], |row| {
241 Ok(StoredRoom {
242 id: row.get(0)?,
243 name: row.get(1)?,
244 creator_fingerprint: row.get(2)?,
245 encrypted: row.get::<_, i64>(3)? != 0,
246 passphrase_salt: row.get(4)?,
247 created_at: row.get(5)?,
248 last_active: row.get(6)?,
249 kind: RoomKind::from_str(&row.get::<_, String>(7).unwrap_or_else(|_| "group".into())),
250 })
251 })?;
252 match rows.next() {
253 Some(row) => Ok(Some(row?)),
254 None => Ok(None),
255 }
256}
257
258pub fn update_room_last_active(db: &Db, room_id: &str, ts: i64) -> Result<()> {
259 let conn = db.lock().unwrap();
260 conn.execute(
261 "UPDATE rooms SET last_active = ?1 WHERE id = ?2",
262 params![ts, room_id],
263 )?;
264 Ok(())
265}
266
267pub fn set_room_muted(db: &Db, room_id: &str, muted: bool) -> Result<()> {
268 let conn = db.lock().unwrap();
269 conn.execute(
270 "UPDATE rooms SET muted = ?1 WHERE id = ?2",
271 params![muted as i64, room_id],
272 )?;
273 Ok(())
274}
275
276pub fn is_room_muted(db: &Db, room_id: &str) -> Result<bool> {
277 let conn = db.lock().unwrap();
278 let mut stmt = conn.prepare("SELECT muted FROM rooms WHERE id = ?1")?;
279 let mut rows = stmt.query_map(params![room_id], |row| row.get::<_, i64>(0))?;
280 Ok(rows.next().map(|r| r.unwrap_or(0) != 0).unwrap_or(false))
281}
282
283#[derive(Debug, Clone)]
288pub struct StoredRoomMember {
289 pub room_id: String,
290 pub peer_id: String,
291 pub fingerprint: String,
292 pub last_seen: Option<i64>,
293 pub verified: bool,
294 pub ed25519_pubkey: Option<String>,
299 pub role: String,
304}
305
306pub fn upsert_room_member(db: &Db, member: &StoredRoomMember) -> Result<()> {
315 let conn = db.lock().unwrap();
316 conn.execute(
317 "INSERT INTO room_members (room_id, peer_id, fingerprint, last_seen, verified, ed25519_pubkey, role)
318 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
319 ON CONFLICT(room_id, fingerprint) DO UPDATE SET
320 last_seen = excluded.last_seen,
321 peer_id = CASE
322 WHEN excluded.peer_id != '' THEN excluded.peer_id
323 ELSE room_members.peer_id
324 END,
325 ed25519_pubkey = COALESCE(excluded.ed25519_pubkey, room_members.ed25519_pubkey)",
326 params![
327 member.room_id,
328 member.peer_id,
329 member.fingerprint,
330 member.last_seen,
331 member.verified as i64,
332 member.ed25519_pubkey,
333 member.role,
334 ],
335 )?;
336 Ok(())
337}
338
339pub fn lookup_peer_ed25519_pubkey(db: &Db, fingerprint: &str) -> Result<Option<String>> {
344 let conn = db.lock().unwrap();
345 let mut stmt = conn.prepare(
346 "SELECT ed25519_pubkey FROM room_members
347 WHERE fingerprint = ?1 AND ed25519_pubkey IS NOT NULL
348 LIMIT 1",
349 )?;
350 let mut rows = stmt.query_map(params![fingerprint], |row| row.get::<_, Option<String>>(0))?;
351 Ok(rows.next().and_then(|r| r.ok()).flatten())
352}
353
354pub fn list_room_members(db: &Db, room_id: &str) -> Result<Vec<StoredRoomMember>> {
355 let conn = db.lock().unwrap();
356 let mut stmt = conn.prepare(
357 "SELECT room_id, peer_id, fingerprint, last_seen, verified, ed25519_pubkey, role FROM room_members WHERE room_id = ?1",
358 )?;
359 let rows = stmt.query_map(params![room_id], |row| {
360 Ok(StoredRoomMember {
361 room_id: row.get(0)?,
362 peer_id: row.get(1)?,
363 fingerprint: row.get(2)?,
364 last_seen: row.get(3)?,
365 verified: row.get::<_, i64>(4).unwrap_or(0) != 0,
366 ed25519_pubkey: row.get(5).ok().flatten(),
367 role: row.get(6).unwrap_or_else(|_| "member".to_string()),
368 })
369 })?;
370 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
371}
372
373pub fn set_member_role(db: &Db, room_id: &str, fingerprint: &str, role: &str) -> Result<()> {
377 let conn = db.lock().unwrap();
378 conn.execute(
379 "UPDATE room_members SET role = ?1 WHERE room_id = ?2 AND fingerprint = ?3",
380 params![role, room_id, fingerprint],
381 )?;
382 Ok(())
383}
384
385pub fn list_room_owners(db: &Db, room_id: &str) -> Result<Vec<String>> {
389 let conn = db.lock().unwrap();
390 let mut stmt = conn.prepare(
391 "SELECT fingerprint FROM room_members WHERE room_id = ?1 AND role = 'owner'",
392 )?;
393 let rows = stmt.query_map(params![room_id], |row| row.get::<_, String>(0))?;
394 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
395}
396
397pub fn add_room_ban(
401 db: &Db,
402 room_id: &str,
403 banned_fingerprint: &str,
404 banned_by_fingerprint: &str,
405 signature_b64: &str,
406 banned_at: i64,
407) -> Result<()> {
408 let conn = db.lock().unwrap();
409 conn.execute(
410 "INSERT INTO room_bans (room_id, banned_fingerprint, banned_by_fingerprint, signature_b64, banned_at)
411 VALUES (?1, ?2, ?3, ?4, ?5)
412 ON CONFLICT(room_id, banned_fingerprint) DO UPDATE SET
413 banned_by_fingerprint = excluded.banned_by_fingerprint,
414 signature_b64 = excluded.signature_b64,
415 banned_at = excluded.banned_at",
416 params![
417 room_id,
418 banned_fingerprint,
419 banned_by_fingerprint,
420 signature_b64,
421 banned_at,
422 ],
423 )?;
424 Ok(())
425}
426
427pub fn is_member_banned(db: &Db, room_id: &str, fingerprint: &str) -> Result<bool> {
428 let conn = db.lock().unwrap();
429 let count: i64 = conn
430 .query_row(
431 "SELECT COUNT(*) FROM room_bans WHERE room_id = ?1 AND banned_fingerprint = ?2",
432 params![room_id, fingerprint],
433 |r| r.get(0),
434 )
435 .unwrap_or(0);
436 Ok(count > 0)
437}
438
439pub fn list_room_bans(db: &Db, room_id: &str) -> Result<Vec<String>> {
443 let conn = db.lock().unwrap();
444 let mut stmt = conn.prepare(
445 "SELECT banned_fingerprint FROM room_bans WHERE room_id = ?1 ORDER BY banned_at DESC",
446 )?;
447 let rows = stmt.query_map(params![room_id], |row| row.get::<_, String>(0))?;
448 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
449}
450
451pub fn get_member_ed25519_pubkey(
463 db: &Db,
464 room_id: &str,
465 fingerprint: &str,
466) -> Result<Option<String>> {
467 let conn = db.lock().unwrap();
468 let mut stmt = conn.prepare(
469 "SELECT ed25519_pubkey FROM room_members WHERE room_id = ?1 AND fingerprint = ?2",
470 )?;
471 let row = stmt
472 .query_row(params![room_id, fingerprint], |row| {
473 row.get::<_, Option<String>>(0)
474 })
475 .ok();
476 Ok(row.flatten())
477}
478
479pub fn remove_room_member(db: &Db, room_id: &str, fingerprint: &str) -> Result<()> {
480 let conn = db.lock().unwrap();
481 conn.execute(
482 "DELETE FROM room_members WHERE room_id = ?1 AND fingerprint = ?2",
483 params![room_id, fingerprint],
484 )?;
485 Ok(())
486}
487
488pub fn set_member_verified(
491 db: &Db,
492 room_id: &str,
493 fingerprint: &str,
494 verified: bool,
495) -> Result<()> {
496 let conn = db.lock().unwrap();
497 conn.execute(
498 "UPDATE room_members SET verified = ?1 WHERE room_id = ?2 AND fingerprint = ?3",
499 params![verified as i64, room_id, fingerprint],
500 )?;
501 Ok(())
502}
503
504pub fn list_verified_fingerprints(db: &Db, room_id: &str) -> Result<Vec<String>> {
505 let conn = db.lock().unwrap();
506 let mut stmt = conn.prepare(
507 "SELECT DISTINCT fingerprint FROM room_members WHERE room_id = ?1 AND verified = 1",
508 )?;
509 let rows = stmt.query_map(params![room_id], |row| row.get::<_, String>(0))?;
510 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
511}
512
513#[derive(Debug, Clone)]
518pub struct StoredMegolmSession {
519 pub room_id: String,
520 pub sender_fingerprint: String,
521 pub session_id: String,
522 pub session_data: Vec<u8>,
523 pub is_outbound: bool,
524 pub created_at: i64,
525}
526
527pub fn save_megolm_session(db: &Db, session: &StoredMegolmSession) -> Result<()> {
528 let conn = db.lock().unwrap();
529 conn.execute(
530 "INSERT OR REPLACE INTO room_megolm_sessions
531 (room_id, sender_fingerprint, session_id, session_data, is_outbound, created_at)
532 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
533 params![
534 session.room_id,
535 session.sender_fingerprint,
536 session.session_id,
537 session.session_data,
538 session.is_outbound as i64,
539 session.created_at,
540 ],
541 )?;
542 Ok(())
543}
544
545pub fn load_megolm_sessions_for_room(
546 db: &Db,
547 room_id: &str,
548) -> Result<Vec<StoredMegolmSession>> {
549 let conn = db.lock().unwrap();
550 let mut stmt = conn.prepare(
551 "SELECT room_id, sender_fingerprint, session_id, session_data, is_outbound, created_at
552 FROM room_megolm_sessions WHERE room_id = ?1",
553 )?;
554 let rows = stmt.query_map(params![room_id], |row| {
555 Ok(StoredMegolmSession {
556 room_id: row.get(0)?,
557 sender_fingerprint: row.get(1)?,
558 session_id: row.get(2)?,
559 session_data: row.get(3)?,
560 is_outbound: row.get::<_, i64>(4)? != 0,
561 created_at: row.get(5)?,
562 })
563 })?;
564 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
565}
566
567#[derive(Debug, Clone)]
572pub struct StoredRoomMessage {
573 pub id: i64,
574 pub room_id: String,
575 pub sender_fingerprint: String,
576 pub direction: String,
577 pub body: String,
578 pub sent_at: i64,
579}
580
581pub fn insert_room_message(
582 db: &Db,
583 room_id: &str,
584 sender_fingerprint: &str,
585 direction: &str,
586 body: &str,
587 sent_at: i64,
588) -> Result<i64> {
589 let conn = db.lock().unwrap();
590 conn.execute(
591 "INSERT INTO room_messages (room_id, sender_fingerprint, direction, body, sent_at)
592 VALUES (?1, ?2, ?3, ?4, ?5)",
593 params![room_id, sender_fingerprint, direction, body, sent_at],
594 )?;
595 Ok(conn.last_insert_rowid())
596}
597
598pub fn search_room_messages(
602 db: &Db,
603 room_id: &str,
604 query: &str,
605 limit: i64,
606) -> Result<Vec<StoredRoomMessage>> {
607 let escaped = query
609 .replace('\\', "\\\\")
610 .replace('%', "\\%")
611 .replace('_', "\\_");
612 let pattern = format!("%{}%", escaped);
613 let conn = db.lock().unwrap();
614 let mut stmt = conn.prepare(
615 "SELECT id, room_id, sender_fingerprint, direction, body, sent_at
616 FROM room_messages
617 WHERE room_id = ?1 AND body LIKE ?2 ESCAPE '\\' COLLATE NOCASE
618 ORDER BY sent_at DESC LIMIT ?3",
619 )?;
620 let rows = stmt.query_map(params![room_id, pattern, limit], |row| {
621 Ok(StoredRoomMessage {
622 id: row.get(0)?,
623 room_id: row.get(1)?,
624 sender_fingerprint: row.get(2)?,
625 direction: row.get(3)?,
626 body: row.get(4)?,
627 sent_at: row.get(5)?,
628 })
629 })?;
630 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
631}
632
633pub fn get_room_messages(db: &Db, room_id: &str, limit: i64) -> Result<Vec<StoredRoomMessage>> {
634 let conn = db.lock().unwrap();
635 let mut stmt = conn.prepare(
636 "SELECT id, room_id, sender_fingerprint, direction, body, sent_at
637 FROM room_messages WHERE room_id = ?1 ORDER BY sent_at DESC LIMIT ?2",
638 )?;
639 let rows = stmt.query_map(params![room_id, limit], |row| {
640 Ok(StoredRoomMessage {
641 id: row.get(0)?,
642 room_id: row.get(1)?,
643 sender_fingerprint: row.get(2)?,
644 direction: row.get(3)?,
645 body: row.get(4)?,
646 sent_at: row.get(5)?,
647 })
648 })?;
649 let mut msgs: Vec<StoredRoomMessage> = rows.collect::<std::result::Result<Vec<_>, _>>()?;
650 msgs.reverse();
651 Ok(msgs)
652}
653
654#[derive(Debug, Clone)]
659pub struct KnownPeer {
660 pub address: String,
661 pub label: Option<String>,
662 pub last_connected_at: Option<i64>,
663 pub last_attempt_at: Option<i64>,
664 pub created_at: i64,
665 pub fingerprint: Option<String>,
669 pub trusted: bool,
674}
675
676pub fn upsert_known_peer(db: &Db, peer: &KnownPeer) -> Result<()> {
677 let conn = db.lock().unwrap();
678 conn.execute(
679 "INSERT INTO known_peers (address, label, last_connected_at, last_attempt_at, created_at, fingerprint, trusted)
680 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
681 ON CONFLICT(address) DO UPDATE SET
682 label = COALESCE(excluded.label, known_peers.label),
683 last_connected_at = COALESCE(excluded.last_connected_at, known_peers.last_connected_at),
684 last_attempt_at = COALESCE(excluded.last_attempt_at, known_peers.last_attempt_at),
685 fingerprint = COALESCE(excluded.fingerprint, known_peers.fingerprint),
686 -- trusted is sticky-once-true: a fresh upsert with trusted=false
687 -- (the default on auto-reconnect) must not demote a previously
688 -- trusted row.
689 trusted = CASE
690 WHEN excluded.trusted = 1 THEN 1
691 ELSE known_peers.trusted
692 END",
693 params![
694 peer.address,
695 peer.label,
696 peer.last_connected_at,
697 peer.last_attempt_at,
698 peer.created_at,
699 peer.fingerprint,
700 peer.trusted as i64,
701 ],
702 )?;
703 Ok(())
704}
705
706pub fn list_known_peers(db: &Db) -> Result<Vec<KnownPeer>> {
707 let conn = db.lock().unwrap();
708 let mut stmt = conn.prepare(
709 "SELECT address, label, last_connected_at, last_attempt_at, created_at, fingerprint, trusted
710 FROM known_peers ORDER BY COALESCE(last_connected_at, 0) DESC, created_at DESC",
711 )?;
712 let rows = stmt.query_map([], |row| {
713 Ok(KnownPeer {
714 address: row.get(0)?,
715 label: row.get(1)?,
716 last_connected_at: row.get(2)?,
717 last_attempt_at: row.get(3)?,
718 created_at: row.get(4)?,
719 fingerprint: row.get(5).ok().flatten(),
720 trusted: row.get::<_, i64>(6).unwrap_or(0) != 0,
721 })
722 })?;
723 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
724}
725
726pub fn forget_known_peer(db: &Db, address: &str) -> Result<()> {
727 let conn = db.lock().unwrap();
728 conn.execute("DELETE FROM known_peers WHERE address = ?1", params![address])?;
729 Ok(())
730}
731
732pub fn is_fingerprint_trusted(db: &Db, fingerprint: &str) -> Result<bool> {
736 let conn = db.lock().unwrap();
737 let count: i64 = conn
738 .query_row(
739 "SELECT COUNT(*) FROM known_peers WHERE fingerprint = ?1 AND trusted = 1",
740 params![fingerprint],
741 |r| r.get(0),
742 )
743 .unwrap_or(0);
744 Ok(count > 0)
745}
746
747#[derive(Debug, Clone)]
752pub struct Contact {
753 pub fingerprint: String,
754 pub alias: Option<String>,
755 pub ed25519_pubkey: Option<String>,
756 pub dm_room_id: Option<String>,
757 pub source: String,
758 pub note: Option<String>,
759 pub added_at: i64,
760 pub last_seen: Option<i64>,
761}
762
763fn row_to_contact(row: &rusqlite::Row) -> rusqlite::Result<Contact> {
764 Ok(Contact {
765 fingerprint: row.get(0)?,
766 alias: row.get(1)?,
767 ed25519_pubkey: row.get(2)?,
768 dm_room_id: row.get(3)?,
769 source: row.get(4)?,
770 note: row.get(5)?,
771 added_at: row.get(6)?,
772 last_seen: row.get(7)?,
773 })
774}
775
776pub fn upsert_contact(db: &Db, c: &Contact) -> Result<()> {
782 let conn = db.lock().unwrap();
783 conn.execute(
784 "INSERT INTO contacts (fingerprint, alias, ed25519_pubkey, dm_room_id, source, note, added_at, last_seen)
785 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
786 ON CONFLICT(fingerprint) DO UPDATE SET
787 alias = COALESCE(excluded.alias, contacts.alias),
788 ed25519_pubkey = COALESCE(excluded.ed25519_pubkey, contacts.ed25519_pubkey),
789 dm_room_id = COALESCE(excluded.dm_room_id, contacts.dm_room_id),
790 note = COALESCE(excluded.note, contacts.note),
791 last_seen = COALESCE(excluded.last_seen, contacts.last_seen)",
792 params![
793 c.fingerprint,
794 c.alias,
795 c.ed25519_pubkey,
796 c.dm_room_id,
797 c.source,
798 c.note,
799 c.added_at,
800 c.last_seen,
801 ],
802 )?;
803 Ok(())
804}
805
806pub fn get_contact(db: &Db, fingerprint: &str) -> Result<Option<Contact>> {
807 let conn = db.lock().unwrap();
808 let mut stmt = conn.prepare(
809 "SELECT fingerprint, alias, ed25519_pubkey, dm_room_id, source, note, added_at, last_seen
810 FROM contacts WHERE fingerprint = ?1",
811 )?;
812 let mut rows = stmt.query_map(params![fingerprint], row_to_contact)?;
813 match rows.next() {
814 Some(r) => Ok(Some(r?)),
815 None => Ok(None),
816 }
817}
818
819pub fn list_contacts(db: &Db) -> Result<Vec<Contact>> {
820 let conn = db.lock().unwrap();
821 let mut stmt = conn.prepare(
822 "SELECT fingerprint, alias, ed25519_pubkey, dm_room_id, source, note, added_at, last_seen
823 FROM contacts ORDER BY COALESCE(last_seen, added_at) DESC",
824 )?;
825 let rows = stmt.query_map([], row_to_contact)?;
826 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
827}
828
829pub fn delete_contact(db: &Db, fingerprint: &str) -> Result<()> {
830 let conn = db.lock().unwrap();
831 conn.execute("DELETE FROM contacts WHERE fingerprint = ?1", params![fingerprint])?;
832 Ok(())
833}
834
835pub fn is_contact(db: &Db, fingerprint: &str) -> Result<bool> {
836 let conn = db.lock().unwrap();
837 let count: i64 = conn
838 .query_row(
839 "SELECT COUNT(*) FROM contacts WHERE fingerprint = ?1",
840 params![fingerprint],
841 |r| r.get(0),
842 )
843 .unwrap_or(0);
844 Ok(count > 0)
845}
846
847pub fn set_contact_alias(db: &Db, fingerprint: &str, alias: Option<&str>) -> Result<()> {
850 let conn = db.lock().unwrap();
851 conn.execute(
852 "UPDATE contacts SET alias = ?2 WHERE fingerprint = ?1",
853 params![fingerprint, alias],
854 )?;
855 Ok(())
856}
857
858#[derive(Debug, Clone)]
866pub struct PendingFriendRequest {
867 pub fingerprint: String,
868 pub address: String,
869 pub peer_id: String,
870 pub received_at: i64,
871}
872
873pub const PENDING_FRIEND_REQUEST_TTL_SECS: i64 = 3 * 24 * 60 * 60;
878
879pub fn upsert_pending_friend_request(db: &Db, req: &PendingFriendRequest) -> Result<()> {
880 let conn = db.lock().unwrap();
881 conn.execute(
882 "INSERT INTO pending_friend_requests (fingerprint, address, peer_id, received_at)
883 VALUES (?1, ?2, ?3, ?4)
884 ON CONFLICT(fingerprint, address) DO UPDATE SET
885 peer_id = excluded.peer_id,
886 received_at = excluded.received_at",
887 params![req.fingerprint, req.address, req.peer_id, req.received_at],
888 )?;
889 Ok(())
890}
891
892pub fn list_pending_friend_requests(db: &Db) -> Result<Vec<PendingFriendRequest>> {
893 let conn = db.lock().unwrap();
894 let mut stmt = conn.prepare(
895 "SELECT fingerprint, address, peer_id, received_at
896 FROM pending_friend_requests
897 ORDER BY received_at DESC",
898 )?;
899 let rows = stmt.query_map([], |row| {
900 Ok(PendingFriendRequest {
901 fingerprint: row.get(0)?,
902 address: row.get(1)?,
903 peer_id: row.get(2)?,
904 received_at: row.get(3)?,
905 })
906 })?;
907 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
908}
909
910pub fn delete_pending_friend_requests_for_fp(db: &Db, fingerprint: &str) -> Result<()> {
915 let conn = db.lock().unwrap();
916 conn.execute(
917 "DELETE FROM pending_friend_requests WHERE fingerprint = ?1",
918 params![fingerprint],
919 )?;
920 Ok(())
921}
922
923pub fn cleanup_expired_pending_friend_requests(db: &Db, now: i64) -> Result<usize> {
927 let cutoff = now.saturating_sub(PENDING_FRIEND_REQUEST_TTL_SECS);
931 let conn = db.lock().unwrap();
932 let removed = conn.execute(
933 "DELETE FROM pending_friend_requests WHERE received_at < ?1",
934 params![cutoff],
935 )?;
936 Ok(removed)
937}
938
939#[derive(Debug, Clone)]
948pub struct PendingContactRequest {
949 pub fingerprint: String,
950 pub display_name: Option<String>,
951 pub note: Option<String>,
952 pub received_at: i64,
953}
954
955pub fn upsert_pending_contact_request(db: &Db, req: &PendingContactRequest) -> Result<()> {
956 let conn = db.lock().unwrap();
957 conn.execute(
958 "INSERT INTO pending_contact_requests (fingerprint, display_name, note, received_at)
959 VALUES (?1, ?2, ?3, ?4)
960 ON CONFLICT(fingerprint) DO UPDATE SET
961 display_name = COALESCE(excluded.display_name, pending_contact_requests.display_name),
962 note = COALESCE(excluded.note, pending_contact_requests.note),
963 received_at = excluded.received_at",
964 params![req.fingerprint, req.display_name, req.note, req.received_at],
965 )?;
966 Ok(())
967}
968
969pub fn list_pending_contact_requests(db: &Db) -> Result<Vec<PendingContactRequest>> {
970 let conn = db.lock().unwrap();
971 let mut stmt = conn.prepare(
972 "SELECT fingerprint, display_name, note, received_at
973 FROM pending_contact_requests ORDER BY received_at DESC",
974 )?;
975 let rows = stmt.query_map([], |row| {
976 Ok(PendingContactRequest {
977 fingerprint: row.get(0)?,
978 display_name: row.get(1)?,
979 note: row.get(2)?,
980 received_at: row.get(3)?,
981 })
982 })?;
983 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
984}
985
986pub fn delete_pending_contact_request(db: &Db, fingerprint: &str) -> Result<()> {
987 let conn = db.lock().unwrap();
988 conn.execute(
989 "DELETE FROM pending_contact_requests WHERE fingerprint = ?1",
990 params![fingerprint],
991 )?;
992 Ok(())
993}
994
995pub fn cleanup_expired_pending_contact_requests(db: &Db, now: i64) -> Result<usize> {
996 let cutoff = now.saturating_sub(PENDING_FRIEND_REQUEST_TTL_SECS);
997 let conn = db.lock().unwrap();
998 let removed = conn.execute(
999 "DELETE FROM pending_contact_requests WHERE received_at < ?1",
1000 params![cutoff],
1001 )?;
1002 Ok(removed)
1003}
1004
1005pub fn block_peer(db: &Db, fingerprint: &str, now: i64) -> Result<()> {
1009 let conn = db.lock().unwrap();
1010 conn.execute(
1011 "INSERT INTO blocked_peers (fingerprint, blocked_at) VALUES (?1, ?2)
1012 ON CONFLICT(fingerprint) DO UPDATE SET blocked_at = excluded.blocked_at",
1013 params![fingerprint, now],
1014 )?;
1015 Ok(())
1016}
1017
1018pub fn get_setting(db: &Db, key: &str) -> Result<Option<String>> {
1021 let conn = db.lock().unwrap();
1022 let mut stmt = conn.prepare("SELECT value FROM app_settings WHERE key = ?1")?;
1023 let row = stmt
1024 .query_row(params![key], |r| r.get::<_, String>(0))
1025 .ok();
1026 Ok(row)
1027}
1028
1029pub fn set_setting(db: &Db, key: &str, value: &str) -> Result<()> {
1030 let conn = db.lock().unwrap();
1031 conn.execute(
1032 "INSERT INTO app_settings (key, value) VALUES (?1, ?2)
1033 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
1034 params![key, value],
1035 )?;
1036 Ok(())
1037}
1038
1039pub fn get_room_verified_only(db: &Db, room_id: &str) -> Result<bool> {
1041 let conn = db.lock().unwrap();
1042 let v: i64 = conn
1043 .query_row(
1044 "SELECT verified_only_join FROM rooms WHERE id = ?1",
1045 params![room_id],
1046 |r| r.get(0),
1047 )
1048 .unwrap_or(0);
1049 Ok(v != 0)
1050}
1051
1052pub fn set_room_verified_only(db: &Db, room_id: &str, on: bool) -> Result<()> {
1053 let conn = db.lock().unwrap();
1054 conn.execute(
1055 "UPDATE rooms SET verified_only_join = ?1 WHERE id = ?2",
1056 params![on as i64, room_id],
1057 )?;
1058 Ok(())
1059}
1060
1061pub fn add_verified_peer(db: &Db, fingerprint: &str, verified_at: i64) -> Result<()> {
1065 let conn = db.lock().unwrap();
1066 conn.execute(
1067 "INSERT INTO verified_peers (fingerprint, verified_at) VALUES (?1, ?2)
1068 ON CONFLICT(fingerprint) DO UPDATE SET verified_at = excluded.verified_at",
1069 params![fingerprint, verified_at],
1070 )?;
1071 Ok(())
1072}
1073
1074pub fn is_globally_verified(db: &Db, fingerprint: &str) -> Result<bool> {
1078 let conn = db.lock().unwrap();
1079 let count: i64 = conn
1080 .query_row(
1081 "SELECT COUNT(*) FROM verified_peers WHERE fingerprint = ?1",
1082 params![fingerprint],
1083 |r| r.get(0),
1084 )
1085 .unwrap_or(0);
1086 Ok(count > 0)
1087}
1088
1089pub fn list_verified_peers(db: &Db) -> Result<Vec<String>> {
1092 let conn = db.lock().unwrap();
1093 let mut stmt =
1094 conn.prepare("SELECT fingerprint FROM verified_peers ORDER BY verified_at DESC")?;
1095 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1096 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
1097}
1098
1099pub fn is_onboarding_seen(db: &Db) -> Result<bool> {
1101 let conn = db.lock().unwrap();
1102 let v: i64 = conn
1103 .query_row(
1104 "SELECT onboarding_seen FROM identity WHERE id = 1",
1105 [],
1106 |r| r.get(0),
1107 )
1108 .unwrap_or(0);
1109 Ok(v != 0)
1110}
1111
1112pub fn mark_onboarding_seen(db: &Db) -> Result<()> {
1113 let conn = db.lock().unwrap();
1114 conn.execute(
1115 "UPDATE identity SET onboarding_seen = 1 WHERE id = 1",
1116 [],
1117 )?;
1118 Ok(())
1119}
1120
1121pub fn get_last_seen_onboarding_version(db: &Db) -> Result<Option<String>> {
1127 get_setting(db, "last_seen_onboarding_version")
1128}
1129
1130pub fn set_last_seen_onboarding_version(db: &Db, version: &str) -> Result<()> {
1131 set_setting(db, "last_seen_onboarding_version", version)
1132}
1133
1134pub fn get_update_check_enabled(db: &Db) -> Result<Option<bool>> {
1138 Ok(get_setting(db, "update_check_enabled")?
1139 .map(|v| v == "1" || v.eq_ignore_ascii_case("true")))
1140}
1141
1142pub fn set_update_check_enabled(db: &Db, enabled: bool) -> Result<()> {
1143 set_setting(db, "update_check_enabled", if enabled { "1" } else { "0" })
1144}
1145
1146pub fn is_peer_blocked(db: &Db, fingerprint: &str) -> Result<bool> {
1147 let conn = db.lock().unwrap();
1148 let count: i64 = conn
1149 .query_row(
1150 "SELECT COUNT(*) FROM blocked_peers WHERE fingerprint = ?1",
1151 params![fingerprint],
1152 |r| r.get(0),
1153 )
1154 .unwrap_or(0);
1155 Ok(count > 0)
1156}
1157
1158pub fn list_blocked_peers(db: &Db) -> Result<Vec<String>> {
1162 let conn = db.lock().unwrap();
1163 let mut stmt = conn.prepare(
1164 "SELECT fingerprint FROM blocked_peers ORDER BY blocked_at DESC",
1165 )?;
1166 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1167 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
1168}
1169
1170pub fn unblock_peer(db: &Db, fingerprint: &str) -> Result<()> {
1174 let conn = db.lock().unwrap();
1175 conn.execute(
1176 "DELETE FROM blocked_peers WHERE fingerprint = ?1",
1177 params![fingerprint],
1178 )?;
1179 Ok(())
1180}
1181
1182pub fn upsert_peer_profile(
1191 db: &Db,
1192 fingerprint: &str,
1193 username: Option<&str>,
1194 updated_at: i64,
1195) -> Result<()> {
1196 let conn = db.lock().unwrap();
1197 conn.execute(
1198 "INSERT INTO peer_profiles (fingerprint, username, updated_at)
1199 VALUES (?1, ?2, ?3)
1200 ON CONFLICT(fingerprint) DO UPDATE SET
1201 username = excluded.username,
1202 updated_at = excluded.updated_at
1203 WHERE excluded.updated_at > peer_profiles.updated_at",
1204 params![fingerprint, username, updated_at],
1205 )?;
1206 Ok(())
1207}
1208
1209pub fn get_peer_username(db: &Db, fingerprint: &str) -> Result<Option<String>> {
1213 let conn = db.lock().unwrap();
1214 let mut stmt = conn.prepare(
1215 "SELECT username FROM peer_profiles WHERE fingerprint = ?1",
1216 )?;
1217 let mut rows = stmt.query(params![fingerprint])?;
1218 if let Some(row) = rows.next()? {
1219 Ok(row.get::<_, Option<String>>(0)?)
1220 } else {
1221 Ok(None)
1222 }
1223}
1224
1225pub fn find_peers_by_username(db: &Db, username: &str) -> Result<Vec<String>> {
1230 let conn = db.lock().unwrap();
1231 let mut stmt = conn.prepare(
1232 "SELECT fingerprint FROM peer_profiles WHERE username = ?1",
1233 )?;
1234 let rows = stmt.query_map(params![username], |row| row.get::<_, String>(0))?;
1235 let mut out = Vec::new();
1236 for r in rows {
1237 out.push(r?);
1238 }
1239 Ok(out)
1240}
1241
1242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1248pub enum AttachmentStatus {
1249 Offered,
1250 Downloading,
1251 Ready,
1252 Saved,
1253 Failed,
1254 Cancelled,
1255}
1256
1257impl AttachmentStatus {
1258 pub fn as_str(self) -> &'static str {
1259 match self {
1260 Self::Offered => "offered",
1261 Self::Downloading => "downloading",
1262 Self::Ready => "ready",
1263 Self::Saved => "saved",
1264 Self::Failed => "failed",
1265 Self::Cancelled => "cancelled",
1266 }
1267 }
1268 pub fn from_str(s: &str) -> Option<Self> {
1269 Some(match s {
1270 "offered" => Self::Offered,
1271 "downloading" => Self::Downloading,
1272 "ready" => Self::Ready,
1273 "saved" => Self::Saved,
1274 "failed" => Self::Failed,
1275 "cancelled" => Self::Cancelled,
1276 _ => return None,
1277 })
1278 }
1279}
1280
1281#[derive(Debug, Clone)]
1282pub struct StoredAttachment {
1283 pub id: i64,
1284 pub room_id: String,
1285 pub message_id: Option<i64>,
1286 pub sender_fingerprint: String,
1287 pub file_id: String,
1288 pub name: String,
1289 pub mime: Option<String>,
1290 pub size_bytes: i64,
1291 pub status: AttachmentStatus,
1292 pub cache_path: Option<String>,
1293 pub saved_path: Option<String>,
1294 pub error: Option<String>,
1295 pub encrypted: bool,
1296 pub wrapped_key: Option<String>,
1297 pub nonce: Option<String>,
1298 pub megolm_session_id: Option<String>,
1299 pub content_hash: Option<String>,
1303 pub created_at: i64,
1304}
1305
1306pub fn upsert_attachment(db: &Db, a: &StoredAttachment) -> Result<()> {
1308 let conn = db.lock().unwrap();
1309 conn.execute(
1310 "INSERT INTO room_attachments
1311 (room_id, message_id, sender_fingerprint, file_id, name, mime,
1312 size_bytes, status, cache_path, saved_path, error,
1313 encrypted, wrapped_key, nonce, megolm_session_id, created_at,
1314 content_hash)
1315 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)
1316 ON CONFLICT(room_id, file_id) DO UPDATE SET
1317 name = excluded.name,
1318 mime = excluded.mime,
1319 size_bytes = excluded.size_bytes,
1320 -- Don't downgrade a more advanced status.
1321 status = CASE
1322 WHEN room_attachments.status IN ('saved','ready')
1323 AND excluded.status IN ('offered','downloading')
1324 THEN room_attachments.status
1325 ELSE excluded.status
1326 END,
1327 cache_path = COALESCE(excluded.cache_path, room_attachments.cache_path),
1328 saved_path = COALESCE(excluded.saved_path, room_attachments.saved_path),
1329 error = excluded.error,
1330 wrapped_key = COALESCE(excluded.wrapped_key, room_attachments.wrapped_key),
1331 nonce = COALESCE(excluded.nonce, room_attachments.nonce),
1332 megolm_session_id = COALESCE(excluded.megolm_session_id, room_attachments.megolm_session_id),
1333 content_hash = COALESCE(excluded.content_hash, room_attachments.content_hash)",
1334 params![
1335 a.room_id,
1336 a.message_id,
1337 a.sender_fingerprint,
1338 a.file_id,
1339 a.name,
1340 a.mime,
1341 a.size_bytes,
1342 a.status.as_str(),
1343 a.cache_path,
1344 a.saved_path,
1345 a.error,
1346 a.encrypted as i64,
1347 a.wrapped_key,
1348 a.nonce,
1349 a.megolm_session_id,
1350 a.created_at,
1351 a.content_hash,
1352 ],
1353 )?;
1354 Ok(())
1355}
1356
1357fn row_to_attachment(row: &rusqlite::Row) -> rusqlite::Result<StoredAttachment> {
1358 let status_s: String = row.get(8)?;
1359 let status = AttachmentStatus::from_str(&status_s).unwrap_or(AttachmentStatus::Failed);
1360 Ok(StoredAttachment {
1361 id: row.get(0)?,
1362 room_id: row.get(1)?,
1363 message_id: row.get(2)?,
1364 sender_fingerprint: row.get(3)?,
1365 file_id: row.get(4)?,
1366 name: row.get(5)?,
1367 mime: row.get(6)?,
1368 size_bytes: row.get(7)?,
1369 status,
1370 cache_path: row.get(9)?,
1371 saved_path: row.get(10)?,
1372 error: row.get(11)?,
1373 encrypted: row.get::<_, i64>(12)? != 0,
1374 wrapped_key: row.get(13)?,
1375 nonce: row.get(14)?,
1376 megolm_session_id: row.get(15)?,
1377 created_at: row.get(16)?,
1378 content_hash: row.get(17)?,
1379 })
1380}
1381
1382pub fn get_attachment(db: &Db, room_id: &str, file_id: &str) -> Result<Option<StoredAttachment>> {
1383 let conn = db.lock().unwrap();
1384 let mut stmt = conn.prepare(
1385 "SELECT id, room_id, message_id, sender_fingerprint, file_id, name, mime,
1386 size_bytes, status, cache_path, saved_path, error,
1387 encrypted, wrapped_key, nonce, megolm_session_id, created_at,
1388 content_hash
1389 FROM room_attachments WHERE room_id = ?1 AND file_id = ?2",
1390 )?;
1391 let mut rows = stmt.query_map(params![room_id, file_id], row_to_attachment)?;
1392 match rows.next() {
1393 Some(r) => Ok(Some(r?)),
1394 None => Ok(None),
1395 }
1396}
1397
1398pub fn list_room_attachments(db: &Db, room_id: &str) -> Result<Vec<StoredAttachment>> {
1399 let conn = db.lock().unwrap();
1400 let mut stmt = conn.prepare(
1401 "SELECT id, room_id, message_id, sender_fingerprint, file_id, name, mime,
1402 size_bytes, status, cache_path, saved_path, error,
1403 encrypted, wrapped_key, nonce, megolm_session_id, created_at,
1404 content_hash
1405 FROM room_attachments WHERE room_id = ?1 ORDER BY created_at ASC",
1406 )?;
1407 let rows = stmt.query_map(params![room_id], row_to_attachment)?;
1408 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
1409}
1410
1411pub fn update_attachment_status(
1412 db: &Db,
1413 room_id: &str,
1414 file_id: &str,
1415 status: AttachmentStatus,
1416 error: Option<&str>,
1417) -> Result<()> {
1418 let conn = db.lock().unwrap();
1419 conn.execute(
1420 "UPDATE room_attachments SET status = ?1, error = ?2
1421 WHERE room_id = ?3 AND file_id = ?4",
1422 params![status.as_str(), error, room_id, file_id],
1423 )?;
1424 Ok(())
1425}
1426
1427pub fn update_attachment_paths(
1428 db: &Db,
1429 room_id: &str,
1430 file_id: &str,
1431 cache_path: Option<&str>,
1432 saved_path: Option<&str>,
1433) -> Result<()> {
1434 let conn = db.lock().unwrap();
1435 conn.execute(
1436 "UPDATE room_attachments
1437 SET cache_path = COALESCE(?1, cache_path),
1438 saved_path = COALESCE(?2, saved_path)
1439 WHERE room_id = ?3 AND file_id = ?4",
1440 params![cache_path, saved_path, room_id, file_id],
1441 )?;
1442 Ok(())
1443}
1444
1445pub fn delete_attachment(db: &Db, room_id: &str, file_id: &str) -> Result<()> {
1446 let conn = db.lock().unwrap();
1447 conn.execute(
1448 "DELETE FROM room_attachments WHERE room_id = ?1 AND file_id = ?2",
1449 params![room_id, file_id],
1450 )?;
1451 Ok(())
1452}
1453
1454#[cfg(test)]
1455mod tests {
1456 use super::*;
1457 use crate::storage::open_db_in_memory;
1458
1459 fn make_room(name: &str) -> StoredRoom {
1460 let creator_fp = "test-creator-fp";
1461 let created_at = 1000;
1462 StoredRoom {
1463 id: derive_room_id(creator_fp, name, created_at),
1464 name: name.into(),
1465 creator_fingerprint: creator_fp.into(),
1466 encrypted: false,
1467 passphrase_salt: None,
1468 created_at,
1469 last_active: None,
1470 kind: RoomKind::Group,
1471 }
1472 }
1473
1474 #[test]
1475 fn identity_round_trip() {
1476 let db = open_db_in_memory().unwrap();
1477 save_identity(&db, b"secret-bytes-32-chars-long-xxxxx", 1000).unwrap();
1478 let loaded = load_identity(&db).unwrap().unwrap();
1479 assert_eq!(loaded.ed25519_secret, b"secret-bytes-32-chars-long-xxxxx");
1480 assert_eq!(loaded.created_at, 1000);
1481 }
1482
1483 #[test]
1484 fn room_id_is_deterministic() {
1485 let id1 = derive_room_id("creator-fp", "test-room", 1000);
1486 let id2 = derive_room_id("creator-fp", "test-room", 1000);
1487 assert_eq!(id1, id2);
1488 assert_eq!(id1.len(), 32); }
1490
1491 #[test]
1492 fn room_id_differs_with_inputs() {
1493 let id1 = derive_room_id("creator-a", "test", 1000);
1494 let id2 = derive_room_id("creator-b", "test", 1000);
1495 let id3 = derive_room_id("creator-a", "test", 1001);
1496 assert_ne!(id1, id2);
1497 assert_ne!(id1, id3);
1498 }
1499
1500 #[test]
1501 fn room_insert_and_get() {
1502 let db = open_db_in_memory().unwrap();
1503 let room = make_room("lunch-talk");
1504 insert_room(&db, &room).unwrap();
1505 let loaded = get_room(&db, &room.id).unwrap().unwrap();
1506 assert_eq!(loaded.name, "lunch-talk");
1507 assert!(!loaded.encrypted);
1508 }
1509
1510 #[test]
1511 fn room_list_orders_by_last_active() {
1512 let db = open_db_in_memory().unwrap();
1513 let mut a = make_room("alpha");
1514 a.last_active = Some(100);
1515 let mut b = make_room("beta");
1516 b.last_active = Some(200);
1517 insert_room(&db, &a).unwrap();
1518 insert_room(&db, &b).unwrap();
1519 let rooms = list_rooms(&db).unwrap();
1520 assert_eq!(rooms[0].name, "beta");
1521 assert_eq!(rooms[1].name, "alpha");
1522 }
1523
1524 #[test]
1525 fn room_member_upsert() {
1526 let db = open_db_in_memory().unwrap();
1527 let room = make_room("r");
1528 insert_room(&db, &room).unwrap();
1529
1530 upsert_room_member(
1531 &db,
1532 &StoredRoomMember {
1533 room_id: room.id.clone(),
1534 peer_id: "peer-x".into(),
1535 fingerprint: "fp-x".into(),
1536 last_seen: Some(500),
1537 verified: false,
1538 ed25519_pubkey: None,
1539 role: "member".into(),
1540 },
1541 )
1542 .unwrap();
1543 let members = list_room_members(&db, &room.id).unwrap();
1544 assert_eq!(members.len(), 1);
1545 assert_eq!(members[0].fingerprint, "fp-x");
1546 assert!(!members[0].verified);
1547 }
1548
1549 #[test]
1550 fn set_and_query_verified() {
1551 let db = open_db_in_memory().unwrap();
1552 let room = make_room("r");
1553 insert_room(&db, &room).unwrap();
1554 upsert_room_member(
1555 &db,
1556 &StoredRoomMember {
1557 room_id: room.id.clone(),
1558 peer_id: "p1".into(),
1559 fingerprint: "fp-1".into(),
1560 last_seen: None,
1561 verified: false,
1562 ed25519_pubkey: None,
1563 role: "member".into(),
1564 },
1565 )
1566 .unwrap();
1567 set_member_verified(&db, &room.id, "fp-1", true).unwrap();
1568 let verified = list_verified_fingerprints(&db, &room.id).unwrap();
1569 assert_eq!(verified, vec!["fp-1".to_string()]);
1570 let m = list_room_members(&db, &room.id).unwrap();
1571 assert!(m[0].verified);
1572 }
1573
1574 #[test]
1575 fn megolm_session_round_trip() {
1576 let db = open_db_in_memory().unwrap();
1577 let room = make_room("r");
1578 insert_room(&db, &room).unwrap();
1579
1580 let session = StoredMegolmSession {
1581 room_id: room.id.clone(),
1582 sender_fingerprint: "fp-sender".into(),
1583 session_id: "session-1".into(),
1584 session_data: vec![1, 2, 3, 4],
1585 is_outbound: true,
1586 created_at: 100,
1587 };
1588 save_megolm_session(&db, &session).unwrap();
1589 let loaded = load_megolm_sessions_for_room(&db, &room.id).unwrap();
1590 assert_eq!(loaded.len(), 1);
1591 assert_eq!(loaded[0].session_data, vec![1, 2, 3, 4]);
1592 assert!(loaded[0].is_outbound);
1593 }
1594
1595 fn make_attachment(room_id: &str, file_id: &str, name: &str) -> StoredAttachment {
1596 StoredAttachment {
1597 id: 0,
1598 room_id: room_id.into(),
1599 message_id: None,
1600 sender_fingerprint: "sender-fp".into(),
1601 file_id: file_id.into(),
1602 name: name.into(),
1603 mime: Some("image/png".into()),
1604 size_bytes: 1234,
1605 status: AttachmentStatus::Offered,
1606 cache_path: None,
1607 saved_path: None,
1608 error: None,
1609 encrypted: false,
1610 wrapped_key: None,
1611 nonce: None,
1612 megolm_session_id: None,
1613 content_hash: None,
1614 created_at: 100,
1615 }
1616 }
1617
1618 #[test]
1619 fn attachment_upsert_and_get() {
1620 let db = open_db_in_memory().unwrap();
1621 let room = make_room("r");
1622 insert_room(&db, &room).unwrap();
1623 let a = make_attachment(&room.id, "file-abc", "photo.png");
1624 upsert_attachment(&db, &a).unwrap();
1625
1626 let loaded = get_attachment(&db, &room.id, "file-abc").unwrap().unwrap();
1627 assert_eq!(loaded.name, "photo.png");
1628 assert_eq!(loaded.status, AttachmentStatus::Offered);
1629 assert_eq!(loaded.size_bytes, 1234);
1630 }
1631
1632 #[test]
1633 fn attachment_status_transitions() {
1634 let db = open_db_in_memory().unwrap();
1635 let room = make_room("r");
1636 insert_room(&db, &room).unwrap();
1637 let a = make_attachment(&room.id, "fid", "f.bin");
1638 upsert_attachment(&db, &a).unwrap();
1639
1640 update_attachment_status(&db, &room.id, "fid", AttachmentStatus::Downloading, None)
1641 .unwrap();
1642 assert_eq!(
1643 get_attachment(&db, &room.id, "fid")
1644 .unwrap()
1645 .unwrap()
1646 .status,
1647 AttachmentStatus::Downloading
1648 );
1649
1650 update_attachment_status(&db, &room.id, "fid", AttachmentStatus::Ready, None).unwrap();
1651 update_attachment_paths(
1652 &db,
1653 &room.id,
1654 "fid",
1655 Some("/cache/fid"),
1656 Some("/Downloads/f.bin"),
1657 )
1658 .unwrap();
1659 let loaded = get_attachment(&db, &room.id, "fid").unwrap().unwrap();
1660 assert_eq!(loaded.status, AttachmentStatus::Ready);
1661 assert_eq!(loaded.cache_path.as_deref(), Some("/cache/fid"));
1662 assert_eq!(loaded.saved_path.as_deref(), Some("/Downloads/f.bin"));
1663 }
1664
1665 #[test]
1666 fn upsert_does_not_downgrade_status() {
1667 let db = open_db_in_memory().unwrap();
1668 let room = make_room("r");
1669 insert_room(&db, &room).unwrap();
1670 let mut a = make_attachment(&room.id, "fid", "f.bin");
1671 a.status = AttachmentStatus::Saved;
1672 upsert_attachment(&db, &a).unwrap();
1673
1674 a.status = AttachmentStatus::Offered;
1675 upsert_attachment(&db, &a).unwrap();
1676 assert_eq!(
1677 get_attachment(&db, &room.id, "fid")
1678 .unwrap()
1679 .unwrap()
1680 .status,
1681 AttachmentStatus::Saved
1682 );
1683 }
1684
1685 #[test]
1686 fn list_attachments_for_room() {
1687 let db = open_db_in_memory().unwrap();
1688 let room = make_room("r");
1689 insert_room(&db, &room).unwrap();
1690 upsert_attachment(&db, &make_attachment(&room.id, "fid-a", "a.bin")).unwrap();
1691 upsert_attachment(&db, &make_attachment(&room.id, "fid-b", "b.bin")).unwrap();
1692 let list = list_room_attachments(&db, &room.id).unwrap();
1693 assert_eq!(list.len(), 2);
1694 assert_eq!(list[0].file_id, "fid-a");
1695 assert_eq!(list[1].file_id, "fid-b");
1696 }
1697
1698 #[test]
1699 fn attachment_status_string_round_trip() {
1700 for &s in &[
1701 AttachmentStatus::Offered,
1702 AttachmentStatus::Downloading,
1703 AttachmentStatus::Ready,
1704 AttachmentStatus::Saved,
1705 AttachmentStatus::Failed,
1706 AttachmentStatus::Cancelled,
1707 ] {
1708 assert_eq!(AttachmentStatus::from_str(s.as_str()), Some(s));
1709 }
1710 }
1711
1712 #[test]
1713 fn room_messages_query_returns_chronological() {
1714 let db = open_db_in_memory().unwrap();
1715 let room = make_room("r");
1716 insert_room(&db, &room).unwrap();
1717
1718 insert_room_message(&db, &room.id, "alice-fp", "in", "hi", 100).unwrap();
1719 insert_room_message(&db, &room.id, "me-fp", "out", "hello", 101).unwrap();
1720 insert_room_message(&db, &room.id, "alice-fp", "in", "bye", 102).unwrap();
1721
1722 let msgs = get_room_messages(&db, &room.id, 10).unwrap();
1723 assert_eq!(msgs.len(), 3);
1724 assert_eq!(msgs[0].body, "hi");
1725 assert_eq!(msgs[1].body, "hello");
1726 assert_eq!(msgs[2].body, "bye");
1727 }
1728
1729 #[test]
1730 fn search_escapes_like_wildcards() {
1731 let db = open_db_in_memory().unwrap();
1732 let room = make_room("r");
1733 insert_room(&db, &room).unwrap();
1734 insert_room_message(&db, &room.id, "fp", "in", "literal percent: 50%", 100).unwrap();
1735 insert_room_message(&db, &room.id, "fp", "in", "no special chars here", 101).unwrap();
1736
1737 let pct = search_room_messages(&db, &room.id, "%", 10).unwrap();
1739 assert_eq!(pct.len(), 1);
1740 assert!(pct[0].body.contains("50%"));
1741
1742 let underscore = search_room_messages(&db, &room.id, "_", 10).unwrap();
1744 assert!(underscore.is_empty());
1745 }
1746}