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