From fd7c813b0e917979b92cc7bbd1679a2960d95284 Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 11:39:44 +0500 Subject: [PATCH 01/13] create specification for notifications --- src/botscript.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/botscript.rs b/src/botscript.rs index fff66c9..1189b75 100644 --- a/src/botscript.rs +++ b/src/botscript.rs @@ -2,10 +2,12 @@ pub mod application; pub mod db; use std::collections::HashMap; use std::sync::{Arc, Mutex, PoisonError}; +use std::time::Duration; use crate::db::raw_calls::RawCallError; use crate::db::{CallDB, DbError, DB}; use crate::utils::parcelable::{ParcelType, Parcelable, ParcelableError, ParcelableResult}; +use chrono::{NaiveTime, ParseError, Timelike}; use db::attach_db_obj; use futures::future::join_all; use futures::lock::MutexGuard; @@ -269,6 +271,10 @@ fn print(s: String) { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BotConfig { version: f64, + /// relative to UTC, for e.g., + /// timezone = 3 will be UTC+3, + /// timezone =-2 will be UTC-2, + timezone: i8, } pub trait ResolveValue { @@ -597,10 +603,77 @@ impl Parcelable for BotDialog { } } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum NotificationTime { + Delta(Duration), + Specific(SpecificTime), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(try_from = "SpecificTimeFormat")] +pub struct SpecificTime { + hour: u8, + minutes: u8, +} + +impl SpecificTime { + pub fn new(hour: u8, minutes: u8) -> Self { + Self { hour, minutes } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum SpecificTimeFormat { + Verbose(SpecificTime), + String(String), +} + +impl TryFrom for SpecificTime { + type Error = ParseError; + + fn try_from(stf: SpecificTimeFormat) -> Result { + match stf { + SpecificTimeFormat::Verbose(specific_time) => Ok(specific_time), + SpecificTimeFormat::String(timestring) => { + let time: NaiveTime = timestring.parse()?; + + Ok(SpecificTime::new(time.hour() as u8, time.minute() as u8)) + } + } + } +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub enum NotificationFilter { + #[default] + All, + /// Send to randomly selected N people + Random(usize), + /// Function that returns list of user id's who should get notification + BotFunction(BotFunction), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum NotificationMessage { + Literal(String), + BotFunction(BotFunction), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BotNotification { + time: NotificationTime, + #[serde(default)] + filter: NotificationFilter, + message: NotificationMessage, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RunnerConfig { config: BotConfig, pub dialog: BotDialog, + #[serde(default)] + notifications: Vec, } impl RunnerConfig { -- 2.47.2 From 57652c2776915bff2c099cf01125713cf8b8fa8a Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 12:02:28 +0500 Subject: [PATCH 02/13] fix: infinite try_from deserealization --- src/botscript.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/botscript.rs b/src/botscript.rs index 1189b75..9efde21 100644 --- a/src/botscript.rs +++ b/src/botscript.rs @@ -625,8 +625,8 @@ impl SpecificTime { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum SpecificTimeFormat { - Verbose(SpecificTime), String(String), + Verbose { hour: u8, minutes: u8 }, } impl TryFrom for SpecificTime { @@ -634,11 +634,11 @@ impl TryFrom for SpecificTime { fn try_from(stf: SpecificTimeFormat) -> Result { match stf { - SpecificTimeFormat::Verbose(specific_time) => Ok(specific_time), + SpecificTimeFormat::Verbose { hour, minutes } => Ok(Self::new(hour, minutes)), SpecificTimeFormat::String(timestring) => { let time: NaiveTime = timestring.parse()?; - Ok(SpecificTime::new(time.hour() as u8, time.minute() as u8)) + Ok(Self::new(time.hour() as u8, time.minute() as u8)) } } } -- 2.47.2 From 91bf739365a2d9b17e010aca3b80c5b3135a270a Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 12:03:16 +0500 Subject: [PATCH 03/13] update BotNotification specification --- src/botscript.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/botscript.rs b/src/botscript.rs index 9efde21..cb9ede2 100644 --- a/src/botscript.rs +++ b/src/botscript.rs @@ -604,12 +604,13 @@ impl Parcelable for BotDialog { } #[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] pub enum NotificationTime { - Delta(Duration), + Delta(isize), Specific(SpecificTime), } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(try_from = "SpecificTimeFormat")] pub struct SpecificTime { hour: u8, @@ -645,18 +646,27 @@ impl TryFrom for SpecificTime { } #[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[serde(untagged)] pub enum NotificationFilter { #[default] + #[serde(rename = "all")] All, /// Send to randomly selected N people - Random(usize), + Random { random: u8 }, /// Function that returns list of user id's who should get notification BotFunction(BotFunction), } #[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] pub enum NotificationMessage { - Literal(String), + Literal { + literal: String, + }, + Text { + text: String, + }, + /// Function can accept user which will be notified and then return generated message BotFunction(BotFunction), } -- 2.47.2 From c9dc1fb479f47dbd061fec3684906d9c91953657 Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 12:03:28 +0500 Subject: [PATCH 04/13] create tests for BotNotification deserealization --- src/botscript.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/botscript.rs b/src/botscript.rs index cb9ede2..64e46b1 100644 --- a/src/botscript.rs +++ b/src/botscript.rs @@ -790,6 +790,7 @@ impl Runner { #[allow(clippy::print_stdout)] mod tests { use quickjs_rusty::{serde::from_js, OwnedJsObject}; + use serde_json::json; use super::*; @@ -868,4 +869,28 @@ mod tests { panic!("test returned an error, but the wrong one, {errstr}") } } + + #[test] + fn test_notification_struct() { + let botn = json!({ + "time": "18:00", + "filter": {"random": 2}, + "message": {"text": "some"}, + }); + let n: BotNotification = serde_json::from_value(botn).unwrap(); + println!("BotNotification: {n:#?}"); + assert!(matches!(n.time, NotificationTime::Specific(..))); + let time = if let NotificationTime::Specific(st) = n.time { + st + } else { + unreachable!() + }; + assert_eq!( + time, + SpecificTime { + hour: 18, + minutes: 00 + } + ); + } } -- 2.47.2 From af3dba28732f63417afdb3c1aae32d6f9f69c218 Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:42:32 +0500 Subject: [PATCH 05/13] updatte mainbot.js --- mainbot.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mainbot.js b/mainbot.js index 1939cb5..ea6c7f2 100644 --- a/mainbot.js +++ b/mainbot.js @@ -76,8 +76,16 @@ print(JSON.stringify(dialog.buttons)) const config = { version: 1.1, + timezone: 3, } +const notifications = [ + // { + // time: "18:14", + // message: {literal: "show_projects"}, + // }, +] + // {config, dialog} -const c = { config: config, dialog: dialog } +const c = { config: config, dialog: dialog, notifications: notifications } c -- 2.47.2 From 0ca057c06455eab095b0a36d76359a82d197aa9f Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:43:53 +0500 Subject: [PATCH 06/13] create test for BotNotification --- src/botscript.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/botscript.rs b/src/botscript.rs index 64e46b1..9c0a9ab 100644 --- a/src/botscript.rs +++ b/src/botscript.rs @@ -893,4 +893,35 @@ mod tests { } ); } + + #[test] + fn test_notification_time() { + let botn = json!({ + "time": "18:00", + "filter": {"random": 2}, + "message": {"text": "some"}, + }); + let n: BotNotification = serde_json::from_value(botn).unwrap(); + println!("BotNotification: {n:#?}"); + let start_time = chrono::offset::Utc::now(); + // let start_time = chrono::offset::Utc::now() + TimeDelta::try_hours(5).unwrap(); + let start_time = start_time.with_hour(13).unwrap().with_minute(23).unwrap(); + let left = n.left_time(&start_time, &start_time); + let secs = left.as_secs(); + let minutes = secs / 60; + let hours = minutes / 60; + let minutes = minutes % 60; + println!("Left: {hours}:{minutes}"); + + let when_should = chrono::offset::Utc::now() + .with_hour(18) + .unwrap() + .with_minute(00) + .unwrap(); + + let should_left = (when_should - start_time).to_std().unwrap(); + let should_left = Duration::from_secs(should_left.as_secs()); + + assert_eq!(left, should_left) + } } -- 2.47.2 From b2df5bf4f3701b75aba6d9addc975da1c4e5bdaf Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:44:48 +0500 Subject: [PATCH 07/13] db: some functions do not require mut db --- src/db/mod.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index 1e5882f..db6b0c2 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -212,6 +212,10 @@ impl CallDB for DB { async fn get_database(&mut self) -> Database { self.client.database(&self.name) } + + async fn get_database_immut(&self) -> Database { + self.client.database(&self.name) + } } impl GetCollection for T { @@ -233,9 +237,10 @@ pub type DbResult = Result; pub trait CallDB { //type C; async fn get_database(&mut self) -> Database; + async fn get_database_immut(&self) -> Database; //async fn get_pool(&mut self) -> PooledConnection<'_, AsyncDieselConnectionManager>; - async fn get_users(&mut self) -> DbResult> { - let db = self.get_database().await; + async fn get_users(&self) -> DbResult> { + let db = self.get_database_immut().await; let users = db.collection::("users"); Ok(users.find(doc! {}).await?.try_collect().await?) @@ -350,8 +355,8 @@ pub trait CallDB { Ok(()) } - async fn get_literal(&mut self, literal: &str) -> DbResult> { - let db = self.get_database().await; + async fn get_literal(&self, literal: &str) -> DbResult> { + let db = self.get_database_immut().await; let messages = db.collection::("literals"); let literal = messages.find_one(doc! { "token": literal }).await?; @@ -359,7 +364,7 @@ pub trait CallDB { Ok(literal) } - async fn get_literal_value(&mut self, literal: &str) -> DbResult> { + async fn get_literal_value(&self, literal: &str) -> DbResult> { let literal = self.get_literal(literal).await?; Ok(literal.map(|l| l.value)) -- 2.47.2 From 597e6de09e116798c67e53ae879021d787cd8cfb Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:45:10 +0500 Subject: [PATCH 08/13] db: create get_random_users --- src/db/mod.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/db/mod.rs b/src/db/mod.rs index db6b0c2..508bdbe 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -11,6 +11,7 @@ use chrono::{DateTime, Utc}; use enum_stringify::EnumStringify; use futures::stream::TryStreamExt; +use futures::StreamExt; use mongodb::options::IndexOptions; use mongodb::{bson::doc, options::ClientOptions, Client}; use mongodb::{Collection, Database, IndexModel}; @@ -230,6 +231,8 @@ impl GetCollection for T { pub enum DbError { #[error("error while processing mongodb query: {0}")] MongodbError(#[from] mongodb::error::Error), + #[error("error while coverting values: {0}")] + SerdeJsonError(#[from] serde_json::error::Error), } pub type DbResult = Result; @@ -246,6 +249,26 @@ pub trait CallDB { Ok(users.find(doc! {}).await?.try_collect().await?) } + async fn get_random_users(&self, n: u32) -> DbResult> { + let db = self.get_database_immut().await; + let users = db.collection::("users"); + + let random_users: Vec = users + .aggregate(vec![doc! {"$sample": {"size": n}}]) + .await? + .try_collect() + .await?; + let random_users = random_users + .into_iter() + .map(|d| match serde_json::to_value(d) { + Ok(value) => serde_json::from_value::(value), + Err(err) => Err(err), + }) + .collect::>()?; + + Ok(random_users) + } + async fn set_admin(&mut self, userid: i64, isadmin: bool) -> DbResult<()> { let db = self.get_database().await; let users = db.collection::("users"); -- 2.47.2 From 3d293501f21a980c1e8beaa8110bafa05002430e Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:45:32 +0500 Subject: [PATCH 09/13] db: create test for get_random_users --- src/db/tests/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/db/tests/mod.rs b/src/db/tests/mod.rs index 8da40ce..5dbc6bc 100644 --- a/src/db/tests/mod.rs +++ b/src/db/tests/mod.rs @@ -174,3 +174,11 @@ async fn test_drop_media_except() { let _ = db.drop_media(literal).await.unwrap(); } + +#[tokio::test] +async fn test_get_random_users() { + let mut db = setup_db().await; + + let users = db.get_random_users(1).await.unwrap(); + assert_eq!(users.len(), 1); +} -- 2.47.2 From 632e77762e04a88c7d0c8d19710724963a8742d2 Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:45:59 +0500 Subject: [PATCH 10/13] fix utils.rs: reuqire Sync --- src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 04ff503..091cba2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -50,7 +50,7 @@ pub async fn create_callback_button( ) -> BotResult where C: Serialize + for<'a> Deserialize<'a> + Send + Sync, - D: CallDB + Send, + D: CallDB + Send + Sync, { let text = db .get_literal_value(literal) -- 2.47.2 From 00af4fae4d66ee76103c4b133599e3dd5c448e97 Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:48:12 +0500 Subject: [PATCH 11/13] create MessageAnswerer.answer_text --- src/message_answerer.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/message_answerer.rs b/src/message_answerer.rs index c9d10ce..924361c 100644 --- a/src/message_answerer.rs +++ b/src/message_answerer.rs @@ -86,6 +86,14 @@ impl<'a> MessageAnswerer<'a> { self.answer_inner(text, literal, variant, keyboard).await } + pub async fn answer_text( + self, + text: String, + keyboard: Option, + ) -> BotResult<(i64, i32)> { + self.send_message(text, keyboard).await + } + async fn answer_inner( mut self, text: String, -- 2.47.2 From de68e41725a85ed6a0022001d610d1c0deef9bfe Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:49:05 +0500 Subject: [PATCH 12/13] create notificator --- src/bot_manager.rs | 51 +++++++++++ src/botscript.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 267 insertions(+), 5 deletions(-) diff --git a/src/bot_manager.rs b/src/bot_manager.rs index 2806fd9..4377e9d 100644 --- a/src/bot_manager.rs +++ b/src/bot_manager.rs @@ -12,6 +12,7 @@ use teloxide::{ dispatching::dialogue::serializer::Json, dptree, prelude::{Dispatcher, Requester}, + types::{ChatId, UserId}, Bot, }; use tokio::runtime::Handle; @@ -19,6 +20,7 @@ use tokio::runtime::Handle; use crate::{ bot_handler::{script_handler, BotHandler}, db::{bots::BotInstance, DbError, DB}, + message_answerer::MessageAnswerer, mongodb_storage::MongodbStorage, BotController, BotError, BotResult, BotRuntime, }; @@ -26,9 +28,15 @@ use crate::{ pub struct BotRunner { controller: BotController, info: BotInfo, + notificator: NotificatorThread, thread: Option>>, } +pub enum NotificatorThread { + Running(Option>>), + Done, +} + #[derive(Clone)] pub struct BotInfo { pub name: String, @@ -149,6 +157,9 @@ where let thread = spawn_bot_thread(controller.bot.clone(), controller.db.clone(), handler).await?; + println!("Starting notificator"); + let notificator = spawn_notificator_thread(controller.clone()).await?; + let notificator = NotificatorThread::Running(Some(notificator)); let info = BotInfo { name: bi.name.clone(), @@ -156,6 +167,7 @@ where let runner = BotRunner { controller, info: info.clone(), + notificator, thread: Some(thread), }; @@ -207,3 +219,42 @@ pub async fn spawn_bot_thread( Ok(thread) } + +pub async fn spawn_notificator_thread( + mut c: BotController, +) -> BotResult>> { + let thread = std::thread::spawn(move || -> BotResult<()> { + let rt = tokio::runtime::Runtime::new().unwrap(); + + rt.block_on(async { + loop { + let r = c.runtime.lock().unwrap(); + let notifications = r.rc.get_nearest_notifications(); + drop(r); // unlocking mutex + + match notifications { + Some(n) => { + // waiting time to send notification + println!("Will send notification after {:?}", n.wait_for()); + println!("Notifications: {:#?}", n.notifications()); + tokio::time::sleep(n.wait_for()).await; + 'n: for n in n.notifications().into_iter() { + for user in n.get_users(&c.db).await?.into_iter() { + let text = match n.resolve_message(&c.db, &user).await? { + Some(text) => text, + None => continue 'n, + }; + + let ma = MessageAnswerer::new(&c.bot, &mut c.db, user.id); + ma.answer_text(text.clone(), None).await?; + } + } + } + None => break Ok(()), + } + } + }) + }); + + Ok(thread) +} diff --git a/src/botscript.rs b/src/botscript.rs index 9c0a9ab..ad745af 100644 --- a/src/botscript.rs +++ b/src/botscript.rs @@ -5,14 +5,14 @@ use std::sync::{Arc, Mutex, PoisonError}; use std::time::Duration; use crate::db::raw_calls::RawCallError; -use crate::db::{CallDB, DbError, DB}; +use crate::db::{CallDB, DbError, User, DB}; use crate::utils::parcelable::{ParcelType, Parcelable, ParcelableError, ParcelableResult}; -use chrono::{NaiveTime, ParseError, Timelike}; +use chrono::{DateTime, Days, NaiveTime, ParseError, TimeDelta, Timelike, Utc}; use db::attach_db_obj; use futures::future::join_all; use futures::lock::MutexGuard; use itertools::Itertools; -use quickjs_rusty::serde::from_js; +use quickjs_rusty::serde::{from_js, to_js}; use quickjs_rusty::utils::create_empty_object; use quickjs_rusty::utils::create_string; use quickjs_rusty::ContextError; @@ -274,6 +274,7 @@ pub struct BotConfig { /// relative to UTC, for e.g., /// timezone = 3 will be UTC+3, /// timezone =-2 will be UTC-2, + #[serde(default)] timezone: i8, } @@ -606,10 +607,56 @@ impl Parcelable for BotDialog { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum NotificationTime { - Delta(isize), + Delta { + #[serde(default)] + delta_hours: u32, + #[serde(default)] + delta_minutes: u32, + }, Specific(SpecificTime), } +impl NotificationTime { + pub fn when_next(&self, start_time: &DateTime, now: &DateTime) -> DateTime { + let now = *now; + match self { + NotificationTime::Delta { + delta_hours, + delta_minutes, + } => { + let delta = TimeDelta::minutes((delta_minutes + delta_hours * 60).into()); + println!("Delta: {delta}"); + + let mut estimation = *start_time; + // super non-optimal, but fun :) + loop { + println!("Adding delta to estimation"); + if estimation < now + Duration::from_secs(1) { + estimation += delta; + } else { + break estimation; + } + } + } + NotificationTime::Specific(time) => { + let estimation = now; + let estimation = estimation.with_hour(time.hour.into()).unwrap_or(estimation); + let mut estimation = estimation + .with_minute(time.minutes.into()) + .unwrap_or(estimation); + // super non-optimal, but fun :) + loop { + if estimation < now { + estimation = estimation + Days::new(1); + } else { + break estimation; + } + } + } + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(try_from = "SpecificTimeFormat")] pub struct SpecificTime { @@ -652,11 +699,42 @@ pub enum NotificationFilter { #[serde(rename = "all")] All, /// Send to randomly selected N people - Random { random: u8 }, + Random { random: u32 }, /// Function that returns list of user id's who should get notification BotFunction(BotFunction), } +impl NotificationFilter { + pub async fn get_users(&self, db: &DB) -> ScriptResult> { + match self { + NotificationFilter::All => Ok(db.get_users().await?), + NotificationFilter::Random { random } => Ok(db.get_random_users(*random).await?), + NotificationFilter::BotFunction(f) => { + let users = f.call()?; + let users = from_js(f.context().unwrap(), &users)?; + Ok(users) + } + } + } +} + +impl Parcelable for NotificationFilter { + fn get_field(&mut self, name: &str) -> ParcelableResult> { + todo!() + } + + fn resolve(&mut self) -> ParcelableResult> + where + Self: Sized + 'static, + { + match self { + NotificationFilter::All => Ok(ParcelType::Other(())), + NotificationFilter::Random { .. } => Ok(ParcelType::Other(())), + NotificationFilter::BotFunction(f) => Ok(Parcelable::<_>::resolve(f)?), + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum NotificationMessage { @@ -670,6 +748,38 @@ pub enum NotificationMessage { BotFunction(BotFunction), } +impl Parcelable for NotificationMessage { + fn get_field(&mut self, name: &str) -> ParcelableResult> { + todo!() + } + + fn resolve(&mut self) -> ParcelableResult> + where + Self: Sized + 'static, + { + match self { + NotificationMessage::Literal { .. } => Ok(ParcelType::Other(())), + NotificationMessage::Text { .. } => Ok(ParcelType::Other(())), + NotificationMessage::BotFunction(f) => Ok(f.resolve()?), + } + } +} + +impl NotificationMessage { + pub async fn resolve(&self, db: &DB, user: &User) -> ScriptResult> { + match self { + NotificationMessage::Literal { literal } => Ok(db.get_literal_value(literal).await?), + NotificationMessage::Text { text } => Ok(Some(text.to_string())), + NotificationMessage::BotFunction(f) => { + let jsuser = to_js(f.context().expect("Function is not js"), user).unwrap(); + let text = f.call_args(vec![jsuser])?; + let text = from_js(f.context().unwrap(), &text)?; + Ok(text) + } + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BotNotification { time: NotificationTime, @@ -678,12 +788,75 @@ pub struct BotNotification { message: NotificationMessage, } +impl Parcelable for BotNotification { + fn get_field(&mut self, name: &str) -> ParcelableResult> { + match name { + "filter" => Ok(Parcelable::<_>::resolve(&mut self.filter)?), + "message" => Ok(Parcelable::::resolve(&mut self.message)?), + field => Err(ParcelableError::FieldError(format!( + "tried to get field {field}, but this field does not exists or private" + ))), + } + } +} + +impl BotNotification { + pub fn left_time(&self, start_time: &DateTime, now: &DateTime) -> Duration { + let next = self.time.when_next(start_time, now); + + // immidate notification if time to do it passed + let duration = (next - now).to_std().unwrap_or(Duration::from_secs(1)); + + println!("Left time: {duration:?}"); + // Rounding partitions of seconds + Duration::from_secs(duration.as_secs()) + } + + pub async fn get_users(&self, db: &DB) -> ScriptResult> { + self.filter.get_users(db).await + } + pub async fn resolve_message(&self, db: &DB, user: &User) -> ScriptResult> { + self.message.resolve(db, user).await + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RunnerConfig { config: BotConfig, pub dialog: BotDialog, #[serde(default)] notifications: Vec, + #[serde(skip)] + created_at: ConfigCreatedAt, +} + +#[derive(Debug, Clone)] +struct ConfigCreatedAt { + at: DateTime, +} + +impl Default for ConfigCreatedAt { + fn default() -> Self { + Self { + at: chrono::offset::Utc::now(), + } + } +} + +#[derive(Debug, Clone)] +pub struct NotificationBlock { + wait_for: Duration, + notifications: Vec, +} + +impl NotificationBlock { + pub fn wait_for(&self) -> Duration { + self.wait_for + } + + pub fn notifications(&self) -> &[BotNotification] { + &self.notifications + } } impl RunnerConfig { @@ -699,12 +872,50 @@ impl RunnerConfig { bm.map(|bm| bm.fill_literal(callback.to_string())) } + + pub fn created_at(&self) -> DateTime { + self.created_at.at + TimeDelta::try_hours(self.config.timezone.into()).unwrap() + } + + /// 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 + pub fn get_nearest_notifications(&self) -> Option { + let start_time = self.created_at(); + let now = + chrono::offset::Utc::now() + TimeDelta::try_hours(self.config.timezone.into()).unwrap(); + + let ordered = self + .notifications + .iter() + .filter(|f| f.left_time(&start_time, &now) > Duration::from_secs(1)) + .sorted_by_key(|f| f.left_time(&start_time, &now)) + .collect::>(); + println!("Orederd: {:#?}", ordered); + + let left = match ordered.first() { + Some(notification) => notification.left_time(&start_time, &now), + // No notifications provided + None => return None, + }; + // get all that should be sent at the same time + let notifications = ordered + .into_iter() + .filter(|n| n.left_time(&start_time, &now) == left) + .cloned() + .collect::>(); + + Some(NotificationBlock { + wait_for: left, + notifications, + }) + } } impl Parcelable for RunnerConfig { fn get_field(&mut self, name: &str) -> Result, ParcelableError> { match name { "dialog" => Ok(ParcelType::Parcelable(&mut self.dialog)), + "notifications" => Ok(ParcelType::Parcelable(&mut self.notifications)), field => Err(ParcelableError::FieldError(format!( "tried to get field {field}, but this field does not exists or private" ))), -- 2.47.2 From daf1e09176ab03413e7e8fe25d600adb5323b13b Mon Sep 17 00:00:00 2001 From: Akulij Date: Mon, 2 Jun 2025 16:51:19 +0500 Subject: [PATCH 13/13] delete unnecessary println --- src/bot_manager.rs | 3 --- src/botscript.rs | 4 ---- 2 files changed, 7 deletions(-) diff --git a/src/bot_manager.rs b/src/bot_manager.rs index 4377e9d..af45b65 100644 --- a/src/bot_manager.rs +++ b/src/bot_manager.rs @@ -157,7 +157,6 @@ where let thread = spawn_bot_thread(controller.bot.clone(), controller.db.clone(), handler).await?; - println!("Starting notificator"); let notificator = spawn_notificator_thread(controller.clone()).await?; let notificator = NotificatorThread::Running(Some(notificator)); @@ -235,8 +234,6 @@ pub async fn spawn_notificator_thread( match notifications { Some(n) => { // waiting time to send notification - println!("Will send notification after {:?}", n.wait_for()); - println!("Notifications: {:#?}", n.notifications()); tokio::time::sleep(n.wait_for()).await; 'n: for n in n.notifications().into_iter() { for user in n.get_users(&c.db).await?.into_iter() { diff --git a/src/botscript.rs b/src/botscript.rs index ad745af..d52a44a 100644 --- a/src/botscript.rs +++ b/src/botscript.rs @@ -625,12 +625,10 @@ impl NotificationTime { delta_minutes, } => { let delta = TimeDelta::minutes((delta_minutes + delta_hours * 60).into()); - println!("Delta: {delta}"); let mut estimation = *start_time; // super non-optimal, but fun :) loop { - println!("Adding delta to estimation"); if estimation < now + Duration::from_secs(1) { estimation += delta; } else { @@ -807,7 +805,6 @@ impl BotNotification { // immidate notification if time to do it passed let duration = (next - now).to_std().unwrap_or(Duration::from_secs(1)); - println!("Left time: {duration:?}"); // Rounding partitions of seconds Duration::from_secs(duration.as_secs()) } @@ -890,7 +887,6 @@ impl RunnerConfig { .filter(|f| f.left_time(&start_time, &now) > Duration::from_secs(1)) .sorted_by_key(|f| f.left_time(&start_time, &now)) .collect::>(); - println!("Orederd: {:#?}", ordered); let left = match ordered.first() { Some(notification) => notification.left_time(&start_time, &now), -- 2.47.2