use super::traits::{Channel, ChannelMessage}; use async_trait::async_trait; use uuid::Uuid; /// Telegram channel — long-polls the Bot API for updates pub struct TelegramChannel { bot_token: String, allowed_users: Vec, client: reqwest::Client, } impl TelegramChannel { pub fn new(bot_token: String, allowed_users: Vec) -> Self { Self { bot_token, allowed_users, client: reqwest::Client::new(), } } fn api_url(&self, method: &str) -> String { format!("https://api.telegram.org/bot{}/{method}", self.bot_token) } fn is_user_allowed(&self, username: &str) -> bool { self.allowed_users.iter().any(|u| u == "*" || u == username) } } #[async_trait] impl Channel for TelegramChannel { fn name(&self) -> &str { "telegram" } async fn send(&self, message: &str, chat_id: &str) -> anyhow::Result<()> { let body = serde_json::json!({ "chat_id": chat_id, "text": message, "parse_mode": "Markdown" }); self.client .post(self.api_url("sendMessage")) .json(&body) .send() .await?; Ok(()) } async fn listen(&self, tx: tokio::sync::mpsc::Sender) -> anyhow::Result<()> { let mut offset: i64 = 0; tracing::info!("Telegram channel listening for messages..."); loop { let url = self.api_url("getUpdates"); let body = serde_json::json!({ "offset": offset, "timeout": 30, "allowed_updates": ["message"] }); let resp = match self.client.post(&url).json(&body).send().await { Ok(r) => r, Err(e) => { tracing::warn!("Telegram poll error: {e}"); tokio::time::sleep(std::time::Duration::from_secs(5)).await; continue; } }; let data: serde_json::Value = match resp.json().await { Ok(d) => d, Err(e) => { tracing::warn!("Telegram parse error: {e}"); tokio::time::sleep(std::time::Duration::from_secs(5)).await; continue; } }; if let Some(results) = data.get("result").and_then(serde_json::Value::as_array) { for update in results { // Advance offset past this update if let Some(uid) = update.get("update_id").and_then(serde_json::Value::as_i64) { offset = uid + 1; } let Some(message) = update.get("message") else { continue; }; let Some(text) = message.get("text").and_then(serde_json::Value::as_str) else { continue; }; let username = message .get("from") .and_then(|f| f.get("username")) .and_then(|u| u.as_str()) .unwrap_or("unknown"); if !self.is_user_allowed(username) { tracing::warn!( "Telegram: ignoring message from unauthorized user: {username}" ); continue; } let chat_id = message .get("chat") .and_then(|c| c.get("id")) .and_then(serde_json::Value::as_i64) .map(|id| id.to_string()) .unwrap_or_default(); let msg = ChannelMessage { id: Uuid::new_v4().to_string(), sender: chat_id, content: text.to_string(), channel: "telegram".to_string(), timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(), }; if tx.send(msg).await.is_err() { return Ok(()); } } } } } async fn health_check(&self) -> bool { self.client .get(self.api_url("getMe")) .send() .await .map(|r| r.status().is_success()) .unwrap_or(false) } } #[cfg(test)] mod tests { use super::*; #[test] fn telegram_channel_name() { let ch = TelegramChannel::new("fake-token".into(), vec!["*".into()]); assert_eq!(ch.name(), "telegram"); } #[test] fn telegram_api_url() { let ch = TelegramChannel::new("123:ABC".into(), vec![]); assert_eq!( ch.api_url("getMe"), "https://api.telegram.org/bot123:ABC/getMe" ); } #[test] fn telegram_user_allowed_wildcard() { let ch = TelegramChannel::new("t".into(), vec!["*".into()]); assert!(ch.is_user_allowed("anyone")); } #[test] fn telegram_user_allowed_specific() { let ch = TelegramChannel::new("t".into(), vec!["alice".into(), "bob".into()]); assert!(ch.is_user_allowed("alice")); assert!(!ch.is_user_allowed("eve")); } #[test] fn telegram_user_denied_empty() { let ch = TelegramChannel::new("t".into(), vec![]); assert!(!ch.is_user_allowed("anyone")); } #[test] fn telegram_user_exact_match_not_substring() { let ch = TelegramChannel::new("t".into(), vec!["alice".into()]); assert!(!ch.is_user_allowed("alice_bot")); assert!(!ch.is_user_allowed("alic")); assert!(!ch.is_user_allowed("malice")); } #[test] fn telegram_user_empty_string_denied() { let ch = TelegramChannel::new("t".into(), vec!["alice".into()]); assert!(!ch.is_user_allowed("")); } #[test] fn telegram_user_case_sensitive() { let ch = TelegramChannel::new("t".into(), vec!["Alice".into()]); assert!(ch.is_user_allowed("Alice")); assert!(!ch.is_user_allowed("alice")); assert!(!ch.is_user_allowed("ALICE")); } #[test] fn telegram_wildcard_with_specific_users() { let ch = TelegramChannel::new("t".into(), vec!["alice".into(), "*".into()]); assert!(ch.is_user_allowed("alice")); assert!(ch.is_user_allowed("bob")); assert!(ch.is_user_allowed("anyone")); } }