Link all top-level commands with macro recording/replaying logic

This commit is contained in:
jude 2024-02-18 13:24:37 +00:00
parent 5e39e16060
commit 76a286076b
25 changed files with 619 additions and 410 deletions

13
Cargo.lock generated
View File

@ -774,7 +774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]] [[package]]
name = "extract_macro" name = "extract_derive"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"quote", "quote",
@ -2235,6 +2235,14 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "recordable_derive"
version = "0.1.0"
dependencies = [
"quote",
"syn 2.0.49",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@ -2317,7 +2325,7 @@ dependencies = [
"chrono-tz", "chrono-tz",
"dotenv", "dotenv",
"env_logger", "env_logger",
"extract_macro", "extract_derive",
"lazy-regex", "lazy-regex",
"lazy_static", "lazy_static",
"levenshtein", "levenshtein",
@ -2326,6 +2334,7 @@ dependencies = [
"poise", "poise",
"postman", "postman",
"rand", "rand",
"recordable_derive",
"regex", "regex",
"reminder_web", "reminder_web",
"reqwest", "reqwest",

View File

@ -35,8 +35,11 @@ path = "postman"
[dependencies.reminder_web] [dependencies.reminder_web]
path = "web" path = "web"
[dependencies.extract_macro] [dependencies.extract_derive]
path = "extract_macro" path = "extract_derive"
[dependencies.recordable_derive]
path = "recordable_derive"
[package.metadata.deb] [package.metadata.deb]
depends = "$auto, python3-dateparser (>= 1.0.0)" depends = "$auto, python3-dateparser (>= 1.0.0)"

View File

@ -1,5 +1,5 @@
[package] [package]
name = "extract_macro" name = "extract_derive"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View File

@ -12,6 +12,7 @@ fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident; let name = &ast.ident;
match &ast.data { match &ast.data {
// Dispatch over struct: extract args directly from context
Data::Struct(st) => match &st.fields { Data::Struct(st) => match &st.fields {
Fields::Named(fields) => { Fields::Named(fields) => {
let extracted = fields.named.iter().map(|field| { let extracted = fields.named.iter().map(|field| {

View File

@ -0,0 +1,11 @@
[package]
name = "recordable_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.35"
syn = { version = "2.0.49", features = ["full"] }

View File

@ -0,0 +1,42 @@
use proc_macro::TokenStream;
use syn::{spanned::Spanned, Data};
/// Macro to allow Recordable to be implemented on an enum by dispatching over each variant.
#[proc_macro_derive(Recordable)]
pub fn extract(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input);
impl_recordable(&ast)
}
fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
match &ast.data {
Data::Enum(en) => {
let extracted = en.variants.iter().map(|var| {
let ident = &var.ident;
quote::quote_spanned! {var.span()=>
Self::#ident (opt) => opt.run(ctx).await?
}
});
TokenStream::from(quote::quote! {
impl Recordable for #name {
async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> {
match self {
#(#extracted,)*
}
Ok(())
}
}
})
}
_ => {
panic!("Only enums can derive Recordable");
}
}
}

View File

@ -2,12 +2,17 @@ use chrono::Utc;
use poise::CreateReply; use poise::CreateReply;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{models::CtxData, utils::Extract, Context, Error}; use crate::{
models::CtxData,
utils::{Extract, Recordable},
Context, Error,
};
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options; pub struct Options;
pub async fn clock(ctx: Context<'_>, _options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
ctx.defer_ephemeral().await?; ctx.defer_ephemeral().await?;
let tz = ctx.timezone().await; let tz = ctx.timezone().await;
@ -21,10 +26,11 @@ pub async fn clock(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
.await?; .await?;
Ok(()) Ok(())
}
} }
/// View the current time in your selected timezone /// View the current time in your selected timezone
#[poise::command(slash_command, rename = "clock")] #[poise::command(slash_command, rename = "clock", identifying_name = "clock")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> { pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
clock(ctx, Options {}).await (Options {}).run(ctx).await
} }

View File

@ -1,7 +1,10 @@
use poise::{serenity_prelude::CreateEmbed, CreateReply}; use poise::{serenity_prelude::CreateEmbed, CreateReply};
use super::super::autocomplete::macro_name_autocomplete; use super::super::autocomplete::macro_name_autocomplete;
use crate::{models::command_macro::guild_command_macro, Context, Data, Error, THEME_COLOR}; use crate::{
models::command_macro::guild_command_macro, utils::Recordable, Context, Data, Error,
THEME_COLOR,
};
/// Run a recorded macro /// Run a recorded macro
#[poise::command( #[poise::command(
@ -32,7 +35,9 @@ pub async fn run_macro(
.await?; .await?;
for command in command_macro.commands { for command in command_macro.commands {
command.execute(poise::ApplicationContext { ..ctx }).await?; command
.run(poise::Context::Application(poise::ApplicationContext { ..ctx }))
.await?;
} }
} }

View File

@ -3,14 +3,15 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
consts::THEME_COLOR, consts::THEME_COLOR,
utils::{footer, Extract}, utils::{footer, Extract, Recordable},
Context, Error, Context, Error,
}; };
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options; pub struct Options;
pub async fn dashboard(ctx: Context<'_>, _options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx); let footer = footer(ctx);
ctx.send( ctx.send(
@ -25,10 +26,11 @@ pub async fn dashboard(ctx: Context<'_>, _options: Options) -> Result<(), Error>
.await?; .await?;
Ok(()) Ok(())
}
} }
/// Get the link to the web dashboard /// Get the link to the web dashboard
#[poise::command(slash_command, rename = "dashboard")] #[poise::command(slash_command, rename = "dashboard", identifying_name = "dashboard")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> { pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
dashboard(ctx, Options {}).await (Options {}).run(ctx).await
} }

View File

@ -15,7 +15,7 @@ use crate::{
}, },
consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR}, consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
models::{reminder::Reminder, CtxData}, models::{reminder::Reminder, CtxData},
utils::Extract, utils::{Extract, Recordable},
Context, Error, Context, Error,
}; };
@ -140,7 +140,8 @@ pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> Cr
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options; pub struct Options;
pub async fn delete(ctx: Context<'_>, _options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let timezone = ctx.timezone().await; let timezone = ctx.timezone().await;
let reminders = let reminders =
@ -151,10 +152,16 @@ pub async fn delete(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
ctx.send(resp).await?; ctx.send(resp).await?;
Ok(()) Ok(())
}
} }
/// Delete reminders /// Delete reminders
#[poise::command(slash_command, rename = "del", default_member_permissions = "MANAGE_GUILD")] #[poise::command(
slash_command,
rename = "delete",
identifying_name = "delete",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> { pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
delete(ctx, Options {}).await (Options {}).run(ctx).await
} }

