rnotifylib/destination/kinds/
telegram.rs

1use std::error::Error;
2use std::fmt::Debug;
3use chrono::{Local, SecondsFormat, TimeZone};
4use serde::{Serialize, Deserialize};
5use crate::util::http_util;
6use crate::destination::message_condition::MessageNotifyConditionConfigEntry;
7use crate::destination::{MessageDestination, SerializableDestination};
8use crate::message::formatted_detail::{FormattedMessageComponent, FormattedString, Style};
9use crate::message::{Message, MessageDetail};
10
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
12pub struct TelegramDestination {
13    bot_token: String,
14    chat_id: String,
15    #[serde(default = "Vec::new")]
16    notify: Vec<MessageNotifyConditionConfigEntry<bool>>,
17}
18
19#[derive(Serialize, Debug)]
20struct TelegramMessage {
21    chat_id: String,
22    text: String,
23    disable_notification: bool,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    parse_mode: Option<ParseMode>,
26}
27
28#[derive(Serialize, Debug)]
29enum ParseMode {
30    HTML,
31}
32
33impl TelegramMessage {
34    pub fn new(chat_id: String, message: String, notify: bool, parse_mode: Option<ParseMode>) -> Self {
35        Self {
36            chat_id,
37            text: message,
38            disable_notification: !notify,
39            parse_mode,
40        }
41    }
42}
43
44impl TelegramDestination {
45    fn to_tg_message(&self, message: &Message) -> TelegramMessage {
46        let html_formatting = message.get_message_detail().has_formatting();
47
48        let mut content = String::new();
49        content.push_str(&format!("{:?}", message.get_level()));
50        if let Some(title) = message.get_title() {
51            if html_formatting {
52                content.push_str(&format!(": <b>{}</b>", title));
53            } else {
54                content.push_str(&format!(": {}", title));
55            }
56        }
57        content.push('\n');
58
59        match message.get_message_detail() {
60            MessageDetail::Raw(raw) => content.push_str(raw),
61            MessageDetail::Formatted(formatted) => {
62                for component in formatted.components() {
63                    match component {
64                        FormattedMessageComponent::Section(title, section_content) => {
65                            content.push_str(&format!("<b><u>{}</u></b>\n", title));
66                            for part in section_content {
67                                content.push_str(&to_tg_format(part))
68                            }
69                        }
70                        FormattedMessageComponent::Text(parts) => {
71                            for part in parts {
72                                content.push_str(&to_tg_format(part))
73                            }
74                        }
75                    }
76                }
77            }
78        }
79
80
81        content.push('\n');
82        content.push_str("-----\n");
83        let timestamp = Local::timestamp_millis(&Local, message.get_unix_timestamp_millis());
84        let timestamp_string = timestamp.to_rfc3339_opts(SecondsFormat::Millis, true);
85        if html_formatting {
86            content.push_str(&format!("<pre>{}</pre>", timestamp_string));
87        } else {
88            content.push_str(&timestamp_string);
89        }
90
91        content.push('\n');
92        content.push_str(&format!("@ {}", message.get_author()));
93        let parse_mode = if html_formatting { Some(ParseMode::HTML) } else { None };
94        let notify = self.notify.iter()
95            .filter(|n| n.matches(&message))
96            .map(|n| n.get_notify())
97            .any(|b| *b);
98        TelegramMessage::new(self.chat_id.clone(), content, notify, parse_mode)
99    }
100}
101
102impl MessageDestination for TelegramDestination {
103    fn send(&self, message: &Message) -> Result<(), Box<dyn Error>> {
104        // TODO: Add component and pretty up.
105        let message = self.to_tg_message(message);
106
107        let url = format!("https://api.telegram.org/bot{}/sendMessage", self.bot_token);
108
109        http_util::post_as_json_to(&url, &message)
110    }
111}
112
113#[typetag::serde(name = "Telegram")]
114impl SerializableDestination for TelegramDestination {
115    fn as_message_destination(&self) -> &dyn MessageDestination {
116        self
117    }
118}
119
120fn to_tg_format(formatted_string: &FormattedString) -> String {
121    let mut result = String::from(formatted_string.get_string());
122    for style in formatted_string.get_styles() {
123        result = apply_style(&result, style);
124    }
125    result
126}
127
128fn apply_style(s: &str, style: &Style) -> String {
129    match style {
130        Style::Bold => format!("*{}*", s),
131        Style::Italics => format!("_{}_", s),
132        Style::Monospace => format!("`{}`", s),
133        Style::Code { lang: _ } => format!("```{}```", s),
134    }
135}