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,29 +2,35 @@ 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 {
ctx.defer_ephemeral().await?; async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
ctx.defer_ephemeral().await?;
let tz = ctx.timezone().await; let tz = ctx.timezone().await;
let now = Utc::now().with_timezone(&tz); let now = Utc::now().with_timezone(&tz);
ctx.send(CreateReply::default().ephemeral(true).content(format!( ctx.send(CreateReply::default().ephemeral(true).content(format!(
"Time in **{}**: `{}`", "Time in **{}**: `{}`",
tz, tz,
now.format("%H:%M") now.format("%H:%M")
))) )))
.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,32 +3,34 @@ 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 {
let footer = footer(ctx); async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx);
ctx.send( ctx.send(
CreateReply::default().ephemeral(true).embed( CreateReply::default().ephemeral(true).embed(
CreateEmbed::new() CreateEmbed::new()
.title("Dashboard") .title("Dashboard")
.description("**https://beta.reminder-bot.com/dashboard**") .description("**https://beta.reminder-bot.com/dashboard**")
.footer(footer) .footer(footer)
.color(*THEME_COLOR), .color(*THEME_COLOR),
), ),
) )
.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,21 +140,28 @@ 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 {
let timezone = ctx.timezone().await; async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let timezone = ctx.timezone().await;
let reminders = let reminders =
Reminder::from_guild(&ctx, &ctx.data().database, ctx.guild_id(), ctx.author().id).await; Reminder::from_guild(&ctx, &ctx.data().database, ctx.guild_id(), ctx.author().id).await;
let resp = show_delete_page(&reminders, 0, timezone); let resp = show_delete_page(&reminders, 0, timezone);
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,17 +3,18 @@ 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 {
let footer = footer(ctx); async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx);
ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate") ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
.description("Thinking of adding a monthly contribution? .description("Thinking of adding a monthly contribution?
Click below for my Patreon and official bot server :) Click below for my Patreon and official bot server :)
@ -36,11 +37,12 @@ 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,23 +3,24 @@ 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 {
let footer = footer(ctx); async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx);
ctx.send( ctx.send(
CreateReply::default().ephemeral(true).embed( CreateReply::default().ephemeral(true).embed(
CreateEmbed::new() CreateEmbed::new()
.title("Help") .title("Help")
.color(*THEME_COLOR) .color(*THEME_COLOR)
.description( .description(
"__Info Commands__ "__Info Commands__
`/help` `/info` `/donate` `/dashboard` `/clock` `/help` `/info` `/donate` `/dashboard` `/clock`
*run these commands with no options* *run these commands with no options*
@ -44,17 +45,18 @@ __Setup Commands__
__Advanced Commands__ __Advanced Commands__
`/macro` - Record and replay command sequences `/macro` - Record and replay command sequences
", ",
) )
.footer(footer), .footer(footer),
), ),
) )
.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,22 +3,23 @@ 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 {
let footer = footer(ctx); async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx);
ctx.send( ctx.send(
CreateReply::default().ephemeral(true).embed( CreateReply::default().ephemeral(true).embed(
CreateEmbed::new() CreateEmbed::new()
.title("Info") .title("Info")
.description( .description(
"Help: `/help` "Help: `/help`
**Welcome to Reminder Bot!** **Welcome to Reminder Bot!**
Developer: <@203532103185465344> Developer: <@203532103185465344>
@ -27,18 +28,19 @@ Find me on https://discord.jellywx.com and on https://github.com/JellyWX :)
Invite the bot: https://invite.reminder-bot.com/ Invite the bot: https://invite.reminder-bot.com/
Use our dashboard: https://reminder-bot.com/", Use our dashboard: https://reminder-bot.com/",
) )
.footer(footer) .footer(footer)
.color(*THEME_COLOR), .color(*THEME_COLOR),
), ),
) )
.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,88 +40,95 @@ pub struct Options {
relative: Option<bool>, relative: Option<bool>,
} }
pub async fn look(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
let timezone = ctx.timezone().await; async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
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 {
TimeDisplayType::Absolute
}
}),
};
let channel_id = if let Some(channel) = ctx.channel_id().to_channel_cached(&ctx.cache()) {
if Some(channel.guild_id) == ctx.guild_id() {
flags.channel_id.unwrap_or_else(|| ctx.channel_id())
} else { } else {
TimeDisplayType::Absolute ctx.channel_id()
} }
}),
};
let channel_id = if let Some(channel) = ctx.channel_id().to_channel_cached(&ctx.cache()) {
if Some(channel.guild_id) == ctx.guild_id() {
flags.channel_id.unwrap_or_else(|| ctx.channel_id())
} else { } else {
ctx.channel_id() ctx.channel_id()
};
let channel_name =
channel_id.to_channel_cached(&ctx.cache()).map(|channel| channel.name.clone());
let reminders = Reminder::from_channel(&ctx.data().database, channel_id, &flags).await;
if reminders.is_empty() {
let _ = ctx.say("No reminders on specified channel").await;
} else {
let mut char_count = 0;
let display = reminders
.iter()
.map(|reminder| reminder.display(&flags, &timezone))
.take_while(|p| {
char_count += p.len();
char_count < EMBED_DESCRIPTION_MAX_LENGTH
})
.collect::<Vec<String>>()
.join("");
let pages = reminders
.iter()
.map(|reminder| reminder.display(&flags, &timezone))
.fold(0, |t, r| t + r.len())
.div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
let pager = LookPager::new(flags, timezone);
ctx.send(
CreateReply::default()
.ephemeral(true)
.embed(
CreateEmbed::new()
.title(format!(
"Reminders{}",
channel_name.map_or(String::new(), |n| format!(" on #{}", n))
))
.description(display)
.footer(CreateEmbedFooter::new(format!("Page {} of {}", 1, pages)))
.color(*THEME_COLOR),
)
.components(vec![pager.create_button_row(pages)]),
)
.await?;
} }
} else {
ctx.channel_id()
};
let channel_name = Ok(())
channel_id.to_channel_cached(&ctx.cache()).map(|channel| channel.name.clone());
let reminders = Reminder::from_channel(&ctx.data().database, channel_id, &flags).await;
if reminders.is_empty() {
let _ = ctx.say("No reminders on specified channel").await;
} else {
let mut char_count = 0;
let display = reminders
.iter()
.map(|reminder| reminder.display(&flags, &timezone))
.take_while(|p| {
char_count += p.len();
char_count < EMBED_DESCRIPTION_MAX_LENGTH
})
.collect::<Vec<String>>()
.join("");
let pages = reminders
.iter()
.map(|reminder| reminder.display(&flags, &timezone))
.fold(0, |t, r| t + r.len())
.div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
let pager = LookPager::new(flags, timezone);
ctx.send(
CreateReply::default()
.ephemeral(true)
.embed(
CreateEmbed::new()
.title(format!(
"Reminders{}",
channel_name.map_or(String::new(), |n| format!(" on #{}", n))
))
.description(display)
.footer(CreateEmbedFooter::new(format!("Page {} of {}", 1, pages)))
.color(*THEME_COLOR),
)
.components(vec![pager.create_button_row(pages)]),
)
.await?;
} }
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,49 +30,58 @@ pub struct Options {
timezone: Option<String>, timezone: Option<String>,
} }
pub async fn multiline(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
match ctx { async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
Context::Application(app_ctx) => { match ctx {
let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten(); Context::Application(app_ctx) => {
let data_opt = ContentModal::execute(app_ctx).await?; let tz = self.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
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
}
None => {
warn!("Unexpected None encountered in /multiline");
Ok(ctx
.send(CreateReply::default().content("Unexpected error.").ephemeral(true))
.await .await
.map(|_| ())?) }
None => {
warn!("Unexpected None encountered in /multiline");
Ok(ctx
.send(
CreateReply::default().content("Unexpected error.").ephemeral(true),
)
.await
.map(|_| ())?)
}
} }
} }
}
_ => { _ => {
warn!("Shouldn't be here"); warn!("Shouldn't be here");
Ok(ctx Ok(ctx
.send(CreateReply::default().content("Unexpected error.").ephemeral(true)) .send(CreateReply::default().content("Unexpected error.").ephemeral(true))
.await .await
.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,30 +13,38 @@ pub struct Options {
seconds: Option<i64>, seconds: Option<i64>,
} }
pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
let combined_time = async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
options.minutes.map_or(0, |m| m * MINUTE as i64) + options.seconds.map_or(0, |s| s); let combined_time =
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?;
} else { } else {
let mut channel_data = ctx.channel_data().await.unwrap(); let mut channel_data = ctx.channel_data().await.unwrap();
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,69 +13,76 @@ pub struct Options {
seconds: Option<i64>, seconds: Option<i64>,
} }
async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> { impl Recordable for Options {
ctx.defer().await?; async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
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?;
} else { } else {
if let Some(channels) = ctx.guild().map(|guild| { if let Some(channels) = ctx.guild().map(|guild| {
guild guild
.channels .channels
.iter() .iter()
.filter(|(_, channel)| channel.is_text_based()) .filter(|(_, channel)| channel.is_text_based())
.map(|(id, _)| id.get().to_string()) .map(|(id, _)| id.get().to_string())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(",") .join(",")
}) { }) {
sqlx::query!( sqlx::query!(
" "
UPDATE reminders UPDATE reminders
INNER JOIN `channels` INNER JOIN `channels`
ON `channels`.id = reminders.channel_id ON `channels`.id = reminders.channel_id
SET reminders.`utc_time` = DATE_ADD(reminders.`utc_time`, INTERVAL ? SECOND) SET reminders.`utc_time` = DATE_ADD(reminders.`utc_time`, INTERVAL ? SECOND)
WHERE FIND_IN_SET(channels.`channel`, ?) WHERE FIND_IN_SET(channels.`channel`, ?)
", ",
combined_time as i64, combined_time as i64,
channels channels
) )
.execute(&ctx.data().database) .execute(&ctx.data().database)
.await .await
.unwrap(); .unwrap();
} else { } else {
sqlx::query!( sqlx::query!(
" "
UPDATE reminders UPDATE reminders
INNER JOIN `channels` INNER JOIN `channels`
ON `channels`.id = reminders.channel_id ON `channels`.id = reminders.channel_id
SET reminders.`utc_time` = reminders.`utc_time` + ? SET reminders.`utc_time` = reminders.`utc_time` + ?
WHERE channels.`channel` = ? WHERE channels.`channel` = ?
", ",
combined_time as i64, combined_time as i64,
ctx.channel_id().get() ctx.channel_id().get()
) )
.execute(&ctx.data().database) .execute(&ctx.data().database)
.await .await
.unwrap(); .unwrap();
}
ctx.say(format!("All reminders offset by {} seconds", combined_time)).await?;
} }
ctx.say(format!("All reminders offset by {} seconds", combined_time)).await?; 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,73 +1,85 @@
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 {
let timezone = ctx.timezone().await; async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
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;
if let Some(timestamp) = parsed { if let Some(timestamp) = parsed {
match NaiveDateTime::from_timestamp_opt(timestamp, 0) { match NaiveDateTime::from_timestamp_opt(timestamp, 0) {
Some(dt) => { Some(dt) => {
channel.paused = true; channel.paused = true;
channel.paused_until = Some(dt); channel.paused_until = Some(dt);
channel.commit_changes(&ctx.data().database).await; channel.commit_changes(&ctx.data().database).await;
ctx.say(format!( ctx.say(format!(
"Reminders in this channel have been silenced until **<t:{}:D>**", "Reminders in this channel have been silenced until **<t:{}:D>**",
timestamp timestamp
)) ))
.await?; .await?;
} }
None => { None => {
ctx.say( ctx.say(
"Time processed could not be interpreted as `DateTime`. Please write the time as clearly as possible", "Time processed could not be interpreted as `DateTime`. Please write the time as clearly as possible",
) )
.await?; .await?;
}
} }
} else {
ctx.say(
"Time could not be processed. Please write the time as clearly as possible",
)
.await?;
}
}
_ => {
channel.paused = !channel.paused;
channel.paused_until = None;
channel.commit_changes(&ctx.data().database).await;
if channel.paused {
ctx.say("Reminders in this channel have been silenced indefinitely").await?;
} else {
ctx.say("Reminders in this channel have been unsilenced").await?;
} }
} else {
ctx.say(
"Time could not be processed. Please write the time as clearly as possible",
)
.await?;
} }
} }
_ => {
channel.paused = !channel.paused;
channel.paused_until = None;
channel.commit_changes(&ctx.data().database).await; Ok(())
if channel.paused {
ctx.say("Reminders in this channel have been silenced indefinitely").await?;
} else {
ctx.say("Reminders in this channel have been unsilenced").await?;
}
}
} }
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,55 +20,56 @@ 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 {
let mut user_data = ctx.author_data().await.unwrap(); async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
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();
user_data.commit_changes(&ctx.data().database).await; user_data.commit_changes(&ctx.data().database).await;
let now = Utc::now().with_timezone(&tz); let now = Utc::now().with_timezone(&tz);
ctx.send( ctx.send(
CreateReply::default().embed( CreateReply::default().embed(
CreateEmbed::new() CreateEmbed::new()
.title("Timezone Set") .title("Timezone Set")
.description(format!( .description(format!(
"Timezone has been set to **{}**. Your current time should be `{}`", "Timezone has been set to **{}**. Your current time should be `{}`",
timezone, timezone,
now.format("%H:%M") now.format("%H:%M")
)) ))
.color(*THEME_COLOR), .color(*THEME_COLOR),
), ),
)
.await?;
}
Err(_) => {
let filtered_tz = TZ_VARIANTS
.iter()
.filter(|tz| {
timezone.contains(&tz.to_string())
|| tz.to_string().contains(&timezone)
|| levenshtein(&tz.to_string(), &timezone) < 4
})
.take(25)
.map(|t| t.to_owned())
.collect::<Vec<Tz>>();
let fields = filtered_tz.iter().map(|tz| {
(
tz.to_string(),
format!("🕗 `{}`", Utc::now().with_timezone(tz).format("%H:%M")),
true,
) )
}); .await?;
}
ctx.send( Err(_) => {
let filtered_tz = TZ_VARIANTS
.iter()
.filter(|tz| {
timezone.contains(&tz.to_string())
|| tz.to_string().contains(&timezone)
|| levenshtein(&tz.to_string(), &timezone) < 4
})
.take(25)
.map(|t| t.to_owned())
.collect::<Vec<Tz>>();
let fields = filtered_tz.iter().map(|tz| {
(
tz.to_string(),
format!("🕗 `{}`", Utc::now().with_timezone(tz).format("%H:%M")),
true,
)
});
ctx.send(
CreateReply::default().embed( CreateReply::default().embed(
CreateEmbed::new() CreateEmbed::new()
.title("Timezone Not Recognized") .title("Timezone Not Recognized")
@ -83,14 +87,18 @@ pub async fn timezone_fn(ctx: Context<'_>, options: Options) -> Result<(), Error
), ),
) )
.await?; .await?;
}
} }
} } 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(
CreateReply::default().embed( CreateReply::default().embed(
CreateEmbed::new() CreateEmbed::new()
.title("Timezone Usage") .title("Timezone Usage")
@ -110,18 +118,19 @@ You may want to use one of the popular timezones below, otherwise click [here](h
), ),
) )
.await?; .await?;
} }
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,39 +2,50 @@ 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 {
match ctx.channel_data().await { async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
Ok(data) => { match ctx.channel_data().await {
if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) { Ok(data) => {
ctx.send(CreateReply::default().ephemeral(true).content(format!( if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
"**Warning!** ctx.send(CreateReply::default().ephemeral(true).content(format!(
"**Warning!**
This link can be used by users to anonymously send messages, with or without permissions. This link can be used by users to anonymously send messages, with or without permissions.
Do not share it! Do not share it!
|| https://discord.com/api/webhooks/{}/{} ||", || https://discord.com/api/webhooks/{}/{} ||",
id, token, id, token,
))) )))
.await?; .await?;
} else { } else {
ctx.say("No webhook configured on this channel.").await?;
}
}
Err(e) => {
warn!("Error fetching channel data: {:?}", e);
ctx.say("No webhook configured on this channel.").await?; ctx.say("No webhook configured on this channel.").await?;
} }
} }
Err(e) => {
warn!("Error fetching channel data: {:?}", e);
ctx.say("No webhook configured on this channel.").await?; 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,16 +22,29 @@ 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) {
command_macro.commands.push(recorded); Some(recorded) => {
command_macro.commands.push(recorded);
let _ = ctx let _ = ctx
.send( .send(
CreateReply::default() CreateReply::default()
.ephemeral(true) .ephemeral(true)
.content("Command recorded to macro"), .content("Command recorded to macro"),
) )
.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) => {