Add option types for top-level commands

This commit is contained in:
jude 2024-02-18 11:04:43 +00:00
parent c1305cfb36
commit 5e39e16060
22 changed files with 395 additions and 268 deletions

11
Cargo.lock generated
View File

@ -773,6 +773,14 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "extract_macro"
version = "0.1.0"
dependencies = [
"quote",
"syn 2.0.49",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.0.1" version = "2.0.1"
@ -2301,7 +2309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]] [[package]]
name = "reminder-rs" name = "reminder_rs"
version = "1.6.50" version = "1.6.50"
dependencies = [ dependencies = [
"base64 0.21.5", "base64 0.21.5",
@ -2309,6 +2317,7 @@ dependencies = [
"chrono-tz", "chrono-tz",
"dotenv", "dotenv",
"env_logger", "env_logger",
"extract_macro",
"lazy-regex", "lazy-regex",
"lazy_static", "lazy_static",
"levenshtein", "levenshtein",

View File

@ -1,5 +1,5 @@
[package] [package]
name = "reminder-rs" name = "reminder_rs"
version = "1.6.50" version = "1.6.50"
authors = ["Jude Southworth <judesouthworth@pm.me>"] authors = ["Jude Southworth <judesouthworth@pm.me>"]
edition = "2021" edition = "2021"
@ -35,6 +35,9 @@ path = "postman"
[dependencies.reminder_web] [dependencies.reminder_web]
path = "web" path = "web"
[dependencies.extract_macro]
path = "extract_macro"
[package.metadata.deb] [package.metadata.deb]
depends = "$auto, python3-dateparser (>= 1.0.0)" depends = "$auto, python3-dateparser (>= 1.0.0)"
suggests = "mysql-server-8.0, nginx" suggests = "mysql-server-8.0, nginx"

52
extract_macro/src/lib.rs Normal file
View File

@ -0,0 +1,52 @@
use proc_macro::TokenStream;
use syn::{spanned::Spanned, Data, Fields};
#[proc_macro_derive(Extract)]
pub fn extract(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input);
impl_extract(&ast)
}
fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
match &ast.data {
Data::Struct(st) => match &st.fields {
Fields::Named(fields) => {
let extracted = fields.named.iter().map(|field| {
let ident = &field.ident;
let ty = &field.ty;
quote::quote_spanned! {field.span()=>
#ident : crate::utils::extract_arg!(ctx, #ident, #ty)
}
});
TokenStream::from(quote::quote! {
impl Extract for #name {
fn extract(ctx: crate::ApplicationContext) -> Self {
Self {
#(#extracted,)*
}
}
}
})
}
Fields::Unit => TokenStream::from(quote::quote! {
impl Extract for #name {
fn extract(ctx: crate::ApplicationContext) -> Self {
Self {}
}
}
}),
_ => {
panic!("Only named/unit structs can derive Extract");
}
},
_ => {
panic!("Only structs can derive Extract");
}
}
}

View File

@ -1,11 +1,13 @@
use chrono::Utc; use chrono::Utc;
use poise::CreateReply; use poise::CreateReply;
use serde::{Deserialize, Serialize};
use crate::{models::CtxData, Context, Error}; use crate::{models::CtxData, utils::Extract, Context, Error};
/// View the current time in your selected timezone #[derive(Serialize, Deserialize, Extract)]
#[poise::command(slash_command)] pub struct Options;
pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
pub async fn clock(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
ctx.defer_ephemeral().await?; ctx.defer_ephemeral().await?;
let tz = ctx.timezone().await; let tz = ctx.timezone().await;
@ -20,3 +22,9 @@ pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
Ok(()) Ok(())
} }
/// View the current time in your selected timezone
#[poise::command(slash_command, rename = "clock")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
clock(ctx, Options {}).await
}

View File

