Skip to main content

tg_cli/
lib.rs

1use std::io::{self};
2
3use teloxide::{
4    Bot, RequestError,
5    payloads::{EditMessageTextSetters, SendMessageSetters},
6    prelude::Requester,
7    types::{ChatId, MessageId, ParseMode as TeloxideParseMode},
8};
9
10use crate::config::Config;
11
12pub(crate) mod config;
13
14#[derive(Debug)]
15pub enum SendMessageError {
16    MissingToken,
17    MissingChatId,
18    RuntimeInit(io::Error),
19    Request(RequestError),
20}
21
22impl std::fmt::Display for SendMessageError {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            SendMessageError::MissingToken => {
26                write!(f, "No token configured. Run `tg setup` first.")
27            }
28            SendMessageError::MissingChatId => {
29                write!(f, "No chat ID configured. Run `tg setup` first.")
30            }
31            SendMessageError::RuntimeInit(err) => {
32                write!(f, "Failed to initialize async runtime: {err}")
33            }
34            SendMessageError::Request(err) => write!(f, "Failed to send message: {err}"),
35        }
36    }
37}
38
39impl std::error::Error for SendMessageError {
40    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
41        match self {
42            SendMessageError::RuntimeInit(err) => Some(err),
43            SendMessageError::Request(err) => Some(err),
44            SendMessageError::MissingToken | SendMessageError::MissingChatId => None,
45        }
46    }
47}
48
49pub type TgResult<T> = Result<T, SendMessageError>;
50
51pub struct TgSession {
52    bot: Bot,
53    chat_id: ChatId,
54}
55
56#[derive(Debug, Clone, Copy)]
57pub enum ParseMode {
58    Markdown,
59    Html,
60}
61
62impl From<ParseMode> for TeloxideParseMode {
63    fn from(mode: ParseMode) -> Self {
64        match mode {
65            ParseMode::Markdown => TeloxideParseMode::MarkdownV2,
66            ParseMode::Html => TeloxideParseMode::Html,
67        }
68    }
69}
70
71impl TgSession {
72    pub fn from_config() -> TgResult<Self> {
73        let config = Config::load();
74        let token = config.token.ok_or(SendMessageError::MissingToken)?;
75        let chat_id = config.chat_id.ok_or(SendMessageError::MissingChatId)?;
76
77        Ok(Self {
78            bot: Bot::new(token),
79            chat_id: ChatId(chat_id),
80        })
81    }
82
83    fn sanitize_text(text: String) -> String {
84        text.replace("\r\n", "\n")
85            .replace('_', "\\_")
86            .replace('*', "\\*")
87            .replace('[', "\\[")
88            .replace(']', "\\]")
89            .replace('(', "\\(")
90            .replace(')', "\\)")
91            .replace('~', "\\~")
92            .replace('`', "\\`")
93            .replace('>', "\\>")
94            .replace('#', "\\#")
95            .replace('+', "\\+")
96            .replace('-', "\\-")
97            .replace('=', "\\=")
98            .replace('|', "\\|")
99            .replace('{', "\\{")
100            .replace('}', "\\}")
101            .replace('.', "\\.")
102            .replace('!', "\\!")
103    }
104
105    pub async fn send_message(
106        &self,
107        text: String,
108        parse_mode: ParseMode,
109        silent: bool,
110    ) -> TgResult<i32> {
111        let mut req = self
112            .bot
113            .send_message(self.chat_id, Self::sanitize_text(text));
114        req = req.parse_mode(parse_mode.into());
115
116        if silent {
117            req = req.disable_notification(true);
118        }
119
120        let message = req.await.map_err(SendMessageError::Request)?;
121        Ok(message.id.0)
122    }
123
124    pub async fn edit_message(
125        &self,
126        message_id: i32,
127        text: String,
128        parse_mode: ParseMode,
129    ) -> TgResult<()> {
130        let mut req = self.bot.edit_message_text(
131            self.chat_id,
132            MessageId(message_id),
133            Self::sanitize_text(text),
134        );
135        req = req.parse_mode(parse_mode.into());
136        req.await.map_err(SendMessageError::Request)?;
137        Ok(())
138    }
139}
140
141pub async fn send_tg_message(text: String, parse_mode: ParseMode, silent: bool) -> TgResult<()> {
142    let session = TgSession::from_config()?;
143    session.send_message(text, parse_mode, silent).await?;
144    Ok(())
145}
146
147pub fn send_tg_message_blocking(text: String, parse_mode: ParseMode, silent: bool) -> TgResult<()> {
148    if tokio::runtime::Handle::try_current().is_ok() {
149        let worker = std::thread::spawn(move || {
150            let rt = tokio::runtime::Builder::new_current_thread()
151                .enable_all()
152                .build()
153                .map_err(SendMessageError::RuntimeInit)?;
154            rt.block_on(send_tg_message(text, parse_mode, silent))
155        });
156
157        return match worker.join() {
158            Ok(result) => result,
159            Err(_) => Err(SendMessageError::RuntimeInit(io::Error::other(
160                "failed to join Telegram sender thread",
161            ))),
162        };
163    }
164
165    let rt = tokio::runtime::Builder::new_current_thread()
166        .enable_all()
167        .build()
168        .map_err(SendMessageError::RuntimeInit)?;
169    rt.block_on(send_tg_message(text, parse_mode, silent))
170}
171
172#[cfg(feature = "non-blocking")]
173#[macro_export]
174macro_rules! telegram {
175    () => {{
176        $crate::telegram!("")
177    }};
178    ($($arg:tt)*) => {{
179        let msg = format!($($arg)*);
180        tokio::spawn(async move {
181            if let Err(err) = $crate::send_tg_message(
182                msg,
183                $crate::ParseMode::Markdown,
184                false,
185            ).await {
186                eprintln!("{err}");
187            }
188        });
189    }};
190}
191
192#[cfg(not(feature = "non-blocking"))]
193#[macro_export]
194macro_rules! telegram {
195    () => {{
196        $crate::telegram!("")
197    }};
198    ($($arg:tt)*) => {{
199        if let Err(err) = $crate::send_tg_message_blocking(
200            format!($($arg)*),
201            $crate::ParseMode::Markdown,
202            false,
203        ) {
204            eprintln!("{err}");
205        }
206    }};
207}