View File

@ -3,14 +3,15 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
consts::THEME_COLOR, consts::THEME_COLOR,
utils::{footer, Extract}, utils::{footer, Extract, Recordable},
Context, Error, Context, Error,
}; };
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options; pub struct Options;
pub async fn donate(ctx: Context<'_>, _options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx); let footer = footer(ctx);
ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate") ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
@ -37,10 +38,11 @@ Just $2 USD/month!
.await?; .await?;
Ok(()) Ok(())
}
} }
/// Details on supporting the bot and Patreon benefits /// Details on supporting the bot and Patreon benefits
#[poise::command(slash_command, rename = "patreon")] #[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> { pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
donate(ctx, Options {}).await (Options {}).run(ctx).await
} }

View File

@ -3,14 +3,15 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
consts::THEME_COLOR, consts::THEME_COLOR,
utils::{footer, Extract}, utils::{footer, Extract, Recordable},
Context, Error, Context, Error,
}; };
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options; pub struct Options;
pub async fn help(ctx: Context<'_>, _options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx); let footer = footer(ctx);
ctx.send( ctx.send(
@ -51,10 +52,11 @@ __Advanced Commands__
.await?; .await?;
Ok(()) Ok(())
}
} }
/// Get an overview of bot commands /// Get an overview of bot commands
#[poise::command(slash_command, rename = "help")] #[poise::command(slash_command, rename = "help", identifying_name = "help")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> { pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
help(ctx, Options {}).await (Options {}).run(ctx).await
} }

View File

