diff --git a/command_attributes/src/consts.rs b/command_attributes/src/consts.rs index 1ed969f..8c334b4 100644 --- a/command_attributes/src/consts.rs +++ b/command_attributes/src/consts.rs @@ -2,6 +2,7 @@ pub mod suffixes { pub const COMMAND: &str = "COMMAND"; pub const ARG: &str = "ARG"; pub const SUBCOMMAND: &str = "SUBCOMMAND"; + pub const SUBCOMMAND_GROUP: &str = "GROUP"; pub const CHECK: &str = "CHECK"; pub const HOOK: &str = "HOOK"; } diff --git a/command_attributes/src/lib.rs b/command_attributes/src/lib.rs index 2ccec4d..1bd1ad0 100644 --- a/command_attributes/src/lib.rs +++ b/command_attributes/src/lib.rs @@ -36,6 +36,13 @@ macro_rules! match_options { #[proc_macro_attribute] pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { + enum LastItem { + Fun, + SubFun, + SubGroup, + SubGroupFun, + } + let mut fun = parse_macro_input!(input as CommandFun); let _name = if !attr.is_empty() { @@ -46,6 +53,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let mut hooks: Vec = Vec::new(); let mut options = Options::new(); + let mut last_desc = LastItem::Fun; for attribute in &fun.attributes { let span = attribute.span(); @@ -56,15 +64,39 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { match name { "subcommand" => { - options - .subcommands - .push(Subcommand::new(propagate_err!(attributes::parse(values)))); + let new_subcommand = Subcommand::new(propagate_err!(attributes::parse(values))); + + if let Some(subcommand_group) = options.subcommand_groups.last_mut() { + last_desc = LastItem::SubGroupFun; + subcommand_group.subcommands.push(new_subcommand); + } else { + last_desc = LastItem::SubFun; + options.subcommands.push(new_subcommand); + } + } + "subcommandgroup" => { + let new_group = SubcommandGroup::new(propagate_err!(attributes::parse(values))); + last_desc = LastItem::SubGroup; + + options.subcommand_groups.push(new_group); } "arg" => { - if let Some(subcommand) = options.subcommands.last_mut() { - subcommand.cmd_args.push(propagate_err!(attributes::parse(values))); + if let Some(subcommand_group) = options.subcommand_groups.last_mut() { + if let Some(subcommand) = subcommand_group.subcommands.last_mut() { + subcommand.cmd_args.push(propagate_err!(attributes::parse(values))); + } else { + 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))); + } + } } else { - options.cmd_args.push(propagate_err!(attributes::parse(values))); + 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" => { @@ -72,10 +104,36 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { } "description" => { let line: String = propagate_err!(attributes::parse(values)); - if let Some(subcommand) = options.subcommands.last_mut() { - util::append_line(&mut subcommand.description, line); - } else { - util::append_line(&mut options.description, line); + + match last_desc { + LastItem::Fun => { + util::append_line(&mut options.description, line); + } + LastItem::SubFun => { + util::append_line( + &mut options.subcommands.last_mut().unwrap().description, + line, + ); + } + LastItem::SubGroup => { + util::append_line( + &mut options.subcommand_groups.last_mut().unwrap().description, + line, + ); + } + LastItem::SubGroupFun => { + util::append_line( + &mut options + .subcommand_groups + .last_mut() + .unwrap() + .subcommands + .last_mut() + .unwrap() + .description, + line, + ); + } } } "hook" => { @@ -101,120 +159,81 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { supports_dm, mut cmd_args, mut subcommands, + mut subcommand_groups, } = options; let visibility = fun.visibility; let name = fun.name.clone(); let body = fun.body; - let n = name.with_suffix(COMMAND); - - let cooked = fun.cooked.clone(); + let root_ident = name.with_suffix(COMMAND); 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 mut subcommand_group_idents = subcommand_groups + .iter() + .map(|subcommand| { + root_ident + .with_suffix(subcommand.name.replace("-", "_").as_str()) + .with_suffix(SUBCOMMAND_GROUP) + }) + .collect::>(); let mut subcommand_idents = subcommands .iter() .map(|subcommand| { - n.with_suffix(subcommand.name.replace("-", "_").as_str()).with_suffix(SUBCOMMAND) + root_ident + .with_suffix(subcommand.name.replace("-", "_").as_str()) + .with_suffix(SUBCOMMAND) }) .collect::>(); - let mut tokens = subcommands - .iter_mut() - .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 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)) + .map(|arg| root_ident.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; + let mut tokens = quote! {}; - 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 - }); + tokens.extend( + subcommand_groups + .iter_mut() + .zip(subcommand_group_idents.iter()) + .map(|(group, group_ident)| group.as_tokens(group_ident)) + .fold(quote! {}, |mut a, b| { + a.extend(b); + a + }), + ); - tokens.extend(arg_tokens); + tokens.extend( + subcommands + .iter_mut() + .zip(subcommand_idents.iter()) + .map(|(subcommand, sc_ident)| subcommand.as_tokens(sc_ident)) + .fold(quote! {}, |mut a, b| { + a.extend(b); + a + }), + ); + + tokens.extend( + cmd_args.iter_mut().zip(arg_idents.iter()).map(|(arg, ident)| arg.as_tokens(ident)).fold( + quote! {}, + |mut a, b| { + a.extend(b); + a + }, + ), + ); + + arg_idents.append(&mut subcommand_group_idents); arg_idents.append(&mut subcommand_idents); + let args = fun.args; + let variant = if args.len() == 2 { quote!(crate::framework::CommandFnType::Multi) } else { @@ -230,9 +249,8 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { }; tokens.extend(quote! { - #(#cooked)* #[allow(missing_docs)] - pub static #n: #command_path = #command_path { + pub static #root_ident: #command_path = #command_path { fun: #variant(#name), names: &[#_name, #(#aliases),*], desc: #description, @@ -243,10 +261,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { args: &[#(&#arg_idents),*], hooks: &[#(&#hooks),*], }; - }); - tokens.extend(quote! { - #(#cooked)* #[allow(missing_docs)] #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> { use ::serenity::futures::future::FutureExt; @@ -269,7 +284,6 @@ pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream { let fn_name = n.with_suffix(CHECK); let visibility = fun.visibility; - let cooked = fun.cooked; let body = fun.body; let ret = fun.ret; populate_fut_lifetimes_on_refs(&mut fun.args); @@ -279,7 +293,6 @@ pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream { let uuid = Uuid::new_v4().as_u128(); (quote! { - #(#cooked)* #[allow(missing_docs)] #visibility fn #fn_name<'fut>(#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> { use ::serenity::futures::future::FutureExt; @@ -291,7 +304,6 @@ pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream { }.boxed() } - #(#cooked)* #[allow(missing_docs)] pub static #name: #hook_path = #hook_path { fun: #fn_name, diff --git a/command_attributes/src/structures.rs b/command_attributes/src/structures.rs index 1985a3a..f77ce46 100644 --- a/command_attributes/src/structures.rs +++ b/command_attributes/src/structures.rs @@ -7,7 +7,10 @@ use syn::{ Attribute, Block, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility, }; -use crate::util::{Argument, Parenthesised}; +use crate::{ + consts::{ARG, SUBCOMMAND}, + util::{Argument, IdentExt2, Parenthesised}, +}; fn parse_argument(arg: FnArg) -> Result { match arg { @@ -38,43 +41,12 @@ fn parse_argument(arg: FnArg) -> Result { } } -/// Test if the attribute is cooked. -fn is_cooked(attr: &Attribute) -> bool { - const COOKED_ATTRIBUTE_NAMES: &[&str] = - &["cfg", "cfg_attr", "derive", "inline", "allow", "warn", "deny", "forbid"]; - - COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n)) -} - -/// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector. -/// -/// # Return -/// -/// Returns a vector of cooked attributes that have been removed from the input vector. -fn remove_cooked(attrs: &mut Vec) -> Vec { - let mut cooked = Vec::new(); - - // FIXME: Replace with `Vec::drain_filter` once it is stable. - let mut i = 0; - while i < attrs.len() { - if !is_cooked(&attrs[i]) { - i += 1; - continue; - } - - cooked.push(attrs.remove(i)); - } - - cooked -} - #[derive(Debug)] pub struct CommandFun { /// `#[...]`-style attributes. pub attributes: Vec, /// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros /// and will appear in generated output. - pub cooked: Vec, pub visibility: Visibility, pub name: Ident, pub args: Vec, @@ -84,9 +56,7 @@ pub struct CommandFun { impl Parse for CommandFun { fn parse(input: ParseStream<'_>) -> Result { - let mut attributes = input.call(Attribute::parse_outer)?; - - let cooked = remove_cooked(&mut attributes); + let attributes = input.call(Attribute::parse_outer)?; let visibility = input.parse::()?; @@ -110,16 +80,15 @@ impl Parse for CommandFun { let args = args.into_iter().map(parse_argument).collect::>>()?; - Ok(Self { attributes, cooked, visibility, name, args, ret, body }) + Ok(Self { attributes, visibility, name, args, ret, body }) } } impl ToTokens for CommandFun { fn to_tokens(&self, stream: &mut TokenStream2) { - let Self { attributes: _, cooked, visibility, name, args, ret, body } = self; + let Self { attributes: _, visibility, name, args, ret, body } = self; stream.extend(quote! { - #(#cooked)* #visibility async fn #name (#(#args),*) -> #ret { #(#body)* } @@ -193,6 +162,24 @@ pub(crate) struct Arg { pub required: bool, } +impl Arg { + pub fn as_tokens(&self, ident: &Ident) -> TokenStream2 { + let arg_path = quote!(crate::framework::Arg); + let Arg { name, description, kind, required } = self; + + quote! { + #[allow(missing_docs)] + pub static #ident: #arg_path = #arg_path { + name: #name, + description: #description, + kind: #kind, + required: #required, + options: &[] + }; + } + } +} + impl Default for Arg { fn default() -> Self { Self { @@ -211,6 +198,44 @@ pub(crate) struct Subcommand { pub cmd_args: Vec, } +impl Subcommand { + pub fn as_tokens(&mut self, ident: &Ident) -> TokenStream2 { + let arg_path = quote!(crate::framework::Arg); + let subcommand_path = ApplicationCommandOptionType::SubCommand; + + let arg_idents = self + .cmd_args + .iter() + .map(|arg| ident.with_suffix(arg.name.as_str()).with_suffix(ARG)) + .collect::>(); + + let mut tokens = self + .cmd_args + .iter_mut() + .zip(arg_idents.iter()) + .map(|(arg, ident)| arg.as_tokens(ident)) + .fold(quote! {}, |mut a, b| { + a.extend(b); + a + }); + + let Subcommand { name, description, .. } = self; + + tokens.extend(quote! { + #[allow(missing_docs)] + pub static #ident: #arg_path = #arg_path { + name: #name, + description: #description, + kind: #subcommand_path, + required: false, + options: &[#(&#arg_idents),*], + }; + }); + + tokens + } +} + impl Default for Subcommand { fn default() -> Self { Self { name: String::new(), description: String::new(), cmd_args: vec![] } @@ -223,6 +248,68 @@ impl Subcommand { } } +#[derive(Debug)] +pub(crate) struct SubcommandGroup { + pub name: String, + pub description: String, + pub subcommands: Vec, +} + +impl SubcommandGroup { + pub fn as_tokens(&mut self, ident: &Ident) -> TokenStream2 { + let arg_path = quote!(crate::framework::Arg); + let subcommand_group_path = ApplicationCommandOptionType::SubCommandGroup; + + let arg_idents = self + .subcommands + .iter() + .map(|arg| { + ident + .with_suffix(self.name.as_str()) + .with_suffix(arg.name.as_str()) + .with_suffix(SUBCOMMAND) + }) + .collect::>(); + + let mut tokens = self + .subcommands + .iter_mut() + .zip(arg_idents.iter()) + .map(|(subcommand, ident)| subcommand.as_tokens(ident)) + .fold(quote! {}, |mut a, b| { + a.extend(b); + a + }); + + let SubcommandGroup { name, description, .. } = self; + + tokens.extend(quote! { + #[allow(missing_docs)] + pub static #ident: #arg_path = #arg_path { + name: #name, + description: #description, + kind: #subcommand_group_path, + required: false, + options: &[#(&#arg_idents),*], + }; + }); + + tokens + } +} + +impl Default for SubcommandGroup { + fn default() -> Self { + Self { name: String::new(), description: String::new(), subcommands: vec![] } + } +} + +impl SubcommandGroup { + pub(crate) fn new(name: String) -> Self { + Self { name, ..Default::default() } + } +} + #[derive(Debug, Default)] pub(crate) struct Options { pub aliases: Vec, @@ -233,6 +320,7 @@ pub(crate) struct Options { pub supports_dm: bool, pub cmd_args: Vec, pub subcommands: Vec, + pub subcommand_groups: Vec, } impl Options { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6bb21cf..e53c6f3 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ pub mod info_cmds; pub mod moderation_cmds; pub mod reminder_cmds; -//pub mod todo_cmds; +pub mod todo_cmds; diff --git a/src/commands/todo_cmds.rs b/src/commands/todo_cmds.rs index 56ec5cc..1732d66 100644 --- a/src/commands/todo_cmds.rs +++ b/src/commands/todo_cmds.rs @@ -1,443 +1,44 @@ -use std::{convert::TryFrom, fmt}; - use regex_command_attr::command; -use serenity::{ - async_trait, - client::Context, - constants::MESSAGE_CODE_LIMIT, - model::{ - channel::Message, - id::{ChannelId, GuildId, UserId}, - }, -}; -use sqlx::MySqlPool; +use serenity::client::Context; -use crate::{ - command_help, get_ctx_data, - models::{user_data::UserData, CtxData}, -}; +use crate::framework::{CommandInvoke, CommandOptions}; -#[derive(Debug)] -struct TodoNotFound; - -impl std::error::Error for TodoNotFound {} -impl fmt::Display for TodoNotFound { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Todo not found") - } -} - -struct Todo { - id: u32, - value: String, -} - -struct TodoTarget { - user: UserId, - guild: Option, - channel: Option, -} - -impl TodoTarget { - pub fn command(&self, subcommand_opt: Option) -> String { - let context = if self.channel.is_some() { - "channel" - } else if self.guild.is_some() { - "guild" - } else { - "user" - }; - - if let Some(subcommand) = subcommand_opt { - format!("todo {} {}", context, subcommand.to_string()) - } else { - format!("todo {}", context) - } - } - - pub fn name(&self) -> String { - if self.channel.is_some() { - "Channel" - } else if self.guild.is_some() { - "Guild" - } else { - "User" - } - .to_string() - } - - pub async fn view( - &self, - pool: MySqlPool, - ) -> Result, Box> { - Ok(if let Some(cid) = self.channel { - sqlx::query_as!( - Todo, - " -SELECT id, value FROM todos WHERE channel_id = (SELECT id FROM channels WHERE channel = ?) - ", - cid.as_u64() - ) - .fetch_all(&pool) - .await? - } else if let Some(gid) = self.guild { - sqlx::query_as!( - Todo, - " -SELECT id, value FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND channel_id IS NULL - ", - gid.as_u64() - ) - .fetch_all(&pool) - .await? - } else { - sqlx::query_as!( - Todo, - " -SELECT id, value FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guild_id IS NULL - ", - self.user.as_u64() - ) - .fetch_all(&pool) - .await? - }) - } - - pub async fn add( - &self, - value: String, - pool: MySqlPool, - ) -> Result<(), Box> { - if let (Some(cid), Some(gid)) = (self.channel, self.guild) { - sqlx::query!( - " -INSERT INTO todos (user_id, guild_id, channel_id, value) VALUES ( - (SELECT id FROM users WHERE user = ?), - (SELECT id FROM guilds WHERE guild = ?), - (SELECT id FROM channels WHERE channel = ?), - ? -) - ", - self.user.as_u64(), - gid.as_u64(), - cid.as_u64(), - value - ) - .execute(&pool) - .await?; - } else if let Some(gid) = self.guild { - sqlx::query!( - " -INSERT INTO todos (user_id, guild_id, value) VALUES ( - (SELECT id FROM users WHERE user = ?), - (SELECT id FROM guilds WHERE guild = ?), - ? -) - ", - self.user.as_u64(), - gid.as_u64(), - value - ) - .execute(&pool) - .await?; - } else { - sqlx::query!( - " -INSERT INTO todos (user_id, value) VALUES ( - (SELECT id FROM users WHERE user = ?), - ? -) - ", - self.user.as_u64(), - value - ) - .execute(&pool) - .await?; - } - - Ok(()) - } - - pub async fn remove( - &self, - num: usize, - pool: &MySqlPool, - ) -> Result> { - let todos = self.view(pool.clone()).await?; - - if let Some(removal_todo) = todos.get(num) { - let deleting = sqlx::query_as!( - Todo, - " -SELECT id, value FROM todos WHERE id = ? - ", - removal_todo.id - ) - .fetch_one(&pool.clone()) - .await?; - - sqlx::query!( - " -DELETE FROM todos WHERE id = ? - ", - removal_todo.id - ) - .execute(pool) - .await?; - - Ok(deleting) - } else { - Err(Box::new(TodoNotFound)) - } - } - - pub async fn clear( - &self, - pool: &MySqlPool, - ) -> Result<(), Box> { - if let Some(cid) = self.channel { - sqlx::query!( - " -DELETE FROM todos WHERE channel_id = (SELECT id FROM channels WHERE channel = ?) - ", - cid.as_u64() - ) - .execute(pool) - .await?; - } else if let Some(gid) = self.guild { - sqlx::query!( - " -DELETE FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND channel_id IS NULL - ", - gid.as_u64() - ) - .execute(pool) - .await?; - } else { - sqlx::query!( - " -DELETE FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guild_id IS NULL - ", - self.user.as_u64() - ) - .execute(pool) - .await?; - } - - Ok(()) - } - - async fn execute(&self, ctx: &Context, msg: &Message, subcommand: SubCommand, extra: String) { - let (pool, lm) = get_ctx_data(&ctx).await; - - let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); - let prefix = ctx.prefix(msg.guild_id).await; - - match subcommand { - SubCommand::View => { - let todo_items = self.view(pool).await.unwrap(); - let mut todo_groups = vec!["".to_string()]; - let mut char_count = 0; - - todo_items.iter().enumerate().for_each(|(count, todo)| { - let display = format!("{}: {}\n", count + 1, todo.value); - - if char_count + display.len() > MESSAGE_CODE_LIMIT as usize { - char_count = display.len(); - - todo_groups.push(display); - } else { - char_count += display.len(); - - let last_group = todo_groups.pop().unwrap(); - - todo_groups.push(format!("{}{}", last_group, display)); - } - }); - - for group in todo_groups { - let _ = msg - .channel_id - .send_message(&ctx, |m| { - m.embed(|e| e.title(format!("{} Todo", self.name())).description(group)) - }) - .await; - } - } - - SubCommand::Add => { - let content = lm - .get(&user_data.language, "todo/added") - .replacen("{name}", &extra, 1); - - self.add(extra, pool).await.unwrap(); - - let _ = msg.channel_id.say(&ctx, content).await; - } - - SubCommand::Remove => { - if let Ok(num) = extra.parse::() { - if let Ok(todo) = self.remove(num - 1, &pool).await { - let content = lm.get(&user_data.language, "todo/removed").replacen( - "{}", - &todo.value, - 1, - ); - - let _ = msg.channel_id.say(&ctx, content).await; - } else { - let _ = msg - .channel_id - .say(&ctx, lm.get(&user_data.language, "todo/error_index")) - .await; - } - } else { - let content = lm - .get(&user_data.language, "todo/error_value") - .replacen("{prefix}", &prefix, 1) - .replacen("{command}", &self.command(Some(subcommand)), 1); - - let _ = msg.channel_id.say(&ctx, content).await; - } - } - - SubCommand::Clear => { - self.clear(&pool).await.unwrap(); - - let content = lm.get(&user_data.language, "todo/cleared"); - - let _ = msg.channel_id.say(&ctx, content).await; - } - } - } -} - -enum SubCommand { - View, - Add, - Remove, - Clear, -} - -impl TryFrom> for SubCommand { - type Error = (); - - fn try_from(value: Option<&str>) -> Result { - match value { - Some("add") => Ok(SubCommand::Add), - - Some("remove") => Ok(SubCommand::Remove), - - Some("clear") => Ok(SubCommand::Clear), - - None | Some("") => Ok(SubCommand::View), - - Some(_unrecognised) => Err(()), - } - } -} - -impl ToString for SubCommand { - fn to_string(&self) -> String { - match self { - SubCommand::View => "", - SubCommand::Add => "add", - SubCommand::Remove => "remove", - SubCommand::Clear => "clear", - } - .to_string() - } -} - -#[async_trait] -trait Execute { - async fn execute(self, ctx: &Context, msg: &Message, extra: String, target: TodoTarget); -} - -#[async_trait] -impl Execute for Result { - async fn execute(self, ctx: &Context, msg: &Message, extra: String, target: TodoTarget) { - if let Ok(subcommand) = self { - target.execute(ctx, msg, subcommand, extra).await; - } else { - show_help(ctx, msg, Some(target)).await; - } - } -} - -#[command("todo")] -async fn todo_user(ctx: &Context, msg: &Message, args: String) { - let mut split = args.split(' '); - - let target = TodoTarget { - user: msg.author.id, - guild: None, - channel: None, - }; - - let subcommand_opt = SubCommand::try_from(split.next()); - - subcommand_opt - .execute(ctx, msg, split.collect::>().join(" "), target) - .await; -} - -#[command("todoc")] -#[supports_dm(false)] -#[permission_level(Managed)] -async fn todo_channel(ctx: &Context, msg: &Message, args: String) { - let mut split = args.split(' '); - - let target = TodoTarget { - user: msg.author.id, - guild: msg.guild_id, - channel: Some(msg.channel_id), - }; - - let subcommand_opt = SubCommand::try_from(split.next()); - - subcommand_opt - .execute(ctx, msg, split.collect::>().join(" "), target) - .await; -} - -#[command("todos")] -#[supports_dm(false)] -#[permission_level(Managed)] -async fn todo_guild(ctx: &Context, msg: &Message, args: String) { - let mut split = args.split(' '); - - let target = TodoTarget { - user: msg.author.id, - guild: msg.guild_id, - channel: None, - }; - - let subcommand_opt = SubCommand::try_from(split.next()); - - subcommand_opt - .execute(ctx, msg, split.collect::>().join(" "), target) - .await; -} - -async fn show_help(ctx: &Context, msg: &Message, target: Option) { - let (pool, lm) = get_ctx_data(&ctx).await; - - let language = UserData::language_of(&msg.author, &pool); - let prefix = ctx.prefix(msg.guild_id); - - let command = match target { - None => "todo", - Some(t) => { - if t.channel.is_some() { - "todoc" - } else if t.guild.is_some() { - "todos" - } else { - "todo" - } - } - }; - - command_help(ctx, msg, lm, &prefix.await, &language.await, command).await; -} +#[command] +#[description("Manage todo lists")] +#[subcommandgroup("server")] +#[description("Manage the server todo list")] +#[subcommand("add")] +#[description("Add an item to the server todo list")] +#[arg( + name = "task", + description = "The task to add to the todo list", + kind = "String", + required = true +)] +#[subcommand("view")] +#[description("View and remove from the server todo list")] +#[subcommandgroup("channel")] +#[description("Manage the channel todo list")] +#[subcommand("add")] +#[description("Add to the channel todo list")] +#[arg( + name = "task", + description = "The task to add to the todo list", + kind = "String", + required = true +)] +#[subcommand("view")] +#[description("View and remove from the channel todo list")] +#[subcommandgroup("user")] +#[description("Manage your personal todo list")] +#[subcommand("add")] +#[description("Add to your personal todo list")] +#[arg( + name = "task", + description = "The task to add to the todo list", + kind = "String", + required = true +)] +#[subcommand("view")] +#[description("View and remove from your personal todo list")] +async fn todo(ctx: &Context, invoke: CommandInvoke, args: CommandOptions) {} diff --git a/src/main.rs b/src/main.rs index c2bd692..d8a5234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ use sqlx::mysql::MySqlPool; use tokio::sync::RwLock; use crate::{ - commands::{info_cmds, moderation_cmds, reminder_cmds}, + commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds}, component_models::ComponentDataModel, consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR}, framework::RegexFramework, @@ -318,16 +318,8 @@ async fn main() -> Result<(), Box> { .add_command(&reminder_cmds::PAUSE_COMMAND) .add_command(&reminder_cmds::OFFSET_COMMAND) .add_command(&reminder_cmds::NUDGE_COMMAND) - /* // to-do commands - .add_command("todo", &todo_cmds::TODO_USER_COMMAND) - .add_command("todo user", &todo_cmds::TODO_USER_COMMAND) - .add_command("todoc", &todo_cmds::TODO_CHANNEL_COMMAND) - .add_command("todo channel", &todo_cmds::TODO_CHANNEL_COMMAND) - .add_command("todos", &todo_cmds::TODO_GUILD_COMMAND) - .add_command("todo server", &todo_cmds::TODO_GUILD_COMMAND) - .add_command("todo guild", &todo_cmds::TODO_GUILD_COMMAND) - */ + .add_command(&todo_cmds::TODO_COMMAND) // moderation commands .add_command(&moderation_cmds::BLACKLIST_COMMAND) .add_command(&moderation_cmds::RESTRICT_COMMAND)