Compare commits
2 Commits
56f7459ad8
...
9c846618d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c846618d1 | |||
|
|
9c0e9a5903 |
119
src/config/dialog/button.rs
Normal file
119
src/config/dialog/button.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
function::BotFunction,
|
||||
result::{ConfigError, ConfigResult},
|
||||
traits::{ProviderDeserialize, ResolveValue},
|
||||
Provider,
|
||||
},
|
||||
db::{CallDB, DB},
|
||||
notify_admin,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum ButtonDefinition<P: Provider> {
|
||||
Button(ButtonRaw),
|
||||
ButtonLiteral(String),
|
||||
Function(BotFunction<P>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ButtonRaw {
|
||||
name: ButtonName,
|
||||
callback_name: String,
|
||||
}
|
||||
|
||||
impl ButtonRaw {
|
||||
pub fn from_literal(literal: String) -> Self {
|
||||
ButtonRaw {
|
||||
name: ButtonName::Literal {
|
||||
literal: literal.clone(),
|
||||
},
|
||||
callback_name: literal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &ButtonName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn callback_name(&self) -> &str {
|
||||
&self.callback_name
|
||||
}
|
||||
|
||||
pub fn literal(&self) -> Option<String> {
|
||||
match self.name() {
|
||||
ButtonName::Value { .. } => None,
|
||||
ButtonName::Literal { literal } => Some(literal.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum ButtonName {
|
||||
Value { name: String },
|
||||
Literal { literal: String },
|
||||
}
|
||||
|
||||
impl ButtonName {
|
||||
pub async fn resolve_name(self, db: &mut DB) -> ConfigResult<String> {
|
||||
match self {
|
||||
ButtonName::Value { name } => Ok(name),
|
||||
ButtonName::Literal { literal } => {
|
||||
let value = db.get_literal_value(&literal).await?;
|
||||
|
||||
Ok(match value {
|
||||
Some(value) => Ok(value),
|
||||
None => {
|
||||
notify_admin(&format!("Literal `{literal}` is not set!!!")).await;
|
||||
Err(ConfigError::Other(format!(
|
||||
"not found literal `{literal}` in DB"
|
||||
)))
|
||||
}
|
||||
}?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ButtonLayout {
|
||||
Callback {
|
||||
name: String,
|
||||
literal: Option<String>,
|
||||
callback: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl ButtonLayout {
|
||||
pub async fn resolve_raw(braw: ButtonRaw, db: &mut DB) -> ConfigResult<Self> {
|
||||
let name = braw.name().clone().resolve_name(db).await?;
|
||||
let literal = braw.literal();
|
||||
let callback = braw.callback_name().to_string();
|
||||
Ok(Self::Callback {
|
||||
name,
|
||||
literal,
|
||||
callback,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Provider> ResolveValue for ButtonDefinition<P> {
|
||||
type Value = ButtonRaw;
|
||||
type Runtime = P;
|
||||
|
||||
fn resolve(self) -> ConfigResult<Self::Value> {
|
||||
match self {
|
||||
ButtonDefinition::Button(button) => Ok(button),
|
||||
ButtonDefinition::ButtonLiteral(l) => Ok(ButtonRaw::from_literal(l)),
|
||||
ButtonDefinition::Function(f) => <Self as ResolveValue>::resolve(match f.call()? {
|
||||
Some(t) => Ok(t.de_into().map_err(ConfigError::as_provider_err)?),
|
||||
None => Err(ConfigError::Other(
|
||||
"Function didn't return value".to_string(),
|
||||
)),
|
||||
}?),
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/config/dialog/keyboard.rs
Normal file
58
src/config/dialog/keyboard.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::{
|
||||
function::BotFunction,
|
||||
result::{ConfigError, ConfigResult},
|
||||
traits::{ProviderDeserialize, ResolveValue},
|
||||
Provider,
|
||||
};
|
||||
|
||||
use super::button::ButtonDefinition;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum KeyboardDefinition<P: Provider> {
|
||||
Rows(Vec<RowDefinition<P>>),
|
||||
Function(BotFunction<P>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum RowDefinition<P: Provider> {
|
||||
Buttons(Vec<ButtonDefinition<P>>),
|
||||
Function(BotFunction<P>),
|
||||
}
|
||||
|
||||
impl<P: Provider> ResolveValue for KeyboardDefinition<P> {
|
||||
type Value = Vec<<RowDefinition<P> as ResolveValue>::Value>;
|
||||
type Runtime = P;
|
||||
|
||||
fn resolve(self) -> ConfigResult<Self::Value> {
|
||||
match self {
|
||||
KeyboardDefinition::Rows(rows) => rows.into_iter().map(|r| r.resolve()).collect(),
|
||||
KeyboardDefinition::Function(f) => <Self as ResolveValue>::resolve(match f.call()? {
|
||||
Some(t) => Ok(t.de_into().map_err(ConfigError::as_provider_err)?),
|
||||
None => Err(ConfigError::Other(
|
||||
"Function didn't return value".to_string(),
|
||||
)),
|
||||
}?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Provider> ResolveValue for RowDefinition<P> {
|
||||
type Value = Vec<<ButtonDefinition<P> as ResolveValue>::Value>;
|
||||
type Runtime = P;
|
||||
|
||||
fn resolve(self) -> ConfigResult<Self::Value> {
|
||||
match self {
|
||||
RowDefinition::Buttons(buttons) => buttons.into_iter().map(|b| b.resolve()).collect(),
|
||||
RowDefinition::Function(f) => <Self as ResolveValue>::resolve(match f.call()? {
|
||||
Some(t) => Ok(t.de_into().map_err(ConfigError::as_provider_err)?),
|
||||
None => Err(ConfigError::Other(
|
||||
"Function didn't return value".to_string(),
|
||||
)),
|
||||
}?),
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/config/dialog/message.rs
Normal file
93
src/config/dialog/message.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use futures::future::join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
config::{function::BotFunction, result::ConfigResult, traits::ResolveValue, Provider},
|
||||
db::DB,
|
||||
};
|
||||
|
||||
use super::{button::ButtonLayout, keyboard::KeyboardDefinition};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BotMessage<P: Provider> {
|
||||
// buttons: Vec<Button>
|
||||
literal: Option<String>,
|
||||
#[serde(default)]
|
||||
replace: bool,
|
||||
buttons: Option<KeyboardDefinition<P>>,
|
||||
state: Option<String>,
|
||||
|
||||
/// flag options to command is meta, so it will be appended to user.metas in db
|
||||
meta: Option<bool>,
|
||||
|
||||
handler: Option<BotFunction<P>>,
|
||||
}
|
||||
|
||||
impl<P: Provider> BotMessage<P> {
|
||||
pub fn fill_literal(self, l: String) -> Self {
|
||||
BotMessage {
|
||||
literal: self.clone().literal.or(Some(l)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// chain of modifications on BotMessage
|
||||
pub fn update_defaults(self) -> Self {
|
||||
let bm = self;
|
||||
// if message is `start`, defaulting meta to true, if not set
|
||||
|
||||
match bm.meta {
|
||||
Some(_) => bm,
|
||||
None => match &bm.literal {
|
||||
Some(l) if l == "start" => Self {
|
||||
meta: Some(true),
|
||||
..bm
|
||||
},
|
||||
_ => bm,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_replace(&self) -> bool {
|
||||
self.replace
|
||||
}
|
||||
|
||||
pub fn get_handler(&self) -> Option<&BotFunction<P>> {
|
||||
self.handler.as_ref()
|
||||
}
|
||||
|
||||
pub fn meta(&self) -> bool {
|
||||
self.meta.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Provider> BotMessage<P> {
|
||||
pub async fn resolve_buttons(
|
||||
&self,
|
||||
db: &mut DB,
|
||||
) -> ConfigResult<Option<Vec<Vec<ButtonLayout>>>> {
|
||||
let raw_buttons = self.buttons.clone().map(|b| b.resolve()).transpose()?;
|
||||
match raw_buttons {
|
||||
Some(braws) => {
|
||||
let kbd: Vec<Vec<_>> = join_all(braws.into_iter().map(|rows| async {
|
||||
join_all(rows.into_iter().map(|b| async {
|
||||
let mut db = db.clone();
|
||||
ButtonLayout::resolve_raw(b, &mut db).await
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect()
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(Some(kbd))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn literal(&self) -> Option<&String> {
|
||||
self.literal.as_ref()
|
||||
}
|
||||
}
|
||||
19
src/config/dialog/mod.rs
Normal file
19
src/config/dialog/mod.rs
Normal file
@ -0,0 +1,19 @@
|
||||
pub mod button;
|
||||
pub mod keyboard;
|
||||
pub mod message;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use message::BotMessage;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Provider;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BotDialog<P: Provider> {
|
||||
pub commands: HashMap<String, BotMessage<P>>,
|
||||
pub buttons: HashMap<String, BotMessage<P>>,
|
||||
stateful_msg_handlers: HashMap<String, BotMessage<P>>,
|
||||
#[serde(default)]
|
||||
pub(crate) variants: HashMap<String, HashMap<String, BotMessage<P>>>,
|
||||
}
|
||||
21
src/config/function.rs
Normal file
21
src/config/function.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
result::{ConfigError, ConfigResult},
|
||||
traits::ProviderCall,
|
||||
Provider,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BotFunction<P: Provider>(P::Function);
|
||||
|
||||
impl<P: Provider> BotFunction<P> {
|
||||
pub fn call(&self) -> ConfigResult<Option<P::Value>> {
|
||||
self.call_args(&[])
|
||||
}
|
||||
|
||||
pub fn call_args(&self, args: &[&P::Value]) -> ConfigResult<Option<P::Value>> {
|
||||
let val = ProviderCall::call(&self.0, args).map_err(ConfigError::as_provider_err)?;
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
128
src/config/mod.rs
Normal file
128
src/config/mod.rs
Normal file
@ -0,0 +1,128 @@
|
||||
pub mod traits;
|
||||
|
||||
pub mod dialog;
|
||||
pub mod function;
|
||||
pub mod notification;
|
||||
pub mod result;
|
||||
pub mod time;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::TimeDelta;
|
||||
use chrono::Utc;
|
||||
use dialog::message::BotMessage;
|
||||
use dialog::BotDialog;
|
||||
use itertools::Itertools;
|
||||
use notification::batch::NotificationBatch;
|
||||
use notification::BotNotification;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
pub use traits::Provider;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RunnerConfig<P: Provider> {
|
||||
config: BotConfig,
|
||||
pub dialog: BotDialog<P>,
|
||||
#[serde(default)]
|
||||
notifications: Vec<BotNotification<P>>,
|
||||
#[serde(skip)]
|
||||
created_at: ConfigCreatedAt,
|
||||
}
|
||||
|
||||
impl<P: Provider> RunnerConfig<P> {
|
||||
/// command without starting `/`
|
||||
pub fn get_command_message(&self, command: &str) -> Option<BotMessage<P>> {
|
||||
let bm = self.dialog.commands.get(command).cloned();
|
||||
|
||||
bm.map(|bm| bm.fill_literal(command.to_string()).update_defaults())
|
||||
}
|
||||
|
||||
pub fn get_command_message_varianted(
|
||||
&self,
|
||||
command: &str,
|
||||
variant: &str,
|
||||
) -> Option<BotMessage<P>> {
|
||||
if !self.dialog.commands.contains_key(command) {
|
||||
return None;
|
||||
}
|
||||
// fallback to regular if not found
|
||||
let bm = match self.dialog.variants.get(command).cloned() {
|
||||
Some(bm) => bm,
|
||||
None => return self.get_command_message(command),
|
||||
};
|
||||
// get variant of message
|
||||
let bm = match bm.get(variant).cloned() {
|
||||
Some(bm) => bm,
|
||||
None => return self.get_command_message(command),
|
||||
};
|
||||
|
||||
Some(bm.fill_literal(command.to_string()).update_defaults())
|
||||
}
|
||||
|
||||
pub fn get_callback_message(&self, callback: &str) -> Option<BotMessage<P>> {
|
||||
let bm = self.dialog.buttons.get(callback).cloned();
|
||||
|
||||
bm.map(|bm| bm.fill_literal(callback.to_string()))
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> DateTime<Utc> {
|
||||
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,
|
||||
/// so, if you'll get None, no notifications will be provided later
|
||||
pub fn get_nearest_notifications(&self) -> Option<NotificationBatch<P>> {
|
||||
let start_time = self.created_at();
|
||||
let now = self.timezoned_time(chrono::offset::Utc::now());
|
||||
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
|
||||
Some(NotificationBatch::new(left, notifications))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ConfigCreatedAt {
|
||||
at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Default for ConfigCreatedAt {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
at: chrono::offset::Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
#[serde(default)]
|
||||
timezone: i8,
|
||||
}
|
||||
28
src/config/notification/batch.rs
Normal file
28
src/config/notification/batch.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::Provider;
|
||||
|
||||
use super::BotNotification;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NotificationBatch<P: Provider> {
|
||||
wait_for: Duration,
|
||||
notifications: Vec<BotNotification<P>>,
|
||||
}
|
||||
|
||||
impl<P: Provider> NotificationBatch<P> {
|
||||
pub fn new(wait_for: Duration, notifications: Vec<BotNotification<P>>) -> Self {
|
||||
Self {
|
||||
wait_for,
|
||||
notifications,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_for(&self) -> Duration {
|
||||
self.wait_for
|
||||
}
|
||||
|
||||
pub fn notifications(&self) -> &[BotNotification<P>] {
|
||||
&self.notifications
|
||||
}
|
||||
}
|
||||
107
src/config/notification/mod.rs
Normal file
107
src/config/notification/mod.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
result::ConfigError,
|
||||
traits::{ProviderDeserialize, ProviderSerialize},
|
||||
},
|
||||
db::{CallDB, User, DB},
|
||||
};
|
||||
|
||||
use super::{function::BotFunction, result::ConfigResult, time::NotificationTime, Provider};
|
||||
|
||||
pub mod batch;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BotNotification<P: Provider> {
|
||||
time: NotificationTime,
|
||||
#[serde(default)]
|
||||
filter: NotificationFilter<P>,
|
||||
message: NotificationMessage<P>,
|
||||
}
|
||||
|
||||
impl<P: Provider> BotNotification<P> {
|
||||
pub fn left_time(&self, start_time: DateTime<Utc>, now: DateTime<Utc>) -> 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(0));
|
||||
|
||||
// Rounding partitions of seconds
|
||||
Duration::from_secs(duration.as_secs())
|
||||
}
|
||||
|
||||
pub async fn get_users(&self, db: &DB) -> ConfigResult<Vec<User>> {
|
||||
self.filter.get_users(db).await
|
||||
}
|
||||
pub async fn resolve_message(&self, db: &DB, user: &User) -> ConfigResult<Option<String>> {
|
||||
self.message.resolve(db, user).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationFilter<P: Provider> {
|
||||
#[default]
|
||||
#[serde(rename = "all")]
|
||||
All,
|
||||
/// Send to randomly selected N people
|
||||
Random { random: u32 },
|
||||
/// Function that returns list of user id's who should get notification
|
||||
BotFunction(BotFunction<P>),
|
||||
}
|
||||
|
||||
impl<P: Provider> NotificationFilter<P> {
|
||||
pub async fn get_users(&self, db: &DB) -> ConfigResult<Vec<User>> {
|
||||
match self {
|
||||
NotificationFilter::All => Ok(db.get_users().await?),
|
||||
NotificationFilter::Random { random } => Ok(db.get_random_users(*random).await?),
|
||||
NotificationFilter::BotFunction(f) => {
|
||||
let uids = match f.call()? {
|
||||
Some(t) => Ok(t),
|
||||
None => Err(ConfigError::Other(
|
||||
"Function didn't return value".to_string(),
|
||||
)),
|
||||
}?;
|
||||
let uids: Vec<i64> = uids.de_into().map_err(ConfigError::as_provider_err)?;
|
||||
let users = db.get_users_by_ids(uids).await?;
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationMessage<P: Provider> {
|
||||
Literal {
|
||||
literal: String,
|
||||
},
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
/// Function can accept user which will be notified and then return generated message
|
||||
BotFunction(BotFunction<P>),
|
||||
}
|
||||
|
||||
impl<P: Provider> NotificationMessage<P> {
|
||||
pub async fn resolve(&self, db: &DB, user: &User) -> ConfigResult<Option<String>> {
|
||||
match self {
|
||||
NotificationMessage::Literal { literal } => Ok(db.get_literal_value(literal).await?),
|
||||
NotificationMessage::Text { text } => Ok(Some(text.to_string())),
|
||||
NotificationMessage::BotFunction(f) => {
|
||||
let puser = <P::Value as ProviderSerialize>::se_from(user)
|
||||
.map_err(ConfigError::as_provider_err)?;
|
||||
let text = match f.call_args(&[&puser])? {
|
||||
Some(t) => t.de_into().map_err(ConfigError::as_provider_err)?,
|
||||
None => None,
|
||||
};
|
||||
Ok(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/config/result.rs
Normal file
29
src/config/result.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use std::error::Error;
|
||||
|
||||
use crate::db::DbError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("error from DB: {0:?}")]
|
||||
DBError(#[from] DbError),
|
||||
#[error("error from runtime provider: {0:?}")]
|
||||
ProviderError(String),
|
||||
// #[error(transparent)]
|
||||
// Other(#[from] anyhow::Error),
|
||||
#[error("error other: {0:?}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type ConfigResult<T> = Result<T, ConfigError>;
|
||||
//
|
||||
// impl<P: Provider> From<P::Error> for ConfigError<P> {
|
||||
// fn from(value: P::Error) -> Self {
|
||||
// ConfigError::ProviderError(value)
|
||||
// }
|
||||
// }
|
||||
|
||||
impl ConfigError {
|
||||
pub fn as_provider_err(err: impl Error) -> Self {
|
||||
Self::ProviderError(format!("ProviderError: {err}"))
|
||||
}
|
||||
}
|
||||
87
src/config/time.rs
Normal file
87
src/config/time.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Days, NaiveTime, ParseError, TimeDelta, Timelike, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationTime {
|
||||
Delta {
|
||||
#[serde(default)]
|
||||
delta_hours: u32,
|
||||
#[serde(default)]
|
||||
delta_minutes: u32,
|
||||
},
|
||||
Specific(SpecificTime),
|
||||
}
|
||||
|
||||
impl NotificationTime {
|
||||
pub fn when_next(&self, start_time: DateTime<Utc>, now: DateTime<Utc>) -> DateTime<Utc> {
|
||||
match self {
|
||||
NotificationTime::Delta {
|
||||
delta_hours,
|
||||
delta_minutes,
|
||||
} => {
|
||||
let delta = TimeDelta::minutes((delta_minutes + delta_hours * 60).into());
|
||||
|
||||
let secs_period = delta.num_seconds();
|
||||
if secs_period == 0 {
|
||||
return now;
|
||||
};
|
||||
|
||||
let diff = now - start_time;
|
||||
let passed = diff.num_seconds().abs() % secs_period;
|
||||
|
||||
now - Duration::from_secs(passed as u64) + delta
|
||||
}
|
||||
NotificationTime::Specific(time) => {
|
||||
let estimation = now;
|
||||
let estimation = estimation.with_hour(time.hour.into()).unwrap_or(estimation);
|
||||
let estimation = estimation
|
||||
.with_minute(time.minutes.into())
|
||||
.unwrap_or(estimation);
|
||||
|
||||
if estimation < now {
|
||||
estimation + Days::new(1)
|
||||
} else {
|
||||
estimation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[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 {
|
||||
String(String),
|
||||
Verbose { hour: u8, minutes: u8 },
|
||||
}
|
||||
|
||||
impl TryFrom<SpecificTimeFormat> for SpecificTime {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(stf: SpecificTimeFormat) -> Result<Self, Self::Error> {
|
||||
match stf {
|
||||
SpecificTimeFormat::Verbose { hour, minutes } => Ok(Self::new(hour, minutes)),
|
||||
SpecificTimeFormat::String(timestring) => {
|
||||
let time: NaiveTime = timestring.parse()?;
|
||||
|
||||
Ok(Self::new(time.hour() as u8, time.minute() as u8))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/config/traits/mod.rs
Normal file
5
src/config/traits/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod provider;
|
||||
mod resolve_value;
|
||||
|
||||
pub use provider::*;
|
||||
pub use resolve_value::*;
|
||||
50
src/config/traits/provider.rs
Normal file
50
src/config/traits/provider.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
|
||||
use crate::config::RunnerConfig;
|
||||
|
||||
pub trait Provider: Clone {
|
||||
type Function: ProviderCall<Provider = Self>
|
||||
+ Serialize
|
||||
+ for<'a> Deserialize<'a>
|
||||
+ Debug
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Clone;
|
||||
type Value: ProviderDeserialize<Provider = Self>
|
||||
+ ProviderSerialize<Provider = Self>
|
||||
+ Serialize
|
||||
+ for<'a> Deserialize<'a>
|
||||
+ Debug
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Clone;
|
||||
type Error: Error;
|
||||
|
||||
type InitData;
|
||||
fn init_config(&self, d: Self::InitData) -> Result<RunnerConfig<Self>, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait ProviderCall {
|
||||
type Provider: Provider<Function = Self>;
|
||||
fn call(
|
||||
&self,
|
||||
args: &[&<Self::Provider as Provider>::Value],
|
||||
) -> Result<Option<<Self::Provider as Provider>::Value>, <Self::Provider as Provider>::Error>;
|
||||
}
|
||||
|
||||
pub trait ProviderDeserialize {
|
||||
type Provider: Provider<Value = Self>;
|
||||
// fn de_into<T: for Deserialize>(&self) -> Result<T, <Self::Provider as Provider>::Error>;
|
||||
fn de_into<T>(&self) -> Result<T, <Self::Provider as Provider>::Error>;
|
||||
}
|
||||
|
||||
pub trait ProviderSerialize {
|
||||
type Provider: Provider<Value = Self>;
|
||||
// fn de_into<T: for Deserialize>(&self) -> Result<T, <Self::Provider as Provider>::Error>;
|
||||
fn se_from<T: Serialize>(from: &T) -> Result<Self, <Self::Provider as Provider>::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
10
src/config/traits/resolve_value.rs
Normal file
10
src/config/traits/resolve_value.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use crate::config::result::ConfigResult;
|
||||
|
||||
use super::Provider;
|
||||
|
||||
pub trait ResolveValue {
|
||||
type Value;
|
||||
type Runtime: Provider;
|
||||
|
||||
fn resolve(self) -> ConfigResult<Self::Value>;
|
||||
}
|
||||
33
src/runtimes/mlua/mod.rs
Normal file
33
src/runtimes/mlua/mod.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use mlua::{Error, Function, Lua, Value};
|
||||
|
||||
use crate::config::Provider;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LuaRuntime {
|
||||
lua: Lua,
|
||||
}
|
||||
|
||||
impl LuaRuntime {
|
||||
pub fn new() -> Self {
|
||||
let lua = Lua::new();
|
||||
Self { lua }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LuaInit {
|
||||
config: String,
|
||||
}
|
||||
|
||||
impl Provider for LuaRuntime {
|
||||
type Function = Function;
|
||||
|
||||
type Value = Value;
|
||||
|
||||
type Error = Error;
|
||||
|
||||
type InitData = LuaInit;
|
||||
|
||||
fn init_config(&self, d: Self::InitData) -> Result<crate::config::RunnerConfig<Self>, Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
2
src/runtimes/mod.rs
Normal file
2
src/runtimes/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod v8;
|
||||
// pub mod mlua;
|
||||
284
src/runtimes/v8/mod.rs
Normal file
284
src/runtimes/v8/mod.rs
Normal file
@ -0,0 +1,284 @@
|
||||
mod value_replace;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
marker::PhantomData,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{
|
||||
mpsc::{Receiver, Sender},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::config::{
|
||||
traits::{ProviderCall, ProviderDeserialize, ProviderSerialize},
|
||||
Provider, RunnerConfig,
|
||||
};
|
||||
use deno_core::{ascii_str, error::CoreError, FastString, JsRuntime, RuntimeOptions};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_v8::{from_v8, Value as SerdeValue};
|
||||
use v8::{Context, ContextScope, Function, HandleScope, Local, OwnedIsolate};
|
||||
|
||||
enum EventType {
|
||||
GetScriptConfig(String),
|
||||
ExecuteFunction(V8Function, Vec<V8Value>),
|
||||
}
|
||||
|
||||
pub struct Event {
|
||||
event: EventType,
|
||||
runtime: Arc<V8Runtime>,
|
||||
}
|
||||
|
||||
enum RuntimeReturn {
|
||||
Value(V8Value),
|
||||
OptionalValue(Option<V8Value>),
|
||||
Config(RunnerConfig<V8Runtime>),
|
||||
}
|
||||
|
||||
impl RuntimeReturn {
|
||||
fn as_value(self) -> Option<V8Value> {
|
||||
if let Self::Value(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_optional_value(self) -> Option<Option<V8Value>> {
|
||||
if let Self::OptionalValue(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_config(self) -> Option<RunnerConfig<V8Runtime>> {
|
||||
if let Self::Config(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct V8Runtime {
|
||||
#[serde(skip, default = "default_runtime")]
|
||||
runtime: Arc<Mutex<JoinHandle<()>>>,
|
||||
#[serde(skip, default = "default_sender")]
|
||||
tx: Sender<Event>,
|
||||
#[serde(skip, default = "default_receiver")]
|
||||
rx: Arc<Mutex<Receiver<RuntimeReturn>>>,
|
||||
}
|
||||
|
||||
fn default_runtime() -> Arc<Mutex<JoinHandle<()>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn default_sender() -> Sender<Event> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn default_receiver() -> Arc<Mutex<Receiver<RuntimeReturn>>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
impl Default for V8Runtime {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl V8Runtime {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = std::sync::mpsc::channel::<Event>();
|
||||
let (rtx, rrx) = std::sync::mpsc::channel::<RuntimeReturn>();
|
||||
let thread = std::thread::spawn(move || {
|
||||
let options = RuntimeOptions::default();
|
||||
let mut runtime = JsRuntime::new(options);
|
||||
let handlers: HashMap<&str, v8::Local<'_, v8::Value>> = HashMap::new();
|
||||
loop {
|
||||
let event = match rx.recv() {
|
||||
Ok(event) => event,
|
||||
Err(err) => break,
|
||||
};
|
||||
match event {
|
||||
Event::GetScriptConfig(script) => {
|
||||
let code = FastString::from(script);
|
||||
|
||||
let result = runtime.execute_script("", code).unwrap();
|
||||
let mut scope = runtime.handle_scope();
|
||||
let result = Local::new(&mut scope, result);
|
||||
value_replace::replace(&mut result, &mut runtime, &mut handlers);
|
||||
let config: RunnerConfig<Self> = from_v8(&mut scope, result).unwrap();
|
||||
|
||||
// rtx.send(RuntimeReturn::Value(unsafe { V8Value::new(SerdeValue::from(result)) }))
|
||||
rtx.send(RuntimeReturn::Config(config)).unwrap();
|
||||
}
|
||||
Event::ExecuteFunction(f, args) => {
|
||||
let value = unsafe { f.get_inner() }.get_value();
|
||||
let value = handlers[value];
|
||||
let mut scope = runtime.handle_scope();
|
||||
let context = Local::new(&mut scope, runtime.main_context());
|
||||
let global = context.global(&mut scope).into();
|
||||
let f: Local<'_, Function> = value.try_into().unwrap();
|
||||
let args: Vec<Local<'_, v8::Value>> = args
|
||||
.into_iter()
|
||||
.map(|a| unsafe {
|
||||
let r = a.get_inner().get_value();
|
||||
r
|
||||
})
|
||||
.collect();
|
||||
let result = f.call(&mut scope, global, args.as_slice());
|
||||
let result = result.map(|r| SerdeValue::from(r));
|
||||
|
||||
rtx.send(RuntimeReturn::OptionalValue(
|
||||
result.map(|result| unsafe { V8Value::new(SerdeValue::from(result)) }),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
runtime: Arc::new(Mutex::new(thread)),
|
||||
tx,
|
||||
rx: Arc::new(Mutex::new(rrx)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_event(&self, event: Event) -> RuntimeReturn {
|
||||
// locking before send to avoid runtime output shuffle
|
||||
// because reciever depends on sender
|
||||
// and runtime single-threaded anyway
|
||||
let rx = self.rx.lock().unwrap();
|
||||
self.tx.send(event).unwrap();
|
||||
rx.recv().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum V8Error {
|
||||
#[error("v8 data error: {0:?}")]
|
||||
DataError(#[from] v8::DataError),
|
||||
#[error("Failed to create v8 string: {0:?}")]
|
||||
StringCreation(String),
|
||||
#[error("Deno core error: {0:?}")]
|
||||
DenoCore(#[from] CoreError),
|
||||
#[error("error context: {0:?}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub struct V8Init {
|
||||
code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct V8Value {
|
||||
value: ValueHandler,
|
||||
runtime: Arc<V8Runtime>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ValueHandler {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl ValueHandler {
|
||||
fn get_value(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl V8Value {
|
||||
unsafe fn get_inner(&self) -> &ValueHandler {
|
||||
&self.value
|
||||
}
|
||||
unsafe fn new(runtime: Arc<V8Runtime>, value: ValueHandler) -> Self {
|
||||
Self { runtime, value }
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderDeserialize for V8Value {
|
||||
type Provider = V8Runtime;
|
||||
|
||||
fn de_into<T>(&self) -> Result<T, <Self::Provider as Provider>::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderSerialize for V8Value {
|
||||
type Provider = V8Runtime;
|
||||
|
||||
fn se_from<T: Serialize>(from: &T) -> Result<Self, <Self::Provider as Provider>::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for V8Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("V8Value")
|
||||
.field("value", &"_".to_string())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct V8Function {
|
||||
value: ValueHandler,
|
||||
runtime: Arc<Mutex<V8Runtime>>,
|
||||
}
|
||||
|
||||
impl V8Function {
|
||||
unsafe fn get_inner(&self) -> &ValueHandler {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderCall for V8Function {
|
||||
type Provider = V8Runtime;
|
||||
|
||||
fn call(
|
||||
&self,
|
||||
args: &[&<Self::Provider as Provider>::Value],
|
||||
) -> Result<Option<<Self::Provider as Provider>::Value>, <Self::Provider as Provider>::Error>
|
||||
{
|
||||
let result: RuntimeReturn =
|
||||
self.runtime
|
||||
.lock()
|
||||
.unwrap()
|
||||
.call_event(Event::ExecuteFunction(
|
||||
self.clone(),
|
||||
args.into_iter().map(|v| (*v).clone()).collect(),
|
||||
));
|
||||
Ok(result.as_optional_value().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for V8Function {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("V8Value")
|
||||
.field("value", &"_".to_string())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for V8Runtime {
|
||||
type Function = V8Function;
|
||||
|
||||
type Value = V8Value;
|
||||
|
||||
type Error = V8Error;
|
||||
|
||||
type InitData = V8Init;
|
||||
|
||||
fn init_config(&self, d: Self::InitData) -> Result<RunnerConfig<Self>, Self::Error> {
|
||||
let result = self.call_event(Event::GetScriptConfig(d.code));
|
||||
let value = result.as_config().unwrap();
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
13
src/runtimes/v8/value_replace.rs
Normal file
13
src/runtimes/v8/value_replace.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use deno_core::JsRuntime;
|
||||
use v8::{Local, Value};
|
||||
|
||||
/// move out unsupported by serde types to map, leaving key instead of value to get this value
|
||||
pub(crate) fn replace(
|
||||
value: &mut Local<'_, Value>,
|
||||
runtime: &mut JsRuntime,
|
||||
outmap: &mut HashMap<String, Local<'_, Value>>,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user