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