#![deny(missing_docs)]
#![warn(clippy::all, clippy::nursery, clippy::pedantic, clippy::cargo)]
#![allow(clippy::multiple_crate_versions, reason = "Dependency")]
#![allow(clippy::future_not_send, reason = "Single-threaded runtime")]
mod commands;
mod dox_impl;
mod doxee_resolution;
mod guest;
mod inline;
mod messages;
mod non_command;
mod setup;
use anyhow::{Result, bail};
pub use commands::{Command, Commands};
pub use dox_impl::{DoxReport, SubjectId};
pub use doxee_resolution::{DoxArg, DoxeeSource};
use frakti::{
AsyncTelegramApi, BASE_API_URL, ParseMode,
client_cyper::Bot,
cyper::{Client, proxy::Proxy},
inline_mode::{InlineQuery, InlineQueryResult},
methods::{
AnswerGuestQueryParams, AnswerInlineQueryParams, GetUpdatesParams, SendMessageParams,
},
types::{Message, ReplyParameters},
updates::UpdateContent,
};
use log::{error, info, trace};
pub use messages::BotError;
use non_command::handle_non_command;
use serde::Deserialize;
use setup::{setup_commands, setup_rights};
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub token: String,
pub proxy: Option<String>,
}
pub async fn run(config: Config) -> Result<()> {
let token = config.token;
let api_url = format!("{BASE_API_URL}{token}");
let mut builder = Client::builder();
if let Some(proxy) = config.proxy {
let proxy = Proxy::all(proxy)?;
builder = builder.proxy(proxy);
}
let client = builder.build()?;
let bot = Bot { api_url, client };
setup_commands(&bot).await?;
setup_rights(&bot).await?;
let me = bot.get_me().await?.result;
let Some(username) = me.username else {
bail!("Cannot get username of the bot");
};
info!("Bot started: @{username}");
let mut update_params = GetUpdatesParams::builder().build();
loop {
match bot.get_updates(&update_params).await {
Ok(updates) => {
let Some(last) = updates.result.last() else {
continue;
};
update_params.offset.replace((last.update_id + 1).into());
for update in updates.result {
trace!("Received update: {update:?}");
match update.content {
UpdateContent::Message(msg) => {
let bot = bot.clone();
let username = username.clone();
compio::runtime::spawn(Box::pin(handle_message(bot, *msg, username)))
.detach();
}
UpdateContent::GuestMessage(msg) => {
let bot = bot.clone();
let username = username.clone();
compio::runtime::spawn(Box::pin(handle_guest(bot, *msg, username)))
.detach();
}
UpdateContent::InlineQuery(inline) => {
let bot = bot.clone();
compio::runtime::spawn(Box::pin(handle_inline(bot, inline))).detach();
}
_ => {}
}
}
}
Err(err) => {
error!("Error getting updates: {err}");
}
}
}
}
async fn handle_message(bot: Bot, msg: Message, username: String) {
let parsed = Commands::parse(msg.text.as_ref(), &username);
let chat_id = msg.chat.id;
let message_id = msg.message_id;
let reply = match parsed {
Some(command) => Some(Box::pin(command.execute(&bot, msg, &username)).await),
None => Box::pin(handle_non_command(&bot, msg)).await,
};
if let Some(reply) = reply {
info!("Reply: {reply}");
let reply_param = ReplyParameters::builder().message_id(message_id).build();
let send_message_param = SendMessageParams::builder()
.chat_id(chat_id)
.text(reply)
.reply_parameters(reply_param)
.parse_mode(ParseMode::Html)
.build();
_ = bot
.send_message(&send_message_param)
.await
.inspect_err(|e| error!("Failed to send message: {e}"));
}
}
async fn handle_guest(bot: Bot, msg: Message, username: String) {
let Some(guest_id) = msg.guest_query_id.clone() else {
error!("Guest message without guest_query_id: {msg:?}");
return;
};
let Some(article) = Box::pin(guest::handle_guest_message(&bot, msg, &username)).await else {
return;
};
info!("Answer: {:?}", article.input_message_content);
let result = InlineQueryResult::Article(article);
let answer_param = AnswerGuestQueryParams::builder()
.guest_query_id(guest_id)
.result(result)
.build();
_ = bot
.answer_guest_query(&answer_param)
.await
.inspect_err(|e| error!("Failed to answer guest query: {e}"));
}
async fn handle_inline(bot: Bot, inline: InlineQuery) {
let inline_id = inline.id.clone();
let article = Box::pin(inline::handle_inline_query(&bot, inline)).await;
info!("Answer: {:?}", article.input_message_content);
let result = InlineQueryResult::Article(article);
let answer_param = AnswerInlineQueryParams::builder()
.inline_query_id(inline_id)
.results(vec![result])
.button(inline::help_button())
.cache_time(60)
.build();
_ = bot
.answer_inline_query(&answer_param)
.await
.inspect_err(|e| error!("Failed to answer inline query: {e}"));
}