@ -1,10 +1,16 @@
use poise::{serenity_prelude::CreateEmbed, CreateReply}; use poise::{serenity_prelude::CreateEmbed, CreateReply};
use serde::{Deserialize, Serialize};
use crate::{consts::THEME_COLOR, utils::footer, Context, Error}; use crate::{
consts::THEME_COLOR,
utils::{footer, Extract},
Context, Error,
};
/// Get the link to the online dashboard #[derive(Serialize, Deserialize, Extract)]
#[poise::command(slash_command)] pub struct Options;
pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> {
pub async fn dashboard(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
let footer = footer(ctx); let footer = footer(ctx);
ctx.send( ctx.send(
@ -20,3 +26,9 @@ pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> {
Ok(()) Ok(())
} }
/// Get the link to the web dashboard
#[poise::command(slash_command, rename = "dashboard")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
dashboard(ctx, Options {}).await
}

View File

@ -6,6 +6,7 @@ use poise::{
}, },
CreateReply, CreateReply,
}; };
use serde::{Deserialize, Serialize};
use crate::{ use crate::{
component_models::{ component_models::{
@ -14,29 +15,10 @@ 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,
Context, Error, Context, Error,
}; };
/// Delete reminders
#[poise::command(
slash_command,
rename = "del",
identifying_name = "delete",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn delete(ctx: Context<'_>) -> Result<(), Error> {
let timezone = ctx.timezone().await;
let reminders =
Reminder::from_guild(&ctx, &ctx.data().database, ctx.guild_id(), ctx.author().id).await;
let resp = show_delete_page(&reminders, 0, timezone);
ctx.send(resp).await?;
Ok(())
}
pub fn max_delete_page(reminders: &[Reminder], timezone: &Tz) -> usize { pub fn max_delete_page(reminders: &[Reminder], timezone: &Tz) -> usize {
let mut rows = 0; let mut rows = 0;
let mut char_count = 0; let mut char_count = 0;
@ -154,3 +136,25 @@ pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> Cr
.embed(embed) .embed(embed)
.components(vec![pager.create_button_row(pages), CreateActionRow::SelectMenu(select_menu)]) .components(vec![pager.create_button_row(pages), CreateActionRow::SelectMenu(select_menu)])
} }
#[derive(Serialize, Deserialize, Extract)]
pub struct Options;
pub async fn delete(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
let timezone = ctx.timezone().await;
let reminders =
Reminder::from_guild(&ctx, &ctx.data().database, ctx.guild_id(), ctx.author().id).await;
let resp = show_delete_page(&reminders, 0, timezone);
ctx.send(resp).await?;
Ok(())
}
/// Delete reminders
#[poise::command(slash_command, rename = "del", default_member_permissions = "MANAGE_GUILD")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
delete(ctx, Options {}).await
}

View File

@ -1,10 +1,16 @@
use poise::{serenity_prelude::CreateEmbed, CreateReply}; use poise::{serenity_prelude::CreateEmbed, CreateReply};
use serde::{Deserialize, Serialize};
use crate::{consts::THEME_COLOR, utils::footer, Context, Error}; use crate::{
consts::THEME_COLOR,
utils::{footer, Extract},
Context, Error,
};
/// Details on supporting the bot and Patreon benefits #[derive(Serialize, Deserialize, Extract)]
#[poise::command(slash_command)] pub struct Options;
pub async fn donate(ctx: Context<'_>) -> Result<(), Error> {
pub async fn donate(ctx: Context<'_>, _options: Options) -> 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")
@ -32,3 +38,9 @@ Just $2 USD/month!
Ok(()) Ok(())
} }
/// Details on supporting the bot and Patreon benefits
#[poise::command(slash_command, rename = "patreon")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
donate(ctx, Options {}).await
}

View File

@ -1,10 +1,16 @@
use poise::{serenity_prelude::CreateEmbed, CreateReply}; use poise::{serenity_prelude::CreateEmbed, CreateReply};
use serde::{Deserialize, Serialize};
use crate::{consts::THEME_COLOR, utils::footer, Context, Error}; use crate::{
consts::THEME_COLOR,
utils::{footer, Extract},
Context, Error,
};
/// Get an overview of bot commands #[derive(Serialize, Deserialize, Extract)]
#[poise::command(slash_command)] pub struct Options;
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
pub async fn help(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
let footer = footer(ctx); let footer = footer(ctx);
ctx.send( ctx.send(
@ -46,3 +52,9 @@ __Advanced Commands__
Ok(()) Ok(())
} }
/// Get an overview of bot commands
#[poise::command(slash_command, rename = "help")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
help(ctx, Options {}).await
}

