move to regex framework to hopefully reduce bad cpu load
This commit is contained in:
parent
d57e1d3ab1
commit
0ea979a2b7
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -1470,6 +1470,15 @@ version = "0.6.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
|
checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex_command_attr"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remove_dir_all"
|
name = "remove_dir_all"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@ -1889,6 +1898,9 @@ version = "1.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"log 0.4.13",
|
||||||
|
"regex",
|
||||||
|
"regex_command_attr",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serenity",
|
"serenity",
|
||||||
"songbird",
|
"songbird",
|
||||||
|
@ -12,3 +12,8 @@ tokio = { version = "1.0", features = ["fs", "process", "io-util"] }
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
songbird = "0.1"
|
songbird = "0.1"
|
||||||
|
regex = "1.4"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
[dependencies.regex_command_attr]
|
||||||
|
path = "./regex_command_attr"
|
||||||
|
14
regex_command_attr/Cargo.toml
Normal file
14
regex_command_attr/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "regex_command_attr"
|
||||||
|
version = "0.2.0"
|
||||||
|
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "Procedural macros for command creation for the RegexFramework for serenity."
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "^1.0"
|
||||||
|
syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] }
|
||||||
|
proc-macro2 = "1.0"
|
300
regex_command_attr/src/attributes.rs
Normal file
300
regex_command_attr/src/attributes.rs
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
use proc_macro2::Span;
|
||||||
|
use syn::parse::{Error, Result};
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
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 {
|
||||||
|
// #[<name>]
|
||||||
|
Name,
|
||||||
|
|
||||||
|
// #[<name> = <value>]
|
||||||
|
Equals,
|
||||||
|
|
||||||
|
// #[<name>([<value>, <value>, <value>, ...])]
|
||||||
|
List,
|
||||||
|
|
||||||
|
// #[<name>(<value>)]
|
||||||
|
SingleList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ValueKind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ValueKind::Name => f.pad("`#[<name>]`"),
|
||||||
|
ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
|
||||||
|
ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
|
||||||
|
ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_ident(p: Path) -> Result<Ident> {
|
||||||
|
if p.segments.is_empty() {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"cannot convert an empty path to an identifier",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.segments.len() > 1 {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"the path must not have more than one segment",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.segments[0].arguments.is_empty() {
|
||||||
|
return Err(Error::new(
|
||||||
|
p.span(),
|
||||||
|
"the singular path segment must not have any arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(p.segments[0].ident.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Values {
|
||||||
|
pub name: Ident,
|
||||||
|
pub literals: Vec<Lit>,
|
||||||
|
pub kind: ValueKind,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Values {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(name: Ident, kind: ValueKind, literals: Vec<Lit>, span: Span) -> Self {
|
||||||
|
Values {
|
||||||
|
name,
|
||||||
|
literals,
|
||||||
|
kind,
|
||||||
|
span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
||||||
|
let meta = attr.parse_meta()?;
|
||||||
|
|
||||||
|
match meta {
|
||||||
|
Meta::Path(path) => {
|
||||||
|
let name = to_ident(path)?;
|
||||||
|
|
||||||
|
Ok(Values::new(name, ValueKind::Name, Vec::new(), attr.span()))
|
||||||
|
}
|
||||||
|
Meta::List(meta) => {
|
||||||
|
let name = to_ident(meta.path)?;
|
||||||
|
let nested = meta.nested;
|
||||||
|
|
||||||
|
if nested.is_empty() {
|
||||||
|
return Err(Error::new(attr.span(), "list cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = if lits.len() == 1 {
|
||||||
|
ValueKind::SingleList
|
||||||
|
} else {
|
||||||
|
ValueKind::List
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Values::new(name, kind, 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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct DisplaySlice<'a, T>(&'a [T]);
|
||||||
|
|
||||||
|
impl<'a, T: fmt::Display> fmt::Display for DisplaySlice<'a, T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut iter = self.0.iter().enumerate();
|
||||||
|
|
||||||
|
match iter.next() {
|
||||||
|
None => f.write_str("nothing")?,
|
||||||
|
Some((idx, elem)) => {
|
||||||
|
write!(f, "{}: {}", idx, elem)?;
|
||||||
|
|
||||||
|
for (idx, elem) in iter {
|
||||||
|
f.write_char('\n')?;
|
||||||
|
write!(f, "{}: {}", idx, elem)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_form_acceptable(expect: &[ValueKind], kind: ValueKind) -> bool {
|
||||||
|
if expect.contains(&ValueKind::List) && kind == ValueKind::SingleList {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
expect.contains(&kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> {
|
||||||
|
if !is_form_acceptable(forms, values.kind) {
|
||||||
|
return Err(Error::new(
|
||||||
|
values.span,
|
||||||
|
// Using the `_args` version here to avoid an allocation.
|
||||||
|
format_args!(
|
||||||
|
"the attribute must be in of these forms:\n{}",
|
||||||
|
DisplaySlice(forms)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn parse<T: AttributeOption>(values: Values) -> Result<T> {
|
||||||
|
T::parse(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AttributeOption: Sized {
|
||||||
|
fn parse(values: Values) -> Result<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Vec<String> {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::List])?;
|
||||||
|
|
||||||
|
Ok(values
|
||||||
|
.literals
|
||||||
|
.into_iter()
|
||||||
|
.map(|lit| lit.to_str())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for String {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values.literals[0].to_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for bool {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::Name, ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values.literals.get(0).map_or(true, |l| l.to_bool()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Ident {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values.literals[0].to_ident())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Vec<Ident> {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::List])?;
|
||||||
|
|
||||||
|
Ok(values.literals.into_iter().map(|l| l.to_ident()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Option<String> {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(
|
||||||
|
&values,
|
||||||
|
&[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(values.literals.get(0).map(|l| l.to_str()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for PermissionLevel {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(values
|
||||||
|
.literals
|
||||||
|
.get(0)
|
||||||
|
.map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap())
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AttributeOption> AttributeOption for AsOption<T> {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
Ok(AsOption(Some(T::parse(values)?)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! attr_option_num {
|
||||||
|
($($n:ty),*) => {
|
||||||
|
$(
|
||||||
|
impl AttributeOption for $n {
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
validate(&values, &[ValueKind::SingleList])?;
|
||||||
|
|
||||||
|
Ok(match &values.literals[0] {
|
||||||
|
Lit::Int(l) => l.base10_parse::<$n>()?,
|
||||||
|
l => {
|
||||||
|
let s = l.to_str();
|
||||||
|
// Use `as_str` to guide the compiler to use `&str`'s parse method.
|
||||||
|
// We don't want to use our `parse` method here (`impl AttributeOption for String`).
|
||||||
|
match s.as_str().parse::<$n>() {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(_) => return Err(Error::new(l.span(), "invalid integer")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption for Option<$n> {
|
||||||
|
#[inline]
|
||||||
|
fn parse(values: Values) -> Result<Self> {
|
||||||
|
<$n as AttributeOption>::parse(values).map(Some)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_option_num!(u16, u32, usize);
|
5
regex_command_attr/src/consts.rs
Normal file
5
regex_command_attr/src/consts.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod suffixes {
|
||||||
|
pub const COMMAND: &str = "COMMAND";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use self::suffixes::*;
|
100
regex_command_attr/src/lib.rs
Normal file
100
regex_command_attr/src/lib.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
#![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;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit};
|
||||||
|
|
||||||
|
pub(crate) mod attributes;
|
||||||
|
pub(crate) mod consts;
|
||||||
|
pub(crate) mod structures;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
pub(crate) mod util;
|
||||||
|
|
||||||
|
use attributes::*;
|
||||||
|
use consts::*;
|
||||||
|
use structures::*;
|
||||||
|
use util::*;
|
||||||
|
|
||||||
|
macro_rules! match_options {
|
||||||
|
($v:expr, $values:ident, $options:ident, $span:expr => [$($name:ident);*]) => {
|
||||||
|
match $v {
|
||||||
|
$(
|
||||||
|
stringify!($name) => $options.$name = propagate_err!($crate::attributes::parse($values)),
|
||||||
|
)*
|
||||||
|
_ => {
|
||||||
|
return Error::new($span, format_args!("invalid attribute: {:?}", $v))
|
||||||
|
.to_compile_error()
|
||||||
|
.into();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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() {
|
||||||
|
parse_macro_input!(attr as Lit).to_str()
|
||||||
|
} else {
|
||||||
|
fun.name.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut options = Options::new();
|
||||||
|
|
||||||
|
for attribute in &fun.attributes {
|
||||||
|
let span = attribute.span();
|
||||||
|
let values = propagate_err!(parse_values(attribute));
|
||||||
|
|
||||||
|
let name = values.name.to_string();
|
||||||
|
let name = &name[..];
|
||||||
|
|
||||||
|
match_options!(name, values, options, span => [
|
||||||
|
permission_level
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Options { permission_level } = options;
|
||||||
|
|
||||||
|
propagate_err!(create_declaration_validations(&mut fun, DeclarFor::Command));
|
||||||
|
|
||||||
|
let res = parse_quote!(serenity::framework::standard::CommandResult);
|
||||||
|
create_return_type_validation(&mut fun, res);
|
||||||
|
|
||||||
|
let visibility = fun.visibility;
|
||||||
|
let name = fun.name.clone();
|
||||||
|
let body = fun.body;
|
||||||
|
let ret = fun.ret;
|
||||||
|
|
||||||
|
let n = name.with_suffix(COMMAND);
|
||||||
|
|
||||||
|
let cooked = fun.cooked.clone();
|
||||||
|
|
||||||
|
let command_path = quote!(crate::framework::Command);
|
||||||
|
|
||||||
|
populate_fut_lifetimes_on_refs(&mut fun.args);
|
||||||
|
let args = fun.args;
|
||||||
|
|
||||||
|
(quote! {
|
||||||
|
#(#cooked)*
|
||||||
|
pub static #n: #command_path = #command_path {
|
||||||
|
func: #name,
|
||||||
|
name: #lit_name,
|
||||||
|
required_perms: #permission_level,
|
||||||
|
};
|
||||||
|
|
||||||
|
#visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> {
|
||||||
|
use ::serenity::futures::future::FutureExt;
|
||||||
|
|
||||||
|
async move { #(#body)* }.boxed()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
236
regex_command_attr/src/structures.rs
Normal file
236
regex_command_attr/src/structures.rs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
use crate::util::{Argument, Parenthesised};
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{quote, ToTokens};
|
||||||
|
use syn::{
|
||||||
|
braced,
|
||||||
|
parse::{Error, Parse, ParseStream, Result},
|
||||||
|
spanned::Spanned,
|
||||||
|
Attribute, Block, FnArg, Ident, Pat, Path, PathSegment, ReturnType, Stmt, Token, Type,
|
||||||
|
Visibility,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parse_argument(arg: FnArg) -> Result<Argument> {
|
||||||
|
match arg {
|
||||||
|
FnArg::Typed(typed) => {
|
||||||
|
let pat = typed.pat;
|
||||||
|
let kind = typed.ty;
|
||||||
|
|
||||||
|
match *pat {
|
||||||
|
Pat::Ident(id) => {
|
||||||
|
let name = id.ident;
|
||||||
|
let mutable = id.mutability;
|
||||||
|
|
||||||
|
Ok(Argument {
|
||||||
|
mutable,
|
||||||
|
name,
|
||||||
|
kind: *kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Pat::Wild(wild) => {
|
||||||
|
let token = wild.underscore_token;
|
||||||
|
|
||||||
|
let name = Ident::new("_", token.spans[0]);
|
||||||
|
|
||||||
|
Ok(Argument {
|
||||||
|
mutable: None,
|
||||||
|
name,
|
||||||
|
kind: *kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(Error::new(
|
||||||
|
pat.span(),
|
||||||
|
format_args!("unsupported pattern: {:?}", pat),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FnArg::Receiver(_) => Err(Error::new(
|
||||||
|
arg.span(),
|
||||||
|
format_args!("`self` arguments are prohibited: {:?}", arg),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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",
|
||||||
|
];
|
||||||
|
|
||||||
|
COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector.
|
||||||
|
///
|
||||||
|
/// # Return
|
||||||
|
///
|
||||||
|
/// Returns a vector of cooked attributes that have been removed from the input vector.
|
||||||
|
fn remove_cooked(attrs: &mut Vec<Attribute>) -> Vec<Attribute> {
|
||||||
|
let mut cooked = Vec::new();
|
||||||
|
|
||||||
|
// FIXME: Replace with `Vec::drain_filter` once it is stable.
|
||||||
|
let mut i = 0;
|
||||||
|
while i < attrs.len() {
|
||||||
|
if !is_cooked(&attrs[i]) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cooked.push(attrs.remove(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
cooked
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandFun {
|
||||||
|
/// `#[...]`-style attributes.
|
||||||
|
pub attributes: Vec<Attribute>,
|
||||||
|
/// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros
|
||||||
|
/// and will appear in generated output.
|
||||||
|
pub cooked: Vec<Attribute>,
|
||||||
|
pub visibility: Visibility,
|
||||||
|
pub name: Ident,
|
||||||
|
pub args: Vec<Argument>,
|
||||||
|
pub ret: Type,
|
||||||
|
pub body: Vec<Stmt>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for CommandFun {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
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(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cooked = remove_cooked(&mut attributes);
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
return Err(input
|
||||||
|
.error("expected a result type of either `CommandResult` or `CheckResult`"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// { ... }
|
||||||
|
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(Self {
|
||||||
|
attributes,
|
||||||
|
cooked,
|
||||||
|
visibility,
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
ret,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for CommandFun {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
let Self {
|
||||||
|
attributes: _,
|
||||||
|
cooked,
|
||||||
|
visibility,
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
ret,
|
||||||
|
body,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
stream.extend(quote! {
|
||||||
|
#(#cooked)*
|
||||||
|
#visibility async fn #name (#(#args),*) -> #ret {
|
||||||
|
#(#body)*
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PermissionLevel {
|
||||||
|
Unrestricted,
|
||||||
|
Managed,
|
||||||
|
Restricted,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PermissionLevel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Unrestricted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PermissionLevel {
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
Some(match s.to_uppercase().as_str() {
|
||||||
|
"UNRESTRICTED" => Self::Unrestricted,
|
||||||
|
"MANAGED" => Self::Managed,
|
||||||
|
"RESTRICTED" => Self::Restricted,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for PermissionLevel {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
let path = quote!(crate::framework::PermissionLevel);
|
||||||
|
let variant;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::Unrestricted => {
|
||||||
|
variant = quote!(Unrestricted);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Managed => {
|
||||||
|
variant = quote!(Managed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Restricted => {
|
||||||
|
variant = quote!(Restricted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.extend(quote! {
|
||||||
|
#path::#variant
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Options {
|
||||||
|
pub permission_level: PermissionLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
244
regex_command_attr/src/util.rs
Normal file
244
regex_command_attr/src/util.rs
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
use crate::structures::CommandFun;
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote, ToTokens};
|
||||||
|
use syn::{
|
||||||
|
braced, bracketed, parenthesized,
|
||||||
|
parse::{Error, Parse, ParseStream, Result as SynResult},
|
||||||
|
parse_quote,
|
||||||
|
punctuated::Punctuated,
|
||||||
|
spanned::Spanned,
|
||||||
|
token::{Comma, Mut},
|
||||||
|
Ident, Lifetime, Lit, Type,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait LitExt {
|
||||||
|
fn to_str(&self) -> String;
|
||||||
|
fn to_bool(&self) -> bool;
|
||||||
|
fn to_ident(&self) -> Ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LitExt for Lit {
|
||||||
|
fn to_str(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Lit::Str(s) => s.value(),
|
||||||
|
Lit::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) },
|
||||||
|
Lit::Char(c) => c.value().to_string(),
|
||||||
|
Lit::Byte(b) => (b.value() as char).to_string(),
|
||||||
|
_ => panic!("values must be a (byte)string or a char"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bool(&self) -> bool {
|
||||||
|
if let Lit::Bool(b) = self {
|
||||||
|
b.value
|
||||||
|
} else {
|
||||||
|
self.to_str()
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|_| panic!("expected bool from {:?}", self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn to_ident(&self) -> Ident {
|
||||||
|
Ident::new(&self.to_str(), self.span())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IdentExt2: Sized {
|
||||||
|
fn to_uppercase(&self) -> Self;
|
||||||
|
fn with_suffix(&self, suf: &str) -> Ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdentExt2 for Ident {
|
||||||
|
#[inline]
|
||||||
|
fn to_uppercase(&self) -> Self {
|
||||||
|
format_ident!("{}", self.to_string().to_uppercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn with_suffix(&self, suffix: &str) -> Ident {
|
||||||
|
format_ident!("{}_{}", self.to_string().to_uppercase(), suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn into_stream(e: Error) -> TokenStream {
|
||||||
|
e.to_compile_error().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! propagate_err {
|
||||||
|
($res:expr) => {{
|
||||||
|
match $res {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return $crate::util::into_stream(e),
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Bracketed<T>(pub Punctuated<T, Comma>);
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Bracketed<T> {
|
||||||
|
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||||
|
let content;
|
||||||
|
bracketed!(content in input);
|
||||||
|
|
||||||
|
Ok(Bracketed(content.parse_terminated(T::parse)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Braced<T>(pub Punctuated<T, Comma>);
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Braced<T> {
|
||||||
|
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||||
|
let content;
|
||||||
|
braced!(content in input);
|
||||||
|
|
||||||
|
Ok(Braced(content.parse_terminated(T::parse)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Parenthesised<T>(pub Punctuated<T, Comma>);
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Parenthesised<T> {
|
||||||
|
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||||
|
let content;
|
||||||
|
parenthesized!(content in input);
|
||||||
|
|
||||||
|
Ok(Parenthesised(content.parse_terminated(T::parse)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AsOption<T>(pub Option<T>);
|
||||||
|
|
||||||
|
impl<T: ToTokens> ToTokens for AsOption<T> {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
match &self.0 {
|
||||||
|
Some(o) => stream.extend(quote!(Some(#o))),
|
||||||
|
None => stream.extend(quote!(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for AsOption<T> {
|
||||||
|
#[inline]
|
||||||
|
fn default() -> Self {
|
||||||
|
AsOption(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Argument {
|
||||||
|
pub mutable: Option<Mut>,
|
||||||
|
pub name: Ident,
|
||||||
|
pub kind: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for Argument {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
let Argument {
|
||||||
|
mutable,
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
stream.extend(quote! {
|
||||||
|
#mutable #name: #kind
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn generate_type_validation(have: Type, expect: Type) -> syn::Stmt {
|
||||||
|
parse_quote! {
|
||||||
|
serenity::static_assertions::assert_type_eq_all!(#have, #expect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
return Err(Error::new(
|
||||||
|
fun.args.last().unwrap().span(),
|
||||||
|
format_args!("function's arity exceeds more than {} arguments", len),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let context: Type = parse_quote!(&serenity::client::Context);
|
||||||
|
let message: Type = parse_quote!(&serenity::model::channel::Message);
|
||||||
|
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<serenity::model::id::UserId>);
|
||||||
|
|
||||||
|
let mut index = 0;
|
||||||
|
|
||||||
|
let mut spoof_or_check = |kind: Type, name: &str| {
|
||||||
|
match fun.args.get(index) {
|
||||||
|
Some(x) => fun
|
||||||
|
.body
|
||||||
|
.insert(0, generate_type_validation(x.kind.clone(), kind)),
|
||||||
|
None => fun.args.push(Argument {
|
||||||
|
mutable: None,
|
||||||
|
name: Ident::new(name, Span::call_site()),
|
||||||
|
kind,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn create_return_type_validation(r#fn: &mut CommandFun, expect: Type) {
|
||||||
|
let stmt = generate_type_validation(r#fn.ret.clone(), expect);
|
||||||
|
r#fn.body.insert(0, stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn populate_fut_lifetimes_on_refs(args: &mut Vec<Argument>) {
|
||||||
|
for arg in args {
|
||||||
|
if let Type::Reference(reference) = &mut arg.kind {
|
||||||
|
reference.lifetime = Some(Lifetime::new("'fut", Span::call_site()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
339
src/framework.rs
Normal file
339
src/framework.rs
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
use serenity::{
|
||||||
|
async_trait,
|
||||||
|
client::Context,
|
||||||
|
constants::MESSAGE_CODE_LIMIT,
|
||||||
|
framework::{standard::Args, Framework},
|
||||||
|
futures::prelude::future::BoxFuture,
|
||||||
|
http::Http,
|
||||||
|
model::{
|
||||||
|
channel::{Channel, GuildChannel, Message},
|
||||||
|
guild::{Guild, Member},
|
||||||
|
id::ChannelId,
|
||||||
|
},
|
||||||
|
Result as SerenityResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::{error, info, warn};
|
||||||
|
|
||||||
|
use regex::{Match, Regex, RegexBuilder};
|
||||||
|
|
||||||
|
use std::{collections::HashMap, fmt};
|
||||||
|
|
||||||
|
use crate::{guild_data::GuildData, MySQL};
|
||||||
|
use serenity::framework::standard::{CommandResult, Delimiter};
|
||||||
|
|
||||||
|
type CommandFn = for<'fut> fn(&'fut Context, &'fut Message, Args) -> BoxFuture<'fut, CommandResult>;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum PermissionLevel {
|
||||||
|
Unrestricted,
|
||||||
|
Managed,
|
||||||
|
Restricted,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Command {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub required_perms: PermissionLevel,
|
||||||
|
pub func: CommandFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool {
|
||||||
|
if self.required_perms == PermissionLevel::Unrestricted {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap();
|
||||||
|
|
||||||
|
if permissions.manage_guild() && self.required_perms == PermissionLevel::Managed {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.required_perms == PermissionLevel::Managed {
|
||||||
|
let pool = ctx
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<MySQL>()
|
||||||
|
.cloned()
|
||||||
|
.expect("Could not get SQLPool from data");
|
||||||
|
|
||||||
|
match sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT role
|
||||||
|
FROM roles
|
||||||
|
WHERE guild_id = ?
|
||||||
|
",
|
||||||
|
guild.id.as_u64()
|
||||||
|
)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(rows) => {
|
||||||
|
let role_ids = member
|
||||||
|
.roles
|
||||||
|
.iter()
|
||||||
|
.map(|r| *r.as_u64())
|
||||||
|
.collect::<Vec<u64>>();
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
if role_ids.contains(&row.role) || &row.role == guild.id.as_u64() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(sqlx::Error::RowNotFound) => false,
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Unexpected error occurred querying roles: {:?}", e);
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.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 {
|
||||||
|
commands: HashMap<String, &'static Command>,
|
||||||
|
command_matcher: Regex,
|
||||||
|
default_prefix: String,
|
||||||
|
client_id: u64,
|
||||||
|
ignore_bots: bool,
|
||||||
|
case_insensitive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegexFramework {
|
||||||
|
pub fn new<T: Into<u64>>(client_id: T) -> Self {
|
||||||
|
Self {
|
||||||
|
commands: HashMap::new(),
|
||||||
|
command_matcher: Regex::new(r#"^$"#).unwrap(),
|
||||||
|
default_prefix: "".to_string(),
|
||||||
|
client_id: client_id.into(),
|
||||||
|
ignore_bots: true,
|
||||||
|
case_insensitive: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn case_insensitive(mut self, case_insensitive: bool) -> Self {
|
||||||
|
self.case_insensitive = case_insensitive;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_prefix<T: ToString>(mut self, new_prefix: T) -> Self {
|
||||||
|
self.default_prefix = new_prefix.to_string();
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ignore_bots(mut self, ignore_bots: bool) -> Self {
|
||||||
|
self.ignore_bots = ignore_bots;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_command<S: ToString>(mut self, name: S, command: &'static Command) -> Self {
|
||||||
|
self.commands.insert(name.to_string(), command);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(mut self) -> Self {
|
||||||
|
let command_names;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut command_names_vec = self.commands.keys().map(|k| &k[..]).collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
command_names_vec.sort_unstable_by(|a, b| b.len().cmp(&a.len()));
|
||||||
|
|
||||||
|
command_names = command_names_vec.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Command names: {}", command_names);
|
||||||
|
|
||||||
|
{
|
||||||
|
let match_string = r#"^(?:(?:<@ID>\s*)|(?:<@!ID>\s*)|(?P<prefix>\S{1,5}?))(?P<cmd>COMMANDS)(?:$|\s+(?P<args>.*))$"#
|
||||||
|
.replace("COMMANDS", command_names.as_str())
|
||||||
|
.replace("ID", self.client_id.to_string().as_str());
|
||||||
|
|
||||||
|
self.command_matcher = RegexBuilder::new(match_string.as_str())
|
||||||
|
.case_insensitive(self.case_insensitive)
|
||||||
|
.dot_matches_new_line(true)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PermissionCheck {
|
||||||
|
None, // No permissions
|
||||||
|
All, // Sufficient permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Framework for RegexFramework {
|
||||||
|
async fn dispatch(&self, ctx: Context, msg: Message) {
|
||||||
|
async fn check_self_permissions(
|
||||||
|
ctx: &Context,
|
||||||
|
channel: &GuildChannel,
|
||||||
|
) -> SerenityResult<PermissionCheck> {
|
||||||
|
let user_id = ctx.cache.current_user_id().await;
|
||||||
|
|
||||||
|
let channel_perms = channel.permissions_for_user(ctx, user_id).await?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
if channel_perms.send_messages() && channel_perms.embed_links() {
|
||||||
|
PermissionCheck::All
|
||||||
|
} else {
|
||||||
|
PermissionCheck::None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_prefix(ctx: &Context, guild: &Guild, prefix_opt: Option<Match<'_>>) -> bool {
|
||||||
|
if let Some(prefix) = prefix_opt {
|
||||||
|
let pool = ctx
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<MySQL>()
|
||||||
|
.cloned()
|
||||||
|
.expect("Could not get SQLPool from data");
|
||||||
|
|
||||||
|
let guild_prefix = match GuildData::get_from_id(guild.clone(), pool.clone()).await {
|
||||||
|
Some(guild_data) => guild_data.prefix,
|
||||||
|
|
||||||
|
None => {
|
||||||
|
GuildData::create_from_guild(guild, pool).await.unwrap();
|
||||||
|
String::from("?")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
guild_prefix.as_str() == prefix.as_str()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gate to prevent analysing messages unnecessarily
|
||||||
|
if msg.author.bot || msg.content.is_empty() {
|
||||||
|
}
|
||||||
|
// Guild Command
|
||||||
|
else if let (Some(guild), Some(Channel::Guild(channel))) =
|
||||||
|
(msg.guild(&ctx).await, msg.channel(&ctx).await)
|
||||||
|
{
|
||||||
|
if let Some(full_match) = self.command_matcher.captures(&msg.content) {
|
||||||
|
if check_prefix(&ctx, &guild, full_match.name("prefix")).await {
|
||||||
|
match check_self_permissions(&ctx, &channel).await {
|
||||||
|
Ok(perms) => match perms {
|
||||||
|
PermissionCheck::All => {
|
||||||
|
let command = self
|
||||||
|
.commands
|
||||||
|
.get(&full_match.name("cmd").unwrap().as_str().to_lowercase())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let args = full_match
|
||||||
|
.name("args")
|
||||||
|
.map(|m| m.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let member = guild.member(&ctx, &msg.author).await.unwrap();
|
||||||
|
|
||||||
|
if command.check_permissions(&ctx, &guild, &member).await {
|
||||||
|
dbg!(command.name);
|
||||||
|
|
||||||
|
(command.func)(
|
||||||
|
&ctx,
|
||||||
|
&msg,
|
||||||
|
Args::new(&args, &[Delimiter::Single(' ')]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
} else if command.required_perms == PermissionLevel::Restricted {
|
||||||
|
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::Managed {
|
||||||
|
let _ = msg
|
||||||
|
.channel_id
|
||||||
|
.say(&ctx, "You must be an Admin to do this command")
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionCheck::None => {
|
||||||
|
warn!("Missing enough permissions for guild {}", guild.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Error occurred getting permissions in guild {}: {:?}",
|
||||||
|
guild.id, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,7 @@ SELECT id, prefix, volume, allow_greets
|
|||||||
match guild_data {
|
match guild_data {
|
||||||
Ok(g) => Some(g),
|
Ok(g) => Some(g),
|
||||||
|
|
||||||
Err(sqlx::Error::RowNotFound) => Self::create_from_guild(guild, db_pool).await.ok(),
|
Err(sqlx::Error::RowNotFound) => Self::create_from_guild(&guild, db_pool).await.ok(),
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("{:?}", e);
|
println!("{:?}", e);
|
||||||
@ -36,7 +36,7 @@ SELECT id, prefix, volume, allow_greets
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_from_guild(
|
pub async fn create_from_guild(
|
||||||
guild: Guild,
|
guild: &Guild,
|
||||||
db_pool: MySqlPool,
|
db_pool: MySqlPool,
|
||||||
) -> Result<GuildData, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<GuildData, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@ -62,7 +62,7 @@ INSERT IGNORE INTO roles (guild_id, role)
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(GuildData {
|
Ok(GuildData {
|
||||||
id: *guild.id.as_u64(),
|
id: guild.id.as_u64().to_owned(),
|
||||||
prefix: String::from("?"),
|
prefix: String::from("?"),
|
||||||
volume: 100,
|
volume: 100,
|
||||||
allow_greets: true,
|
allow_greets: true,
|
248
src/main.rs
248
src/main.rs
@ -4,18 +4,18 @@ extern crate lazy_static;
|
|||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod guilddata;
|
mod framework;
|
||||||
|
mod guild_data;
|
||||||
mod sound;
|
mod sound;
|
||||||
|
|
||||||
use guilddata::GuildData;
|
use guild_data::GuildData;
|
||||||
use sound::Sound;
|
use sound::Sound;
|
||||||
|
|
||||||
|
use regex_command_attr::command;
|
||||||
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
client::{bridge::gateway::GatewayIntents, Client, Context},
|
client::{bridge::gateway::GatewayIntents, Client, Context},
|
||||||
framework::standard::{
|
framework::standard::{Args, CommandResult},
|
||||||
macros::{check, command, group, hook},
|
|
||||||
Args, CommandError, CommandResult, DispatchError, Reason, StandardFramework,
|
|
||||||
},
|
|
||||||
http::Http,
|
http::Http,
|
||||||
model::{
|
model::{
|
||||||
channel::{Channel, Message},
|
channel::{Channel, Message},
|
||||||
@ -34,12 +34,11 @@ use songbird::{
|
|||||||
Call, SerenityInit,
|
Call, SerenityInit,
|
||||||
};
|
};
|
||||||
|
|
||||||
type CheckResult = Result<(), Reason>;
|
|
||||||
|
|
||||||
use sqlx::mysql::MySqlPool;
|
use sqlx::mysql::MySqlPool;
|
||||||
|
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
|
|
||||||
|
use crate::framework::RegexFramework;
|
||||||
use std::{collections::HashMap, env, sync::Arc, time::Duration};
|
use std::{collections::HashMap, env, sync::Arc, time::Duration};
|
||||||
use tokio::sync::MutexGuard;
|
use tokio::sync::MutexGuard;
|
||||||
|
|
||||||
@ -72,164 +71,6 @@ lazy_static! {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[group]
|
|
||||||
#[commands(
|
|
||||||
info,
|
|
||||||
help,
|
|
||||||
list_sounds,
|
|
||||||
change_public,
|
|
||||||
search_sounds,
|
|
||||||
show_popular_sounds,
|
|
||||||
show_random_sounds,
|
|
||||||
set_greet_sound
|
|
||||||
)]
|
|
||||||
#[checks(self_perm_check)]
|
|
||||||
struct AllUsers;
|
|
||||||
|
|
||||||
#[group]
|
|
||||||
#[commands(play, upload_new_sound, change_volume, delete_sound, stop_playing)]
|
|
||||||
#[checks(self_perm_check, role_check)]
|
|
||||||
struct RoleManagedUsers;
|
|
||||||
|
|
||||||
#[group]
|
|
||||||
#[commands(change_prefix, set_allowed_roles, allow_greet_sounds)]
|
|
||||||
#[checks(self_perm_check, permission_check)]
|
|
||||||
struct PermissionManagedUsers;
|
|
||||||
|
|
||||||
#[check]
|
|
||||||
#[name("self_perm_check")]
|
|
||||||
async fn self_perm_check(ctx: &Context, msg: &Message, _args: &mut Args) -> CheckResult {
|
|
||||||
let channel_o = msg.channel(&ctx).await;
|
|
||||||
|
|
||||||
if let Some(channel_e) = channel_o {
|
|
||||||
if let Channel::Guild(channel) = channel_e {
|
|
||||||
let permissions_r = channel
|
|
||||||
.permissions_for_user(&ctx, &ctx.cache.current_user_id().await)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(permissions) = permissions_r {
|
|
||||||
if permissions.send_messages() && permissions.embed_links() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Reason::Log(
|
|
||||||
"Bot does not have enough permissions".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Reason::Log("No perms found".to_string()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Reason::Log("No DM commands".to_string()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Reason::Log("Channel not available".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[check]
|
|
||||||
#[name("role_check")]
|
|
||||||
async fn role_check(ctx: &Context, msg: &Message, _args: &mut Args) -> CheckResult {
|
|
||||||
async fn check_for_roles(ctx: &&Context, msg: &&Message) -> CheckResult {
|
|
||||||
let pool = ctx
|
|
||||||
.data
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.get::<MySQL>()
|
|
||||||
.cloned()
|
|
||||||
.expect("Could not get SQLPool from data");
|
|
||||||
|
|
||||||
let guild_opt = msg.guild(&ctx).await;
|
|
||||||
|
|
||||||
match guild_opt {
|
|
||||||
Some(guild) => {
|
|
||||||
let member_res = guild.member(*ctx, msg.author.id).await;
|
|
||||||
|
|
||||||
match member_res {
|
|
||||||
Ok(member) => {
|
|
||||||
let user_roles: String = member
|
|
||||||
.roles
|
|
||||||
.iter()
|
|
||||||
.map(|r| (*r.as_u64()).to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
let guild_id = *msg.guild_id.unwrap().as_u64();
|
|
||||||
|
|
||||||
let role_res = sqlx::query!(
|
|
||||||
"
|
|
||||||
SELECT COUNT(1) as count
|
|
||||||
FROM roles
|
|
||||||
WHERE
|
|
||||||
(guild_id = ? AND role IN (?)) OR
|
|
||||||
(role = ?)
|
|
||||||
",
|
|
||||||
guild_id,
|
|
||||||
user_roles,
|
|
||||||
guild_id
|
|
||||||
)
|
|
||||||
.fetch_one(&pool)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match role_res {
|
|
||||||
Ok(role_count) => {
|
|
||||||
if role_count.count > 0 {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Err(Reason::User("User has not got a sufficient role. Use `?roles` to set up role restrictions".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(_) => {
|
|
||||||
Err(Reason::User("User has not got a sufficient role. Use `?roles` to set up role restrictions".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(_) => Err(Reason::User(
|
|
||||||
"Unexpected error looking up user roles".to_string(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None => Err(Reason::User(
|
|
||||||
"Unexpected error looking up guild".to_string(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if perform_permission_check(ctx, &msg).await.is_ok() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
check_for_roles(&ctx, &msg).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[check]
|
|
||||||
#[name("permission_check")]
|
|
||||||
async fn permission_check(ctx: &Context, msg: &Message, _args: &mut Args) -> CheckResult {
|
|
||||||
perform_permission_check(ctx, &msg).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn perform_permission_check(ctx: &Context, msg: &&Message) -> CheckResult {
|
|
||||||
if let Some(guild) = msg.guild(&ctx).await {
|
|
||||||
if guild
|
|
||||||
.member_permissions(&ctx, &msg.author)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.manage_guild()
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Reason::User(String::from(
|
|
||||||
"User needs `Manage Guild` permission",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(Reason::User(String::from("Guild not cached")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create event handler for bot
|
// create event handler for bot
|
||||||
struct Handler;
|
struct Handler;
|
||||||
|
|
||||||
@ -403,28 +244,6 @@ async fn join_channel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[hook]
|
|
||||||
async fn log_errors(_: &Context, m: &Message, cmd_name: &str, error: Result<(), CommandError>) {
|
|
||||||
if let Err(e) = error {
|
|
||||||
println!("Error in command {} ({}): {:?}", cmd_name, m.content, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[hook]
|
|
||||||
async fn dispatch_error_hook(ctx: &Context, msg: &Message, error: DispatchError) {
|
|
||||||
match error {
|
|
||||||
DispatchError::CheckFailed(_f, reason) => {
|
|
||||||
if let Reason::User(description) = reason {
|
|
||||||
let _ = msg
|
|
||||||
.reply(ctx, format!("You cannot do this command: {}", description))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// entry point
|
// entry point
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
@ -436,46 +255,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
|
|
||||||
let logged_in_id = http.get_current_user().await?.id;
|
let logged_in_id = http.get_current_user().await?.id;
|
||||||
|
|
||||||
let framework = StandardFramework::new()
|
let framework = RegexFramework::new(logged_in_id)
|
||||||
.configure(|c| {
|
.default_prefix("?")
|
||||||
c.dynamic_prefix(|ctx, msg| {
|
.case_insensitive(true)
|
||||||
Box::pin(async move {
|
.ignore_bots(true)
|
||||||
let pool = ctx
|
// info commands
|
||||||
.data
|
.add_command("help", &HELP_COMMAND)
|
||||||
.read()
|
.add_command("info", &INFO_COMMAND)
|
||||||
.await
|
.add_command("invite", &INFO_COMMAND)
|
||||||
.get::<MySQL>()
|
.add_command("donate", &INFO_COMMAND)
|
||||||
.cloned()
|
.build();
|
||||||
.expect("Could not get SQLPool from data");
|
|
||||||
|
|
||||||
let guild = match msg.guild(&ctx.cache).await {
|
|
||||||
Some(guild) => guild,
|
|
||||||
|
|
||||||
None => {
|
|
||||||
return Some(String::from("?"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match GuildData::get_from_id(guild.clone(), pool.clone()).await {
|
|
||||||
Some(guild_data) => Some(guild_data.prefix),
|
|
||||||
|
|
||||||
None => {
|
|
||||||
GuildData::create_from_guild(guild, pool).await.unwrap();
|
|
||||||
Some(String::from("?"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.allow_dm(false)
|
|
||||||
.ignore_bots(true)
|
|
||||||
.ignore_webhooks(true)
|
|
||||||
.on_mention(Some(logged_in_id))
|
|
||||||
})
|
|
||||||
.group(&ALLUSERS_GROUP)
|
|
||||||
.group(&ROLEMANAGEDUSERS_GROUP)
|
|
||||||
.group(&PERMISSIONMANAGEDUSERS_GROUP)
|
|
||||||
.after(log_errors)
|
|
||||||
.on_dispatch_error(dispatch_error_hook);
|
|
||||||
|
|
||||||
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"))
|
||||||
@ -518,8 +307,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command("play")]
|
#[command]
|
||||||
#[aliases("p")]
|
|
||||||
async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
let guild = match msg.guild(&ctx.cache).await {
|
let guild = match msg.guild(&ctx.cache).await {
|
||||||
Some(guild) => guild,
|
Some(guild) => guild,
|
||||||
|
@ -130,7 +130,7 @@ SELECT src
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
return record.src;
|
record.src
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn store_sound_source(
|
pub async fn store_sound_source(
|
||||||
|
Loading…
Reference in New Issue
Block a user