Lines of
src/telegram.rs
from check-in b3ca5f23ee
that are changed by the sequence of edits moving toward
check-in c4f7a906af:
1: //! Telegram API integration for sending messages and attachments.
2:
3: use crate::utils::{
4: Attachment,
5: validate,
6: };
7:
8: use std::{
9: collections::HashMap,
10: fmt::Debug,
11: };
12:
13: use stacked_errors::{
14: bail,
15: Result,
16: StackableErr,
17: };
18: use tgbot::{
19: api::Client,
20: types::{
b3ca5f23ee 2026-09-12 21: ChatPeerId, InputFile, InputFileReader, InputMediaDocument, InputText, MediaGroup, MediaGroupItem, Message, ParseMode::Html, SendDocument, SendMediaGroup, SendMessage
22: },
23: };
24:
25: #[derive(Debug)]
26: pub struct TelegramTransport {
27: tg: Client,
28: recipients: HashMap<String, ChatPeerId>,
29: pub default: ChatPeerId,
30: }
31:
32: impl TelegramTransport {
33: /// Creates a new `TelegramTransport` instance.
34: ///
35: /// # Arguments
36: /// * `api_key` - Telegram Bot API token.
37: /// * `recipients` - Mapping of email addresses to Telegram chat IDs.
38: /// * `settings` - Additional configuration (API gateway, default chat).
39: ///
40: /// # Errors
41: /// Returns an error if configuration values cannot be read or if Telegram
42: /// API client creation fails.
43: pub fn new (api_key: String, recipients: HashMap<String, i64>, settings: &config::Config) -> Result<TelegramTransport> {
44: let default = settings.get_int("default")
45: .context("[smtp2tg.toml] missing \"default\" recipient.\n")?;
46: let api_gateway = settings.get_string("api_gateway")
47: .context("[smtp2tg.toml] missing \"api_gateway\" destination.\n")?;
48:
49: let tg = Client::new(api_key)
50: .context("Failed to create API.\n")?
51: .with_host(api_gateway);
52: let recipients = recipients.into_iter()
53: .map(|(a, b)| (a, ChatPeerId::from(b))).collect();
54: let default = ChatPeerId::from(default);
55:
56: Ok(TelegramTransport {
57: tg,
58: recipients,
59: default,
60: })
61: }
62:
63: /// Sends a debug message to the default chat.
64: ///
65: /// # Arguments
66: /// * `msg` - Message text to send.
67: ///
68: /// # Returns
69: /// * `Result<Message>` - Telegram API response.
70: ///
71: /// # Errors
72: /// Returns an error if `msg` contains a closing Telegram tag or sending fails.
73: pub async fn debug (&self, msg: &str) -> Result<Message> {
74: self.send(&self.default, &format!("<pre>{}</pre>", validate(msg).stack()?)).await
75: }
76:
77: /// Retrieves a chat ID by name.
78: ///
79: /// # Arguments
80: /// * `name` - Name or email to look up.
81: ///
82: /// # Returns
83: /// * `Result<&ChatPeerId>` - Chat ID if found.
84: ///
85: /// # Errors
86: /// Returns an error if `name` is not configured.
87: pub fn get (&self, name: &str) -> Result<&ChatPeerId> {
88: self.recipients.get(&name.to_lowercase())
89: .with_context(|| format!("Recipient {name:?} not found in configuration"))
90: }
91:
92: /// Sends a text message to a specified chat.
93: ///
94: /// # Arguments
95: /// * `to` - Target chat ID.
96: /// * `msg` - Message text (supports HTML formatting).
97: ///
98: /// # Returns
99: /// * `Result<Message>` - Telegram API response.
100: pub async fn send (&self, to: &ChatPeerId, msg: &str) -> Result<Message> {
101: self.tg.execute(
102: SendMessage::new(*to, InputText::from(msg).with_format(Html))
103: ).await.stack()
104: }
105:
106: /// Sends a message with attachments to a specified chat.
107: ///
108: /// # Arguments
109: /// * `to` - Target chat ID.
110: /// * `media` - List of attachments, non-empty.
111: /// * `msg` - Message text (supports HTML formatting).
112: ///
113: /// # Returns
114: /// * `Result<()>` - fails if `media` is empty, problems forming media
115: /// group, request fails.
116: pub async fn sendgroup (&self, to: &ChatPeerId, media: Vec<Attachment>, msg: &str) -> Result<()> {
117: if media.len() > 1 {
118: let mut attach = vec![];
119: let mut pos = media.len();
120: for file in media {
121: let mut doc = InputMediaDocument::from(
122: InputFile::from(
123: InputFileReader::from(file.data)
124: .with_file_name(file.name)
125: )
126: );
127: if pos == 1 {
128: doc = doc.with_caption(InputText::from(msg)
129: .with_format(Html));
130: }
131: pos -= 1;
132: attach.push(MediaGroupItem::from(doc));
133: }
134: self.tg.execute(SendMediaGroup::new(*to, MediaGroup::new(attach).stack()?)).await.stack()?;
135: } else {
136: if media.is_empty() {
137: bail!("At least one attachment is required.");
138: }
139: self.tg.execute(
140: SendDocument::new(
141: *to,
142: InputFileReader::from(media[0].data.clone())
143: .with_file_name(media[0].name.clone())
144: ).with_caption(InputText::from(msg)
145: .with_format(Html))
146: ).await.stack()?;
147: }
148: Ok(())
149: }
150: }