diff --git a/Cargo.lock b/Cargo.lock index 9a9ed63..9bc11f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1248,7 +1248,7 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "regex_command_attr" -version = "0.2.0" +version = "0.3.6" dependencies = [ "proc-macro2", "quote", diff --git a/regex_command_attr/Cargo.toml b/regex_command_attr/Cargo.toml index dbe01d1..24e5fa5 100644 --- a/regex_command_attr/Cargo.toml +++ b/regex_command_attr/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "regex_command_attr" -version = "0.2.0" +version = "0.3.6" authors = ["acdenisSK ", "jellywx "] edition = "2018" -description = "Procedural macros for command creation for the RegexFramework for serenity." +description = "Procedural macros for command creation for the Serenity library." +license = "ISC" [lib] proc-macro = true diff --git a/regex_command_attr/src/attributes.rs b/regex_command_attr/src/attributes.rs index d4c2a27..3b93fd8 100644 --- a/regex_command_attr/src/attributes.rs +++ b/regex_command_attr/src/attributes.rs @@ -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 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)] pub enum ValueKind { // #[] @@ -19,6 +23,9 @@ pub enum ValueKind { // #[([, , , ...])] List, + // #[([ = , = , ...])] + EqualsList, + // #[()] SingleList, } @@ -29,6 +36,9 @@ impl fmt::Display for ValueKind { ValueKind::Name => f.pad("`#[]`"), ValueKind::Equals => f.pad("`#[ = ]`"), ValueKind::List => f.pad("`#[([, , , ...])]`"), + ValueKind::EqualsList => { + f.pad("`#[([ = , = , ...])]`") + } ValueKind::SingleList => f.pad("`#[()]`"), } } @@ -62,14 +72,19 @@ fn to_ident(p: Path) -> Result { #[derive(Debug)] pub struct Values { pub name: Ident, - pub literals: Vec, + pub literals: Vec<(Option, Lit)>, pub kind: ValueKind, pub span: Span, } impl Values { #[inline] - pub fn new(name: Ident, kind: ValueKind, literals: Vec, span: Span) -> Self { + pub fn new( + name: Ident, + kind: ValueKind, + literals: Vec<(Option, Lit)>, + span: Span, + ) -> Self { Values { name, literals, @@ -80,6 +95,19 @@ impl Values { } pub fn parse_values(attr: &Attribute) -> Result { + fn is_list_or_named_list(meta: &NestedMeta) -> ValueKind { + match meta { + // catch if the nested value is a literal value + NestedMeta::Lit(_) => ValueKind::List, + // catch if the nested value is a meta value + NestedMeta::Meta(m) => match m { + // path => some quoted value + Meta::Path(_) => ValueKind::List, + Meta::List(_) | Meta::NameValue(_) => ValueKind::EqualsList, + }, + } + } + let meta = attr.parse_meta()?; match meta { @@ -96,36 +124,71 @@ pub fn parse_values(attr: &Attribute) -> Result { return Err(Error::new(attr.span(), "list cannot be empty")); } - let mut lits = Vec::with_capacity(nested.len()); + if is_list_or_named_list(nested.first().unwrap()) == ValueKind::List { + let mut lits = Vec::with_capacity(nested.len()); - for meta in nested { - match meta { - NestedMeta::Lit(l) => lits.push(l), - NestedMeta::Meta(m) => match m { - Meta::Path(path) => { - let i = to_ident(path)?; - lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span()))) - } - Meta::List(_) | Meta::NameValue(_) => { - return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) - } - }, + for meta in nested { + match meta { + // catch if the nested value is a literal value + NestedMeta::Lit(l) => lits.push((None, l)), + // catch if the nested value is a meta value + NestedMeta::Meta(m) => match m { + // path => some quoted value + Meta::Path(path) => { + let i = to_ident(path)?; + lits.push((None, Lit::Str(LitStr::new(&i.to_string(), i.span())))) + } + Meta::List(_) | Meta::NameValue(_) => { + return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) + } + }, + } } - } - let kind = if lits.len() == 1 { - ValueKind::SingleList + let kind = if lits.len() == 1 { + ValueKind::SingleList + } else { + ValueKind::List + }; + + Ok(Values::new(name, kind, lits, attr.span())) } else { - ValueKind::List - }; + let mut lits = Vec::with_capacity(nested.len()); - Ok(Values::new(name, kind, lits, attr.span())) + for meta in nested { + match meta { + // catch if the nested value is a literal value + NestedMeta::Lit(_) => { + return Err(Error::new(attr.span(), "key-value pairs expected")) + } + // catch if the nested value is a meta value + NestedMeta::Meta(m) => match m { + Meta::NameValue(n) => { + let name = to_ident(n.path)?.to_string(); + let value = n.lit; + + lits.push((Some(name), value)); + } + Meta::List(_) | Meta::Path(_) => { + return Err(Error::new(attr.span(), "key-value pairs expected")) + } + }, + } + } + + Ok(Values::new(name, ValueKind::EqualsList, lits, attr.span())) + } } Meta::NameValue(meta) => { let name = to_ident(meta.path)?; let lit = meta.lit; - Ok(Values::new(name, ValueKind::Equals, vec![lit], attr.span())) + Ok(Values::new( + name, + ValueKind::Equals, + vec![(None, lit)], + attr.span(), + )) } } } @@ -194,7 +257,7 @@ impl AttributeOption for Vec { Ok(values .literals .into_iter() - .map(|lit| lit.to_str()) + .map(|(_, l)| l.to_str()) .collect()) } } @@ -204,7 +267,7 @@ impl AttributeOption for String { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?; - Ok(values.literals[0].to_str()) + Ok(values.literals[0].1.to_str()) } } @@ -213,7 +276,7 @@ impl AttributeOption for bool { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::Name, ValueKind::SingleList])?; - Ok(values.literals.get(0).map_or(true, |l| l.to_bool())) + Ok(values.literals.get(0).map_or(true, |(_, l)| l.to_bool())) } } @@ -222,7 +285,7 @@ impl AttributeOption for Ident { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::SingleList])?; - Ok(values.literals[0].to_ident()) + Ok(values.literals[0].1.to_ident()) } } @@ -231,15 +294,22 @@ impl AttributeOption for Vec { fn parse(values: Values) -> Result { validate(&values, &[ValueKind::List])?; - Ok(values.literals.into_iter().map(|l| l.to_ident()).collect()) + Ok(values + .literals + .into_iter() + .map(|(_, l)| l.to_ident()) + .collect()) } } impl AttributeOption for Option { fn parse(values: Values) -> Result { - 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 { 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 { + 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 { validate(&values, &[ValueKind::SingleList])?; - Ok(match &values.literals[0] { + Ok(match &values.literals[0].1 { Lit::Int(l) => l.base10_parse::<$n>()?, l => { let s = l.to_str(); diff --git a/regex_command_attr/src/consts.rs b/regex_command_attr/src/consts.rs index 94ca381..9235297 100644 --- a/regex_command_attr/src/consts.rs +++ b/regex_command_attr/src/consts.rs @@ -1,5 +1,6 @@ pub mod suffixes { pub const COMMAND: &str = "COMMAND"; + pub const ARG: &str = "ARG"; } pub use self::suffixes::*; diff --git a/regex_command_attr/src/lib.rs b/regex_command_attr/src/lib.rs index bcbad40..4302bf9 100644 --- a/regex_command_attr/src/lib.rs +++ b/regex_command_attr/src/lib.rs @@ -1,14 +1,10 @@ #![deny(rust_2018_idioms)] -// FIXME: Remove this in a foreseeable future. -// Currently exists for backwards compatibility to previous Rust versions. -#![recursion_limit = "128"] - -#[allow(unused_extern_crates)] -extern crate proc_macro; +#![deny(broken_intra_doc_links)] use proc_macro::TokenStream; +use proc_macro2::Ident; 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 consts; @@ -41,7 +37,7 @@ macro_rules! match_options { pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let mut fun = parse_macro_input!(input as CommandFun); - let lit_name = if !attr.is_empty() { + let _name = if !attr.is_empty() { parse_macro_input!(attr as Lit).to_str() } else { fun.name.to_string() @@ -56,17 +52,40 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let name = values.name.to_string(); let name = &name[..]; - match_options!(name, values, options, span => [ - permission_level; - supports_dm; - can_blacklist - ]); + 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 => [ + aliases; + group; + required_permissions; + can_blacklist; + supports_dm + ]); + } + } } let Options { - permission_level, - supports_dm, + aliases, + description, + group, + examples, + required_permissions, can_blacklist, + supports_dm, + mut cmd_args, } = options; let visibility = fun.visibility; @@ -78,25 +97,88 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { let cooked = fun.cooked.clone(); let command_path = quote!(crate::framework::Command); + let arg_path = quote!(crate::framework::Arg); populate_fut_lifetimes_on_refs(&mut fun.args); let args = fun.args; - (quote! { - #(#cooked)* - pub static #n: #command_path = #command_path { - func: #name, - name: #lit_name, - required_perms: #permission_level, - supports_dm: #supports_dm, - can_blacklist: #can_blacklist, - }; + let arg_idents = cmd_args + .iter() + .map(|arg| { + n.with_suffix(arg.name.replace(" ", "_").replace("-", "_").as_str()) + .with_suffix(ARG) + }) + .collect::>(); + 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)* + #[allow(missing_docs)] + pub static #an: #arg_path = #arg_path { + name: #name, + description: #description, + kind: #kind, + 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, ()> { use ::serenity::futures::future::FutureExt; - async move { #(#body)* }.boxed() + async move { + #(#body)*; + }.boxed() } - }) - .into() + }); + + tokens.into() } diff --git a/regex_command_attr/src/structures.rs b/regex_command_attr/src/structures.rs index 56ef1d5..7fcf15b 100644 --- a/regex_command_attr/src/structures.rs +++ b/regex_command_attr/src/structures.rs @@ -1,14 +1,14 @@ -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, Stmt, Token, Visibility, + Attribute, Block, FnArg, Ident, Pat, Stmt, Token, Visibility, }; +use crate::util::{Argument, Parenthesised}; + fn parse_argument(arg: FnArg) -> Result { match arg { FnArg::Typed(typed) => { @@ -53,7 +53,7 @@ fn parse_argument(arg: FnArg) -> Result { /// Test if the attribute is cooked. fn is_cooked(attr: &Attribute) -> bool { const COOKED_ATTRIBUTE_NAMES: &[&str] = &[ - "cfg", "cfg_attr", "doc", "derive", "inline", "allow", "warn", "deny", "forbid", + "cfg", "cfg_attr", "derive", "inline", "allow", "warn", "deny", "forbid", ]; COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n)) @@ -98,17 +98,6 @@ impl Parse for CommandFun { fn parse(input: ParseStream<'_>) -> Result { let mut attributes = input.call(Attribute::parse_outer)?; - // `#[doc = "..."]` is a cooked attribute but it is special-cased for commands. - for attr in &mut attributes { - // Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`. - if attr.path.is_ident("doc") { - attr.path = Path::from(PathSegment::from(Ident::new( - "description", - Span::call_site(), - ))); - } - } - let cooked = remove_cooked(&mut attributes); let visibility = input.parse::()?; @@ -155,7 +144,7 @@ impl ToTokens for CommandFun { stream.extend(quote! { #(#cooked)* - #visibility async fn #name (#(#args),*) -> () { + #visibility async fn #name (#(#args),*) { #(#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)] -pub struct Options { - pub permission_level: PermissionLevel, - pub supports_dm: bool, +pub(crate) struct Options { + pub aliases: Vec, + pub description: String, + pub group: String, + pub examples: Vec, + pub required_permissions: PermissionLevel, pub can_blacklist: bool, + pub supports_dm: bool, + pub cmd_args: Vec, } impl Options { #[inline] pub fn new() -> Self { - let mut options = Self::default(); - - options.can_blacklist = true; - options.supports_dm = true; - - options + Self { + group: "Other".to_string(), + ..Default::default() + } } } diff --git a/regex_command_attr/src/util.rs b/regex_command_attr/src/util.rs index f3c8a75..0c01e73 100644 --- a/regex_command_attr/src/util.rs +++ b/regex_command_attr/src/util.rs @@ -1,6 +1,5 @@ use proc_macro::TokenStream; -use proc_macro2::Span; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens}; use syn::{ braced, bracketed, parenthesized, @@ -158,3 +157,20 @@ pub fn populate_fut_lifetimes_on_refs(args: &mut Vec) { } } } + +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'); + } + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..455c820 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +imports_granularity = "Crate" +group_imports = "StdExternalCrate" diff --git a/src/commands/info_cmds.rs b/src/commands/info_cmds.rs index 757854a..1d008fd 100644 --- a/src/commands/info_cmds.rs +++ b/src/commands/info_cmds.rs @@ -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::{ sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; -#[command] -#[can_blacklist(false)] -async fn ping(ctx: &Context, msg: &Message, _args: String) { - let now = SystemTime::now(); - let since_epoch = now - .duration_since(UNIX_EPOCH) - .expect("Time calculated as going backwards. Very bad"); +use chrono::offset::Utc; +use regex_command_attr::command; +use serenity::{builder::CreateEmbedFooter, client::Context, model::channel::Message}; - 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 - .channel_id - .say(&ctx, format!("Time taken to receive message: {}ms", delta)) - .await; -} - -async fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter { +fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter { let shard_count = ctx.cache.shard_count(); let shard = ctx.shard_id; @@ -49,173 +29,105 @@ async fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut Cr } #[command] -#[can_blacklist(false)] -async fn help(ctx: &Context, msg: &Message, args: String) { - async fn default_help( - ctx: &Context, - msg: &Message, - lm: Arc, - 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::() - .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); +#[aliases("invite")] +#[description("Get information about the bot")] +#[group("Info")] +async fn info(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) { + let prefix = ctx.prefix(invoke.guild_id()).await; let current_user = ctx.cache.current_user(); - let footer = footer(ctx).await; + let footer = footer(ctx); - let desc = lm - .get(&language.await, "info") - .replacen("{user}", ¤t_user.name, 1) - .replace("{default_prefix}", &*DEFAULT_PREFIX) - .replace("{prefix}", &prefix.await); - - let _ = msg - .channel_id - .send_message(ctx, |m| { - m.embed(move |e| { + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().embed(|e| { 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) .color(*THEME_COLOR) - }) - }) - .await; -} - -#[command] -async fn donate(ctx: &Context, msg: &Message, _args: String) { - let (pool, lm) = get_ctx_data(&ctx).await; - - let language = UserData::language_of(&msg.author, &pool).await; - let desc = lm.get(&language, "donate"); - let footer = footer(ctx).await; - - let _ = msg - .channel_id - .send_message(ctx, |m| { - m.embed(move |e| { - e.title("Donate") - .description(desc) - .footer(footer) - .color(*THEME_COLOR) - }) - }) - .await; -} - -#[command] -async fn dashboard(ctx: &Context, msg: &Message, _args: String) { - let footer = footer(ctx).await; - - let _ = msg - .channel_id - .send_message(ctx, |m| { - m.embed(move |e| { - e.title("Dashboard") - .description("https://reminder-bot.com/dashboard") - .footer(footer) - .color(*THEME_COLOR) - }) - }) - .await; -} - -#[command] -async fn clock(ctx: &Context, msg: &Message, _args: String) { - let (pool, lm) = get_ctx_data(&ctx).await; - - let language = UserData::language_of(&msg.author, &pool).await; - let timezone = UserData::timezone_of(&msg.author, &pool).await; - - let now = Utc::now().with_timezone(&timezone); - - let clock_display = lm.get(&language, "clock/time"); - - let _ = msg - .channel_id - .say( - &ctx, - clock_display.replacen("{}", &now.format("%H:%M").to_string(), 1), + }), + ) + .await; +} + +#[command] +#[description("Details on supporting the bot and Patreon benefits")] +#[group("Info")] +async fn donate(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) { + let footer = footer(ctx); + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().embed(|e| { + e.title("Donate") + .description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :) + +**https://www.patreon.com/jellywx/** +**https://discord.jellywx.com/** + +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: +• Set repeating reminders with `interval`, `natural` or the dashboard +• Use unlimited uploads on SoundFX + +(Also, members of servers you __own__ will be able to set repeating reminders via commands) + +Just $2 USD/month! + +*Please note, you must be in the JellyWX Discord server to receive Patreon features*") + .footer(footer) + .color(*THEME_COLOR) + }), + ) + .await; +} + +#[command] +#[description("Get the link to the online dashboard")] +#[group("Info")] +async fn dashboard(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) { + let footer = footer(ctx); + + invoke + .respond( + ctx.http.clone(), + CreateGenericResponse::new().embed(|e| { + e.title("Dashboard") + .description("**https://reminder-bot.com/dashboard**") + .footer(footer) + .color(*THEME_COLOR) + }), + ) + .await; +} + +#[command] +#[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; } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e53c6f3..31582d1 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ pub mod info_cmds; -pub mod moderation_cmds; -pub mod reminder_cmds; -pub mod todo_cmds; +//pub mod moderation_cmds; +//pub mod reminder_cmds; +//pub mod todo_cmds; diff --git a/src/commands/moderation_cmds.rs b/src/commands/moderation_cmds.rs index 2daac7d..ebf6b8c 100644 --- a/src/commands/moderation_cmds.rs +++ b/src/commands/moderation_cmds.rs @@ -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::{ builder::CreateActionRow, 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::{ command_help, consts::{REGEX_ALIAS, REGEX_CHANNEL, REGEX_COMMANDS, REGEX_ROLE, THEME_COLOR}, @@ -28,8 +25,6 @@ use crate::{ FrameworkCtx, PopularTimezones, }; -use std::{collections::HashMap, iter}; - #[command] #[supports_dm(false)] #[permission_level(Restricted)] diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index 8c95af8..e87cb81 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -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::{ client::Context, - model::{channel::Channel, channel::Message}, + model::channel::{Channel, Message}, }; use crate::{ @@ -16,7 +23,12 @@ use crate::{ models::{ channel_data::ChannelData, guild_data::GuildData, - reminder::{builder::ReminderScope, content::Content, look_flags::LookFlags, Reminder}, + reminder::{ + builder::{MultiReminderBuilder, ReminderScope}, + content::Content, + look_flags::LookFlags, + Reminder, + }, timer::Timer, user_data::UserData, CtxData, @@ -24,17 +36,6 @@ use crate::{ 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] #[supports_dm(false)] #[permission_level(Restricted)] diff --git a/src/commands/todo_cmds.rs b/src/commands/todo_cmds.rs index 413948a..56ec5cc 100644 --- a/src/commands/todo_cmds.rs +++ b/src/commands/todo_cmds.rs @@ -1,5 +1,6 @@ -use regex_command_attr::command; +use std::{convert::TryFrom, fmt}; +use regex_command_attr::command; use serenity::{ async_trait, client::Context, @@ -9,15 +10,12 @@ use serenity::{ id::{ChannelId, GuildId, UserId}, }, }; - -use std::fmt; +use sqlx::MySqlPool; use crate::{ command_help, get_ctx_data, models::{user_data::UserData, CtxData}, }; -use sqlx::MySqlPool; -use std::convert::TryFrom; #[derive(Debug)] struct TodoNotFound; diff --git a/src/consts.rs b/src/consts.rs index 926b560..fe99a94 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -74,9 +74,6 @@ lazy_static! { pub static ref LOCAL_TIMEZONE: 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 = env::var("DEFAULT_PREFIX").unwrap_or_else(|_| "$".to_string()); diff --git a/src/framework.rs b/src/framework.rs index e3f8735..c8acedd 100644 --- a/src/framework.rs +++ b/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::{ async_trait, + builder::{CreateComponents, CreateEmbed}, + cache::Cache, client::Context, - constants::MESSAGE_CODE_LIMIT, framework::Framework, futures::prelude::future::BoxFuture, http::Http, model::{ channel::{Channel, GuildChannel, Message}, 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::{ - language_manager::LanguageManager, - models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData, CtxData}, + models::{channel_data::ChannelData, guild_data::GuildData, CtxData}, LimitExecutors, SQLPool, }; -type CommandFn = for<'fut> fn(&'fut Context, &'fut Message, String) -> BoxFuture<'fut, ()>; - #[derive(Debug, PartialEq)] pub enum PermissionLevel { Unrestricted, @@ -34,29 +38,334 @@ pub enum PermissionLevel { Restricted, } -pub struct Command { - pub name: &'static str, - pub required_perms: PermissionLevel, - pub supports_dm: bool, - pub can_blacklist: bool, - pub func: CommandFn, +pub struct Args { + pub args: HashMap, } +impl Args { + pub fn named(&self, name: D) -> Option<&String> { + let name = name.to_string(); + + self.args.get(&name) + } +} + +pub struct CreateGenericResponse { + content: String, + embed: Option, + components: Option, +} + +impl CreateGenericResponse { + pub fn new() -> Self { + Self { + content: "".to_string(), + embed: None, + components: None, + } + } + + pub fn content(mut self, content: D) -> Self { + self.content = content.to_string(); + + self + } + + pub fn embed &mut CreateEmbed>(mut self, f: F) -> Self { + let mut embed = CreateEmbed::default(); + f(&mut embed); + + self.embed = Some(embed); + self + } + + pub fn components &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; + fn guild(&self, cache: Arc) -> Option; + fn author_id(&self) -> UserId; + async fn member(&self, context: &Context) -> SerenityResult; + fn msg(&self) -> Option; + fn interaction(&self) -> Option; + async fn respond( + &self, + http: Arc, + generic_response: CreateGenericResponse, + ) -> SerenityResult<()>; + async fn followup( + &self, + http: Arc, + generic_response: CreateGenericResponse, + ) -> SerenityResult<()>; +} + +#[async_trait] +impl CommandInvoke for Message { + fn channel_id(&self) -> ChannelId { + self.channel_id + } + + fn guild_id(&self) -> Option { + self.guild_id + } + + fn guild(&self, cache: Arc) -> Option { + self.guild(cache) + } + + fn author_id(&self) -> UserId { + self.author.id + } + + async fn member(&self, context: &Context) -> SerenityResult { + self.member(context).await + } + + fn msg(&self) -> Option { + Some(self.clone()) + } + + fn interaction(&self) -> Option { + None + } + + async fn respond( + &self, + http: Arc, + 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, + 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 { + self.guild_id + } + + fn guild(&self, cache: Arc) -> Option { + 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 { + Ok(self.member.clone().unwrap()) + } + + fn msg(&self) -> Option { + None + } + + fn interaction(&self) -> Option { + Some(self.clone()) + } + + async fn respond( + &self, + http: Arc, + 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, + 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(&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 { async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool { - if self.required_perms == PermissionLevel::Unrestricted { + if self.required_permissions == PermissionLevel::Unrestricted { true } else { let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap(); if permissions.manage_guild() || (permissions.manage_messages() - && self.required_perms == PermissionLevel::Managed) + && self.required_permissions == PermissionLevel::Managed) { return true; } - if self.required_perms == PermissionLevel::Managed { + if self.required_permissions == PermissionLevel::Managed { let pool = ctx .data .read() @@ -83,7 +392,7 @@ WHERE WHERE guild = ?) ", - self.name, + self.names[0], guild.id.as_u64() ) .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 + Send + Sync + 'async_trait, - content: impl Iterator + Send + 'async_trait, - ) -> SerenityResult<()>; -} - -#[async_trait] -impl SendIterator for ChannelId { - async fn say_lines( - self, - http: impl AsRef + Send + Sync + 'async_trait, - content: impl Iterator + Send + 'async_trait, - ) -> SerenityResult<()> { - let mut current_content = String::new(); - - for line in content { - if current_content.len() + line.len() > MESSAGE_CODE_LIMIT as usize { - self.send_message(&http, |m| { - m.allowed_mentions(|am| am.empty_parse()) - .content(¤t_content) - }) - .await?; - - current_content = line; - } else { - current_content = format!("{}\n{}", current_content, line); - } - } - if !current_content.is_empty() { - self.send_message(&http, |m| { - m.allowed_mentions(|am| am.empty_parse()) - .content(¤t_content) - }) - .await?; - } - - Ok(()) - } -} - pub struct RegexFramework { - pub commands: HashMap, + pub commands_map: HashMap, + pub commands: HashSet<&'static Command>, command_matcher: Regex, dm_regex_matcher: Regex, default_prefix: String, @@ -186,12 +442,23 @@ pub struct RegexFramework { ignore_bots: bool, case_insensitive: bool, dm_enabled: bool, + default_text_fun: TextCommandFn, +} + +fn drop_text<'fut>( + _: &'fut Context, + _: &'fut (dyn CommandInvoke + Sync + Send), + _: String, +) -> std::pin::Pin + std::marker::Send + 'fut)>> +{ + async move {}.boxed() } impl RegexFramework { pub fn new>(client_id: T) -> Self { Self { - commands: HashMap::new(), + commands_map: HashMap::new(), + commands: HashSet::new(), command_matcher: Regex::new(r#"^$"#).unwrap(), dm_regex_matcher: Regex::new(r#"^$"#).unwrap(), default_prefix: "".to_string(), @@ -199,6 +466,7 @@ impl RegexFramework { ignore_bots: true, case_insensitive: true, dm_enabled: true, + default_text_fun: drop_text, } } @@ -226,8 +494,12 @@ impl RegexFramework { self } - pub fn add_command(mut self, name: S, command: &'static Command) -> Self { - self.commands.insert(name.to_string(), command); + pub fn add_command(mut self, command: &'static Command) -> Self { + self.commands.insert(command); + + for name in command.names { + self.commands_map.insert(name.to_string(), command); + } self } @@ -237,8 +509,11 @@ impl RegexFramework { let command_names; { - let mut command_names_vec = - self.commands.keys().map(|k| &k[..]).collect::>(); + let mut command_names_vec = self + .commands_map + .keys() + .map(|k| &k[..]) + .collect::>(); command_names_vec.sort_unstable_by_key(|a| a.len()); @@ -265,7 +540,7 @@ impl RegexFramework { { let mut command_names_vec = self - .commands + .commands_map .iter() .filter_map(|(key, command)| { if command.supports_dm { @@ -359,15 +634,11 @@ impl Framework for RegexFramework { if let Some(full_match) = self.command_matcher.captures(&msg.content) { if check_prefix(&ctx, &guild, full_match.name("prefix")).await { - let lm = data.get::().unwrap(); - - let language = UserData::language_of(&msg.author, &pool); - match check_self_permissions(&ctx, &guild, &channel).await { Ok(perms) => match perms { PermissionCheck::All => { let command = self - .commands + .commands_map .get( &full_match .name("cmd") @@ -394,8 +665,6 @@ impl Framework for RegexFramework { let member = guild.member(&ctx, &msg.author).await.unwrap(); if command.check_permissions(&ctx, &guild, &member).await { - dbg!(command.name); - { 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.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; } - } else if command.required_perms + } else if command.required_permissions == PermissionLevel::Restricted { let _ = msg .channel_id .say( &ctx, - lm.get(&language.await, "no_perms_restricted"), + "You must have the `Manage Server` permission to use this command.", ) .await; - } else if command.required_perms == PermissionLevel::Managed + } else if command.required_permissions + == PermissionLevel::Managed { let _ = msg .channel_id .say( &ctx, - lm.get(&language.await, "no_perms_managed") - .replace( - "{prefix}", - &ctx.prefix(msg.guild_id).await, - ), + "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.", ) .await; } @@ -444,18 +717,21 @@ impl Framework for RegexFramework { } PermissionCheck::Basic(manage_webhooks, embed_links) => { - let response = lm - .get(&language.await, "no_perms_general") - .replace( - "{manage_webhooks}", - if manage_webhooks { "✅" } else { "❌" }, - ) - .replace( - "{embed_links}", - if embed_links { "✅" } else { "❌" }, - ); + let _ = msg + .channel_id + .say( + &ctx, + format!( + "Please ensure the bot has the correct permissions: - 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 => { @@ -477,7 +753,7 @@ impl Framework for RegexFramework { else if self.dm_enabled { if let Some(full_match) = self.dm_regex_matcher.captures(&msg.content[..]) { let command = self - .commands + .commands_map .get(&full_match.name("cmd").unwrap().as_str().to_lowercase()) .unwrap(); let args = full_match @@ -486,11 +762,16 @@ impl Framework for RegexFramework { .unwrap_or("") .to_string(); - dbg!(command.name); - if msg.id == MessageId(0) || !ctx.check_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; } } diff --git a/src/language_manager.rs b/src/language_manager.rs deleted file mode 100644 index bd90d4a..0000000 --- a/src/language_manager.rs +++ /dev/null @@ -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, - strings: HashMap>, -} - -impl LanguageManager { - pub fn from_compiled(content: &'static str) -> Result> { - 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 { - self.languages.iter().map(|(k, v)| (k.as_str(), v.as_str())) - } -} - -impl TypeMapKey for LanguageManager { - type Value = Arc; -} diff --git a/src/main.rs b/src/main.rs index 3875582..40be1a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,17 @@ extern crate lazy_static; mod commands; mod consts; mod framework; -mod language_manager; mod models; 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::{ async_trait, cache::Cache, @@ -15,8 +22,7 @@ use serenity::{ futures::TryFutureExt, http::{client::Http, CacheHttp}, model::{ - channel::GuildChannel, - channel::Message, + channel::{GuildChannel, Message}, guild::{Guild, GuildUnavailable}, id::{GuildId, UserId}, interactions::{ @@ -26,18 +32,13 @@ use serenity::{ prelude::{Context, EventHandler, TypeMapKey}, utils::shard_id, }; - use sqlx::mysql::MySqlPool; - -use dotenv::dotenv; - -use std::{collections::HashMap, env, sync::Arc, time::Instant}; +use tokio::sync::RwLock; use crate::{ - commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds}, + commands::info_cmds, consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR}, framework::RegexFramework, - language_manager::LanguageManager, models::{ guild_data::GuildData, 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; impl TypeMapKey for GuildDataCache { @@ -266,128 +256,6 @@ DELETE FROM guilds WHERE guild = ? .await .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::(); - - 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] @@ -414,14 +282,13 @@ async fn main() -> Result<(), Box> { .ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1")) .dm_enabled(dm_enabled) // info commands - .add_command("ping", &info_cmds::PING_COMMAND) - .add_command("help", &info_cmds::HELP_COMMAND) - .add_command("info", &info_cmds::INFO_COMMAND) - .add_command("invite", &info_cmds::INFO_COMMAND) - .add_command("donate", &info_cmds::DONATE_COMMAND) - .add_command("dashboard", &info_cmds::DASHBOARD_COMMAND) - .add_command("clock", &info_cmds::CLOCK_COMMAND) + //.add_command("help", &info_cmds::HELP_COMMAND) + .add_command(&info_cmds::INFO_COMMAND) + .add_command(&info_cmds::DONATE_COMMAND) + //.add_command("dashboard", &info_cmds::DASHBOARD_COMMAND) + //.add_command("clock", &info_cmds::CLOCK_COMMAND) // reminder commands + /* .add_command("timer", &reminder_cmds::TIMER_COMMAND) .add_command("remind", &reminder_cmds::REMIND_COMMAND) .add_command("r", &reminder_cmds::REMIND_COMMAND) @@ -452,6 +319,7 @@ async fn main() -> Result<(), Box> { .add_command("nudge", &reminder_cmds::NUDGE_COMMAND) .add_command("alias", &moderation_cmds::ALIAS_COMMAND) .add_command("a", &moderation_cmds::ALIAS_COMMAND) + */ .build(); let framework_arc = Arc::new(framework); @@ -460,13 +328,9 @@ async fn main() -> Result<(), Box> { .intents(if dm_enabled { GatewayIntents::GUILD_MESSAGES | GatewayIntents::GUILDS - | GatewayIntents::GUILD_MESSAGE_REACTIONS | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::DIRECT_MESSAGE_REACTIONS } else { - GatewayIntents::GUILD_MESSAGES - | GatewayIntents::GUILDS - | GatewayIntents::GUILD_MESSAGE_REACTIONS + GatewayIntents::GUILD_MESSAGES | GatewayIntents::GUILDS }) .application_id(application_id.0) .event_handler(Handler) @@ -483,13 +347,6 @@ async fn main() -> Result<(), Box> { .await .unwrap(); - let language_manager = LanguageManager::from_compiled(include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/assets/", - env!("STRINGS_FILE") - ))) - .unwrap(); - let popular_timezones = sqlx::query!( "SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21" ) @@ -508,7 +365,6 @@ async fn main() -> Result<(), Box> { data.insert::(Arc::new(popular_timezones)); data.insert::(Arc::new(reqwest::Client::new())); data.insert::(framework_arc.clone()); - data.insert::(Arc::new(language_manager)) } 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 } } - -pub async fn get_ctx_data(ctx: &&Context) -> (MySqlPool, Arc) { - let pool; - let lm; - - { - let data = ctx.data.read().await; - - pool = data - .get::() - .cloned() - .expect("Could not get SQLPool"); - - lm = data - .get::() - .cloned() - .expect("Could not get LanguageManager"); - } - - (pool, lm) -} - -async fn command_help( - ctx: &Context, - msg: &Message, - lm: Arc, - 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; -} diff --git a/src/models/channel_data.rs b/src/models/channel_data.rs index a9fd65f..c431ec7 100644 --- a/src/models/channel_data.rs +++ b/src/models/channel_data.rs @@ -1,8 +1,6 @@ -use serenity::model::channel::Channel; - -use sqlx::MySqlPool; - use chrono::NaiveDateTime; +use serenity::model::channel::Channel; +use sqlx::MySqlPool; pub struct ChannelData { pub id: u32, diff --git a/src/models/guild_data.rs b/src/models/guild_data.rs index 81dcaca..f42b54f 100644 --- a/src/models/guild_data.rs +++ b/src/models/guild_data.rs @@ -1,8 +1,6 @@ -use serenity::model::guild::Guild; - -use sqlx::MySqlPool; - use log::error; +use serenity::model::guild::Guild; +use sqlx::MySqlPool; use crate::consts::DEFAULT_PREFIX; diff --git a/src/models/mod.rs b/src/models/mod.rs index baff568..a80fd4c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -4,22 +4,18 @@ pub mod reminder; pub mod timer; pub mod user_data; +use std::sync::Arc; + +use guild_data::GuildData; use serenity::{ async_trait, model::id::{GuildId, UserId}, 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 crate::{consts::DEFAULT_PREFIX, models::user_data::UserData, GuildDataCache, SQLPool}; + #[async_trait] pub trait CtxData { async fn guild_data + Send + Sync>( diff --git a/src/models/reminder/builder.rs b/src/models/reminder/builder.rs index 61dbbe1..beea19e 100644 --- a/src/models/reminder/builder.rs +++ b/src/models/reminder/builder.rs @@ -1,3 +1,7 @@ +use std::{collections::HashSet, fmt::Display}; + +use chrono::{Duration, NaiveDateTime, Utc}; +use chrono_tz::Tz; use serenity::{ client::Context, http::CacheHttp, @@ -8,9 +12,7 @@ use serenity::{ }, Result as SerenityResult, }; - -use chrono::{Duration, NaiveDateTime, Utc}; -use chrono_tz::Tz; +use sqlx::MySqlPool; use crate::{ consts::{MAX_TIME, MIN_INTERVAL}, @@ -23,10 +25,6 @@ use crate::{ SQLPool, }; -use sqlx::MySqlPool; - -use std::{collections::HashSet, fmt::Display}; - async fn create_webhook( ctx: impl CacheHttp, channel: GuildChannel, diff --git a/src/models/reminder/content.rs b/src/models/reminder/content.rs index 3b41f1b..7af093c 100644 --- a/src/models/reminder/content.rs +++ b/src/models/reminder/content.rs @@ -1,6 +1,5 @@ -use serenity::model::{channel::Message, guild::Guild, misc::Mentionable}; - use regex::Captures; +use serenity::model::{channel::Message, guild::Guild, misc::Mentionable}; use crate::{consts::REGEX_CONTENT_SUBSTITUTION, models::reminder::errors::ContentError}; diff --git a/src/models/reminder/helper.rs b/src/models/reminder/helper.rs index b8e67f3..05edcde 100644 --- a/src/models/reminder/helper.rs +++ b/src/models/reminder/helper.rs @@ -1,9 +1,8 @@ -use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE}; - use num_integer::Integer; - use rand::{rngs::OsRng, seq::IteratorRandom}; +use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE}; + pub fn longhand_displacement(seconds: u64) -> String { let (days, seconds) = seconds.div_rem(&DAY); let (hours, seconds) = seconds.div_rem(&HOUR); diff --git a/src/models/reminder/mod.rs b/src/models/reminder/mod.rs index 0f0b222..4197d6b 100644 --- a/src/models/reminder/mod.rs +++ b/src/models/reminder/mod.rs @@ -4,13 +4,19 @@ pub mod errors; mod helper; pub mod look_flags; -use serenity::{ - client::Context, - model::id::{ChannelId, GuildId, UserId}, +use std::{ + convert::{TryFrom, TryInto}, + env, }; use chrono::{NaiveDateTime, TimeZone}; use chrono_tz::Tz; +use ring::hmac; +use serenity::{ + client::Context, + model::id::{ChannelId, GuildId, UserId}, +}; +use sqlx::MySqlPool; use crate::{ models::reminder::{ @@ -21,14 +27,6 @@ use crate::{ SQLPool, }; -use ring::hmac; - -use sqlx::MySqlPool; -use std::{ - convert::{TryFrom, TryInto}, - env, -}; - #[derive(Clone, Copy)] pub enum ReminderAction { Delete, diff --git a/src/models/timer.rs b/src/models/timer.rs index 8a56b9f..080b0da 100644 --- a/src/models/timer.rs +++ b/src/models/timer.rs @@ -1,6 +1,5 @@ -use sqlx::MySqlPool; - use chrono::NaiveDateTime; +use sqlx::MySqlPool; pub struct Timer { pub name: String, diff --git a/src/models/user_data.rs b/src/models/user_data.rs index fe365a1..f06eef2 100644 --- a/src/models/user_data.rs +++ b/src/models/user_data.rs @@ -1,47 +1,22 @@ +use chrono_tz::Tz; +use log::error; use serenity::{ http::CacheHttp, model::{id::UserId, user::User}, }; - use sqlx::MySqlPool; -use chrono_tz::Tz; - -use log::error; - -use crate::consts::{LOCAL_LANGUAGE, LOCAL_TIMEZONE}; +use crate::consts::LOCAL_TIMEZONE; pub struct UserData { pub id: u32, pub user: u64, pub name: String, pub dm_channel: u32, - pub language: String, pub timezone: String, } impl UserData { - pub async fn language_of(user: U, pool: &MySqlPool) -> String - where - U: Into, - { - 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(user: U, pool: &MySqlPool) -> Tz where U: Into, @@ -75,9 +50,9 @@ SELECT timezone FROM users WHERE user = ? match sqlx::query_as_unchecked!( 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) .await @@ -101,15 +76,15 @@ INSERT IGNORE INTO channels (channel) VALUES (?) sqlx::query!( " -INSERT INTO users (user, name, dm_channel, language, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?, ?) - ", user_id, user.name, dm_id, *LOCAL_LANGUAGE, *LOCAL_TIMEZONE) +INSERT INTO users (user, name, dm_channel, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?) + ", user_id, user.name, dm_id, *LOCAL_TIMEZONE) .execute(&pool_c) .await?; Ok(sqlx::query_as_unchecked!( 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 ) @@ -128,10 +103,9 @@ SELECT id, user, name, dm_channel, language, timezone FROM users WHERE user = ? pub async fn commit_changes(&self, pool: &MySqlPool) { sqlx::query!( " -UPDATE users SET name = ?, language = ?, timezone = ? WHERE id = ? +UPDATE users SET name = ?, timezone = ? WHERE id = ? ", self.name, - self.language, self.timezone, self.id ) diff --git a/src/time_parser.rs b/src/time_parser.rs index f910a06..0e21a06 100644 --- a/src/time_parser.rs +++ b/src/time_parser.rs @@ -1,15 +1,16 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use std::fmt::{Display, Formatter, Result as FmtResult}; - -use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION}; +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, + str::from_utf8, + time::{SystemTime, UNIX_EPOCH}, +}; use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono_tz::Tz; -use std::convert::TryFrom; -use std::str::from_utf8; use tokio::process::Command; +use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION}; + #[derive(Debug)] pub enum InvalidTime { ParseErrorDMY,