@ -3,14 +3,15 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
consts::THEME_COLOR, consts::THEME_COLOR,
utils::{footer, Extract}, utils::{footer, Extract, Recordable},
Context, Error, Context, Error,
}; };
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options; pub struct Options;
pub async fn info(ctx: Context<'_>, _options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx); let footer = footer(ctx);
ctx.send( ctx.send(
@ -35,10 +36,11 @@ Use our dashboard: https://reminder-bot.com/",
.await?; .await?;
Ok(()) Ok(())
}
} }
/// Get information about the bot /// Get information about the bot
#[poise::command(slash_command, rename = "info")] #[poise::command(slash_command, rename = "info", identifying_name = "info")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> { pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
info(ctx, Options {}).await (Options {}).run(ctx).await
} }

View File

@ -9,7 +9,7 @@ use crate::{
component_models::pager::{LookPager, Pager}, component_models::pager::{LookPager, Pager},
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
models::{reminder::Reminder, CtxData}, models::{reminder::Reminder, CtxData},
utils::Extract, utils::{Extract, Recordable},
Context, Error, Context, Error,
}; };
@ -40,13 +40,14 @@ pub struct Options {
relative: Option<bool>, relative: Option<bool>,
} }
pub async fn look(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let timezone = ctx.timezone().await; let timezone = ctx.timezone().await;
let flags = LookFlags { let flags = LookFlags {
show_disabled: options.disabled.unwrap_or(true), show_disabled: self.disabled.unwrap_or(true),
channel_id: options.channel.map(|c| c.id), channel_id: self.channel.map(|c| c.id),
time_display: options.relative.map_or(TimeDisplayType::Relative, |b| { time_display: self.relative.map_or(TimeDisplayType::Relative, |b| {
if b { if b {
TimeDisplayType::Relative TimeDisplayType::Relative
} else { } else {
@ -113,15 +114,21 @@ pub async fn look(ctx: Context<'_>, options: Options) -> Result<(), Error> {
} }
Ok(()) Ok(())
}
} }
/// View reminders on a specific channel /// View reminders on a specific channel
#[poise::command(slash_command, rename = "look", default_member_permissions = "MANAGE_GUILD")] #[poise::command(
slash_command,
rename = "look",
identifying_name = "look",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn command( pub async fn command(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Channel to view reminders on"] channel: Option<PartialChannel>, #[description = "Channel to view reminders on"] channel: Option<PartialChannel>,
#[description = "Whether to show disabled reminders or not"] disabled: Option<bool>, #[description = "Whether to show disabled reminders or not"] disabled: Option<bool>,
#[description = "Whether to display times as relative or exact times"] relative: Option<bool>, #[description = "Whether to display times as relative or exact times"] relative: Option<bool>,
) -> Result<(), Error> { ) -> Result<(), Error> {
look(ctx, Options { channel, disabled, relative }).await (Options { channel, disabled, relative }).run(ctx).await
} }

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete}, commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
models::reminder::create_reminder, models::reminder::create_reminder,
utils::Extract, utils::{Extract, Recordable},
Context, Error, Context, Error,
}; };
@ -30,22 +30,23 @@ pub struct Options {
timezone: Option<String>, timezone: Option<String>,
} }
pub async fn multiline(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
match ctx { match ctx {
Context::Application(app_ctx) => { Context::Application(app_ctx) => {
let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten(); let tz = self.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
let data_opt = ContentModal::execute(app_ctx).await?; let data_opt = ContentModal::execute(app_ctx).await?;
match data_opt { match data_opt {
Some(data) => { Some(data) => {
create_reminder( create_reminder(
ctx, ctx,
options.time, self.time,
data.content, data.content,
options.channels, self.channels,
options.interval, self.interval,
options.expires, self.expires,
options.tts, self.tts,
tz, tz,
) )
.await .await
@ -54,7 +55,9 @@ pub async fn multiline(ctx: Context<'_>, options: Options) -> Result<(), Error>
None => { None => {
warn!("Unexpected None encountered in /multiline"); warn!("Unexpected None encountered in /multiline");
Ok(ctx Ok(ctx
.send(CreateReply::default().content("Unexpected error.").ephemeral(true)) .send(
CreateReply::default().content("Unexpected error.").ephemeral(true),
)
.await .await
.map(|_| ())?) .map(|_| ())?)
} }
@ -69,10 +72,16 @@ pub async fn multiline(ctx: Context<'_>, options: Options) -> Result<(), Error>
.map(|_| ())?) .map(|_| ())?)
} }
} }
}
} }
/// Create a reminder with multi-line content. Press "+4 more" for other options. /// Create a reminder with multi-line content. Press "+4 more" for other options.
#[poise::command(slash_command, rename = "multiline", default_member_permissions = "MANAGE_GUILD")] #[poise::command(
slash_command,
rename = "multiline",
identifying_name = "multiline",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn command( pub async fn command(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "A description of the time to set the reminder for"] #[description = "A description of the time to set the reminder for"]
@ -89,5 +98,5 @@ pub async fn command(
#[autocomplete = "timezone_autocomplete"] #[autocomplete = "timezone_autocomplete"]
timezone: Option<String>, timezone: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
multiline(ctx, Options { time, channels, interval, expires, tts, timezone }).await (Options { time, channels, interval, expires, tts, timezone }).run(ctx).await
} }

View File

@ -1,6 +1,11 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{consts::MINUTE, models::CtxData, utils::Extract, Context, Error}; use crate::{
consts::MINUTE,
models::CtxData,
utils::{Extract, Recordable},
Context, Error,
};
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options { pub struct Options {
@ -8,9 +13,10 @@ pub struct Options {
seconds: Option<i64>, seconds: Option<i64>,
} }
pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let combined_time = let combined_time =
options.minutes.map_or(0, |m| m * MINUTE as i64) + options.seconds.map_or(0, |s| s); self.minutes.map_or(0, |m| m * MINUTE as i64) + self.seconds.map_or(0, |s| s);
if combined_time < i16::MIN as i64 || combined_time > i16::MAX as i64 { if combined_time < i16::MIN as i64 || combined_time > i16::MAX as i64 {
ctx.say("Nudge times must be less than 500 minutes").await?; ctx.say("Nudge times must be less than 500 minutes").await?;
@ -20,18 +26,25 @@ pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> {
channel_data.nudge = combined_time as i16; channel_data.nudge = combined_time as i16;
channel_data.commit_changes(&ctx.data().database).await; channel_data.commit_changes(&ctx.data().database).await;
ctx.say(format!("Future reminders will be nudged by {} seconds", combined_time)).await?; ctx.say(format!("Future reminders will be nudged by {} seconds", combined_time))
.await?;
} }
Ok(()) Ok(())
}
} }
/// Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`) /// Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`)
#[poise::command(slash_command, rename = "nudge", default_member_permissions = "MANAGE_GUILD")] #[poise::command(
slash_command,
rename = "nudge",
identifying_name = "nudge",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn command( pub async fn command(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Number of minutes to nudge new reminders by"] minutes: Option<i64>, #[description = "Number of minutes to nudge new reminders by"] minutes: Option<i64>,
#[description = "Number of seconds to nudge new reminders by"] seconds: Option<i64>, #[description = "Number of seconds to nudge new reminders by"] seconds: Option<i64>,
) -> Result<(), Error> { ) -> Result<(), Error> {
nudge(ctx, Options { minutes, seconds }).await (Options { minutes, seconds }).run(ctx).await
} }

View File

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
consts::{HOUR, MINUTE}, consts::{HOUR, MINUTE},
utils::Extract, utils::{Extract, Recordable},
Context, Error, Context, Error,
}; };
@ -13,12 +13,13 @@ pub struct Options {
seconds: Option<i64>, seconds: Option<i64>,
} }
async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
ctx.defer().await?; ctx.defer().await?;
let combined_time = options.hours.map_or(0, |h| h * HOUR as i64) let combined_time = self.hours.map_or(0, |h| h * HOUR as i64)
+ options.minutes.map_or(0, |m| m * MINUTE as i64) + self.minutes.map_or(0, |m| m * MINUTE as i64)
+ options.seconds.map_or(0, |s| s); + self.seconds.map_or(0, |s| s);
if combined_time == 0 { if combined_time == 0 {
ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?; ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?;
@ -67,15 +68,21 @@ async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> {
} }
Ok(()) Ok(())
}
} }
/// Move all reminders in the current server by a certain amount of time. Times get added together /// Move all reminders in the current server by a certain amount of time. Times get added together
#[poise::command(slash_command, rename = "offset", default_member_permissions = "MANAGE_GUILD")] #[poise::command(
slash_command,
rename = "offset",
identifying_name = "offset",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn command( pub async fn command(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Number of hours to offset by"] hours: Option<i64>, #[description = "Number of hours to offset by"] hours: Option<i64>,
#[description = "Number of minutes to offset by"] minutes: Option<i64>, #[description = "Number of minutes to offset by"] minutes: Option<i64>,
#[description = "Number of seconds to offset by"] seconds: Option<i64>, #[description = "Number of seconds to offset by"] seconds: Option<i64>,
) -> Result<(), Error> { ) -> Result<(), Error> {
offset(ctx, Options { hours, minutes, seconds }).await (Options { hours, minutes, seconds }).run(ctx).await
} }