View File

@ -1,14 +1,19 @@
use poise::{serenity_prelude::CreateEmbed, CreateReply}; use poise::{serenity_prelude::CreateEmbed, CreateReply};
use serde::{Deserialize, Serialize};
use crate::{consts::THEME_COLOR, utils::footer, Context, Error}; use crate::{
consts::THEME_COLOR,
utils::{footer, Extract},
Context, Error,
};
/// Get information about the bot #[derive(Serialize, Deserialize, Extract)]
#[poise::command(slash_command)] pub struct Options;
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
pub async fn info(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
let footer = footer(ctx); let footer = footer(ctx);
let _ = ctx ctx.send(
.send(
CreateReply::default().ephemeral(true).embed( CreateReply::default().ephemeral(true).embed(
CreateEmbed::new() CreateEmbed::new()
.title("Info") .title("Info")
@ -27,7 +32,13 @@ Use our dashboard: https://reminder-bot.com/",
.color(*THEME_COLOR), .color(*THEME_COLOR),
), ),
) )
.await; .await?;
Ok(()) Ok(())
} }
/// Get information about the bot
#[poise::command(slash_command, rename = "info")]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
info(ctx, Options {}).await
}

View File

@ -1,5 +1,5 @@
use poise::{ use poise::{
serenity_prelude::{model::id::ChannelId, Channel, CreateEmbed, CreateEmbedFooter}, serenity_prelude::{model::id::ChannelId, CreateEmbed, CreateEmbedFooter, PartialChannel},
CreateReply, CreateReply,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,17 +9,18 @@ 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,
Context, Error, Context, Error,
}; };
#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)] #[derive(Serialize_repr, Deserialize_repr, Copy, Clone)]
#[repr(u8)] #[repr(u8)]
pub enum TimeDisplayType { pub enum TimeDisplayType {
Absolute = 0, Absolute = 0,
Relative = 1, Relative = 1,
} }
#[derive(Serialize, Deserialize, Copy, Clone, Debug)] #[derive(Serialize, Deserialize, Copy, Clone)]
pub struct LookFlags { pub struct LookFlags {
pub show_disabled: bool, pub show_disabled: bool,
pub channel_id: Option<ChannelId>, pub channel_id: Option<ChannelId>,
@ -32,24 +33,20 @@ impl Default for LookFlags {
} }
} }
/// View reminders on a specific channel #[derive(Serialize, Deserialize, Extract)]
#[poise::command( pub struct Options {
slash_command, channel: Option<PartialChannel>,
identifying_name = "look", disabled: Option<bool>,
default_member_permissions = "MANAGE_GUILD" relative: Option<bool>,
)] }
pub async fn look(
ctx: Context<'_>, pub async fn look(ctx: Context<'_>, options: Options) -> Result<(), Error> {
#[description = "Channel to view reminders on"] channel: Option<Channel>,
#[description = "Whether to show disabled reminders or not"] disabled: Option<bool>,
#[description = "Whether to display times as relative or exact times"] relative: Option<bool>,
) -> Result<(), Error> {
let timezone = ctx.timezone().await; let timezone = ctx.timezone().await;
let flags = LookFlags { let flags = LookFlags {
show_disabled: disabled.unwrap_or(true), show_disabled: options.disabled.unwrap_or(true),
channel_id: channel.map(|c| c.id()), channel_id: options.channel.map(|c| c.id),
time_display: relative.map_or(TimeDisplayType::Relative, |b| { time_display: options.relative.map_or(TimeDisplayType::Relative, |b| {
if b { if b {
TimeDisplayType::Relative TimeDisplayType::Relative
} else { } else {
@ -117,3 +114,14 @@ pub async fn look(
Ok(()) Ok(())
} }
/// View reminders on a specific channel
#[poise::command(slash_command, rename = "look", default_member_permissions = "MANAGE_GUILD")]
pub async fn command(
ctx: Context<'_>,
#[description = "Channel to view reminders on"] channel: Option<PartialChannel>,
#[description = "Whether to show disabled reminders or not"] disabled: Option<bool>,
#[description = "Whether to display times as relative or exact times"] relative: Option<bool>,
) -> Result<(), Error> {
look(ctx, Options { channel, disabled, relative }).await
}

View File

@ -1,11 +1,13 @@
use chrono_tz::Tz; use chrono_tz::Tz;
use log::warn; use log::warn;
use poise::{CreateReply, Modal}; use poise::{CreateReply, Modal};
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,
ApplicationContext, Context, Error, utils::Extract,
Context, Error,
}; };
#[derive(poise::Modal)] #[derive(poise::Modal)]
@ -18,14 +20,61 @@ struct ContentModal {
content: String, content: String,
} }
#[derive(Serialize, Deserialize, Extract)]
pub struct Options {
time: String,
channels: Option<String>,
interval: Option<String>,
expires: Option<String>,
tts: Option<bool>,
timezone: Option<String>,
}
pub async fn multiline(ctx: Context<'_>, options: Options) -> Result<(), Error> {
match ctx {
Context::Application(app_ctx) => {
let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
let data_opt = ContentModal::execute(app_ctx).await?;
match data_opt {
Some(data) => {
create_reminder(
ctx,
options.time,
data.content,
options.channels,
options.interval,
options.expires,
options.tts,
tz,
)
.await
}
None => {
warn!("Unexpected None encountered in /multiline");
Ok(ctx
.send(CreateReply::default().content("Unexpected error.").ephemeral(true))
.await
.map(|_| ())?)
}
}
}
_ => {
warn!("Shouldn't be here");
Ok(ctx
.send(CreateReply::default().content("Unexpected error.").ephemeral(true))
.await
.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( #[poise::command(slash_command, rename = "multiline", default_member_permissions = "MANAGE_GUILD")]
slash_command, pub async fn command(
identifying_name = "multiline", ctx: Context<'_>,
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn multiline(
ctx: ApplicationContext<'_>,
#[description = "A description of the time to set the reminder for"] #[description = "A description of the time to set the reminder for"]
#[autocomplete = "time_hint_autocomplete"] #[autocomplete = "time_hint_autocomplete"]
time: String, time: String,
@ -40,30 +89,5 @@ pub async fn multiline(
#[autocomplete = "timezone_autocomplete"] #[autocomplete = "timezone_autocomplete"]
timezone: Option<String>, timezone: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let tz = timezone.map(|t| t.parse::<Tz>().ok()).flatten(); multiline(ctx, Options { time, channels, interval, expires, tts, timezone }).await
let data_opt = ContentModal::execute(ctx).await?;
match data_opt {
Some(data) => {
create_reminder(
Context::Application(ctx),
time,
data.content,
channels,
interval,
expires,
tts,
tz,
)
.await
}
None => {
warn!("Unexpected None encountered in /multiline");
Ok(Context::Application(ctx)
.send(CreateReply::default().content("Unexpected error.").ephemeral(true))
.await
.map(|_| ())?)
}
}
} }

View File

@ -1,15 +1,18 @@
use crate::{consts::MINUTE, models::CtxData, Context, Error}; use serde::{Deserialize, Serialize};
use crate::{consts::MINUTE, models::CtxData, utils::Extract, Context, Error};
#[derive(Serialize, Deserialize, Extract)]
pub struct Options { pub struct Options {
minutes: Option<isize>, minutes: Option<i64>,
seconds: Option<isize>, seconds: Option<i64>,
} }
pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> { pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> {
let combined_time = let combined_time =
options.minutes.map_or(0, |m| m * MINUTE as isize) + options.seconds.map_or(0, |s| s); options.minutes.map_or(0, |m| m * MINUTE as i64) + options.seconds.map_or(0, |s| s);
if combined_time < i16::MIN as isize || combined_time > i16::MAX as isize { 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();
@ -24,15 +27,11 @@ pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> {
} }
/// 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( #[poise::command(slash_command, rename = "nudge", default_member_permissions = "MANAGE_GUILD")]
slash_command,
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<isize>, #[description = "Number of minutes to nudge new reminders by"] minutes: Option<i64>,
#[description = "Number of seconds to nudge new reminders by"] seconds: Option<isize>, #[description = "Number of seconds to nudge new reminders by"] seconds: Option<i64>,
) -> Result<(), Error> { ) -> Result<(), Error> {
nudge(ctx, Options { minutes, seconds }).await nudge(ctx, Options { minutes, seconds }).await
} }

View File

@ -2,21 +2,22 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
consts::{HOUR, MINUTE}, consts::{HOUR, MINUTE},
utils::Extract,
Context, Error, Context, Error,
}; };
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options { pub struct Options {
hours: Option<isize>, hours: Option<i64>,
minutes: Option<isize>, minutes: Option<i64>,
seconds: Option<isize>, seconds: Option<i64>,
} }
async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> { async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> {
ctx.defer().await?; ctx.defer().await?;
let combined_time = options.hours.map_or(0, |h| h * HOUR as isize) let combined_time = options.hours.map_or(0, |h| h * HOUR as i64)
+ options.minutes.map_or(0, |m| m * MINUTE as isize) + options.minutes.map_or(0, |m| m * MINUTE as i64)
+ options.seconds.map_or(0, |s| s); + options.seconds.map_or(0, |s| s);
if combined_time == 0 { if combined_time == 0 {
@ -69,16 +70,12 @@ async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> {
} }
/// 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( #[poise::command(slash_command, rename = "offset", default_member_permissions = "MANAGE_GUILD")]
slash_command,
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<isize>, #[description = "Number of hours to offset by"] hours: Option<i64>,
#[description = "Number of minutes to offset by"] minutes: Option<isize>, #[description = "Number of minutes to offset by"] minutes: Option<i64>,
#[description = "Number of seconds to offset by"] seconds: Option<isize>, #[description = "Number of seconds to offset by"] seconds: Option<i64>,
) -> Result<(), Error> { ) -> Result<(), Error> {
offset(ctx, Options { hours, minutes, seconds }).await offset(ctx, Options { hours, minutes, seconds }).await
} }

View File

@ -1,22 +1,13 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{models::CtxData, time_parser::natural_parser, utils::Extract, Context, Error};
models::CtxData, time_parser::natural_parser, utils::Extract, ApplicationContext, Context,
Error,
};
#[derive(Serialize, Deserialize, Extract)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options { pub struct Options {
until: Option<String>, until: Option<String>,
} }
impl Extract for Options {
fn extract(ctx: ApplicationContext) -> Self {
Self { until: extract_arg!(ctx, "until", Option<String>) }
}
}
pub async fn pause(ctx: Context<'_>, options: Options) -> Result<(), Error> { pub async fn pause(ctx: Context<'_>, options: Options) -> Result<(), Error> {
let timezone = ctx.timezone().await; let timezone = ctx.timezone().await;
@ -73,11 +64,7 @@ pub async fn pause(ctx: Context<'_>, options: Options) -> Result<(), Error> {
} }
/// 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( #[poise::command(slash_command, rename = "pause", default_member_permissions = "MANAGE_GUILD")]
slash_command,
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>,

View File

@ -4,11 +4,11 @@ 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_arg, Extract}, utils::Extract,
ApplicationContext, Context, Error, Context, Error,
}; };
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options { pub struct Options {
time: String, time: String,
content: String, content: String,
@ -19,20 +19,6 @@ pub struct Options {
timezone: Option<String>, timezone: Option<String>,
} }
impl Extract for Options {
fn extract(ctx: ApplicationContext) -> Self {
Self {
time: extract_arg!(ctx, "time", String),
content: extract_arg!(ctx, "content", String),
channels: extract_arg!(ctx, "channels", Option<String>),
interval: extract_arg!(ctx, "interval", Option<String>),
expires: extract_arg!(ctx, "expires", Option<String>),
tts: extract_arg!(ctx, "tts", Option<bool>),
timezone: extract_arg!(ctx, "timezone", Option<String>),
}
}
}
pub async fn remind(ctx: Context<'_>, options: Options) -> Result<(), Error> { pub async fn remind(ctx: Context<'_>, options: Options) -> Result<(), Error> {
let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten(); let tz = options.timezone.map(|t| t.parse::<Tz>().ok()).flatten();
@ -50,11 +36,7 @@ pub async fn remind(ctx: Context<'_>, options: Options) -> Result<(), Error> {
} }
/// 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( #[poise::command(slash_command, rename = "remind", default_member_permissions = "MANAGE_GUILD")]
slash_command,
identifying_name = "remind",
default_member_permissions = "MANAGE_GUILD"
)]
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"]

View File

@ -8,11 +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, Context, commands::autocomplete::timezone_autocomplete, consts::THEME_COLOR, models::CtxData,
Error, utils::Extract, Context, Error,
}; };
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options { pub struct Options {
pub timezone: Option<String>, pub timezone: Option<String>,
} }
@ -116,7 +116,7 @@ You may want to use one of the popular timezones below, otherwise click [here](h
} }
/// Select your timezone /// Select your timezone
#[poise::command(slash_command, identifying_name = "timezone")] #[poise::command(slash_command, rename = "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"]

