Skip to main content

codoseo_notify/
discord.rs

1//! Discord webhook JSON: a short `content` line and one embed, inside Discord's limits
2//! (2000-character content, 256-character title, 4096-character description, 25 fields).
3
4use codoseo_core::check::Severity;
5use serde_json::{Value, json};
6
7use crate::message::{AlertItem, AlertMessage, fit_lines, one_line, truncate};
8
9const DESCRIPTION_BUDGET: usize = 3900;
10
11fn emoji(s: Severity) -> &'static str {
12    match s {
13        Severity::Critical => ":red_circle:",
14        Severity::Warning => ":orange_circle:",
15        Severity::Notice => ":blue_circle:",
16    }
17}
18
19fn color(s: Severity) -> u32 {
20    match s {
21        Severity::Critical => 0xE5484D,
22        Severity::Warning => 0xF5A524,
23        Severity::Notice => 0x3E63DD,
24    }
25}
26
27fn line(item: &AlertItem) -> String {
28    let mut out = format!("{} **{}**", emoji(item.severity), item.kind_label);
29    if let Some(url) = &item.url {
30        // Angle brackets keep Discord from unfurling every link.
31        out.push_str(&format!(" <{}>", url.replace('>', "%3E")));
32    }
33    let change = item.change_text();
34    if !change.is_empty() {
35        out.push_str(&format!(": {}", truncate(&one_line(&change), 80)));
36    }
37    truncate(&out, 300)
38}
39
40pub fn payload(msg: &AlertMessage) -> Value {
41    let lines: Vec<String> = msg.items.iter().map(line).collect();
42    let (mut description, left_out) = fit_lines(&lines, DESCRIPTION_BUDGET);
43    let more = msg.more as usize + left_out;
44    if more > 0 {
45        if !description.is_empty() {
46            description.push_str("\n\n");
47        }
48        description.push_str(&format!("and {more} more"));
49    }
50    let mut embed = json!({
51        "title": truncate(&msg.site_domain, 256),
52        "url": msg.site_url,
53        "color": color(msg.severity()),
54    });
55    if !description.is_empty() {
56        embed["description"] = Value::String(description);
57    }
58    json!({
59        "content": truncate(&msg.headline, 2000),
60        // Page content must never ping anyone.
61        "allowed_mentions": {"parse": []},
62        "embeds": [embed],
63    })
64}