Compare commits

..

30 Commits

Author SHA1 Message Date
Akulij
7e01186178 use async_trait for RawCall
All checks were successful
Build && Deploy / cargo build (push) Successful in 1m5s
2025-06-07 03:33:13 +05:00
Akulij
12af8f3653 delete unused import 2025-06-07 03:31:34 +05:00
Akulij
fd24b6953e on downloading new script notify admin about failure 2025-06-07 03:31:03 +05:00
Akulij
4b78ebbb7b allow unwrap in src/commands.rs's tests 2025-06-07 03:30:45 +05:00
Akulij
93852b9155 refactor timezoned time creation in botscript 2025-06-07 03:23:35 +05:00
Akulij
1edaac9d8a remove unnecessary use on Arc in Runner 2025-06-07 03:18:06 +05:00
Akulij
51e4d1a1fc fix warnings in botscript/application 2025-06-07 03:17:34 +05:00
Akulij
c0eb5ba412 bot_manager: fix MutexGuard lifetime 2025-06-07 02:59:07 +05:00
Akulij
c3386a1e2f use MessageAnswererError in message_answerer.rs 2025-06-07 02:56:17 +05:00
Akulij
3c0ae02139 fix: reutnr error from bot managers dispatcher 2025-06-07 02:55:29 +05:00
Akulij
0c1ab767d3 handle MessageAnswererError in BotError 2025-06-07 02:55:09 +05:00
Akulij
6776716faf fix $crate literal in query_call_consume macro 2025-06-07 02:54:35 +05:00
Akulij
5399fb682e handle MessageAnswererError in ScriptError 2025-06-07 02:54:08 +05:00
Akulij
c9a3916304 do not use unwrap in botscript/application.rs 2025-06-07 02:53:44 +05:00
Akulij
18d6331344 bot manager: propagate errors 2025-06-07 02:53:06 +05:00
Akulij
f6a5a42b71 propagate error instead of unwrap in bot_handler.rs 2025-06-07 02:52:37 +05:00
Akulij
8e57f5da7e fix: do not box leak in botscript/application 2025-06-07 02:27:42 +05:00
Akulij
e6c9cfb0c1 fix warnings in botscript.rs 2025-06-07 02:27:24 +05:00
Akulij
0c3fb0788a delete unnecessary printlns 2025-06-07 02:10:04 +05:00
Akulij
bd8b1e8843 impl GetCollection via async_trait 2025-06-07 02:08:00 +05:00
Akulij
5a7bb0e0f6 clippy fix 2025-06-07 01:50:30 +05:00
Akulij
4a090de77b fix warnings in src/message_answerer.rs 2025-06-07 01:46:35 +05:00
Akulij
3bb03365ed fix warnings in main.rs 2025-06-07 01:43:50 +05:00
Akulij
b27edd421d delete not used anymore UserCommands 2025-06-07 01:40:12 +05:00
Akulij
7752160807 delete unused functions in main.rs 2025-06-07 01:39:01 +05:00
Akulij
3dbfbe48ce fix: reuse init logic in Runner::init_with_db 2025-06-07 01:37:34 +05:00
Akulij
99403b7282 fix: implement js's db callback without box leak 2025-06-07 01:28:21 +05:00
Akulij
b8bd104f3d use CallbackInfo for buttons 2025-06-06 03:32:58 +05:00
Akulij
6e31fa86e6 create function that stores button parts and creates tg button wrapper 2025-06-06 03:32:27 +05:00
Akulij
bde3c1a0e1 fix: call "answer callback query" when handling callback 2025-06-06 02:08:39 +05:00
16 changed files with 295 additions and 457 deletions

View File

