From aa931328b0f456e2484ee22abf5655816b1d9a4d Mon Sep 17 00:00:00 2001 From: jude Date: Thu, 11 May 2023 19:40:33 +0100 Subject: [PATCH] Support ephemeral reminder confirmations --- .../20230511125236_reminder_threads.sql | 1 - ...20230511180231_ephemeral_confirmations.sql | 1 + src/commands/moderation_cmds.rs | 76 ++++++++++++++++++- src/commands/reminder_cmds.rs | 30 +++++--- src/main.rs | 12 ++- src/models/channel_data.rs | 8 +- src/models/guild_data.rs | 48 ++++++++++++ src/models/mod.rs | 20 +++-- 8 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 migrations/20230511180231_ephemeral_confirmations.sql create mode 100644 src/models/guild_data.rs diff --git a/migrations/20230511125236_reminder_threads.sql b/migrations/20230511125236_reminder_threads.sql index 6dadba8..48559c6 100644 --- a/migrations/20230511125236_reminder_threads.sql +++ b/migrations/20230511125236_reminder_threads.sql @@ -1,2 +1 @@ --- Add migration script here ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL; diff --git a/migrations/20230511180231_ephemeral_confirmations.sql b/migrations/20230511180231_ephemeral_confirmations.sql new file mode 100644 index 0000000..25c56d5 --- /dev/null +++ b/migrations/20230511180231_ephemeral_confirmations.sql @@ -0,0 +1 @@ +ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0; diff --git a/src/commands/moderation_cmds.rs b/src/commands/moderation_cmds.rs index 666bd22..5f0f69c 100644 --- a/src/commands/moderation_cmds.rs +++ b/src/commands/moderation_cmds.rs @@ -102,6 +102,78 @@ You may want to use one of the popular timezones below, otherwise click [here](h Ok(()) } +/// Configure server settings +#[poise::command( + slash_command, + rename = "settings", + identifying_name = "settings", + guild_only = true +)] +pub async fn settings(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +/// Configure ephemeral setup +#[poise::command( + slash_command, + rename = "ephemeral", + identifying_name = "ephemeral_confirmations", + guild_only = true +)] +pub async fn ephemeral_confirmations(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +/// Set reminder confirmations to be sent "ephemerally" (private and cleared automatically) +#[poise::command( + slash_command, + rename = "on", + identifying_name = "set_ephemeral_confirmations", + guild_only = true +)] +pub async fn set_ephemeral_confirmations(ctx: Context<'_>) -> Result<(), Error> { + let mut guild_data = ctx.guild_data().await.unwrap()?; + guild_data.ephemeral_confirmations = true; + guild_data.commit_changes(&ctx.data().database).await; + + ctx.send(|r| { + r.ephemeral(true).embed(|e| { + e.title("Confirmations ephemeral") + .description("Reminder confirmations will be sent privately, and removed when your client restarts.") + .color(*THEME_COLOR) + }) + }) + .await?; + + Ok(()) +} + +/// Set reminder confirmations to persist indefinitely +#[poise::command( + slash_command, + rename = "off", + identifying_name = "unset_ephemeral_confirmations", + guild_only = true +)] +pub async fn unset_ephemeral_confirmations(ctx: Context<'_>) -> Result<(), Error> { + let mut guild_data = ctx.guild_data().await.unwrap()?; + guild_data.ephemeral_confirmations = false; + guild_data.commit_changes(&ctx.data().database).await; + + ctx.send(|r| { + r.ephemeral(true).embed(|e| { + e.title("Confirmations public") + .description( + "Reminder confirmations will be sent as regular messages, and won't be removed automatically.", + ) + .color(*THEME_COLOR) + }) + }) + .await?; + + Ok(()) +} + /// Configure whether other users can set reminders to your direct messages #[poise::command(slash_command, rename = "dm", identifying_name = "allowed_dm")] pub async fn allowed_dm(_ctx: Context<'_>) -> Result<(), Error> { @@ -109,7 +181,7 @@ pub async fn allowed_dm(_ctx: Context<'_>) -> Result<(), Error> { } /// Allow other users to set reminders in your direct messages -#[poise::command(slash_command, rename = "allow", identifying_name = "allowed_dm")] +#[poise::command(slash_command, rename = "allow", identifying_name = "set_allowed_dm")] pub async fn set_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { let mut user_data = ctx.author_data().await?; user_data.allowed_dm = true; @@ -128,7 +200,7 @@ pub async fn set_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { } /// Block other users from setting reminders in your direct messages -#[poise::command(slash_command, rename = "block", identifying_name = "allowed_dm")] +#[poise::command(slash_command, rename = "block", identifying_name = "unset_allowed_dm")] pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> { let mut user_data = ctx.author_data().await?; user_data.allowed_dm = false; diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index 25e877a..efc7e51 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -645,7 +645,13 @@ async fn create_reminder( return Ok(()); } - ctx.defer().await?; + let ephemeral = + ctx.guild_data().await.map_or(false, |gr| gr.map_or(false, |g| g.ephemeral_confirmations)); + if ephemeral { + ctx.defer_ephemeral().await?; + } else { + ctx.defer().await?; + } let user_data = ctx.author_data().await.unwrap(); let timezone = timezone.unwrap_or(ctx.timezone().await); @@ -692,9 +698,10 @@ async fn create_reminder( }, ) } else { - ctx.say( - "`repeat` is only available to Patreon subscribers or self-hosted users", - ) + ctx.send(|b| { + b.content( + "`repeat` is only available to Patreon subscribers or self-hosted users") + }) .await?; return Ok(()); @@ -704,13 +711,18 @@ async fn create_reminder( }; if processed_interval.is_none() && interval.is_some() { - ctx.say( - "Repeat interval could not be processed. Try similar to `1 hour` or `4 days`", - ) + ctx.send(|b| { + b.content( + "Repeat interval could not be processed. Try similar to `1 hour` or `4 days`") + }) .await?; } else if processed_expires.is_none() && expires.is_some() { - ctx.say("Expiry time failed to process. Please make it as clear as possible") - .await?; + ctx.send(|b| { + b.ephemeral(true).content( + "Expiry time failed to process. Please make it as clear as possible", + ) + }) + .await?; } else { let mut builder = MultiReminderBuilder::new(&ctx, ctx.guild_id()) .author(user_data) diff --git a/src/main.rs b/src/main.rs index 58455cb..9cc4cdb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,7 +91,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { if Path::new("/etc/reminder-rs/config.env").exists() { dotenv::from_path("/etc/reminder-rs/config.env")?; } else { - dotenv::from_path(".env")?; + let _ = dotenv::dotenv(); } let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment"); @@ -112,6 +112,16 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { ], ..moderation_cmds::allowed_dm() }, + poise::Command { + subcommands: vec![poise::Command { + subcommands: vec![ + moderation_cmds::set_ephemeral_confirmations(), + moderation_cmds::unset_ephemeral_confirmations(), + ], + ..moderation_cmds::ephemeral_confirmations() + }], + ..moderation_cmds::settings() + }, moderation_cmds::webhook(), poise::Command { subcommands: vec![ diff --git a/src/models/channel_data.rs b/src/models/channel_data.rs index c3d2f0c..9983056 100644 --- a/src/models/channel_data.rs +++ b/src/models/channel_data.rs @@ -22,9 +22,7 @@ impl ChannelData { if let Ok(c) = sqlx::query_as_unchecked!( Self, - " -SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ? - ", + "SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?", channel_id ) .fetch_one(pool) @@ -37,9 +35,7 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) }; sqlx::query!( - " -INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?)) - ", + "INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))", channel_id, channel_name, guild_id diff --git a/src/models/guild_data.rs b/src/models/guild_data.rs new file mode 100644 index 0000000..21153c4 --- /dev/null +++ b/src/models/guild_data.rs @@ -0,0 +1,48 @@ +use poise::serenity_prelude::GuildId; +use sqlx::MySqlPool; + +pub struct GuildData { + pub ephemeral_confirmations: bool, + pub id: u32, +} + +impl GuildData { + pub async fn from_guild( + guild_id: GuildId, + pool: &MySqlPool, + ) -> Result> { + if let Ok(c) = sqlx::query_as_unchecked!( + Self, + "SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?", + guild_id.0 + ) + .fetch_one(pool) + .await + { + Ok(c) + } else { + sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id.0) + .execute(&pool.clone()) + .await?; + + Ok(sqlx::query_as_unchecked!( + Self, + "SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?", + guild_id.0 + ) + .fetch_one(pool) + .await?) + } + } + + pub async fn commit_changes(&self, pool: &MySqlPool) { + sqlx::query!( + "UPDATE guilds SET ephemeral_confirmations = ? WHERE id = ?", + self.ephemeral_confirmations, + self.id + ) + .execute(pool) + .await + .unwrap(); + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 760d15a..c81d5af 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,6 @@ pub mod channel_data; pub mod command_macro; +pub mod guild_data; pub mod reminder; pub mod timer; pub mod user_data; @@ -8,7 +9,7 @@ use chrono_tz::Tz; use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelType}; use crate::{ - models::{channel_data::ChannelData, user_data::UserData}, + models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData}, CommandMacro, Context, Data, Error, GuildId, }; @@ -18,6 +19,8 @@ pub trait CtxData { async fn author_data(&self) -> Result; + async fn guild_data(&self) -> Option>; + async fn timezone(&self) -> Tz; async fn channel_data(&self) -> Result; @@ -27,17 +30,22 @@ pub trait CtxData { #[async_trait] impl CtxData for Context<'_> { - async fn user_data + Send>( - &self, - user_id: U, - ) -> Result> { + async fn user_data + Send>(&self, user_id: U) -> Result { UserData::from_user(user_id, &self.discord(), &self.data().database).await } - async fn author_data(&self) -> Result> { + async fn author_data(&self) -> Result { UserData::from_user(&self.author().id, &self.discord(), &self.data().database).await } + async fn guild_data(&self) -> Option> { + if let Some(guild_id) = self.guild_id() { + Some(GuildData::from_guild(guild_id, &self.data().database).await) + } else { + None + } + } + async fn timezone(&self) -> Tz { UserData::timezone_of(self.author().id, &self.data().database).await }