arg macro to define arguments on commands
This commit is contained in:
parent
d5d2ac2bee
commit
1286f5f50e
@ -5,7 +5,7 @@ use syn::parse::{Error, Result};
|
|||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path};
|
use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path};
|
||||||
|
|
||||||
use crate::structures::PermissionLevel;
|
use crate::structures::{ApplicationCommandOptionType, Arg, PermissionLevel};
|
||||||
use crate::util::{AsOption, LitExt};
|
use crate::util::{AsOption, LitExt};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@ -19,6 +19,9 @@ pub enum ValueKind {
|
|||||||
// #[<name>([<value>, <value>, <value>, ...])]
|
// #[<name>([<value>, <value>, <value>, ...])]
|
||||||
List,
|
List,
|
||||||
|
|
||||||
|
// #[<name>([<prop> = <value>, <prop> = <value>, ...])]
|
||||||
|
EqualsList,
|
||||||
|
|
||||||
// #[<name>(<value>)]
|
// #[<name>(<value>)]
|
||||||
SingleList,
|
SingleList,
|
||||||
}
|
}
|
||||||
@ -29,6 +32,9 @@ impl fmt::Display for ValueKind {
|
|||||||
ValueKind::Name => f.pad("`#[<name>]`"),
|
ValueKind::Name => f.pad("`#[<name>]`"),
|
||||||
ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
|
ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
|
||||||
ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
|
ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
|
||||||
|
ValueKind::EqualsList => {
|
||||||
|
f.pad("`#[<name>([<prop> = <value>, <prop> = <value>, ...])]`")
|
||||||
|
}
|
||||||
ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
|
ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,14 +68,19 @@ fn to_ident(p: Path) -> Result<Ident> {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Values {
|
pub struct Values {
|
||||||
pub name: Ident,
|
pub name: Ident,
|
||||||
pub literals: Vec<Lit>,
|
pub literals: Vec<(Option<String>, Lit)>,
|
||||||
pub kind: ValueKind,
|
pub kind: ValueKind,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Values {
|
impl Values {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(name: Ident, kind: ValueKind, literals: Vec<Lit>, span: Span) -> Self {
|
pub fn new(
|
||||||
|
name: Ident,
|
||||||
|
kind: ValueKind,
|
||||||
|
literals: Vec<(Option<String>, Lit)>,
|
||||||
|
span: Span,
|
||||||
|
) -> Self {
|
||||||
Values {
|
Values {
|
||||||
name,
|
name,
|
||||||
literals,
|
literals,
|
||||||
@ -80,6 +91,19 @@ impl Values {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
||||||
|
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()?;
|
let meta = attr.parse_meta()?;
|
||||||
|
|
||||||
match meta {
|
match meta {
|
||||||
@ -96,36 +120,71 @@ pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
|||||||
return Err(Error::new(attr.span(), "list cannot be empty"));
|
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 {
|
for meta in nested {
|
||||||
match meta {
|
match meta {
|
||||||
NestedMeta::Lit(l) => lits.push(l),
|
// catch if the nested value is a literal value
|
||||||
NestedMeta::Meta(m) => match m {
|
NestedMeta::Lit(l) => lits.push((None, l)),
|
||||||
Meta::Path(path) => {
|
// catch if the nested value is a meta value
|
||||||
let i = to_ident(path)?;
|
NestedMeta::Meta(m) => match m {
|
||||||
lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span())))
|
// path => some quoted value
|
||||||
}
|
Meta::Path(path) => {
|
||||||
Meta::List(_) | Meta::NameValue(_) => {
|
let i = to_ident(path)?;
|
||||||
return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level"))
|
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 {
|
let kind = if lits.len() == 1 {
|
||||||
ValueKind::SingleList
|
ValueKind::SingleList
|
||||||
|
} else {
|
||||||
|
ValueKind::List
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Values::new(name, kind, lits, attr.span()))
|
||||||
} else {
|
} 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) => {
|
Meta::NameValue(meta) => {
|
||||||
let name = to_ident(meta.path)?;
|
let name = to_ident(meta.path)?;
|
||||||
let lit = meta.lit;
|
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<String> {
|
|||||||
Ok(values
|
Ok(values
|
||||||
.literals
|
.literals
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|lit| lit.to_str())
|
.map(|(_, l)| l.to_str())
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,7 +263,7 @@ impl AttributeOption for String {
|
|||||||
fn parse(values: Values) -> Result<Self> {
|
fn parse(values: Values) -> Result<Self> {
|
||||||
validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
|
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<Self> {
|
fn parse(values: Values) -> Result<Self> {
|
||||||
validate(&values, &[ValueKind::Name, ValueKind::SingleList])?;
|
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<Self> {
|
fn parse(values: Values) -> Result<Self> {
|
||||||
validate(&values, &[ValueKind::SingleList])?;
|
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<Ident> {
|
|||||||
fn parse(values: Values) -> Result<Self> {
|
fn parse(values: Values) -> Result<Self> {
|
||||||
validate(&values, &[ValueKind::List])?;
|
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<String> {
|
|||||||
&[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList],
|
&[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
|
Ok(values
|
||||||
.literals
|
.literals
|
||||||
.get(0)
|
.get(0)
|
||||||
.map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap())
|
.map(|(_, l)| PermissionLevel::from_str(&*l.to_str()).unwrap())
|
||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Arg {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
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<T: AttributeOption> AttributeOption for AsOption<T> {
|
impl<T: AttributeOption> AttributeOption for AsOption<T> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn parse(values: Values) -> Result<Self> {
|
fn parse(values: Values) -> Result<Self> {
|
||||||
@ -272,7 +371,7 @@ macro_rules! attr_option_num {
|
|||||||
fn parse(values: Values) -> Result<Self> {
|
fn parse(values: Values) -> Result<Self> {
|
||||||
validate(&values, &[ValueKind::SingleList])?;
|
validate(&values, &[ValueKind::SingleList])?;
|
||||||
|
|
||||||
Ok(match &values.literals[0] {
|
Ok(match &values.literals[0].1 {
|
||||||
Lit::Int(l) => l.base10_parse::<$n>()?,
|
Lit::Int(l) => l.base10_parse::<$n>()?,
|
||||||
l => {
|
l => {
|
||||||
let s = l.to_str();
|
let s = l.to_str();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
pub mod suffixes {
|
pub mod suffixes {
|
||||||
pub const COMMAND: &str = "COMMAND";
|
pub const COMMAND: &str = "COMMAND";
|
||||||
|
pub const ARG: &str = "ARG";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use self::suffixes::*;
|
pub use self::suffixes::*;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
#![deny(broken_intra_doc_links)]
|
#![deny(broken_intra_doc_links)]
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::Ident;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit};
|
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)]` </br> `#[description = desc]` | The command's description or summary. | `desc` is a string describing the command. |
|
|
||||||
/// | `#[usage(use)]` </br> `#[usage = use]` | The command's intended usage. | `use` is a string stating the schema for the command's usage. |
|
|
||||||
/// | `#[example(ex)]` </br> `#[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)]` </br> `#[max_args(max)]` </br> `#[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.</br> 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]` </br> `#[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)]` </br> `#[bucket = name]` | What bucket will impact this command. | `name` is a string containing the bucket's name.</br> 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]` </br> `#[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]` </br> `#[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]
|
#[proc_macro_attribute]
|
||||||
pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
let mut fun = parse_macro_input!(input as CommandFun);
|
let mut fun = parse_macro_input!(input as CommandFun);
|
||||||
@ -107,6 +53,9 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
let name = &name[..];
|
let name = &name[..];
|
||||||
|
|
||||||
match name {
|
match name {
|
||||||
|
"arg" => options
|
||||||
|
.cmd_args
|
||||||
|
.push(propagate_err!(attributes::parse(values))),
|
||||||
"example" => {
|
"example" => {
|
||||||
options
|
options
|
||||||
.examples
|
.examples
|
||||||
@ -134,6 +83,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
examples,
|
examples,
|
||||||
required_permissions,
|
required_permissions,
|
||||||
allow_slash,
|
allow_slash,
|
||||||
|
mut cmd_args,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
propagate_err!(create_declaration_validations(&mut fun));
|
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 cooked = fun.cooked.clone();
|
||||||
|
|
||||||
let command_path = quote!(crate::framework::Command);
|
let command_path = quote!(crate::framework::Command);
|
||||||
|
let arg_path = quote!(crate::framework::Arg);
|
||||||
|
|
||||||
populate_fut_lifetimes_on_refs(&mut fun.args);
|
populate_fut_lifetimes_on_refs(&mut fun.args);
|
||||||
let args = 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::<Vec<Ident>>();
|
||||||
|
|
||||||
|
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)*
|
#(#cooked)*
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub static #n: #command_path = #command_path {
|
pub static #n: #command_path = #command_path {
|
||||||
@ -166,6 +152,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
examples: &[#(#examples),*],
|
examples: &[#(#examples),*],
|
||||||
required_permissions: #required_permissions,
|
required_permissions: #required_permissions,
|
||||||
allow_slash: #allow_slash,
|
allow_slash: #allow_slash,
|
||||||
|
args: &[#(&#arg_idents),*],
|
||||||
};
|
};
|
||||||
|
|
||||||
#(#cooked)*
|
#(#cooked)*
|
||||||
@ -179,163 +166,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
_output
|
_output
|
||||||
}.boxed()
|
}.boxed()
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.into()
|
|
||||||
}
|
tokens.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<dyn std::future::Future<Output = i32>>> {
|
|
||||||
/// 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<Box<dyn std::future::Future<Output = i32> + '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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
use quote::{quote, ToTokens};
|
use quote::{quote, ToTokens};
|
||||||
use syn::{
|
use syn::{
|
||||||
braced,
|
braced,
|
||||||
parse::{Error, Parse, ParseStream, Result},
|
parse::{Error, Parse, ParseStream, Result},
|
||||||
punctuated::Punctuated,
|
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
Attribute, Block, Expr, ExprClosure, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type,
|
Attribute, Block, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility,
|
||||||
Visibility,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::util::{self, Argument, AsOption, Parenthesised};
|
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<Attribute>,
|
|
||||||
/// 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<Attribute>,
|
|
||||||
pub visibility: Visibility,
|
|
||||||
pub name: Ident,
|
|
||||||
pub args: Vec<Argument>,
|
|
||||||
pub ret: Type,
|
|
||||||
pub body: Vec<Stmt>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ClosureHook {
|
|
||||||
/// `#[...]`-style attributes.
|
|
||||||
pub attributes: Vec<Attribute>,
|
|
||||||
/// 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<Attribute>,
|
|
||||||
pub args: Punctuated<Pat, Token![,]>,
|
|
||||||
pub ret: ReturnType,
|
|
||||||
pub body: Box<Expr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Hook {
|
|
||||||
Function(FunctionHook),
|
|
||||||
Closure(ClosureHook),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for Hook {
|
|
||||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
||||||
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<Attribute>,
|
|
||||||
cooked: Vec<Attribute>,
|
|
||||||
) -> Result<FunctionHook> {
|
|
||||||
let visibility = input.parse::<Visibility>()?;
|
|
||||||
|
|
||||||
input.parse::<Token![async]>()?;
|
|
||||||
input.parse::<Token![fn]>()?;
|
|
||||||
|
|
||||||
let name = input.parse()?;
|
|
||||||
|
|
||||||
// (...)
|
|
||||||
let Parenthesised(args) = input.parse::<Parenthesised<FnArg>>()?;
|
|
||||||
|
|
||||||
let ret = match input.parse::<ReturnType>()? {
|
|
||||||
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::<Result<Vec<_>>>()?;
|
|
||||||
|
|
||||||
Ok(FunctionHook {
|
|
||||||
attributes,
|
|
||||||
cooked,
|
|
||||||
visibility,
|
|
||||||
name,
|
|
||||||
args,
|
|
||||||
ret,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_closure_hook(
|
|
||||||
input: ParseStream<'_>,
|
|
||||||
attributes: Vec<Attribute>,
|
|
||||||
cooked: Vec<Attribute>,
|
|
||||||
) -> Result<ClosureHook> {
|
|
||||||
input.parse::<Token![async]>()?;
|
|
||||||
let closure = input.parse::<ExprClosure>()?;
|
|
||||||
|
|
||||||
Ok(ClosureHook {
|
|
||||||
attributes,
|
|
||||||
cooked,
|
|
||||||
args: closure.inputs,
|
|
||||||
ret: closure.output,
|
|
||||||
body: closure.body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PermissionLevel {
|
pub enum PermissionLevel {
|
||||||
Unrestricted,
|
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)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Options {
|
pub(crate) struct Options {
|
||||||
pub aliases: Vec<String>,
|
pub aliases: Vec<String>,
|
||||||
pub description: AsOption<String>,
|
pub description: AsOption<String>,
|
||||||
pub usage: AsOption<String>,
|
pub usage: AsOption<String>,
|
||||||
pub examples: Vec<String>,
|
pub examples: Vec<String>,
|
||||||
pub required_permissions: PermissionLevel,
|
pub required_permissions: PermissionLevel,
|
||||||
pub allow_slash: bool,
|
pub allow_slash: bool,
|
||||||
|
pub cmd_args: Vec<Arg>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
framework::RegexFramework,
|
||||||
guild_data::CtxGuildData,
|
guild_data::CtxGuildData,
|
||||||
join_channel, play_audio,
|
join_channel, play_audio,
|
||||||
sound::{JoinSoundCtx, Sound},
|
sound::{JoinSoundCtx, Sound},
|
||||||
@ -8,7 +9,9 @@ use crate::{
|
|||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
client::{Context, EventHandler},
|
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,
|
utils::shard_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -33,6 +36,18 @@ pub struct Handler;
|
|||||||
|
|
||||||
#[serenity::async_trait]
|
#[serenity::async_trait]
|
||||||
impl EventHandler for Handler {
|
impl EventHandler for Handler {
|
||||||
|
async fn cache_ready(&self, ctx: Context, _: Vec<GuildId>) {
|
||||||
|
let framework = ctx
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<RegexFramework>()
|
||||||
|
.cloned()
|
||||||
|
.expect("RegexFramework not found in context");
|
||||||
|
|
||||||
|
framework.build_slash(ctx).await;
|
||||||
|
}
|
||||||
|
|
||||||
async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) {
|
async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) {
|
||||||
if is_new {
|
if is_new {
|
||||||
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
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) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
|
builder::CreateEmbed,
|
||||||
|
cache::Cache,
|
||||||
client::Context,
|
client::Context,
|
||||||
constants::MESSAGE_CODE_LIMIT,
|
|
||||||
framework::{
|
framework::{
|
||||||
standard::{Args, CommandResult, Delimiter},
|
standard::{Args, CommandResult, Delimiter},
|
||||||
Framework,
|
Framework,
|
||||||
@ -13,7 +14,9 @@ use serenity::{
|
|||||||
guild::{Guild, Member},
|
guild::{Guild, Member},
|
||||||
id::{ChannelId, GuildId, UserId},
|
id::{ChannelId, GuildId, UserId},
|
||||||
interactions::Interaction,
|
interactions::Interaction,
|
||||||
|
prelude::{ApplicationCommandOptionType, InteractionResponseType},
|
||||||
},
|
},
|
||||||
|
prelude::TypeMapKey,
|
||||||
Result as SerenityResult,
|
Result as SerenityResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,9 +27,6 @@ use regex::{Match, Regex, RegexBuilder};
|
|||||||
use std::{collections::HashMap, fmt};
|
use std::{collections::HashMap, fmt};
|
||||||
|
|
||||||
use crate::{guild_data::CtxGuildData, MySQL};
|
use crate::{guild_data::CtxGuildData, MySQL};
|
||||||
use serenity::builder::CreateEmbed;
|
|
||||||
use serenity::cache::Cache;
|
|
||||||
use serenity::model::prelude::InteractionResponseType;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
type CommandFn = for<'fut> fn(
|
type CommandFn = for<'fut> fn(
|
||||||
@ -190,6 +190,15 @@ pub enum PermissionLevel {
|
|||||||
Restricted,
|
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 struct Command {
|
||||||
pub fun: CommandFn,
|
pub fun: CommandFn,
|
||||||
pub names: &'static [&'static str],
|
pub names: &'static [&'static str],
|
||||||
@ -198,6 +207,7 @@ pub struct Command {
|
|||||||
pub examples: &'static [&'static str],
|
pub examples: &'static [&'static str],
|
||||||
pub required_permissions: PermissionLevel,
|
pub required_permissions: PermissionLevel,
|
||||||
pub allow_slash: bool,
|
pub allow_slash: bool,
|
||||||
|
pub args: &'static [&'static Arg],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
@ -267,53 +277,11 @@ impl fmt::Debug for Command {
|
|||||||
f.debug_struct("Command")
|
f.debug_struct("Command")
|
||||||
.field("name", &self.names[0])
|
.field("name", &self.names[0])
|
||||||
.field("required_permissions", &self.required_permissions)
|
.field("required_permissions", &self.required_permissions)
|
||||||
|
.field("args", &self.args)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait SendIterator {
|
|
||||||
async fn say_lines(
|
|
||||||
self,
|
|
||||||
http: impl AsRef<Http> + Send + Sync + 'async_trait,
|
|
||||||
content: impl Iterator<Item = String> + Send + 'async_trait,
|
|
||||||
) -> SerenityResult<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl SendIterator for ChannelId {
|
|
||||||
async fn say_lines(
|
|
||||||
self,
|
|
||||||
http: impl AsRef<Http> + Send + Sync + 'async_trait,
|
|
||||||
content: impl Iterator<Item = String> + 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 {
|
pub struct RegexFramework {
|
||||||
commands: HashMap<String, &'static Command>,
|
commands: HashMap<String, &'static Command>,
|
||||||
command_matcher: Regex,
|
command_matcher: Regex,
|
||||||
@ -323,6 +291,10 @@ pub struct RegexFramework {
|
|||||||
case_insensitive: bool,
|
case_insensitive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TypeMapKey for RegexFramework {
|
||||||
|
type Value = Arc<RegexFramework>;
|
||||||
|
}
|
||||||
|
|
||||||
impl RegexFramework {
|
impl RegexFramework {
|
||||||
pub fn new<T: Into<u64>>(client_id: T) -> Self {
|
pub fn new<T: Into<u64>>(client_id: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -354,6 +326,8 @@ impl RegexFramework {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_command(mut self, command: &'static Command) -> Self {
|
pub fn add_command(mut self, command: &'static Command) -> Self {
|
||||||
|
info!("{:?}", command);
|
||||||
|
|
||||||
for name in command.names {
|
for name in command.names {
|
||||||
self.commands.insert(name.to_string(), command);
|
self.commands.insert(name.to_string(), command);
|
||||||
}
|
}
|
||||||
@ -388,6 +362,10 @@ impl RegexFramework {
|
|||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn build_slash(&self, http: impl AsRef<Http>) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PermissionCheck {
|
enum PermissionCheck {
|
||||||
|
42
src/main.rs
42
src/main.rs
@ -215,6 +215,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
|
|
||||||
framework = framework.build();
|
framework = framework.build();
|
||||||
|
|
||||||
|
let framework_arc = Arc::new(framework);
|
||||||
|
|
||||||
let mut client =
|
let mut client =
|
||||||
Client::builder(&env::var("DISCORD_TOKEN").expect("Missing token from environment"))
|
Client::builder(&env::var("DISCORD_TOKEN").expect("Missing token from environment"))
|
||||||
.intents(
|
.intents(
|
||||||
@ -222,7 +224,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
| GatewayIntents::GUILD_MESSAGES
|
| GatewayIntents::GUILD_MESSAGES
|
||||||
| GatewayIntents::GUILDS,
|
| GatewayIntents::GUILDS,
|
||||||
)
|
)
|
||||||
.framework(framework)
|
.framework_arc(framework_arc.clone())
|
||||||
.application_id(application_id.0)
|
.application_id(application_id.0)
|
||||||
.event_handler(Handler)
|
.event_handler(Handler)
|
||||||
.register_songbird()
|
.register_songbird()
|
||||||
@ -242,7 +244,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
data.insert::<GuildDataCache>(guild_data_cache);
|
data.insert::<GuildDataCache>(guild_data_cache);
|
||||||
data.insert::<JoinSoundCache>(join_sound_cache);
|
data.insert::<JoinSoundCache>(join_sound_cache);
|
||||||
data.insert::<MySQL>(mysql_pool);
|
data.insert::<MySQL>(mysql_pool);
|
||||||
|
data.insert::<RegexFramework>(framework_arc.clone());
|
||||||
data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
|
data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
|
||||||
|
|
||||||
if let Some(audio_index) = audio_index {
|
if let Some(audio_index) = audio_index {
|
||||||
@ -425,6 +427,20 @@ Please select a category from the following:
|
|||||||
#[command]
|
#[command]
|
||||||
#[aliases("p")]
|
#[aliases("p")]
|
||||||
#[required_permissions(Managed)]
|
#[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(
|
async fn play(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
invoke: &(dyn CommandInvoke + Sync + Send),
|
invoke: &(dyn CommandInvoke + Sync + Send),
|
||||||
@ -443,8 +459,14 @@ async fn play(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command("loop")]
|
||||||
#[required_permissions(Managed)]
|
#[required_permissions(Managed)]
|
||||||
|
#[arg(
|
||||||
|
name = "query",
|
||||||
|
description = "Play sound with the specified name or ID",
|
||||||
|
kind = "String",
|
||||||
|
required = true
|
||||||
|
)]
|
||||||
async fn loop_play(
|
async fn loop_play(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
invoke: &(dyn CommandInvoke + Sync + Send),
|
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")]
|
#[command("ambience")]
|
||||||
#[required_permissions(Managed)]
|
#[required_permissions(Managed)]
|
||||||
|
#[arg(
|
||||||
|
name = "name",
|
||||||
|
description = "Play sound with the specified name",
|
||||||
|
kind = "String",
|
||||||
|
required = false
|
||||||
|
)]
|
||||||
async fn play_ambience(
|
async fn play_ambience(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
invoke: &(dyn CommandInvoke + Sync + Send),
|
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")]
|
#[command("volume")]
|
||||||
|
#[aliases("vol")]
|
||||||
#[required_permissions(Managed)]
|
#[required_permissions(Managed)]
|
||||||
async fn change_volume(
|
async fn change_volume(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
@ -753,6 +782,7 @@ async fn change_volume(
|
|||||||
|
|
||||||
#[command("prefix")]
|
#[command("prefix")]
|
||||||
#[required_permissions(Restricted)]
|
#[required_permissions(Restricted)]
|
||||||
|
#[allow_slash(false)]
|
||||||
async fn change_prefix(
|
async fn change_prefix(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
invoke: &(dyn CommandInvoke + Sync + Send),
|
invoke: &(dyn CommandInvoke + Sync + Send),
|
||||||
@ -1057,6 +1087,12 @@ INSERT INTO roles (guild_id, role)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command("list")]
|
#[command("list")]
|
||||||
|
#[arg(
|
||||||
|
name = "me",
|
||||||
|
description = "Whether to list your sounds or server sounds (default: server)",
|
||||||
|
kind = "Boolean",
|
||||||
|
required = false
|
||||||
|
)]
|
||||||
async fn list_sounds(
|
async fn list_sounds(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
invoke: &(dyn CommandInvoke + Sync + Send),
|
invoke: &(dyn CommandInvoke + Sync + Send),
|
||||||
|
Loading…
Reference in New Issue
Block a user