From 6aa8a44b3e39b8b12e7903f6fbc8afa4a9b10d91 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 27 Jul 2025 13:07:00 -0600 Subject: [PATCH] Started working on event system + Only used for "bad bot" for now but the framework is there --- Cargo.lock | 1 + Cargo.toml | 3 +- src/config.rs | 31 ++++++ src/discord/mod.rs | 26 ++++- src/event_listener/mod.rs | 203 ++++++++++++++++++++++++++++++++++++++ src/inventory/mod.rs | 11 ++- src/main.rs | 7 +- 7 files changed, 274 insertions(+), 8 deletions(-) create mode 100644 src/event_listener/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 2638292..88364b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1123,6 +1123,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tonic", + "tracing-core", "tracing-subscriber", ] diff --git a/Cargo.toml b/Cargo.toml index e38bdbb..29afc92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,10 +28,11 @@ tonic = "0.12.3" prost = "0.13.5" emojis = "0.6.2" poise = "0.6.1" -tracing-subscriber = "0.3.19" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } log = "0.4.26" cta-api = { version = "0.5.0", registry = "ahines"} thiserror = "2.0.12" +tracing-core = "0.1.33" [dependencies.tokio] version = "1.35.1" diff --git a/src/config.rs b/src/config.rs index 1d1e591..895134a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,13 @@ use crate::album_manager::AlbumManager; use crate::error::Error; +use crate::event_listener::{Action, Expiration, Listener, TriggerType}; use crate::migrations::{CURRENT_DB_VERSION, do_migration}; use config::{Config, File}; use cta_api::CTAClient; use j_db::database::{DB_METADATA_ID, Database}; use j_db::metadata::DBMetadata; +use j_db::model::JdbModel; +use log::info; use poise::serenity_prelude::model::id::ChannelId; use poise::serenity_prelude::model::prelude::{GuildId, UserId}; use poise::serenity_prelude::prelude::TypeMapKey; @@ -83,6 +86,32 @@ pub struct GlobalData { } impl GlobalData { + fn setup_system_listeners(db: &Database) -> Result<(), Error> { + db.filter(|_, listener: &Listener| listener.system_listener)? + .for_each(|listener: Listener| { + let _ = db.remove::(listener.id().unwrap()).is_ok(); + }); + + info!("Adding system listeners..."); + Listener::add_event( + db, + Listener::new( + TriggerType::OnMessage { + channel_id: None, + content: "bad bot".to_string(), + }, + vec![Action::React { + emoji: "😭".to_string(), + }], + 1.0, + Expiration::Never, + true, + ), + )?; + + Ok(()) + } + pub async fn new(args: Args, cfg: BotConfig) -> Result { let db = Database::new(&cfg.db_path)?; @@ -97,6 +126,8 @@ impl GlobalData { do_migration(&db); + Self::setup_system_listeners(&db)?; + Ok(Self { args, bot_state: Mutex::new(BotState::new().await?), diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 55fabc2..59da692 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -18,11 +18,12 @@ use crate::config::GlobalData; use crate::discord::fren_coin::give_coin; use crate::discord::joke::random; use crate::error::Error; +use crate::event_listener::{Listener, TriggerEvent, TriggerType}; use crate::models::lil_fren::lil_fren_task; use crate::models::social_credit::SocialCreditPhrase; use crate::models::task::Task; use crate::user::User; -use log::{error, info}; +use log::{debug, error, info}; use poise::serenity_prelude::{GuildId, Http, Message, MessageBuilder, ReactionType, RoleId}; use poise::{FrameworkOptions, find_command, serenity_prelude as serenity}; use rand::prelude::IteratorRandom; @@ -122,10 +123,6 @@ async fn handle_message( } }; - if new_message.content.to_lowercase().contains("bad bot") { - new_message.react(&ctx.http, '😭').await?; - } - give_coin(&data.db, new_message.author.id, 0.05, 10).await?; if let Some(phrase) = SocialCreditPhrase::check_if_match(&data.db, &new_message.content)? { @@ -140,6 +137,25 @@ async fn handle_message( User::update_social_credit(&data.db, new_message.author.id, social_credit_change)?; } + let trigger_event = TriggerEvent::new( + new_message.author.id, + TriggerType::OnMessage { + channel_id: Some(new_message.channel_id), + content: new_message.content.clone(), + }, + new_message.id, + new_message.channel_id, + ); + + match Listener::process_trigger(ctx, data, trigger_event).await { + Ok(_) => { + debug!("Processed message trigger successfully") + } + Err(err) => { + error!("Failed to process message trigger: {err}") + } + } + Ok(()) } diff --git a/src/event_listener/mod.rs b/src/event_listener/mod.rs new file mode 100644 index 0000000..66ec92e --- /dev/null +++ b/src/event_listener/mod.rs @@ -0,0 +1,203 @@ +use crate::config::GlobalData; +use crate::error::Error; +use crate::inventory::{ItemData, ItemType}; +use chrono::Utc; +use j_db::database::Database; +use log::{debug, error}; +use poise::serenity_prelude::{ArgumentConvert, ChannelId, Context, Emoji, MessageId, UserId}; +use rand::{Rng, rng}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::sync::Arc; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum TriggerType { + OnMessage { + channel_id: Option, + content: String, + }, + UseItem { + item: ItemType, + }, +} + +impl TriggerType { + pub fn match_event(&self, trigger_event: &Self) -> bool { + match (self, trigger_event) { + ( + TriggerType::OnMessage { + content: trigger, + channel_id: channel_id_trigger, + }, + TriggerType::OnMessage { + content: event_msg, + channel_id: channel_id_event, + }, + ) => { + let match_channel = if channel_id_trigger.is_some() { + channel_id_trigger == channel_id_event + } else { + true + }; + event_msg + .to_ascii_lowercase() + .contains(&trigger.to_ascii_lowercase()) + && match_channel + } + (TriggerType::UseItem { item: trigger }, TriggerType::UseItem { item: event_item }) => { + trigger == event_item + } + _ => false, + } + } +} + +#[derive(Debug, Clone)] +pub struct TriggerEvent { + triggerer: UserId, + trigger_type: TriggerType, + message_id: MessageId, + channel_id: ChannelId, +} + +impl TriggerEvent { + pub fn new( + triggerer: UserId, + trigger_type: TriggerType, + message_id: MessageId, + channel_id: ChannelId, + ) -> Self { + Self { + triggerer, + trigger_type, + message_id, + channel_id, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Action { + Kill { hours: u16 }, + Cancel { hours: u16 }, + UpdateSocialCredit { score_diff: i64 }, + UpdateFrenCoins { fren_coin_diff: i64 }, + GiveItem { item: ItemType, data: ItemData }, + TakeItem { item: ItemType }, + Speak { msg: String }, + React { emoji: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Expiration { + NumberOfTriggers(u16), + Time(chrono::DateTime), + Never, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Listener { + id: Option, + pub trigger: TriggerType, + pub actions: Vec, + pub trigger_chance: f64, + pub expiration: Expiration, + pub system_listener: bool, +} + +impl Listener { + pub fn new( + trigger: TriggerType, + actions: Vec, + trigger_chance: f64, + expiration: Expiration, + system_listener: bool, + ) -> Self { + Self { + id: None, + trigger, + actions, + trigger_chance, + expiration, + system_listener, + } + } + + pub fn check_if_triggers(&self, trigger_type: &TriggerType) -> bool { + self.trigger.match_event(trigger_type) && rng().random_bool(self.trigger_chance) + } + + pub async fn handle_action( + &self, + ctx: &Context, + data: &Arc, + trigger_event: &TriggerEvent, + ) -> Result<(), Error> { + for action in &self.actions { + if let Action::React { emoji } = action { + let msg = ctx + .http + .get_message(trigger_event.channel_id, trigger_event.message_id) + .await?; + + if emoji.chars().count() == 1 { + msg.react(&ctx.http, emoji.chars().next().unwrap()).await?; + } else { + let emoji = + Emoji::convert(&ctx.http, None, Some(msg.channel_id), emoji.as_str()) + .await + .unwrap(); + + msg.react(&ctx.http, emoji).await?; + } + } + } + + Ok(()) + } + + pub fn add_event(db: &Database, new_event: Self) -> Result { + debug!("Adding listener: {new_event:?}"); + Ok(db.insert(new_event)?) + } + + pub async fn process_trigger( + ctx: &Context, + data: &Arc, + trigger_event: TriggerEvent, + ) -> Result<(), Error> { + let triggered_listeners = data.db.filter(|_, listener: &Listener| { + listener.check_if_triggers(&trigger_event.trigger_type) + })?; + + for listener in triggered_listeners { + match listener.handle_action(ctx, data, &trigger_event).await { + Ok(_) => { + debug!("Processed event: {listener:?}"); + } + Err(err) => { + error!("Got error processing event '{listener:?}': {err}") + } + } + } + + Ok(()) + } +} + +impl j_db::model::JdbModel for Listener { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + fn tree() -> String { + "Events".to_string() + } +} diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index 55966bc..1d076ee 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -29,7 +29,16 @@ pub enum ItemData { } #[derive( - Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq, Copy, poise::ChoiceParameter, + Debug, + Clone, + Serialize, + Deserialize, + Hash, + Eq, + PartialEq, + Copy, + poise::ChoiceParameter, + PartialOrd, )] #[allow(clippy::upper_case_acronyms)] pub enum ItemType { diff --git a/src/main.rs b/src/main.rs index b6061ba..421e3ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod album_manager; mod config; mod discord; mod error; +mod event_listener; mod image_manipulation; mod inventory; mod migrations; @@ -14,6 +15,7 @@ use log::{error, info}; use magick_rust::magick_wand_genesis; use std::sync::Once; use structopt::StructOpt; +use tracing_subscriber::EnvFilter; const BAD_APPLE: &str = include_str!("assets/bad_apple.txt"); @@ -22,7 +24,10 @@ static START: Once = Once::new(); #[tokio::main] async fn main() { let args: Args = Args::from_args(); - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt() + .with_max_level(tracing_core::metadata::Level::INFO) + .with_env_filter(EnvFilter::from_default_env()) + .init(); let cfg = match BotConfig::new(&args.cfg_path) { Ok(cfg) => cfg,