View File

@ -2,17 +2,11 @@ use log::warn;
use poise::CreateReply; use poise::CreateReply;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{models::CtxData, utils::Extract, ApplicationContext, Context, Error}; use crate::{models::CtxData, utils::Extract, Context, Error};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Extract)]
pub struct Options; pub struct Options;
impl Extract for Options {
fn extract(_ctx: ApplicationContext) -> Self {
Self {}
}
}
pub async fn webhook(ctx: Context<'_>, _options: Options) -> Result<(), Error> { pub async fn webhook(ctx: Context<'_>, _options: Options) -> Result<(), Error> {
match ctx.channel_data().await { match ctx.channel_data().await {
Ok(data) => { Ok(data) => {
@ -40,11 +34,7 @@ Do not share it!
} }
/// View the webhook being used to send reminders to this channel /// View the webhook being used to send reminders to this channel
#[poise::command( #[poise::command(slash_command, rename = "webhook", required_permissions = "ADMINISTRATOR")]
slash_command,
identifying_name = "webhook_url",
required_permissions = "ADMINISTRATOR"
)]
pub async fn command(ctx: Context<'_>) -> Result<(), Error> { pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
webhook(ctx, Options {}).await webhook(ctx, Options {}).await
} }

View File

@ -1,70 +0,0 @@
macro_rules! extract_arg {
($ctx:ident, $name:literal, String) => {
$ctx.args.iter().find(|opt| opt.name == $name).map(|opt| &opt.value).map_or_else(
|| String::new(),
|v| match v {
poise::serenity_prelude::ResolvedValue::String(s) => s.to_string(),
_ => String::new(),
},
)
};
($ctx:ident, $name:literal, Option<String>) => {
$ctx.args
.iter()
.find(|opt| opt.name == $name)
.map(|opt| &opt.value)
.map(|v| match v {
poise::serenity_prelude::ResolvedValue::String(s) => Some(s.to_string()),
_ => None,
})
.flatten()
};
($ctx:ident, $name:literal, bool) => {
$ctx.args.iter().find(|opt| opt.name == $name).map(|opt| &opt.value).map_or(false, |v| {
match v {
poise::serenity_prelude::ResolvedValue::Boolean(b) => b.to_owned(),
_ => false,
}
})
};
($ctx:ident, $name:literal, Option<bool>) => {
$ctx.args
.iter()
.find(|opt| opt.name == $name)
.map(|opt| &opt.value)
.map(|v| match v {
poise::serenity_prelude::ResolvedValue::Boolean(b) => Some(b.to_owned()),
_ => None,
})
.flatten()
};
}
use proc_macro::TokenStream;
use syn::parse::Parser;
#[proc_macro_derive(Extract)]
pub fn extract(input: TokenStream) -> TokenStream {
// Construct a string representation of the type definition
let s = input.to_string();
// Parse the string representation
let ast = syn::parse_derive_input(&s).unwrap();
// Build the impl
let gen = impl_extract(&ast);
// Return the generated impl
gen.parse().unwrap()
}
fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
TokenStream::from(quote::quote! {
impl Extract for #name {
fn extract(ctx: ) -> Self {
println!("Hello, World! My name is {}", stringify!(#name));
}
}
})
}

