1use std::sync::Arc;
2
3use botkit_core::{BotBuilder, BotError, Context, ContextData, IntoHandler, Response};
4use executor_core::spawn;
5use http_kit::{Body, Endpoint, HttpError, Request, Response as HttpResponse, StatusCode};
6use tracing::{error, warn};
7
8use crate::client::TelegramClient;
9use crate::event::TelegramContextData;
10use crate::types::{
11 BotCommand, InlineKeyboardButton, InlineKeyboardMarkup, ReplyMarkup, Update, UpdateKind,
12};
13
14#[derive(Debug)]
16pub struct WebhookError(BotError);
17
18impl std::fmt::Display for WebhookError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "{}", self.0)
21 }
22}
23
24impl std::error::Error for WebhookError {}
25
26impl HttpError for WebhookError {
27 fn status(&self) -> StatusCode {
28 StatusCode::INTERNAL_SERVER_ERROR
29 }
30}
31
32pub struct TelegramBot {
62 token: String,
63 builder: BotBuilder,
64}
65
66impl TelegramBot {
67 pub fn new(token: impl Into<String>) -> Self {
69 Self {
70 token: token.into(),
71 builder: BotBuilder::new(),
72 }
73 }
74
75 pub fn command<H, Args>(mut self, name: impl Into<String>, handler: H) -> Self
77 where
78 H: IntoHandler<Args>,
79 {
80 self.builder = self.builder.command(name, handler);
81 self
82 }
83
84 pub fn command_with_description<H, Args>(
88 mut self,
89 name: impl Into<String>,
90 description: impl Into<String>,
91 handler: H,
92 ) -> Self
93 where
94 H: IntoHandler<Args>,
95 {
96 self.builder = self
97 .builder
98 .command_with_description(name, description, handler);
99 self
100 }
101
102 pub fn button<H, Args>(mut self, pattern: impl Into<String>, handler: H) -> Self
106 where
107 H: IntoHandler<Args>,
108 {
109 self.builder = self.builder.button(pattern, handler);
110 self
111 }
112
113 pub fn message<H, Args>(mut self, handler: H) -> Self
115 where
116 H: IntoHandler<Args>,
117 {
118 self.builder = self.builder.message(handler);
119 self
120 }
121
122 pub fn build(self) -> TelegramWebhook {
124 TelegramWebhook {
125 client: TelegramClient::new(&self.token),
126 builder: Arc::new(self.builder),
127 }
128 }
129
130 pub async fn run_polling(self) -> Result<(), BotError> {
147 let client = TelegramClient::new(&self.token);
148
149 let commands: Vec<BotCommand> = self
150 .builder
151 .commands()
152 .map(|(name, desc)| BotCommand::new(name, if desc.is_empty() { name } else { desc }))
153 .collect();
154
155 if !commands.is_empty()
156 && let Err(e) = client.set_my_commands(&commands).await
157 {
158 warn!("Failed to register commands: {}", e);
159 }
160
161 let builder = Arc::new(self.builder);
162
163 client.delete_webhook().await?;
164
165 let mut offset: Option<i64> = None;
166
167 loop {
168 match client.get_updates(offset, Some(30)).await {
169 Ok(updates) => {
170 for update in updates {
171 offset = Some(update.update_id + 1);
172 if let Err(e) = process_update_sync(&client, &builder, update).await {
173 error!("Error handling update: {}", e);
174 }
175 }
176 }
177 Err(e) => {
178 error!("Error fetching updates: {}", e);
179 }
180 }
181 }
182 }
183}
184
185#[derive(Clone)]
189pub struct TelegramWebhook {
190 client: TelegramClient,
191 builder: Arc<BotBuilder>,
192}
193
194impl TelegramWebhook {
195 pub fn client(&self) -> &TelegramClient {
197 &self.client
198 }
199
200 pub async fn handle(&self, update: Update) -> Result<(), BotError> {
204 self.handle_update(update).await
205 }
206
207 async fn handle_update(&self, update: Update) -> Result<(), BotError> {
209 acknowledge_callback_query(&self.client, &update).await?;
210
211 let (event_type, value) = match &update.kind {
212 UpdateKind::Message(msg) => {
213 let data = TelegramContextData::new(update.clone(), self.client.clone());
214 if let Some(cmd_name) = data.command_name() {
215 ("command", cmd_name.to_string())
216 } else {
217 ("message", msg.text.clone().unwrap_or_default())
218 }
219 }
220 UpdateKind::CallbackQuery(cq) => ("button", cq.data.clone().unwrap_or_default()),
221 _ => return Ok(()),
222 };
223
224 let handler = self.builder.find_handler(event_type, &value);
225
226 if let Some(handler) = handler {
227 let data = TelegramContextData::new(update.clone(), self.client.clone());
228 let ctx = Context::new(data);
229
230 let client = self.client.clone();
231 spawn(async move {
232 let response = handler.call(ctx).await;
233 if let Err(e) = send_response(&client, &update, response).await {
234 error!("Telegram response error: {}", e);
235 }
236 })
237 .detach();
238 }
239
240 Ok(())
241 }
242}
243
244async fn process_update_sync(
245 client: &TelegramClient,
246 builder: &BotBuilder,
247 update: Update,
248) -> Result<(), BotError> {
249 acknowledge_callback_query(client, &update).await?;
250
251 let (event_type, value) = match &update.kind {
252 UpdateKind::Message(msg) => {
253 let data = TelegramContextData::new(update.clone(), client.clone());
254 if let Some(cmd_name) = data.command_name() {
255 ("command", cmd_name.to_string())
256 } else {
257 ("message", msg.text.clone().unwrap_or_default())
258 }
259 }
260 UpdateKind::CallbackQuery(cq) => ("button", cq.data.clone().unwrap_or_default()),
261 _ => return Ok(()),
262 };
263
264 if let Some(handler) = builder.find_handler(event_type, &value) {
265 let data = TelegramContextData::new(update.clone(), client.clone());
266 let ctx = Context::new(data);
267 let response = handler.call(ctx).await;
268 send_response(client, &update, response).await?;
269 }
270
271 Ok(())
272}
273
274async fn acknowledge_callback_query(
275 client: &TelegramClient,
276 update: &Update,
277) -> Result<(), BotError> {
278 if let UpdateKind::CallbackQuery(callback_query) = &update.kind {
279 client
280 .answer_callback_query(&callback_query.id, None, false)
281 .await?;
282 }
283
284 Ok(())
285}
286
287async fn send_response(
288 client: &TelegramClient,
289 update: &Update,
290 mut response: Response,
291) -> Result<(), BotError> {
292 if response.is_empty() || response.is_acknowledge() {
293 return Ok(());
294 }
295
296 let chat_id = match &update.kind {
297 UpdateKind::Message(m) | UpdateKind::EditedMessage(m) => m.chat.id,
298 UpdateKind::CallbackQuery(cq) => cq.message.as_ref().map(|m| m.chat.id).unwrap_or(0),
299 _ => return Ok(()),
300 };
301
302 if chat_id == 0 {
303 return Ok(());
304 }
305
306 if response.is_file()
307 && let Some(file_response) = response.take_file()
308 {
309 let _ = client.send_chat_action(chat_id, "upload_document").await;
310
311 return client
312 .send_document(
313 chat_id,
314 file_response.file,
315 file_response.filename.as_deref(),
316 file_response.caption.as_deref(),
317 )
318 .await;
319 }
320
321 let content = response.content().unwrap_or("");
322 if content.is_empty() {
323 return Ok(());
324 }
325
326 let reply_markup = build_reply_markup(&response);
327
328 client.send_message(chat_id, content, reply_markup).await
329}
330
331fn build_reply_markup(response: &Response) -> Option<ReplyMarkup> {
332 use botkit_core::types::component::Component;
333
334 let components = response.components();
335 if components.is_empty() {
336 return None;
337 }
338
339 let mut rows: Vec<Vec<InlineKeyboardButton>> = Vec::new();
340
341 for component in components {
342 match component {
343 Component::ActionRow(action_row) => {
344 let row: Vec<InlineKeyboardButton> = action_row
345 .components
346 .iter()
347 .filter_map(|c| match c {
348 Component::Button(btn) => {
349 if let Some(url) = &btn.url {
350 Some(InlineKeyboardButton::url(&btn.label, url))
351 } else {
352 btn.custom_id.as_ref().map(|custom_id| {
353 InlineKeyboardButton::callback(&btn.label, custom_id)
354 })
355 }
356 }
357 _ => None,
358 })
359 .collect();
360
361 if !row.is_empty() {
362 rows.push(row);
363 }
364 }
365 Component::Button(btn) => {
366 let button = if let Some(url) = &btn.url {
367 InlineKeyboardButton::url(&btn.label, url)
368 } else if let Some(custom_id) = &btn.custom_id {
369 InlineKeyboardButton::callback(&btn.label, custom_id)
370 } else {
371 continue;
372 };
373 rows.push(vec![button]);
374 }
375 _ => {}
376 }
377 }
378
379 if rows.is_empty() {
380 None
381 } else {
382 Some(ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup {
383 inline_keyboard: rows,
384 }))
385 }
386}
387
388impl Endpoint for TelegramWebhook {
389 type Error = WebhookError;
390
391 async fn respond(&mut self, request: &mut Request) -> Result<HttpResponse, Self::Error> {
392 let update: Update = request
393 .body_mut()
394 .into_json()
395 .await
396 .map_err(|e| WebhookError(BotError::Other(e.to_string())))?;
397
398 self.handle(update).await.map_err(WebhookError)?;
399
400 Ok(HttpResponse::new(Body::from_bytes("OK")))
401 }
402}