Started working on event system

+ Only used for "bad bot" for now but the framework is there
This commit is contained in:
Joey Hines 2025-07-27 13:07:00 -06:00
parent bf43d63c8a
commit 6aa8a44b3e
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
7 changed files with 274 additions and 8 deletions

1
Cargo.lock generated
View File

@ -1123,6 +1123,7 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tonic", "tonic",
"tracing-core",
"tracing-subscriber", "tracing-subscriber",
] ]

View File

@ -28,10 +28,11 @@ tonic = "0.12.3"
prost = "0.13.5" prost = "0.13.5"
emojis = "0.6.2" emojis = "0.6.2"
poise = "0.6.1" poise = "0.6.1"
tracing-subscriber = "0.3.19" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
log = "0.4.26" log = "0.4.26"
cta-api = { version = "0.5.0", registry = "ahines"} cta-api = { version = "0.5.0", registry = "ahines"}
thiserror = "2.0.12" thiserror = "2.0.12"
tracing-core = "0.1.33"
[dependencies.tokio] [dependencies.tokio]
version = "1.35.1" version = "1.35.1"

View File

@ -1,10 +1,13 @@
use crate::album_manager::AlbumManager; use crate::album_manager::AlbumManager;
use crate::error::Error; use crate::error::Error;
use crate::event_listener::{Action, Expiration, Listener, TriggerType};
use crate::migrations::{CURRENT_DB_VERSION, do_migration}; use crate::migrations::{CURRENT_DB_VERSION, do_migration};
use config::{Config, File}; use config::{Config, File};
use cta_api::CTAClient; use cta_api::CTAClient;
use j_db::database::{DB_METADATA_ID, Database}; use j_db::database::{DB_METADATA_ID, Database};
use j_db::metadata::DBMetadata; 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::id::ChannelId;
use poise::serenity_prelude::model::prelude::{GuildId, UserId}; use poise::serenity_prelude::model::prelude::{GuildId, UserId};
use poise::serenity_prelude::prelude::TypeMapKey; use poise::serenity_prelude::prelude::TypeMapKey;
@ -83,6 +86,32 @@ pub struct GlobalData {
} }
impl 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>(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<Self, Error> { pub async fn new(args: Args, cfg: BotConfig) -> Result<Self, Error> {
let db = Database::new(&cfg.db_path)?; let db = Database::new(&cfg.db_path)?;
@ -97,6 +126,8 @@ impl GlobalData {
do_migration(&db); do_migration(&db);
Self::setup_system_listeners(&db)?;
Ok(Self { Ok(Self {
args, args,
bot_state: Mutex::new(BotState::new().await?), bot_state: Mutex::new(BotState::new().await?),

View File

@ -18,11 +18,12 @@ use crate::config::GlobalData;
use crate::discord::fren_coin::give_coin; use crate::discord::fren_coin::give_coin;
use crate::discord::joke::random; use crate::discord::joke::random;
use crate::error::Error; use crate::error::Error;
use crate::event_listener::{Listener, TriggerEvent, TriggerType};
use crate::models::lil_fren::lil_fren_task; use crate::models::lil_fren::lil_fren_task;
use crate::models::social_credit::SocialCreditPhrase; use crate::models::social_credit::SocialCreditPhrase;
use crate::models::task::Task; use crate::models::task::Task;
use crate::user::User; 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::serenity_prelude::{GuildId, Http, Message, MessageBuilder, ReactionType, RoleId};
use poise::{FrameworkOptions, find_command, serenity_prelude as serenity}; use poise::{FrameworkOptions, find_command, serenity_prelude as serenity};
use rand::prelude::IteratorRandom; 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?; 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)? { 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)?; 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(()) Ok(())
} }

203
src/event_listener/mod.rs Normal file
View File

@ -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<ChannelId>,
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<Utc>),
Never,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Listener {
id: Option<u64>,
pub trigger: TriggerType,
pub actions: Vec<Action>,
pub trigger_chance: f64,
pub expiration: Expiration,
pub system_listener: bool,
}
impl Listener {
pub fn new(
trigger: TriggerType,
actions: Vec<Action>,
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<GlobalData>,
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<Self, Error> {
debug!("Adding listener: {new_event:?}");
Ok(db.insert(new_event)?)
}
pub async fn process_trigger(
ctx: &Context,
data: &Arc<GlobalData>,
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<u64> {
self.id
}
fn set_id(&mut self, id: u64) {
self.id = Some(id)
}
fn tree() -> String {
"Events".to_string()
}
}

View File

@ -29,7 +29,16 @@ pub enum ItemData {
} }
#[derive( #[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)] #[allow(clippy::upper_case_acronyms)]
pub enum ItemType { pub enum ItemType {

View File

@ -2,6 +2,7 @@ mod album_manager;
mod config; mod config;
mod discord; mod discord;
mod error; mod error;
mod event_listener;
mod image_manipulation; mod image_manipulation;
mod inventory; mod inventory;
mod migrations; mod migrations;
@ -14,6 +15,7 @@ use log::{error, info};
use magick_rust::magick_wand_genesis; use magick_rust::magick_wand_genesis;
use std::sync::Once; use std::sync::Once;
use structopt::StructOpt; use structopt::StructOpt;
use tracing_subscriber::EnvFilter;
const BAD_APPLE: &str = include_str!("assets/bad_apple.txt"); const BAD_APPLE: &str = include_str!("assets/bad_apple.txt");
@ -22,7 +24,10 @@ static START: Once = Once::new();
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let args: Args = Args::from_args(); 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) { let cfg = match BotConfig::new(&args.cfg_path) {
Ok(cfg) => cfg, Ok(cfg) => cfg,