rnotifylib/destination/kinds/
telegram.rs1use 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(×tamp_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 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}