@ -178,12 +178,9 @@ pub async fn admin_command_handler(
} }
}; };
let bi = BotInstance::new(name.clone(), token.to_string(), DEFAULT_SCRIPT.to_string())
BotInstance::new(name.clone(), token.to_string(), DEFAULT_SCRIPT.to_string()) .store(&mut db)
.store(&mut db) .await?
.await?;
bi
}; };
bot.send_message( bot.send_message(

View File

@ -1,28 +1,34 @@
use futures::future::join_all;
use log::{error, info}; use log::{error, info};
use quickjs_rusty::serde::{from_js, to_js}; use quickjs_rusty::serde::to_js;
use serde_json::Value;
use std::{ use std::{
str::FromStr, str::FromStr,
sync::{Arc, Mutex, RwLock}, sync::{Arc, Mutex},
}; };
use teloxide::{ use teloxide::{
dispatching::{dialogue::GetChatId, UpdateFilterExt}, dispatching::{dialogue::GetChatId, UpdateFilterExt},
dptree::{self, Handler}, dptree::{self, Handler},
prelude::DependencyMap, prelude::{DependencyMap, Requester},
types::{CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update}, types::{CallbackQuery, InlineKeyboardMarkup, Message, Update},
Bot, Bot,
}; };
use crate::{ use crate::{
botscript::{self, message_info::MessageInfoBuilder, BotMessage, RunnerConfig}, botscript::{self, message_info::MessageInfoBuilder, BotMessage, ScriptError},
commands::BotCommand, commands::BotCommand,
db::{CallDB, DB}, db::{callback_info::CallbackInfo, CallDB, DB},
message_answerer::MessageAnswerer, message_answerer::MessageAnswerer,
notify_admin, update_user_tg, BotError, BotResult, BotRuntime, notify_admin, update_user_tg,
utils::callback_button,
BotError, BotResult, BotRuntime,
}; };
pub type BotHandler = pub type BotHandler =
Handler<'static, DependencyMap, BotResult<()>, teloxide::dispatching::DpHandlerDescription>; Handler<'static, DependencyMap, BotResult<()>, teloxide::dispatching::DpHandlerDescription>;
type CallbackStore = CallbackInfo<Value>;
pub fn script_handler(r: Arc<Mutex<BotRuntime>>) -> BotHandler { pub fn script_handler(r: Arc<Mutex<BotRuntime>>) -> BotHandler {
let cr = r.clone(); let cr = r.clone();
dptree::entry() dptree::entry()
@ -48,14 +54,38 @@ pub fn script_handler(r: Arc<Mutex<BotRuntime>>) -> BotHandler {
) )
.branch( .branch(
Update::filter_callback_query() Update::filter_callback_query()
.filter_map(move |q: CallbackQuery| { .filter_map_async(move |q: CallbackQuery, mut db: DB| {
q.data.and_then(|data| { let r = Arc::clone(&cr);
let r = std::sync::Arc::clone(&cr); async move {
let data = match q.data {
Some(data) => data,
None => return None,
};
let ci = match CallbackStore::get(&mut db, &data).await {
Ok(ci) => ci,
Err(err) => {
notify_admin(&format!(
"Failed to get callback from CallbackInfo, err: {err}"
))
.await;
return None;
}
};
let ci = match ci {
Some(ci) => ci,
None => return None,
};
let data = match ci.literal {
Some(data) => data,
None => return None,
};
let r = r.lock().expect("RwLock lock on commands map failed"); let r = r.lock().expect("RwLock lock on commands map failed");
let rc = &r.rc; let rc = &r.rc;
rc.get_callback_message(&data) rc.get_callback_message(&data)
}) }
}) })
.endpoint(handle_callback), .endpoint(handle_callback),
) )
@ -78,7 +108,7 @@ async fn handle_botmessage(bot: Bot, mut db: DB, bm: BotMessage, msg: Message) -
Err(_) => None, Err(_) => None,
}; };
if bm.meta() == true { if bm.meta() {
if let Some(ref meta) = variant { if let Some(ref meta) = variant {
user.insert_meta(&mut db, meta).await?; user.insert_meta(&mut db, meta).await?;
}; };
@ -91,11 +121,11 @@ async fn handle_botmessage(bot: Bot, mut db: DB, bm: BotMessage, msg: Message) -
// falling back to propagation // falling back to propagation
None => break 'prop true, None => break 'prop true,
}; };
let jsuser = to_js(ctx, &tguser).unwrap(); let jsuser = to_js(ctx, &tguser).map_err(ScriptError::from)?;
let mi = MessageInfoBuilder::new() let mi = MessageInfoBuilder::new()
.set_variant(variant.clone()) .set_variant(variant.clone())
.build(); .build();
let mi = to_js(ctx, &mi).unwrap(); let mi = to_js(ctx, &mi).map_err(ScriptError::from)?;
info!( info!(
"Calling handler {:?} with msg literal: {:?}", "Calling handler {:?} with msg literal: {:?}",
handler, handler,
@ -126,35 +156,50 @@ async fn handle_botmessage(bot: Bot, mut db: DB, bm: BotMessage, msg: Message) -
return Ok(()); return Ok(());
} }
let buttons = bm let button_db = db.clone();
.resolve_buttons(&mut db) let buttons = bm.resolve_buttons(&mut db).await?.map(async |buttons| {
.await? join_all(buttons.iter().map(async |r| {
.map(|buttons| InlineKeyboardMarkup { join_all(r.iter().map(async |b| {
inline_keyboard: buttons match b {
.iter() botscript::ButtonLayout::Callback {
.map(|r| { name,
r.iter() literal: _,
.map(|b| match b { callback,
botscript::ButtonLayout::Callback { } => {
name, callback_button(
literal: _, name,
callback, callback.to_string(),
} => InlineKeyboardButton::callback(name, callback), None::<bool>,
}) &mut button_db.clone(),
.collect() )
}) .await
.collect(), }
}); }
}))
.await
.into_iter()
.collect::<Result<_, _>>()
}))
.await
.into_iter()
.collect::<Result<_, _>>()
});
let buttons = match buttons {
Some(b) => Some(InlineKeyboardMarkup {
inline_keyboard: b.await?,
}),
None => None,
};
let literal = bm.literal().map_or("", |s| s.as_str()); let literal = bm.literal().map_or("", |s| s.as_str());
let ma = MessageAnswerer::new(&bot, &mut db, msg.chat.id.0); let ma = MessageAnswerer::new(&bot, &mut db, msg.chat.id.0);
ma.answer(literal, variant.as_ref().map(|v| v.as_str()), buttons) ma.answer(literal, variant.as_deref(), buttons).await?;
.await?;
Ok(()) Ok(())
} }
async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery) -> BotResult<()> { async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery) -> BotResult<()> {
bot.answer_callback_query(&q.id).await?;
info!("Eval BM: {:?}", bm); info!("Eval BM: {:?}", bm);
let tguser = q.from.clone(); let tguser = q.from.clone();
let user = db let user = db
@ -163,7 +208,6 @@ async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery)
let user = update_user_tg(user, &tguser); let user = update_user_tg(user, &tguser);
user.update_user(&mut db).await?; user.update_user(&mut db).await?;
println!("Is handler set: {}", bm.get_handler().is_some());
let is_propagate: bool = match bm.get_handler() { let is_propagate: bool = match bm.get_handler() {
Some(handler) => 'prop: { Some(handler) => 'prop: {
let ctx = match handler.context() { let ctx = match handler.context() {
@ -171,17 +215,11 @@ async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery)
// falling back to propagation // falling back to propagation
None => break 'prop true, None => break 'prop true,
}; };
let jsuser = to_js(ctx, &tguser).unwrap(); let jsuser = to_js(ctx, &tguser).map_err(ScriptError::from)?;
let mi = MessageInfoBuilder::new().build(); let mi = MessageInfoBuilder::new().build();
let mi = to_js(ctx, &mi).unwrap(); let mi = to_js(ctx, &mi).map_err(ScriptError::from)?;
println!(
"Calling handler {:?} with msg literal: {:?}",
handler,
bm.literal()
);
match handler.call_args(vec![jsuser, mi]) { match handler.call_args(vec![jsuser, mi]) {
Ok(v) => { Ok(v) => {
println!("Ok branch, got value: {v:?}");
if v.is_bool() { if v.is_bool() {
v.to_bool().unwrap_or(true) v.to_bool().unwrap_or(true)
} else if v.is_int() { } else if v.is_int() {
@ -192,7 +230,6 @@ async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery)
} }
} }
Err(err) => { Err(err) => {
println!("ERR branch");
error!("Failed to get return of handler, err: {err}"); error!("Failed to get return of handler, err: {err}");
// falling back to propagation // falling back to propagation
true true
@ -206,25 +243,40 @@ async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery)
return Ok(()); return Ok(());
} }
let buttons = bm let button_db = db.clone();
.resolve_buttons(&mut db) let buttons = bm.resolve_buttons(&mut db).await?.map(async |buttons| {
.await? join_all(buttons.iter().map(async |r| {
.map(|buttons| InlineKeyboardMarkup { join_all(r.iter().map(async |b| {
inline_keyboard: buttons match b {
.iter() botscript::ButtonLayout::Callback {
.map(|r| { name,
r.iter() literal: _,
.map(|b| match b { callback,
botscript::ButtonLayout::Callback { } => {
name, callback_button(
literal: _, name,
callback, callback.to_string(),
} => InlineKeyboardButton::callback(name, callback), None::<bool>,
}) &mut button_db.clone(),
.collect() )
}) .await
.collect(), }
}); }
}))
.await
.into_iter()
.collect::<Result<_, _>>()
}))
.await
.into_iter()
.collect::<Result<_, _>>()
});
let buttons = match buttons {
Some(b) => Some(InlineKeyboardMarkup {
inline_keyboard: b.await?,
}),
None => None,
};
let literal = bm.literal().map_or("", |s| s.as_str()); let literal = bm.literal().map_or("", |s| s.as_str());
let (chat_id, msg_id) = { let (chat_id, msg_id) = {
@ -252,7 +304,7 @@ async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery)
Ok(msg_id) => { Ok(msg_id) => {
ma.replace_message(msg_id, literal, buttons).await?; ma.replace_message(msg_id, literal, buttons).await?;
} }
Err(err) => { Err(_) => {
ma.answer(literal, None, buttons).await?; ma.answer(literal, None, buttons).await?;
} }
}; };

View File

@ -1,28 +1,20 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
future::Future, future::Future,
sync::{Arc, Mutex, RwLock}, sync::{Arc, Mutex},
thread::JoinHandle, thread::JoinHandle,
time::Duration, time::Duration,
}; };
use lazy_static::lazy_static;
use log::{error, info}; use log::{error, info};
use teloxide::{ use teloxide::{dispatching::dialogue::serializer::Json, dptree, prelude::Dispatcher, Bot};
dispatching::dialogue::serializer::Json,
dptree,
prelude::{Dispatcher, Requester},
types::{ChatId, UserId},
Bot,
};
use tokio::runtime::Handle;
use crate::{ use crate::{
bot_handler::{script_handler, BotHandler}, bot_handler::{script_handler, BotHandler},
db::{bots::BotInstance, DbError, DB}, db::{bots::BotInstance, DbError, DB},
message_answerer::MessageAnswerer, message_answerer::MessageAnswerer,
mongodb_storage::MongodbStorage, mongodb_storage::MongodbStorage,
BotController, BotError, BotResult, BotRuntime, BotController, BotResult, BotRuntime,
}; };
pub struct BotRunner { pub struct BotRunner {
@ -78,7 +70,7 @@ where
} }
} }
pub async fn dispatch(mut self, db: &mut DB) -> ! { pub async fn dispatch(mut self, db: &mut DB) -> BotResult<()> {
loop { loop {
'biter: for bi in (self.bi_getter)().await { 'biter: for bi in (self.bi_getter)().await {
// removing handler to force restart // removing handler to force restart
@ -90,7 +82,7 @@ where
"Trying to restart bot `{}`, new script: {}", "Trying to restart bot `{}`, new script: {}",
bi.name, bi.script bi.name, bi.script
); );
let runner = self.bot_pool.remove(&bi.name); let _runner = self.bot_pool.remove(&bi.name);
}; };
// start, if not started // start, if not started
let mut bot_runner = match self.bot_pool.remove(&bi.name) { let mut bot_runner = match self.bot_pool.remove(&bi.name) {
@ -98,7 +90,7 @@ where
None => { None => {
let handlers = (self.h_mapper)(bi.clone()).await; let handlers = (self.h_mapper)(bi.clone()).await;
info!("NEW INSTANCE: Starting new instance! bot name: {}", bi.name); info!("NEW INSTANCE: Starting new instance! bot name: {}", bi.name);
self.start_bot(bi, db, handlers.collect()).await.unwrap(); self.start_bot(bi, db, handlers.collect()).await?;
continue 'biter; continue 'biter;
} }
}; };
@ -133,8 +125,7 @@ where
bot_runner.controller.db.clone(), bot_runner.controller.db.clone(),
handler, handler,
) )
.await .await?,
.unwrap(),
) )
} }
}; };
@ -204,7 +195,7 @@ pub async fn spawn_bot_thread(
// let rt = tokio::runtime::Builder::new_current_thread() // let rt = tokio::runtime::Builder::new_current_thread()
// .enable_all() // .enable_all()
// .build()?; // .build()?;
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new()?;
rt.block_on( rt.block_on(
Dispatcher::builder(bot, handler) Dispatcher::builder(bot, handler)
@ -223,19 +214,20 @@ pub async fn spawn_notificator_thread(
mut c: BotController, mut c: BotController,
) -> BotResult<JoinHandle<BotResult<()>>> { ) -> BotResult<JoinHandle<BotResult<()>>> {
let thread = std::thread::spawn(move || -> BotResult<()> { let thread = std::thread::spawn(move || -> BotResult<()> {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { rt.block_on(async {
loop { loop {
let r = c.runtime.lock().unwrap(); let notifications = {
let notifications = r.rc.get_nearest_notifications(); let r = c.runtime.lock().expect("Poisoned Runtime lock");
drop(r); // unlocking mutex r.rc.get_nearest_notifications()
};
match notifications { match notifications {
Some(n) => { Some(n) => {
// waiting time to send notification // waiting time to send notification
tokio::time::sleep(n.wait_for()).await; tokio::time::sleep(n.wait_for()).await;
'n: for n in n.notifications().into_iter() { 'n: for n in n.notifications().iter() {
for user in n.get_users(&c.db).await?.into_iter() { for user in n.get_users(&c.db).await?.into_iter() {
let text = match n.resolve_message(&c.db, &user).await? { let text = match n.resolve_message(&c.db, &user).await? {
Some(text) => text, Some(text) => text,

View File

@ -2,17 +2,17 @@ pub mod application;
pub mod db; pub mod db;
pub mod message_info; pub mod message_info;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError}; use std::sync::Mutex;
use std::time::Duration; use std::time::Duration;
use crate::db::raw_calls::RawCallError; use crate::db::raw_calls::RawCallError;
use crate::db::{CallDB, DbError, User, DB}; use crate::db::{CallDB, DbError, User, DB};
use crate::message_answerer::MessageAnswererError;
use crate::notify_admin; use crate::notify_admin;
use crate::utils::parcelable::{ParcelType, Parcelable, ParcelableError, ParcelableResult}; use crate::utils::parcelable::{ParcelType, Parcelable, ParcelableError, ParcelableResult};
use chrono::{DateTime, Days, NaiveTime, ParseError, TimeDelta, Timelike, Utc}; use chrono::{DateTime, Days, NaiveTime, ParseError, TimeDelta, Timelike, Utc};
use db::attach_db_obj; 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, to_js}; use quickjs_rusty::serde::{from_js, to_js};
use quickjs_rusty::utils::create_empty_object; use quickjs_rusty::utils::create_empty_object;
@ -46,6 +46,8 @@ pub enum ScriptError {
RawCallError(#[from] RawCallError), RawCallError(#[from] RawCallError),
#[error("error while locking mutex: {0:?}")] #[error("error while locking mutex: {0:?}")]
MutexError(String), MutexError(String),
#[error("can't send message to user to user: {0:?}")]
MAError(#[from] MessageAnswererError),
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -543,7 +545,8 @@ impl BotMessage {
pub fn update_defaults(self) -> Self { pub fn update_defaults(self) -> Self {
let bm = self; let bm = self;
// if message is `start`, defaulting meta to true, if not set // if message is `start`, defaulting meta to true, if not set
let bm = match bm.meta {
match bm.meta {
Some(_) => bm, Some(_) => bm,
None => match &bm.literal { None => match &bm.literal {
Some(l) if l == "start" => Self { Some(l) if l == "start" => Self {
@ -552,9 +555,7 @@ impl BotMessage {
}, },
_ => bm, _ => bm,
}, },
}; }
bm
} }
pub fn is_replace(&self) -> bool { pub fn is_replace(&self) -> bool {
@ -762,7 +763,11 @@ impl NotificationFilter {
NotificationFilter::Random { random } => Ok(db.get_random_users(*random).await?), NotificationFilter::Random { random } => Ok(db.get_random_users(*random).await?),
NotificationFilter::BotFunction(f) => { NotificationFilter::BotFunction(f) => {
let users = f.call()?; let users = f.call()?;
let users = from_js(f.context().unwrap(), &users)?; // we just called function, so context is definetly valid
let users = from_js(
f.context().expect("Context invalid after function call"),
&users,
)?;
Ok(users) Ok(users)
} }
} }
@ -770,7 +775,7 @@ impl NotificationFilter {
} }
impl Parcelable<BotFunction> for NotificationFilter { impl Parcelable<BotFunction> for NotificationFilter {
fn get_field(&mut self, name: &str) -> ParcelableResult<ParcelType<BotFunction>> { fn get_field(&mut self, _name: &str) -> ParcelableResult<ParcelType<BotFunction>> {
todo!() todo!()
} }
@ -800,7 +805,7 @@ pub enum NotificationMessage {
} }
impl Parcelable<BotFunction> for NotificationMessage { impl Parcelable<BotFunction> for NotificationMessage {
fn get_field(&mut self, name: &str) -> ParcelableResult<ParcelType<BotFunction>> { fn get_field(&mut self, _name: &str) -> ParcelableResult<ParcelType<BotFunction>> {
todo!() todo!()
} }
@ -822,9 +827,13 @@ impl NotificationMessage {
NotificationMessage::Literal { literal } => Ok(db.get_literal_value(literal).await?), NotificationMessage::Literal { literal } => Ok(db.get_literal_value(literal).await?),
NotificationMessage::Text { text } => Ok(Some(text.to_string())), NotificationMessage::Text { text } => Ok(Some(text.to_string())),
NotificationMessage::BotFunction(f) => { NotificationMessage::BotFunction(f) => {
let jsuser = to_js(f.context().expect("Function is not js"), user).unwrap(); let jsuser = to_js(f.context().expect("Function is not js"), user)?;
let text = f.call_args(vec![jsuser])?; let text = f.call_args(vec![jsuser])?;
let text = from_js(f.context().unwrap(), &text)?; let text = from_js(
f.context()
.expect("Context was not provided after function call"),
&text,
)?;
Ok(text) Ok(text)
} }
} }
@ -946,15 +955,19 @@ impl RunnerConfig {
} }
pub fn created_at(&self) -> DateTime<Utc> { pub fn created_at(&self) -> DateTime<Utc> {
self.created_at.at + TimeDelta::try_hours(self.config.timezone.into()).unwrap() self.timezoned_time(self.created_at.at)
}
pub fn timezoned_time(&self, dt: DateTime<Utc>) -> DateTime<Utc> {
dt + TimeDelta::try_hours(self.config.timezone.into())
.unwrap_or_else(|| TimeDelta::try_hours(0).expect("Timezone UTC+0 does not exists"))
} }
/// if None is returned, then garanteed that later calls will also return None, /// if None is returned, then garanteed that later calls will also return None,
/// so, if you'll get None, no notifications will be provided later /// so, if you'll get None, no notifications will be provided later
pub fn get_nearest_notifications(&self) -> Option<NotificationBlock> { pub fn get_nearest_notifications(&self) -> Option<NotificationBlock> {
let start_time = self.created_at(); let start_time = self.created_at();
let now = let now = self.timezoned_time(chrono::offset::Utc::now());
chrono::offset::Utc::now() + TimeDelta::try_hours(self.config.timezone.into()).unwrap();
let ordered = self let ordered = self
.notifications .notifications
@ -994,9 +1007,8 @@ impl Parcelable<BotFunction> for RunnerConfig {
} }
} }
#[derive(Clone)]
pub struct Runner { pub struct Runner {
context: Arc<Mutex<Context>>, context: Mutex<Context>,
} }
impl Runner { impl Runner {
@ -1010,31 +1022,22 @@ impl Runner {
})?; })?;
Ok(Runner { Ok(Runner {
context: Arc::new(Mutex::new(context)), context: Mutex::new(context),
}) })
} }
pub fn init_with_db(db: &mut DB) -> ScriptResult<Self> { pub fn init_with_db(db: &mut DB) -> ScriptResult<Self> {
let context = Context::new(None)?; let mut runner = Self::init()?;
let mut global = context.global()?; runner.call_attacher(|c, o| attach_db_obj(c, o, db))??;
attach_db_obj(&context, &mut global, db)?;
context.add_callback("print", |a: String| { Ok(runner)
print(a);
None::<bool>
})?;
Ok(Runner {
context: Arc::new(Mutex::new(context)),
})
} }
pub fn call_attacher<F, R>(&mut self, f: F) -> ScriptResult<R> pub fn call_attacher<F, R>(&mut self, f: F) -> ScriptResult<R>
where where
F: FnOnce(&Context, &mut OwnedJsObject) -> R, F: FnOnce(&Context, &mut OwnedJsObject) -> R,
{ {
let context = self.context.lock().unwrap(); let context = self.context.lock().expect("Can't lock context");
let mut global = context.global()?; let mut global = context.global()?;
let res = f(&context, &mut global); let res = f(&context, &mut global);
@ -1071,7 +1074,6 @@ impl Runner {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
#[allow(clippy::print_stdout)] #[allow(clippy::print_stdout)]
mod tests { mod tests {
use quickjs_rusty::{serde::from_js, OwnedJsObject};
use serde_json::json; use serde_json::json;
use super::*; use super::*;
@ -1117,22 +1119,6 @@ mod tests {
assert_eq!(sres, "cancelation"); assert_eq!(sres, "cancelation");
} }
fn recursive_format(o: OwnedJsObject) -> String {
let props: Vec<_> = o.properties_iter().unwrap().map(|x| x.unwrap()).collect();
let sp: Vec<String> = props
.into_iter()
.map(|v| {
if v.is_object() {
recursive_format(v.try_into_object().unwrap())
} else {
format!("{:?}", v)
}
})
.collect();
format!("{:?}", sp)
}
#[test] #[test]
fn test_run_script_invalid() { fn test_run_script_invalid() {
let runner = Runner::init().unwrap(); let runner = Runner::init().unwrap();

View File

@ -1,3 +1,5 @@
// just keeping track locks in asychronous calls
#![allow(clippy::await_holding_lock)]
use std::sync::RwLock; use std::sync::RwLock;
use log::info; use log::info;
@ -6,9 +8,9 @@ use teloxide::Bot;
use tokio::runtime::Handle; use tokio::runtime::Handle;
use crate::{ use crate::{
db::{application::Application, message_forward::MessageForward, CallDB, DB}, db::{application::Application, message_forward::MessageForward, DB},
message_answerer::MessageAnswerer, message_answerer::MessageAnswerer,
send_application_to_chat, BotError, send_application_to_chat,
}; };
use super::ScriptError; use super::ScriptError;
@ -20,39 +22,35 @@ pub fn attach_user_application(
bot: &Bot, bot: &Bot,
) -> Result<(), ScriptError> { ) -> Result<(), ScriptError> {
let db: std::sync::Arc<RwLock<DB>> = std::sync::Arc::new(RwLock::new(db.clone())); let db: std::sync::Arc<RwLock<DB>> = std::sync::Arc::new(RwLock::new(db.clone()));
let dbbox = Box::new(db.clone());
let db: &'static _ = Box::leak(dbbox);
let bot: std::sync::Arc<RwLock<Bot>> = std::sync::Arc::new(RwLock::new(bot.clone())); let bot: std::sync::Arc<RwLock<Bot>> = std::sync::Arc::new(RwLock::new(bot.clone()));
let botbox = Box::new(bot.clone());
let bot: &'static _ = Box::leak(botbox);
let user_application = let user_application =
c.create_callback(move |q: OwnedJsObject| -> Result<_, ScriptError> { c.create_callback(move |q: OwnedJsObject| -> Result<_, ScriptError> {
println!("user_application is called"); let bot1 = bot.clone();
let db = db.clone(); let bot1 = bot1.read().expect("Can't read lock bot");
let bot2 = bot.read().expect("Can't read lock bot");
let user: teloxide::types::User = match from_js(q.context(), &q) { let user: teloxide::types::User = match from_js(q.context(), &q) {
Ok(q) => q, Ok(q) => q,
Err(_) => todo!(), Err(_) => todo!(),
}; };
let application = futures::executor::block_on( let application = futures::executor::block_on(
Application::new(user.clone()).store_db(&mut db.write().unwrap()), Application::new(user.clone())
.store_db(&mut db.write().expect("Can't write lock db")),
)?; )?;
println!("there1");
let db2 = db.clone(); let db2 = db.clone();
let msg = tokio::task::block_in_place(move || { let msg = tokio::task::block_in_place(move || {
Handle::current().block_on(async move { Handle::current().block_on(async move {
send_application_to_chat( send_application_to_chat(
&bot.read().unwrap(), &bot1,
&mut db2.write().unwrap(), &mut db2.write().expect("Can't write lock db"),
&application, &application,
) )
.await .await
}) })
}); });
println!("there2");
let msg = match msg { let msg = match msg {
Ok(msg) => msg, Ok(msg) => msg,
Err(err) => { Err(err) => {
@ -63,19 +61,16 @@ pub fn attach_user_application(
let (chat_id, msg_id) = futures::executor::block_on( let (chat_id, msg_id) = futures::executor::block_on(
MessageAnswerer::new( MessageAnswerer::new(
&bot.read().unwrap(), &bot2,
&mut db.write().unwrap(), &mut db.write().expect("Can't write lock db"),
user.id.0 as i64, user.id.0 as i64,
) )
.answer("left_application_msg", None, None), .answer("left_application_msg", None, None),
) )?;
.unwrap();
println!("there3");
futures::executor::block_on( futures::executor::block_on(
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_db(&mut db.write().unwrap()), .store_db(&mut db.write().expect("Can't write lock db")),
)?; )?;
println!("there4");
let ret = true; let ret = true;
Ok(ret) Ok(ret)

View File

@ -16,16 +16,14 @@ pub fn attach_db_obj(c: &Context, o: &mut OwnedJsObject, db: &DB) -> Result<(),
.expect("the created object was not an 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 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( let find_one = c.create_callback(
|collection: String, q: OwnedJsObject| -> Result<_, ScriptError> { move |collection: String, q: OwnedJsObject| -> Result<_, ScriptError> {
// let db = db.clone();
let query: serde_json::Value = match from_js(q.context(), &q) { let query: serde_json::Value = match from_js(q.context(), &q) {
Ok(q) => q, Ok(q) => q,
Err(_) => todo!(), Err(_) => todo!(),
}; };
let db = db.clone();
let value = futures::executor::block_on( let value = futures::executor::block_on(
db.write() db.write()

View File

@ -9,6 +9,12 @@ pub struct MessageInfoBuilder {
inner: MessageInfo, inner: MessageInfo,
} }
impl Default for MessageInfoBuilder {
fn default() -> Self {
Self::new()
}
}
impl MessageInfoBuilder { impl MessageInfoBuilder {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View File

@ -67,6 +67,7 @@ impl BotCommand {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,7 +1,5 @@
use bson::doc; use bson::doc;
use bson::oid::ObjectId;
use chrono::{DateTime, FixedOffset, Local}; use chrono::{DateTime, FixedOffset, Local};
use futures::StreamExt;
use futures::TryStreamExt; use futures::TryStreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -45,19 +43,23 @@ impl BotInstance {
Ok(self) Ok(self)
}); });
pub async fn get_all<D: CallDB>(db: &mut D) -> DbResult<Vec<Self>> { pub async fn get_all<D: GetCollection>(db: &mut D) -> DbResult<Vec<Self>> {
let bi = db.get_collection::<Self>().await; let bi = db.get_collection::<Self>().await;
Ok(bi.find(doc! {}).await?.try_collect().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>> { pub async fn get_by_name<D: GetCollection>(db: &mut D, name: &str) -> DbResult<Option<Self>> {
let bi = db.get_collection::<Self>().await; let bi = db.get_collection::<Self>().await;
Ok(bi.find_one(doc! {"name": name}).await?) Ok(bi.find_one(doc! {"name": name}).await?)
} }
pub async fn restart_one<D: CallDB>(db: &mut D, name: &str, restart: bool) -> DbResult<()> { pub async fn restart_one<D: GetCollection>(
db: &mut D,
name: &str,
restart: bool,
) -> DbResult<()> {
let bi = db.get_collection::<Self>().await; let bi = db.get_collection::<Self>().await;
bi.update_one( bi.update_one(
@ -68,7 +70,7 @@ impl BotInstance {
Ok(()) Ok(())
} }
pub async fn restart_all<D: CallDB>(db: &mut D, restart: bool) -> DbResult<()> { pub async fn restart_all<D: GetCollection>(db: &mut D, restart: bool) -> DbResult<()> {
let bi = db.get_collection::<Self>().await; let bi = db.get_collection::<Self>().await;
bi.update_many(doc! {}, doc! { "$set": { "restart_flag": restart } }) bi.update_many(doc! {}, doc! { "$set": { "restart_flag": restart } })
@ -76,7 +78,11 @@ impl BotInstance {
Ok(()) Ok(())
} }
pub async fn update_script<D: CallDB>(db: &mut D, name: &str, script: &str) -> DbResult<()> { pub async fn update_script<D: GetCollection>(
db: &mut D,
name: &str,
script: &str,
) -> DbResult<()> {
let bi = db.get_collection::<Self>().await; let bi = db.get_collection::<Self>().await;
bi.update_one( bi.update_one(

View File

@ -7,11 +7,10 @@ pub mod raw_calls;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, FixedOffset, Local, Utc}; use chrono::{DateTime, Local, Utc};
use enum_stringify::EnumStringify; use enum_stringify::EnumStringify;
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
use futures::StreamExt;
use mongodb::bson::serde_helpers::chrono_datetime_as_bson_datetime; use mongodb::bson::serde_helpers::chrono_datetime_as_bson_datetime;
use mongodb::options::IndexOptions; use mongodb::options::IndexOptions;
use mongodb::{bson::doc, options::ClientOptions, Client}; use mongodb::{bson::doc, options::ClientOptions, Client};
@ -57,7 +56,7 @@ macro_rules! query_call {
#[macro_export] #[macro_export]
macro_rules! query_call_consume { macro_rules! query_call_consume {
($func_name:ident, $self:ident, $db:ident, $return_type:ty, $body:block) => { ($func_name:ident, $self:ident, $db:ident, $return_type:ty, $body:block) => {
pub async fn $func_name<D: CallDB>($self, $db: &mut D) pub async fn $func_name<D: $crate::db::GetCollection + CallDB>($self, $db: &mut D)
-> DbResult<$return_type> $body -> DbResult<$return_type> $body
}; };
} }
@ -207,6 +206,7 @@ pub trait DbCollection {
const COLLECTION: &str; const COLLECTION: &str;
} }
#[async_trait]
pub trait GetCollection { pub trait GetCollection {
async fn get_collection<C: DbCollection + Send + Sync>(&mut self) -> Collection<C>; async fn get_collection<C: DbCollection + Send + Sync>(&mut self) -> Collection<C>;
} }
@ -222,7 +222,8 @@ impl CallDB for DB {
} }
} }
impl<T: CallDB> GetCollection for T { #[async_trait]
impl<T: CallDB + Send> GetCollection for T {
async fn get_collection<C: DbCollection + Send + Sync>(&mut self) -> Collection<C> { async fn get_collection<C: DbCollection + Send + Sync>(&mut self) -> Collection<C> {
self.get_database() self.get_database()
.await .await

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use mongodb::Database; use mongodb::Database;
use super::CallDB; use super::CallDB;
@ -14,6 +15,7 @@ pub enum RawCallError {
} }
pub type RawCallResult<T> = Result<T, RawCallError>; pub type RawCallResult<T> = Result<T, RawCallError>;
#[async_trait]
pub trait RawCall { pub trait RawCall {
async fn get_database(&mut self) -> Database; async fn get_database(&mut self) -> Database;
async fn find_one(&mut self, collection: &str, query: Value) -> RawCallResult<Option<Value>> { async fn find_one(&mut self, collection: &str, query: Value) -> RawCallResult<Option<Value>> {
@ -31,7 +33,8 @@ pub trait RawCall {
} }
} }
impl<T: CallDB> RawCall for T { #[async_trait]
impl<T: CallDB + Send> RawCall for T {
async fn get_database(&mut self) -> Database { async fn get_database(&mut self) -> Database {
CallDB::get_database(self).await CallDB::get_database(self).await
} }

View File

@ -177,7 +177,7 @@ async fn test_drop_media_except() {
#[tokio::test] #[tokio::test]
async fn test_get_random_users() { async fn test_get_random_users() {
let mut db = setup_db().await; let db = setup_db().await;
let users = db.get_random_users(1).await.unwrap(); let users = db.get_random_users(1).await.unwrap();
assert_eq!(users.len(), 1); assert_eq!(users.len(), 1);

View File

@ -1,5 +1,3 @@
use std::str::FromStr;
use itertools::Itertools; use itertools::Itertools;
use log::{info, warn}; use log::{info, warn};
use std::time::Duration; use std::time::Duration;
@ -19,7 +17,7 @@ use crate::db::bots::BotInstance;
use crate::db::message_forward::MessageForward; use crate::db::message_forward::MessageForward;
use crate::db::{CallDB, DB}; use crate::db::{CallDB, DB};
use crate::mongodb_storage::MongodbStorage; use crate::mongodb_storage::MongodbStorage;
use crate::{BotDialogue, BotError, BotResult, CallbackStore, State}; use crate::{notify_admin, BotDialogue, BotError, BotResult, CallbackStore, State};
pub fn admin_handler() -> BotHandler { pub fn admin_handler() -> BotHandler {
dptree::entry() dptree::entry()
@ -105,18 +103,28 @@ async fn newscript_handler(bot: Bot, mut db: DB, msg: Message, name: String) ->
let mut stream = bot.download_file_stream(&file.path); let mut stream = bot.download_file_stream(&file.path);
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
while let Some(bytes) = stream.next().await { while let Some(bytes) = stream.next().await {
let mut bytes = bytes.unwrap().to_vec(); let mut bytes = match bytes {
Ok(bytes) => bytes.to_vec(),
Err(err) => {
notify_admin(&format!(
"Failed to download file: {}, err: {err}",
file.path
))
.await;
return Ok(());
}
};
buf.append(&mut bytes); buf.append(&mut bytes);
} }
let script = match String::from_utf8(buf) {
match String::from_utf8(buf) {
Ok(s) => s, Ok(s) => s,
Err(err) => { Err(err) => {
warn!("Failed to parse buf to string, 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?; bot.send_message(msg.chat.id, format!("Failed to Convert file to script: file is not UTF-8, err: {err}")).await?;
return Ok(()); return Ok(());
} }
}; }
script
} }
_ => todo!(), _ => todo!(),
} }
@ -129,7 +137,7 @@ async fn newscript_handler(bot: Bot, mut db: DB, msg: Message, name: String) ->
None => { None => {
bot.send_message( bot.send_message(
msg.chat.id, msg.chat.id,
format!("Failed to set script, possibly bots name is incorrent"), "Failed to set script, possibly bots name is incorrent".to_string(),
) )
.await?; .await?;
return Ok(()); return Ok(());

View File

@ -11,16 +11,14 @@ pub mod utils;
use bot_manager::BotManager; use bot_manager::BotManager;
use botscript::application::attach_user_application; use botscript::application::attach_user_application;
use botscript::{BotMessage, Runner, RunnerConfig, ScriptError, ScriptResult}; use botscript::{Runner, RunnerConfig, ScriptError, ScriptResult};
use db::application::Application; use db::application::Application;
use db::bots::BotInstance; use db::bots::BotInstance;
use db::callback_info::CallbackInfo; use db::callback_info::CallbackInfo;
use db::message_forward::MessageForward;
use handlers::admin::admin_handler; use handlers::admin::admin_handler;
use log::{error, info, warn}; use log::{error, info};
use message_answerer::MessageAnswerer; use message_answerer::MessageAnswererError;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex};
use utils::create_callback_button;
use crate::db::{CallDB, DB}; use crate::db::{CallDB, DB};
use crate::mongodb_storage::MongodbStorage; use crate::mongodb_storage::MongodbStorage;
@ -29,13 +27,8 @@ use db::DbError;
use envconfig::Envconfig; use envconfig::Envconfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use teloxide::dispatching::dialogue::serializer::Json; use teloxide::dispatching::dialogue::serializer::Json;
use teloxide::dispatching::dialogue::{GetChatId, Serializer}; use teloxide::dispatching::dialogue::Serializer;
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup}; use teloxide::prelude::*;
use teloxide::{
payloads::SendMessageSetters,
prelude::*,
utils::{command::BotCommands, render::RenderMessageTextHelper},
};
type BotDialogue = Dialogue<State, MongodbStorage<Json>>; type BotDialogue = Dialogue<State, MongodbStorage<Json>>;
@ -53,15 +46,6 @@ pub struct Config {
pub bot_name: String, pub bot_name: String,
} }
#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase")]
enum UserCommands {
/// The first message of user
Start(String),
/// Shows this message.
Help,
}
trait LogMsg { trait LogMsg {
fn log(self) -> Self; fn log(self) -> Self;
} }
@ -162,6 +146,7 @@ pub enum BotError {
ScriptError(#[from] ScriptError), ScriptError(#[from] ScriptError),
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
RwLockError(String), RwLockError(String),
MAError(#[from] MessageAnswererError),
} }
pub type BotResult<T> = Result<T, BotError>; pub type BotResult<T> = Result<T, BotError>;
@ -181,6 +166,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut db = DB::init(&config.db_url, config.bot_name.to_owned()).await?; let mut db = DB::init(&config.db_url, config.bot_name.to_owned()).await?;
BotInstance::restart_all(&mut db, false).await?; BotInstance::restart_all(&mut db, false).await?;
// if we can't get info for main bot, we should stop anyway
#[allow(clippy::unwrap_used)]
let bm = BotManager::with( let bm = BotManager::with(
async || { async || {
let config = config.clone(); let config = config.clone();
@ -197,167 +184,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
BotInstance::restart_all(&mut db, false).await.unwrap(); BotInstance::restart_all(&mut db, false).await.unwrap();
std::iter::once(bi).chain(instances) std::iter::once(bi).chain(instances)
}, },
async |bi| vec![admin_handler()].into_iter(), async |_| vec![admin_handler()].into_iter(),
); );
bm.dispatch(&mut db).await; bm.dispatch(&mut db).await?;
}
async fn botscript_command_handler(
bot: Bot,
mut db: DB,
bm: BotMessage,
msg: Message,
) -> BotResult<()> {
info!("Eval BM: {:?}", bm);
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 callback_handler(bot: Bot, mut db: DB, q: CallbackQuery) -> BotResult<()> {
bot.answer_callback_query(&q.id).await?;
let data = match q.data {
Some(ref data) => data,
None => {
// not really our case to handle
return Ok(());
}
};
let callback = match CallbackStore::get_callback(&mut db, data).await? {
Some(callback) => callback,
None => {
warn!("Not found callback for data: {data}");
// doing this silently beacuse end user shouldn't know about backend internal data
return Ok(());
}
};
match callback {
Callback::MoreInfo => {
let keyboard = Some(single_button_markup!(
create_callback_button("go_home", Callback::GoHome, &mut db).await?
));
let chat_id = q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64);
let message_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),
)?;
MessageAnswerer::new(&bot, &mut db, chat_id)
.replace_message(message_id, "more_info_msg", keyboard)
.await?
}
Callback::ProjectPage { id } => {
let nextproject = match db
.get_literal_value(&format!("project_{}_msg", id + 1))
.await?
.unwrap_or("emptyproject".into())
.as_str()
{
"end" | "empty" | "none" => None,
_ => Some(
create_callback_button(
"next_project",
Callback::ProjectPage { id: id + 1 },
&mut db,
)
.await?,
),
};
let prevproject = match id.wrapping_sub(1) {
0 => None,
_ => Some(
create_callback_button(
"prev_project",
Callback::ProjectPage {
id: id.wrapping_sub(1),
},
&mut db,
)
.await?,
),
};
let keyboard = buttons_markup!(
[prevproject, nextproject].into_iter().flatten(),
[create_callback_button("go_home", Callback::GoHome, &mut db).await?]
);
let chat_id = q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64);
let message_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),
)?;
MessageAnswerer::new(&bot, &mut db, chat_id)
.replace_message(message_id, &format!("project_{}_msg", id), Some(keyboard))
.await?
}
Callback::GoHome => {
let keyboard = make_start_buttons(&mut db).await?;
let chat_id = q.chat_id().map(|i| i.0).unwrap_or(q.from.id.0 as i64);
let message_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),
)?;
MessageAnswerer::new(&bot, &mut db, chat_id)
.replace_message(message_id, "start", Some(keyboard))
.await?
}
Callback::LeaveApplication => {
let application = Application::new(q.from.clone()).store(&mut db).await?;
let msg = send_application_to_chat(&bot, &mut db, &application).await?;
let (chat_id, msg_id) = MessageAnswerer::new(&bot, &mut db, q.from.id.0 as i64)
.answer("left_application_msg", None, None)
.await?;
MessageForward::new(msg.chat.id.0, msg.id.0, chat_id, msg_id, false)
.store(&mut db)
.await?;
}
Callback::AskQuestion => {
MessageAnswerer::new(&bot, &mut db, q.from.id.0 as i64)
.answer("ask_question_msg", None, None)
.await?;
}
};
Ok(()) Ok(())
} }
@ -385,9 +215,9 @@ async fn send_application_to_chat(
app.from app.from
)) ))
.await; .await;
return Err(BotError::AdminMisconfiguration(format!( return Err(BotError::AdminMisconfiguration(
"admin forget to set support_chat_id" "admin forget to set support_chat_id".to_string(),
))); ));
} }
}; };
let msg = match db.get_literal_value("application_format").await? { let msg = match db.get_literal_value("application_format").await? {
@ -403,9 +233,9 @@ async fn send_application_to_chat(
), ),
None => { None => {
notify_admin("format for support_chat_id is not set").await; notify_admin("format for support_chat_id is not set").await;
return Err(BotError::AdminMisconfiguration(format!( return Err(BotError::AdminMisconfiguration(
"admin forget to set application_format" "admin forget to set application_format".to_string(),
))); ));
} }
}; };
@ -431,76 +261,6 @@ async fn notify_admin(text: &str) {
} }
} }
async fn user_command_handler(
mut db: DB,
bot: Bot,
msg: Message,
cmd: UserCommands,
) -> BotResult<()> {
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?;
info!(
"MSG: {}",
msg.html_text().unwrap_or("|EMPTY_MESSAGE|".into())
);
match cmd {
UserCommands::Start(meta) => {
if !meta.is_empty() {
user.insert_meta(&mut db, &meta).await?;
}
let variant = match meta.as_str() {
"" => None,
variant => Some(variant),
};
let mut db2 = db.clone();
MessageAnswerer::new(&bot, &mut db, msg.chat.id.0)
.answer("start", variant, Some(make_start_buttons(&mut db2).await?))
.await?;
Ok(())
}
UserCommands::Help => {
bot.send_message(msg.chat.id, UserCommands::descriptions().to_string())
.await?;
Ok(())
}
}
}
async fn make_start_buttons(db: &mut DB) -> BotResult<InlineKeyboardMarkup> {
let mut buttons: Vec<Vec<InlineKeyboardButton>> = Vec::new();
buttons.push(vec![
create_callback_button("show_projects", Callback::ProjectPage { id: 1 }, db).await?,
]);
buttons.push(vec![
create_callback_button("more_info", Callback::MoreInfo, db).await?,
]);
buttons.push(vec![
create_callback_button("leave_application", Callback::LeaveApplication, db).await?,
]);
buttons.push(vec![
create_callback_button("ask_question", Callback::AskQuestion, db).await?,
]);
Ok(InlineKeyboardMarkup::new(buttons))
}
async fn echo(bot: Bot, msg: Message) -> BotResult<()> {
if let Some(photo) = msg.photo() {
info!("File ID: {}", photo[0].file.id);
}
bot.send_message(msg.chat.id, msg.html_text().unwrap_or("UNWRAP".into()))
.parse_mode(teloxide::types::ParseMode::Html)
.await?;
Ok(())
}
fn update_user_tg(user: db::User, tguser: &teloxide::types::User) -> db::User { fn update_user_tg(user: db::User, tguser: &teloxide::types::User) -> db::User {
db::User { db::User {
first_name: tguser.first_name.clone(), first_name: tguser.first_name.clone(),

View File

@ -8,10 +8,10 @@ use teloxide::{
Bot, Bot,
}; };
use crate::db::Media; use crate::db::{DbError, DbResult, Media};
use crate::{ use crate::{
db::{CallDB, DB}, db::{CallDB, DB},
notify_admin, BotResult, notify_admin,
}; };
macro_rules! send_media { macro_rules! send_media {
@ -40,6 +40,16 @@ pub struct MessageAnswerer<'a> {
db: &'a mut DB, db: &'a mut DB,
} }
#[derive(thiserror::Error, Debug)]
pub enum MessageAnswererError {
#[error("Failed request to DB: {0:?}")]
DbError(#[from] DbError),
#[error("Failed teloxide request: {0:?}")]
RequestError(#[from] teloxide::RequestError),
}
pub type MAResult<T> = Result<T, MessageAnswererError>;
impl<'a> MessageAnswerer<'a> { impl<'a> MessageAnswerer<'a> {
pub fn new(bot: &'a Bot, db: &'a mut DB, chat_id: i64) -> Self { pub fn new(bot: &'a Bot, db: &'a mut DB, chat_id: i64) -> Self {
Self { bot, chat_id, db } Self { bot, chat_id, db }
@ -50,7 +60,7 @@ impl<'a> MessageAnswerer<'a> {
literal: &str, literal: &str,
variant: Option<&str>, variant: Option<&str>,
is_replace: bool, is_replace: bool,
) -> BotResult<String> { ) -> DbResult<String> {
let variant_text = match variant { let variant_text = match variant {
Some(variant) => { Some(variant) => {
let value = self let value = self
@ -81,7 +91,7 @@ impl<'a> MessageAnswerer<'a> {
literal: &str, literal: &str,
variant: Option<&str>, variant: Option<&str>,
keyboard: Option<InlineKeyboardMarkup>, keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> { ) -> MAResult<(i64, i32)> {
let text = self.get_text(literal, variant, false).await?; let text = self.get_text(literal, variant, false).await?;
self.answer_inner(text, literal, variant, keyboard).await self.answer_inner(text, literal, variant, keyboard).await
} }
@ -90,8 +100,10 @@ impl<'a> MessageAnswerer<'a> {
self, self,
text: String, text: String,
keyboard: Option<InlineKeyboardMarkup>, keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> { ) -> MAResult<(i64, i32)> {
self.send_message(text, keyboard).await self.send_message(text, keyboard)
.await
.map_err(MessageAnswererError::from)
} }
async fn answer_inner( async fn answer_inner(
@ -100,7 +112,7 @@ impl<'a> MessageAnswerer<'a> {
literal: &str, literal: &str,
variant: Option<&str>, variant: Option<&str>,
keyboard: Option<InlineKeyboardMarkup>, keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> { ) -> MAResult<(i64, i32)> {
let media = self.db.get_media(literal).await?; let media = self.db.get_media(literal).await?;
let (chat_id, msg_id) = match media.len() { let (chat_id, msg_id) = match media.len() {
// just a text // just a text
@ -119,7 +131,7 @@ impl<'a> MessageAnswerer<'a> {
message_id: i32, message_id: i32,
literal: &str, literal: &str,
keyboard: Option<InlineKeyboardMarkup>, keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<()> { ) -> MAResult<()> {
let variant = self let variant = self
.db .db
.get_message(self.chat_id, message_id) .get_message(self.chat_id, message_id)
@ -127,7 +139,7 @@ impl<'a> MessageAnswerer<'a> {
.and_then(|m| m.variant); .and_then(|m| m.variant);
let text = self.get_text(literal, variant.as_deref(), true).await?; let text = self.get_text(literal, variant.as_deref(), true).await?;
let media = self.db.get_media(literal).await?; let media = self.db.get_media(literal).await?;
let (chat_id, msg_id) = match media.len() { let (_, msg_id) = match media.len() {
// just a text // just a text
0 => { 0 => {
let msg = let msg =
@ -203,7 +215,7 @@ impl<'a> MessageAnswerer<'a> {
message_id: i32, message_id: i32,
literal: &str, literal: &str,
variant: Option<&str>, variant: Option<&str>,
) -> BotResult<()> { ) -> DbResult<()> {
match variant { match variant {
Some(variant) => { Some(variant) => {
self.db self.db
@ -224,7 +236,7 @@ impl<'a> MessageAnswerer<'a> {
&self, &self,
text: String, text: String,
keyboard: Option<InlineKeyboardMarkup>, keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> { ) -> Result<(i64, i32), teloxide::RequestError> {
let msg = self.bot.send_message(ChatId(self.chat_id), text); let msg = self.bot.send_message(ChatId(self.chat_id), text);
let msg = match keyboard { let msg = match keyboard {
Some(kbd) => msg.reply_markup(kbd), Some(kbd) => msg.reply_markup(kbd),
@ -242,7 +254,7 @@ impl<'a> MessageAnswerer<'a> {
media: &Media, media: &Media,
text: String, text: String,
keyboard: Option<InlineKeyboardMarkup>, keyboard: Option<InlineKeyboardMarkup>,
) -> BotResult<(i64, i32)> { ) -> Result<(i64, i32), teloxide::RequestError> {
match media.media_type.as_str() { match media.media_type.as_str() {
"photo" => { "photo" => {
send_media!( send_media!(
@ -270,7 +282,11 @@ impl<'a> MessageAnswerer<'a> {
} }
} }
async fn send_media_group(&self, media: Vec<Media>, text: String) -> BotResult<(i64, i32)> { async fn send_media_group(
&self,
media: Vec<Media>,
text: String,
) -> Result<(i64, i32), teloxide::RequestError> {
let media: Vec<InputMedia> = media let media: Vec<InputMedia> = media
.into_iter() .into_iter()
.enumerate() .enumerate()

View File

@ -66,9 +66,26 @@ where
)) ))
} }
pub async fn callback_button<C, D>(
name: &str,
callback_name: String,
callback_data: C,
db: &mut D,
) -> BotResult<InlineKeyboardButton>
where
C: Serialize + for<'a> Deserialize<'a> + Send + Sync,
D: CallDB + Send + Sync,
{
let ci = CallbackInfo::new_with_literal(callback_data, callback_name)
.store(db)
.await?;
Ok(InlineKeyboardButton::callback(name, ci.get_id()))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use teloxide::types::InlineKeyboardButton; use teloxide::types::InlineKeyboardButton;
use teloxide::types::InlineKeyboardMarkup; use teloxide::types::InlineKeyboardMarkup;