Add option types for top-level commands
This commit is contained in:
parent
c1305cfb36
commit
5e39e16060
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
52
extract_macro/src/lib.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
22
src/main.rs
22
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<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![
|
||||
|
87
src/utils.rs
87
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<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;
|
||||
|
Loading…
Reference in New Issue
Block a user