From d5d2ac2beeffee7911a405ae56fce9b5f297d532 Mon Sep 17 00:00:00 2001 From: jellywx Date: Thu, 10 Jun 2021 21:16:16 +0100 Subject: [PATCH] updated the regex_command_attr to reflect upstream. changed add_command to not accept a name --- Cargo.lock | 2 +- regex_command_attr/Cargo.toml | 5 +- regex_command_attr/src/attributes.rs | 4 +- regex_command_attr/src/lib.rs | 270 +++++++++++++++++++++++++-- regex_command_attr/src/structures.rs | 142 ++++++++++++-- regex_command_attr/src/util.rs | 71 ++++--- src/framework.rs | 31 +-- src/main.rs | 100 +++++----- 8 files changed, 485 insertions(+), 140 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df85d35..b2b7917 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,7 +1347,7 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "regex_command_attr" -version = "0.2.0" +version = "0.3.6" dependencies = [ "proc-macro2", "quote", diff --git a/regex_command_attr/Cargo.toml b/regex_command_attr/Cargo.toml index dbe01d1..24e5fa5 100644 --- a/regex_command_attr/Cargo.toml +++ b/regex_command_attr/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "regex_command_attr" -version = "0.2.0" +version = "0.3.6" authors = ["acdenisSK ", "jellywx "] edition = "2018" -description = "Procedural macros for command creation for the RegexFramework for serenity." +description = "Procedural macros for command creation for the Serenity library." +license = "ISC" [lib] proc-macro = true diff --git a/regex_command_attr/src/attributes.rs b/regex_command_attr/src/attributes.rs index d1b6386..19fbb28 100644 --- a/regex_command_attr/src/attributes.rs +++ b/regex_command_attr/src/attributes.rs @@ -1,3 +1,5 @@ +use std::fmt::{self, Write}; + use proc_macro2::Span; use syn::parse::{Error, Result}; use syn::spanned::Spanned; @@ -6,8 +8,6 @@ use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path}; use crate::structures::PermissionLevel; use crate::util::{AsOption, LitExt}; -use std::fmt::{self, Write}; - #[derive(Debug, Clone, Copy, PartialEq)] pub enum ValueKind { // #[] diff --git a/regex_command_attr/src/lib.rs b/regex_command_attr/src/lib.rs index 635ac5a..352654f 100644 --- a/regex_command_attr/src/lib.rs +++ b/regex_command_attr/src/lib.rs @@ -1,10 +1,5 @@ #![deny(rust_2018_idioms)] -// FIXME: Remove this in a foreseeable future. -// Currently exists for backwards compatibility to previous Rust versions. -#![recursion_limit = "128"] - -#[allow(unused_extern_crates)] -extern crate proc_macro; +#![deny(broken_intra_doc_links)] use proc_macro::TokenStream; use quote::quote; @@ -37,11 +32,66 @@ macro_rules! match_options { }; } +/// The heart of the attribute-based framework. +/// +/// This is a function attribute macro. Using this on other Rust constructs won't work. +/// +/// ## Options +/// +/// To alter how the framework will interpret the command, +/// you can provide options as attributes following this `#[command]` macro. +/// +/// Each option has its own kind of data to stock and manipulate with. +/// They're given to the option either with the `#[option(...)]` or `#[option = ...]` syntaxes. +/// If an option doesn't require for any data to be supplied, then it's simply an empty `#[option]`. +/// +/// If the input to the option is malformed, the macro will give you can error, describing +/// the correct method for passing data, and what it should be. +/// +/// The list of available options, is, as follows: +/// +/// | Syntax | Description | Argument explanation | +/// | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +/// | `#[checks(identifiers)]` | Preconditions that must met before the command's execution. | `identifiers` is a comma separated list of identifiers referencing functions marked by the `#[check]` macro | +/// | `#[aliases(names)]` | Alternative names to refer to this command. | `names` is a comma separated list of desired aliases. | +/// | `#[description(desc)]`
`#[description = desc]` | The command's description or summary. | `desc` is a string describing the command. | +/// | `#[usage(use)]`
`#[usage = use]` | The command's intended usage. | `use` is a string stating the schema for the command's usage. | +/// | `#[example(ex)]`
`#[example = ex]` | An example of the command's usage. May be called multiple times to add many examples at once. | `ex` is a string | +/// | `#[delimiters(delims)]` | Argument delimiters specific to this command. Overrides the global list of delimiters in the framework. | `delims` is a comma separated list of strings | +/// | `#[min_args(min)]`
`#[max_args(max)]`
`#[num_args(min_and_max)]` | The expected length of arguments that the command must receive in order to function correctly. | `min`, `max` and `min_and_max` are 16-bit, unsigned integers. | +/// | `#[required_permissions(perms)]` | Set of permissions the user must possess. | `perms` is a comma separated list of permission names.
These can be found at [Discord's official documentation](https://discord.com/developers/docs/topics/permissions). | +/// | `#[allowed_roles(roles)]` | Set of roles the user must possess. | `roles` is a comma separated list of role names. | +/// | `#[help_available]`
`#[help_available(b)]` | If the command should be displayed in the help message. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | +/// | `#[only_in(ctx)]` | Which environment the command can be executed in. | `ctx` is a string with the accepted values `guild`/`guilds` and `dm`/`dms` (Direct Message). | +/// | `#[bucket(name)]`
`#[bucket = name]` | What bucket will impact this command. | `name` is a string containing the bucket's name.
Refer to [the bucket example in the standard framework](https://docs.rs/serenity/*/serenity/framework/standard/struct.StandardFramework.html#method.bucket) for its usage. | +/// | `#[owners_only]`
`#[owners_only(b)]` | If this command is exclusive to owners. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | +/// | `#[owner_privilege]`
`#[owner_privilege(b)]` | If owners can bypass certain options. | `b` is a boolean. If no boolean is provided, the value is assumed to be `true`. | +/// | `#[sub_commands(commands)]` | The sub or children commands of this command. They are executed in the form: `this-command sub-command`. | `commands` is a comma separated list of identifiers referencing functions marked by the `#[command]` macro. | +/// +/// Documentation comments (`///`) applied onto the function are interpreted as sugar for the +/// `#[description]` option. When more than one application of the option is performed, +/// the text is delimited by newlines. This mimics the behaviour of regular doc-comments, +/// which are sugar for the `#[doc = "..."]` attribute. If you wish to join lines together, +/// however, you have to end the previous lines with `\$`. +/// +/// # Notes +/// The name of the command is parsed from the applied function, +/// or may be specified inside the `#[command]` attribute, a lá `#[command("foobar")]`. +/// +/// This macro attribute generates static instances of `Command` and `CommandOptions`, +/// conserving the provided options. +/// +/// The names of the instances are all uppercased names of the command name. +/// For example, with a name of "foo": +/// ```rust,ignore +/// pub static FOO_COMMAND_OPTIONS: CommandOptions = CommandOptions { ... }; +/// pub static FOO_COMMAND: Command = Command { options: FOO_COMMAND_OPTIONS, ... }; +/// ``` #[proc_macro_attribute] pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let mut fun = parse_macro_input!(input as CommandFun); - let lit_name = if !attr.is_empty() { + let _name = if !attr.is_empty() { parse_macro_input!(attr as Lit).to_str() } else { fun.name.to_string() @@ -56,18 +106,37 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let name = values.name.to_string(); let name = &name[..]; - match_options!(name, values, options, span => [ - permission_level; - allow_slash - ]); + match name { + "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); + } + _ => { + match_options!(name, values, options, span => [ + aliases; + usage; + required_permissions; + allow_slash + ]); + } + } } let Options { - permission_level, + aliases, + description, + usage, + examples, + required_permissions, allow_slash, } = options; - propagate_err!(create_declaration_validations(&mut fun, DeclarFor::Command)); + propagate_err!(create_declaration_validations(&mut fun)); let res = parse_quote!(serenity::framework::standard::CommandResult); create_return_type_validation(&mut fun, res); @@ -88,18 +157,185 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { (quote! { #(#cooked)* + #[allow(missing_docs)] pub static #n: #command_path = #command_path { - func: #name, - name: #lit_name, - required_perms: #permission_level, + fun: #name, + names: &[#_name, #(#aliases),*], + desc: #description, + usage: #usage, + examples: &[#(#examples),*], + required_permissions: #required_permissions, allow_slash: #allow_slash, }; + #(#cooked)* + #[allow(missing_docs)] #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> { use ::serenity::futures::future::FutureExt; - async move { #(#body)* }.boxed() + async move { + let _output: #ret = { #(#body)* }; + #[allow(unreachable_code)] + _output + }.boxed() } }) .into() } + +/// A macro that transforms `async` functions (and closures) into plain functions, whose +/// return type is a boxed [`Future`]. +/// +/// # Transformation +/// +/// The macro transforms an `async` function, which may look like this: +/// +/// ```rust,no_run +/// async fn foo(n: i32) -> i32 { +/// n + 4 +/// } +/// ``` +/// +/// into this (some details omitted): +/// +/// ```rust,no_run +/// use std::future::Future; +/// use std::pin::Pin; +/// +/// fn foo(n: i32) -> Pin>> { +/// Box::pin(async move { +/// n + 4 +/// }) +/// } +/// ``` +/// +/// This transformation also applies to closures, which are converted more simply. For instance, +/// this closure: +/// +/// ```rust,no_run +/// # #![feature(async_closure)] +/// # +/// async move |x: i32| { +/// x * 2 + 4 +/// } +/// # ; +/// ``` +/// +/// is changed to: +/// +/// ```rust,no_run +/// |x: i32| { +/// Box::pin(async move { +/// x * 2 + 4 +/// }) +/// } +/// # ; +/// ``` +/// +/// ## How references are handled +/// +/// When a function contains references, their lifetimes are constrained to the returned +/// [`Future`]. If the above `foo` function had `&i32` as a parameter, the transformation would be +/// instead this: +/// +/// ```rust,no_run +/// use std::future::Future; +/// use std::pin::Pin; +/// +/// fn foo<'fut>(n: &'fut i32) -> Pin + 'fut>> { +/// Box::pin(async move { +/// *n + 4 +/// }) +/// } +/// ``` +/// +/// Explicitly specifying lifetimes (in the parameters or in the return type) or complex usage of +/// lifetimes (e.g. `'a: 'b`) is not supported. +/// +/// # Necessity for the macro +/// +/// The macro performs the transformation to permit the framework to store and invoke the functions. +/// +/// Functions marked with the `async` keyword will wrap their return type with the [`Future`] trait, +/// which a state-machine generated by the compiler for the function will implement. This complicates +/// matters for the framework, as [`Future`] is a trait. Depending on a type that implements a trait +/// is done with two methods in Rust: +/// +/// 1. static dispatch - generics +/// 2. dynamic dispatch - trait objects +/// +/// First method is infeasible for the framework. Typically, the framework will contain a plethora +/// of different commands that will be stored in a single list. And due to the nature of generics, +/// generic types can only resolve to a single concrete type. If commands had a generic type for +/// their function's return type, the framework would be unable to store commands, as only a single +/// [`Future`] type from one of the commands would get resolved, preventing other commands from being +/// stored. +/// +/// Second method involves heap allocations, but is the only working solution. If a trait is +/// object-safe (which [`Future`] is), the compiler can generate a table of function pointers +/// (a vtable) that correspond to certain implementations of the trait. This allows to decide +/// which implementation to use at runtime. Thus, we can use the interface for the [`Future`] trait, +/// and avoid depending on the underlying value (such as its size). To opt-in to dynamic dispatch, +/// trait objects must be used with a pointer, like references (`&` and `&mut`) or `Box`. The +/// latter is what's used by the macro, as the ownership of the value (the state-machine) must be +/// given to the caller, the framework in this case. +/// +/// The macro exists to retain the normal syntax of `async` functions (and closures), while +/// granting the user the ability to pass those functions to the framework, like command functions +/// and hooks (`before`, `after`, `on_dispatch_error`, etc.). +/// +/// # Notes +/// +/// If applying the macro on an `async` closure, you will need to enable the `async_closure` +/// feature. Inputs to procedural macro attributes must be valid Rust code, and `async` +/// closures are not stable yet. +/// +/// [`Future`]: std::future::Future +#[proc_macro_attribute] +pub fn hook(_attr: TokenStream, input: TokenStream) -> TokenStream { + let hook = parse_macro_input!(input as Hook); + + match hook { + Hook::Function(mut fun) => { + let cooked = fun.cooked; + let visibility = fun.visibility; + let fun_name = fun.name; + let body = fun.body; + let ret = fun.ret; + + populate_fut_lifetimes_on_refs(&mut fun.args); + let args = fun.args; + + (quote! { + #(#cooked)* + #[allow(missing_docs)] + #visibility fn #fun_name<'fut>(#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> { + use ::serenity::futures::future::FutureExt; + + async move { + let _output: #ret = { #(#body)* }; + #[allow(unreachable_code)] + _output + }.boxed() + } + }) + .into() + } + Hook::Closure(closure) => { + let cooked = closure.cooked; + let args = closure.args; + let ret = closure.ret; + let body = closure.body; + + (quote! { + #(#cooked)* + |#args| #ret { + use ::serenity::futures::future::FutureExt; + + async move { #body }.boxed() + } + }) + .into() + } + } +} diff --git a/regex_command_attr/src/structures.rs b/regex_command_attr/src/structures.rs index 3b0043e..f2ba215 100644 --- a/regex_command_attr/src/structures.rs +++ b/regex_command_attr/src/structures.rs @@ -1,15 +1,18 @@ -use crate::util::{Argument, Parenthesised}; -use proc_macro2::Span; +use std::str::FromStr; + use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens}; use syn::{ braced, parse::{Error, Parse, ParseStream, Result}, + punctuated::Punctuated, spanned::Spanned, - Attribute, Block, FnArg, Ident, Pat, Path, PathSegment, ReturnType, Stmt, Token, Type, + Attribute, Block, Expr, ExprClosure, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility, }; +use crate::util::{self, Argument, AsOption, Parenthesised}; + fn parse_argument(arg: FnArg) -> Result { match arg { FnArg::Typed(typed) => { @@ -54,7 +57,7 @@ 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", "doc", "derive", "inline", "allow", "warn", "deny", "forbid", + "cfg", "cfg_attr", "derive", "inline", "allow", "warn", "deny", "forbid", ]; COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n)) @@ -100,16 +103,8 @@ impl Parse for CommandFun { fn parse(input: ParseStream<'_>) -> Result { let mut attributes = input.call(Attribute::parse_outer)?; - // `#[doc = "..."]` is a cooked attribute but it is special-cased for commands. - for attr in &mut attributes { - // Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`. - if attr.path.is_ident("doc") { - attr.path = Path::from(PathSegment::from(Ident::new( - "description", - Span::call_site(), - ))); - } - } + // Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`. + util::rename_attributes(&mut attributes, "doc", "description"); let cooked = remove_cooked(&mut attributes); @@ -174,6 +169,115 @@ impl ToTokens for CommandFun { } } +#[derive(Debug)] +pub struct FunctionHook { + /// `#[...]`-style attributes. + pub attributes: Vec, + /// Populated by 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, + pub ret: Type, + pub body: Vec, +} + +#[derive(Debug)] +pub struct ClosureHook { + /// `#[...]`-style attributes. + pub attributes: Vec, + /// Populated by 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 args: Punctuated, + pub ret: ReturnType, + pub body: Box, +} + +#[derive(Debug)] +pub enum Hook { + Function(FunctionHook), + Closure(ClosureHook), +} + +impl Parse for Hook { + fn parse(input: ParseStream<'_>) -> Result { + let mut attributes = input.call(Attribute::parse_outer)?; + let cooked = remove_cooked(&mut attributes); + + if is_function(input) { + parse_function_hook(input, attributes, cooked).map(Self::Function) + } else { + parse_closure_hook(input, attributes, cooked).map(Self::Closure) + } + } +} + +fn is_function(input: ParseStream<'_>) -> bool { + input.peek(Token![pub]) || (input.peek(Token![async]) && input.peek2(Token![fn])) +} + +fn parse_function_hook( + input: ParseStream<'_>, + attributes: Vec, + cooked: Vec, +) -> Result { + let visibility = input.parse::()?; + + input.parse::()?; + input.parse::()?; + + let name = input.parse()?; + + // (...) + let Parenthesised(args) = input.parse::>()?; + + let ret = match input.parse::()? { + ReturnType::Type(_, t) => (*t).clone(), + ReturnType::Default => { + Type::Verbatim(TokenStream2::from_str("()").expect("Invalid str to create `()`-type")) + } + }; + + // { ... } + let bcont; + braced!(bcont in input); + let body = bcont.call(Block::parse_within)?; + + let args = args + .into_iter() + .map(parse_argument) + .collect::>>()?; + + Ok(FunctionHook { + attributes, + cooked, + visibility, + name, + args, + ret, + body, + }) +} + +fn parse_closure_hook( + input: ParseStream<'_>, + attributes: Vec, + cooked: Vec, +) -> Result { + input.parse::()?; + let closure = input.parse::()?; + + Ok(ClosureHook { + attributes, + cooked, + args: closure.inputs, + ret: closure.output, + body: closure.body, + }) +} + #[derive(Debug)] pub enum PermissionLevel { Unrestricted, @@ -225,7 +329,11 @@ impl ToTokens for PermissionLevel { #[derive(Debug, Default)] pub struct Options { - pub permission_level: PermissionLevel, + pub aliases: Vec, + pub description: AsOption, + pub usage: AsOption, + pub examples: Vec, + pub required_permissions: PermissionLevel, pub allow_slash: bool, } @@ -233,8 +341,8 @@ impl Options { #[inline] pub fn new() -> Self { Self { - permission_level: PermissionLevel::default(), - allow_slash: false, + allow_slash: true, + ..Default::default() } } } diff --git a/regex_command_attr/src/util.rs b/regex_command_attr/src/util.rs index ed514bf..668b387 100644 --- a/regex_command_attr/src/util.rs +++ b/regex_command_attr/src/util.rs @@ -1,4 +1,3 @@ -use crate::structures::CommandFun; use proc_macro::TokenStream; use proc_macro2::Span; use proc_macro2::TokenStream as TokenStream2; @@ -10,9 +9,11 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::{Comma, Mut}, - Ident, Lifetime, Lit, Type, + Attribute, Ident, Lifetime, Lit, Path, PathSegment, Type, }; +use crate::structures::CommandFun; + pub trait LitExt { fn to_str(&self) -> String; fn to_bool(&self) -> bool; @@ -160,35 +161,17 @@ pub fn generate_type_validation(have: Type, expect: Type) -> syn::Stmt { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DeclarFor { - Command, - Help, - Check, -} - -pub fn create_declaration_validations(fun: &mut CommandFun, dec_for: DeclarFor) -> SynResult<()> { - let len = match dec_for { - DeclarFor::Command => 3, - DeclarFor::Help => 6, - DeclarFor::Check => 4, - }; - - if fun.args.len() > len { +pub fn create_declaration_validations(fun: &mut CommandFun) -> SynResult<()> { + if fun.args.len() > 3 { return Err(Error::new( fun.args.last().unwrap().span(), - format_args!("function's arity exceeds more than {} arguments", len), + format_args!("function's arity exceeds more than 3 arguments"), )); } let context: Type = parse_quote!(&serenity::client::Context); let message: Type = parse_quote!(&(dyn crate::framework::CommandInvoke + Sync + Send)); let args: Type = parse_quote!(serenity::framework::standard::Args); - let args2: Type = parse_quote!(&mut serenity::framework::standard::Args); - let options: Type = parse_quote!(&serenity::framework::standard::CommandOptions); - let hoptions: Type = parse_quote!(&'static serenity::framework::standard::HelpOptions); - let groups: Type = parse_quote!(&[&'static serenity::framework::standard::CommandGroup]); - let owners: Type = parse_quote!(std::collections::HashSet); let mut index = 0; @@ -209,22 +192,8 @@ pub fn create_declaration_validations(fun: &mut CommandFun, dec_for: DeclarFor) spoof_or_check(context, "_ctx"); spoof_or_check(message, "_msg"); - - if dec_for == DeclarFor::Check { - spoof_or_check(args2, "_args"); - spoof_or_check(options, "_options"); - - return Ok(()); - } - spoof_or_check(args, "_args"); - if dec_for == DeclarFor::Help { - spoof_or_check(hoptions, "_hoptions"); - spoof_or_check(groups, "_groups"); - spoof_or_check(owners, "_owners"); - } - Ok(()) } @@ -242,3 +211,31 @@ pub fn populate_fut_lifetimes_on_refs(args: &mut Vec) { } } } + +/// Renames all attributes that have a specific `name` to the `target`. +pub fn rename_attributes(attributes: &mut Vec, name: &str, target: &str) { + for attr in attributes { + if attr.path.is_ident(name) { + attr.path = Path::from(PathSegment::from(Ident::new(target, Span::call_site()))); + } + } +} + +pub fn append_line(desc: &mut AsOption, mut line: String) { + if line.starts_with(' ') { + line.remove(0); + } + + let desc = desc.0.get_or_insert_with(String::default); + + match line.rfind("\\$") { + Some(i) => { + desc.push_str(line[..i].trim_end()); + desc.push(' '); + } + None => { + desc.push_str(&line); + desc.push('\n'); + } + } +} diff --git a/src/framework.rs b/src/framework.rs index 88b57b8..9f77d06 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -191,15 +191,18 @@ pub enum PermissionLevel { } pub struct Command { - pub name: &'static str, - pub required_perms: PermissionLevel, - pub func: CommandFn, + pub fun: CommandFn, + pub names: &'static [&'static str], + pub desc: Option<&'static str>, + pub usage: Option<&'static str>, + pub examples: &'static [&'static str], + pub required_permissions: PermissionLevel, pub allow_slash: bool, } impl Command { async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool { - if self.required_perms == PermissionLevel::Unrestricted { + if self.required_permissions == PermissionLevel::Unrestricted { true } else { let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap(); @@ -208,7 +211,7 @@ impl Command { return true; } - if self.required_perms == PermissionLevel::Managed { + if self.required_permissions == PermissionLevel::Managed { let pool = ctx .data .read() @@ -262,8 +265,8 @@ SELECT role impl fmt::Debug for Command { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Command") - .field("name", &self.name) - .field("required_perms", &self.required_perms) + .field("name", &self.names[0]) + .field("required_permissions", &self.required_permissions) .finish() } } @@ -350,8 +353,10 @@ impl RegexFramework { self } - pub fn add_command(mut self, name: S, command: &'static Command) -> Self { - self.commands.insert(name.to_string(), command); + pub fn add_command(mut self, command: &'static Command) -> Self { + for name in command.names { + self.commands.insert(name.to_string(), command); + } self } @@ -448,16 +453,18 @@ impl Framework for RegexFramework { let member = guild.member(&ctx, &msg.author).await.unwrap(); if command.check_permissions(&ctx, &guild, &member).await { - (command.func)( + (command.fun)( &ctx, &msg, Args::new(&args, &[Delimiter::Single(' ')]), ) .await .unwrap(); - } else if command.required_perms == PermissionLevel::Managed { + } else if command.required_permissions == PermissionLevel::Managed { let _ = msg.channel_id.say(&ctx, "You must either be an Admin or have a role specified in `?roles` to do this command").await; - } else if command.required_perms == PermissionLevel::Restricted { + } else if command.required_permissions + == PermissionLevel::Restricted + { let _ = msg .channel_id .say(&ctx, "You must be an Admin to do this command") diff --git a/src/main.rs b/src/main.rs index 6399575..0a07d2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -184,35 +184,33 @@ async fn main() -> Result<(), Box> { .case_insensitive(true) .ignore_bots(true) // info commands - .add_command("help", &HELP_COMMAND) - .add_command("info", &INFO_COMMAND) - .add_command("invite", &INFO_COMMAND) - .add_command("donate", &INFO_COMMAND) + .add_command(&HELP_COMMAND) + .add_command(&INFO_COMMAND) + .add_command(&INFO_COMMAND) + .add_command(&INFO_COMMAND) // play commands - .add_command("loop", &LOOP_PLAY_COMMAND) - .add_command("play", &PLAY_COMMAND) - .add_command("p", &PLAY_COMMAND) - .add_command("stop", &STOP_PLAYING_COMMAND) - .add_command("disconnect", &DISCONNECT_COMMAND) - .add_command("dc", &DISCONNECT_COMMAND) + .add_command(&LOOP_PLAY_COMMAND) + .add_command(&PLAY_COMMAND) + .add_command(&STOP_PLAYING_COMMAND) + .add_command(&DISCONNECT_COMMAND) // sound management commands - .add_command("upload", &UPLOAD_NEW_SOUND_COMMAND) - .add_command("delete", &DELETE_SOUND_COMMAND) - .add_command("list", &LIST_SOUNDS_COMMAND) - .add_command("public", &CHANGE_PUBLIC_COMMAND) + .add_command(&UPLOAD_NEW_SOUND_COMMAND) + .add_command(&DELETE_SOUND_COMMAND) + .add_command(&LIST_SOUNDS_COMMAND) + .add_command(&CHANGE_PUBLIC_COMMAND) // setting commands - .add_command("prefix", &CHANGE_PREFIX_COMMAND) - .add_command("roles", &SET_ALLOWED_ROLES_COMMAND) - .add_command("volume", &CHANGE_VOLUME_COMMAND) - .add_command("allow_greet", &ALLOW_GREET_SOUNDS_COMMAND) - .add_command("greet", &SET_GREET_SOUND_COMMAND) + .add_command(&CHANGE_PREFIX_COMMAND) + .add_command(&SET_ALLOWED_ROLES_COMMAND) + .add_command(&CHANGE_VOLUME_COMMAND) + .add_command(&ALLOW_GREET_SOUNDS_COMMAND) + .add_command(&SET_GREET_SOUND_COMMAND) // search commands - .add_command("search", &SEARCH_SOUNDS_COMMAND) - .add_command("popular", &SHOW_POPULAR_SOUNDS_COMMAND) - .add_command("random", &SHOW_RANDOM_SOUNDS_COMMAND); + .add_command(&SEARCH_SOUNDS_COMMAND) + .add_command(&SHOW_POPULAR_SOUNDS_COMMAND) + .add_command(&SHOW_RANDOM_SOUNDS_COMMAND); if audio_index.is_some() { - framework = framework.add_command("ambience", &PLAY_AMBIENCE_COMMAND); + framework = framework.add_command(&PLAY_AMBIENCE_COMMAND); } framework = framework.build(); @@ -425,18 +423,14 @@ Please select a category from the following: } #[command] -#[permission_level(Managed)] +#[aliases("p")] +#[required_permissions(Managed)] async fn play( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), args: Args, ) -> CommandResult { - let guild = invoke - .guild_id() - .unwrap() - .to_guild_cached(&ctx) - .await - .unwrap(); + let guild = invoke.guild(ctx.cache.clone()).await.unwrap(); invoke .channel_id() @@ -450,7 +444,7 @@ async fn play( } #[command] -#[permission_level(Managed)] +#[required_permissions(Managed)] async fn loop_play( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -528,8 +522,8 @@ async fn play_cmd(ctx: &Context, guild: Guild, user_id: UserId, args: Args, loop } } -#[command] -#[permission_level(Managed)] +#[command("ambience")] +#[required_permissions(Managed)] async fn play_ambience( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -617,8 +611,8 @@ __Available ambience sounds:__ Ok(()) } -#[command] -#[permission_level(Managed)] +#[command("stop")] +#[required_permissions(Managed)] async fn stop_playing( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -639,7 +633,8 @@ async fn stop_playing( } #[command] -#[permission_level(Managed)] +#[aliases("dc")] +#[required_permissions(Managed)] async fn disconnect( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -654,6 +649,7 @@ async fn disconnect( } #[command] +#[aliases("invite")] async fn info( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -690,8 +686,8 @@ There is a maximum sound limit per user. This can be removed by subscribing at * Ok(()) } -#[command] -#[permission_level(Managed)] +#[command("volume")] +#[required_permissions(Managed)] async fn change_volume( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -755,8 +751,8 @@ async fn change_volume( Ok(()) } -#[command] -#[permission_level(Restricted)] +#[command("prefix")] +#[required_permissions(Restricted)] async fn change_prefix( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -838,7 +834,7 @@ async fn change_prefix( Ok(()) } -#[command] +#[command("upload")] #[allow_slash(false)] async fn upload_new_sound( ctx: &Context, @@ -971,8 +967,8 @@ async fn upload_new_sound( Ok(()) } -#[command] -#[permission_level(Restricted)] +#[command("roles")] +#[required_permissions(Restricted)] #[allow_slash(false)] async fn set_allowed_roles( ctx: &Context, @@ -1060,7 +1056,7 @@ INSERT INTO roles (guild_id, role) Ok(()) } -#[command] +#[command("list")] async fn list_sounds( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -1121,7 +1117,7 @@ async fn list_sounds( Ok(()) } -#[command] +#[command("public")] async fn change_public( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -1186,7 +1182,7 @@ async fn change_public( Ok(()) } -#[command] +#[command("delete")] async fn delete_sound( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -1290,7 +1286,7 @@ fn format_search_results(search_results: Vec) -> CreateGenericResponse { CreateGenericResponse::new().embed(|e| e.title(title).fields(field_iter)) } -#[command] +#[command("search")] async fn search_sounds( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -1322,7 +1318,7 @@ async fn search_sounds( Ok(()) } -#[command] +#[command("popular")] async fn show_popular_sounds( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -1356,7 +1352,7 @@ SELECT name, id, plays, public, server_id, uploader_id Ok(()) } -#[command] +#[command("random")] async fn show_random_sounds( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -1391,7 +1387,7 @@ SELECT name, id, plays, public, server_id, uploader_id Ok(()) } -#[command] +#[command("greet")] async fn set_greet_sound( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -1457,8 +1453,8 @@ async fn set_greet_sound( Ok(()) } -#[command] -#[permission_level(Managed)] +#[command("allow_greet")] +#[required_permissions(Managed)] async fn allow_greet_sounds( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send),