Add reminder functionality
This commit is contained in:
parent
e20a327db1
commit
ce8836ed9c
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="hal_development@localhost" uuid="e615e80e-8d72-443f-bb35-e5745367f643">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://localhost:5432/hal_development</jdbc-url>
|
||||||
|
<jdbc-additional-properties>
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||||
|
</jdbc-additional-properties>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/migrations/2025-05-04-131216_create reminder table/up.sql" dialect="GenericSQL" />
|
||||||
|
<file url="PROJECT" dialect="PostgreSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -922,10 +922,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hal"
|
name = "hal"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"built",
|
"built",
|
||||||
"clap",
|
"clap",
|
||||||
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1405,8 +1406,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "manifold"
|
name = "manifold"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
source = "git+https://code.orbiter-radio.uk/discord/manifold.git#8602c883d3d3146b0ec8258f4986d33203e5e92a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"built",
|
"built",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ built = { version = "0.8.0", features = ["git2", "semver", "chrono"] }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
built = { version = "0.8.0", features = ["git2", "semver", "chrono"] }
|
built = { version = "0.8.0", features = ["git2", "semver", "chrono"] }
|
||||||
clap = { version = "4.3.23", features = ["cargo"] }
|
clap = { version = "4.3.23", features = ["cargo"] }
|
||||||
|
diesel = { version = "2.1.0", features = ["postgres", "r2d2", "chrono"] }
|
||||||
diesel_migrations = "2.1.0"
|
diesel_migrations = "2.1.0"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
|
|
@ -17,4 +18,3 @@ manifold = { git = "https://code.orbiter-radio.uk/discord/manifold.git" }
|
||||||
poise = { version = "0.5.*", features = [ "cache" ] }
|
poise = { version = "0.5.*", features = [ "cache" ] }
|
||||||
rand = { version = "0.9.1", features = ["std_rng"] }
|
rand = { version = "0.9.1", features = ["std_rng"] }
|
||||||
tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] }
|
tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE IF EXISTS "reminders";
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -1,21 +1,62 @@
|
||||||
use manifold::error::{ManifoldError, ManifoldResult};
|
use manifold::error::{ManifoldError, ManifoldResult};
|
||||||
use manifold::{ManifoldContext, ManifoldData};
|
use manifold::{ManifoldContext, ManifoldData};
|
||||||
use poise::serenity_prelude::RoleId;
|
use poise::serenity_prelude::{ChannelId, RoleId};
|
||||||
use poise::ChoiceParameter;
|
use models::reminder::ReminderCadence;
|
||||||
|
use crate::hal::models;
|
||||||
|
use crate::hal::models::reminder::Reminder;
|
||||||
|
|
||||||
#[derive(ChoiceParameter)]
|
#[poise::command(slash_command, subcommands("add", "list", "delete"))]
|
||||||
enum ReminderCadence {
|
async fn reminder(_ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
|
||||||
Daily,
|
Ok(())
|
||||||
Weekly,
|
|
||||||
Monthly,
|
|
||||||
Yearly,
|
|
||||||
Weekdaily,
|
|
||||||
Fortnightly,
|
|
||||||
FourWeekly,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command, prefix_command, aliases("r"))]
|
#[poise::command(slash_command)]
|
||||||
async fn reminder(_ctx: ManifoldContext<'_>, _target: RoleId, _cadence: ReminderCadence, _tod: u8) -> ManifoldResult<()> {
|
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 <t:{next_alert_time}>.", 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 <t:{next_alert_time}>", 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,66 @@
|
||||||
|
use crate::hal::models::reminder::Reminder;
|
||||||
|
use built::chrono::Utc;
|
||||||
use manifold::error::{ManifoldError, ManifoldResult};
|
use manifold::error::{ManifoldError, ManifoldResult};
|
||||||
use manifold::events::EventHandler;
|
use manifold::events::EventHandler;
|
||||||
use manifold::ManifoldData;
|
use manifold::{Db, ManifoldData};
|
||||||
use poise::serenity_prelude::{Context, Ready};
|
use poise::serenity_prelude::{ChannelId, Context, TypeMapKey};
|
||||||
use poise::{async_trait, Event, FrameworkContext};
|
use poise::{async_trait, Event, FrameworkContext};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub struct HalHandler {
|
pub struct HalHandler {
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HalTimer;
|
||||||
|
|
||||||
|
impl TypeMapKey for HalTimer {
|
||||||
|
type Value = AtomicBool;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl EventHandler for HalHandler {
|
impl EventHandler for HalHandler {
|
||||||
async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<bool> {
|
async fn listen(ctx: Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<bool> {
|
||||||
match event {
|
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::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::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)
|
_ => Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HalHandler {
|
impl HalHandler {
|
||||||
|
|
||||||
async fn standard_startup(_ctx: &Context, _framework_ctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, _data_about_bot: &Ready) -> ManifoldResult<bool> {
|
async fn standard_startup(ctx: Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>) -> ManifoldResult<bool> {
|
||||||
|
|
||||||
|
let arc_db: Arc<Db> = 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::<HalTimer>();
|
||||||
|
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<Db> = 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)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
mod events;
|
mod events;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod models;
|
||||||
|
mod schema;
|
||||||
|
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
use poise::serenity_prelude::GatewayIntents;
|
use poise::serenity_prelude::GatewayIntents;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod reminder;
|
||||||
|
|
@ -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<String> 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<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reminder {
|
||||||
|
pub fn get_next_alert_time(cadence: &ReminderCadence, time_of_day: &i32) -> ManifoldResult<i64> {
|
||||||
|
|
||||||
|
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<Self> {
|
||||||
|
Ok(reminders::table
|
||||||
|
.filter(reminders::id.eq(needle))
|
||||||
|
.select(Reminder::as_select())
|
||||||
|
.get_result(&mut conn.get()?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all(conn: &Db) -> ManifoldResult<Vec<Self>> {
|
||||||
|
Ok(reminders::table.load::<Reminder>(&mut conn.get()?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_next_alert(conn: &Db, needle: &i64) -> ManifoldResult<Vec<Self>> {
|
||||||
|
Ok(reminders::table
|
||||||
|
.filter(reminders::next_alert_time.le(needle))
|
||||||
|
.load::<Reminder>(&mut conn.get()?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&self, conn: &Db) -> ManifoldResult<usize> {
|
||||||
|
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<usize> {
|
||||||
|
|
||||||
|
Ok(delete(reminders::table)
|
||||||
|
.filter(reminders::id.eq(needle))
|
||||||
|
.execute(&mut conn.get()?)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Int8>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
userinfo (user_id) {
|
||||||
|
user_id -> Int8,
|
||||||
|
username -> Text,
|
||||||
|
#[max_length = 36]
|
||||||
|
weather_location -> Nullable<Varchar>,
|
||||||
|
#[max_length = 1]
|
||||||
|
weather_units -> Nullable<Varchar>,
|
||||||
|
#[max_length = 8]
|
||||||
|
timezone -> Nullable<Varchar>,
|
||||||
|
last_seen -> Nullable<Int8>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
reminders,
|
||||||
|
userinfo,
|
||||||
|
);
|
||||||
Loading…
Reference in New Issue