From 1286f5f50e4e1a6230fa5c36cc430a30ba5c3ceb Mon Sep 17 00:00:00 2001 From: jellywx Date: Fri, 11 Jun 2021 10:10:48 +0100 Subject: [PATCH] arg macro to define arguments on commands --- regex_command_attr/src/attributes.rs | 161 +++++++++++++---- regex_command_attr/src/consts.rs | 1 + regex_command_attr/src/lib.rs | 261 +++++---------------------- regex_command_attr/src/structures.rs | 192 ++++++++------------ src/event_handlers.rs | 21 ++- src/framework.rs | 72 +++----- src/main.rs | 42 ++++- 7 files changed, 338 insertions(+), 412 deletions(-) diff --git a/regex_command_attr/src/attributes.rs b/regex_command_attr/src/attributes.rs index 19fbb28..74728d3 100644 --- a/regex_command_attr/src/attributes.rs +++ b/regex_command_attr/src/attributes.rs @@ -5,7 +5,7 @@ use syn::parse::{Error, Result}; use syn::spanned::Spanned; use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path}; -use crate::structures::PermissionLevel; +use crate::structures::{ApplicationCommandOptionType, Arg, PermissionLevel}; use crate::util::{AsOption, LitExt}; #[derive(Debug, Clone, Copy, PartialEq)] @@ -19,6 +19,9 @@ pub enum ValueKind { // #[([, , , ...])] List, + // #[([ = , = , ...])] + EqualsList, + // #[()] SingleList, } @@ -29,6 +32,9 @@ impl fmt::Display for ValueKind { ValueKind::Name => f.pad("`#[]`"), ValueKind::Equals => f.pad("`#[ = ]`"), ValueKind::List => f.pad("`#[([, , , ...])]`"), + ValueKind::EqualsList => { + f.pad("`#[([ = , = , ...])]`") + } ValueKind::SingleList => f.pad("`#[()]`"), } } @@ -62,14 +68,19 @@ fn to_ident(p: Path) -> Result { #[derive(Debug)] pub struct Values { pub name: Ident, - pub literals: Vec, + pub literals: Vec<(Option, Lit)>, pub kind: ValueKind, pub span: Span, } impl Values { #[inline] - pub fn new(name: Ident, kind: ValueKind, literals: Vec, span: Span) -> Self { + pub fn new( + name: Ident, + kind: ValueKind, + literals: Vec<(Option, Lit)>, + span: Span, + ) -> Self { Values { name, literals, @@ -80,6 +91,19 @@ impl Values { } pub fn parse_values(attr: &Attribute) -> Result { + fn is_list_or_named_list(meta: &NestedMeta) -> ValueKind { + match meta { + // catch if the nested value is a literal value + NestedMeta::Lit(_) => ValueKind::List, + // catch if the nested value is a meta value + NestedMeta::Meta(m) => match m { + // path => some quoted value + Meta::Path(_) => ValueKind::List, + Meta::List(_) | Meta::NameValue(_) => ValueKind::EqualsList, + }, + } + } + let meta = attr.parse_meta()?; match meta { @@ -96,36 +120,71 @@ pub fn parse_values(attr: &Attribute) -> Result { return Err(Error::new(attr.span(), "list cannot be empty")); } - let mut lits = Vec::with_capacity(nested.len()); + if is_list_or_named_list(nested.first().unwrap()) == ValueKind::List { + let mut lits = Vec::with_capacity(nested.len()); - for meta in nested { - match meta { - NestedMeta::Lit(l) => lits.push(l), - NestedMeta::Meta(m) => match m { - Meta::Path(path) => { - let i = to_ident(path)?; - lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span()))) - } - Meta::List(_) | Meta::NameValue(_) => { - return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) - } - }, + for meta in nested { + match meta { + // catch if the nested value is a literal value + NestedMeta::Lit(l) => lits.push((None, l)), + // catch if the nested value is a meta value + NestedMeta::Meta(m) => match m { + // path => some quoted value + Meta::Path(path) => { + let i = to_ident(path)?; + lits.push((None, Lit::Str(LitStr::new(&i.to_string(), i.span())))) + } + Meta::List(_) | Meta::NameValue(_) => { + return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) + } + }, + } } - } - let kind = if lits.len() == 1 { - ValueKind::SingleList + let kind = if lits.len() == 1 { + ValueKind::SingleList + } else { + ValueKind::List + }; + + Ok(Values::new(name, kind, lits, attr.span())) } else { - ValueKind::List - }; + let mut lits = Vec::with_capacity(nested.len()); - Ok(Values::new(name, kind, lits, attr.span())) + for meta in nested { + match meta { + // catch if the nested value is a literal value + NestedMeta::Lit(_) => { + return Err(Error::new(attr.span(), "key-value pairs expected")) + } + // catch if the nested value is a meta value + NestedMeta::Meta(m) => match m { + Meta::NameValue(n) => { + let name = to_ident(n.path)?.to_string(); + let value = n.lit; + + lits.push((Some(name), value)); + } + Meta::List(_) | Meta::Path(_) => { + return Err(Error::new(attr.span(), "key-value pairs expected")) + } + }, + } + } + + Ok(Values::new(name, ValueKind::EqualsList, lits, attr.span())) + } } Meta::NameValue(meta) => { let name = to_ident(meta.path)?; let lit = meta.lit; - Ok(Values::new(name, ValueKind::Equals, vec![lit], attr.span())) + Ok(Values::new( + name, + ValueKind::Equals, + vec![(None, lit)], + attr.span(), + )) } } } @@ -194,7 +253,7 @@ impl AttributeOption for Vec { Ok(values .literals .into_iter() - .map(|lit| lit.to_str()) + .map(|(_, l)| l.to_str()) .collect()) } } @@ -204,7 +263,7 @@ impl AttributeOption for String { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?; - Ok(values.literals[0].to_str()) + Ok(values.literals[0].1.to_str()) } } @@ -213,7 +272,7 @@ impl AttributeOption for bool { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::Name, ValueKind::SingleList])?; - Ok(values.literals.get(0).map_or(true, |l| l.to_bool())) + Ok(values.literals.get(0).map_or(true, |(_, l)| l.to_bool())) } } @@ -222,7 +281,7 @@ impl AttributeOption for Ident { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::SingleList])?; - Ok(values.literals[0].to_ident()) + Ok(values.literals[0].1.to_ident()) } } @@ -231,7 +290,11 @@ impl AttributeOption for Vec { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::List])?; - Ok(values.literals.into_iter().map(|l| l.to_ident()).collect()) + Ok(values + .literals + .into_iter() + .map(|(_, l)| l.to_ident()) + .collect()) } } @@ -242,7 +305,7 @@ impl AttributeOption for Option { &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList], )?; - Ok(values.literals.get(0).map(|l| l.to_str())) + Ok(values.literals.get(0).map(|(_, l)| l.to_str())) } } @@ -253,11 +316,47 @@ impl AttributeOption for PermissionLevel { Ok(values .literals .get(0) - .map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap()) + .map(|(_, l)| PermissionLevel::from_str(&*l.to_str()).unwrap()) .unwrap()) } } +impl AttributeOption for Arg { + fn parse(values: Values) -> Result { + validate(&values, &[ValueKind::EqualsList])?; + + let mut arg: Arg = Default::default(); + + for (key, value) in &values.literals { + match key { + Some(s) => match s.as_str() { + "name" => { + arg.name = value.to_str(); + } + "description" => { + arg.description = value.to_str(); + } + "required" => { + arg.required = value.to_bool(); + } + "default" => { + arg.default = value.to_bool(); + } + "kind" => arg.kind = ApplicationCommandOptionType::from_str(value.to_str()), + _ => { + return Err(Error::new(key.span(), "unexpected attribute")); + } + }, + _ => { + return Err(Error::new(key.span(), "unnamed attribute")); + } + } + } + + Ok(arg) + } +} + impl AttributeOption for AsOption { #[inline] fn parse(values: Values) -> Result { @@ -272,7 +371,7 @@ macro_rules! attr_option_num { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::SingleList])?; - Ok(match &values.literals[0] { + Ok(match &values.literals[0].1 { Lit::Int(l) => l.base10_parse::<$n>()?, l => { let s = l.to_str(); diff --git a/regex_command_attr/src/consts.rs b/regex_command_attr/src/consts.rs index 94ca381..9235297 100644 --- a/regex_command_attr/src/consts.rs +++ b/regex_command_attr/src/consts.rs @@ -1,5 +1,6 @@ pub mod suffixes { pub const COMMAND: &str = "COMMAND"; + pub const ARG: &str = "ARG"; } pub use self::suffixes::*; diff --git a/regex_command_attr/src/lib.rs b/regex_command_attr/src/lib.rs index 352654f..1ef8208 100644 --- a/regex_command_attr/src/lib.rs +++ b/regex_command_attr/src/lib.rs @@ -2,6 +2,7 @@ #![deny(broken_intra_doc_links)] use proc_macro::TokenStream; +use proc_macro2::Ident; use quote::quote; use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit}; @@ -32,61 +33,6 @@ 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); @@ -107,6 +53,9 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let name = &name[..]; match name { + "arg" => options + .cmd_args + .push(propagate_err!(attributes::parse(values))), "example" => { options .examples @@ -134,6 +83,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { examples, required_permissions, allow_slash, + mut cmd_args, } = options; propagate_err!(create_declaration_validations(&mut fun)); @@ -151,11 +101,47 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let cooked = fun.cooked.clone(); let command_path = quote!(crate::framework::Command); + let arg_path = quote!(crate::framework::Arg); populate_fut_lifetimes_on_refs(&mut fun.args); let args = fun.args; - (quote! { + let arg_idents = cmd_args + .iter() + .map(|arg| n.with_suffix(arg.name.as_str()).with_suffix(ARG)) + .collect::>(); + + let mut tokens = cmd_args + .iter_mut() + .map(|arg| { + let Arg { + name, + description, + kind, + required, + default, + } = arg; + + let an = n.with_suffix(name.as_str()).with_suffix(ARG); + + quote! { + #(#cooked)* + #[allow(missing_docs)] + pub static #an: #arg_path = #arg_path { + name: #name, + description: #description, + required: #required, + default: #default, + kind: #kind, + }; + } + }) + .fold(quote! {}, |mut a, b| { + a.extend(b); + a + }); + + tokens.extend(quote! { #(#cooked)* #[allow(missing_docs)] pub static #n: #command_path = #command_path { @@ -166,6 +152,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { examples: &[#(#examples),*], required_permissions: #required_permissions, allow_slash: #allow_slash, + args: &[#(&#arg_idents),*], }; #(#cooked)* @@ -179,163 +166,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { _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() - } - } + }); + + tokens.into() } diff --git a/regex_command_attr/src/structures.rs b/regex_command_attr/src/structures.rs index f2ba215..1b65b15 100644 --- a/regex_command_attr/src/structures.rs +++ b/regex_command_attr/src/structures.rs @@ -1,14 +1,10 @@ -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, Expr, ExprClosure, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, - Visibility, + Attribute, Block, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility, }; use crate::util::{self, Argument, AsOption, Parenthesised}; @@ -169,115 +165,6 @@ 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, @@ -327,14 +214,89 @@ impl ToTokens for PermissionLevel { } } +#[derive(Debug)] +pub(crate) enum ApplicationCommandOptionType { + SubCommand, + SubCommandGroup, + String, + Integer, + Boolean, + User, + Channel, + Role, + Mentionable, + Unknown, +} + +impl ApplicationCommandOptionType { + pub fn from_str(s: String) -> Self { + match s.as_str() { + "SubCommand" => Self::SubCommand, + "SubCommandGroup" => Self::SubCommandGroup, + "String" => Self::String, + "Integer" => Self::Integer, + "Boolean" => Self::Boolean, + "User" => Self::User, + "Channel" => Self::Channel, + "Role" => Self::Role, + "Mentionable" => Self::Mentionable, + _ => Self::Unknown, + } + } +} + +impl ToTokens for ApplicationCommandOptionType { + fn to_tokens(&self, stream: &mut TokenStream2) { + let path = quote!(serenity::model::interactions::ApplicationCommandOptionType); + let variant = match self { + ApplicationCommandOptionType::SubCommand => quote!(SubCommand), + ApplicationCommandOptionType::SubCommandGroup => quote!(SubCommandGroup), + ApplicationCommandOptionType::String => quote!(String), + ApplicationCommandOptionType::Integer => quote!(Integer), + ApplicationCommandOptionType::Boolean => quote!(Boolean), + ApplicationCommandOptionType::User => quote!(User), + ApplicationCommandOptionType::Channel => quote!(Channel), + ApplicationCommandOptionType::Role => quote!(Role), + ApplicationCommandOptionType::Mentionable => quote!(Mentionable), + ApplicationCommandOptionType::Unknown => quote!(Unknown), + }; + + stream.extend(quote! { + #path::#variant + }); + } +} + +#[derive(Debug)] +pub(crate) struct Arg { + pub name: String, + pub description: String, + pub kind: ApplicationCommandOptionType, + pub required: bool, + pub default: bool, +} + +impl Default for Arg { + fn default() -> Self { + Self { + name: String::new(), + description: String::new(), + kind: ApplicationCommandOptionType::String, + required: false, + default: false, + } + } +} + #[derive(Debug, Default)] -pub struct Options { +pub(crate) struct Options { pub aliases: Vec, pub description: AsOption, pub usage: AsOption, pub examples: Vec, pub required_permissions: PermissionLevel, pub allow_slash: bool, + pub cmd_args: Vec, } impl Options { diff --git a/src/event_handlers.rs b/src/event_handlers.rs index 35101a2..35b2ddd 100644 --- a/src/event_handlers.rs +++ b/src/event_handlers.rs @@ -1,4 +1,5 @@ use crate::{ + framework::RegexFramework, guild_data::CtxGuildData, join_channel, play_audio, sound::{JoinSoundCtx, Sound}, @@ -8,7 +9,9 @@ use crate::{ use serenity::{ async_trait, client::{Context, EventHandler}, - model::{channel::Channel, guild::Guild, id::GuildId, voice::VoiceState}, + model::{ + channel::Channel, guild::Guild, id::GuildId, interactions::Interaction, voice::VoiceState, + }, utils::shard_id, }; @@ -33,6 +36,18 @@ pub struct Handler; #[serenity::async_trait] impl EventHandler for Handler { + async fn cache_ready(&self, ctx: Context, _: Vec) { + let framework = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("RegexFramework not found in context"); + + framework.build_slash(ctx).await; + } + async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) { if is_new { if let Ok(token) = env::var("DISCORDBOTS_TOKEN") { @@ -154,4 +169,8 @@ SELECT name, id, plays, public, server_id, uploader_id } } } + + async fn interaction_create(&self, ctx: Context, interaction: Interaction) { + // + } } diff --git a/src/framework.rs b/src/framework.rs index 9f77d06..7fe82a0 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -1,7 +1,8 @@ use serenity::{ async_trait, + builder::CreateEmbed, + cache::Cache, client::Context, - constants::MESSAGE_CODE_LIMIT, framework::{ standard::{Args, CommandResult, Delimiter}, Framework, @@ -13,7 +14,9 @@ use serenity::{ guild::{Guild, Member}, id::{ChannelId, GuildId, UserId}, interactions::Interaction, + prelude::{ApplicationCommandOptionType, InteractionResponseType}, }, + prelude::TypeMapKey, Result as SerenityResult, }; @@ -24,9 +27,6 @@ use regex::{Match, Regex, RegexBuilder}; use std::{collections::HashMap, fmt}; use crate::{guild_data::CtxGuildData, MySQL}; -use serenity::builder::CreateEmbed; -use serenity::cache::Cache; -use serenity::model::prelude::InteractionResponseType; use std::sync::Arc; type CommandFn = for<'fut> fn( @@ -190,6 +190,15 @@ pub enum PermissionLevel { Restricted, } +#[derive(Debug)] +pub struct Arg { + pub name: &'static str, + pub description: &'static str, + pub kind: ApplicationCommandOptionType, + pub required: bool, + pub default: bool, +} + pub struct Command { pub fun: CommandFn, pub names: &'static [&'static str], @@ -198,6 +207,7 @@ pub struct Command { pub examples: &'static [&'static str], pub required_permissions: PermissionLevel, pub allow_slash: bool, + pub args: &'static [&'static Arg], } impl Command { @@ -267,53 +277,11 @@ impl fmt::Debug for Command { f.debug_struct("Command") .field("name", &self.names[0]) .field("required_permissions", &self.required_permissions) + .field("args", &self.args) .finish() } } -#[async_trait] -pub trait SendIterator { - async fn say_lines( - self, - http: impl AsRef + Send + Sync + 'async_trait, - content: impl Iterator + Send + 'async_trait, - ) -> SerenityResult<()>; -} - -#[async_trait] -impl SendIterator for ChannelId { - async fn say_lines( - self, - http: impl AsRef + Send + Sync + 'async_trait, - content: impl Iterator + Send + 'async_trait, - ) -> SerenityResult<()> { - let mut current_content = String::new(); - - for line in content { - if current_content.len() + line.len() > MESSAGE_CODE_LIMIT as usize { - self.send_message(&http, |m| { - m.allowed_mentions(|am| am.empty_parse()) - .content(¤t_content) - }) - .await?; - - current_content = line; - } else { - current_content = format!("{}\n{}", current_content, line); - } - } - if !current_content.is_empty() { - self.send_message(&http, |m| { - m.allowed_mentions(|am| am.empty_parse()) - .content(¤t_content) - }) - .await?; - } - - Ok(()) - } -} - pub struct RegexFramework { commands: HashMap, command_matcher: Regex, @@ -323,6 +291,10 @@ pub struct RegexFramework { case_insensitive: bool, } +impl TypeMapKey for RegexFramework { + type Value = Arc; +} + impl RegexFramework { pub fn new>(client_id: T) -> Self { Self { @@ -354,6 +326,8 @@ impl RegexFramework { } pub fn add_command(mut self, command: &'static Command) -> Self { + info!("{:?}", command); + for name in command.names { self.commands.insert(name.to_string(), command); } @@ -388,6 +362,10 @@ impl RegexFramework { self } + + pub async fn build_slash(&self, http: impl AsRef) { + // + } } enum PermissionCheck { diff --git a/src/main.rs b/src/main.rs index 0a07d2e..46e3dfd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -215,6 +215,8 @@ async fn main() -> Result<(), Box> { framework = framework.build(); + let framework_arc = Arc::new(framework); + let mut client = Client::builder(&env::var("DISCORD_TOKEN").expect("Missing token from environment")) .intents( @@ -222,7 +224,7 @@ async fn main() -> Result<(), Box> { | GatewayIntents::GUILD_MESSAGES | GatewayIntents::GUILDS, ) - .framework(framework) + .framework_arc(framework_arc.clone()) .application_id(application_id.0) .event_handler(Handler) .register_songbird() @@ -242,7 +244,7 @@ async fn main() -> Result<(), Box> { data.insert::(guild_data_cache); data.insert::(join_sound_cache); data.insert::(mysql_pool); - + data.insert::(framework_arc.clone()); data.insert::(Arc::new(reqwest::Client::new())); if let Some(audio_index) = audio_index { @@ -425,6 +427,20 @@ Please select a category from the following: #[command] #[aliases("p")] #[required_permissions(Managed)] +#[arg( + name = "query", + description = "Play sound with the specified name or ID", + kind = "String", + required = true, + default = true +)] +#[arg( + name = "loop", + description = "Whether to loop the sound or not (default: no)", + kind = "Boolean", + required = false, + default = false +)] async fn play( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -443,8 +459,14 @@ async fn play( Ok(()) } -#[command] +#[command("loop")] #[required_permissions(Managed)] +#[arg( + name = "query", + description = "Play sound with the specified name or ID", + kind = "String", + required = true +)] async fn loop_play( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -524,6 +546,12 @@ async fn play_cmd(ctx: &Context, guild: Guild, user_id: UserId, args: Args, loop #[command("ambience")] #[required_permissions(Managed)] +#[arg( + name = "name", + description = "Play sound with the specified name", + kind = "String", + required = false +)] async fn play_ambience( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -687,6 +715,7 @@ There is a maximum sound limit per user. This can be removed by subscribing at * } #[command("volume")] +#[aliases("vol")] #[required_permissions(Managed)] async fn change_volume( ctx: &Context, @@ -753,6 +782,7 @@ async fn change_volume( #[command("prefix")] #[required_permissions(Restricted)] +#[allow_slash(false)] async fn change_prefix( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send), @@ -1057,6 +1087,12 @@ INSERT INTO roles (guild_id, role) } #[command("list")] +#[arg( + name = "me", + description = "Whether to list your sounds or server sounds (default: server)", + kind = "Boolean", + required = false +)] async fn list_sounds( ctx: &Context, invoke: &(dyn CommandInvoke + Sync + Send),