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