Skip to main content

chipa_webhooks/platform/
telegram.rs

1use serde_json::{Map, Value, json};
2
3use super::Platform;
4
5pub struct Telegram {
6    token: String,
7    chat_id: i64,
8    parse_mode: ParseMode,
9}
10
11#[derive(Clone, Copy, Default)]
12pub enum ParseMode {
13    #[default]
14    MarkdownV2,
15    Html,
16    Plain,
17}
18
19impl ParseMode {
20    fn as_str(&self) -> Option<&'static str> {
21        match self {
22            Self::MarkdownV2 => Some("MarkdownV2"),
23            Self::Html => Some("HTML"),
24            Self::Plain => None,
25        }
26    }
27}
28
29impl Telegram {
30    pub fn new(token: impl Into<String>, chat_id: i64) -> Self {
31        Self {
32            token: token.into(),
33            chat_id,
34            parse_mode: ParseMode::default(),
35        }
36    }
37
38    pub fn with_parse_mode(mut self, parse_mode: ParseMode) -> Self {
39        self.parse_mode = parse_mode;
40        self
41    }
42}
43
44/// Escapes all reserved MarkdownV2 characters as required by Telegram.
45/// See: https://core.telegram.org/bots/api#markdownv2-style
46fn escape_markdown_v2(text: &str) -> String {
47    const RESERVED: &[char] = &[
48        '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!',
49    ];
50
51    let mut out = String::with_capacity(text.len());
52    for ch in text.chars() {
53        if RESERVED.contains(&ch) {
54            out.push('\\');
55        }
56        out.push(ch);
57    }
58    out
59}
60
61impl Platform for Telegram {
62    fn build_payload(&self, rendered: &str, hints: &Map<String, Value>) -> Value {
63        let text = match self.parse_mode {
64            ParseMode::MarkdownV2 => escape_markdown_v2(rendered),
65            _ => rendered.to_owned(),
66        };
67
68        let mut payload = json!({
69            "chat_id": self.chat_id,
70            "text": text,
71        });
72
73        if let Some(mode) = self.parse_mode.as_str() {
74            payload["parse_mode"] = Value::String(mode.to_owned());
75        }
76
77        if hints
78            .get("__tg_silent")
79            .and_then(Value::as_bool)
80            .unwrap_or(false)
81        {
82            payload["disable_notification"] = Value::Bool(true);
83        }
84
85        if hints
86            .get("__tg_disable_preview")
87            .and_then(Value::as_bool)
88            .unwrap_or(false)
89        {
90            payload["disable_web_page_preview"] = Value::Bool(true);
91        }
92
93        payload
94    }
95
96    fn endpoint(&self) -> &str {
97        // Telegram requires a static-lifetime string for endpoint() but we build
98        // the URL dynamically, so we leak it once per Telegram instance.
99        // This is acceptable since Telegram instances are created at startup and
100        // live for the duration of the program.
101        Box::leak(
102            format!("https://api.telegram.org/bot{}/sendMessage", self.token).into_boxed_str(),
103        )
104    }
105}