Lines of src/telegram.rs from check-in 14ef340959 that are changed by the sequence of edits moving toward check-in 0f47e23e21:
14ef340959 2026-01-01 1: use crate::utils::{ 14ef340959 2026-01-01 2: Attachment, 14ef340959 2026-01-01 3: RE_SPECIAL, 14ef340959 2026-01-01 4: }; 5: 6: use std::{ 14ef340959 2026-01-01 7: borrow::Cow, 8: collections::HashMap, 9: fmt::Debug, 10: }; 11: 12: use stacked_errors::{ 13: Result, 14: StackableErr, 15: }; 16: use tgbot::{ 17: api::Client, 18: types::{ 19: ChatPeerId, 20: InputFile, 21: InputFileReader, 22: InputMediaDocument, 23: MediaGroup, 24: MediaGroupItem, 25: Message, 14ef340959 2026-01-01 26: ParseMode::MarkdownV2, 27: SendMediaGroup, 28: SendMessage, 29: SendDocument, 30: }, 31: }; 14ef340959 2026-01-01 32: 14ef340959 2026-01-01 33: /// Encodes special HTML entities to prevent them interfering with Telegram HTML 14ef340959 2026-01-01 34: pub fn encode (text: &str) -> Cow<'_, str> { 14ef340959 2026-01-01 35: RE_SPECIAL.replace_all(text, "\\$1") 14ef340959 2026-01-01 36: } 37: 38: #[derive(Debug)] 39: pub struct TelegramTransport { 40: tg: Client, 41: recipients: HashMap<String, ChatPeerId>, 42: pub default: ChatPeerId, 43: } 44: 45: impl TelegramTransport { 14ef340959 2026-01-01 46: 47: pub fn new (api_key: String, recipients: HashMap<String, i64>, settings: &config::Config) -> Result<TelegramTransport> { 48: let default = settings.get_int("default") 49: .context("[smtp2tg.toml] missing \"default\" recipient.\n")?; 50: let api_gateway = settings.get_string("api_gateway") 51: .context("[smtp2tg.toml] missing \"api_gateway\" destination.\n")?; 52: let tg = Client::new(api_key) 53: .context("Failed to create API.\n")? 54: .with_host(api_gateway); 55: let recipients = recipients.into_iter() 56: .map(|(a, b)| (a, ChatPeerId::from(b))).collect(); 57: let default = ChatPeerId::from(default); 58: 59: Ok(TelegramTransport { 60: tg, 61: recipients, 62: default, 63: }) 64: } 65: 66: /// Send message to default user, used for debug/log/info purposes 67: pub async fn debug (&self, msg: &str) -> Result<Message> { 14ef340959 2026-01-01 68: self.send(&self.default, encode(msg)).await 69: } 70: 71: /// Get recipient by address 72: pub fn get (&self, name: &str) -> Result<&ChatPeerId> { 73: self.recipients.get(name) 74: .with_context(|| format!("Recipient \"{name}\" not found in configuration")) 75: } 76: 77: /// Send message to specified user 78: pub async fn send <S> (&self, to: &ChatPeerId, msg: S) -> Result<Message> 79: where S: Into<String> + Debug{ 80: self.tg.execute( 81: SendMessage::new(*to, msg) 14ef340959 2026-01-01 82: .with_parse_mode(MarkdownV2) 83: ).await.stack() 84: } 85: 86: /// Send media to specified user 87: pub async fn sendgroup (&self, to: &ChatPeerId, media: Vec<Attachment>, msg: &str) -> Result<()> { 88: if media.len() > 1 { 89: let mut attach = vec![]; 90: let mut pos = media.len(); 91: for file in media { 92: let mut caption = InputMediaDocument::default(); 93: if pos == 1 { 94: caption = caption.with_caption(msg) 14ef340959 2026-01-01 95: .with_caption_parse_mode(MarkdownV2); 96: } 97: pos -= 1; 98: attach.push( 99: MediaGroupItem::for_document( 100: InputFile::from( 101: InputFileReader::from(file.data) 102: .with_file_name(file.name) 103: ), 104: caption 105: ) 106: ); 107: } 108: self.tg.execute(SendMediaGroup::new(*to, MediaGroup::new(attach).stack()?)).await.stack()?; 109: } else { 110: self.tg.execute( 111: SendDocument::new( 112: *to, 113: InputFileReader::from(media[0].data.clone()) 114: .with_file_name(media[0].name.clone()) 115: ).with_caption(msg) 14ef340959 2026-01-01 116: .with_caption_parse_mode(MarkdownV2) 117: ).await.stack()?; 118: } 119: Ok(()) 120: } 121: }