View File

@ -1,19 +1,25 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{models::CtxData, time_parser::natural_parser, utils::Extract, Context, Error}; use crate::{
models::CtxData,
time_parser::natural_parser,
utils::{Extract, Recordable},
Context, Error,
};
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options { pub struct Options {
until: Option<String>, until: Option<String>,
} }
pub async fn pause(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let timezone = ctx.timezone().await; let timezone = ctx.timezone().await;
let mut channel = ctx.channel_data().await.unwrap(); let mut channel = ctx.channel_data().await.unwrap();
match options.until { match self.until {
Some(until) => { Some(until) => {
let parsed = natural_parser(&until, &timezone.to_string()).await; let parsed = natural_parser(&until, &timezone.to_string()).await;
@ -61,13 +67,19 @@ pub async fn pause(ctx: Context<'_>, options: Options) -> Result<(), Error> {
} }
Ok(()) Ok(())
}
} }
/// Pause all reminders on the current channel until a certain time or indefinitely /// Pause all reminders on the current channel until a certain time or indefinitely
#[poise::command(slash_command, rename = "pause", default_member_permissions = "MANAGE_GUILD")] #[poise::command(
slash_command,
rename = "pause",
identifying_name = "pause",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn command( pub async fn command(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "When to pause until"] until: Option<String>, #[description = "When to pause until"] until: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
pause(ctx, Options { until }).await (Options { until }).run(ctx).await
} }

