78b11b0319 2026-08-01 arcade: //! Telegram API integration for sending messages and attachments.
78b11b0319 2026-08-01 arcade:
f5ed284f8c 2025-06-21 arcade: use crate::utils::{
f5ed284f8c 2025-06-21 arcade: Attachment,
d87e80b9be 2026-01-12 arcade: validate,
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: use std::{
f5ed284f8c 2025-06-21 arcade: collections::HashMap,
f5ed284f8c 2025-06-21 arcade: fmt::Debug,
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade:
a044f68fa7 2025-08-23 arcade: use stacked_errors::{
78b11b0319 2026-08-01 arcade: bail,
f5ed284f8c 2025-06-21 arcade: Result,
a044f68fa7 2025-08-23 arcade: StackableErr,
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: use tgbot::{
f5ed284f8c 2025-06-21 arcade: api::Client,
f5ed284f8c 2025-06-21 arcade: types::{
f5ed284f8c 2025-06-21 arcade: ChatPeerId,
f5ed284f8c 2025-06-21 arcade: InputFile,
f5ed284f8c 2025-06-21 arcade: InputFileReader,
f5ed284f8c 2025-06-21 arcade: InputMediaDocument,
42867194d6 2026-09-13 arcade: InputText,
f5ed284f8c 2025-06-21 arcade: MediaGroup,
f5ed284f8c 2025-06-21 arcade: MediaGroupItem,
f5ed284f8c 2025-06-21 arcade: Message,
d87e80b9be 2026-01-12 arcade: ParseMode::Html,
42867194d6 2026-09-13 arcade: SendDocument,
f5ed284f8c 2025-06-21 arcade: SendMediaGroup,
f5ed284f8c 2025-06-21 arcade: SendMessage,
f5ed284f8c 2025-06-21 arcade: },
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: #[derive(Debug)]
f5ed284f8c 2025-06-21 arcade: pub struct TelegramTransport {
f5ed284f8c 2025-06-21 arcade: tg: Client,
f5ed284f8c 2025-06-21 arcade: recipients: HashMap<String, ChatPeerId>,
f5ed284f8c 2025-06-21 arcade: pub default: ChatPeerId,
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: impl TelegramTransport {
78b11b0319 2026-08-01 arcade: /// Creates a new `TelegramTransport` instance.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Arguments
78b11b0319 2026-08-01 arcade: /// * `api_key` - Telegram Bot API token.
78b11b0319 2026-08-01 arcade: /// * `recipients` - Mapping of email addresses to Telegram chat IDs.
78b11b0319 2026-08-01 arcade: /// * `settings` - Additional configuration (API gateway, default chat).
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Errors
78b11b0319 2026-08-01 arcade: /// Returns an error if configuration values cannot be read or if Telegram
78b11b0319 2026-08-01 arcade: /// API client creation fails.
14ef340959 2026-01-01 arcade: pub fn new (api_key: String, recipients: HashMap<String, i64>, settings: &config::Config) -> Result<TelegramTransport> {
14ef340959 2026-01-01 arcade: let default = settings.get_int("default")
14ef340959 2026-01-01 arcade: .context("[smtp2tg.toml] missing \"default\" recipient.\n")?;
14ef340959 2026-01-01 arcade: let api_gateway = settings.get_string("api_gateway")
14ef340959 2026-01-01 arcade: .context("[smtp2tg.toml] missing \"api_gateway\" destination.\n")?;
d87e80b9be 2026-01-12 arcade:
f5ed284f8c 2025-06-21 arcade: let tg = Client::new(api_key)
14ef340959 2026-01-01 arcade: .context("Failed to create API.\n")?
14ef340959 2026-01-01 arcade: .with_host(api_gateway);
f5ed284f8c 2025-06-21 arcade: let recipients = recipients.into_iter()
f5ed284f8c 2025-06-21 arcade: .map(|(a, b)| (a, ChatPeerId::from(b))).collect();
f5ed284f8c 2025-06-21 arcade: let default = ChatPeerId::from(default);
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: Ok(TelegramTransport {
f5ed284f8c 2025-06-21 arcade: tg,
f5ed284f8c 2025-06-21 arcade: recipients,
f5ed284f8c 2025-06-21 arcade: default,
f5ed284f8c 2025-06-21 arcade: })
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
78b11b0319 2026-08-01 arcade: /// Sends a debug message to the default chat.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Arguments
78b11b0319 2026-08-01 arcade: /// * `msg` - Message text to send.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Returns
78b11b0319 2026-08-01 arcade: /// * `Result<Message>` - Telegram API response.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Errors
78b11b0319 2026-08-01 arcade: /// Returns an error if `msg` contains a closing Telegram tag or sending fails.
78b11b0319 2026-08-01 arcade: pub async fn debug (&self, msg: &str) -> Result<Message> {
42867194d6 2026-09-13 arcade: self.send(&self.default, &format!("<pre>{}</pre>", validate(msg).stack()?)).await
78b11b0319 2026-08-01 arcade: }
78b11b0319 2026-08-01 arcade:
78b11b0319 2026-08-01 arcade: /// Retrieves a chat ID by name.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Arguments
78b11b0319 2026-08-01 arcade: /// * `name` - Name or email to look up.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Returns
78b11b0319 2026-08-01 arcade: /// * `Result<&ChatPeerId>` - Chat ID if found.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Errors
78b11b0319 2026-08-01 arcade: /// Returns an error if `name` is not configured.
78b11b0319 2026-08-01 arcade: pub fn get (&self, name: &str) -> Result<&ChatPeerId> {
78b11b0319 2026-08-01 arcade: self.recipients.get(&name.to_lowercase())
42867194d6 2026-09-13 arcade: .with_context(|| format!("Recipient {name:?} not found in configuration"))
78b11b0319 2026-08-01 arcade: }
78b11b0319 2026-08-01 arcade:
78b11b0319 2026-08-01 arcade: /// Sends a text message to a specified chat.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Arguments
78b11b0319 2026-08-01 arcade: /// * `to` - Target chat ID.
78b11b0319 2026-08-01 arcade: /// * `msg` - Message text (supports HTML formatting).
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Returns
78b11b0319 2026-08-01 arcade: /// * `Result<Message>` - Telegram API response.
42867194d6 2026-09-13 arcade: pub async fn send (&self, to: &ChatPeerId, msg: &str) -> Result<Message> {
78b11b0319 2026-08-01 arcade: self.tg.execute(
42867194d6 2026-09-13 arcade: SendMessage::new(*to, InputText::from(msg).with_format(Html))
a044f68fa7 2025-08-23 arcade: ).await.stack()
a044f68fa7 2025-08-23 arcade: }
a044f68fa7 2025-08-23 arcade:
78b11b0319 2026-08-01 arcade: /// Sends a message with attachments to a specified chat.
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Arguments
78b11b0319 2026-08-01 arcade: /// * `to` - Target chat ID.
78b11b0319 2026-08-01 arcade: /// * `media` - List of attachments, non-empty.
78b11b0319 2026-08-01 arcade: /// * `msg` - Message text (supports HTML formatting).
78b11b0319 2026-08-01 arcade: ///
78b11b0319 2026-08-01 arcade: /// # Returns
42867194d6 2026-09-13 arcade: /// * `Result<()>` - fails if `media` is empty, problems forming media
42867194d6 2026-09-13 arcade: /// group, request fails.
f5ed284f8c 2025-06-21 arcade: pub async fn sendgroup (&self, to: &ChatPeerId, media: Vec<Attachment>, msg: &str) -> Result<()> {
f5ed284f8c 2025-06-21 arcade: if media.len() > 1 {
f5ed284f8c 2025-06-21 arcade: let mut attach = vec![];
f5ed284f8c 2025-06-21 arcade: let mut pos = media.len();
f5ed284f8c 2025-06-21 arcade: for file in media {
42867194d6 2026-09-13 arcade: let mut doc = InputMediaDocument::from(
42867194d6 2026-09-13 arcade: InputFile::from(
42867194d6 2026-09-13 arcade: InputFileReader::from(file.data)
42867194d6 2026-09-13 arcade: .with_file_name(file.name)
42867194d6 2026-09-13 arcade: )
42867194d6 2026-09-13 arcade: );
f5ed284f8c 2025-06-21 arcade: if pos == 1 {
42867194d6 2026-09-13 arcade: doc = doc.with_caption(InputText::from(msg)
42867194d6 2026-09-13 arcade: .with_format(Html));
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: pos -= 1;
42867194d6 2026-09-13 arcade: attach.push(MediaGroupItem::from(doc));
a044f68fa7 2025-08-23 arcade: }
a044f68fa7 2025-08-23 arcade: self.tg.execute(SendMediaGroup::new(*to, MediaGroup::new(attach).stack()?)).await.stack()?;
a044f68fa7 2025-08-23 arcade: } else {
78b11b0319 2026-08-01 arcade: if media.is_empty() {
78b11b0319 2026-08-01 arcade: bail!("At least one attachment is required.");
78b11b0319 2026-08-01 arcade: }
f5ed284f8c 2025-06-21 arcade: self.tg.execute(
f5ed284f8c 2025-06-21 arcade: SendDocument::new(
f5ed284f8c 2025-06-21 arcade: *to,
f5ed284f8c 2025-06-21 arcade: InputFileReader::from(media[0].data.clone())
42867194d6 2026-09-13 arcade: .with_file_name(media[0].name.clone())
42867194d6 2026-09-13 arcade: ).with_caption(InputText::from(msg)
42867194d6 2026-09-13 arcade: .with_format(Html))
a044f68fa7 2025-08-23 arcade: ).await.stack()?;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: Ok(())
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }