Compare commits

...

11 Commits

Author SHA1 Message Date
Akulij
adad94ad43 update mainbot.js logic 2025-05-31 13:47:46 +05:00
Akulij
714853730a create /cancel command for users 2025-05-31 13:47:14 +05:00
Akulij
b980a653cb attach user_application to runner 2025-05-31 13:46:54 +05:00
Akulij
b8d07d0ad5 create MessageForward.store_db to store by DB type
reason: easier to use with RwLock
2025-05-31 13:45:45 +05:00
Akulij
2e447e87fd create Application.store_db to store by DB type 2025-05-31 13:45:02 +05:00
Akulij
d749b57811 call js function handler if set in script_handler 2025-05-31 13:43:41 +05:00
Akulij
a106891050 create call_attacher for Runner to attach global objects on initialization 2025-05-31 13:42:54 +05:00
Akulij
9cd0765030 create BotMessage js function handler getter 2025-05-31 13:42:10 +05:00
Akulij
bc46e0fda4 create BotFunction context getter 2025-05-31 13:41:42 +05:00
Akulij
845071c800 create botscript application module for js's runtime user_application function 2025-05-31 13:39:17 +05:00
Akulij
c936ea38a9 update mainbot.js 2025-05-31 11:04:35 +05:00
8 changed files with 264 additions and 36 deletions

View File

@ -1,21 +1,32 @@
// db - is set globally
const PROJECTS_COUNT = 2
const start_msg = {
buttons: [
[{ name: { literal: "show_projects" }, callback_name: "project_0" }],
[{ name: { literal: "more_info_btn" }, callback_name: "more_info" }],
[{ name: { literal: "leave_application" }, callback_name: "leave_application" }],
[{ name: { literal: "ask_question_btn" }, callback_name: "ask_question" }],
], // default is `null`
replace: true,
state: "start"
};
const dialog = {
commands: {
start: {
buttons: start_buttons, // default is `null`
state: "start"
},
cancel: {
buttons: [
[{name: {name: "Def"}, callback_name: "defcall"}]
],
state: "none"
},
somecomplicatedcmd: {}
start: start_msg,
},
buttons: {
more_info: {},
more_info: {
buttons: [
[{ name: { name: "На главную" }, callback_name: "start" }],
]
},
start: start_msg,
leave_application: {
handler: leave_application
},
ask_question: {}
},
stateful_msg_handlers: {
start: {}, // everything is by default, so just send message `start`
@ -29,36 +40,43 @@ const dialog = {
},
}
function enter_name() {}
function leave_application(user) {
print(JSON.stringify(user))
user_application(user)
return false
}
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 add_project_callbacks(point) {
for (const i of Array(PROJECTS_COUNT).keys()) {
buttons = [
[],
[{ name: { name: "На главную" }, callback_name: "start" }]
]
if (i > 0) {
buttons[0].push({ name: { literal: "prev_project" }, callback_name: `project_${i - 1}` })
}
if (i < PROJECTS_COUNT - 1) {
buttons[0].push({ name: { literal: "next_project" }, callback_name: `project_${i + 1}` })
}
function start_buttons() {
const now = new Date();
const dateFormated = formatDate(now);
// return 1
return [
[{name: {name: dateFormated}, callback_name: "no"}],
[{name: {name: "Hello!"}, callback_name: "no"}],
]
point[`project_${i}`] = {
replace: true,
buttons: buttons
}
}
}
add_project_callbacks(dialog.buttons)
print(JSON.stringify(dialog.buttons))
const config = {
version: 1.1,
}
// {config, dialog}
const c = {config: config, dialog: dialog}
const c = { config: config, dialog: dialog }
c

View File

@ -1,4 +1,5 @@
use log::info;
use log::{error, info};
use quickjs_rusty::serde::to_js;
use std::{
str::FromStr,
sync::{Arc, RwLock},
@ -66,6 +67,39 @@ async fn handle_botmessage(bot: Bot, mut db: DB, bm: BotMessage, msg: Message) -
let user = update_user_tg(user, &tguser);
user.update_user(&mut db).await?;
let is_propagate: bool = match bm.get_handler() {
Some(handler) => 'prop: {
let ctx = match handler.context() {
Some(ctx) => ctx,
// falling back to propagation
None => break 'prop true,
};
let jsuser = to_js(ctx, &tguser).unwrap();
match handler.call_args(vec![jsuser]) {
Ok(v) => {
if v.is_bool() {
v.to_bool().unwrap_or(true)
} else if v.is_int() {
v.to_int().unwrap_or(1) != 0
} else {
// falling back to propagation
true
}
}
Err(err) => {
error!("Failed to get return of handler, err: {err}");
// falling back to propagation
true
}
}
}
None => true,
};
if !is_propagate {
return Ok(());
}
let buttons = bm
.resolve_buttons(&mut db)
.await?
@ -102,6 +136,39 @@ async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery)
let user = update_user_tg(user, &tguser);
user.update_user(&mut db).await?;
let is_propagate: bool = match bm.get_handler() {
Some(handler) => 'prop: {
let ctx = match handler.context() {
Some(ctx) => ctx,
// falling back to propagation
None => break 'prop true,
};
let jsuser = to_js(ctx, &tguser).unwrap();
match handler.call_args(vec![jsuser]) {
Ok(v) => {
if v.is_bool() {
v.to_bool().unwrap_or(true)
} else if v.is_int() {
v.to_int().unwrap_or(1) != 0
} else {
// falling back to propagation
true
}
}
Err(err) => {
error!("Failed to get return of handler, err: {err}");
// falling back to propagation
true
}
}
}
None => true,
};
if !is_propagate {
return Ok(());
}
let buttons = bm
.resolve_buttons(&mut db)
.await?

View File

@ -1,3 +1,4 @@
pub mod application;
pub mod db;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError};
@ -12,12 +13,12 @@ use itertools::Itertools;
use quickjs_rusty::serde::from_js;
use quickjs_rusty::utils::create_empty_object;
use quickjs_rusty::utils::create_string;
use quickjs_rusty::Context;
use quickjs_rusty::ContextError;
use quickjs_rusty::ExecutionError;
use quickjs_rusty::JsFunction;
use quickjs_rusty::OwnedJsValue as JsValue;
use quickjs_rusty::ValueError;
use quickjs_rusty::{Context, OwnedJsObject};
use serde::Deserialize;
use serde::Serialize;
@ -121,6 +122,13 @@ impl BotFunction {
}
}
pub fn context(&self) -> Option<*mut quickjs_rusty::JSContext> {
match &self.func {
FunctionMarker::Function(js_function) => Some(js_function.context()),
FunctionMarker::StrTemplate(_) => None,
}
}
pub fn call(&self) -> ScriptResult<JsValue> {
self.call_args(Default::default())
}
@ -498,6 +506,10 @@ impl BotMessage {
pub fn is_replace(&self) -> bool {
self.replace
}
pub fn get_handler(&self) -> Option<&BotFunction> {
self.handler.as_ref()
}
}
impl BotMessage {
@ -653,6 +665,17 @@ impl Runner {
})
}
pub fn call_attacher<F, R>(&mut self, f: F) -> ScriptResult<R>
where
F: FnOnce(&Context, &mut OwnedJsObject) -> R,
{
let context = self.context.lock().unwrap();
let mut global = context.global()?;
let res = f(&context, &mut global);
Ok(res)
}
pub fn run_script(&self, content: &str) -> ScriptResult<JsValue> {
let ctx = match self.context.lock() {
Ok(ctx) => ctx,

View File

@ -0,0 +1,81 @@
use std::sync::RwLock;
use log::info;
use quickjs_rusty::{context::Context, serde::from_js, OwnedJsObject};
use teloxide::Bot;
use tokio::runtime::Handle;
use crate::{
db::{application::Application, message_forward::MessageForward, CallDB, DB},
message_answerer::MessageAnswerer,
send_application_to_chat, BotError,
};
use super::ScriptError;
pub fn attach_user_application(
c: &Context,
o: &mut OwnedJsObject,
db: &DB,
bot: &Bot,
) -> Result<(), ScriptError> {
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 botbox = Box::new(bot.clone());
let bot: &'static _ = Box::leak(botbox);
let user_application =
c.create_callback(move |q: OwnedJsObject| -> Result<_, ScriptError> {
let db = db.clone();
let user: teloxide::types::User = match from_js(q.context(), &q) {
Ok(q) => q,
Err(_) => todo!(),
};
let application = futures::executor::block_on(
Application::new(user.clone()).store_db(&mut db.write().unwrap()),
)?;
let db2 = db.clone();
let msg = tokio::task::block_in_place(move || {
Handle::current().block_on(async move {
send_application_to_chat(
&bot.read().unwrap(),
&mut db2.write().unwrap(),
&application,
)
.await
})
});
let msg = match msg {
Ok(msg) => msg,
Err(err) => {
info!("Got err: {err}");
return Err(ScriptError::MutexError("🤦‍♂️".to_string()));
}
};
let (chat_id, msg_id) = futures::executor::block_on(
MessageAnswerer::new(
&bot.read().unwrap(),
&mut db.write().unwrap(),
user.id.0 as i64,
)
.answer("left_application_msg", None, None),
)
.unwrap();
futures::executor::block_on(
MessageForward::new(msg.chat.id.0, msg.id.0, chat_id, msg_id, false)
.store_db(&mut db.write().unwrap()),
)?;
let ret = true;
Ok(ret)
})?;
o.set_property("user_application", user_application.into_value())?;
Ok(())
}

View File

@ -2,6 +2,7 @@ use chrono::{DateTime, FixedOffset, Local};
use serde::{Deserialize, Serialize};
use super::DbResult;
use super::DB;
use crate::query_call_consume;
use crate::CallDB;
@ -36,4 +37,13 @@ where
Ok(self)
});
pub async fn store_db(self, db: &mut DB) -> DbResult<Self> {
let db = db.get_database().await;
let ci = db.collection::<Self>("applications");
ci.insert_one(&self).await?;
Ok(self)
}
}

