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