Compare commits
No commits in common. "bee93b32d1f2248fb267fad39c070fe736213871" and "4548419946daabe6f866d3bbd1f0302d16c2d6bd" have entirely different histories.
bee93b32d1
...
4548419946
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -916,13 +916,11 @@ dependencies = [
|
|||||||
"envconfig",
|
"envconfig",
|
||||||
"futures",
|
"futures",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"lazy_static",
|
|
||||||
"log",
|
"log",
|
||||||
"mongodb",
|
"mongodb",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"quickjs-rusty",
|
"quickjs-rusty",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"teloxide",
|
"teloxide",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1438,12 +1436,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy_static"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.171"
|
version = "0.2.171"
|
||||||
|
|||||||
@ -15,13 +15,11 @@ enum_stringify = "0.6.3"
|
|||||||
envconfig = "0.11.0"
|
envconfig = "0.11.0"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
lazy_static = "1.5.0"
|
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
mongodb = "3.2.3"
|
mongodb = "3.2.3"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
quickjs-rusty = "0.9.0"
|
quickjs-rusty = "0.9.0"
|
||||||
serde = { version = "1.0.219", features = ["derive", "serde_derive"] }
|
serde = { version = "1.0.219", features = ["derive", "serde_derive"] }
|
||||||
serde_json = "1.0.140"
|
|
||||||
teloxide = { version = "0.14.0", features = ["macros", "postgres-storage-nativetls"] }
|
teloxide = { version = "0.14.0", features = ["macros", "postgres-storage-nativetls"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.44.1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.44.1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
// db - is set globally
|
|
||||||
|
|
||||||
const dialog = {
|
|
||||||
commands: {
|
|
||||||
start: {
|
|
||||||
buttons: start_buttons, // default is `null`
|
|
||||||
state: "start"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
buttons: {
|
|
||||||
more_info: {},
|
|
||||||
},
|
|
||||||
stateful_msg_handlers: {
|
|
||||||
start: {}, // everything is by default, so just send message `start`
|
|
||||||
enter_name: {
|
|
||||||
// name of the handler function. This field has a side effect:
|
|
||||||
// when is set, no automatic sending of message, should be sent
|
|
||||||
// manually in handler
|
|
||||||
handler: enter_name,
|
|
||||||
state: "none"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function enter_name() {}
|
|
||||||
|
|
||||||
const fmt = (number) => number.toString().padStart(2, '0');
|
|
||||||
|
|
||||||
const formatDate = (date) => {
|
|
||||||
const [h, m, d, M, y] = [
|
|
||||||
date.getHours(),
|
|
||||||
date.getMinutes(),
|
|
||||||
date.getDate(),
|
|
||||||
date.getMonth(),
|
|
||||||
date.getFullYear()
|
|
||||||
];
|
|
||||||
return `${fmt(h)}:${fmt(m)} ${fmt(d)}-${fmt(M + 1)}-${y}`
|
|
||||||
};
|
|
||||||
|
|
||||||
function start_buttons() {
|
|
||||||
const now = new Date();
|
|
||||||
const dateFormated = formatDate(now);
|
|
||||||
|
|
||||||
// const user = db.find_one("users", {id: 1});
|
|
||||||
|
|
||||||
return [
|
|
||||||
// [{name: {name: user.first_name}, callback_name: "no"}],
|
|
||||||
[{name: {name: dateFormated}, callback_name: "no"}],
|
|
||||||
[{name: {name: "Hello!"}, callback_name: "no"}],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
version: 1.1,
|
|
||||||
}
|
|
||||||
|
|
||||||
const c = {config: config, dialog: dialog}
|
|
||||||
c
|
|
||||||
35
src/admin.rs
35
src/admin.rs
@ -5,8 +5,7 @@ use teloxide::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bot_manager::DEFAULT_SCRIPT,
|
db::{CallDB, DB},
|
||||||
db::{bots::BotInstance, CallDB, DB},
|
|
||||||
BotResult,
|
BotResult,
|
||||||
};
|
};
|
||||||
use crate::{BotDialogue, LogMsg, State};
|
use crate::{BotDialogue, LogMsg, State};
|
||||||
@ -42,8 +41,6 @@ pub enum AdminCommands {
|
|||||||
Users,
|
Users,
|
||||||
/// Cancel current action and sets user state to default
|
/// Cancel current action and sets user state to default
|
||||||
Cancel,
|
Cancel,
|
||||||
/// Create new instance of telegram bot
|
|
||||||
Deploy { token: String },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn admin_command_handler(
|
pub async fn admin_command_handler(
|
||||||
@ -159,36 +156,6 @@ pub async fn admin_command_handler(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
AdminCommands::Deploy { token } => {
|
|
||||||
let bot_instance = {
|
|
||||||
let botnew = Bot::new(&token);
|
|
||||||
let name = match botnew.get_me().await {
|
|
||||||
Ok(me) => me.username().to_string(),
|
|
||||||
Err(teloxide::RequestError::Api(teloxide::ApiError::InvalidToken)) => {
|
|
||||||
bot.send_message(msg.chat.id, "Error: bot token is invalid")
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let bi =
|
|
||||||
BotInstance::new(name.clone(), token.to_string(), DEFAULT_SCRIPT.to_string())
|
|
||||||
.store(&mut db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
bi
|
|
||||||
};
|
|
||||||
|
|
||||||
bot.send_message(
|
|
||||||
msg.chat.id,
|
|
||||||
format!("Deployed bot with name: {}", bot_instance.name),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,162 +0,0 @@
|
|||||||
use log::info;
|
|
||||||
use std::{
|
|
||||||
str::FromStr,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
use teloxide::{
|
|
||||||
dispatching::{dialogue::GetChatId, UpdateFilterExt},
|
|
||||||
dptree::{self, Handler},
|
|
||||||
prelude::DependencyMap,
|
|
||||||
types::{CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update},
|
|
||||||
Bot,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
botscript::{self, BotMessage, RunnerConfig},
|
|
||||||
commands::BotCommand,
|
|
||||||
db::{CallDB, DB},
|
|
||||||
message_answerer::MessageAnswerer,
|
|
||||||
update_user_tg, BotError, BotResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub type BotHandler =
|
|
||||||
Handler<'static, DependencyMap, BotResult<()>, teloxide::dispatching::DpHandlerDescription>;
|
|
||||||
|
|
||||||
pub fn script_handler(rc: Arc<RwLock<RunnerConfig>>) -> BotHandler {
|
|
||||||
let crc = rc.clone();
|
|
||||||
dptree::entry()
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
// check if message is command
|
|
||||||
.filter_map(|m: Message| m.text().and_then(|t| BotCommand::from_str(t).ok()))
|
|
||||||
// check if command is presented in config
|
|
||||||
.filter_map(move |bc: BotCommand| {
|
|
||||||
let rc = std::sync::Arc::clone(&rc);
|
|
||||||
let command = bc.command();
|
|
||||||
|
|
||||||
let rc = rc.read().expect("RwLock lock on commands map failed");
|
|
||||||
|
|
||||||
rc.get_command_message(command)
|
|
||||||
})
|
|
||||||
.endpoint(handle_botmessage),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
Update::filter_callback_query()
|
|
||||||
.filter_map(move |q: CallbackQuery| {
|
|
||||||
q.data.and_then(|data| {
|
|
||||||
let rc = std::sync::Arc::clone(&crc);
|
|
||||||
let rc = rc.read().expect("RwLock lock on commands map failed");
|
|
||||||
|
|
||||||
rc.get_callback_message(&data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.endpoint(handle_callback),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_botmessage(bot: Bot, mut db: DB, bm: BotMessage, msg: Message) -> BotResult<()> {
|
|
||||||
info!("Eval BM: {:?}", bm);
|
|
||||||
let tguser = match msg.from.clone() {
|
|
||||||
Some(user) => user,
|
|
||||||
None => return Ok(()), // do nothing, cause its not usecase of function
|
|
||||||
};
|
|
||||||
let user = db
|
|
||||||
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
|
||||||
.await?;
|
|
||||||
let user = update_user_tg(user, &tguser);
|
|
||||||
user.update_user(&mut db).await?;
|
|
||||||
|
|
||||||
let buttons = bm
|
|
||||||
.resolve_buttons(&mut db)
|
|
||||||
.await?
|
|
||||||
.map(|buttons| InlineKeyboardMarkup {
|
|
||||||
inline_keyboard: buttons
|
|
||||||
.iter()
|
|
||||||
.map(|r| {
|
|
||||||
r.iter()
|
|
||||||
.map(|b| match b {
|
|
||||||
botscript::ButtonLayout::Callback {
|
|
||||||
name,
|
|
||||||
literal: _,
|
|
||||||
callback,
|
|
||||||
} => InlineKeyboardButton::callback(name, callback),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
});
|
|
||||||
let literal = bm.literal().map_or("", |s| s.as_str());
|
|
||||||
|
|
||||||
let ma = MessageAnswerer::new(&bot, &mut db, msg.chat.id.0);
|
|
||||||
ma.answer(literal, None, buttons).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery) -> BotResult<()> {
|
|
||||||
info!("Eval BM: {:?}", bm);
|
|
||||||
let tguser = q.from.clone();
|
|
||||||
let user = db
|
|
||||||
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
|
||||||
.await?;
|
|
||||||
let user = update_user_tg(user, &tguser);
|
|
||||||
user.update_user(&mut db).await?;
|
|
||||||
|
|
||||||
let buttons = bm
|
|
||||||
.resolve_buttons(&mut db)
|
|
||||||
.await?
|
|
||||||
.map(|buttons| InlineKeyboardMarkup {
|
|
||||||
inline_keyboard: buttons
|
|
||||||
.iter()
|
|
||||||
.map(|r| {
|
|
||||||
r.iter()
|
|
||||||
.map(|b| match b {
|
|
||||||
botscript::ButtonLayout::Callback {
|
|
||||||
name,
|
|
||||||
literal: _,
|
|
||||||
callback,
|
|
||||||
} => InlineKeyboardButton::callback(name, callback),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
});
|
|
||||||
let literal = bm.literal().map_or("", |s| s.as_str());
|
|
||||||
|
|
||||||
let (chat_id, msg_id) = {
|
|
||||||
let chat_id = match q.chat_id() {
|
|
||||||
Some(chat_id) => chat_id.0,
|
|
||||||
None => tguser.id.0 as i64,
|
|
||||||
};
|
|
||||||
|
|
||||||
let msg_id = q.message.map_or_else(
|
|
||||||
|| {
|
|
||||||
Err(BotError::MsgTooOld(
|
|
||||||
"Failed to get message id, probably message too old".to_string(),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
|m| Ok(m.id().0),
|
|
||||||
);
|
|
||||||
|
|
||||||
(chat_id, msg_id)
|
|
||||||
};
|
|
||||||
|
|
||||||
let ma = MessageAnswerer::new(&bot, &mut db, chat_id);
|
|
||||||
match bm.is_replace() {
|
|
||||||
true => {
|
|
||||||
match msg_id {
|
|
||||||
Ok(msg_id) => {
|
|
||||||
ma.replace_message(msg_id, literal, buttons).await?;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
ma.answer(literal, None, buttons).await?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
false => {
|
|
||||||
ma.answer(literal, None, buttons).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,240 +0,0 @@
|
|||||||
use std::{collections::HashMap, future::Future, sync::RwLock, thread::JoinHandle, time::Duration};
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use log::{error, info};
|
|
||||||
use teloxide::{
|
|
||||||
dispatching::dialogue::serializer::Json,
|
|
||||||
dptree,
|
|
||||||
prelude::{Dispatcher, Requester},
|
|
||||||
Bot,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
bot_handler::{script_handler, BotHandler},
|
|
||||||
db::{bots::BotInstance, DbError, DB},
|
|
||||||
mongodb_storage::MongodbStorage,
|
|
||||||
BotController, BotError, BotResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct BotRunner {
|
|
||||||
controller: BotController,
|
|
||||||
info: BotInfo,
|
|
||||||
thread: Option<JoinHandle<BotResult<()>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Sync for BotRunner {}
|
|
||||||
unsafe impl Send for BotRunner {}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct BotInfo {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref BOT_POOL: RwLock<HashMap<String, BotRunner>> = RwLock::new(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static DEFAULT_SCRIPT: &str =
|
|
||||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/default_script.js"));
|
|
||||||
|
|
||||||
pub struct BotManager<BIG, HM, BIS, HI, FBIS, FHI>
|
|
||||||
where
|
|
||||||
BIG: FnMut() -> FBIS,
|
|
||||||
FBIS: Future<Output = BIS>,
|
|
||||||
BIS: Iterator<Item = BotInstance>,
|
|
||||||
HM: FnMut(BotInstance) -> FHI,
|
|
||||||
FHI: Future<Output = HI>,
|
|
||||||
HI: Iterator<Item = BotHandler>,
|
|
||||||
{
|
|
||||||
bot_pool: HashMap<String, BotRunner>,
|
|
||||||
bi_getter: BIG,
|
|
||||||
h_mapper: HM,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<BIG, HM, BIS, HI, FBIS, FHI> BotManager<BIG, HM, BIS, HI, FBIS, FHI>
|
|
||||||
where
|
|
||||||
BIG: FnMut() -> FBIS,
|
|
||||||
FBIS: Future<Output = BIS>,
|
|
||||||
BIS: Iterator<Item = BotInstance>,
|
|
||||||
HM: FnMut(BotInstance) -> FHI,
|
|
||||||
FHI: Future<Output = HI>,
|
|
||||||
HI: Iterator<Item = BotHandler>,
|
|
||||||
{
|
|
||||||
/// bi_getter - fnmut that returns iterator over BotInstance
|
|
||||||
/// h_map - fnmut that returns iterator over handlers by BotInstance
|
|
||||||
pub fn with(bi_getter: BIG, h_mapper: HM) -> Self {
|
|
||||||
Self {
|
|
||||||
bot_pool: Default::default(),
|
|
||||||
bi_getter,
|
|
||||||
h_mapper,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn dispatch(mut self, db: &mut DB) -> ! {
|
|
||||||
loop {
|
|
||||||
'biter: for bi in (self.bi_getter)().await {
|
|
||||||
// removing handler to force restart
|
|
||||||
// TODO: wait till all updates are processed in bot
|
|
||||||
// Temporarly disabling code, because it's free of js runtime
|
|
||||||
// spreads panic
|
|
||||||
if bi.restart_flag {
|
|
||||||
info!(
|
|
||||||
"Trying to restart bot `{}`, new script: {}",
|
|
||||||
bi.name, bi.script
|
|
||||||
);
|
|
||||||
let runner = self.bot_pool.remove(&bi.name);
|
|
||||||
};
|
|
||||||
// start, if not started
|
|
||||||
let mut bot_runner = match self.bot_pool.remove(&bi.name) {
|
|
||||||
Some(br) => br,
|
|
||||||
None => {
|
|
||||||
let handlers = (self.h_mapper)(bi.clone()).await;
|
|
||||||
info!("NEW INSTANCE: Starting new instance! bot name: {}", bi.name);
|
|
||||||
self.start_bot(bi, db, handlers.collect()).await.unwrap();
|
|
||||||
continue 'biter;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// checking if thread is not finished, otherwise clearing handler
|
|
||||||
bot_runner.thread = match bot_runner.thread {
|
|
||||||
Some(thread) => {
|
|
||||||
if thread.is_finished() {
|
|
||||||
let err = thread.join();
|
|
||||||
error!("Thread bot `{}` finished with error: {:?}", bi.name, err);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(thread)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// checking if thread is running, otherwise start thread
|
|
||||||
bot_runner.thread = match bot_runner.thread {
|
|
||||||
Some(thread) => Some(thread),
|
|
||||||
None => {
|
|
||||||
let handlers = (self.h_mapper)(bi.clone()).await;
|
|
||||||
let handler =
|
|
||||||
script_handler_gen(bot_runner.controller.clone(), handlers.collect())
|
|
||||||
.await;
|
|
||||||
Some(
|
|
||||||
spawn_bot_thread(bot_runner.controller.clone(), db, handler)
|
|
||||||
.await
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.bot_pool.insert(bi.name.clone(), bot_runner);
|
|
||||||
}
|
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_bot(
|
|
||||||
&mut self,
|
|
||||||
bi: BotInstance,
|
|
||||||
db: &mut DB,
|
|
||||||
plug_handlers: Vec<BotHandler>,
|
|
||||||
) -> BotResult<BotInfo> {
|
|
||||||
let mut db = db.clone().with_name(bi.name.clone());
|
|
||||||
let controller = BotController::with_db(db.clone(), &bi.token, &bi.script).await?;
|
|
||||||
|
|
||||||
let handler = script_handler_gen(controller.clone(), plug_handlers).await;
|
|
||||||
|
|
||||||
let thread = spawn_bot_thread(controller.clone(), &mut db, handler).await?;
|
|
||||||
|
|
||||||
let info = BotInfo {
|
|
||||||
name: bi.name.clone(),
|
|
||||||
};
|
|
||||||
let runner = BotRunner {
|
|
||||||
controller,
|
|
||||||
info: info.clone(),
|
|
||||||
thread: Some(thread),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.bot_pool.insert(bi.name.clone(), runner);
|
|
||||||
|
|
||||||
Ok(info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn script_handler_gen(c: BotController, plug_handlers: Vec<BotHandler>) -> BotHandler {
|
|
||||||
let handler = script_handler(c.rc.clone());
|
|
||||||
// each handler will be added to dptree::entry()
|
|
||||||
let handler = plug_handlers
|
|
||||||
.into_iter()
|
|
||||||
// as well as the script handler at the end
|
|
||||||
.chain(std::iter::once(handler))
|
|
||||||
.fold(dptree::entry(), |h, plug| h.branch(plug));
|
|
||||||
handler
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start_bot(
|
|
||||||
bi: BotInstance,
|
|
||||||
db: &mut DB,
|
|
||||||
plug_handlers: Vec<BotHandler>,
|
|
||||||
) -> BotResult<BotInfo> {
|
|
||||||
let mut db = db.clone().with_name(bi.name.clone());
|
|
||||||
let controller = BotController::with_db(db.clone(), &bi.token, &bi.script).await?;
|
|
||||||
|
|
||||||
let handler = script_handler(controller.rc.clone());
|
|
||||||
// each handler will be added to dptree::entry()
|
|
||||||
let handler = plug_handlers
|
|
||||||
.into_iter()
|
|
||||||
// as well as the script handler at the end
|
|
||||||
.chain(std::iter::once(handler))
|
|
||||||
.fold(dptree::entry(), |h, plug| h.branch(plug));
|
|
||||||
|
|
||||||
let thread = spawn_bot_thread(controller.clone(), &mut db, handler).await?;
|
|
||||||
|
|
||||||
let info = BotInfo {
|
|
||||||
name: bi.name.clone(),
|
|
||||||
};
|
|
||||||
let runner = BotRunner {
|
|
||||||
controller,
|
|
||||||
info: info.clone(),
|
|
||||||
thread: Some(thread),
|
|
||||||
};
|
|
||||||
|
|
||||||
BOT_POOL
|
|
||||||
.write()
|
|
||||||
.map_or_else(
|
|
||||||
|err| {
|
|
||||||
Err(BotError::RwLockError(format!(
|
|
||||||
"Failed to lock BOT_POOL because previous thread paniced, err: {err}"
|
|
||||||
)))
|
|
||||||
},
|
|
||||||
Ok,
|
|
||||||
)?
|
|
||||||
.insert(bi.name.clone(), runner);
|
|
||||||
|
|
||||||
Ok(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn spawn_bot_thread(
|
|
||||||
bc: BotController,
|
|
||||||
db: &mut DB,
|
|
||||||
handler: BotHandler,
|
|
||||||
) -> BotResult<JoinHandle<BotResult<()>>> {
|
|
||||||
let state_mgr = MongodbStorage::from_db(db, Json)
|
|
||||||
.await
|
|
||||||
.map_err(DbError::from)?;
|
|
||||||
let thread = std::thread::spawn(move || -> BotResult<()> {
|
|
||||||
let state_mgr = state_mgr;
|
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
rt.block_on(
|
|
||||||
Dispatcher::builder(bc.bot, handler)
|
|
||||||
.dependencies(dptree::deps![bc.db, state_mgr])
|
|
||||||
.build()
|
|
||||||
.dispatch(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(thread)
|
|
||||||
}
|
|
||||||
@ -1,13 +1,8 @@
|
|||||||
pub mod db;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex, PoisonError};
|
|
||||||
|
|
||||||
use crate::db::raw_calls::RawCallError;
|
|
||||||
use crate::db::{CallDB, DbError, DB};
|
use crate::db::{CallDB, DbError, DB};
|
||||||
use crate::utils::parcelable::{ParcelType, Parcelable, ParcelableError, ParcelableResult};
|
use crate::utils::parcelable::{ParcelType, Parcelable, ParcelableError, ParcelableResult};
|
||||||
use db::attach_db_obj;
|
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use futures::lock::MutexGuard;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use quickjs_rusty::serde::from_js;
|
use quickjs_rusty::serde::from_js;
|
||||||
use quickjs_rusty::utils::create_empty_object;
|
use quickjs_rusty::utils::create_empty_object;
|
||||||
@ -37,10 +32,6 @@ pub enum ScriptError {
|
|||||||
DBError(#[from] DbError),
|
DBError(#[from] DbError),
|
||||||
#[error("error resolving data: {0:?}")]
|
#[error("error resolving data: {0:?}")]
|
||||||
ResolveError(#[from] ResolveError),
|
ResolveError(#[from] ResolveError),
|
||||||
#[error("error while calling db from runtime: {0:?}")]
|
|
||||||
RawCallError(#[from] RawCallError),
|
|
||||||
#[error("error while locking mutex: {0:?}")]
|
|
||||||
MutexError(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@ -258,7 +249,7 @@ fn print(s: String) {
|
|||||||
println!("{s}");
|
println!("{s}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct BotConfig {
|
pub struct BotConfig {
|
||||||
version: f64,
|
version: f64,
|
||||||
}
|
}
|
||||||
@ -479,8 +470,6 @@ pub struct Button {
|
|||||||
pub struct BotMessage {
|
pub struct BotMessage {
|
||||||
// buttons: Vec<Button>
|
// buttons: Vec<Button>
|
||||||
literal: Option<String>,
|
literal: Option<String>,
|
||||||
#[serde(default)]
|
|
||||||
replace: bool,
|
|
||||||
buttons: Option<KeyboardDefinition>,
|
buttons: Option<KeyboardDefinition>,
|
||||||
state: Option<String>,
|
state: Option<String>,
|
||||||
|
|
||||||
@ -494,10 +483,6 @@ impl BotMessage {
|
|||||||
..self.clone()
|
..self.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_replace(&self) -> bool {
|
|
||||||
self.replace
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotMessage {
|
impl BotMessage {
|
||||||
@ -565,10 +550,9 @@ impl Parcelable<BotFunction> for BotMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct BotDialog {
|
pub struct BotDialog {
|
||||||
pub commands: HashMap<String, BotMessage>,
|
pub commands: HashMap<String, BotMessage>,
|
||||||
pub buttons: HashMap<String, BotMessage>,
|
|
||||||
stateful_msg_handlers: HashMap<String, BotMessage>,
|
stateful_msg_handlers: HashMap<String, BotMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,7 +560,6 @@ impl Parcelable<BotFunction> for BotDialog {
|
|||||||
fn get_field(&mut self, name: &str) -> Result<ParcelType<BotFunction>, ParcelableError> {
|
fn get_field(&mut self, name: &str) -> Result<ParcelType<BotFunction>, ParcelableError> {
|
||||||
match name {
|
match name {
|
||||||
"commands" => Ok(ParcelType::Parcelable(&mut self.commands)),
|
"commands" => Ok(ParcelType::Parcelable(&mut self.commands)),
|
||||||
"buttons" => Ok(ParcelType::Parcelable(&mut self.buttons)),
|
|
||||||
"stateful_msg_handlers" => Ok(ParcelType::Parcelable(&mut self.stateful_msg_handlers)),
|
"stateful_msg_handlers" => Ok(ParcelType::Parcelable(&mut self.stateful_msg_handlers)),
|
||||||
field => Err(ParcelableError::FieldError(format!(
|
field => Err(ParcelableError::FieldError(format!(
|
||||||
"tried to get field {field}, but this field does not exists or private"
|
"tried to get field {field}, but this field does not exists or private"
|
||||||
@ -585,7 +568,7 @@ impl Parcelable<BotFunction> for BotDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct RunnerConfig {
|
pub struct RunnerConfig {
|
||||||
config: BotConfig,
|
config: BotConfig,
|
||||||
pub dialog: BotDialog,
|
pub dialog: BotDialog,
|
||||||
@ -598,12 +581,6 @@ impl RunnerConfig {
|
|||||||
|
|
||||||
bm.map(|bm| bm.fill_literal(command.to_string()))
|
bm.map(|bm| bm.fill_literal(command.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_callback_message(&self, callback: &str) -> Option<BotMessage> {
|
|
||||||
let bm = self.dialog.buttons.get(callback).cloned();
|
|
||||||
|
|
||||||
bm.map(|bm| bm.fill_literal(callback.to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parcelable<BotFunction> for RunnerConfig {
|
impl Parcelable<BotFunction> for RunnerConfig {
|
||||||
@ -617,9 +594,8 @@ impl Parcelable<BotFunction> for RunnerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Runner {
|
pub struct Runner {
|
||||||
context: Arc<Mutex<Context>>,
|
context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Runner {
|
impl Runner {
|
||||||
@ -632,36 +608,11 @@ impl Runner {
|
|||||||
None::<bool>
|
None::<bool>
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Runner {
|
Ok(Runner { context })
|
||||||
context: Arc::new(Mutex::new(context)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_with_db(db: &mut DB) -> ScriptResult<Self> {
|
|
||||||
let context = Context::new(None)?;
|
|
||||||
let mut global = context.global()?;
|
|
||||||
attach_db_obj(&context, &mut global, db)?;
|
|
||||||
|
|
||||||
context.add_callback("print", |a: String| {
|
|
||||||
print(a);
|
|
||||||
|
|
||||||
None::<bool>
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Runner {
|
|
||||||
context: Arc::new(Mutex::new(context)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_script(&self, content: &str) -> ScriptResult<JsValue> {
|
pub fn run_script(&self, content: &str) -> ScriptResult<JsValue> {
|
||||||
let ctx = match self.context.lock() {
|
let ctx = &self.context;
|
||||||
Ok(ctx) => ctx,
|
|
||||||
Err(err) => {
|
|
||||||
return Err(ScriptError::MutexError(format!(
|
|
||||||
"can't lock js Context mutex, err: {err}"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let val = ctx.eval(content, false)?;
|
let val = ctx.eval(content, false)?;
|
||||||
|
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
use quickjs_rusty::context::Context;
|
|
||||||
|
|
||||||
use quickjs_rusty::serde::{from_js, to_js};
|
|
||||||
use quickjs_rusty::{utils::create_empty_object, OwnedJsObject, OwnedJsValue as JsValue};
|
|
||||||
|
|
||||||
use crate::db::raw_calls::RawCall;
|
|
||||||
use crate::db::DB;
|
|
||||||
|
|
||||||
use super::ScriptError;
|
|
||||||
|
|
||||||
pub fn attach_db_obj(c: &Context, o: &mut OwnedJsObject, db: &DB) -> Result<(), ScriptError> {
|
|
||||||
let dbobj = JsValue::new(o.context(), create_empty_object(o.context())?)
|
|
||||||
.try_into_object()
|
|
||||||
.expect("the created object was not an object :/");
|
|
||||||
|
|
||||||
let db: std::sync::Arc<RwLock<DB>> = std::sync::Arc::new(RwLock::new(db.clone()));
|
|
||||||
let dbbox = Box::new(db);
|
|
||||||
let db: &'static _ = Box::leak(dbbox);
|
|
||||||
|
|
||||||
let find_one = c.create_callback(
|
|
||||||
|collection: String, q: OwnedJsObject| -> Result<_, ScriptError> {
|
|
||||||
let query: serde_json::Value = match from_js(q.context(), &q) {
|
|
||||||
Ok(q) => q,
|
|
||||||
Err(_) => todo!(),
|
|
||||||
};
|
|
||||||
let db = db.clone();
|
|
||||||
|
|
||||||
let value = futures::executor::block_on(
|
|
||||||
db.write()
|
|
||||||
.expect("failed to gain write acces to db (probably RwLock is poisoned)")
|
|
||||||
.find_one(&collection, query),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let ret = match value {
|
|
||||||
Some(v) => Some(to_js(q.context(), &v)?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
Ok(ret)
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
let find_one = JsValue::from((unsafe { c.context_raw() }, find_one));
|
|
||||||
|
|
||||||
dbobj.set_property("find_one", find_one)?;
|
|
||||||
|
|
||||||
o.set_property("db", dbobj.into_value())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
use bson::doc;
|
|
||||||
use bson::oid::ObjectId;
|
|
||||||
use chrono::{DateTime, FixedOffset, Local};
|
|
||||||
use futures::StreamExt;
|
|
||||||
use futures::TryStreamExt;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::DbCollection;
|
|
||||||
use super::DbResult;
|
|
||||||
use crate::db::GetCollection;
|
|
||||||
use crate::query_call_consume;
|
|
||||||
use crate::CallDB;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
|
||||||
pub struct BotInstance {
|
|
||||||
pub _id: bson::oid::ObjectId,
|
|
||||||
pub name: String,
|
|
||||||
pub token: String,
|
|
||||||
pub script: String,
|
|
||||||
pub restart_flag: bool,
|
|
||||||
pub created_at: DateTime<FixedOffset>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbCollection for BotInstance {
|
|
||||||
const COLLECTION: &str = "bots";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BotInstance {
|
|
||||||
pub fn new(name: String, token: String, script: String) -> Self {
|
|
||||||
Self {
|
|
||||||
_id: Default::default(),
|
|
||||||
name,
|
|
||||||
token,
|
|
||||||
script,
|
|
||||||
restart_flag: false,
|
|
||||||
created_at: Local::now().into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query_call_consume!(store, self, db, Self, {
|
|
||||||
let bi = db.get_collection::<Self>().await;
|
|
||||||
|
|
||||||
bi.insert_one(&self).await?;
|
|
||||||
|
|
||||||
Ok(self)
|
|
||||||
});
|
|
||||||
|
|
||||||
pub async fn get_all<D: CallDB>(db: &mut D) -> DbResult<Vec<Self>> {
|
|
||||||
let bi = db.get_collection::<Self>().await;
|
|
||||||
|
|
||||||
Ok(bi.find(doc! {}).await?.try_collect().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_by_name<D: CallDB>(db: &mut D, name: &str) -> DbResult<Option<Self>> {
|
|
||||||
let bi = db.get_collection::<Self>().await;
|
|
||||||
|
|
||||||
Ok(bi.find_one(doc! {"name": name}).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn restart_one<D: CallDB>(db: &mut D, name: &str, restart: bool) -> DbResult<()> {
|
|
||||||
let bi = db.get_collection::<Self>().await;
|
|
||||||
|
|
||||||
bi.update_one(
|
|
||||||
doc! {"name": name},
|
|
||||||
doc! { "$set": { "restart_flag": restart } },
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn restart_all<D: CallDB>(db: &mut D, restart: bool) -> DbResult<()> {
|
|
||||||
let bi = db.get_collection::<Self>().await;
|
|
||||||
|
|
||||||
bi.update_many(doc! {}, doc! { "$set": { "restart_flag": restart } })
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_script<D: CallDB>(db: &mut D, name: &str, script: &str) -> DbResult<()> {
|
|
||||||
let bi = db.get_collection::<Self>().await;
|
|
||||||
|
|
||||||
bi.update_one(
|
|
||||||
doc! {"name": name},
|
|
||||||
doc! { "$set": {
|
|
||||||
"script": script,
|
|
||||||
"restart_flag": true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
pub mod application;
|
pub mod application;
|
||||||
pub mod bots;
|
|
||||||
pub mod callback_info;
|
pub mod callback_info;
|
||||||
pub mod message_forward;
|
pub mod message_forward;
|
||||||
pub mod raw_calls;
|
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -13,7 +11,7 @@ use futures::stream::TryStreamExt;
|
|||||||
|
|
||||||
use mongodb::options::IndexOptions;
|
use mongodb::options::IndexOptions;
|
||||||
use mongodb::{bson::doc, options::ClientOptions, Client};
|
use mongodb::{bson::doc, options::ClientOptions, Client};
|
||||||
use mongodb::{Collection, Database, IndexModel};
|
use mongodb::{Database, IndexModel};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(EnumStringify)]
|
#[derive(EnumStringify)]
|
||||||
@ -142,15 +140,14 @@ pub struct Media {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DB {
|
pub struct DB {
|
||||||
client: Client,
|
client: Client,
|
||||||
name: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DB {
|
impl DB {
|
||||||
pub async fn new<S: Into<String>>(db_url: S, name: String) -> DbResult<Self> {
|
pub async fn new<S: Into<String>>(db_url: S) -> DbResult<Self> {
|
||||||
let options = ClientOptions::parse(db_url.into()).await?;
|
let options = ClientOptions::parse(db_url.into()).await?;
|
||||||
let client = Client::with_options(options)?;
|
let client = Client::with_options(options)?;
|
||||||
|
|
||||||
Ok(DB { client, name })
|
Ok(DB { client })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn migrate(&mut self) -> DbResult<()> {
|
pub async fn migrate(&mut self) -> DbResult<()> {
|
||||||
@ -187,38 +184,18 @@ impl DB {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init<S: Into<String>>(db_url: S, name: String) -> DbResult<Self> {
|
pub async fn init<S: Into<String>>(db_url: S) -> DbResult<Self> {
|
||||||
let mut db = Self::new(db_url, name).await?;
|
let mut db = Self::new(db_url).await?;
|
||||||
db.migrate().await?;
|
db.migrate().await?;
|
||||||
|
|
||||||
Ok(db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_name(self, name: String) -> Self {
|
|
||||||
Self { name, ..self }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DbCollection {
|
|
||||||
const COLLECTION: &str;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait GetCollection {
|
|
||||||
async fn get_collection<C: DbCollection + Send + Sync>(&mut self) -> Collection<C>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CallDB for DB {
|
impl CallDB for DB {
|
||||||
async fn get_database(&mut self) -> Database {
|
async fn get_database(&mut self) -> Database {
|
||||||
self.client.database(&self.name)
|
self.client.database("gongbot")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: CallDB> GetCollection for T {
|
|
||||||
async fn get_collection<C: DbCollection + Send + Sync>(&mut self) -> Collection<C> {
|
|
||||||
self.get_database()
|
|
||||||
.await
|
|
||||||
.collection(<C as DbCollection>::COLLECTION)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
use mongodb::Database;
|
|
||||||
|
|
||||||
use super::CallDB;
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum RawCallError {
|
|
||||||
#[error("error while processing mongodb query: {0}")]
|
|
||||||
MongodbError(#[from] mongodb::error::Error),
|
|
||||||
#[error("error while buildint bson's query document: {0}")]
|
|
||||||
DocumentError(#[from] mongodb::bson::extjson::de::Error),
|
|
||||||
#[error("error when expected map: {0}")]
|
|
||||||
NotAMapError(String),
|
|
||||||
}
|
|
||||||
pub type RawCallResult<T> = Result<T, RawCallError>;
|
|
||||||
|
|
||||||
pub trait RawCall {
|
|
||||||
async fn get_database(&mut self) -> Database;
|
|
||||||
async fn find_one(&mut self, collection: &str, query: Value) -> RawCallResult<Option<Value>> {
|
|
||||||
let db = self.get_database().await;
|
|
||||||
let value = db.collection::<Value>(collection);
|
|
||||||
|
|
||||||
let map = match query {
|
|
||||||
Value::Object(map) => map,
|
|
||||||
_ => return Err(RawCallError::NotAMapError("query is not a map".to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
let doc = map.try_into()?;
|
|
||||||
let ret = value.find_one(doc).await?;
|
|
||||||
Ok(ret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: CallDB> RawCall for T {
|
|
||||||
async fn get_database(&mut self) -> Database {
|
|
||||||
CallDB::get_database(self).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -10,7 +10,7 @@ async fn setup_db() -> DB {
|
|||||||
dotenvy::dotenv().unwrap();
|
dotenvy::dotenv().unwrap();
|
||||||
let db_url = std::env::var("DATABASE_URL").unwrap();
|
let db_url = std::env::var("DATABASE_URL").unwrap();
|
||||||
|
|
||||||
DB::new(db_url, "gongbot".to_string()).await.unwrap()
|
DB::new(db_url).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@ -1,532 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
use log::{info, warn};
|
|
||||||
use std::time::Duration;
|
|
||||||
use teloxide::dispatching::dialogue::serializer::Json;
|
|
||||||
use teloxide::net::Download;
|
|
||||||
use teloxide::prelude::*;
|
|
||||||
use teloxide::sugar::request::RequestReplyExt;
|
|
||||||
use teloxide::types::{MediaKind, MessageId, MessageKind, ParseMode};
|
|
||||||
use teloxide::utils::render::RenderMessageTextHelper;
|
|
||||||
use teloxide::{dptree, types::Update};
|
|
||||||
|
|
||||||
use futures::StreamExt;
|
|
||||||
|
|
||||||
use crate::admin::{admin_command_handler, AdminCommands};
|
|
||||||
use crate::bot_handler::BotHandler;
|
|
||||||
use crate::db::bots::BotInstance;
|
|
||||||
use crate::db::message_forward::MessageForward;
|
|
||||||
use crate::db::{CallDB, DB};
|
|
||||||
use crate::mongodb_storage::MongodbStorage;
|
|
||||||
use crate::{BotDialogue, BotError, BotResult, CallbackStore, State};
|
|
||||||
|
|
||||||
pub fn admin_handler() -> BotHandler {
|
|
||||||
dptree::entry()
|
|
||||||
.branch(
|
|
||||||
Update::filter_callback_query()
|
|
||||||
.filter_async(async |q: CallbackQuery, mut db: DB| {
|
|
||||||
let tguser = q.from.clone();
|
|
||||||
let user = db
|
|
||||||
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
|
||||||
.await;
|
|
||||||
user.map(|u| u.is_admin).unwrap_or(false)
|
|
||||||
})
|
|
||||||
.enter_dialogue::<CallbackQuery, MongodbStorage<Json>, State>()
|
|
||||||
.branch(dptree::case![State::EditButton].endpoint(button_edit_callback)),
|
|
||||||
)
|
|
||||||
.branch(command_handler())
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.filter_async(async |msg: Message, mut db: DB| {
|
|
||||||
let tguser = match msg.from.clone() {
|
|
||||||
Some(user) => user,
|
|
||||||
None => return false, // do nothing, cause its not usecase of function
|
|
||||||
};
|
|
||||||
let user = db
|
|
||||||
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
|
||||||
.await;
|
|
||||||
user.map(|u| u.is_admin).unwrap_or(false)
|
|
||||||
})
|
|
||||||
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.filter(|msg: Message| {
|
|
||||||
msg.text().unwrap_or("").to_lowercase().as_str() == "edit"
|
|
||||||
})
|
|
||||||
.endpoint(edit_msg_cmd_handler),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.filter_map(|msg: Message| {
|
|
||||||
let text = msg.caption().unwrap_or("");
|
|
||||||
let mut parts = text.split_whitespace();
|
|
||||||
let cmd = parts.next().unwrap_or("");
|
|
||||||
let arg = parts.next().unwrap_or("");
|
|
||||||
|
|
||||||
match cmd.to_lowercase().as_str() == "/newscript" {
|
|
||||||
true => Some(arg.to_string()),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.endpoint(newscript_handler),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.filter(|msg: Message| msg.reply_to_message().is_some())
|
|
||||||
.filter(|state: State| matches!(state, State::Start))
|
|
||||||
.endpoint(support_reply_handler),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
dptree::case![State::Edit {
|
|
||||||
literal,
|
|
||||||
variant,
|
|
||||||
lang,
|
|
||||||
is_caption_set
|
|
||||||
}]
|
|
||||||
.endpoint(edit_msg_handler),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
|
||||||
.branch(dptree::case![State::MessageForwardReply].endpoint(user_reply_to_support)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
async fn newscript_handler(bot: Bot, mut db: DB, msg: Message, name: String) -> BotResult<()> {
|
|
||||||
let script = match msg.kind {
|
|
||||||
MessageKind::Common(message) => {
|
|
||||||
match message.media_kind {
|
|
||||||
MediaKind::Document(media_document) => {
|
|
||||||
let doc = media_document.document;
|
|
||||||
let file = bot.get_file(doc.file.id).await?;
|
|
||||||
let mut stream = bot.download_file_stream(&file.path);
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
|
||||||
while let Some(bytes) = stream.next().await {
|
|
||||||
let mut bytes = bytes.unwrap().to_vec();
|
|
||||||
buf.append(&mut bytes);
|
|
||||||
}
|
|
||||||
let script = match String::from_utf8(buf) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to parse buf to string, err: {err}");
|
|
||||||
bot.send_message(msg.chat.id, format!("Failed to Convert file to script: file is not UTF-8, err: {err}")).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
script
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match BotInstance::get_by_name(&mut db, &name).await? {
|
|
||||||
Some(bi) => bi,
|
|
||||||
None => {
|
|
||||||
bot.send_message(
|
|
||||||
msg.chat.id,
|
|
||||||
format!("Failed to set script, possibly bots name is incorrent"),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
BotInstance::update_script(&mut db, &name, &script).await?;
|
|
||||||
|
|
||||||
bot.send_message(msg.chat.id, "New script is set!").await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn button_edit_callback(
|
|
||||||
bot: Bot,
|
|
||||||
mut db: DB,
|
|
||||||
dialogue: BotDialogue,
|
|
||||||
q: CallbackQuery,
|
|
||||||
) -> BotResult<()> {
|
|
||||||
bot.answer_callback_query(&q.id).await?;
|
|
||||||
|
|
||||||
let id = match q.data {
|
|
||||||
Some(id) => id,
|
|
||||||
None => {
|
|
||||||
bot.send_message(q.from.id, "Not compatible callback to edit text on")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let ci = match CallbackStore::get(&mut db, &id).await? {
|
|
||||||
Some(ci) => ci,
|
|
||||||
None => {
|
|
||||||
bot.send_message(
|
|
||||||
q.from.id,
|
|
||||||
"Can't get button information. Maybe created not by this bot or message too old",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let literal = match ci.literal {
|
|
||||||
Some(l) => l,
|
|
||||||
None => {
|
|
||||||
bot.send_message(
|
|
||||||
q.from.id,
|
|
||||||
"This button is not editable (probably text is generated)",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let lang = "ru".to_string();
|
|
||||||
dialogue
|
|
||||||
.update(State::Edit {
|
|
||||||
literal,
|
|
||||||
variant: None,
|
|
||||||
lang,
|
|
||||||
is_caption_set: false,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
bot.send_message(q.from.id, "Send text of button").await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_handler() -> BotHandler {
|
|
||||||
Update::filter_message()
|
|
||||||
.filter_async(async |msg: Message, mut db: DB| {
|
|
||||||
let tguser = match msg.from.clone() {
|
|
||||||
Some(user) => user,
|
|
||||||
None => return false, // do nothing, cause its not usecase of function
|
|
||||||
};
|
|
||||||
let user = db
|
|
||||||
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
|
||||||
.await;
|
|
||||||
user.map(|u| u.is_admin).unwrap_or(false)
|
|
||||||
})
|
|
||||||
.filter_command::<AdminCommands>()
|
|
||||||
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
|
||||||
.endpoint(admin_command_handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn edit_msg_cmd_handler(
|
|
||||||
bot: Bot,
|
|
||||||
mut db: DB,
|
|
||||||
dialogue: BotDialogue,
|
|
||||||
msg: Message,
|
|
||||||
) -> BotResult<()> {
|
|
||||||
match msg.reply_to_message() {
|
|
||||||
Some(replied) => {
|
|
||||||
let msgid = replied.id;
|
|
||||||
// look for message in db and set text
|
|
||||||
let literal = match db.get_message_literal(msg.chat.id.0, msgid.0).await? {
|
|
||||||
Some(l) => l,
|
|
||||||
None => {
|
|
||||||
bot.send_message(msg.chat.id, "No such message found to edit. Look if you replying bot's message and this message is supposed to be editable").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// TODO: language selector will be implemented in future 😈
|
|
||||||
let lang = "ru".to_string();
|
|
||||||
dialogue
|
|
||||||
.update(State::Edit {
|
|
||||||
literal,
|
|
||||||
variant: None,
|
|
||||||
lang,
|
|
||||||
is_caption_set: false,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
bot.send_message(
|
|
||||||
msg.chat.id,
|
|
||||||
"Ok, now you have to send message text (formatting supported)\n\
|
|
||||||
<b>Notice:</b> if this message supposed to replace message (tg shows them as edited) \
|
|
||||||
or be raplaced, do NOT send message with multiple media, only single photo, video etc. \
|
|
||||||
To get more information about why, see in /why_media_group",
|
|
||||||
).parse_mode(ParseMode::Html)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
bot.send_message(msg.chat.id, "You have to reply to message to edit it")
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn support_reply_handler(
|
|
||||||
bot: Bot,
|
|
||||||
mut db: DB,
|
|
||||||
msg: Message,
|
|
||||||
state_mgr: std::sync::Arc<MongodbStorage<Json>>,
|
|
||||||
) -> BotResult<()> {
|
|
||||||
use teloxide::utils::render::Renderer;
|
|
||||||
|
|
||||||
let rm = match msg.reply_to_message() {
|
|
||||||
Some(rm) => rm,
|
|
||||||
None => {
|
|
||||||
return Err(BotError::BotLogicError(
|
|
||||||
"support_reply_handler should not be called when no message is replied".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let (chat_id, message_id) = (rm.chat.id.0, rm.id.0);
|
|
||||||
let mf = match MessageForward::get(&mut db, chat_id, message_id).await? {
|
|
||||||
Some(mf) => mf,
|
|
||||||
None => {
|
|
||||||
bot.send_message(msg.chat.id, "No forwarded message found for your reply")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let text = match msg.kind {
|
|
||||||
MessageKind::Common(message_common) => match message_common.media_kind {
|
|
||||||
MediaKind::Text(media_text) => {
|
|
||||||
Renderer::new(&media_text.text, &media_text.entities).as_html()
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
bot.send_message(msg.chat.id, "Only text messages currently supported!")
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// can't hapen because we already have check for reply
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let msg = bot
|
|
||||||
.send_message(ChatId(mf.source_chat_id), text)
|
|
||||||
.parse_mode(ParseMode::Html);
|
|
||||||
let msg = match mf.reply {
|
|
||||||
false => msg,
|
|
||||||
true => msg.reply_to(MessageId(mf.source_message_id)),
|
|
||||||
};
|
|
||||||
msg.await?;
|
|
||||||
|
|
||||||
let user_dialogue = BotDialogue::new(state_mgr, ChatId(mf.source_chat_id));
|
|
||||||
user_dialogue.update(State::MessageForwardReply).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn edit_msg_handler(
|
|
||||||
bot: Bot,
|
|
||||||
mut db: DB,
|
|
||||||
dialogue: BotDialogue,
|
|
||||||
(literal, variant, lang, is_caption_set): (String, Option<String>, String, bool),
|
|
||||||
msg: Message,
|
|
||||||
) -> BotResult<()> {
|
|
||||||
use teloxide::utils::render::Renderer;
|
|
||||||
|
|
||||||
let chat_id = msg.chat.id;
|
|
||||||
info!("Type: {:#?}", msg.kind);
|
|
||||||
let msg = if let MessageKind::Common(msg) = msg.kind {
|
|
||||||
msg
|
|
||||||
} else {
|
|
||||||
info!("Not a Common, somehow");
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(variant) = variant {
|
|
||||||
if let MediaKind::Text(text) = msg.media_kind {
|
|
||||||
let html_text = Renderer::new(&text.text, &text.entities).as_html();
|
|
||||||
|
|
||||||
db.set_literal_alternative(&literal, &variant, &html_text)
|
|
||||||
.await?;
|
|
||||||
bot.send_message(chat_id, "Updated text of variant!")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
dialogue.exit().await?;
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
bot.send_message(
|
|
||||||
chat_id,
|
|
||||||
"On variants only text alternating supported. Try to send text only",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match msg.media_kind {
|
|
||||||
MediaKind::Text(text) => {
|
|
||||||
db.drop_media(&literal).await?;
|
|
||||||
if is_caption_set {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let html_text = Renderer::new(&text.text, &text.entities).as_html();
|
|
||||||
db.set_literal(&literal, &html_text).await?;
|
|
||||||
bot.send_message(chat_id, "Updated text of message!")
|
|
||||||
.await?;
|
|
||||||
dialogue.exit().await?;
|
|
||||||
}
|
|
||||||
MediaKind::Photo(photo) => {
|
|
||||||
let group = photo.media_group_id;
|
|
||||||
if let Some(group) = group.clone() {
|
|
||||||
db.drop_media_except(&literal, &group).await?;
|
|
||||||
} else {
|
|
||||||
db.drop_media(&literal).await?;
|
|
||||||
}
|
|
||||||
let file_id = photo.photo[0].file.id.clone();
|
|
||||||
db.add_media(&literal, "photo", &file_id, group.as_deref())
|
|
||||||
.await?;
|
|
||||||
match photo.caption {
|
|
||||||
Some(text) => {
|
|
||||||
let html_text = Renderer::new(&text, &photo.caption_entities).as_html();
|
|
||||||
db.set_literal(&literal, &html_text).await?;
|
|
||||||
bot.send_message(chat_id, "Updated photo caption!").await?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// if it is a first message in group,
|
|
||||||
// or just a photo without caption (unwrap_or case),
|
|
||||||
// set text empty
|
|
||||||
if !db
|
|
||||||
.is_media_group_exists(group.as_deref().unwrap_or(""))
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
db.set_literal(&literal, "").await?;
|
|
||||||
bot.send_message(chat_id, "Set photo without caption")
|
|
||||||
.await?;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Some workaround because Telegram's group system
|
|
||||||
// is not easily and obviously handled with this
|
|
||||||
// code architecture, but probably there is a solution.
|
|
||||||
//
|
|
||||||
// So, this code will just wait for all media group
|
|
||||||
// updates to be processed
|
|
||||||
dialogue
|
|
||||||
.update(State::Edit {
|
|
||||||
literal,
|
|
||||||
variant: None,
|
|
||||||
lang,
|
|
||||||
is_caption_set: true,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
||||||
dialogue.exit().await.unwrap_or(());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
MediaKind::Video(video) => {
|
|
||||||
let group = video.media_group_id;
|
|
||||||
if let Some(group) = group.clone() {
|
|
||||||
db.drop_media_except(&literal, &group).await?;
|
|
||||||
} else {
|
|
||||||
db.drop_media(&literal).await?;
|
|
||||||
}
|
|
||||||
let file_id = video.video.file.id;
|
|
||||||
db.add_media(&literal, "video", &file_id, group.as_deref())
|
|
||||||
.await?;
|
|
||||||
match video.caption {
|
|
||||||
Some(text) => {
|
|
||||||
let html_text = Renderer::new(&text, &video.caption_entities).as_html();
|
|
||||||
db.set_literal(&literal, &html_text).await?;
|
|
||||||
bot.send_message(chat_id, "Updated video caption!").await?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// if it is a first message in group,
|
|
||||||
// or just a video without caption (unwrap_or case),
|
|
||||||
// set text empty
|
|
||||||
if !db
|
|
||||||
.is_media_group_exists(group.as_deref().unwrap_or(""))
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
db.set_literal(&literal, "").await?;
|
|
||||||
bot.send_message(chat_id, "Set video without caption")
|
|
||||||
.await?;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Some workaround because Telegram's group system
|
|
||||||
// is not easily and obviously handled with this
|
|
||||||
// code architecture, but probably there is a solution.
|
|
||||||
//
|
|
||||||
// So, this code will just wait for all media group
|
|
||||||
// updates to be processed
|
|
||||||
dialogue
|
|
||||||
.update(State::Edit {
|
|
||||||
literal,
|
|
||||||
variant: None,
|
|
||||||
lang,
|
|
||||||
is_caption_set: true,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
||||||
dialogue.exit().await.unwrap_or(());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
bot.send_message(chat_id, "this type of message is not supported yet")
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn user_reply_to_support(bot: Bot, mut db: DB, msg: Message) -> BotResult<()> {
|
|
||||||
let (source_chat_id, source_message_id) = (msg.chat.id.0, msg.id.0);
|
|
||||||
let text = match msg.html_text() {
|
|
||||||
Some(text) => text,
|
|
||||||
// TODO: come up with better idea than just ignoring (say something to user)
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
let scid =
|
|
||||||
db.get_literal_value("support_chat_id")
|
|
||||||
.await?
|
|
||||||
.ok_or(BotError::AdminMisconfiguration(
|
|
||||||
"support_chat_id is not set".to_string(),
|
|
||||||
))?;
|
|
||||||
let support_chat_id = match scid.parse::<i64>() {
|
|
||||||
Ok(cid) => cid,
|
|
||||||
Err(parseerr) => {
|
|
||||||
return Err(BotError::BotLogicError(format!(
|
|
||||||
"source_chat_id, got: {scid}, expected: i64, err: {parseerr}"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let user = msg.from.ok_or(BotError::BotLogicError(
|
|
||||||
"Unable to get user somehow:/".to_string(),
|
|
||||||
))?;
|
|
||||||
let parts = [
|
|
||||||
Some(user.first_name),
|
|
||||||
user.last_name,
|
|
||||||
user.username.map(|un| format!("(@{un})")),
|
|
||||||
];
|
|
||||||
#[allow(unstable_name_collisions)]
|
|
||||||
let userformat: String = parts
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.intersperse(" ".to_string())
|
|
||||||
.collect();
|
|
||||||
let msgtext = format!("From: {userformat}\nMessage:\n{text}");
|
|
||||||
|
|
||||||
// TODO: fix bug: parse mode's purpose is to display user-formated text in right way,
|
|
||||||
// but there is a bug: user can inject html code with his first/last/user name
|
|
||||||
// it's not harmful, only visible to support, but still need a fix
|
|
||||||
let sentmsg = bot
|
|
||||||
.send_message(ChatId(support_chat_id), msgtext)
|
|
||||||
.parse_mode(ParseMode::Html)
|
|
||||||
.await?;
|
|
||||||
MessageForward::new(
|
|
||||||
sentmsg.chat.id.0,
|
|
||||||
sentmsg.id.0,
|
|
||||||
source_chat_id,
|
|
||||||
source_message_id,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.store(&mut db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
pub mod admin;
|
|
||||||
979
src/main.rs
979
src/main.rs
File diff suppressed because it is too large
Load Diff
@ -1,302 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,8 +9,6 @@ use mongodb::Database;
|
|||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use teloxide::dispatching::dialogue::{Serializer, Storage};
|
use teloxide::dispatching::dialogue::{Serializer, Storage};
|
||||||
|
|
||||||
use crate::db::{CallDB, DB};
|
|
||||||
|
|
||||||
pub struct MongodbStorage<S> {
|
pub struct MongodbStorage<S> {
|
||||||
database: Database,
|
database: Database,
|
||||||
serializer: S,
|
serializer: S,
|
||||||
@ -30,13 +28,6 @@ impl<S> MongodbStorage<S> {
|
|||||||
serializer,
|
serializer,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_db(db: &mut DB, serializer: S) -> Result<Arc<Self>, mongodb::error::Error> {
|
|
||||||
Ok(Arc::new(Self {
|
|
||||||
database: CallDB::get_database(db).await,
|
|
||||||
serializer,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user