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]]
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
||||
|
|
|
|||
|
|
@ -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::{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 <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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,30 @@
|
|||
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<bool> {
|
||||
async fn listen(ctx: Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<bool> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +32,35 @@ impl EventHandler for 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
mod events;
|
||||
mod commands;
|
||||
mod models;
|
||||
mod schema;
|
||||
|
||||
use clap::ArgMatches;
|
||||
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