removed language_manager.rs. framework reworked for slash commands. updated info commands for new framework

This commit is contained in:
jellywx 2021-09-06 13:46:16 +01:00
parent 98aed91d21
commit c148cdf556
27 changed files with 961 additions and 802 deletions

2
Cargo.lock generated
View File

@ -1248,7 +1248,7 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "regex_command_attr"
version = "0.2.0"
version = "0.3.6"
dependencies = [
"proc-macro2",
"quote",

View File

@ -1,9 +1,10 @@
[package]
name = "regex_command_attr"
version = "0.2.0"
version = "0.3.6"
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
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

View File

@ -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 {
// #[<name>]
@ -19,6 +23,9 @@ pub enum ValueKind {
// #[<name>([<value>, <value>, <value>, ...])]
List,
// #[<name>([<prop> = <value>, <prop> = <value>, ...])]
EqualsList,
// #[<name>(<value>)]
SingleList,
}
@ -29,6 +36,9 @@ impl fmt::Display for ValueKind {
ValueKind::Name => f.pad("`#[<name>]`"),
ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
ValueKind::EqualsList => {
f.pad("`#[<name>([<prop> = <value>, <prop> = <value>, ...])]`")
}
ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
}
}
@ -62,14 +72,19 @@ fn to_ident(p: Path) -> Result<Ident> {
#[derive(Debug)]
pub struct Values {
pub name: Ident,
pub literals: Vec<Lit>,
pub literals: Vec<(Option<String>, Lit)>,
pub kind: ValueKind,
pub span: Span,
}
impl Values {
#[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 {
name,
literals,
@ -80,6 +95,19 @@ impl 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()?;
match meta {
@ -96,36 +124,71 @@ pub fn parse_values(attr: &Attribute) -> Result<Values> {
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<String> {
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<Self> {
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> {
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> {
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> {
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> {
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> {
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> {
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();

View File

@ -1,5 +1,6 @@
pub mod suffixes {
pub const COMMAND: &str = "COMMAND";
pub const ARG: &str = "ARG";
}
pub use self::suffixes::*;

View File

@ -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::<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)*
#[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()
}

View File

@ -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<Argument> {
match arg {
FnArg::Typed(typed) => {
@ -53,7 +53,7 @@ fn parse_argument(arg: FnArg) -> Result<Argument> {
/// 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<Self> {
let mut attributes = input.call(Attribute::parse_outer)?;
// `#[doc = "..."]` is a cooked attribute but it is special-cased for commands.
for attr in &mut attributes {
// Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`.
if attr.path.is_ident("doc") {
attr.path = Path::from(PathSegment::from(Ident::new(
"description",
Span::call_site(),
)));
}
}
let cooked = remove_cooked(&mut attributes);
let visibility = input.parse::<Visibility>()?;
@ -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<String>,
pub description: String,
pub group: String,
pub examples: Vec<String>,
pub required_permissions: PermissionLevel,
pub can_blacklist: bool,
pub supports_dm: bool,
pub cmd_args: Vec<Arg>,
}
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()
}
}
}

View File

@ -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<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
View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -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<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);
#[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}", &current_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;
}

View File

@ -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;

View File

@ -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)]

View File

@ -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)]

View File

@ -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;

View File

@ -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());

View File

@ -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<String, String>,
}
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 {
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<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(&current_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(&current_content)
})
.await?;
}
Ok(())
}
}
pub struct RegexFramework {
pub commands: HashMap<String, &'static Command>,
pub commands_map: HashMap<String, &'static Command>,
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::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 'fut)>>
{
async move {}.boxed()
}
impl RegexFramework {
pub fn new<T: Into<u64>>(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<S: ToString>(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::<Vec<&str>>();
let mut command_names_vec = self
.commands_map
.keys()
.map(|k| &k[..])
.collect::<Vec<&str>>();
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::<LanguageManager>().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;
}
}

View File

@ -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>;
}

View File

@ -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::<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]
@ -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"))
.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<dyn std::error::Error + Send + Sync>> {
.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<dyn std::error::Error + Send + Sync>> {
.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<dyn std::error::Error + Send + Sync>> {
.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<dyn std::error::Error + Send + Sync>> {
data.insert::<PopularTimezones>(Arc::new(popular_timezones));
data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
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| {
@ -585,54 +441,3 @@ pub async fn check_subscription_on_message(
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;
}

View File

@ -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,

View File

@ -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;

View File

@ -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<G: Into<GuildId> + Send + Sync>(

View File

@ -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,

View File

@ -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};

View File

@ -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);

View File

@ -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,

View File

@ -1,6 +1,5 @@
use sqlx::MySqlPool;
use chrono::NaiveDateTime;
use sqlx::MySqlPool;
pub struct Timer {
pub name: String,

View File

@ -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<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
where
U: Into<UserId>,
@ -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
)

View File

@ -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,