View File

@ -35,9 +35,9 @@ use tokio::sync::{broadcast, broadcast::Sender, RwLock};
use crate::{ use crate::{
commands::{ commands::{
allowed_dm, clock::clock, clock_context_menu::clock_context_menu, command_macro, allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
dashboard::dashboard, delete, donate::donate, help::help, info::info, look, multiline, delete, donate, help, info, look, multiline, nudge, offset, pause, remind, settings, timer,
nudge, offset, pause, remind, settings, timer, timezone, todo, webhook, timezone, todo, webhook,
}, },
consts::THEME_COLOR, consts::THEME_COLOR,
event_handlers::listener, event_handlers::listener,
@ -103,12 +103,12 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
let options = poise::FrameworkOptions { let options = poise::FrameworkOptions {
commands: vec![ commands: vec![
help(), help::command(),
info(), info::command(),
donate(), clock::command(),
clock(), donate::command(),
clock_context_menu(), clock_context_menu(),
dashboard(), dashboard::command(),
timezone::command(), timezone::command(),
poise::Command { poise::Command {
subcommands: vec![ subcommands: vec![
@ -141,8 +141,8 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
pause::command(), pause::command(),
offset::command(), offset::command(),
nudge::command(), nudge::command(),
look::look(), look::command(),
delete::delete(), delete::command(),
poise::Command { poise::Command {
subcommands: vec![ subcommands: vec![
timer::list_timer::list_timer(), timer::list_timer::list_timer(),
@ -151,7 +151,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
], ],
..timer::timer() ..timer::timer()
}, },
multiline::multiline(), multiline::command(),
remind::command(), remind::command(),
poise::Command { poise::Command {
subcommands: vec![ subcommands: vec![

View File

@ -68,3 +68,90 @@ pub fn footer(ctx: Context<'_>) -> CreateEmbedFooter {
pub trait Extract { pub trait Extract {
fn extract(ctx: ApplicationContext) -> Self; fn extract(ctx: ApplicationContext) -> Self;
} }
pub use extract_macro::Extract;
macro_rules! extract_arg {
($ctx:ident, $name:ident, String) => {
$ctx.args
.iter()
.find(|opt| opt.name == stringify!($name))
.map(|opt| &opt.value)
.map_or_else(
|| String::new(),
|v| match v {
poise::serenity_prelude::ResolvedValue::String(s) => s.to_string(),
_ => String::new(),
},
)
};
($ctx:ident, $name:ident, Option<String>) => {
$ctx.args
.iter()
.find(|opt| opt.name == stringify!($name))
.map(|opt| &opt.value)
.map(|v| match v {
poise::serenity_prelude::ResolvedValue::String(s) => Some(s.to_string()),
_ => None,
})
.flatten()
};
($ctx:ident, $name:ident, bool) => {
$ctx.args.iter().find(|opt| opt.name == stringify!($name)).map(|opt| &opt.value).map_or(
false,
|v| match v {
poise::serenity_prelude::ResolvedValue::Boolean(b) => b.to_owned(),
_ => false,
},
)
};
($ctx:ident, $name:ident, Option<bool>) => {
$ctx.args
.iter()
.find(|opt| opt.name == stringify!($name))
.map(|opt| &opt.value)
.map(|v| match v {
poise::serenity_prelude::ResolvedValue::Boolean(b) => Some(b.to_owned()),
_ => None,
})
.flatten()
};
($ctx:ident, $name:ident, Option<PartialChannel>) => {
$ctx.args
.iter()
.find(|opt| opt.name == stringify!($name))
.map(|opt| &opt.value)
.map(|v| match v {
poise::serenity_prelude::ResolvedValue::Channel(partial) => {
Some(partial.to_owned())
}
_ => None,
})
.flatten()
.cloned()
};
($ctx:ident, $name:ident, i64) => {
$ctx.args
.iter()
.find(|opt| opt.name == stringify!($name))
.map(|opt| &opt.value)
.map(|v| match v {
poise::serenity_prelude::ResolvedValue::Integer(int) => *int,
_ => 0,
})
.flatten()
};
($ctx:ident, $name:ident, Option<i64>) => {
$ctx.args
.iter()
.find(|opt| opt.name == stringify!($name))
.map(|opt| &opt.value)
.map(|v| match v {
poise::serenity_prelude::ResolvedValue::Integer(int) => Some(*int),
_ => None,
})
.flatten()
};
}
pub(crate) use extract_arg;