Skip to main content

chat_system/messengers/
telegram.rs

1//! Telegram messenger — Bot API polling implementation.
2
3use crate::message::MessageType;
4use crate::{Message, Messenger};
5use anyhow::Result;
6use async_trait::async_trait;
7use reqwest::Client;
8use serde_json::Value;
9use tokio::sync::Mutex;
10
11pub struct TelegramMessenger {
12    name: String,
13    api_base_url: String,
14    client: Client,
15    last_update_id: Mutex<Option<i64>>,
16    connected: bool,
17}
18
19impl TelegramMessenger {
20    pub fn new(name: impl Into<String>, token: impl Into<String>) -> Self {
21        let token = token.into();
22        Self {
23            name: name.into(),
24            api_base_url: format!("https://api.telegram.org/bot{token}"),
25            client: Client::new(),
26            last_update_id: Mutex::new(None),
27            connected: false,
28        }
29    }
30
31    pub fn with_api_base_url(mut self, url: impl Into<String>) -> Self {
32        self.api_base_url = url.into();
33        self
34    }
35
36    fn api_url(&self, method: impl AsRef<str>) -> String {
37        format!(
38            "{}/{}",
39            self.api_base_url.trim_end_matches('/'),
40            method.as_ref()
41        )
42    }
43
44    fn get_updates_url(&self, offset: Option<i64>) -> String {
45        match offset {
46            Some(offset) => format!("{}?offset={offset}&timeout=0", self.api_url("getUpdates")),
47            None => format!("{}?timeout=0", self.api_url("getUpdates")),
48        }
49    }
50}
51
52#[async_trait]
53impl Messenger for TelegramMessenger {
54    fn name(&self) -> &str {
55        &self.name
56    }
57
58    fn messenger_type(&self) -> &str {
59        "telegram"
60    }
61
62    async fn initialize(&mut self) -> Result<()> {
63        let resp = self.client.get(self.api_url("getMe")).send().await?;
64
65        let data: Value = resp.json().await?;
66        if data["ok"].as_bool().unwrap_or(false) {
67            self.connected = true;
68            Ok(())
69        } else {
70            anyhow::bail!("Telegram getMe failed: {:?}", data);
71        }
72    }
73
74    async fn send_message(&self, chat_id: &str, text: &str) -> Result<String> {
75        let resp = self
76            .client
77            .post(self.api_url("sendMessage"))
78            .json(&serde_json::json!({
79                "chat_id": chat_id,
80                "text": text,
81                "parse_mode": "HTML",
82            }))
83            .send()
84            .await?;
85
86        let data: Value = resp.json().await?;
87        if data["ok"].as_bool().unwrap_or(false) {
88            let id = data["result"]["message_id"]
89                .as_i64()
90                .map(|i| i.to_string())
91                .unwrap_or_default();
92            Ok(id)
93        } else {
94            anyhow::bail!("Telegram sendMessage failed: {:?}", data);
95        }
96    }
97
98    async fn receive_messages(&self) -> Result<Vec<Message>> {
99        let next_offset = {
100            let last_update_id = self.last_update_id.lock().await;
101            last_update_id.map(|update_id| update_id + 1)
102        };
103        let resp = self
104            .client
105            .get(self.get_updates_url(next_offset))
106            .send()
107            .await?;
108
109        let data: Value = resp.json().await?;
110        let mut messages = Vec::new();
111        let mut max_update_id: Option<i64> = None;
112
113        if let Some(updates) = data["result"].as_array() {
114            for update in updates {
115                if let Some(update_id) = update["update_id"].as_i64() {
116                    max_update_id = Some(match max_update_id {
117                        Some(current) => current.max(update_id),
118                        None => update_id,
119                    });
120                }
121
122                if let Some(msg) = update.get("message") {
123                    let id = msg["message_id"].as_i64().unwrap_or(0).to_string();
124                    let sender = msg["from"]["username"]
125                        .as_str()
126                        .or_else(|| msg["from"]["first_name"].as_str())
127                        .unwrap_or("unknown")
128                        .to_string();
129                    let content = msg["text"].as_str().unwrap_or("").to_string();
130                    let timestamp = msg["date"].as_i64().unwrap_or(0);
131                    let chat_id = msg["chat"]["id"].as_i64().map(|i| i.to_string());
132
133                    messages.push(Message {
134                        id,
135                        sender,
136                        content,
137                        timestamp,
138                        channel: chat_id,
139                        reply_to: None,
140                        thread_id: None,
141                        media: None,
142                        is_direct: false,
143                        message_type: MessageType::Text,
144                        edited_timestamp: None,
145                        reactions: None,
146                    });
147                }
148            }
149        }
150
151        if let Some(max_update_id) = max_update_id {
152            *self.last_update_id.lock().await = Some(max_update_id);
153        }
154
155        Ok(messages)
156    }
157
158    fn is_connected(&self) -> bool {
159        self.connected
160    }
161
162    async fn disconnect(&mut self) -> Result<()> {
163        *self.last_update_id.lock().await = None;
164        self.connected = false;
165        Ok(())
166    }
167}