View File

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete}, commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete},
models::reminder::create_reminder, models::reminder::create_reminder,
utils::Extract, utils::{Extract, Recordable},
Context, Error, Context, Error,
}; };
@ -19,24 +19,31 @@ pub struct Options {
timezone: Option<String>, timezone: Option<String>,
} }
pub async fn remind(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten(); async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let tz = self.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
create_reminder( create_reminder(
ctx, ctx,
options.time, self.time,
options.content, self.content,
options.channels, self.channels,
options.interval, self.interval,
options.expires, self.expires,
options.tts, self.tts,
tz, tz,
) )
.await .await
}
} }
/// Create a reminder. Press "+4 more" for other options. Use "/multiline" for multiline content. /// Create a reminder. Press "+4 more" for other options. Use "/multiline" for multiline content.
#[poise::command(slash_command, rename = "remind", default_member_permissions = "MANAGE_GUILD")] #[poise::command(
slash_command,
rename = "remind",
default_member_permissions = "MANAGE_GUILD",
identifying_name = "remind"
)]
pub async fn command( pub async fn command(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "The time (and optionally date) to set the reminder for"] #[description = "The time (and optionally date) to set the reminder for"]
@ -54,5 +61,5 @@ pub async fn command(
#[autocomplete = "timezone_autocomplete"] #[autocomplete = "timezone_autocomplete"]
timezone: Option<String>, timezone: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
remind(ctx, Options { time, content, channels, interval, expires, tts, timezone }).await (Options { time, content, channels, interval, expires, tts, timezone }).run(ctx).await
} }

View File

