migration to JS engine #1

Merged
akulij merged 131 commits from dev into main 2025-05-31 08:49:52 +00:00
2 changed files with 351 additions and 340 deletions
Showing only changes of commit d5dbaa0b75 - Show all commits

View File

@ -2,6 +2,7 @@ pub mod admin;
pub mod botscript; pub mod botscript;
pub mod commands; pub mod commands;
pub mod db; pub mod db;
pub mod message_answerer;
pub mod mongodb_storage; pub mod mongodb_storage;
pub mod utils; pub mod utils;
@ -12,6 +13,7 @@ use db::callback_info::CallbackInfo;
use db::message_forward::MessageForward; use db::message_forward::MessageForward;
use itertools::Itertools; use itertools::Itertools;
use log::{error, info, warn}; use log::{error, info, warn};
use message_answerer::MessageAnswerer;
use std::str::FromStr; use std::str::FromStr;
use std::sync::RwLock; use std::sync::RwLock;
use std::time::Duration; use std::time::Duration;
@ -283,7 +285,8 @@ async fn botscript_command_handler(
}); });
let literal = bm.literal().map_or("", |s| s.as_str()); let literal = bm.literal().map_or("", |s| s.as_str());
answer_message_varianted(&bot, msg.chat.id.0, &mut db, literal, None, buttons).await?; let ma = MessageAnswerer::new(&bot, &mut db, msg.chat.id.0);
ma.answer(literal, None, buttons).await?;
Ok(()) Ok(())
} }
@ -486,22 +489,18 @@ async fn callback_handler(bot: Bot, mut db: DB, q: CallbackQuery) -> BotResult<(
create_callback_button("go_home", Callback::GoHome, &mut db).await? create_callback_button("go_home", Callback::GoHome, &mut db).await?
)); ));
replace_message( let chat_id = q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64);
&bot, let message_id = q.message.map_or_else(
&mut db, || {
q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64), Err(BotError::MsgTooOld(
q.message.map_or_else( "Failed to get message id, probably message too old".to_string(),
|| { ))
Err(BotError::MsgTooOld( },
"Failed to get message id, probably message too old".to_string(), |m| Ok(m.id().0),
)) )?;
}, MessageAnswerer::new(&bot, &mut db, chat_id)
|m| Ok(m.id().0), .replace_message(message_id, "more_info_msg", keyboard)
)?, .await?
"more_info_msg",
keyboard,
)
.await?
} }
Callback::ProjectPage { id } => { Callback::ProjectPage { id } => {
let nextproject = match db let nextproject = match db
@ -538,68 +537,50 @@ async fn callback_handler(bot: Bot, mut db: DB, q: CallbackQuery) -> BotResult<(
[create_callback_button("go_home", Callback::GoHome, &mut db).await?] [create_callback_button("go_home", Callback::GoHome, &mut db).await?]
); );
replace_message( let chat_id = q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64);
&bot, let message_id = q.message.map_or_else(
&mut db, || {
q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64), Err(BotError::MsgTooOld(
q.message.map_or_else( "Failed to get message id, probably message too old".to_string(),
|| { ))
Err(BotError::MsgTooOld( },
"Failed to get message id, probably message too old".to_string(), |m| Ok(m.id().0),
)) )?;
}, MessageAnswerer::new(&bot, &mut db, chat_id)
|m| Ok(m.id().0), .replace_message(message_id, &format!("project_{}_msg", id), Some(keyboard))
)?, .await?
&format!("project_{}_msg", id),
Some(keyboard),
)
.await?
} }
Callback::GoHome => { Callback::GoHome => {
let keyboard = make_start_buttons(&mut db).await?; let keyboard = make_start_buttons(&mut db).await?;
replace_message( let chat_id = q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64);
&bot, let message_id = q.message.map_or_else(
&mut db, || {
q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64), Err(BotError::MsgTooOld(
q.message.map_or_else( "Failed to get message id, probably message too old".to_string(),
|| { ))
Err(BotError::MsgTooOld( },
"Failed to get message id, probably message too old".to_string(), |m| Ok(m.id().0),
)) )?;
}, MessageAnswerer::new(&bot, &mut db, chat_id)
|m| Ok(m.id().0), .replace_message(message_id, "start", Some(keyboard))
)?, .await?
"start",
Some(keyboard),
)
.await?
} }
Callback::LeaveApplication => { Callback::LeaveApplication => {
let application = Application::new(q.from.clone()).store(&mut db).await?; let application = Application::new(q.from.clone()).store(&mut db).await?;
let msg = send_application_to_chat(&bot, &mut db, &application).await?; let msg = send_application_to_chat(&bot, &mut db, &application).await?;
let (chat_id, msg_id) = answer_message( let (chat_id, msg_id) = MessageAnswerer::new(&bot, &mut db, q.from.id.0 as i64)
&bot, .answer("left_application_msg", None, None)
q.from.id.0 as i64, .await?;
&mut db,
"left_application_msg",
None as Option<InlineKeyboardMarkup>,
)
.await?;
MessageForward::new(msg.chat.id.0, msg.id.0, chat_id, msg_id, false) MessageForward::new(msg.chat.id.0, msg.id.0, chat_id, msg_id, false)
.store(&mut db) .store(&mut db)
.await?; .await?;
} }
Callback::AskQuestion => { Callback::AskQuestion => {
answer_message( MessageAnswerer::new(&bot, &mut db, q.from.id.0 as i64)
&bot, .answer("ask_question_msg", None, None)
q.from.id.0 as i64, .await?;
&mut db,
"ask_question_msg",
None as Option<InlineKeyboardMarkup>,
)
.await?;
} }
}; };
@ -941,15 +922,9 @@ async fn user_command_handler(
variant => Some(variant), variant => Some(variant),
}; };
let mut db2 = db.clone(); let mut db2 = db.clone();
answer_message_varianted( MessageAnswerer::new(&bot, &mut db, msg.chat.id.0)
&bot, .answer("start", variant, Some(make_start_buttons(&mut db2).await?))
msg.chat.id.0, .await?;
&mut db,
"start",
variant,
Some(make_start_buttons(&mut db2).await?),
)
.await?;
Ok(()) Ok(())
} }
UserCommands::Help => { UserCommands::Help => {
@ -960,272 +935,6 @@ async fn user_command_handler(
} }
} }
async fn answer_message<RM: Into<ReplyMarkup>>(
bot: &Bot,
chat_id: i64,
db: &mut DB,
literal: &str,
keyboard: Option<RM>,
) -> BotResult<(i64, i32)> {
answer_message_varianted(bot, chat_id, db, literal, None, keyboard).await
}
async fn answer_message_varianted<RM: Into<ReplyMarkup>>(
bot: &Bot,
chat_id: i64,
db: &mut DB,
literal: &str,
variant: Option<&str>,
keyboard: Option<RM>,
) -> BotResult<(i64, i32)> {
answer_message_varianted_silence_flag(bot, chat_id, db, literal, variant, false, keyboard).await
}
async fn answer_message_varianted_silence_flag<RM: Into<ReplyMarkup>>(
bot: &Bot,
chat_id: i64,
db: &mut DB,
literal: &str,
variant: Option<&str>,
silence_non_variant: bool,
keyboard: Option<RM>,
) -> BotResult<(i64, i32)> {
let variant_text = match variant {
Some(variant) => {
let value = db.get_literal_alternative_value(literal, variant).await?;
if value.is_none() && !silence_non_variant {
notify_admin(&format!("variant {variant} for literal {literal} is not found! falling back to just literal")).await;
}
value
}
None => None,
};
let text = match variant_text {
Some(text) => text,
None => db
.get_literal_value(literal)
.await?
.unwrap_or("Please, set content of this message".into()),
};
let media = db.get_media(literal).await?;
let (chat_id, msg_id) = match media.len() {
// just a text
0 => {
let msg = bot.send_message(ChatId(chat_id), text);
let msg = match keyboard {
Some(kbd) => msg.reply_markup(kbd),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
info!("ENTS: {:?}", msg.entities);
let msg = msg.await?;
(msg.chat.id.0, msg.id.0)
}
// single media
1 => {
let media = &media[0]; // safe, cause we just checked len
match media.media_type.as_str() {
"photo" => {
let msg = bot.send_photo(
ChatId(chat_id),
InputFile::file_id(media.file_id.to_string()),
);
let msg = match text.as_str() {
"" => msg,
text => msg.caption(text),
};
let msg = match keyboard {
Some(kbd) => msg.reply_markup(kbd),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
let msg = msg.await?;
(msg.chat.id.0, msg.id.0)
}
"video" => {
let msg = bot.send_video(
ChatId(chat_id),
InputFile::file_id(media.file_id.to_string()),
);
let msg = match text.as_str() {
"" => msg,
text => msg.caption(text),
};
let msg = match keyboard {
Some(kbd) => msg.reply_markup(kbd),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
let msg = msg.await?;
(msg.chat.id.0, msg.id.0)
}
_ => {
todo!()
}
}
}
// >= 2, should use media group
_ => {
let media: Vec<InputMedia> = media
.into_iter()
.enumerate()
.map(|(i, m)| {
let ifile = InputFile::file_id(m.file_id);
let caption = if i == 0 {
match text.as_str() {
"" => None,
text => Some(text.to_string()),
}
} else {
None
};
match m.media_type.as_str() {
"photo" => InputMedia::Photo(teloxide::types::InputMediaPhoto {
media: ifile,
caption,
parse_mode: Some(ParseMode::Html),
caption_entities: None,
has_spoiler: false,
show_caption_above_media: false,
}),
"video" => InputMedia::Video(teloxide::types::InputMediaVideo {
media: ifile,
thumbnail: None,
caption,
parse_mode: Some(ParseMode::Html),
caption_entities: None,
show_caption_above_media: false,
width: None,
height: None,
duration: None,
supports_streaming: None,
has_spoiler: false,
}),
_ => {
todo!()
}
}
})
.collect();
let msg = bot.send_media_group(ChatId(chat_id), media);
let msg = msg.await?;
(msg[0].chat.id.0, msg[0].id.0)
}
};
match variant {
Some(variant) => {
db.set_message_literal_variant(chat_id, msg_id, literal, variant)
.await?
}
None => db.set_message_literal(chat_id, msg_id, literal).await?,
};
Ok((chat_id, msg_id))
}
async fn replace_message(
bot: &Bot,
db: &mut DB,
chat_id: i64,
message_id: i32,
literal: &str,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<()> {
let variant = db
.get_message(chat_id, message_id)
.await?
.and_then(|m| m.variant);
let variant_text = match variant {
Some(ref variant) => db.get_literal_alternative_value(literal, variant).await?,
None => None,
};
let text = match variant_text {
Some(ref text) => text.to_string(),
None => db
.get_literal_value(literal)
.await?
.unwrap_or("Please, set content of this message".into()),
};
let media = db.get_media(literal).await?;
let (chat_id, msg_id) = match media.len() {
// just a text
0 => {
let msg = bot.edit_message_text(ChatId(chat_id), MessageId(message_id), text);
let msg = match keyboard {
Some(ref kbd) => msg.reply_markup(kbd.clone()),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
info!("ENTS: {:?}", msg.entities);
let msg = match msg.await {
Ok(msg) => msg,
Err(teloxide::RequestError::Api(teloxide::ApiError::Unknown(errtext)))
if errtext.as_str()
== "Bad Request: there is no text in the message to edit" =>
{
// fallback to sending message
warn!("Fallback into sending message instead of editing because it contains media");
answer_message_varianted_silence_flag(
bot,
chat_id,
db,
literal,
variant.as_deref(),
true,
keyboard,
)
.await?;
return Ok(());
}
Err(err) => return Err(err.into()),
};
(msg.chat.id.0, msg.id.0)
}
// single media
1 => {
let media = &media[0]; // safe, cause we just checked len
let input_file = InputFile::file_id(media.file_id.to_string());
let media = match media.media_type.as_str() {
"photo" => InputMedia::Photo(teloxide::types::InputMediaPhoto::new(input_file)),
"video" => InputMedia::Video(teloxide::types::InputMediaVideo::new(input_file)),
_ => todo!(),
};
bot.edit_message_media(ChatId(chat_id), MessageId(message_id), media)
.await?;
let msg = bot.edit_message_caption(ChatId(chat_id), MessageId(message_id));
let msg = match text.as_str() {
"" => msg,
text => msg.caption(text),
};
let msg = match keyboard {
Some(kbd) => msg.reply_markup(kbd),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
let msg = msg.await?;
(msg.chat.id.0, msg.id.0)
}
// >= 2, should use media group
_ => {
unreachable!();
}
};
db.set_message_literal(chat_id, msg_id, literal).await?;
Ok(())
}
async fn make_start_buttons(db: &mut DB) -> BotResult<InlineKeyboardMarkup> { async fn make_start_buttons(db: &mut DB) -> BotResult<InlineKeyboardMarkup> {
let mut buttons: Vec<Vec<InlineKeyboardButton>> = Vec::new(); let mut buttons: Vec<Vec<InlineKeyboardButton>> = Vec::new();
buttons.push(vec![ buttons.push(vec![

302
src/message_answerer.rs Normal file
View File

@ -0,0 +1,302 @@
use log::{info, warn};
use teloxide::prelude::*;
use teloxide::types::{
InputFile, InputMedia, InputMediaPhoto, InputMediaVideo, MessageId, ParseMode,
};
use teloxide::{
types::{ChatId, InlineKeyboardMarkup},
Bot,
};
use crate::db::Media;
use crate::{
db::{CallDB, DB},
notify_admin, BotResult,
};
macro_rules! send_media {
($self:ident, $method:ident, $chat_id:expr, $file_id: expr, $text: expr, $keyboard: expr) => {{
let msg = $self
.bot
.$method(ChatId($chat_id), InputFile::file_id($file_id.to_string()));
let msg = match $text.as_str() {
"" => msg,
text => msg.caption(text),
};
let msg = match $keyboard {
Some(kbd) => msg.reply_markup(kbd),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
let msg = msg.await?;
Ok((msg.chat.id.0, msg.id.0))
}};
}
pub struct MessageAnswerer<'a> {
bot: &'a Bot,
chat_id: i64,
db: &'a mut DB,
}
impl<'a> MessageAnswerer<'a> {
pub fn new(bot: &'a Bot, db: &'a mut DB, chat_id: i64) -> Self {
Self { bot, chat_id, db }
}
async fn get_text(
&mut self,
literal: &str,
variant: Option<&str>,
is_replace: bool,
) -> BotResult<String> {
let variant_text = match variant {
Some(variant) => {
let value = self
.db
.get_literal_alternative_value(literal, variant)
.await?;
if value.is_none() && !is_replace {
notify_admin(&format!("variant {variant} for literal {literal} is not found! falling back to just literal")).await;
}
value
}
None => None,
};
let text = match variant_text {
Some(text) => text,
None => self
.db
.get_literal_value(literal)
.await?
.unwrap_or("Please, set content of this message".into()),
};
Ok(text)
}
pub async fn answer(
mut self,
literal: &str,
variant: Option<&str>,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> {
let text = self.get_text(literal, variant, false).await?;
self.answer_inner(text, literal, variant, keyboard).await
}
async fn answer_inner(
mut self,
text: String,
literal: &str,
variant: Option<&str>,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> {
let media = self.db.get_media(literal).await?;
let (chat_id, msg_id) = match media.len() {
// just a text
0 => self.send_message(text, keyboard).await?,
// single media
1 => self.send_media(&media[0], text, keyboard).await?,
// >= 2, should use media group
_ => self.send_media_group(media, text).await?,
};
self.store_message_info(msg_id, literal, variant).await?;
Ok((chat_id, msg_id))
}
pub async fn replace_message(
mut self,
message_id: i32,
literal: &str,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<()> {
let variant = self
.db
.get_message(self.chat_id, message_id)
.await?
.and_then(|m| m.variant);
let text = self.get_text(literal, variant.as_deref(), true).await?;
let media = self.db.get_media(literal).await?;
let (chat_id, msg_id) = match media.len() {
// just a text
0 => {
let msg =
self.bot
.edit_message_text(ChatId(self.chat_id), MessageId(message_id), &text);
let msg = match keyboard {
Some(ref kbd) => msg.reply_markup(kbd.clone()),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
info!("ENTS: {:?}", msg.entities);
let msg = match msg.await {
Ok(msg) => msg,
Err(teloxide::RequestError::Api(teloxide::ApiError::Unknown(errtext)))
if errtext.as_str()
== "Bad Request: there is no text in the message to edit" =>
{
// fallback to sending message
warn!("Fallback into sending message instead of editing because it contains media");
self.answer_inner(text, literal, variant.as_deref(), keyboard)
.await?;
return Ok(());
}
Err(err) => return Err(err.into()),
};
(msg.chat.id.0, msg.id.0)
}
// single media
1 => {
let media = &media[0]; // safe, cause we just checked len
let input_file = InputFile::file_id(media.file_id.to_string());
let media = match media.media_type.as_str() {
"photo" => InputMedia::Photo(teloxide::types::InputMediaPhoto::new(input_file)),
"video" => InputMedia::Video(teloxide::types::InputMediaVideo::new(input_file)),
_ => todo!(),
};
self.bot
.edit_message_media(ChatId(self.chat_id), MessageId(message_id), media)
.await?;
let msg = self
.bot
.edit_message_caption(ChatId(self.chat_id), MessageId(message_id));
let msg = match text.as_str() {
"" => msg,
text => msg.caption(text),
};
let msg = match keyboard {
Some(kbd) => msg.reply_markup(kbd),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
let msg = msg.await?;
(msg.chat.id.0, msg.id.0)
}
// >= 2, should use media group
_ => {
todo!();
}
};
self.store_message_info(msg_id, literal, variant.as_deref())
.await?;
Ok(())
}
async fn store_message_info(
&mut self,
message_id: i32,
literal: &str,
variant: Option<&str>,
) -> BotResult<()> {
match variant {
Some(variant) => {
self.db
.set_message_literal_variant(self.chat_id, message_id, literal, variant)
.await?
}
None => {
self.db
.set_message_literal(self.chat_id, message_id, literal)
.await?
}
};
Ok(())
}
async fn send_message(
&self,
text: String,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> {
let msg = self.bot.send_message(ChatId(self.chat_id), text);
let msg = match keyboard {
Some(kbd) => msg.reply_markup(kbd),
None => msg,
};
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
info!("ENTS: {:?}", msg.entities);
let msg = msg.await?;
Ok((msg.chat.id.0, msg.id.0))
}
async fn send_media(
&self,
media: &Media,
text: String,
keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> {
match media.media_type.as_str() {
"photo" => {
send_media!(
self,
send_photo,
self.chat_id,
media.file_id,
text,
keyboard
)
}
"video" => {
send_media!(
self,
send_video,
self.chat_id,
media.file_id,
text,
keyboard
)
}
_ => {
todo!()
}
}
}
async fn send_media_group(&self, media: Vec<Media>, text: String) -> BotResult<(i64, i32)> {
let media: Vec<InputMedia> = media
.into_iter()
.enumerate()
.map(|(i, m)| {
let ifile = InputFile::file_id(m.file_id);
let caption = if i == 0 {
match text.as_str() {
"" => None,
text => Some(text.to_string()),
}
} else {
None
};
match m.media_type.as_str() {
"photo" => InputMedia::Photo(InputMediaPhoto {
caption,
parse_mode: Some(ParseMode::Html),
..InputMediaPhoto::new(ifile)
}),
"video" => InputMedia::Video(InputMediaVideo {
caption,
parse_mode: Some(ParseMode::Html),
..InputMediaVideo::new(ifile)
}),
_ => {
todo!()
}
}
})
.collect();
let msg = self.bot.send_media_group(ChatId(self.chat_id), media);
let msg = msg.await?;
Ok((msg[0].chat.id.0, msg[0].id.0))
}
}