OXYGENHUB
# 34beb1a7e376 oxygenhub / crates / hub / src / notify / mod.rs
3.1 KiB · Rust · last changed Password reset and email verification: single-use hashed links, reset by verified email or an administrator's link, verification on every address change, notification mail only to verified addresses, and a file:// mail transport for development 1 month ago source blame history raw
1
//! Notifications (hub spec §10): who to tell about an event, the inbox
2
//! row, and email when the account asked for it and SMTP is configured.
3
4
use crate::db::{self, accounts::Account, notify as ndb};
5
use crate::Shared;
6
use std::collections::BTreeSet;
7
8
/// An event worth telling people about.
9
pub struct Event<'a> {
10
    pub kind: &'a str,
11
    pub title: String,
12
    pub url: String,
13
    pub actor: &'a Account,
14
    pub repo_id: i64,
15
    /// `issue` or `proposal` and its id, for thread subscribers.
16
    pub thread: Option<(&'a str, i64)>,
17
    /// Bodies to scan for `@mentions`.
18
    pub mention_text: &'a str,
19
    /// Accounts to tell whatever their subscriptions (author, assignees).
20
    pub direct: Vec<i64>,
21
}
22
23
/// `@username` tokens in a text, unique.
24
pub fn mentions(text: &str) -> Vec<String> {
25
    let re = regex::Regex::new(r"(?:^|[^\w/])@([A-Za-z0-9][A-Za-z0-9._-]{0,63})").unwrap();
26
    let mut out = BTreeSet::new();
27
    for c in re.captures_iter(text) {
28
        out.insert(c[1].trim_end_matches('.').to_string());
29
    }
30
    out.into_iter().collect()
31
}
32
33
/// Deliver an event: subscribers of the thread, watchers of the
34
/// repository, mentioned accounts and direct targets, never the actor.
35
pub async fn deliver(app: &Shared, ev: Event<'_>) {
36
    let pool = &app.db;
37
    let mut targets: BTreeSet<i64> = BTreeSet::new();
38
    if let Some((kind, id)) = ev.thread {
39
        if let Ok(subs) = ndb::subscribers(pool, kind, id).await {
40
            targets.extend(subs);
41
        }
42
        // Acting on a thread subscribes the actor to it.
43
        ndb::subscribe(pool, ev.actor.id, kind, id).await.ok();
44
    }
45
    if let Ok(w) = ndb::watchers(pool, ev.repo_id).await {
46
        targets.extend(w);
47
    }
48
    for name in mentions(ev.mention_text) {
49
        if let Ok(Some(a)) = db::accounts::account_by_name(pool, &name).await {
50
            targets.insert(a.id);
51
        }
52
    }
53
    targets.extend(ev.direct.iter().copied());
54
    targets.remove(&ev.actor.id);
55
    for id in targets {
56
        ndb::add(pool, id, ev.kind, &ev.title, &ev.url, &ev.actor.username).await.ok();
57
        if let Ok(Some(a)) = db::accounts::account_by_id(pool, id).await {
58
            // Only to addresses the account has proven it reads (hub spec §5.1.1).
59
            if a.email_notifications && a.email_verified && !a.email.is_empty() && crate::mail::enabled(app) {
60
                email(app, &a, &ev.title, &ev.url, &ev.actor.username).await;
61
            }
62
        }
63
    }
64
}
65
66
async fn email(app: &Shared, to: &Account, title: &str, url: &str, actor: &str) {
67
    let unsubscribe = format!("{}/settings/notifications?unsubscribe={}", app.config.base_url(), to.unsubscribe_key);
68
    let body = format!("{actor}: {title}\n\n{}{url}\n\nStop these emails: {unsubscribe}\n", app.config.base_url());
69
    if let Err(e) = crate::mail::send(app, to.name(), &to.email, title, &body).await {
70
        tracing::warn!("email to {}: {e}", to.email);
71
    }
72
}
73
74
#[cfg(test)]
75
mod tests {
76
    use super::*;
77
78
    #[test]
79
    fn finds_mentions() {
80
        assert_eq!(mentions("cc @andy and @bob. not an email [email protected], nor /@path"), vec!["andy", "bob"]);
81
        assert!(mentions("nothing here").is_empty());
82
    }
83
}