Skip to main content

codoseo_notify/
slack.rs

1//! Slack incoming-webhook JSON: a plain-text fallback plus blocks.
2
3use codoseo_core::check::Severity;
4use serde_json::{Value, json};
5
6use crate::message::{AlertItem, AlertMessage, fit_lines, one_line, short_url, truncate};
7
8/// Slack's limit for a section's text.
9const SECTION_BUDGET: usize = 2800;
10
11fn emoji(s: Severity) -> &'static str {
12    match s {
13        Severity::Critical => ":red_circle:",
14        Severity::Warning => ":large_orange_circle:",
15        Severity::Notice => ":large_blue_circle:",
16    }
17}
18
19/// Slack treats `&`, `<` and `>` as markup (`<!channel>` pings everyone), so page content is
20/// escaped before it goes in.
21fn escape(s: &str) -> String {
22    s.replace('&', "&amp;")
23        .replace('<', "&lt;")
24        .replace('>', "&gt;")
25}
26
27fn line(item: &AlertItem) -> String {
28    let mut out = format!("{} *{}*", emoji(item.severity), escape(&item.kind_label));
29    if let Some(url) = &item.url {
30        out.push_str(&format!(
31            " <{}|{}>",
32            escape(url),
33            escape(&short_url(url, 70))
34        ));
35    }
36    let change = item.change_text();
37    if !change.is_empty() {
38        out.push_str(&format!(": {}", escape(&truncate(&one_line(&change), 80))));
39    }
40    out
41}
42
43pub fn payload(msg: &AlertMessage) -> Value {
44    let mut blocks = vec![json!({
45        "type": "header",
46        "text": {"type": "plain_text", "text": truncate(&msg.headline, 150)},
47    })];
48    let mut more = msg.more as usize;
49    if !msg.items.is_empty() {
50        let lines: Vec<String> = msg.items.iter().map(line).collect();
51        let (text, left_out) = fit_lines(&lines, SECTION_BUDGET);
52        more += left_out;
53        blocks.push(json!({"type": "section", "text": {"type": "mrkdwn", "text": text}}));
54    }
55    if more > 0 {
56        blocks.push(json!({
57            "type": "context",
58            "elements": [{"type": "mrkdwn", "text": format!("and {more} more")}],
59        }));
60    }
61    blocks.push(json!({
62        "type": "actions",
63        "elements": [{
64            "type": "button",
65            "text": {"type": "plain_text", "text": "Open in CodoSEO"},
66            "url": msg.site_url,
67        }],
68    }));
69    json!({"text": escape(&msg.headline), "blocks": blocks})
70}