Skip to main content

vector_core/db/
notify.rs

1//! Per-scope notification preferences: one row per community, channel or DM
2//! that deviates from the defaults. Absence of a row means "inherit
3//! everything", so an untouched account stores nothing.
4
5use std::collections::HashMap;
6
7use crate::notify::ScopePrefs;
8
9fn row_to_prefs(row: &rusqlite::Row<'_>) -> rusqlite::Result<(String, ScopePrefs)> {
10    let id: String = row.get(0)?;
11    let level: Option<u8> = row.get::<_, Option<i64>>(1)?.map(|v| v as u8);
12    let mute_until: i64 = row.get(2)?;
13    let suppress: Option<bool> = row.get::<_, Option<i64>>(3)?.map(|v| v != 0);
14    Ok((id, ScopePrefs {
15        level: level.and_then(crate::notify::NotifyLevel::from_u8),
16        mute_until,
17        suppress_everyone: suppress,
18    }))
19}
20
21/// Every stored scope. The table holds only customised scopes, so this stays
22/// small enough to keep in memory rather than querying per lookup.
23pub fn load_all() -> Result<HashMap<String, ScopePrefs>, String> {
24    let conn = super::get_db_connection_guard_static()?;
25    let mut stmt = conn
26        .prepare("SELECT scope_id, level, mute_until, suppress_everyone FROM notify_prefs")
27        .map_err(|e| format!("prepare notify_prefs: {e}"))?;
28    let rows = stmt
29        .query_map([], row_to_prefs)
30        .map_err(|e| format!("query notify_prefs: {e}"))?;
31    let mut out = HashMap::new();
32    for row in rows {
33        let (id, prefs) = row.map_err(|e| format!("read notify_prefs: {e}"))?;
34        out.insert(id, prefs);
35    }
36    Ok(out)
37}
38
39/// Write one scope. A scope back at every default is deleted rather than
40/// stored, so the table never accumulates rows that say nothing.
41pub fn save(scope_id: &str, prefs: &ScopePrefs) -> Result<(), String> {
42    let conn = super::get_write_connection_guard_static()?;
43    if prefs.is_default() {
44        conn.execute("DELETE FROM notify_prefs WHERE scope_id = ?1", rusqlite::params![scope_id])
45            .map_err(|e| format!("clear notify_prefs: {e}"))?;
46        return Ok(());
47    }
48    conn.execute(
49        "INSERT OR REPLACE INTO notify_prefs (scope_id, level, mute_until, suppress_everyone) \
50         VALUES (?1, ?2, ?3, ?4)",
51        rusqlite::params![
52            scope_id,
53            prefs.level.map(|l| l.as_u8() as i64),
54            prefs.mute_until,
55            prefs.suppress_everyone.map(|s| s as i64),
56        ],
57    )
58    .map_err(|e| format!("save notify_prefs: {e}"))?;
59    Ok(())
60}
61
62/// Clear the mute on scopes whose timer has run out, in one statement.
63pub fn clear_expired_mutes(now_ms: i64) -> Result<(), String> {
64    let conn = super::get_write_connection_guard_static()?;
65    conn.execute(
66        "UPDATE notify_prefs SET mute_until = 0 WHERE mute_until > 0 AND mute_until <= ?1",
67        rusqlite::params![now_ms],
68    )
69    .map_err(|e| format!("clear expired mutes: {e}"))?;
70    conn.execute(
71        "DELETE FROM notify_prefs WHERE level IS NULL AND mute_until = 0 AND suppress_everyone IS NULL",
72        [],
73    )
74    .map_err(|e| format!("prune notify_prefs: {e}"))?;
75    Ok(())
76}
77
78/// Replace the whole table in one transaction, for a list arriving from
79/// another device. Whole-list newest-wins is the merge these projections use,
80/// so a partial apply would be a third state neither device holds.
81pub fn replace_all(scopes: &HashMap<String, ScopePrefs>) -> Result<(), String> {
82    let mut conn = super::get_write_connection_guard_static()?;
83    let tx = conn.transaction().map_err(|e| format!("begin notify tx: {e}"))?;
84    tx.execute("DELETE FROM notify_prefs", [])
85        .map_err(|e| format!("clear notify_prefs: {e}"))?;
86    for (scope_id, prefs) in scopes.iter().filter(|(_, p)| !p.is_default()) {
87        tx.execute(
88            "INSERT OR REPLACE INTO notify_prefs (scope_id, level, mute_until, suppress_everyone) \
89             VALUES (?1, ?2, ?3, ?4)",
90            rusqlite::params![
91                scope_id,
92                prefs.level.map(|l| l.as_u8() as i64),
93                prefs.mute_until,
94                prefs.suppress_everyone.map(|s| s as i64),
95            ],
96        )
97        .map_err(|e| format!("write notify_prefs: {e}"))?;
98    }
99    tx.commit().map_err(|e| format!("commit notify tx: {e}"))?;
100    Ok(())
101}
102
103/// Every channel id its community still lists. A tombstoned channel's row is
104/// pruned here while its chat row and history stay on disk, so this is the
105/// difference between "belongs to a community" and "can still be opened".
106pub fn live_channel_ids() -> Result<std::collections::HashSet<String>, String> {
107    let conn = super::get_db_connection_guard_static()?;
108    let mut stmt = conn
109        .prepare("SELECT channel_id FROM community_channels")
110        .map_err(|e| format!("prepare live channels: {e}"))?;
111    let rows = stmt
112        .query_map([], |row| row.get::<_, String>(0))
113        .map_err(|e| format!("query live channels: {e}"))?;
114    let mut out = std::collections::HashSet::new();
115    for row in rows {
116        out.insert(row.map_err(|e| format!("read live channels: {e}"))?);
117    }
118    Ok(out)
119}