Extract trait

This commit is contained in:
jude 2024-02-17 20:24:30 +00:00
parent 4823754955
commit c1305cfb36
14 changed files with 336 additions and 176 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
/target
target
.env
/venv
.cargo

View File

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

View File

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

View File

@ -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<String>,
) -> Result<(), 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;
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<String>,
) -> Result<(), Error> {
pause(ctx, Options { until }).await
}

View File

@ -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<String>,
pub interval: Option<String>,
pub expires: Option<String>,
pub tts: Option<bool>,
pub timezone: Option<Tz>,
#[derive(Serialize, Deserialize)]
pub struct Options {
time: String,
content: String,
channels: Option<String>,
interval: Option<String>,
expires: Option<String>,
tts: Option<bool>,
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();
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<String>,
) -> Result<(), Error> {
let tz = timezone.map(|t| t.parse::<Tz>().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
}

View File

@ -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<String>,
) -> Result<(), Error> {
#[derive(Serialize, Deserialize)]
pub struct Options {
pub timezone: Option<String>,
}
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::<Tz>() {
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<String>,
) -> Result<(), Error> {
timezone_fn(ctx, Options { timezone }).await
}

View File

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

46
src/extract_macro/Cargo.lock generated Normal file
View File

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

View File

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

View File

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

@ -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<dyn StdError + Send + Sync>> {
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<dyn StdError + Send + Sync>> {
}],
..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<dyn StdError + Send + Sync>> {
],
..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<dyn StdError + Send + Sync>> {
..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<dyn StdError + Send + Sync>> {
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

View File

@ -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<String>) => {
$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<Tz>) => {
$ctx.args
.iter()
.find(|opt| opt.name == $name)
.map(|opt| &opt.value)
.map(|v| match v {
ResolvedValue::String(s) => s.parse::<Tz>().ok(),
_ => None,
})
.flatten()
};
Remind(remind::Options),
}
impl RecordedCommand {
pub fn from_context(ctx: ApplicationContext) -> Option<Self> {
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<String>),
interval: extract_arg!(ctx, "interval", Option<String>),
expires: extract_arg!(ctx, "expires", Option<String>),
tts: extract_arg!(ctx, "tts", bool),
timezone: extract_arg!(ctx, "timezone", Option<Tz>),
})),
"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
}
}
}

View File

@ -69,7 +69,9 @@ pub struct ReminderBuilder {
impl ReminderBuilder {
pub async fn build(self) -> Result<Reminder, ReminderError> {
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,

View File

@ -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<UserId>) -> bool {
@ -64,3 +64,7 @@ pub fn footer(ctx: Context<'_>) -> CreateEmbedFooter {
shard_count,
))
}
pub trait Extract {
fn extract(ctx: ApplicationContext) -> Self;
}