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"
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",

View File

@ -1,5 +1,5 @@
[package]
name = "reminder-rs"
name = "reminder_rs"
version = "1.6.50"
authors = ["Jude Southworth <judesouthworth@pm.me>"]
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"

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 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,14 +1,19 @@
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(
ctx.send(
CreateReply::default().ephemeral(true).embed(
CreateEmbed::new()
.title("Info")
@ -27,7 +32,13 @@ Use our dashboard: https://reminder-bot.com/",
.color(*THEME_COLOR),
),
)
.await;
.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
}

View File

@ -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<ChannelId>,
@ -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<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> {
#[derive(Serialize, Deserialize, Extract)]
pub struct Options {
channel: Option<PartialChannel>,
disabled: Option<bool>,
relative: Option<bool>,
}
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<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 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<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.
#[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<String>,
) -> Result<(), Error> {
let tz = timezone.map(|t| t.parse::<Tz>().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
}

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 {
minutes: Option<isize>,
seconds: Option<isize>,
minutes: Option<i64>,
seconds: Option<i64>,
}
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<isize>,
#[description = "Number of seconds to nudge new reminders by"] seconds: 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<i64>,
) -> Result<(), Error> {
nudge(ctx, Options { minutes, seconds }).await
}

View File

@ -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<isize>,
minutes: Option<isize>,
seconds: Option<isize>,
hours: Option<i64>,
minutes: Option<i64>,
seconds: Option<i64>,
}
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<isize>,
#[description = "Number of minutes to offset by"] minutes: Option<isize>,
#[description = "Number of seconds to offset by"] seconds: Option<isize>,
#[description = "Number of hours to offset by"] hours: Option<i64>,
#[description = "Number of minutes to offset by"] minutes: Option<i64>,
#[description = "Number of seconds to offset by"] seconds: Option<i64>,
) -> Result<(), Error> {
offset(ctx, Options { hours, minutes, seconds }).await
}

View File

@ -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<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> {
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<String>,

View File

@ -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<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> {
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.
#[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"]

View File

@ -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<String>,
}
@ -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"]

View File

@ -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
}

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::{
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<dyn StdError + Send + Sync>> {
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<dyn StdError + Send + Sync>> {
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<dyn StdError + Send + Sync>> {
],
..timer::timer()
},
multiline::multiline(),
multiline::command(),
remind::command(),
poise::Command {
subcommands: vec![

View File

@ -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<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;