From bae0433bd9154cb7156cfb2df6bc652a07e3fb5a Mon Sep 17 00:00:00 2001 From: jellywx Date: Sun, 12 Sep 2021 16:09:57 +0100 Subject: [PATCH] framework now supports subcommands. timer cmd working --- regex_command_attr/src/consts.rs | 1 + regex_command_attr/src/lib.rs | 104 +++++++++++++++-- regex_command_attr/src/structures.rs | 22 +++- src/commands/reminder_cmds.rs | 166 ++++++++++++++++----------- src/framework.rs | 127 ++++++++++---------- src/main.rs | 2 +- 6 files changed, 281 insertions(+), 141 deletions(-) diff --git a/regex_command_attr/src/consts.rs b/regex_command_attr/src/consts.rs index 9235297..f3e2533 100644 --- a/regex_command_attr/src/consts.rs +++ b/regex_command_attr/src/consts.rs @@ -1,6 +1,7 @@ pub mod suffixes { pub const COMMAND: &str = "COMMAND"; pub const ARG: &str = "ARG"; + pub const SUBCOMMAND: &str = "SUBCOMMAND"; } pub use self::suffixes::*; diff --git a/regex_command_attr/src/lib.rs b/regex_command_attr/src/lib.rs index be7bdab..26133c0 100644 --- a/regex_command_attr/src/lib.rs +++ b/regex_command_attr/src/lib.rs @@ -53,13 +53,28 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let name = &name[..]; match name { - "arg" => options.cmd_args.push(propagate_err!(attributes::parse(values))), + "subcommand" => { + options + .subcommands + .push(Subcommand::new(propagate_err!(attributes::parse(values)))); + } + "arg" => { + if let Some(subcommand) = options.subcommands.last_mut() { + subcommand.cmd_args.push(propagate_err!(attributes::parse(values))); + } else { + options.cmd_args.push(propagate_err!(attributes::parse(values))); + } + } "example" => { options.examples.push(propagate_err!(attributes::parse(values))); } "description" => { let line: String = propagate_err!(attributes::parse(values)); - util::append_line(&mut options.description, line); + if let Some(subcommand) = options.subcommands.last_mut() { + util::append_line(&mut subcommand.description, line); + } else { + util::append_line(&mut options.description, line); + } } _ => { match_options!(name, values, options, span => [ @@ -82,6 +97,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { can_blacklist, supports_dm, mut cmd_args, + mut subcommands, } = options; let visibility = fun.visibility; @@ -94,32 +110,97 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let command_path = quote!(crate::framework::Command); let arg_path = quote!(crate::framework::Arg); + let subcommand_path = ApplicationCommandOptionType::SubCommand; populate_fut_lifetimes_on_refs(&mut fun.args); let args = fun.args; - let arg_idents = cmd_args + let mut subcommand_idents = subcommands .iter() - .map(|arg| { - n.with_suffix(arg.name.replace(" ", "_").replace("-", "_").as_str()).with_suffix(ARG) + .map(|subcommand| { + n.with_suffix(subcommand.name.replace("-", "_").as_str()).with_suffix(SUBCOMMAND) }) .collect::>(); - let mut tokens = cmd_args + let mut tokens = subcommands .iter_mut() - .map(|arg| { - let Arg { name, description, kind, required } = arg; + .zip(subcommand_idents.iter()) + .map(|(subcommand, sc_ident)| { + let arg_idents = subcommand + .cmd_args + .iter() + .map(|arg| { + n.with_suffix(subcommand.name.as_str()) + .with_suffix(arg.name.as_str()) + .with_suffix(ARG) + }) + .collect::>(); - let an = n.with_suffix(name.as_str()).with_suffix(ARG); + let mut tokens = subcommand + .cmd_args + .iter_mut() + .zip(arg_idents.iter()) + .map(|(arg, ident)| { + let Arg { name, description, kind, required } = arg; + + quote! { + #(#cooked)* + #[allow(missing_docs)] + pub static #ident: #arg_path = #arg_path { + name: #name, + description: #description, + kind: #kind, + required: #required, + options: &[] + }; + } + }) + .fold(quote! {}, |mut a, b| { + a.extend(b); + a + }); + + let Subcommand { name, description, .. } = subcommand; + + tokens.extend(quote! { + #(#cooked)* + #[allow(missing_docs)] + pub static #sc_ident: #arg_path = #arg_path { + name: #name, + description: #description, + kind: #subcommand_path, + required: false, + options: &[#(&#arg_idents),*], + }; + }); + + tokens + }) + .fold(quote! {}, |mut a, b| { + a.extend(b); + a + }); + + let mut arg_idents = cmd_args + .iter() + .map(|arg| n.with_suffix(arg.name.replace("-", "_").as_str()).with_suffix(ARG)) + .collect::>(); + + let arg_tokens = cmd_args + .iter_mut() + .zip(arg_idents.iter()) + .map(|(arg, ident)| { + let Arg { name, description, kind, required } = arg; quote! { #(#cooked)* #[allow(missing_docs)] - pub static #an: #arg_path = #arg_path { + pub static #ident: #arg_path = #arg_path { name: #name, description: #description, kind: #kind, required: #required, + options: &[], }; } }) @@ -128,6 +209,9 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { a }); + tokens.extend(arg_tokens); + arg_idents.append(&mut subcommand_idents); + let variant = if args.len() == 2 { quote!(crate::framework::CommandFnType::Multi) } else { diff --git a/regex_command_attr/src/structures.rs b/regex_command_attr/src/structures.rs index a983eb4..dc781ee 100644 --- a/regex_command_attr/src/structures.rs +++ b/regex_command_attr/src/structures.rs @@ -247,6 +247,25 @@ impl Default for Arg { } } +#[derive(Debug)] +pub(crate) struct Subcommand { + pub name: String, + pub description: String, + pub cmd_args: Vec, +} + +impl Default for Subcommand { + fn default() -> Self { + Self { name: String::new(), description: String::new(), cmd_args: vec![] } + } +} + +impl Subcommand { + pub(crate) fn new(name: String) -> Self { + Self { name, ..Default::default() } + } +} + #[derive(Debug, Default)] pub(crate) struct Options { pub aliases: Vec, @@ -257,11 +276,12 @@ pub(crate) struct Options { pub can_blacklist: bool, pub supports_dm: bool, pub cmd_args: Vec, + pub subcommands: Vec, } impl Options { #[inline] pub fn new() -> Self { - Self { group: "Other".to_string(), ..Default::default() } + Self { group: "None".to_string(), ..Default::default() } } } diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index 4bc7ee6..9927e4a 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -460,10 +460,24 @@ INSERT INTO events (event_name, bulk_count, guild_id, user_id) VALUES ('delete', } } } +*/ #[command("timer")] -#[permission_level(Managed)] -async fn timer(ctx: &Context, msg: &Message, args: String) { +#[description("Manage timers")] +#[subcommand("list")] +#[description("List the timers in this server or DM channel")] +#[subcommand("start")] +#[description("Start a new timer from now")] +#[arg(name = "name", description = "Name for the new timer", kind = "String", required = true)] +#[subcommand("delete")] +#[description("Delete a timer")] +#[arg(name = "name", description = "Name of the timer to delete", kind = "String", required = true)] +#[required_permissions(Managed)] +async fn timer( + ctx: &Context, + invoke: &(dyn CommandInvoke + Send + Sync), + args: HashMap, +) { fn time_difference(start_time: NaiveDateTime) -> String { let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; let now = NaiveDateTime::from_timestamp(unix_time, 0); @@ -477,106 +491,120 @@ async fn timer(ctx: &Context, msg: &Message, args: String) { format!("{} days, {:02}:{:02}:{:02}", days, hours, minutes, seconds) } - let (pool, lm) = get_ctx_data(&ctx).await; + let pool = ctx.data.read().await.get::().cloned().unwrap(); - let language = UserData::language_of(&msg.author, &pool).await; - - let mut args_iter = args.splitn(2, ' '); - - let owner = msg - .guild_id - .map(|g| g.as_u64().to_owned()) - .unwrap_or_else(|| msg.author.id.as_u64().to_owned()); - - match args_iter.next() { - Some("list") => { - let timers = Timer::from_owner(owner, &pool).await; - - let _ = msg - .channel_id - .send_message(&ctx, |m| { - m.embed(|e| { - e.fields(timers.iter().map(|timer| { - ( - &timer.name, - format!("⏳ `{}`", time_difference(timer.start_time)), - false, - ) - })) - }) - }) - .await; - } + let owner = invoke.guild_id().map(|g| g.0).unwrap_or_else(|| invoke.author_id().0); + match args.get("").map(|s| s.as_str()) { Some("start") => { let count = Timer::count_from_owner(owner, &pool).await; if count >= 25 { - let _ = msg.channel_id.say(&ctx, lm.get(&language, "timer/limit")).await; + let _ = invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new() + .content("You already have 25 timers. Please delete some timers before creating a new one"), + ) + .await; } else { - let name = args_iter - .next() - .map(|s| s.to_string()) - .unwrap_or(format!("New timer #{}", count + 1)); + let name = args.get("name").unwrap(); if name.len() <= 32 { Timer::create(&name, owner, &pool).await; - let _ = msg.channel_id.say(&ctx, lm.get(&language, "timer/success")).await; + let _ = invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content("Created a new timer"), + ) + .await; } else { - let _ = msg - .channel_id - .say( - &ctx, - lm.get(&language, "timer/name_length") - .replace("{}", &name.len().to_string()), + let _ = invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new() + .content(format!("Please name your timer something shorted (max. 32 characters, you used {})", name.len())), ) .await; } } } - Some("delete") => { - if let Some(name) = args_iter.next() { - let exists = sqlx::query!( - " + let name = args.get("name").unwrap(); + + let exists = sqlx::query!( + " SELECT 1 as _r FROM timers WHERE owner = ? AND name = ? ", + owner, + name + ) + .fetch_one(&pool) + .await; + + if exists.is_ok() { + sqlx::query!( + " +DELETE FROM timers WHERE owner = ? AND name = ? + ", owner, name ) - .fetch_one(&pool) - .await; + .execute(&pool) + .await + .unwrap(); - if exists.is_ok() { - sqlx::query!( - " -DELETE FROM timers WHERE owner = ? AND name = ? - ", - owner, - name + let _ = invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content("Deleted a timer"), ) - .execute(&pool) - .await - .unwrap(); - - let _ = msg.channel_id.say(&ctx, lm.get(&language, "timer/deleted")).await; - } else { - let _ = msg.channel_id.say(&ctx, lm.get(&language, "timer/not_found")).await; - } + .await; } else { - let _ = msg.channel_id.say(&ctx, lm.get(&language, "timer/help")).await; + let _ = invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content("Could not find a timer by that name"), + ) + .await; } } + Some("list") => { + let timers = Timer::from_owner(owner, &pool).await; - _ => { - let prefix = ctx.prefix(msg.guild_id).await; - - command_help(ctx, msg, lm, &prefix, &language, "timer").await; + if timers.len() > 0 { + let _ = invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().embed(|e| { + e.fields(timers.iter().map(|timer| { + ( + &timer.name, + format!("⌚ `{}`", time_difference(timer.start_time)), + false, + ) + })) + .color(*THEME_COLOR) + }), + ) + .await; + } else { + let _ = invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().content( + "No timers currently. Use `/timer start` to create a new timer", + ), + ) + .await; + } } + _ => {} } } +/* #[derive(PartialEq)] enum RemindCommand { Remind, diff --git a/src/framework.rs b/src/framework.rs index cd38330..c0fc710 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -8,7 +8,7 @@ use log::{error, info, warn}; use regex::{Match, Regex, RegexBuilder}; use serenity::{ async_trait, - builder::{CreateComponents, CreateEmbed}, + builder::{CreateApplicationCommands, CreateComponents, CreateEmbed}, cache::Cache, client::Context, framework::Framework, @@ -278,6 +278,7 @@ pub struct Arg { pub description: &'static str, pub kind: ApplicationCommandOptionType, pub required: bool, + pub options: &'static [&'static Self], } type SlashCommandFn = for<'fut> fn( @@ -561,59 +562,56 @@ impl RegexFramework { self } + fn _populate_commands<'a>( + &self, + commands: &'a mut CreateApplicationCommands, + ) -> &'a mut CreateApplicationCommands { + for command in &self.commands { + if command.fun.is_slash() { + commands.create_application_command(|c| { + c.name(command.names[0]).description(command.desc); + + for arg in command.args { + c.create_option(|o| { + o.name(arg.name) + .description(arg.description) + .kind(arg.kind) + .required(arg.required); + + for option in arg.options { + o.create_sub_option(|s| { + s.name(option.name) + .description(option.description) + .kind(option.kind) + .required(option.required) + }); + } + + o + }); + } + + c + }); + } + } + + commands + } + pub async fn build_slash(&self, http: impl AsRef) { info!("Building slash commands..."); match self.debug_guild { None => { - ApplicationCommand::set_global_application_commands(&http, |commands| { - for command in &self.commands { - if command.fun.is_slash() { - commands.create_application_command(|c| { - c.name(command.names[0]).description(command.desc); - - for arg in command.args { - c.create_option(|o| { - o.name(arg.name) - .description(arg.description) - .kind(arg.kind) - .required(arg.required) - }); - } - - c - }); - } - } - - commands + ApplicationCommand::set_global_application_commands(&http, |c| { + self._populate_commands(c) }) .await; } Some(debug_guild) => { debug_guild - .set_application_commands(&http, |commands| { - for command in &self.commands { - if command.fun.is_slash() { - commands.create_application_command(|c| { - c.name(command.names[0]).description(command.desc); - - for arg in command.args { - c.create_option(|o| { - o.name(arg.name) - .description(arg.description) - .kind(arg.kind) - .required(arg.required) - }); - } - - c - }); - } - } - - commands - }) + .set_application_commands(&http, |c| self._populate_commands(c)) .await .unwrap(); } @@ -635,22 +633,31 @@ impl RegexFramework { if command.check_permissions(&ctx, &guild, &member).await { let mut args = HashMap::new(); - for arg in interaction.data.options.iter().filter(|o| o.value.is_some()) { - args.insert( - arg.name.clone(), - match arg.value.clone().unwrap() { - Value::Bool(b) => { - if b { - arg.name.clone() - } else { - String::new() - } - } - Value::Number(n) => n.to_string(), - Value::String(s) => s, - _ => String::new(), - }, - ); + for arg in interaction.data.options.iter() { + if let Some(value) = &arg.value { + args.insert( + arg.name.clone(), + match value { + Value::Bool(b) => b.to_string(), + Value::Number(n) => n.to_string(), + Value::String(s) => s.to_owned(), + _ => String::new(), + }, + ); + } else { + args.insert("".to_string(), arg.name.clone()); + for sub_arg in arg.options.iter().filter(|o| o.value.is_some()) { + args.insert( + sub_arg.name.clone(), + match sub_arg.value.as_ref().unwrap() { + Value::Bool(b) => b.to_string(), + Value::Number(n) => n.to_string(), + Value::String(s) => s.to_owned(), + _ => String::new(), + }, + ); + } + } } if !ctx.check_executing(interaction.author_id()).await { diff --git a/src/main.rs b/src/main.rs index 62c27cb..a9fe418 100644 --- a/src/main.rs +++ b/src/main.rs @@ -295,8 +295,8 @@ async fn main() -> Result<(), Box> { .add_command(&info_cmds::DASHBOARD_COMMAND) .add_command(&info_cmds::CLOCK_COMMAND) // reminder commands + .add_command(&reminder_cmds::TIMER_COMMAND) /* - .add_command("timer", &reminder_cmds::TIMER_COMMAND) .add_command("remind", &reminder_cmds::REMIND_COMMAND) .add_command("r", &reminder_cmds::REMIND_COMMAND) .add_command("interval", &reminder_cmds::INTERVAL_COMMAND)