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,
+);