use std::sync::Arc;
use botkit_core::{BotBuilder, BotError, Context, ContextData, IntoHandler, Response};
use executor_core::spawn;
use http_kit::{Body, Endpoint, HttpError, Request, Response as HttpResponse, StatusCode};
use tracing::{error, warn};
use crate::client::TelegramClient;
use crate::event::TelegramContextData;
use crate::types::{
BotCommand, InlineKeyboardButton, InlineKeyboardMarkup, ReplyMarkup, Update, UpdateKind,
};
#[derive(Debug)]
pub struct WebhookError(BotError);
impl std::fmt::Display for WebhookError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for WebhookError {}
impl HttpError for WebhookError {
fn status(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
}
pub struct TelegramBot {
token: String,
builder: BotBuilder,
}
impl TelegramBot {
pub fn new(token: impl Into<String>) -> Self {
Self {
token: token.into(),
builder: BotBuilder::new(),
}
}
pub fn command<H, Args>(mut self, name: impl Into<String>, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.builder = self.builder.command(name, handler);
self
}
pub fn command_with_description<H, Args>(
mut self,
name: impl Into<String>,
description: impl Into<String>,
handler: H,
) -> Self
where
H: IntoHandler<Args>,
{
self.builder = self
.builder
.command_with_description(name, description, handler);
self
}
pub fn button<H, Args>(mut self, pattern: impl Into<String>, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.builder = self.builder.button(pattern, handler);
self
}
pub fn message<H, Args>(mut self, handler: H) -> Self
where
H: IntoHandler<Args>,
{
self.builder = self.builder.message(handler);
self
}
pub fn build(self) -> TelegramWebhook {
TelegramWebhook {
client: TelegramClient::new(&self.token),
builder: Arc::new(self.builder),
}
}
pub async fn run_polling(self) -> Result<(), BotError> {
let client = TelegramClient::new(&self.token);
let commands: Vec<BotCommand> = self
.builder
.commands()
.map(|(name, desc)| BotCommand::new(name, if desc.is_empty() { name } else { desc }))
.collect();
if !commands.is_empty()
&& let Err(e) = client.set_my_commands(&commands).await
{
warn!("Failed to register commands: {}", e);
}
let builder = Arc::new(self.builder);
client.delete_webhook().await?;
let mut offset: Option<i64> = None;
loop {
match client.get_updates(offset, Some(30)).await {
Ok(updates) => {
for update in updates {
offset = Some(update.update_id + 1);
if let Err(e) = process_update_sync(&client, &builder, update).await {
error!("Error handling update: {}", e);
}
}
}
Err(e) => {
error!("Error fetching updates: {}", e);
}
}
}
}
}
#[derive(Clone)]
pub struct TelegramWebhook {
client: TelegramClient,
builder: Arc<BotBuilder>,
}
impl TelegramWebhook {
pub fn client(&self) -> &TelegramClient {
&self.client
}
pub async fn handle(&self, update: Update) -> Result<(), BotError> {
self.handle_update(update).await
}
async fn handle_update(&self, update: Update) -> Result<(), BotError> {
acknowledge_callback_query(&self.client, &update).await?;
let (event_type, value) = match &update.kind {
UpdateKind::Message(msg) => {
let data = TelegramContextData::new(update.clone(), self.client.clone());
if let Some(cmd_name) = data.command_name() {
("command", cmd_name.to_string())
} else {
("message", msg.text.clone().unwrap_or_default())
}
}
UpdateKind::CallbackQuery(cq) => ("button", cq.data.clone().unwrap_or_default()),
_ => return Ok(()),
};
let handler = self.builder.find_handler(event_type, &value);
if let Some(handler) = handler {
let data = TelegramContextData::new(update.clone(), self.client.clone());
let ctx = Context::new(data);
let client = self.client.clone();
spawn(async move {
let response = handler.call(ctx).await;
if let Err(e) = send_response(&client, &update, response).await {
error!("Telegram response error: {}", e);
}
})
.detach();
}
Ok(())
}
}
async fn process_update_sync(
client: &TelegramClient,
builder: &BotBuilder,
update: Update,
) -> Result<(), BotError> {
acknowledge_callback_query(client, &update).await?;
let (event_type, value) = match &update.kind {
UpdateKind::Message(msg) => {
let data = TelegramContextData::new(update.clone(), client.clone());
if let Some(cmd_name) = data.command_name() {
("command", cmd_name.to_string())
} else {
("message", msg.text.clone().unwrap_or_default())
}
}
UpdateKind::CallbackQuery(cq) => ("button", cq.data.clone().unwrap_or_default()),
_ => return Ok(()),
};
if let Some(handler) = builder.find_handler(event_type, &value) {
let data = TelegramContextData::new(update.clone(), client.clone());
let ctx = Context::new(data);
let response = handler.call(ctx).await;
send_response(client, &update, response).await?;
}
Ok(())
}
async fn acknowledge_callback_query(
client: &TelegramClient,
update: &Update,
) -> Result<(), BotError> {
if let UpdateKind::CallbackQuery(callback_query) = &update.kind {
client
.answer_callback_query(&callback_query.id, None, false)
.await?;
}
Ok(())
}
async fn send_response(
client: &TelegramClient,
update: &Update,
mut response: Response,
) -> Result<(), BotError> {
if response.is_empty() || response.is_acknowledge() {
return Ok(());
}
let chat_id = match &update.kind {
UpdateKind::Message(m) | UpdateKind::EditedMessage(m) => m.chat.id,
UpdateKind::CallbackQuery(cq) => cq.message.as_ref().map(|m| m.chat.id).unwrap_or(0),
_ => return Ok(()),
};
if chat_id == 0 {
return Ok(());
}
if response.is_file()
&& let Some(file_response) = response.take_file()
{
let _ = client.send_chat_action(chat_id, "upload_document").await;
return client
.send_document(
chat_id,
file_response.file,
file_response.filename.as_deref(),
file_response.caption.as_deref(),
)
.await;
}
let content = response.content().unwrap_or("");
if content.is_empty() {
return Ok(());
}
let reply_markup = build_reply_markup(&response);
client.send_message(chat_id, content, reply_markup).await
}
fn build_reply_markup(response: &Response) -> Option<ReplyMarkup> {
use botkit_core::types::component::Component;
let components = response.components();
if components.is_empty() {
return None;
}
let mut rows: Vec<Vec<InlineKeyboardButton>> = Vec::new();
for component in components {
match component {
Component::ActionRow(action_row) => {
let row: Vec<InlineKeyboardButton> = action_row
.components
.iter()
.filter_map(|c| match c {
Component::Button(btn) => {
if let Some(url) = &btn.url {
Some(InlineKeyboardButton::url(&btn.label, url))
} else {
btn.custom_id.as_ref().map(|custom_id| {
InlineKeyboardButton::callback(&btn.label, custom_id)
})
}
}
_ => None,
})
.collect();
if !row.is_empty() {
rows.push(row);
}
}
Component::Button(btn) => {
let button = if let Some(url) = &btn.url {
InlineKeyboardButton::url(&btn.label, url)
} else if let Some(custom_id) = &btn.custom_id {
InlineKeyboardButton::callback(&btn.label, custom_id)
} else {
continue;
};
rows.push(vec![button]);
}
_ => {}
}
}
if rows.is_empty() {
None
} else {
Some(ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup {
inline_keyboard: rows,
}))
}
}
impl Endpoint for TelegramWebhook {
type Error = WebhookError;
async fn respond(&mut self, request: &mut Request) -> Result<HttpResponse, Self::Error> {
let update: Update = request
.body_mut()
.into_json()
.await
.map_err(|e| WebhookError(BotError::Other(e.to_string())))?;
self.handle(update).await.map_err(WebhookError)?;
Ok(HttpResponse::new(Body::from_bytes("OK")))
}
}