@ -8,8 +8,11 @@ use poise::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
commands::autocomplete::timezone_autocomplete, consts::THEME_COLOR, models::CtxData, commands::autocomplete::timezone_autocomplete,
utils::Extract, Context, Error, consts::THEME_COLOR,
models::CtxData,
utils::{Extract, Recordable},
Context, Error,
}; };
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
@ -17,12 +20,13 @@ pub struct Options {
pub timezone: Option<String>, pub timezone: Option<String>,
} }
pub async fn timezone_fn(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let mut user_data = ctx.author_data().await.unwrap(); let mut user_data = ctx.author_data().await.unwrap();
let footer_text = format!("Current timezone: {}", user_data.timezone); let footer_text = format!("Current timezone: {}", user_data.timezone);
if let Some(timezone) = options.timezone { if let Some(timezone) = self.timezone {
match timezone.parse::<Tz>() { match timezone.parse::<Tz>() {
Ok(tz) => { Ok(tz) => {
user_data.timezone = timezone.clone(); user_data.timezone = timezone.clone();
@ -87,7 +91,11 @@ pub async fn timezone_fn(ctx: Context<'_>, options: Options) -> Result<(), Error
} }
} else { } else {
let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| { let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
(t.to_string(), format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")), true) (
t.to_string(),
format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")),
true,
)
}); });
ctx.send( ctx.send(
@ -113,15 +121,16 @@ You may want to use one of the popular timezones below, otherwise click [here](h
} }
Ok(()) Ok(())
}
} }
/// Select your timezone /// Select your timezone
#[poise::command(slash_command, rename = "timezone")] #[poise::command(slash_command, rename = "timezone", identifying_name = "timezone")]
pub async fn command( pub async fn command(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"] #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
#[autocomplete = "timezone_autocomplete"] #[autocomplete = "timezone_autocomplete"]
timezone: Option<String>, timezone: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
timezone_fn(ctx, Options { timezone }).await (Options { timezone }).run(ctx).await
} }

View File

@ -2,12 +2,17 @@ use log::warn;
use poise::CreateReply; use poise::CreateReply;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{models::CtxData, utils::Extract, Context, Error}; use crate::{
models::CtxData,
utils::{Extract, Recordable},
Context, Error,
};
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options; pub struct Options;
pub async fn webhook(ctx: Context<'_>, _options: Options) -> Result<(), Error> { impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
match ctx.channel_data().await { match ctx.channel_data().await {
Ok(data) => { Ok(data) => {
if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) { if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
@ -31,10 +36,16 @@ Do not share it!
} }
Ok(()) Ok(())
}
} }
/// View the webhook being used to send reminders to this channel /// View the webhook being used to send reminders to this channel
#[poise::command(slash_command, rename = "webhook", required_permissions = "ADMINISTRATOR")] #[poise::command(
slash_command,
rename = "webhook",
identifying_name = "webhook",
required_permissions = "ADMINISTRATOR"
)]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> { pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
webhook(ctx, Options {}).await (Options {}).run(ctx).await
} }

View File

@ -13,18 +13,6 @@ async fn macro_check(ctx: Context<'_>) -> bool {
let mut lock = ctx.data().recording_macros.write().await; let mut lock = ctx.data().recording_macros.write().await;
if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) { if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
if ctx.command().identifying_name != "remind" {
let _ = ctx
.send(
CreateReply::default()
.ephemeral(true)
.content("Macro recording only supports `/remind`. Please stop recording with `/macro finish` before using other commands.")
)
.await;
return false;
}
if command_macro.commands.len() >= MACRO_MAX_COMMANDS { if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
let _ = ctx let _ = ctx
.send( .send(
@ -34,7 +22,8 @@ async fn macro_check(ctx: Context<'_>) -> bool {
) )
.await; .await;
} else { } else {
let recorded = RecordedCommand::from_context(app_ctx).unwrap(); match RecordedCommand::from_context(app_ctx) {
Some(recorded) => {
command_macro.commands.push(recorded); command_macro.commands.push(recorded);
let _ = ctx let _ = ctx
@ -46,6 +35,18 @@ async fn macro_check(ctx: Context<'_>) -> bool {
.await; .await;
} }
None => {
let _ = ctx
.send(
CreateReply::default().ephemeral(true).content(
"This command is not supported in macros yet.",
),
)
.await;
}
}
}
return false; return false;
} }
} }