View File

@ -2,6 +2,7 @@ use bson::doc;
use serde::{Deserialize, Serialize};
use super::DbResult;
use super::DB;
use crate::query_call_consume;
use crate::CallDB;
@ -42,6 +43,15 @@ impl MessageForward {
Ok(self)
});
pub async fn store_db(self, db: &mut DB) -> DbResult<Self> {
let db = db.get_database().await;
let ci = db.collection::<Self>("message_forward");
ci.insert_one(&self).await?;
Ok(self)
}
pub async fn get<D: CallDB>(
db: &mut D,
chat_id: i64,

View File

@ -23,6 +23,8 @@ use crate::{BotDialogue, BotError, BotResult, CallbackStore, State};
pub fn admin_handler() -> BotHandler {
dptree::entry()
// keep on top to cancel any action
.branch(cancel_handler())
.branch(
Update::filter_callback_query()
.filter_async(async |q: CallbackQuery, mut db: DB| {
@ -197,6 +199,17 @@ async fn button_edit_callback(
Ok(())
}
fn cancel_handler() -> BotHandler {
Update::filter_message()
.filter(|msg: Message| msg.text() == Some("/cancel"))
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
.endpoint(async |bot: Bot, msg: Message, dialogue: BotDialogue| {
dialogue.exit().await?;
bot.send_message(msg.chat.id, "Диалог закончен!").await?;
Ok(())
})
}
fn command_handler() -> BotHandler {
Update::filter_message()
.filter_async(async |msg: Message, mut db: DB| {
@ -300,6 +313,10 @@ async fn support_reply_handler(
_ => unreachable!(),
};
let text = format!(
"Сообщение от поддержки:\n{}\nЧтобы закончить диалог, нажмите на /cancel",
text
);
let msg = bot
.send_message(ChatId(mf.source_chat_id), text)
.parse_mode(ParseMode::Html);

View File

@ -10,6 +10,7 @@ pub mod mongodb_storage;
pub mod utils;
use bot_manager::BotManager;
use botscript::application::attach_user_application;
use botscript::{BotMessage, Runner, RunnerConfig, ScriptError, ScriptResult};
use db::application::Application;
use db::bots::BotInstance;
@ -131,7 +132,8 @@ impl BotController {
pub async fn with_db(mut db: DB, token: &str, script: &str) -> ScriptResult<Self> {
let bot = Bot::new(token);
let runner = Runner::init_with_db(&mut db)?;
let mut runner = Runner::init_with_db(&mut db)?;
runner.call_attacher(|c, o| attach_user_application(c, o, &db, &bot))??;
let rc = runner.init_config(script)?;
let rc = Arc::new(RwLock::new(rc));