| 18e381ea Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
use crate::db::{self, accounts::Account, notify as ndb}; |
| 5 |
use crate::Shared; |
| 6 |
use std::collections::BTreeSet; |
| 7 |
|
| bff9106e Andy McCall ↶ |
8 |
tokio::task_local! { |
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
pub static AGENT: Option<String>; |
| 13 |
} |
| 14 |
|
| 15 |
|
| 16 |
pub fn agent_acting() -> Option<String> { |
| 17 |
AGENT.try_with(|a| a.clone()).ok().flatten() |
| 18 |
} |
| 19 |
|
| 18e381ea Andy McCall ↶ |
20 |
|
| 21 |
pub struct Event<'a> { |
| 22 |
pub kind: &'a str, |
| 23 |
pub title: String, |
| 24 |
pub url: String, |
| 25 |
pub actor: &'a Account, |
| 26 |
pub repo_id: i64, |
| 27 |
|
| 28 |
pub thread: Option<(&'a str, i64)>, |
| 29 |
|
| 30 |
pub mention_text: &'a str, |
| 31 |
|
| 32 |
pub direct: Vec<i64>, |
| 33 |
} |
| 34 |
|
| 35 |
|
| 36 |
pub fn mentions(text: &str) -> Vec<String> { |
| 37 |
let re = regex::Regex::new(r"(?:^|[^\w/])@([A-Za-z0-9][A-Za-z0-9._-]{0,63})").unwrap(); |
| 38 |
let mut out = BTreeSet::new(); |
| 39 |
for c in re.captures_iter(text) { |
| 40 |
out.insert(c[1].trim_end_matches('.').to_string()); |
| 41 |
} |
| 42 |
out.into_iter().collect() |
| 43 |
} |
| 44 |
|
| 45 |
|
| 46 |
|
| 47 |
pub async fn deliver(app: &Shared, ev: Event<'_>) { |
| 48 |
let pool = &app.db; |
| eb0c44a9 Andy McCall ↶ |
49 |
|
| 50 |
|
| 51 |
let mut participants: BTreeSet<i64> = BTreeSet::new(); |
| 18e381ea Andy McCall ↶ |
52 |
if let Some((kind, id)) = ev.thread { |
| 53 |
if let Ok(subs) = ndb::subscribers(pool, kind, id).await { |
| eb0c44a9 Andy McCall ↶ |
54 |
participants.extend(subs); |
| 18e381ea Andy McCall ↶ |
55 |
} |
| 56 |
|
| 57 |
ndb::subscribe(pool, ev.actor.id, kind, id).await.ok(); |
| 58 |
} |
| 59 |
for name in mentions(ev.mention_text) { |
| 60 |
if let Ok(Some(a)) = db::accounts::account_by_name(pool, &name).await { |
| eb0c44a9 Andy McCall ↶ |
61 |
participants.insert(a.id); |
| 18e381ea Andy McCall ↶ |
62 |
} |
| 63 |
} |
| eb0c44a9 Andy McCall ↶ |
64 |
participants.extend(ev.direct.iter().copied()); |
| 65 |
let mut targets: Vec<(i64, ndb::Reason)> = participants.iter().map(|id| (*id, ndb::Reason::Participating)).collect(); |
| 66 |
if let Ok(w) = ndb::watchers(pool, ev.repo_id).await { |
| 67 |
targets.extend(w.into_iter().filter(|id| !participants.contains(id)).map(|id| (id, ndb::Reason::Watching))); |
| 68 |
} |
| bff9106e Andy McCall ↶ |
69 |
|
| 70 |
|
| 71 |
let agent = agent_acting(); |
| 72 |
if agent.is_none() { |
| 73 |
targets.retain(|(id, _)| *id != ev.actor.id); |
| 74 |
} |
| 75 |
let actor = match &agent { |
| 76 |
Some(token) => format!("{} (agent token {token})", ev.actor.username), |
| 77 |
None => ev.actor.username.clone(), |
| 78 |
}; |
| 79 |
tell(app, ev.kind, &ev.title, &ev.url, &actor, (ev.repo_id > 0).then_some(ev.repo_id), targets).await; |
| 80 |
} |
| 81 |
|
| 82 |
|
| 83 |
|
| 84 |
pub async fn tell(app: &Shared, kind: &str, title: &str, url: &str, actor: &str, repo_id: Option<i64>, targets: Vec<(i64, ndb::Reason)>) { |
| 85 |
let pool = &app.db; |
| eb0c44a9 Andy McCall ↶ |
86 |
for (id, reason) in targets { |
| bff9106e Andy McCall ↶ |
87 |
ndb::add(pool, ndb::New { account_id: id, repo_id, reason, kind, title, url, actor }).await.ok(); |
| 33d98147 Andy McCall |
88 |
app.bus.live(&crate::events::topic::user(id), "notification"); |
| 18e381ea Andy McCall ↶ |
89 |
if let Ok(Some(a)) = db::accounts::account_by_id(pool, id).await { |
| 34beb1a7 Andy McCall ↶ |
90 |
|
| 91 |
if a.email_notifications && a.email_verified && !a.email.is_empty() && crate::mail::enabled(app) { |
| bff9106e Andy McCall ↶ |
92 |
email(app, &a, title, url, actor).await; |
| 18e381ea Andy McCall ↶ |
93 |
} |
| 94 |
} |
| 95 |
} |
| 96 |
} |
| 97 |
|
| 98 |
async fn email(app: &Shared, to: &Account, title: &str, url: &str, actor: &str) { |
| 99 |
let unsubscribe = format!("{}/settings/notifications?unsubscribe={}", app.config.base_url(), to.unsubscribe_key); |
| cd46e56b Andy McCall ↶ |
100 |
let link = format!("{}{url}", app.config.base_url()); |
| 101 |
let body = format!("{actor}: {title}\n\n{link}\n\nStop these emails: {unsubscribe}\n"); |
| 102 |
let action = crate::mail::Action { label: "Open it", url: &link }; |
| 103 |
if let Err(e) = crate::mail::send_letter(app, to.name(), &to.email, title, &body, Some(action), Some(&unsubscribe)).await { |
| 18e381ea Andy McCall ↶ |
104 |
tracing::warn!("email to {}: {e}", to.email); |
| dd327d76 Andy McCall ↶ |
105 |
|
| 106 |
if crate::mail::enabled(app) { |
| 107 |
crate::queue::queue_mail(app, to.name(), &to.email, &subject, &body, Some(("Open it", &link)), Some(&unsubscribe)).await; |
| 108 |
} |
| 18e381ea Andy McCall ↶ |
109 |
} |
| 110 |
} |
| 111 |
|
| 112 |
#[cfg(test)] |
| 113 |
mod tests { |
| 114 |
use super::*; |
| 115 |
|
| 116 |
#[test] |
| 117 |
fn finds_mentions() { |
| 118 |
assert_eq!(mentions("cc @andy and @bob. not an email [email protected], nor /@path"), vec!["andy", "bob"]); |
| 119 |
assert!(mentions("nothing here").is_empty()); |
| 120 |
} |
| 121 |
} |