View File

@ -2,29 +2,64 @@ use poise::serenity_prelude::model::id::GuildId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use crate::{commands::remind, utils::Extract, ApplicationContext, Context, Error}; use crate::{
utils::{Extract, Recordable},
ApplicationContext, Context,
};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Recordable)]
#[serde(tag = "command_name")] #[serde(tag = "command_name")]
pub enum RecordedCommand { pub enum RecordedCommand {
Remind(remind::Options), #[serde(rename = "clock")]
Clock(crate::commands::clock::Options),
#[serde(rename = "dashboard")]
Dashboard(crate::commands::dashboard::Options),
#[serde(rename = "delete")]
Delete(crate::commands::delete::Options),
#[serde(rename = "donate")]
Donate(crate::commands::donate::Options),
#[serde(rename = "help")]
Help(crate::commands::help::Options),
#[serde(rename = "info")]
Info(crate::commands::info::Options),
#[serde(rename = "look")]
Look(crate::commands::look::Options),
#[serde(rename = "multiline")]
Multiline(crate::commands::multiline::Options),
#[serde(rename = "nudge")]
Nudge(crate::commands::nudge::Options),
#[serde(rename = "offset")]
Offset(crate::commands::offset::Options),
#[serde(rename = "pause")]
Pause(crate::commands::pause::Options),
#[serde(rename = "remind")]
Remind(crate::commands::remind::Options),
#[serde(rename = "timezone")]
Timezone(crate::commands::timezone::Options),
#[serde(rename = "webhook")]
Webhook(crate::commands::webhook::Options),
} }
impl RecordedCommand { impl RecordedCommand {
pub fn from_context(ctx: ApplicationContext) -> Option<Self> { pub fn from_context(ctx: ApplicationContext) -> Option<Self> {
match ctx.command().identifying_name.as_str() { match ctx.command().identifying_name.as_str() {
"remind" => Some(Self::Remind(remind::Options::extract(ctx))), "clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
"dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
"delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
"donate" => Some(Self::Donate(crate::commands::donate::Options::extract(ctx))),
"help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
"info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
"look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
"multiline" => Some(Self::Multiline(crate::commands::multiline::Options::extract(ctx))),
"nudge" => Some(Self::Nudge(crate::commands::nudge::Options::extract(ctx))),
"offset" => Some(Self::Offset(crate::commands::offset::Options::extract(ctx))),
"pause" => Some(Self::Pause(crate::commands::pause::Options::extract(ctx))),
"remind" => Some(Self::Remind(crate::commands::remind::Options::extract(ctx))),
"timezone" => Some(Self::Timezone(crate::commands::timezone::Options::extract(ctx))),
"webhook" => Some(Self::Webhook(crate::commands::webhook::Options::extract(ctx))),
_ => None, _ => None,
} }
} }
pub async fn execute(self, ctx: ApplicationContext<'_>) -> Result<(), Error> {
match self {
RecordedCommand::Remind(options) => {
remind::remind(Context::Application(ctx), options).await
}
}
}
} }
pub struct CommandMacro { pub struct CommandMacro {

View File

@ -9,7 +9,7 @@ use poise::{
use crate::{ use crate::{
consts::{CNC_GUILD, SUBSCRIPTION_ROLES}, consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
ApplicationContext, Context, ApplicationContext, Context, Error,
}; };
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool { pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
@ -65,11 +65,17 @@ pub fn footer(ctx: Context<'_>) -> CreateEmbedFooter {
)) ))
} }
pub trait Recordable {
async fn run(self, ctx: Context<'_>) -> Result<(), Error>;
}
pub use recordable_derive::Recordable;
pub trait Extract { pub trait Extract {
fn extract(ctx: ApplicationContext) -> Self; fn extract(ctx: ApplicationContext) -> Self;
} }
pub use extract_macro::Extract; pub use extract_derive::Extract;
macro_rules! extract_arg { macro_rules! extract_arg {
($ctx:ident, $name:ident, String) => { ($ctx:ident, $name:ident, String) => {