removed language_manager.rs. framework reworked for slash commands. updated info commands for new framework
This commit is contained in:
parent
98aed91d21
commit
c148cdf556
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1248,7 +1248,7 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex_command_attr"
|
name = "regex_command_attr"
|
||||||
version = "0.2.0"
|
version = "0.3.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "regex_command_attr"
|
name = "regex_command_attr"
|
||||||
version = "0.2.0"
|
version = "0.3.6"
|
||||||
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
|
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Procedural macros for command creation for the RegexFramework for serenity."
|
description = "Procedural macros for command creation for the Serenity library."
|
||||||
|
license = "ISC"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
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};
|
use std::fmt::{self, Write};
|
||||||
|
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use syn::{
|
||||||
|
parse::{Error, Result},
|
||||||
|
spanned::Spanned,
|
||||||
|
Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
structures::{ApplicationCommandOptionType, Arg, PermissionLevel},
|
||||||
|
util::{AsOption, LitExt},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum ValueKind {
|
pub enum ValueKind {
|
||||||
// #[<name>]
|
// #[<name>]
|
||||||
@ -19,6 +23,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 +36,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 +72,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 +95,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,15 +124,19 @@ 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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if is_list_or_named_list(nested.first().unwrap()) == ValueKind::List {
|
||||||
let mut lits = Vec::with_capacity(nested.len());
|
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::Lit(l) => lits.push((None, l)),
|
||||||
|
// catch if the nested value is a meta value
|
||||||
NestedMeta::Meta(m) => match m {
|
NestedMeta::Meta(m) => match m {
|
||||||
|
// path => some quoted value
|
||||||
Meta::Path(path) => {
|
Meta::Path(path) => {
|
||||||
let i = to_ident(path)?;
|
let i = to_ident(path)?;
|
||||||
lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span())))
|
lits.push((None, Lit::Str(LitStr::new(&i.to_string(), i.span()))))
|
||||||
}
|
}
|
||||||
Meta::List(_) | Meta::NameValue(_) => {
|
Meta::List(_) | Meta::NameValue(_) => {
|
||||||
return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level"))
|
return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level"))
|
||||||
@ -120,12 +152,43 @@ pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Values::new(name, kind, lits, attr.span()))
|
Ok(Values::new(name, kind, lits, attr.span()))
|
||||||
|
} else {
|
||||||
|
let mut lits = Vec::with_capacity(nested.len());
|
||||||
|
|
||||||
|
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 +257,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 +267,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 +276,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 +285,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,15 +294,22 @@ 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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributeOption for Option<String> {
|
impl AttributeOption for Option<String> {
|
||||||
fn parse(values: Values) -> Result<Self> {
|
fn parse(values: Values) -> Result<Self> {
|
||||||
validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?;
|
validate(
|
||||||
|
&values,
|
||||||
|
&[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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +317,44 @@ impl AttributeOption for PermissionLevel {
|
|||||||
fn parse(values: Values) -> Result<Self> {
|
fn parse(values: Values) -> Result<Self> {
|
||||||
validate(&values, &[ValueKind::SingleList])?;
|
validate(&values, &[ValueKind::SingleList])?;
|
||||||
|
|
||||||
Ok(values.literals.get(0).map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap()).unwrap())
|
Ok(values
|
||||||
|
.literals
|
||||||
|
.get(0)
|
||||||
|
.map(|(_, l)| PermissionLevel::from_str(&*l.to_str()).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();
|
||||||
|
}
|
||||||
|
"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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +372,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::*;
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
#![deny(rust_2018_idioms)]
|
#![deny(rust_2018_idioms)]
|
||||||
// FIXME: Remove this in a foreseeable future.
|
#![deny(broken_intra_doc_links)]
|
||||||
// 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 proc_macro::TokenStream;
|
||||||
|
use proc_macro2::Ident;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{parse::Error, parse_macro_input, spanned::Spanned, Lit};
|
use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit, Type};
|
||||||
|
|
||||||
pub(crate) mod attributes;
|
pub(crate) mod attributes;
|
||||||
pub(crate) mod consts;
|
pub(crate) mod consts;
|
||||||
@ -41,7 +37,7 @@ macro_rules! match_options {
|
|||||||
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);
|
||||||
|
|
||||||
let lit_name = if !attr.is_empty() {
|
let _name = if !attr.is_empty() {
|
||||||
parse_macro_input!(attr as Lit).to_str()
|
parse_macro_input!(attr as Lit).to_str()
|
||||||
} else {
|
} else {
|
||||||
fun.name.to_string()
|
fun.name.to_string()
|
||||||
@ -56,17 +52,40 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
let name = values.name.to_string();
|
let name = values.name.to_string();
|
||||||
let name = &name[..];
|
let name = &name[..];
|
||||||
|
|
||||||
|
match name {
|
||||||
|
"arg" => options
|
||||||
|
.cmd_args
|
||||||
|
.push(propagate_err!(attributes::parse(values))),
|
||||||
|
"example" => {
|
||||||
|
options
|
||||||
|
.examples
|
||||||
|
.push(propagate_err!(attributes::parse(values)));
|
||||||
|
}
|
||||||
|
"description" => {
|
||||||
|
let line: String = propagate_err!(attributes::parse(values));
|
||||||
|
util::append_line(&mut options.description, line);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
match_options!(name, values, options, span => [
|
match_options!(name, values, options, span => [
|
||||||
permission_level;
|
aliases;
|
||||||
supports_dm;
|
group;
|
||||||
can_blacklist
|
required_permissions;
|
||||||
|
can_blacklist;
|
||||||
|
supports_dm
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let Options {
|
let Options {
|
||||||
permission_level,
|
aliases,
|
||||||
supports_dm,
|
description,
|
||||||
|
group,
|
||||||
|
examples,
|
||||||
|
required_permissions,
|
||||||
can_blacklist,
|
can_blacklist,
|
||||||
|
supports_dm,
|
||||||
|
mut cmd_args,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
let visibility = fun.visibility;
|
let visibility = fun.visibility;
|
||||||
@ -78,25 +97,88 @@ 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.replace(" ", "_").replace("-", "_").as_str())
|
||||||
|
.with_suffix(ARG)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Ident>>();
|
||||||
|
|
||||||
|
let mut tokens = cmd_args
|
||||||
|
.iter_mut()
|
||||||
|
.map(|arg| {
|
||||||
|
let Arg {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
kind,
|
||||||
|
required,
|
||||||
|
} = arg;
|
||||||
|
|
||||||
|
let an = n.with_suffix(name.as_str()).with_suffix(ARG);
|
||||||
|
|
||||||
|
quote! {
|
||||||
#(#cooked)*
|
#(#cooked)*
|
||||||
pub static #n: #command_path = #command_path {
|
#[allow(missing_docs)]
|
||||||
func: #name,
|
pub static #an: #arg_path = #arg_path {
|
||||||
name: #lit_name,
|
name: #name,
|
||||||
required_perms: #permission_level,
|
description: #description,
|
||||||
supports_dm: #supports_dm,
|
kind: #kind,
|
||||||
can_blacklist: #can_blacklist,
|
required: #required,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fold(quote! {}, |mut a, b| {
|
||||||
|
a.extend(b);
|
||||||
|
a
|
||||||
|
});
|
||||||
|
|
||||||
|
let variant = if args.len() == 2 {
|
||||||
|
quote!(crate::framework::CommandFnType::Multi)
|
||||||
|
} else {
|
||||||
|
let string: Type = parse_quote!(std::string::String);
|
||||||
|
|
||||||
|
let final_arg = args.get(2).unwrap();
|
||||||
|
|
||||||
|
if final_arg.kind == string {
|
||||||
|
quote!(crate::framework::CommandFnType::Text)
|
||||||
|
} else {
|
||||||
|
quote!(crate::framework::CommandFnType::Slash)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tokens.extend(quote! {
|
||||||
|
#(#cooked)*
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub static #n: #command_path = #command_path {
|
||||||
|
fun: #variant(#name),
|
||||||
|
names: &[#_name, #(#aliases),*],
|
||||||
|
desc: #description,
|
||||||
|
group: #group,
|
||||||
|
examples: &[#(#examples),*],
|
||||||
|
required_permissions: #required_permissions,
|
||||||
|
can_blacklist: #can_blacklist,
|
||||||
|
supports_dm: #supports_dm,
|
||||||
|
args: &[#(&#arg_idents),*],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
tokens.extend(quote! {
|
||||||
|
#(#cooked)*
|
||||||
|
#[allow(missing_docs)]
|
||||||
#visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
|
#visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
|
||||||
use ::serenity::futures::future::FutureExt;
|
use ::serenity::futures::future::FutureExt;
|
||||||
|
|
||||||
async move { #(#body)* }.boxed()
|
async move {
|
||||||
|
#(#body)*;
|
||||||
|
}.boxed()
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.into()
|
|
||||||
|
tokens.into()
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
use crate::util::{Argument, Parenthesised};
|
|
||||||
use proc_macro2::Span;
|
|
||||||
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},
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
Attribute, Block, FnArg, Ident, Pat, Path, PathSegment, Stmt, Token, Visibility,
|
Attribute, Block, FnArg, Ident, Pat, Stmt, Token, Visibility,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::util::{Argument, Parenthesised};
|
||||||
|
|
||||||
fn parse_argument(arg: FnArg) -> Result<Argument> {
|
fn parse_argument(arg: FnArg) -> Result<Argument> {
|
||||||
match arg {
|
match arg {
|
||||||
FnArg::Typed(typed) => {
|
FnArg::Typed(typed) => {
|
||||||
@ -53,7 +53,7 @@ fn parse_argument(arg: FnArg) -> Result<Argument> {
|
|||||||
/// Test if the attribute is cooked.
|
/// Test if the attribute is cooked.
|
||||||
fn is_cooked(attr: &Attribute) -> bool {
|
fn is_cooked(attr: &Attribute) -> bool {
|
||||||
const COOKED_ATTRIBUTE_NAMES: &[&str] = &[
|
const COOKED_ATTRIBUTE_NAMES: &[&str] = &[
|
||||||
"cfg", "cfg_attr", "doc", "derive", "inline", "allow", "warn", "deny", "forbid",
|
"cfg", "cfg_attr", "derive", "inline", "allow", "warn", "deny", "forbid",
|
||||||
];
|
];
|
||||||
|
|
||||||
COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
|
COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
|
||||||
@ -98,17 +98,6 @@ impl Parse for CommandFun {
|
|||||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
let mut attributes = input.call(Attribute::parse_outer)?;
|
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 cooked = remove_cooked(&mut attributes);
|
||||||
|
|
||||||
let visibility = input.parse::<Visibility>()?;
|
let visibility = input.parse::<Visibility>()?;
|
||||||
@ -155,7 +144,7 @@ impl ToTokens for CommandFun {
|
|||||||
|
|
||||||
stream.extend(quote! {
|
stream.extend(quote! {
|
||||||
#(#cooked)*
|
#(#cooked)*
|
||||||
#visibility async fn #name (#(#args),*) -> () {
|
#visibility async fn #name (#(#args),*) {
|
||||||
#(#body)*
|
#(#body)*
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -211,21 +200,98 @@ 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::application_command::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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Arg {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
name: String::new(),
|
||||||
|
description: String::new(),
|
||||||
|
kind: ApplicationCommandOptionType::String,
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Options {
|
pub(crate) struct Options {
|
||||||
pub permission_level: PermissionLevel,
|
pub aliases: Vec<String>,
|
||||||
pub supports_dm: bool,
|
pub description: String,
|
||||||
|
pub group: String,
|
||||||
|
pub examples: Vec<String>,
|
||||||
|
pub required_permissions: PermissionLevel,
|
||||||
pub can_blacklist: bool,
|
pub can_blacklist: bool,
|
||||||
|
pub supports_dm: bool,
|
||||||
|
pub cmd_args: Vec<Arg>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Options {
|
impl Options {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut options = Self::default();
|
Self {
|
||||||
|
group: "Other".to_string(),
|
||||||
options.can_blacklist = true;
|
..Default::default()
|
||||||
options.supports_dm = true;
|
}
|
||||||
|
|
||||||
options
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::Span;
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::{format_ident, quote, ToTokens};
|
use quote::{format_ident, quote, ToTokens};
|
||||||
use syn::{
|
use syn::{
|
||||||
braced, bracketed, parenthesized,
|
braced, bracketed, parenthesized,
|
||||||
@ -158,3 +157,20 @@ pub fn populate_fut_lifetimes_on_refs(args: &mut Vec<Argument>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn append_line(desc: &mut String, mut line: String) {
|
||||||
|
if line.starts_with(' ') {
|
||||||
|
line.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
match line.rfind("\\$") {
|
||||||
|
Some(i) => {
|
||||||
|
desc.push_str(line[..i].trim_end());
|
||||||
|
desc.push(' ');
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
desc.push_str(&line);
|
||||||
|
desc.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
imports_granularity = "Crate"
|
||||||
|
group_imports = "StdExternalCrate"
|
@ -1,40 +1,20 @@
|
|||||||
use regex_command_attr::command;
|
|
||||||
|
|
||||||
use serenity::{builder::CreateEmbedFooter, client::Context, model::channel::Message};
|
|
||||||
|
|
||||||
use chrono::offset::Utc;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
command_help,
|
|
||||||
consts::DEFAULT_PREFIX,
|
|
||||||
get_ctx_data,
|
|
||||||
language_manager::LanguageManager,
|
|
||||||
models::{user_data::UserData, CtxData},
|
|
||||||
FrameworkCtx, THEME_COLOR,
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[command]
|
use chrono::offset::Utc;
|
||||||
#[can_blacklist(false)]
|
use regex_command_attr::command;
|
||||||
async fn ping(ctx: &Context, msg: &Message, _args: String) {
|
use serenity::{builder::CreateEmbedFooter, client::Context, model::channel::Message};
|
||||||
let now = SystemTime::now();
|
|
||||||
let since_epoch = now
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.expect("Time calculated as going backwards. Very bad");
|
|
||||||
|
|
||||||
let delta = since_epoch.as_millis() as i64 - msg.timestamp.timestamp_millis();
|
use crate::{
|
||||||
|
consts::DEFAULT_PREFIX,
|
||||||
|
framework::{CommandInvoke, CreateGenericResponse},
|
||||||
|
models::{user_data::UserData, CtxData},
|
||||||
|
FrameworkCtx, THEME_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
let _ = msg
|
fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
|
||||||
.channel_id
|
|
||||||
.say(&ctx, format!("Time taken to receive message: {}ms", delta))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
|
|
||||||
let shard_count = ctx.cache.shard_count();
|
let shard_count = ctx.cache.shard_count();
|
||||||
let shard = ctx.shard_id;
|
let shard = ctx.shard_id;
|
||||||
|
|
||||||
@ -49,173 +29,105 @@ async fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut Cr
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[can_blacklist(false)]
|
#[aliases("invite")]
|
||||||
async fn help(ctx: &Context, msg: &Message, args: String) {
|
#[description("Get information about the bot")]
|
||||||
async fn default_help(
|
#[group("Info")]
|
||||||
ctx: &Context,
|
async fn info(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) {
|
||||||
msg: &Message,
|
let prefix = ctx.prefix(invoke.guild_id()).await;
|
||||||
lm: Arc<LanguageManager>,
|
|
||||||
prefix: &str,
|
|
||||||
language: &str,
|
|
||||||
) {
|
|
||||||
let desc = lm.get(language, "help/desc").replace("{prefix}", prefix);
|
|
||||||
let footer = footer(ctx).await;
|
|
||||||
|
|
||||||
let _ = msg
|
|
||||||
.channel_id
|
|
||||||
.send_message(ctx, |m| {
|
|
||||||
m.embed(move |e| {
|
|
||||||
e.title("Help Menu")
|
|
||||||
.description(desc)
|
|
||||||
.field(
|
|
||||||
lm.get(language, "help/setup_title"),
|
|
||||||
"`lang` `timezone` `meridian`",
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.field(
|
|
||||||
lm.get(language, "help/mod_title"),
|
|
||||||
"`prefix` `blacklist` `restrict` `alias`",
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.field(
|
|
||||||
lm.get(language, "help/reminder_title"),
|
|
||||||
"`remind` `interval` `natural` `look` `countdown`",
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.field(
|
|
||||||
lm.get(language, "help/reminder_mod_title"),
|
|
||||||
"`del` `offset` `pause` `nudge`",
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.field(
|
|
||||||
lm.get(language, "help/info_title"),
|
|
||||||
"`help` `info` `donate` `clock`",
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.field(
|
|
||||||
lm.get(language, "help/todo_title"),
|
|
||||||
"`todo` `todos` `todoc`",
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.field(lm.get(language, "help/other_title"), "`timer`", true)
|
|
||||||
.footer(footer)
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (pool, lm) = get_ctx_data(&ctx).await;
|
|
||||||
|
|
||||||
let language = UserData::language_of(&msg.author, &pool);
|
|
||||||
let prefix = ctx.prefix(msg.guild_id);
|
|
||||||
|
|
||||||
if !args.is_empty() {
|
|
||||||
let framework = ctx
|
|
||||||
.data
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.get::<FrameworkCtx>()
|
|
||||||
.cloned()
|
|
||||||
.expect("Could not get FrameworkCtx from data");
|
|
||||||
|
|
||||||
let matched = framework
|
|
||||||
.commands
|
|
||||||
.get(args.as_str())
|
|
||||||
.map(|inner| inner.name);
|
|
||||||
|
|
||||||
if let Some(command_name) = matched {
|
|
||||||
command_help(ctx, msg, lm, &prefix.await, &language.await, command_name).await
|
|
||||||
} else {
|
|
||||||
default_help(ctx, msg, lm, &prefix.await, &language.await).await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
default_help(ctx, msg, lm, &prefix.await, &language.await).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
async fn info(ctx: &Context, msg: &Message, _args: String) {
|
|
||||||
let (pool, lm) = get_ctx_data(&ctx).await;
|
|
||||||
|
|
||||||
let language = UserData::language_of(&msg.author, &pool);
|
|
||||||
let prefix = ctx.prefix(msg.guild_id);
|
|
||||||
let current_user = ctx.cache.current_user();
|
let current_user = ctx.cache.current_user();
|
||||||
let footer = footer(ctx).await;
|
let footer = footer(ctx);
|
||||||
|
|
||||||
let desc = lm
|
invoke
|
||||||
.get(&language.await, "info")
|
.respond(
|
||||||
.replacen("{user}", ¤t_user.name, 1)
|
ctx.http.clone(),
|
||||||
.replace("{default_prefix}", &*DEFAULT_PREFIX)
|
CreateGenericResponse::new().embed(|e| {
|
||||||
.replace("{prefix}", &prefix.await);
|
|
||||||
|
|
||||||
let _ = msg
|
|
||||||
.channel_id
|
|
||||||
.send_message(ctx, |m| {
|
|
||||||
m.embed(move |e| {
|
|
||||||
e.title("Info")
|
e.title("Info")
|
||||||
.description(desc)
|
.description(format!(
|
||||||
|
"Default prefix: `{default_prefix}`
|
||||||
|
Reset prefix: `@{user} prefix {default_prefix}`
|
||||||
|
Help: `{prefix}help`
|
||||||
|
|
||||||
|
**Welcome to Reminder Bot!**
|
||||||
|
Developer: <@203532103185465344>
|
||||||
|
Icon: <@253202252821430272>
|
||||||
|
Find me on https://discord.jellywx.com and on https://github.com/JellyWX :)
|
||||||
|
|
||||||
|
Invite the bot: https://invite.reminder-bot.com/
|
||||||
|
Use our dashboard: https://reminder-bot.com/",
|
||||||
|
default_prefix = *DEFAULT_PREFIX,
|
||||||
|
user = current_user.name,
|
||||||
|
prefix = prefix
|
||||||
|
))
|
||||||
.footer(footer)
|
.footer(footer)
|
||||||
.color(*THEME_COLOR)
|
.color(*THEME_COLOR)
|
||||||
})
|
}),
|
||||||
})
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
async fn donate(ctx: &Context, msg: &Message, _args: String) {
|
#[description("Details on supporting the bot and Patreon benefits")]
|
||||||
let (pool, lm) = get_ctx_data(&ctx).await;
|
#[group("Info")]
|
||||||
|
async fn donate(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) {
|
||||||
let language = UserData::language_of(&msg.author, &pool).await;
|
let footer = footer(ctx);
|
||||||
let desc = lm.get(&language, "donate");
|
|
||||||
let footer = footer(ctx).await;
|
invoke
|
||||||
|
.respond(
|
||||||
let _ = msg
|
ctx.http.clone(),
|
||||||
.channel_id
|
CreateGenericResponse::new().embed(|e| {
|
||||||
.send_message(ctx, |m| {
|
e.title("Donate")
|
||||||
m.embed(move |e| {
|
.description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :)
|
||||||
e.title("Donate")
|
|
||||||
.description(desc)
|
**https://www.patreon.com/jellywx/**
|
||||||
.footer(footer)
|
**https://discord.jellywx.com/**
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
When you subscribe, Patreon will automatically rank you up on our Discord server (make sure you link your Patreon and Discord accounts!)
|
||||||
})
|
With your new rank, you'll be able to:
|
||||||
.await;
|
• Set repeating reminders with `interval`, `natural` or the dashboard
|
||||||
}
|
• Use unlimited uploads on SoundFX
|
||||||
|
|
||||||
#[command]
|
(Also, members of servers you __own__ will be able to set repeating reminders via commands)
|
||||||
async fn dashboard(ctx: &Context, msg: &Message, _args: String) {
|
|
||||||
let footer = footer(ctx).await;
|
Just $2 USD/month!
|
||||||
|
|
||||||
let _ = msg
|
*Please note, you must be in the JellyWX Discord server to receive Patreon features*")
|
||||||
.channel_id
|
.footer(footer)
|
||||||
.send_message(ctx, |m| {
|
.color(*THEME_COLOR)
|
||||||
m.embed(move |e| {
|
}),
|
||||||
e.title("Dashboard")
|
)
|
||||||
.description("https://reminder-bot.com/dashboard")
|
.await;
|
||||||
.footer(footer)
|
}
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
#[command]
|
||||||
})
|
#[description("Get the link to the online dashboard")]
|
||||||
.await;
|
#[group("Info")]
|
||||||
}
|
async fn dashboard(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) {
|
||||||
|
let footer = footer(ctx);
|
||||||
#[command]
|
|
||||||
async fn clock(ctx: &Context, msg: &Message, _args: String) {
|
invoke
|
||||||
let (pool, lm) = get_ctx_data(&ctx).await;
|
.respond(
|
||||||
|
ctx.http.clone(),
|
||||||
let language = UserData::language_of(&msg.author, &pool).await;
|
CreateGenericResponse::new().embed(|e| {
|
||||||
let timezone = UserData::timezone_of(&msg.author, &pool).await;
|
e.title("Dashboard")
|
||||||
|
.description("**https://reminder-bot.com/dashboard**")
|
||||||
let now = Utc::now().with_timezone(&timezone);
|
.footer(footer)
|
||||||
|
.color(*THEME_COLOR)
|
||||||
let clock_display = lm.get(&language, "clock/time");
|
}),
|
||||||
|
)
|
||||||
let _ = msg
|
.await;
|
||||||
.channel_id
|
}
|
||||||
.say(
|
|
||||||
&ctx,
|
#[command]
|
||||||
clock_display.replacen("{}", &now.format("%H:%M").to_string(), 1),
|
#[description("View the current time in your selected timezone")]
|
||||||
|
#[group("Info")]
|
||||||
|
async fn clock(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) {
|
||||||
|
let ud = ctx.user_data(&msg.author).await.unwrap();
|
||||||
|
let now = Utc::now().with_timezone(ud.timezone());
|
||||||
|
|
||||||
|
invoke
|
||||||
|
.respond(
|
||||||
|
ctx.http.clone(),
|
||||||
|
CreateGenericResponse::new().content(format!("Current time: {}", now.format("%H:%M"))),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
pub mod info_cmds;
|
pub mod info_cmds;
|
||||||
pub mod moderation_cmds;
|
//pub mod moderation_cmds;
|
||||||
pub mod reminder_cmds;
|
//pub mod reminder_cmds;
|
||||||
pub mod todo_cmds;
|
//pub mod todo_cmds;
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
use regex_command_attr::command;
|
use std::{collections::HashMap, iter};
|
||||||
|
|
||||||
|
use chrono::offset::Utc;
|
||||||
|
use chrono_tz::{Tz, TZ_VARIANTS};
|
||||||
|
use inflector::Inflector;
|
||||||
|
use levenshtein::levenshtein;
|
||||||
|
use regex_command_attr::command;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
builder::CreateActionRow,
|
builder::CreateActionRow,
|
||||||
client::Context,
|
client::Context,
|
||||||
@ -11,14 +16,6 @@ use serenity::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono_tz::{Tz, TZ_VARIANTS};
|
|
||||||
|
|
||||||
use chrono::offset::Utc;
|
|
||||||
|
|
||||||
use inflector::Inflector;
|
|
||||||
|
|
||||||
use levenshtein::levenshtein;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
command_help,
|
command_help,
|
||||||
consts::{REGEX_ALIAS, REGEX_CHANNEL, REGEX_COMMANDS, REGEX_ROLE, THEME_COLOR},
|
consts::{REGEX_ALIAS, REGEX_CHANNEL, REGEX_COMMANDS, REGEX_ROLE, THEME_COLOR},
|
||||||
@ -28,8 +25,6 @@ use crate::{
|
|||||||
FrameworkCtx, PopularTimezones,
|
FrameworkCtx, PopularTimezones,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{collections::HashMap, iter};
|
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[supports_dm(false)]
|
#[supports_dm(false)]
|
||||||
#[permission_level(Restricted)]
|
#[permission_level(Restricted)]
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
use regex_command_attr::command;
|
use std::{
|
||||||
|
default::Default,
|
||||||
|
string::ToString,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use num_integer::Integer;
|
||||||
|
use regex_command_attr::command;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
client::Context,
|
client::Context,
|
||||||
model::{channel::Channel, channel::Message},
|
model::channel::{Channel, Message},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -16,7 +23,12 @@ use crate::{
|
|||||||
models::{
|
models::{
|
||||||
channel_data::ChannelData,
|
channel_data::ChannelData,
|
||||||
guild_data::GuildData,
|
guild_data::GuildData,
|
||||||
reminder::{builder::ReminderScope, content::Content, look_flags::LookFlags, Reminder},
|
reminder::{
|
||||||
|
builder::{MultiReminderBuilder, ReminderScope},
|
||||||
|
content::Content,
|
||||||
|
look_flags::LookFlags,
|
||||||
|
Reminder,
|
||||||
|
},
|
||||||
timer::Timer,
|
timer::Timer,
|
||||||
user_data::UserData,
|
user_data::UserData,
|
||||||
CtxData,
|
CtxData,
|
||||||
@ -24,17 +36,6 @@ use crate::{
|
|||||||
time_parser::{natural_parser, TimeParser},
|
time_parser::{natural_parser, TimeParser},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
|
|
||||||
use num_integer::Integer;
|
|
||||||
|
|
||||||
use crate::models::reminder::builder::MultiReminderBuilder;
|
|
||||||
use std::{
|
|
||||||
default::Default,
|
|
||||||
string::ToString,
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[supports_dm(false)]
|
#[supports_dm(false)]
|
||||||
#[permission_level(Restricted)]
|
#[permission_level(Restricted)]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use regex_command_attr::command;
|
use std::{convert::TryFrom, fmt};
|
||||||
|
|
||||||
|
use regex_command_attr::command;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
client::Context,
|
client::Context,
|
||||||
@ -9,15 +10,12 @@ use serenity::{
|
|||||||
id::{ChannelId, GuildId, UserId},
|
id::{ChannelId, GuildId, UserId},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use sqlx::MySqlPool;
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
command_help, get_ctx_data,
|
command_help, get_ctx_data,
|
||||||
models::{user_data::UserData, CtxData},
|
models::{user_data::UserData, CtxData},
|
||||||
};
|
};
|
||||||
use sqlx::MySqlPool;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct TodoNotFound;
|
struct TodoNotFound;
|
||||||
|
@ -74,9 +74,6 @@ lazy_static! {
|
|||||||
pub static ref LOCAL_TIMEZONE: String =
|
pub static ref LOCAL_TIMEZONE: String =
|
||||||
env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
|
env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
|
||||||
|
|
||||||
pub static ref LOCAL_LANGUAGE: String =
|
|
||||||
env::var("LOCAL_LANGUAGE").unwrap_or_else(|_| "EN".to_string());
|
|
||||||
|
|
||||||
pub static ref DEFAULT_PREFIX: String =
|
pub static ref DEFAULT_PREFIX: String =
|
||||||
env::var("DEFAULT_PREFIX").unwrap_or_else(|_| "$".to_string());
|
env::var("DEFAULT_PREFIX").unwrap_or_else(|_| "$".to_string());
|
||||||
|
|
||||||
|
511
src/framework.rs
511
src/framework.rs
@ -1,32 +1,36 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::{error, info, warn};
|
||||||
|
use regex::{Match, Regex, RegexBuilder};
|
||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
|
builder::{CreateComponents, CreateEmbed},
|
||||||
|
cache::Cache,
|
||||||
client::Context,
|
client::Context,
|
||||||
constants::MESSAGE_CODE_LIMIT,
|
|
||||||
framework::Framework,
|
framework::Framework,
|
||||||
futures::prelude::future::BoxFuture,
|
futures::prelude::future::BoxFuture,
|
||||||
http::Http,
|
http::Http,
|
||||||
model::{
|
model::{
|
||||||
channel::{Channel, GuildChannel, Message},
|
channel::{Channel, GuildChannel, Message},
|
||||||
guild::{Guild, Member},
|
guild::{Guild, Member},
|
||||||
id::{ChannelId, MessageId},
|
id::{ChannelId, GuildId, MessageId, UserId},
|
||||||
|
interactions::{
|
||||||
|
application_command::{ApplicationCommandInteraction, ApplicationCommandOptionType},
|
||||||
|
InteractionResponseType,
|
||||||
},
|
},
|
||||||
Result as SerenityResult,
|
},
|
||||||
|
FutureExt, Result as SerenityResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::{error, info, warn};
|
|
||||||
|
|
||||||
use regex::{Match, Regex, RegexBuilder};
|
|
||||||
|
|
||||||
use std::{collections::HashMap, fmt};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
language_manager::LanguageManager,
|
models::{channel_data::ChannelData, guild_data::GuildData, CtxData},
|
||||||
models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData, CtxData},
|
|
||||||
LimitExecutors, SQLPool,
|
LimitExecutors, SQLPool,
|
||||||
};
|
};
|
||||||
|
|
||||||
type CommandFn = for<'fut> fn(&'fut Context, &'fut Message, String) -> BoxFuture<'fut, ()>;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum PermissionLevel {
|
pub enum PermissionLevel {
|
||||||
Unrestricted,
|
Unrestricted,
|
||||||
@ -34,29 +38,334 @@ pub enum PermissionLevel {
|
|||||||
Restricted,
|
Restricted,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Command {
|
pub struct Args {
|
||||||
pub name: &'static str,
|
pub args: HashMap<String, String>,
|
||||||
pub required_perms: PermissionLevel,
|
|
||||||
pub supports_dm: bool,
|
|
||||||
pub can_blacklist: bool,
|
|
||||||
pub func: CommandFn,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Args {
|
||||||
|
pub fn named<D: ToString>(&self, name: D) -> Option<&String> {
|
||||||
|
let name = name.to_string();
|
||||||
|
|
||||||
|
self.args.get(&name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateGenericResponse {
|
||||||
|
content: String,
|
||||||
|
embed: Option<CreateEmbed>,
|
||||||
|
components: Option<CreateComponents>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateGenericResponse {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
content: "".to_string(),
|
||||||
|
embed: None,
|
||||||
|
components: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content<D: ToString>(mut self, content: D) -> Self {
|
||||||
|
self.content = content.to_string();
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn embed<F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed>(mut self, f: F) -> Self {
|
||||||
|
let mut embed = CreateEmbed::default();
|
||||||
|
f(&mut embed);
|
||||||
|
|
||||||
|
self.embed = Some(embed);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn components<F: FnOnce(&mut CreateComponents) -> &mut CreateComponents>(
|
||||||
|
mut self,
|
||||||
|
f: F,
|
||||||
|
) -> Self {
|
||||||
|
let mut components = CreateComponents::default();
|
||||||
|
f(&mut components);
|
||||||
|
|
||||||
|
self.components = Some(components);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait CommandInvoke {
|
||||||
|
fn channel_id(&self) -> ChannelId;
|
||||||
|
fn guild_id(&self) -> Option<GuildId>;
|
||||||
|
fn guild(&self, cache: Arc<Cache>) -> Option<Guild>;
|
||||||
|
fn author_id(&self) -> UserId;
|
||||||
|
async fn member(&self, context: &Context) -> SerenityResult<Member>;
|
||||||
|
fn msg(&self) -> Option<Message>;
|
||||||
|
fn interaction(&self) -> Option<ApplicationCommandInteraction>;
|
||||||
|
async fn respond(
|
||||||
|
&self,
|
||||||
|
http: Arc<Http>,
|
||||||
|
generic_response: CreateGenericResponse,
|
||||||
|
) -> SerenityResult<()>;
|
||||||
|
async fn followup(
|
||||||
|
&self,
|
||||||
|
http: Arc<Http>,
|
||||||
|
generic_response: CreateGenericResponse,
|
||||||
|
) -> SerenityResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandInvoke for Message {
|
||||||
|
fn channel_id(&self) -> ChannelId {
|
||||||
|
self.channel_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guild_id(&self) -> Option<GuildId> {
|
||||||
|
self.guild_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guild(&self, cache: Arc<Cache>) -> Option<Guild> {
|
||||||
|
self.guild(cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn author_id(&self) -> UserId {
|
||||||
|
self.author.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn member(&self, context: &Context) -> SerenityResult<Member> {
|
||||||
|
self.member(context).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg(&self) -> Option<Message> {
|
||||||
|
Some(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interaction(&self) -> Option<ApplicationCommandInteraction> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn respond(
|
||||||
|
&self,
|
||||||
|
http: Arc<Http>,
|
||||||
|
generic_response: CreateGenericResponse,
|
||||||
|
) -> SerenityResult<()> {
|
||||||
|
self.channel_id
|
||||||
|
.send_message(http, |m| {
|
||||||
|
m.content(generic_response.content);
|
||||||
|
|
||||||
|
if let Some(embed) = generic_response.embed {
|
||||||
|
m.set_embed(embed.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(components) = generic_response.components {
|
||||||
|
m.components(|c| {
|
||||||
|
*c = components;
|
||||||
|
c
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn followup(
|
||||||
|
&self,
|
||||||
|
http: Arc<Http>,
|
||||||
|
generic_response: CreateGenericResponse,
|
||||||
|
) -> SerenityResult<()> {
|
||||||
|
self.channel_id
|
||||||
|
.send_message(http, |m| {
|
||||||
|
m.content(generic_response.content);
|
||||||
|
|
||||||
|
if let Some(embed) = generic_response.embed {
|
||||||
|
m.set_embed(embed.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(components) = generic_response.components {
|
||||||
|
m.components(|c| {
|
||||||
|
*c = components;
|
||||||
|
c
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CommandInvoke for ApplicationCommandInteraction {
|
||||||
|
fn channel_id(&self) -> ChannelId {
|
||||||
|
self.channel_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guild_id(&self) -> Option<GuildId> {
|
||||||
|
self.guild_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guild(&self, cache: Arc<Cache>) -> Option<Guild> {
|
||||||
|
if let Some(guild_id) = self.guild_id {
|
||||||
|
guild_id.to_guild_cached(cache)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn author_id(&self) -> UserId {
|
||||||
|
self.member.as_ref().unwrap().user.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn member(&self, _: &Context) -> SerenityResult<Member> {
|
||||||
|
Ok(self.member.clone().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msg(&self) -> Option<Message> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interaction(&self) -> Option<ApplicationCommandInteraction> {
|
||||||
|
Some(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn respond(
|
||||||
|
&self,
|
||||||
|
http: Arc<Http>,
|
||||||
|
generic_response: CreateGenericResponse,
|
||||||
|
) -> SerenityResult<()> {
|
||||||
|
self.create_interaction_response(http, |r| {
|
||||||
|
r.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
|
.interaction_response_data(|d| {
|
||||||
|
d.content(generic_response.content);
|
||||||
|
|
||||||
|
if let Some(embed) = generic_response.embed {
|
||||||
|
d.add_embed(embed.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(components) = generic_response.components {
|
||||||
|
d.components(|c| {
|
||||||
|
*c = components;
|
||||||
|
c
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
d
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn followup(
|
||||||
|
&self,
|
||||||
|
http: Arc<Http>,
|
||||||
|
generic_response: CreateGenericResponse,
|
||||||
|
) -> SerenityResult<()> {
|
||||||
|
self.create_followup_message(http, |d| {
|
||||||
|
d.content(generic_response.content);
|
||||||
|
|
||||||
|
if let Some(embed) = generic_response.embed {
|
||||||
|
d.add_embed(embed.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(components) = generic_response.components {
|
||||||
|
d.components(|c| {
|
||||||
|
*c = components;
|
||||||
|
c
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
d
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Arg {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub description: &'static str,
|
||||||
|
pub kind: ApplicationCommandOptionType,
|
||||||
|
pub required: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SlashCommandFn = for<'fut> fn(
|
||||||
|
&'fut Context,
|
||||||
|
&'fut (dyn CommandInvoke + Sync + Send),
|
||||||
|
Args,
|
||||||
|
) -> BoxFuture<'fut, ()>;
|
||||||
|
|
||||||
|
type TextCommandFn = for<'fut> fn(
|
||||||
|
&'fut Context,
|
||||||
|
&'fut (dyn CommandInvoke + Sync + Send),
|
||||||
|
String,
|
||||||
|
) -> BoxFuture<'fut, ()>;
|
||||||
|
|
||||||
|
type MultiCommandFn =
|
||||||
|
for<'fut> fn(&'fut Context, &'fut (dyn CommandInvoke + Sync + Send)) -> BoxFuture<'fut, ()>;
|
||||||
|
|
||||||
|
pub enum CommandFnType {
|
||||||
|
Slash(SlashCommandFn),
|
||||||
|
Text(TextCommandFn),
|
||||||
|
Multi(MultiCommandFn),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandFnType {
|
||||||
|
pub fn text(&self) -> Option<&TextCommandFn> {
|
||||||
|
match self {
|
||||||
|
CommandFnType::Text(t) => Some(t),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Command {
|
||||||
|
pub fun: CommandFnType,
|
||||||
|
|
||||||
|
pub names: &'static [&'static str],
|
||||||
|
|
||||||
|
pub desc: &'static str,
|
||||||
|
pub examples: &'static [&'static str],
|
||||||
|
pub group: &'static str,
|
||||||
|
|
||||||
|
pub required_permissions: PermissionLevel,
|
||||||
|
pub args: &'static [&'static Arg],
|
||||||
|
|
||||||
|
pub can_blacklist: bool,
|
||||||
|
pub supports_dm: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Command {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.names[0].hash(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Command {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.names[0] == other.names[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Command {}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool {
|
async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool {
|
||||||
if self.required_perms == PermissionLevel::Unrestricted {
|
if self.required_permissions == PermissionLevel::Unrestricted {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap();
|
let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap();
|
||||||
|
|
||||||
if permissions.manage_guild()
|
if permissions.manage_guild()
|
||||||
|| (permissions.manage_messages()
|
|| (permissions.manage_messages()
|
||||||
&& self.required_perms == PermissionLevel::Managed)
|
&& self.required_permissions == PermissionLevel::Managed)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.required_perms == PermissionLevel::Managed {
|
if self.required_permissions == PermissionLevel::Managed {
|
||||||
let pool = ctx
|
let pool = ctx
|
||||||
.data
|
.data
|
||||||
.read()
|
.read()
|
||||||
@ -83,7 +392,7 @@ WHERE
|
|||||||
WHERE
|
WHERE
|
||||||
guild = ?)
|
guild = ?)
|
||||||
",
|
",
|
||||||
self.name,
|
self.names[0],
|
||||||
guild.id.as_u64()
|
guild.id.as_u64()
|
||||||
)
|
)
|
||||||
.fetch_all(&pool)
|
.fetch_all(&pool)
|
||||||
@ -123,62 +432,9 @@ WHERE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Command {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("Command")
|
|
||||||
.field("name", &self.name)
|
|
||||||
.field("required_perms", &self.required_perms)
|
|
||||||
.field("supports_dm", &self.supports_dm)
|
|
||||||
.field("can_blacklist", &self.can_blacklist)
|
|
||||||
.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 {
|
||||||
pub commands: HashMap<String, &'static Command>,
|
pub commands_map: HashMap<String, &'static Command>,
|
||||||
|
pub commands: HashSet<&'static Command>,
|
||||||
command_matcher: Regex,
|
command_matcher: Regex,
|
||||||
dm_regex_matcher: Regex,
|
dm_regex_matcher: Regex,
|
||||||
default_prefix: String,
|
default_prefix: String,
|
||||||
@ -186,12 +442,23 @@ pub struct RegexFramework {
|
|||||||
ignore_bots: bool,
|
ignore_bots: bool,
|
||||||
case_insensitive: bool,
|
case_insensitive: bool,
|
||||||
dm_enabled: bool,
|
dm_enabled: bool,
|
||||||
|
default_text_fun: TextCommandFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_text<'fut>(
|
||||||
|
_: &'fut Context,
|
||||||
|
_: &'fut (dyn CommandInvoke + Sync + Send),
|
||||||
|
_: String,
|
||||||
|
) -> std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 'fut)>>
|
||||||
|
{
|
||||||
|
async move {}.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
commands: HashMap::new(),
|
commands_map: HashMap::new(),
|
||||||
|
commands: HashSet::new(),
|
||||||
command_matcher: Regex::new(r#"^$"#).unwrap(),
|
command_matcher: Regex::new(r#"^$"#).unwrap(),
|
||||||
dm_regex_matcher: Regex::new(r#"^$"#).unwrap(),
|
dm_regex_matcher: Regex::new(r#"^$"#).unwrap(),
|
||||||
default_prefix: "".to_string(),
|
default_prefix: "".to_string(),
|
||||||
@ -199,6 +466,7 @@ impl RegexFramework {
|
|||||||
ignore_bots: true,
|
ignore_bots: true,
|
||||||
case_insensitive: true,
|
case_insensitive: true,
|
||||||
dm_enabled: true,
|
dm_enabled: true,
|
||||||
|
default_text_fun: drop_text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,8 +494,12 @@ impl RegexFramework {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_command<S: ToString>(mut self, name: S, command: &'static Command) -> Self {
|
pub fn add_command(mut self, command: &'static Command) -> Self {
|
||||||
self.commands.insert(name.to_string(), command);
|
self.commands.insert(command);
|
||||||
|
|
||||||
|
for name in command.names {
|
||||||
|
self.commands_map.insert(name.to_string(), command);
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -237,8 +509,11 @@ impl RegexFramework {
|
|||||||
let command_names;
|
let command_names;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut command_names_vec =
|
let mut command_names_vec = self
|
||||||
self.commands.keys().map(|k| &k[..]).collect::<Vec<&str>>();
|
.commands_map
|
||||||
|
.keys()
|
||||||
|
.map(|k| &k[..])
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
|
|
||||||
command_names_vec.sort_unstable_by_key(|a| a.len());
|
command_names_vec.sort_unstable_by_key(|a| a.len());
|
||||||
|
|
||||||
@ -265,7 +540,7 @@ impl RegexFramework {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut command_names_vec = self
|
let mut command_names_vec = self
|
||||||
.commands
|
.commands_map
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(key, command)| {
|
.filter_map(|(key, command)| {
|
||||||
if command.supports_dm {
|
if command.supports_dm {
|
||||||
@ -359,15 +634,11 @@ impl Framework for RegexFramework {
|
|||||||
|
|
||||||
if let Some(full_match) = self.command_matcher.captures(&msg.content) {
|
if let Some(full_match) = self.command_matcher.captures(&msg.content) {
|
||||||
if check_prefix(&ctx, &guild, full_match.name("prefix")).await {
|
if check_prefix(&ctx, &guild, full_match.name("prefix")).await {
|
||||||
let lm = data.get::<LanguageManager>().unwrap();
|
|
||||||
|
|
||||||
let language = UserData::language_of(&msg.author, &pool);
|
|
||||||
|
|
||||||
match check_self_permissions(&ctx, &guild, &channel).await {
|
match check_self_permissions(&ctx, &guild, &channel).await {
|
||||||
Ok(perms) => match perms {
|
Ok(perms) => match perms {
|
||||||
PermissionCheck::All => {
|
PermissionCheck::All => {
|
||||||
let command = self
|
let command = self
|
||||||
.commands
|
.commands_map
|
||||||
.get(
|
.get(
|
||||||
&full_match
|
&full_match
|
||||||
.name("cmd")
|
.name("cmd")
|
||||||
@ -394,8 +665,6 @@ impl Framework for RegexFramework {
|
|||||||
let member = guild.member(&ctx, &msg.author).await.unwrap();
|
let member = guild.member(&ctx, &msg.author).await.unwrap();
|
||||||
|
|
||||||
if command.check_permissions(&ctx, &guild, &member).await {
|
if command.check_permissions(&ctx, &guild, &member).await {
|
||||||
dbg!(command.name);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let guild_id = guild.id.as_u64().to_owned();
|
let guild_id = guild.id.as_u64().to_owned();
|
||||||
|
|
||||||
@ -413,30 +682,34 @@ impl Framework for RegexFramework {
|
|||||||
|| !ctx.check_executing(msg.author.id).await
|
|| !ctx.check_executing(msg.author.id).await
|
||||||
{
|
{
|
||||||
ctx.set_executing(msg.author.id).await;
|
ctx.set_executing(msg.author.id).await;
|
||||||
(command.func)(&ctx, &msg, args).await;
|
|
||||||
|
match command.fun {
|
||||||
|
CommandFnType::Text(t) => t(&ctx, &msg, args),
|
||||||
|
CommandFnType::Multi(m) => m(&ctx, &msg),
|
||||||
|
_ => (self.default_text_fun)(&ctx, &msg, args),
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
ctx.drop_executing(msg.author.id).await;
|
ctx.drop_executing(msg.author.id).await;
|
||||||
}
|
}
|
||||||
} else if command.required_perms
|
} else if command.required_permissions
|
||||||
== PermissionLevel::Restricted
|
== PermissionLevel::Restricted
|
||||||
{
|
{
|
||||||
let _ = msg
|
let _ = msg
|
||||||
.channel_id
|
.channel_id
|
||||||
.say(
|
.say(
|
||||||
&ctx,
|
&ctx,
|
||||||
lm.get(&language.await, "no_perms_restricted"),
|
"You must have the `Manage Server` permission to use this command.",
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
} else if command.required_perms == PermissionLevel::Managed
|
} else if command.required_permissions
|
||||||
|
== PermissionLevel::Managed
|
||||||
{
|
{
|
||||||
let _ = msg
|
let _ = msg
|
||||||
.channel_id
|
.channel_id
|
||||||
.say(
|
.say(
|
||||||
&ctx,
|
&ctx,
|
||||||
lm.get(&language.await, "no_perms_managed")
|
"You must have `Manage Messages` or have a role capable of sending reminders to that channel. Please talk to your server admin, and ask them to use the `/restrict` command to specify allowed roles.",
|
||||||
.replace(
|
|
||||||
"{prefix}",
|
|
||||||
&ctx.prefix(msg.guild_id).await,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -444,18 +717,21 @@ impl Framework for RegexFramework {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PermissionCheck::Basic(manage_webhooks, embed_links) => {
|
PermissionCheck::Basic(manage_webhooks, embed_links) => {
|
||||||
let response = lm
|
let _ = msg
|
||||||
.get(&language.await, "no_perms_general")
|
.channel_id
|
||||||
.replace(
|
.say(
|
||||||
"{manage_webhooks}",
|
&ctx,
|
||||||
if manage_webhooks { "✅" } else { "❌" },
|
format!(
|
||||||
)
|
"Please ensure the bot has the correct permissions:
|
||||||
.replace(
|
|
||||||
"{embed_links}",
|
|
||||||
if embed_links { "✅" } else { "❌" },
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = msg.channel_id.say(&ctx, response).await;
|
✅ **Send Message**
|
||||||
|
{} **Embed Links**
|
||||||
|
{} **Manage Webhooks**",
|
||||||
|
if manage_webhooks { "✅" } else { "❌" },
|
||||||
|
if embed_links { "✅" } else { "❌" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionCheck::None => {
|
PermissionCheck::None => {
|
||||||
@ -477,7 +753,7 @@ impl Framework for RegexFramework {
|
|||||||
else if self.dm_enabled {
|
else if self.dm_enabled {
|
||||||
if let Some(full_match) = self.dm_regex_matcher.captures(&msg.content[..]) {
|
if let Some(full_match) = self.dm_regex_matcher.captures(&msg.content[..]) {
|
||||||
let command = self
|
let command = self
|
||||||
.commands
|
.commands_map
|
||||||
.get(&full_match.name("cmd").unwrap().as_str().to_lowercase())
|
.get(&full_match.name("cmd").unwrap().as_str().to_lowercase())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let args = full_match
|
let args = full_match
|
||||||
@ -486,11 +762,16 @@ impl Framework for RegexFramework {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
dbg!(command.name);
|
|
||||||
|
|
||||||
if msg.id == MessageId(0) || !ctx.check_executing(msg.author.id).await {
|
if msg.id == MessageId(0) || !ctx.check_executing(msg.author.id).await {
|
||||||
ctx.set_executing(msg.author.id).await;
|
ctx.set_executing(msg.author.id).await;
|
||||||
(command.func)(&ctx, &msg, args).await;
|
|
||||||
|
match command.fun {
|
||||||
|
CommandFnType::Text(t) => t(&ctx, &msg, args),
|
||||||
|
CommandFnType::Multi(m) => m(&ctx, &msg),
|
||||||
|
_ => (self.default_text_fun)(&ctx, &msg, args),
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
ctx.drop_executing(msg.author.id).await;
|
ctx.drop_executing(msg.author.id).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
use serde::Deserialize;
|
|
||||||
use serde_json::from_str;
|
|
||||||
use serenity::prelude::TypeMapKey;
|
|
||||||
|
|
||||||
use std::{collections::HashMap, error::Error, sync::Arc};
|
|
||||||
|
|
||||||
use crate::consts::LOCAL_LANGUAGE;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct LanguageManager {
|
|
||||||
languages: HashMap<String, String>,
|
|
||||||
strings: HashMap<String, HashMap<String, String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LanguageManager {
|
|
||||||
pub fn from_compiled(content: &'static str) -> Result<Self, Box<dyn Error + Send + Sync>> {
|
|
||||||
let new: Self = from_str(content)?;
|
|
||||||
|
|
||||||
Ok(new)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, language: &str, name: &str) -> &str {
|
|
||||||
self.strings
|
|
||||||
.get(language)
|
|
||||||
.map(|sm| sm.get(name))
|
|
||||||
.unwrap_or_else(|| panic!(r#"Language does not exist: "{}""#, language))
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
self.strings
|
|
||||||
.get(&*LOCAL_LANGUAGE)
|
|
||||||
.map(|sm| {
|
|
||||||
sm.get(name)
|
|
||||||
.unwrap_or_else(|| panic!(r#"String does not exist: "{}""#, name))
|
|
||||||
})
|
|
||||||
.expect("LOCAL_LANGUAGE is not available")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_language(&self, language: &str) -> Option<&str> {
|
|
||||||
let language_normal = language.to_lowercase();
|
|
||||||
|
|
||||||
self.languages
|
|
||||||
.iter()
|
|
||||||
.filter(|(k, v)| {
|
|
||||||
k.to_lowercase() == language_normal || v.to_lowercase() == language_normal
|
|
||||||
})
|
|
||||||
.map(|(k, _)| k.as_str())
|
|
||||||
.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_language_by_flag(&self, flag: &str) -> Option<&str> {
|
|
||||||
self.languages
|
|
||||||
.iter()
|
|
||||||
.filter(|(k, _)| self.get(k, "flag") == flag)
|
|
||||||
.map(|(k, _)| k.as_str())
|
|
||||||
.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all_languages(&self) -> impl Iterator<Item = (&str, &str)> {
|
|
||||||
self.languages.iter().map(|(k, v)| (k.as_str(), v.as_str()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TypeMapKey for LanguageManager {
|
|
||||||
type Value = Arc<Self>;
|
|
||||||
}
|
|
233
src/main.rs
233
src/main.rs
@ -4,10 +4,17 @@ extern crate lazy_static;
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod consts;
|
mod consts;
|
||||||
mod framework;
|
mod framework;
|
||||||
mod language_manager;
|
|
||||||
mod models;
|
mod models;
|
||||||
mod time_parser;
|
mod time_parser;
|
||||||
|
|
||||||
|
use std::{collections::HashMap, env, sync::Arc, time::Instant};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use inflector::Inflector;
|
||||||
|
use log::info;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
cache::Cache,
|
cache::Cache,
|
||||||
@ -15,8 +22,7 @@ use serenity::{
|
|||||||
futures::TryFutureExt,
|
futures::TryFutureExt,
|
||||||
http::{client::Http, CacheHttp},
|
http::{client::Http, CacheHttp},
|
||||||
model::{
|
model::{
|
||||||
channel::GuildChannel,
|
channel::{GuildChannel, Message},
|
||||||
channel::Message,
|
|
||||||
guild::{Guild, GuildUnavailable},
|
guild::{Guild, GuildUnavailable},
|
||||||
id::{GuildId, UserId},
|
id::{GuildId, UserId},
|
||||||
interactions::{
|
interactions::{
|
||||||
@ -26,18 +32,13 @@ use serenity::{
|
|||||||
prelude::{Context, EventHandler, TypeMapKey},
|
prelude::{Context, EventHandler, TypeMapKey},
|
||||||
utils::shard_id,
|
utils::shard_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
use sqlx::mysql::MySqlPool;
|
use sqlx::mysql::MySqlPool;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use dotenv::dotenv;
|
|
||||||
|
|
||||||
use std::{collections::HashMap, env, sync::Arc, time::Instant};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
|
commands::info_cmds,
|
||||||
consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR},
|
consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR},
|
||||||
framework::RegexFramework,
|
framework::RegexFramework,
|
||||||
language_manager::LanguageManager,
|
|
||||||
models::{
|
models::{
|
||||||
guild_data::GuildData,
|
guild_data::GuildData,
|
||||||
reminder::{Reminder, ReminderAction},
|
reminder::{Reminder, ReminderAction},
|
||||||
@ -45,17 +46,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use inflector::Inflector;
|
|
||||||
use log::info;
|
|
||||||
|
|
||||||
use dashmap::DashMap;
|
|
||||||
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
use chrono::Utc;
|
|
||||||
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
|
|
||||||
struct GuildDataCache;
|
struct GuildDataCache;
|
||||||
|
|
||||||
impl TypeMapKey for GuildDataCache {
|
impl TypeMapKey for GuildDataCache {
|
||||||
@ -266,128 +256,6 @@ DELETE FROM guilds WHERE guild = ?
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
|
||||||
let (pool, lm) = get_ctx_data(&&ctx).await;
|
|
||||||
|
|
||||||
match interaction {
|
|
||||||
Interaction::MessageComponent(component) => {
|
|
||||||
if component.data.custom_id.starts_with("timezone:") {
|
|
||||||
let mut user_data = UserData::from_user(&component.user, &ctx, &pool)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let new_timezone = component
|
|
||||||
.data
|
|
||||||
.custom_id
|
|
||||||
.replace("timezone:", "")
|
|
||||||
.parse::<Tz>();
|
|
||||||
|
|
||||||
if let Ok(timezone) = new_timezone {
|
|
||||||
user_data.timezone = timezone.to_string();
|
|
||||||
user_data.commit_changes(&pool).await;
|
|
||||||
|
|
||||||
let _ = component.create_interaction_response(&ctx, |r| {
|
|
||||||
r.kind(InteractionResponseType::ChannelMessageWithSource)
|
|
||||||
.interaction_response_data(|d| {
|
|
||||||
let footer_text = lm.get(&user_data.language, "timezone/footer").replacen(
|
|
||||||
"{timezone}",
|
|
||||||
&user_data.timezone,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
let now = Utc::now().with_timezone(&user_data.timezone());
|
|
||||||
|
|
||||||
let content = lm
|
|
||||||
.get(&user_data.language, "timezone/set_p")
|
|
||||||
.replacen("{timezone}", &user_data.timezone, 1)
|
|
||||||
.replacen(
|
|
||||||
"{time}",
|
|
||||||
&now.format("%H:%M").to_string(),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
d.create_embed(|e| e.title(lm.get(&user_data.language, "timezone/set_p_title"))
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
.description(content)
|
|
||||||
.footer(|f| f.text(footer_text)))
|
|
||||||
.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
|
|
||||||
|
|
||||||
d
|
|
||||||
})
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
} else if component.data.custom_id.starts_with("lang:") {
|
|
||||||
let mut user_data = UserData::from_user(&component.user, &ctx, &pool)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let lang_code = component.data.custom_id.replace("lang:", "");
|
|
||||||
|
|
||||||
if let Some(lang) = lm.get_language(&lang_code) {
|
|
||||||
user_data.language = lang.to_string();
|
|
||||||
user_data.commit_changes(&pool).await;
|
|
||||||
|
|
||||||
let _ = component
|
|
||||||
.create_interaction_response(&ctx, |r| {
|
|
||||||
r.kind(InteractionResponseType::ChannelMessageWithSource)
|
|
||||||
.interaction_response_data(|d| {
|
|
||||||
d.create_embed(|e| {
|
|
||||||
e.title(
|
|
||||||
lm.get(&user_data.language, "lang/set_p_title"),
|
|
||||||
)
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
.description(
|
|
||||||
lm.get(&user_data.language, "lang/set_p"),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match Reminder::from_interaction(
|
|
||||||
&ctx,
|
|
||||||
component.user.id,
|
|
||||||
component.data.custom_id.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((reminder, action)) => {
|
|
||||||
let response = match action {
|
|
||||||
ReminderAction::Delete => {
|
|
||||||
reminder.delete(&ctx).await;
|
|
||||||
"Reminder has been deleted"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = component
|
|
||||||
.create_interaction_response(&ctx, |r| {
|
|
||||||
r.kind(InteractionResponseType::ChannelMessageWithSource)
|
|
||||||
.interaction_response_data(|d| d
|
|
||||||
.content(response)
|
|
||||||
.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ie) => {
|
|
||||||
let _ = component
|
|
||||||
.create_interaction_response(&ctx, |r| {
|
|
||||||
r.kind(InteractionResponseType::ChannelMessageWithSource)
|
|
||||||
.interaction_response_data(|d| d
|
|
||||||
.content(ie.to_string())
|
|
||||||
.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@ -414,14 +282,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
.ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1"))
|
.ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1"))
|
||||||
.dm_enabled(dm_enabled)
|
.dm_enabled(dm_enabled)
|
||||||
// info commands
|
// info commands
|
||||||
.add_command("ping", &info_cmds::PING_COMMAND)
|
//.add_command("help", &info_cmds::HELP_COMMAND)
|
||||||
.add_command("help", &info_cmds::HELP_COMMAND)
|
.add_command(&info_cmds::INFO_COMMAND)
|
||||||
.add_command("info", &info_cmds::INFO_COMMAND)
|
.add_command(&info_cmds::DONATE_COMMAND)
|
||||||
.add_command("invite", &info_cmds::INFO_COMMAND)
|
//.add_command("dashboard", &info_cmds::DASHBOARD_COMMAND)
|
||||||
.add_command("donate", &info_cmds::DONATE_COMMAND)
|
//.add_command("clock", &info_cmds::CLOCK_COMMAND)
|
||||||
.add_command("dashboard", &info_cmds::DASHBOARD_COMMAND)
|
|
||||||
.add_command("clock", &info_cmds::CLOCK_COMMAND)
|
|
||||||
// reminder commands
|
// reminder commands
|
||||||
|
/*
|
||||||
.add_command("timer", &reminder_cmds::TIMER_COMMAND)
|
.add_command("timer", &reminder_cmds::TIMER_COMMAND)
|
||||||
.add_command("remind", &reminder_cmds::REMIND_COMMAND)
|
.add_command("remind", &reminder_cmds::REMIND_COMMAND)
|
||||||
.add_command("r", &reminder_cmds::REMIND_COMMAND)
|
.add_command("r", &reminder_cmds::REMIND_COMMAND)
|
||||||
@ -452,6 +319,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
.add_command("nudge", &reminder_cmds::NUDGE_COMMAND)
|
.add_command("nudge", &reminder_cmds::NUDGE_COMMAND)
|
||||||
.add_command("alias", &moderation_cmds::ALIAS_COMMAND)
|
.add_command("alias", &moderation_cmds::ALIAS_COMMAND)
|
||||||
.add_command("a", &moderation_cmds::ALIAS_COMMAND)
|
.add_command("a", &moderation_cmds::ALIAS_COMMAND)
|
||||||
|
*/
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let framework_arc = Arc::new(framework);
|
let framework_arc = Arc::new(framework);
|
||||||
@ -460,13 +328,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
.intents(if dm_enabled {
|
.intents(if dm_enabled {
|
||||||
GatewayIntents::GUILD_MESSAGES
|
GatewayIntents::GUILD_MESSAGES
|
||||||
| GatewayIntents::GUILDS
|
| GatewayIntents::GUILDS
|
||||||
| GatewayIntents::GUILD_MESSAGE_REACTIONS
|
|
||||||
| GatewayIntents::DIRECT_MESSAGES
|
| GatewayIntents::DIRECT_MESSAGES
|
||||||
| GatewayIntents::DIRECT_MESSAGE_REACTIONS
|
|
||||||
} else {
|
} else {
|
||||||
GatewayIntents::GUILD_MESSAGES
|
GatewayIntents::GUILD_MESSAGES | GatewayIntents::GUILDS
|
||||||
| GatewayIntents::GUILDS
|
|
||||||
| GatewayIntents::GUILD_MESSAGE_REACTIONS
|
|
||||||
})
|
})
|
||||||
.application_id(application_id.0)
|
.application_id(application_id.0)
|
||||||
.event_handler(Handler)
|
.event_handler(Handler)
|
||||||
@ -483,13 +347,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let language_manager = LanguageManager::from_compiled(include_str!(concat!(
|
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
|
||||||
"/assets/",
|
|
||||||
env!("STRINGS_FILE")
|
|
||||||
)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let popular_timezones = sqlx::query!(
|
let popular_timezones = sqlx::query!(
|
||||||
"SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
|
"SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
|
||||||
)
|
)
|
||||||
@ -508,7 +365,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
data.insert::<PopularTimezones>(Arc::new(popular_timezones));
|
data.insert::<PopularTimezones>(Arc::new(popular_timezones));
|
||||||
data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
|
data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
|
||||||
data.insert::<FrameworkCtx>(framework_arc.clone());
|
data.insert::<FrameworkCtx>(framework_arc.clone());
|
||||||
data.insert::<LanguageManager>(Arc::new(language_manager))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
|
if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
|
||||||
@ -585,54 +441,3 @@ pub async fn check_subscription_on_message(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_ctx_data(ctx: &&Context) -> (MySqlPool, Arc<LanguageManager>) {
|
|
||||||
let pool;
|
|
||||||
let lm;
|
|
||||||
|
|
||||||
{
|
|
||||||
let data = ctx.data.read().await;
|
|
||||||
|
|
||||||
pool = data
|
|
||||||
.get::<SQLPool>()
|
|
||||||
.cloned()
|
|
||||||
.expect("Could not get SQLPool");
|
|
||||||
|
|
||||||
lm = data
|
|
||||||
.get::<LanguageManager>()
|
|
||||||
.cloned()
|
|
||||||
.expect("Could not get LanguageManager");
|
|
||||||
}
|
|
||||||
|
|
||||||
(pool, lm)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn command_help(
|
|
||||||
ctx: &Context,
|
|
||||||
msg: &Message,
|
|
||||||
lm: Arc<LanguageManager>,
|
|
||||||
prefix: &str,
|
|
||||||
language: &str,
|
|
||||||
command_name: &str,
|
|
||||||
) {
|
|
||||||
let _ = msg
|
|
||||||
.channel_id
|
|
||||||
.send_message(ctx, |m| {
|
|
||||||
m.embed(move |e| {
|
|
||||||
e.title(format!("{} Help", command_name.to_title_case()))
|
|
||||||
.description(
|
|
||||||
lm.get(language, &format!("help/{}", command_name))
|
|
||||||
.replace("{prefix}", prefix),
|
|
||||||
)
|
|
||||||
.footer(|f| {
|
|
||||||
f.text(concat!(
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
" ver ",
|
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use serenity::model::channel::Channel;
|
|
||||||
|
|
||||||
use sqlx::MySqlPool;
|
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
use serenity::model::channel::Channel;
|
||||||
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
pub struct ChannelData {
|
pub struct ChannelData {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use serenity::model::guild::Guild;
|
|
||||||
|
|
||||||
use sqlx::MySqlPool;
|
|
||||||
|
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use serenity::model::guild::Guild;
|
||||||
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
use crate::consts::DEFAULT_PREFIX;
|
use crate::consts::DEFAULT_PREFIX;
|
||||||
|
|
||||||
|
@ -4,22 +4,18 @@ pub mod reminder;
|
|||||||
pub mod timer;
|
pub mod timer;
|
||||||
pub mod user_data;
|
pub mod user_data;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use guild_data::GuildData;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
model::id::{GuildId, UserId},
|
model::id::{GuildId, UserId},
|
||||||
prelude::Context,
|
prelude::Context,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{consts::DEFAULT_PREFIX, GuildDataCache, SQLPool};
|
|
||||||
|
|
||||||
use guild_data::GuildData;
|
|
||||||
|
|
||||||
use crate::models::user_data::UserData;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::{consts::DEFAULT_PREFIX, models::user_data::UserData, GuildDataCache, SQLPool};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait CtxData {
|
pub trait CtxData {
|
||||||
async fn guild_data<G: Into<GuildId> + Send + Sync>(
|
async fn guild_data<G: Into<GuildId> + Send + Sync>(
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
use std::{collections::HashSet, fmt::Display};
|
||||||
|
|
||||||
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
|
use chrono_tz::Tz;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
client::Context,
|
client::Context,
|
||||||
http::CacheHttp,
|
http::CacheHttp,
|
||||||
@ -8,9 +12,7 @@ use serenity::{
|
|||||||
},
|
},
|
||||||
Result as SerenityResult,
|
Result as SerenityResult,
|
||||||
};
|
};
|
||||||
|
use sqlx::MySqlPool;
|
||||||
use chrono::{Duration, NaiveDateTime, Utc};
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::{MAX_TIME, MIN_INTERVAL},
|
consts::{MAX_TIME, MIN_INTERVAL},
|
||||||
@ -23,10 +25,6 @@ use crate::{
|
|||||||
SQLPool,
|
SQLPool,
|
||||||
};
|
};
|
||||||
|
|
||||||
use sqlx::MySqlPool;
|
|
||||||
|
|
||||||
use std::{collections::HashSet, fmt::Display};
|
|
||||||
|
|
||||||
async fn create_webhook(
|
async fn create_webhook(
|
||||||
ctx: impl CacheHttp,
|
ctx: impl CacheHttp,
|
||||||
channel: GuildChannel,
|
channel: GuildChannel,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use serenity::model::{channel::Message, guild::Guild, misc::Mentionable};
|
|
||||||
|
|
||||||
use regex::Captures;
|
use regex::Captures;
|
||||||
|
use serenity::model::{channel::Message, guild::Guild, misc::Mentionable};
|
||||||
|
|
||||||
use crate::{consts::REGEX_CONTENT_SUBSTITUTION, models::reminder::errors::ContentError};
|
use crate::{consts::REGEX_CONTENT_SUBSTITUTION, models::reminder::errors::ContentError};
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE};
|
|
||||||
|
|
||||||
use num_integer::Integer;
|
use num_integer::Integer;
|
||||||
|
|
||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
|
use rand::{rngs::OsRng, seq::IteratorRandom};
|
||||||
|
|
||||||
|
use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE};
|
||||||
|
|
||||||
pub fn longhand_displacement(seconds: u64) -> String {
|
pub fn longhand_displacement(seconds: u64) -> String {
|
||||||
let (days, seconds) = seconds.div_rem(&DAY);
|
let (days, seconds) = seconds.div_rem(&DAY);
|
||||||
let (hours, seconds) = seconds.div_rem(&HOUR);
|
let (hours, seconds) = seconds.div_rem(&HOUR);
|
||||||
|
@ -4,13 +4,19 @@ pub mod errors;
|
|||||||
mod helper;
|
mod helper;
|
||||||
pub mod look_flags;
|
pub mod look_flags;
|
||||||
|
|
||||||
use serenity::{
|
use std::{
|
||||||
client::Context,
|
convert::{TryFrom, TryInto},
|
||||||
model::id::{ChannelId, GuildId, UserId},
|
env,
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::{NaiveDateTime, TimeZone};
|
use chrono::{NaiveDateTime, TimeZone};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
|
use ring::hmac;
|
||||||
|
use serenity::{
|
||||||
|
client::Context,
|
||||||
|
model::id::{ChannelId, GuildId, UserId},
|
||||||
|
};
|
||||||
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::reminder::{
|
models::reminder::{
|
||||||
@ -21,14 +27,6 @@ use crate::{
|
|||||||
SQLPool,
|
SQLPool,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ring::hmac;
|
|
||||||
|
|
||||||
use sqlx::MySqlPool;
|
|
||||||
use std::{
|
|
||||||
convert::{TryFrom, TryInto},
|
|
||||||
env,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum ReminderAction {
|
pub enum ReminderAction {
|
||||||
Delete,
|
Delete,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use sqlx::MySqlPool;
|
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
pub struct Timer {
|
pub struct Timer {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -1,47 +1,22 @@
|
|||||||
|
use chrono_tz::Tz;
|
||||||
|
use log::error;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
http::CacheHttp,
|
http::CacheHttp,
|
||||||
model::{id::UserId, user::User},
|
model::{id::UserId, user::User},
|
||||||
};
|
};
|
||||||
|
|
||||||
use sqlx::MySqlPool;
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
use chrono_tz::Tz;
|
use crate::consts::LOCAL_TIMEZONE;
|
||||||
|
|
||||||
use log::error;
|
|
||||||
|
|
||||||
use crate::consts::{LOCAL_LANGUAGE, LOCAL_TIMEZONE};
|
|
||||||
|
|
||||||
pub struct UserData {
|
pub struct UserData {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub user: u64,
|
pub user: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub dm_channel: u32,
|
pub dm_channel: u32,
|
||||||
pub language: String,
|
|
||||||
pub timezone: String,
|
pub timezone: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserData {
|
impl UserData {
|
||||||
pub async fn language_of<U>(user: U, pool: &MySqlPool) -> String
|
|
||||||
where
|
|
||||||
U: Into<UserId>,
|
|
||||||
{
|
|
||||||
let user_id = user.into().as_u64().to_owned();
|
|
||||||
|
|
||||||
match sqlx::query!(
|
|
||||||
"
|
|
||||||
SELECT language FROM users WHERE user = ?
|
|
||||||
",
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(r) => r.language,
|
|
||||||
|
|
||||||
Err(_) => LOCAL_LANGUAGE.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn timezone_of<U>(user: U, pool: &MySqlPool) -> Tz
|
pub async fn timezone_of<U>(user: U, pool: &MySqlPool) -> Tz
|
||||||
where
|
where
|
||||||
U: Into<UserId>,
|
U: Into<UserId>,
|
||||||
@ -75,9 +50,9 @@ SELECT timezone FROM users WHERE user = ?
|
|||||||
match sqlx::query_as_unchecked!(
|
match sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, user, name, dm_channel, IF(language IS NULL, ?, language) AS language, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
|
SELECT id, user, name, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
|
||||||
",
|
",
|
||||||
*LOCAL_LANGUAGE, *LOCAL_TIMEZONE, user_id
|
*LOCAL_TIMEZONE, user_id
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await
|
.await
|
||||||
@ -101,15 +76,15 @@ INSERT IGNORE INTO channels (channel) VALUES (?)
|
|||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO users (user, name, dm_channel, language, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?, ?)
|
INSERT INTO users (user, name, dm_channel, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?)
|
||||||
", user_id, user.name, dm_id, *LOCAL_LANGUAGE, *LOCAL_TIMEZONE)
|
", user_id, user.name, dm_id, *LOCAL_TIMEZONE)
|
||||||
.execute(&pool_c)
|
.execute(&pool_c)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(sqlx::query_as_unchecked!(
|
Ok(sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, user, name, dm_channel, language, timezone FROM users WHERE user = ?
|
SELECT id, user, name, dm_channel, timezone FROM users WHERE user = ?
|
||||||
",
|
",
|
||||||
user_id
|
user_id
|
||||||
)
|
)
|
||||||
@ -128,10 +103,9 @@ SELECT id, user, name, dm_channel, language, timezone FROM users WHERE user = ?
|
|||||||
pub async fn commit_changes(&self, pool: &MySqlPool) {
|
pub async fn commit_changes(&self, pool: &MySqlPool) {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE users SET name = ?, language = ?, timezone = ? WHERE id = ?
|
UPDATE users SET name = ?, timezone = ? WHERE id = ?
|
||||||
",
|
",
|
||||||
self.name,
|
self.name,
|
||||||
self.language,
|
|
||||||
self.timezone,
|
self.timezone,
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::{
|
||||||
|
convert::TryFrom,
|
||||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
|
str::from_utf8,
|
||||||
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
use chrono::{DateTime, Datelike, Timelike, Utc};
|
use chrono::{DateTime, Datelike, Timelike, Utc};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::from_utf8;
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum InvalidTime {
|
pub enum InvalidTime {
|
||||||
ParseErrorDMY,
|
ParseErrorDMY,
|
||||||
|
Loading…
Reference in New Issue
Block a user