From ce8836ed9ca4602d4c8a5234fc6a2638562f8665 Mon Sep 17 00:00:00 2001 From: Lucy Bladen Date: Sun, 4 May 2025 16:47:43 +0100 Subject: [PATCH] Add reminder functionality --- .idea/dataSources.xml | 18 +++ .idea/sqldialects.xml | 7 ++ Cargo.lock | 6 +- Cargo.toml | 2 +- diesel.toml | 9 ++ .../down.sql | 2 + .../up.sql | 10 ++ src/hal/commands/utility.rs | 67 +++++++++-- src/hal/events.rs | 50 +++++++- src/hal/mod.rs | 2 + src/hal/models/mod.rs | 1 + src/hal/models/reminder.rs | 112 ++++++++++++++++++ src/hal/schema.rs | 32 +++++ 13 files changed, 295 insertions(+), 23 deletions(-) create mode 100644 .idea/dataSources.xml create mode 100644 .idea/sqldialects.xml create mode 100644 diesel.toml create mode 100644 migrations/2025-05-04-131216_create reminder table/down.sql create mode 100644 migrations/2025-05-04-131216_create reminder table/up.sql create mode 100644 src/hal/models/mod.rs create mode 100644 src/hal/models/reminder.rs create mode 100644 src/hal/schema.rs diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..63c87d4 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,18 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/hal_development + + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..b3b498d --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 68d9f99..707a680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -922,10 +922,11 @@ dependencies = [ [[package]] name = "hal" -version = "0.1.0" +version = "1.0.0" dependencies = [ "built", "clap", + "diesel", "diesel_migrations", "env_logger", "log", @@ -1405,8 +1406,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "manifold" -version = "7.0.0" -source = "git+https://code.orbiter-radio.uk/discord/manifold.git#8602c883d3d3146b0ec8258f4986d33203e5e92a" +version = "8.0.0" dependencies = [ "built", "chrono", diff --git a/Cargo.toml b/Cargo.toml index de422ef..c8b6900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ built = { version = "0.8.0", features = ["git2", "semver", "chrono"] } [dependencies] built = { version = "0.8.0", features = ["git2", "semver", "chrono"] } clap = { version = "4.3.23", features = ["cargo"] } +diesel = { version = "2.1.0", features = ["postgres", "r2d2", "chrono"] } diesel_migrations = "2.1.0" env_logger = "0.10.0" log = "0.4.20" @@ -17,4 +18,3 @@ manifold = { git = "https://code.orbiter-radio.uk/discord/manifold.git" } poise = { version = "0.5.*", features = [ "cache" ] } rand = { version = "0.9.1", features = ["std_rng"] } tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] } - diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..a033338 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/hal/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId"] + +[migrations_directory] +dir = "migrations" diff --git a/migrations/2025-05-04-131216_create reminder table/down.sql b/migrations/2025-05-04-131216_create reminder table/down.sql new file mode 100644 index 0000000..4500b44 --- /dev/null +++ b/migrations/2025-05-04-131216_create reminder table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS "reminders"; diff --git a/migrations/2025-05-04-131216_create reminder table/up.sql b/migrations/2025-05-04-131216_create reminder table/up.sql new file mode 100644 index 0000000..c33519e --- /dev/null +++ b/migrations/2025-05-04-131216_create reminder table/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS "reminders" ( + id SERIAL PRIMARY KEY, + cadence TEXT NOT NULL, + target_role BIGINT NOT NULL, + target_channel BIGINT NOT NULL, + time_of_day INTEGER NOT NULL, + message_content TEXT NOT NULL, + next_alert_time BIGINT +); \ No newline at end of file diff --git a/src/hal/commands/utility.rs b/src/hal/commands/utility.rs index 2da22de..5699d05 100644 --- a/src/hal/commands/utility.rs +++ b/src/hal/commands/utility.rs @@ -1,21 +1,62 @@ use manifold::error::{ManifoldError, ManifoldResult}; use manifold::{ManifoldContext, ManifoldData}; -use poise::serenity_prelude::RoleId; -use poise::ChoiceParameter; +use poise::serenity_prelude::{ChannelId, RoleId}; +use models::reminder::ReminderCadence; +use crate::hal::models; +use crate::hal::models::reminder::Reminder; -#[derive(ChoiceParameter)] -enum ReminderCadence { - Daily, - Weekly, - Monthly, - Yearly, - Weekdaily, - Fortnightly, - FourWeekly, +#[poise::command(slash_command, subcommands("add", "list", "delete"))] +async fn reminder(_ctx: ManifoldContext<'_>) -> ManifoldResult<()> { + Ok(()) } -#[poise::command(slash_command, prefix_command, aliases("r"))] -async fn reminder(_ctx: ManifoldContext<'_>, _target: RoleId, _cadence: ReminderCadence, _tod: u8) -> ManifoldResult<()> { +#[poise::command(slash_command)] +async fn add(ctx: ManifoldContext<'_>, target_role: RoleId, tgt_channel: ChannelId, cadence: ReminderCadence, time_of_day: i32, message_content: String) -> ManifoldResult<()> { + + let db = &ctx.data().database; + + let nat = Reminder::get_next_alert_time(&cadence, &time_of_day)?; + + Reminder { + id: 0, + cadence: cadence.to_string(), + target_role: target_role.into(), + target_channel: tgt_channel.into(), + time_of_day, + message_content: message_content.clone(), + next_alert_time: Some(nat), + }.insert(&db)?; + + ctx.reply(format!("Okay, I have set up a {cadence} reminder at {time_of_day} in <#{tgt_channel}> for <@{target_role}> with the message: {message_content}. It will first trigger at .", cadence = cadence, time_of_day = time_of_day, tgt_channel = tgt_channel, target_role = target_role, message_content = &message_content, next_alert_time = nat)).await?; + + Ok(()) +} + +#[poise::command(slash_command)] +async fn delete(ctx: ManifoldContext<'_>, target: i32) -> ManifoldResult<()> { + + let db = &ctx.data().database; + + Reminder::delete_by_id(&db, target)?; + + ctx.reply("Okay. I removed that reminder. I promise.").await?; + + Ok(()) +} + +#[poise::command(slash_command)] +async fn list(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { + let db = &ctx.data().database; + + let reminders = Reminder::get_all(&db)?; + + ctx.reply(format!("I currently have {count} reminder(s) stored.", count = reminders.len())).await?; + for reminder in reminders { + ctx.reply(format!("Reminder ID: {id} notifies role {target_role} in channel <#{target_channel}> at {time_of_day}:00 (ish) every {cadence}. It will next fire ", id = reminder.id, target_role = reminder.target_role, target_channel = reminder.target_channel, time_of_day = reminder.time_of_day, cadence = reminder.cadence, next_alert_time = reminder.next_alert_time.unwrap_or(0))).await?; + } + + ctx.reply("That's all the reminders I know about.").await?; + Ok(()) } diff --git a/src/hal/events.rs b/src/hal/events.rs index 56fcec4..a8330e2 100644 --- a/src/hal/events.rs +++ b/src/hal/events.rs @@ -1,28 +1,66 @@ +use crate::hal::models::reminder::Reminder; +use built::chrono::Utc; use manifold::error::{ManifoldError, ManifoldResult}; use manifold::events::EventHandler; -use manifold::ManifoldData; -use poise::serenity_prelude::{Context, Ready}; +use manifold::{Db, ManifoldData}; +use poise::serenity_prelude::{ChannelId, Context, TypeMapKey}; use poise::{async_trait, Event, FrameworkContext}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; pub struct HalHandler { +} +struct HalTimer; + +impl TypeMapKey for HalTimer { + type Value = AtomicBool; } #[async_trait] impl EventHandler for HalHandler { - async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult { + async fn listen(ctx: Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult { match event { + Event::CacheReady { guilds: _guilds } => HalHandler::standard_startup(ctx, framework_ctx).await, Event::MessageDelete { channel_id: _channel_id, deleted_message_id: _deleted_message_id, guild_id: _guild_id } => HalHandler::message_deleted().await, Event::MessageUpdate { old_if_available: _old_if_available, new: _new, event: _event } => HalHandler::message_edited().await, - Event::Ready { data_about_bot } => HalHandler::standard_startup(&ctx, &framework_ctx, data_about_bot).await, _ => Ok(false) } } } impl HalHandler { - - async fn standard_startup(_ctx: &Context, _framework_ctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, _data_about_bot: &Ready) -> ManifoldResult { + + async fn standard_startup(ctx: Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>) -> ManifoldResult { + + let arc_db: Arc = Arc::new(Db { pool: framework_ctx.user_data().await.database.clone() }); + let ctx_clone = Arc::clone(&Arc::new(ctx.clone())); + let mut data = ctx.data.write().await; + let timer_running = data.get_mut::(); + if timer_running.is_none_or(|x| !x.load(Ordering::Relaxed)) { + info!("Timer init."); + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(60)).await; + info!("Waking to check for scheduled actions"); + let db: Arc = Arc::clone(&arc_db); + let curr_time = Utc::now().timestamp(); + + for mut reminder in Reminder::get_by_next_alert(&db, &curr_time).unwrap() { + _ = ChannelId(reminder.target_channel as u64).say(&ctx_clone, format!("Scheduled notifier for <@&{target_role}>: {message_content}", target_role = &reminder.target_role, message_content = &reminder.message_content)).await; + + reminder.next_alert_time = Some(Reminder::get_next_alert_time(&reminder.cadence.clone().into(), &reminder.time_of_day).unwrap()); + + // Store new alert time + _ = reminder.insert(&db); + } + } + }); + } else { + info!("Not initialising timer due to pre-check"); + } + Ok(false) } diff --git a/src/hal/mod.rs b/src/hal/mod.rs index a789f73..461c540 100644 --- a/src/hal/mod.rs +++ b/src/hal/mod.rs @@ -1,5 +1,7 @@ mod events; mod commands; +mod models; +mod schema; use clap::ArgMatches; use poise::serenity_prelude::GatewayIntents; diff --git a/src/hal/models/mod.rs b/src/hal/models/mod.rs new file mode 100644 index 0000000..dfcb1a0 --- /dev/null +++ b/src/hal/models/mod.rs @@ -0,0 +1 @@ +pub mod reminder; \ No newline at end of file diff --git a/src/hal/models/reminder.rs b/src/hal/models/reminder.rs new file mode 100644 index 0000000..ae9c5c0 --- /dev/null +++ b/src/hal/models/reminder.rs @@ -0,0 +1,112 @@ +use built::chrono::{Datelike, Weekday}; +use built::chrono; +use built::chrono::{Duration, Timelike}; +use diesel::dsl::delete; +use diesel::prelude::*; +use diesel::insert_into; +use manifold::Db; +use manifold::error::{ManifoldError, ManifoldResult}; +use poise::ChoiceParameter; +use crate::hal::schema::*; + +#[derive(ChoiceParameter)] +pub enum ReminderCadence { + Daily, + Weekly, + Weekdaily, + Fortnightly, + FourWeekly, +} + +impl From for ReminderCadence { + fn from(s: String) -> Self { + match s.to_lowercase().as_str() { + "daily" => ReminderCadence::Daily, + "weekly" => ReminderCadence::Weekly, + "weekdaily" => ReminderCadence::Weekdaily, + "fortnightly" => ReminderCadence::Fortnightly, + "fourweekly" => ReminderCadence::FourWeekly, + _ => ReminderCadence::Daily, + } + } +} + +#[derive(Queryable, Selectable, Identifiable, Insertable, AsChangeset, Debug, PartialEq, Clone)] +#[diesel(table_name = reminders)] +pub struct Reminder { + pub id: i32, + pub cadence: String, + pub target_role: i64, + pub target_channel: i64, + pub time_of_day: i32, + pub message_content: String, + pub next_alert_time: Option, +} + +impl Reminder { + pub fn get_next_alert_time(cadence: &ReminderCadence, time_of_day: &i32) -> ManifoldResult { + + let current_time = chrono::Utc::now(); + + let delta: i64 = match cadence { + ReminderCadence::Daily => { + let tt = (current_time + Duration::days(1)).with_hour(time_of_day.to_owned() as u32).unwrap().with_minute(0).unwrap().with_second(0).unwrap(); + tt.signed_duration_since(current_time).to_std().unwrap().as_secs() as i64 + } + ReminderCadence::Weekly => { + let tt = (current_time + Duration::days(7)).with_hour(time_of_day.to_owned() as u32).unwrap().with_minute(0).unwrap().with_second(0).unwrap(); + tt.signed_duration_since(current_time).to_std().unwrap().as_secs() as i64 + }, + ReminderCadence::Weekdaily => { + // if today is friday or saturday bump the delta across the weekend + // this ignores bank holidays and so should you + let days_to_add = match current_time.weekday() { + Weekday::Fri => 3, + Weekday::Sat => 2, + _ => 1, + }; + + let tt = (current_time + Duration::days(days_to_add)).with_hour(time_of_day.to_owned() as u32).unwrap().with_minute(0).unwrap().with_second(0).unwrap(); + tt.signed_duration_since(current_time).to_std().unwrap().as_secs() as i64 + } + ReminderCadence::Fortnightly => 1209600, + ReminderCadence::FourWeekly => 2419200, + }; + + Ok(chrono::Utc::now().timestamp() + delta) + } + + pub fn get_by_id(conn: &Db, needle: &i32) -> ManifoldResult { + Ok(reminders::table + .filter(reminders::id.eq(needle)) + .select(Reminder::as_select()) + .get_result(&mut conn.get()?)?) + } + + pub fn get_all(conn: &Db) -> ManifoldResult> { + Ok(reminders::table.load::(&mut conn.get()?)?) + } + + pub fn get_by_next_alert(conn: &Db, needle: &i64) -> ManifoldResult> { + Ok(reminders::table + .filter(reminders::next_alert_time.le(needle)) + .load::(&mut conn.get()?)?) + } + + pub fn insert(&self, conn: &Db) -> ManifoldResult { + insert_into(reminders::table) + .values(self) + .on_conflict(reminders::id) + .do_update() + .set(self) + .execute(&mut conn.get()?) + .map_err(|e| ManifoldError::from(e)) + } + + pub fn delete_by_id(conn: &Db, needle: i32) -> ManifoldResult { + + Ok(delete(reminders::table) + .filter(reminders::id.eq(needle)) + .execute(&mut conn.get()?)?) + } +} \ No newline at end of file diff --git a/src/hal/schema.rs b/src/hal/schema.rs new file mode 100644 index 0000000..4482dd1 --- /dev/null +++ b/src/hal/schema.rs @@ -0,0 +1,32 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + reminders (id) { + id -> Int4, + cadence -> Text, + target_role -> Int8, + target_channel -> Int8, + time_of_day -> Int4, + message_content -> Text, + next_alert_time -> Nullable, + } +} + +diesel::table! { + userinfo (user_id) { + user_id -> Int8, + username -> Text, + #[max_length = 36] + weather_location -> Nullable, + #[max_length = 1] + weather_units -> Nullable, + #[max_length = 8] + timezone -> Nullable, + last_seen -> Nullable, + } +} + +diesel::allow_tables_to_appear_in_same_query!( + reminders, + userinfo, +);