OXYGENHUB
# 76f262e1a666 oxygenhub / crates / hub / src / db / notify.rs
5.5 KiB · Rust · last changed Milestone 0.4: issues and notifications 1 month ago source blame history raw
1
//! Notifications, watching, subscriptions and the search index (hub spec
2
//! §10, §11).
3
4
use super::{now_ms, Pool};
5
use sqlx::FromRow;
6
7
#[derive(Clone, Debug, FromRow)]
8
pub struct Notification {
9
    pub id: i64,
10
    pub account_id: i64,
11
    pub at_ms: i64,
12
    pub kind: String,
13
    pub title: String,
14
    pub url: String,
15
    pub actor: String,
16
    pub read: bool,
17
}
18
19
pub async fn add(pool: &Pool, account_id: i64, kind: &str, title: &str, url: &str, actor: &str) -> sqlx::Result<()> {
20
    sqlx::query("INSERT INTO notifications (account_id, at_ms, kind, title, url, actor) VALUES (?, ?, ?, ?, ?, ?)")
21
        .bind(account_id)
22
        .bind(now_ms())
23
        .bind(kind)
24
        .bind(title)
25
        .bind(url)
26
        .bind(actor)
27
        .execute(pool)
28
        .await?;
29
    Ok(())
30
}
31
32
pub async fn inbox(pool: &Pool, account_id: i64, limit: i64) -> sqlx::Result<Vec<Notification>> {
33
    sqlx::query_as("SELECT * FROM notifications WHERE account_id = ? ORDER BY read, at_ms DESC, id DESC LIMIT ?")
34
        .bind(account_id)
35
        .bind(limit)
36
        .fetch_all(pool)
37
        .await
38
}
39
40
pub async fn unread(pool: &Pool, account_id: i64) -> sqlx::Result<i64> {
41
    sqlx::query_scalar("SELECT COUNT(*) FROM notifications WHERE account_id = ? AND read = 0").bind(account_id).fetch_one(pool).await
42
}
43
44
pub async fn mark_read(pool: &Pool, account_id: i64, id: Option<i64>) -> sqlx::Result<()> {
45
    match id {
46
        Some(id) => {
47
            sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = ? AND id = ?").bind(account_id).bind(id).execute(pool).await?
48
        }
49
        None => sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = ?").bind(account_id).execute(pool).await?,
50
    };
51
    Ok(())
52
}
53
54
// ---------------------------------------------------------------- watching
55
56
pub async fn watching(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result<bool> {
57
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM watches WHERE account_id = ? AND repo_id = ?")
58
        .bind(account_id)
59
        .bind(repo_id)
60
        .fetch_one(pool)
61
        .await?;
62
    Ok(n > 0)
63
}
64
65
pub async fn set_watch(pool: &Pool, account_id: i64, repo_id: i64, on: bool) -> sqlx::Result<()> {
66
    if on {
67
        sqlx::query("INSERT OR IGNORE INTO watches (account_id, repo_id) VALUES (?, ?)")
68
            .bind(account_id)
69
            .bind(repo_id)
70
            .execute(pool)
71
            .await?;
72
    } else {
73
        sqlx::query("DELETE FROM watches WHERE account_id = ? AND repo_id = ?").bind(account_id).bind(repo_id).execute(pool).await?;
74
    }
75
    Ok(())
76
}
77
78
pub async fn watchers(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<i64>> {
79
    sqlx::query_scalar("SELECT account_id FROM watches WHERE repo_id = ?").bind(repo_id).fetch_all(pool).await
80
}
81
82
pub async fn subscribe(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result<()> {
83
    sqlx::query("INSERT OR IGNORE INTO subscriptions (account_id, kind, target_id) VALUES (?, ?, ?)")
84
        .bind(account_id)
85
        .bind(kind)
86
        .bind(target_id)
87
        .execute(pool)
88
        .await?;
89
    Ok(())
90
}
91
92
pub async fn unsubscribe(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result<()> {
93
    sqlx::query("DELETE FROM subscriptions WHERE account_id = ? AND kind = ? AND target_id = ?")
94
        .bind(account_id)
95
        .bind(kind)
96
        .bind(target_id)
97
        .execute(pool)
98
        .await?;
99
    Ok(())
100
}
101
102
pub async fn subscribed(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result<bool> {
103
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM subscriptions WHERE account_id = ? AND kind = ? AND target_id = ?")
104
        .bind(account_id)
105
        .bind(kind)
106
        .bind(target_id)
107
        .fetch_one(pool)
108
        .await?;
109
    Ok(n > 0)
110
}
111
112
pub async fn subscribers(pool: &Pool, kind: &str, target_id: i64) -> sqlx::Result<Vec<i64>> {
113
    sqlx::query_scalar("SELECT account_id FROM subscriptions WHERE kind = ? AND target_id = ?")
114
        .bind(kind)
115
        .bind(target_id)
116
        .fetch_all(pool)
117
        .await
118
}
119
120
// ------------------------------------------------------------------ search
121
122
#[derive(Clone, Debug, FromRow)]
123
pub struct Hit {
124
    pub kind: String,
125
    pub repo_id: i64,
126
    pub target_id: i64,
127
    pub number: i64,
128
    pub title: String,
129
    pub snippet: String,
130
}
131
132
/// Replace the indexed text of one thread (title plus body plus comments).
133
pub async fn index(pool: &Pool, kind: &str, repo_id: i64, target_id: i64, number: i64, title: &str, body: &str) -> sqlx::Result<()> {
134
    sqlx::query("DELETE FROM search WHERE kind = ? AND target_id = ?").bind(kind).bind(target_id).execute(pool).await?;
135
    sqlx::query("INSERT INTO search (kind, repo_id, target_id, number, title, body) VALUES (?, ?, ?, ?, ?, ?)")
136
        .bind(kind)
137
        .bind(repo_id)
138
        .bind(target_id)
139
        .bind(number)
140
        .bind(title)
141
        .bind(body)
142
        .execute(pool)
143
        .await?;
144
    Ok(())
145
}
146
147
/// Full-text hits, best first. The query is quoted so punctuation cannot
148
/// reach the FTS5 parser.
149
pub async fn search(pool: &Pool, query: &str, repo_id: Option<i64>, limit: i64) -> sqlx::Result<Vec<Hit>> {
150
    let terms: Vec<String> = query.split_whitespace().map(|t| format!("\"{}\"", t.replace('"', ""))).collect();
151
    if terms.is_empty() {
152
        return Ok(vec![]);
153
    }
154
    let q = terms.join(" ");
155
    let sql = "SELECT kind, repo_id, target_id, number, title, snippet(search, 5, '<mark>', '</mark>', '…', 12) AS snippet FROM search WHERE search MATCH ? AND (? IS NULL OR repo_id = ?) ORDER BY rank LIMIT ?";
156
    sqlx::query_as(sql).bind(q).bind(repo_id).bind(repo_id).bind(limit).fetch_all(pool).await
157
}