diff --git a/Cargo.lock b/Cargo.lock index 6a7bd16..479f575 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,6 +773,14 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "extract_macro" +version = "0.1.0" +dependencies = [ + "quote", + "syn 2.0.49", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -2301,7 +2309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] -name = "reminder-rs" +name = "reminder_rs" version = "1.6.50" dependencies = [ "base64 0.21.5", @@ -2309,6 +2317,7 @@ dependencies = [ "chrono-tz", "dotenv", "env_logger", + "extract_macro", "lazy-regex", "lazy_static", "levenshtein", diff --git a/Cargo.toml b/Cargo.toml index 27cec1d..1da88c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "reminder-rs" +name = "reminder_rs" version = "1.6.50" authors = ["Jude Southworth "] edition = "2021" @@ -35,6 +35,9 @@ path = "postman" [dependencies.reminder_web] path = "web" +[dependencies.extract_macro] +path = "extract_macro" + [package.metadata.deb] depends = "$auto, python3-dateparser (>= 1.0.0)" suggests = "mysql-server-8.0, nginx" diff --git a/src/extract_macro/Cargo.lock b/extract_macro/Cargo.lock similarity index 100% rename from src/extract_macro/Cargo.lock rename to extract_macro/Cargo.lock diff --git a/src/extract_macro/Cargo.toml b/extract_macro/Cargo.toml similarity index 100% rename from src/extract_macro/Cargo.toml rename to extract_macro/Cargo.toml diff --git a/extract_macro/src/lib.rs b/extract_macro/src/lib.rs new file mode 100644 index 0000000..bca3857 --- /dev/null +++ b/extract_macro/src/lib.rs @@ -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"); + } + } +} diff --git a/src/commands/clock.rs b/src/commands/clock.rs index 6e13ebb..9709271 100644 --- a/src/commands/clock.rs +++ b/src/commands/clock.rs @@ -1,11 +1,13 @@ use chrono::Utc; 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 -#[poise::command(slash_command)] -pub async fn clock(ctx: Context<'_>) -> Result<(), Error> { +#[derive(Serialize, Deserialize, Extract)] +pub struct Options; + +pub async fn clock(ctx: Context<'_>, _options: Options) -> Result<(), Error> { ctx.defer_ephemeral().await?; let tz = ctx.timezone().await; @@ -20,3 +22,9 @@ pub async fn clock(ctx: Context<'_>) -> Result<(), Error> { 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 +} diff --git a/src/commands/dashboard.rs b/src/commands/dashboard.rs index 15a5987..7a65500 100644 --- a/src/commands/dashboard.rs +++ b/src/commands/dashboard.rs @@ -1,10 +1,16 @@ 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 -#[poise::command(slash_command)] -pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> { +#[derive(Serialize, Deserialize, Extract)] +pub struct Options; + +pub async fn dashboard(ctx: Context<'_>, _options: Options) -> Result<(), Error> { let footer = footer(ctx); ctx.send( @@ -20,3 +26,9 @@ pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> { 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 +} diff --git a/src/commands/delete.rs b/src/commands/delete.rs index 3555909..3c2c561 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -6,6 +6,7 @@ use poise::{ }, CreateReply, }; +use serde::{Deserialize, Serialize}; use crate::{ component_models::{ @@ -14,29 +15,10 @@ use crate::{ }, consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR}, models::{reminder::Reminder, CtxData}, + utils::Extract, 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 { let mut rows = 0; let mut char_count = 0; @@ -154,3 +136,25 @@ pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> Cr .embed(embed) .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 +} diff --git a/src/commands/donate.rs b/src/commands/donate.rs index 9005453..9459803 100644 --- a/src/commands/donate.rs +++ b/src/commands/donate.rs @@ -1,10 +1,16 @@ 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 -#[poise::command(slash_command)] -pub async fn donate(ctx: Context<'_>) -> Result<(), Error> { +#[derive(Serialize, Deserialize, Extract)] +pub struct Options; + +pub async fn donate(ctx: Context<'_>, _options: Options) -> Result<(), Error> { let footer = footer(ctx); ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate") @@ -32,3 +38,9 @@ Just $2 USD/month! 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 +} diff --git a/src/commands/help.rs b/src/commands/help.rs index bb76eac..47d4c3c 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -1,10 +1,16 @@ 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 -#[poise::command(slash_command)] -pub async fn help(ctx: Context<'_>) -> Result<(), Error> { +#[derive(Serialize, Deserialize, Extract)] +pub struct Options; + +pub async fn help(ctx: Context<'_>, _options: Options) -> Result<(), Error> { let footer = footer(ctx); ctx.send( @@ -46,3 +52,9 @@ __Advanced Commands__ 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 +} diff --git a/src/commands/info.rs b/src/commands/info.rs index 03870e9..2bc22fc 100644 --- a/src/commands/info.rs +++ b/src/commands/info.rs @@ -1,19 +1,24 @@ 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 -#[poise::command(slash_command)] -pub async fn info(ctx: Context<'_>) -> Result<(), Error> { +#[derive(Serialize, Deserialize, Extract)] +pub struct Options; + +pub async fn info(ctx: Context<'_>, _options: Options) -> Result<(), Error> { let footer = footer(ctx); - let _ = ctx - .send( - CreateReply::default().ephemeral(true).embed( - CreateEmbed::new() - .title("Info") - .description( - "Help: `/help` + ctx.send( + CreateReply::default().ephemeral(true).embed( + CreateEmbed::new() + .title("Info") + .description( + "Help: `/help` **Welcome to Reminder Bot!** Developer: <@203532103185465344> @@ -22,12 +27,18 @@ Find me on https://discord.jellywx.com and on https://github.com/JellyWX :) Invite the bot: https://invite.reminder-bot.com/ Use our dashboard: https://reminder-bot.com/", - ) - .footer(footer) - .color(*THEME_COLOR), - ), - ) - .await; + ) + .footer(footer) + .color(*THEME_COLOR), + ), + ) + .await?; Ok(()) } + +/// Get information about the bot +#[poise::command(slash_command, rename = "info")] +pub async fn command(ctx: Context<'_>) -> Result<(), Error> { + info(ctx, Options {}).await +} diff --git a/src/commands/look.rs b/src/commands/look.rs index 4fbbcf3..bb4a01e 100644 --- a/src/commands/look.rs +++ b/src/commands/look.rs @@ -1,5 +1,5 @@ use poise::{ - serenity_prelude::{model::id::ChannelId, Channel, CreateEmbed, CreateEmbedFooter}, + serenity_prelude::{model::id::ChannelId, CreateEmbed, CreateEmbedFooter, PartialChannel}, CreateReply, }; use serde::{Deserialize, Serialize}; @@ -9,17 +9,18 @@ use crate::{ component_models::pager::{LookPager, Pager}, consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, models::{reminder::Reminder, CtxData}, + utils::Extract, Context, Error, }; -#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)] +#[derive(Serialize_repr, Deserialize_repr, Copy, Clone)] #[repr(u8)] pub enum TimeDisplayType { Absolute = 0, Relative = 1, } -#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +#[derive(Serialize, Deserialize, Copy, Clone)] pub struct LookFlags { pub show_disabled: bool, pub channel_id: Option, @@ -32,24 +33,20 @@ impl Default for LookFlags { } } -/// View reminders on a specific channel -#[poise::command( - slash_command, - identifying_name = "look", - default_member_permissions = "MANAGE_GUILD" -)] -pub async fn look( - ctx: Context<'_>, - #[description = "Channel to view reminders on"] channel: Option, - #[description = "Whether to show disabled reminders or not"] disabled: Option, - #[description = "Whether to display times as relative or exact times"] relative: Option, -) -> Result<(), Error> { +#[derive(Serialize, Deserialize, Extract)] +pub struct Options { + channel: Option, + disabled: Option, + relative: Option, +} + +pub async fn look(ctx: Context<'_>, options: Options) -> Result<(), Error> { let timezone = ctx.timezone().await; let flags = LookFlags { - show_disabled: disabled.unwrap_or(true), - channel_id: channel.map(|c| c.id()), - time_display: relative.map_or(TimeDisplayType::Relative, |b| { + show_disabled: options.disabled.unwrap_or(true), + channel_id: options.channel.map(|c| c.id), + time_display: options.relative.map_or(TimeDisplayType::Relative, |b| { if b { TimeDisplayType::Relative } else { @@ -117,3 +114,14 @@ pub async fn look( 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, + #[description = "Whether to show disabled reminders or not"] disabled: Option, + #[description = "Whether to display times as relative or exact times"] relative: Option, +) -> Result<(), Error> { + look(ctx, Options { channel, disabled, relative }).await +} diff --git a/src/commands/multiline.rs b/src/commands/multiline.rs index f4c0fba..36bb7fd 100644 --- a/src/commands/multiline.rs +++ b/src/commands/multiline.rs @@ -1,11 +1,13 @@ use chrono_tz::Tz; use log::warn; use poise::{CreateReply, Modal}; +use serde::{Deserialize, Serialize}; use crate::{ commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete}, models::reminder::create_reminder, - ApplicationContext, Context, Error, + utils::Extract, + Context, Error, }; #[derive(poise::Modal)] @@ -18,14 +20,61 @@ struct ContentModal { content: String, } +#[derive(Serialize, Deserialize, Extract)] +pub struct Options { + time: String, + channels: Option, + interval: Option, + expires: Option, + tts: Option, + timezone: Option, +} + +pub async fn multiline(ctx: Context<'_>, options: Options) -> Result<(), Error> { + match ctx { + Context::Application(app_ctx) => { + let tz = options.timezone.map(|t| t.parse::().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. -#[poise::command( - slash_command, - identifying_name = "multiline", - default_member_permissions = "MANAGE_GUILD" -)] -pub async fn multiline( - ctx: ApplicationContext<'_>, +#[poise::command(slash_command, rename = "multiline", default_member_permissions = "MANAGE_GUILD")] +pub async fn command( + ctx: Context<'_>, #[description = "A description of the time to set the reminder for"] #[autocomplete = "time_hint_autocomplete"] time: String, @@ -40,30 +89,5 @@ pub async fn multiline( #[autocomplete = "timezone_autocomplete"] timezone: Option, ) -> Result<(), Error> { - let tz = timezone.map(|t| t.parse::().ok()).flatten(); - 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(|_| ())?) - } - } + multiline(ctx, Options { time, channels, interval, expires, tts, timezone }).await } diff --git a/src/commands/nudge.rs b/src/commands/nudge.rs index 06a39e3..2c23999 100644 --- a/src/commands/nudge.rs +++ b/src/commands/nudge.rs @@ -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 { - minutes: Option, - seconds: Option, + minutes: Option, + seconds: Option, } pub async fn nudge(ctx: Context<'_>, options: Options) -> Result<(), Error> { 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?; } else { 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`) -#[poise::command( - slash_command, - identifying_name = "nudge", - default_member_permissions = "MANAGE_GUILD" -)] +#[poise::command(slash_command, rename = "nudge", default_member_permissions = "MANAGE_GUILD")] pub async fn command( ctx: Context<'_>, - #[description = "Number of minutes to nudge new reminders by"] minutes: Option, - #[description = "Number of seconds to nudge new reminders by"] seconds: Option, + #[description = "Number of minutes to nudge new reminders by"] minutes: Option, + #[description = "Number of seconds to nudge new reminders by"] seconds: Option, ) -> Result<(), Error> { nudge(ctx, Options { minutes, seconds }).await } diff --git a/src/commands/offset.rs b/src/commands/offset.rs index d923502..b1bd9bb 100644 --- a/src/commands/offset.rs +++ b/src/commands/offset.rs @@ -2,21 +2,22 @@ use serde::{Deserialize, Serialize}; use crate::{ consts::{HOUR, MINUTE}, + utils::Extract, Context, Error, }; -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Extract)] pub struct Options { - hours: Option, - minutes: Option, - seconds: Option, + hours: Option, + minutes: Option, + seconds: Option, } async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> { ctx.defer().await?; - let combined_time = options.hours.map_or(0, |h| h * HOUR as isize) - + options.minutes.map_or(0, |m| m * MINUTE as isize) + let combined_time = options.hours.map_or(0, |h| h * HOUR as i64) + + options.minutes.map_or(0, |m| m * MINUTE as i64) + options.seconds.map_or(0, |s| s); 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 -#[poise::command( - slash_command, - identifying_name = "offset", - default_member_permissions = "MANAGE_GUILD" -)] +#[poise::command(slash_command, rename = "offset", default_member_permissions = "MANAGE_GUILD")] pub async fn command( ctx: Context<'_>, - #[description = "Number of hours to offset by"] hours: Option, - #[description = "Number of minutes to offset by"] minutes: Option, - #[description = "Number of seconds to offset by"] seconds: Option, + #[description = "Number of hours to offset by"] hours: Option, + #[description = "Number of minutes to offset by"] minutes: Option, + #[description = "Number of seconds to offset by"] seconds: Option, ) -> Result<(), Error> { offset(ctx, Options { hours, minutes, seconds }).await } diff --git a/src/commands/pause.rs b/src/commands/pause.rs index ed1c187..c8b65ea 100644 --- a/src/commands/pause.rs +++ b/src/commands/pause.rs @@ -1,22 +1,13 @@ use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; -use crate::{ - models::CtxData, time_parser::natural_parser, utils::Extract, ApplicationContext, Context, - Error, -}; +use crate::{models::CtxData, time_parser::natural_parser, utils::Extract, Context, Error}; #[derive(Serialize, Deserialize, Extract)] pub struct Options { until: Option, } -impl Extract for Options { - fn extract(ctx: ApplicationContext) -> Self { - Self { until: extract_arg!(ctx, "until", Option) } - } -} - pub async fn pause(ctx: Context<'_>, options: Options) -> Result<(), Error> { 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 -#[poise::command( - slash_command, - identifying_name = "pause", - default_member_permissions = "MANAGE_GUILD" -)] +#[poise::command(slash_command, rename = "pause", default_member_permissions = "MANAGE_GUILD")] pub async fn command( ctx: Context<'_>, #[description = "When to pause until"] until: Option, diff --git a/src/commands/remind.rs b/src/commands/remind.rs index 302a4ae..a88b8f5 100644 --- a/src/commands/remind.rs +++ b/src/commands/remind.rs @@ -4,11 +4,11 @@ use serde::{Deserialize, Serialize}; use crate::{ commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete}, models::reminder::create_reminder, - utils::{extract_arg, Extract}, - ApplicationContext, Context, Error, + utils::Extract, + Context, Error, }; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Extract)] pub struct Options { time: String, content: String, @@ -19,20 +19,6 @@ pub struct Options { timezone: Option, } -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), - interval: extract_arg!(ctx, "interval", Option), - expires: extract_arg!(ctx, "expires", Option), - tts: extract_arg!(ctx, "tts", Option), - timezone: extract_arg!(ctx, "timezone", Option), - } - } -} - pub async fn remind(ctx: Context<'_>, options: Options) -> Result<(), Error> { let tz = options.timezone.map(|t| t.parse::().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. -#[poise::command( - slash_command, - identifying_name = "remind", - default_member_permissions = "MANAGE_GUILD" -)] +#[poise::command(slash_command, rename = "remind", default_member_permissions = "MANAGE_GUILD")] pub async fn command( ctx: Context<'_>, #[description = "The time (and optionally date) to set the reminder for"] diff --git a/src/commands/timezone.rs b/src/commands/timezone.rs index c441de1..d2a46de 100644 --- a/src/commands/timezone.rs +++ b/src/commands/timezone.rs @@ -8,11 +8,11 @@ use poise::{ use serde::{Deserialize, Serialize}; use crate::{ - commands::autocomplete::timezone_autocomplete, consts::THEME_COLOR, models::CtxData, Context, - Error, + commands::autocomplete::timezone_autocomplete, consts::THEME_COLOR, models::CtxData, + utils::Extract, Context, Error, }; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Extract)] pub struct Options { pub timezone: Option, } @@ -116,7 +116,7 @@ You may want to use one of the popular timezones below, otherwise click [here](h } /// Select your timezone -#[poise::command(slash_command, identifying_name = "timezone")] +#[poise::command(slash_command, rename = "timezone")] pub async fn command( ctx: Context<'_>, #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"] diff --git a/src/commands/webhook.rs b/src/commands/webhook.rs index 51bc33a..800835a 100644 --- a/src/commands/webhook.rs +++ b/src/commands/webhook.rs @@ -2,17 +2,11 @@ use log::warn; use poise::CreateReply; 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; -impl Extract for Options { - fn extract(_ctx: ApplicationContext) -> Self { - Self {} - } -} - pub async fn webhook(ctx: Context<'_>, _options: Options) -> Result<(), Error> { match ctx.channel_data().await { Ok(data) => { @@ -40,11 +34,7 @@ Do not share it! } /// View the webhook being used to send reminders to this channel -#[poise::command( - slash_command, - identifying_name = "webhook_url", - required_permissions = "ADMINISTRATOR" -)] +#[poise::command(slash_command, rename = "webhook", required_permissions = "ADMINISTRATOR")] pub async fn command(ctx: Context<'_>) -> Result<(), Error> { webhook(ctx, Options {}).await } diff --git a/src/extract_macro/src/lib.rs b/src/extract_macro/src/lib.rs deleted file mode 100644 index 33267af..0000000 --- a/src/extract_macro/src/lib.rs +++ /dev/null @@ -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) => { - $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) => { - $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)); - } - } - }) -} diff --git a/src/main.rs b/src/main.rs index c852357..0cbf931 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,9 +35,9 @@ use tokio::sync::{broadcast, broadcast::Sender, RwLock}; use crate::{ commands::{ - allowed_dm, clock::clock, clock_context_menu::clock_context_menu, command_macro, - dashboard::dashboard, delete, donate::donate, help::help, info::info, look, multiline, - nudge, offset, pause, remind, settings, timer, timezone, todo, webhook, + allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard, + delete, donate, help, info, look, multiline, nudge, offset, pause, remind, settings, timer, + timezone, todo, webhook, }, consts::THEME_COLOR, event_handlers::listener, @@ -103,12 +103,12 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { let options = poise::FrameworkOptions { commands: vec![ - help(), - info(), - donate(), - clock(), + help::command(), + info::command(), + clock::command(), + donate::command(), clock_context_menu(), - dashboard(), + dashboard::command(), timezone::command(), poise::Command { subcommands: vec![ @@ -141,8 +141,8 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { pause::command(), offset::command(), nudge::command(), - look::look(), - delete::delete(), + look::command(), + delete::command(), poise::Command { subcommands: vec![ timer::list_timer::list_timer(), @@ -151,7 +151,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { ], ..timer::timer() }, - multiline::multiline(), + multiline::command(), remind::command(), poise::Command { subcommands: vec![ diff --git a/src/utils.rs b/src/utils.rs index 86f5575..682d68b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -68,3 +68,90 @@ pub fn footer(ctx: Context<'_>) -> CreateEmbedFooter { pub trait Extract { 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) => { + $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) => { + $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) => { + $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) => { + $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;