diff --git a/.gitignore b/.gitignore index 7c89042..e72569b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +target .env /venv .cargo diff --git a/src/commands/nudge.rs b/src/commands/nudge.rs index f6816a5..06a39e3 100644 --- a/src/commands/nudge.rs +++ b/src/commands/nudge.rs @@ -1,17 +1,13 @@ use crate::{consts::MINUTE, models::CtxData, Context, 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" -)] -pub async fn nudge( - ctx: Context<'_>, - #[description = "Number of minutes to nudge new reminders by"] minutes: Option, - #[description = "Number of seconds to nudge new reminders by"] seconds: Option, -) -> Result<(), Error> { - let combined_time = minutes.map_or(0, |m| m * MINUTE as isize) + seconds.map_or(0, |s| s); +pub struct Options { + 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); if combined_time < i16::MIN as isize || combined_time > i16::MAX as isize { ctx.say("Nudge times must be less than 500 minutes").await?; @@ -26,3 +22,17 @@ pub async fn nudge( Ok(()) } + +/// 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" +)] +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, +) -> Result<(), Error> { + nudge(ctx, Options { minutes, seconds }).await +} diff --git a/src/commands/offset.rs b/src/commands/offset.rs index b359dfb..d923502 100644 --- a/src/commands/offset.rs +++ b/src/commands/offset.rs @@ -1,25 +1,23 @@ +use serde::{Deserialize, Serialize}; + use crate::{ consts::{HOUR, MINUTE}, Context, 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" -)] -pub async fn offset( - 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, -) -> Result<(), Error> { +#[derive(Serialize, Deserialize, Default)] +pub struct Options { + hours: Option, + minutes: Option, + seconds: Option, +} + +async fn offset(ctx: Context<'_>, options: Options) -> Result<(), Error> { ctx.defer().await?; - let combined_time = hours.map_or(0, |h| h * HOUR as isize) - + minutes.map_or(0, |m| m * MINUTE as isize) - + seconds.map_or(0, |s| s); + let combined_time = options.hours.map_or(0, |h| h * HOUR as isize) + + options.minutes.map_or(0, |m| m * MINUTE as isize) + + options.seconds.map_or(0, |s| s); if combined_time == 0 { ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?; @@ -69,3 +67,18 @@ pub async fn offset( Ok(()) } + +/// 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" +)] +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, +) -> Result<(), Error> { + offset(ctx, Options { hours, minutes, seconds }).await +} diff --git a/src/commands/pause.rs b/src/commands/pause.rs index 158a387..ed1c187 100644 --- a/src/commands/pause.rs +++ b/src/commands/pause.rs @@ -1,22 +1,28 @@ use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; -use crate::{models::CtxData, time_parser::natural_parser, Context, Error}; +use crate::{ + models::CtxData, time_parser::natural_parser, utils::Extract, ApplicationContext, Context, + 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" -)] -pub async fn pause( - ctx: Context<'_>, - #[description = "When to pause until"] until: Option, -) -> Result<(), 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; let mut channel = ctx.channel_data().await.unwrap(); - match until { + match options.until { Some(until) => { let parsed = natural_parser(&until, &timezone.to_string()).await; @@ -65,3 +71,16 @@ pub async fn pause( Ok(()) } + +/// 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" +)] +pub async fn command( + ctx: Context<'_>, + #[description = "When to pause until"] until: Option, +) -> Result<(), Error> { + pause(ctx, Options { until }).await +} diff --git a/src/commands/remind.rs b/src/commands/remind.rs index 8141986..302a4ae 100644 --- a/src/commands/remind.rs +++ b/src/commands/remind.rs @@ -4,18 +4,49 @@ use serde::{Deserialize, Serialize}; use crate::{ commands::autocomplete::{time_hint_autocomplete, timezone_autocomplete}, models::reminder::create_reminder, + utils::{extract_arg, Extract}, ApplicationContext, Context, Error, }; -#[derive(Serialize, Deserialize, Default)] -pub struct RemindOptions { - pub time: String, - pub content: String, - pub channels: Option, - pub interval: Option, - pub expires: Option, - pub tts: Option, - pub timezone: Option, +#[derive(Serialize, Deserialize)] +pub struct Options { + time: String, + content: String, + channels: Option, + interval: Option, + expires: Option, + tts: Option, + 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(); + + create_reminder( + ctx, + options.time, + options.content, + options.channels, + options.interval, + options.expires, + options.tts, + tz, + ) + .await } /// Create a reminder. Press "+4 more" for other options. Use "/multiline" for multiline content. @@ -24,8 +55,8 @@ pub struct RemindOptions { identifying_name = "remind", default_member_permissions = "MANAGE_GUILD" )] -pub async fn remind( - ctx: ApplicationContext<'_>, +pub async fn command( + ctx: Context<'_>, #[description = "The time (and optionally date) to set the reminder for"] #[autocomplete = "time_hint_autocomplete"] time: String, @@ -41,8 +72,5 @@ pub async fn remind( #[autocomplete = "timezone_autocomplete"] timezone: Option, ) -> Result<(), Error> { - let tz = timezone.map(|t| t.parse::().ok()).flatten(); - - create_reminder(Context::Application(ctx), time, content, channels, interval, expires, tts, tz) - .await + remind(ctx, Options { time, content, channels, interval, expires, tts, timezone }).await } diff --git a/src/commands/timezone.rs b/src/commands/timezone.rs index 452fa50..c441de1 100644 --- a/src/commands/timezone.rs +++ b/src/commands/timezone.rs @@ -5,25 +5,24 @@ use poise::{ serenity_prelude::{CreateEmbed, CreateEmbedFooter}, CreateReply, }; +use serde::{Deserialize, Serialize}; use crate::{ commands::autocomplete::timezone_autocomplete, consts::THEME_COLOR, models::CtxData, Context, Error, }; -/// Select your timezone -#[poise::command(slash_command, identifying_name = "timezone")] -pub async fn timezone( - ctx: Context<'_>, - #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"] - #[autocomplete = "timezone_autocomplete"] - timezone: Option, -) -> Result<(), Error> { +#[derive(Serialize, Deserialize)] +pub struct Options { + pub timezone: Option, +} + +pub async fn timezone_fn(ctx: Context<'_>, options: Options) -> Result<(), Error> { let mut user_data = ctx.author_data().await.unwrap(); let footer_text = format!("Current timezone: {}", user_data.timezone); - if let Some(timezone) = timezone { + if let Some(timezone) = options.timezone { match timezone.parse::() { Ok(tz) => { user_data.timezone = timezone.clone(); @@ -115,3 +114,14 @@ You may want to use one of the popular timezones below, otherwise click [here](h Ok(()) } + +/// Select your timezone +#[poise::command(slash_command, identifying_name = "timezone")] +pub async fn command( + ctx: Context<'_>, + #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"] + #[autocomplete = "timezone_autocomplete"] + timezone: Option, +) -> Result<(), Error> { + timezone_fn(ctx, Options { timezone }).await +} diff --git a/src/commands/webhook.rs b/src/commands/webhook.rs index 904b0d0..51bc33a 100644 --- a/src/commands/webhook.rs +++ b/src/commands/webhook.rs @@ -1,15 +1,19 @@ use log::warn; use poise::CreateReply; +use serde::{Deserialize, Serialize}; -use crate::{models::CtxData, Context, Error}; +use crate::{models::CtxData, utils::Extract, ApplicationContext, Context, Error}; -/// View the webhook being used to send reminders to this channel -#[poise::command( - slash_command, - identifying_name = "webhook_url", - required_permissions = "ADMINISTRATOR" -)] -pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> { +#[derive(Serialize, Deserialize)] +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) => { if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) { @@ -34,3 +38,13 @@ Do not share it! Ok(()) } + +/// View the webhook being used to send reminders to this channel +#[poise::command( + slash_command, + identifying_name = "webhook_url", + required_permissions = "ADMINISTRATOR" +)] +pub async fn command(ctx: Context<'_>) -> Result<(), Error> { + webhook(ctx, Options {}).await +} diff --git a/src/extract_macro/Cargo.lock b/src/extract_macro/Cargo.lock new file mode 100644 index 0000000..f0b5f53 --- /dev/null +++ b/src/extract_macro/Cargo.lock @@ -0,0 +1,46 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "extract_macro" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/src/extract_macro/Cargo.toml b/src/extract_macro/Cargo.toml new file mode 100644 index 0000000..77b3315 --- /dev/null +++ b/src/extract_macro/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "extract_macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.35" +syn = { version = "2.0.49", features = ["full"] } diff --git a/src/extract_macro/src/lib.rs b/src/extract_macro/src/lib.rs new file mode 100644 index 0000000..33267af --- /dev/null +++ b/src/extract_macro/src/lib.rs @@ -0,0 +1,70 @@ +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 f9dc616..c852357 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,7 +37,7 @@ 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::timezone, todo, webhook::webhook, + nudge, offset, pause, remind, settings, timer, timezone, todo, webhook, }, consts::THEME_COLOR, event_handlers::listener, @@ -109,7 +109,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { clock(), clock_context_menu(), dashboard(), - timezone(), + timezone::command(), poise::Command { subcommands: vec![ allowed_dm::set_allowed_dm::set_allowed_dm(), @@ -127,7 +127,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { }], ..settings::settings() }, - webhook(), + webhook::command(), poise::Command { subcommands: vec![ command_macro::delete_macro::delete_macro(), @@ -138,9 +138,9 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { ], ..command_macro::command_macro() }, - pause::pause(), - offset::offset(), - nudge::nudge(), + pause::command(), + offset::command(), + nudge::command(), look::look(), delete::delete(), poise::Command { @@ -152,7 +152,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { ..timer::timer() }, multiline::multiline(), - remind::remind(), + remind::command(), poise::Command { subcommands: vec![ poise::Command { @@ -197,12 +197,14 @@ async fn _main(tx: Sender<()>) -> Result<(), Box> { sqlx::migrate!().run(&database).await?; let popular_timezones = sqlx::query!( - "SELECT IFNULL(timezone, 'UTC') AS timezone + " + SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE timezone IS NOT NULL GROUP BY timezone ORDER BY COUNT(timezone) DESC - LIMIT 21" + LIMIT 21 + " ) .fetch_all(&database) .await diff --git a/src/models/command_macro.rs b/src/models/command_macro.rs index 5f82fff..d385f3b 100644 --- a/src/models/command_macro.rs +++ b/src/models/command_macro.rs @@ -1,84 +1,27 @@ -use chrono_tz::Tz; -use poise::serenity_prelude::{model::id::GuildId, ResolvedValue}; +use poise::serenity_prelude::model::id::GuildId; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::{ - commands::remind::RemindOptions, models::reminder::create_reminder, ApplicationContext, - Context, Error, -}; +use crate::{commands::remind, utils::Extract, ApplicationContext, Context, Error}; #[derive(Serialize, Deserialize)] #[serde(tag = "command_name")] pub enum RecordedCommand { - Remind(RemindOptions), -} - -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 { - 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 { - ResolvedValue::String(s) => s.to_string(), - _ => String::new(), - }) - }; - ($ctx:ident, $name:literal, bool) => { - $ctx.args.iter().find(|opt| opt.name == $name).map(|opt| &opt.value).map(|v| match v { - 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 { - ResolvedValue::String(s) => s.parse::().ok(), - _ => None, - }) - .flatten() - }; + Remind(remind::Options), } impl RecordedCommand { pub fn from_context(ctx: ApplicationContext) -> Option { match ctx.command().identifying_name.as_str() { - "remind" => Some(Self::Remind(RemindOptions { - 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", bool), - timezone: extract_arg!(ctx, "timezone", Option), - })), + "remind" => Some(Self::Remind(remind::Options::extract(ctx))), _ => None, } } pub async fn execute(self, ctx: ApplicationContext<'_>) -> Result<(), Error> { match self { - RecordedCommand::Remind(command_options) => { - create_reminder( - Context::Application(ctx), - command_options.time, - command_options.content, - command_options.channels, - command_options.interval, - command_options.expires, - command_options.tts, - command_options.timezone, - ) - .await + RecordedCommand::Remind(options) => { + remind::remind(Context::Application(ctx), options).await } } } diff --git a/src/models/reminder/builder.rs b/src/models/reminder/builder.rs index 4baef20..218fa8d 100644 --- a/src/models/reminder/builder.rs +++ b/src/models/reminder/builder.rs @@ -69,7 +69,9 @@ pub struct ReminderBuilder { impl ReminderBuilder { pub async fn build(self) -> Result { let queried_time = sqlx::query!( - "SELECT DATE_ADD(?, INTERVAL (SELECT nudge FROM channels WHERE id = ?) SECOND) AS `utc_time`", + " + SELECT DATE_ADD(?, INTERVAL (SELECT nudge FROM channels WHERE id = ?) SECOND) AS `utc_time` + ", self.utc_time, self.channel, ) @@ -84,36 +86,24 @@ impl ReminderBuilder { } else { sqlx::query!( " -INSERT INTO reminders ( - `uid`, - `channel_id`, - `utc_time`, - `timezone`, - `interval_seconds`, - `interval_days`, - `interval_months`, - `expires`, - `content`, - `tts`, - `attachment_name`, - `attachment`, - `set_by` -) VALUES ( - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ?, - ? -) - ", + INSERT INTO reminders ( + `uid`, + `channel_id`, + `utc_time`, + `timezone`, + `interval_seconds`, + `interval_days`, + `interval_months`, + `expires`, + `content`, + `tts`, + `attachment_name`, + `attachment`, + `set_by` + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + ", self.uid, self.channel, utc_time, diff --git a/src/utils.rs b/src/utils.rs index f2ff8a0..86f5575 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use poise::{ use crate::{ consts::{CNC_GUILD, SUBSCRIPTION_ROLES}, - Context, + ApplicationContext, Context, }; pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into) -> bool { @@ -64,3 +64,7 @@ pub fn footer(ctx: Context<'_>) -> CreateEmbedFooter { shard_count, )) } + +pub trait Extract { + fn extract(ctx: ApplicationContext) -> Self; +}