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]] [[package]]
name = "regex_command_attr" name = "regex_command_attr"
version = "0.2.0" version = "0.3.6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -1,9 +1,10 @@
[package] [package]
name = "regex_command_attr" name = "regex_command_attr"
version = "0.2.0" version = "0.3.6"
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"] authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
edition = "2018" edition = "2018"
description = "Procedural macros for command creation for the RegexFramework for serenity." description = "Procedural macros for command creation for the Serenity library."
license = "ISC"
[lib] [lib]
proc-macro = true proc-macro = true

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 std::fmt::{self, Write};
use proc_macro2::Span;
use syn::{
parse::{Error, Result},
spanned::Spanned,
Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path,
};
use crate::{
structures::{ApplicationCommandOptionType, Arg, PermissionLevel},
util::{AsOption, LitExt},
};
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum ValueKind { pub enum ValueKind {
// #[<name>] // #[<name>]
@ -19,6 +23,9 @@ pub enum ValueKind {
// #[<name>([<value>, <value>, <value>, ...])] // #[<name>([<value>, <value>, <value>, ...])]
List, List,
// #[<name>([<prop> = <value>, <prop> = <value>, ...])]
EqualsList,
// #[<name>(<value>)] // #[<name>(<value>)]
SingleList, SingleList,
} }
@ -29,6 +36,9 @@ impl fmt::Display for ValueKind {
ValueKind::Name => f.pad("`#[<name>]`"), ValueKind::Name => f.pad("`#[<name>]`"),
ValueKind::Equals => f.pad("`#[<name> = <value>]`"), ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"), ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
ValueKind::EqualsList => {
f.pad("`#[<name>([<prop> = <value>, <prop> = <value>, ...])]`")
}
ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"), ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
} }
} }
@ -62,14 +72,19 @@ fn to_ident(p: Path) -> Result<Ident> {
#[derive(Debug)] #[derive(Debug)]
pub struct Values { pub struct Values {
pub name: Ident, pub name: Ident,
pub literals: Vec<Lit>, pub literals: Vec<(Option<String>, Lit)>,
pub kind: ValueKind, pub kind: ValueKind,
pub span: Span, pub span: Span,
} }
impl Values { impl Values {
#[inline] #[inline]
pub fn new(name: Ident, kind: ValueKind, literals: Vec<Lit>, span: Span) -> Self { pub fn new(
name: Ident,
kind: ValueKind,
literals: Vec<(Option<String>, Lit)>,
span: Span,
) -> Self {
Values { Values {
name, name,
literals, literals,
@ -80,6 +95,19 @@ impl Values {
} }
pub fn parse_values(attr: &Attribute) -> Result<Values> { pub fn parse_values(attr: &Attribute) -> Result<Values> {
fn is_list_or_named_list(meta: &NestedMeta) -> ValueKind {
match meta {
// catch if the nested value is a literal value
NestedMeta::Lit(_) => ValueKind::List,
// catch if the nested value is a meta value
NestedMeta::Meta(m) => match m {
// path => some quoted value
Meta::Path(_) => ValueKind::List,
Meta::List(_) | Meta::NameValue(_) => ValueKind::EqualsList,
},
}
}
let meta = attr.parse_meta()?; let meta = attr.parse_meta()?;
match meta { match meta {
@ -96,15 +124,19 @@ pub fn parse_values(attr: &Attribute) -> Result<Values> {
return Err(Error::new(attr.span(), "list cannot be empty")); return Err(Error::new(attr.span(), "list cannot be empty"));
} }
if is_list_or_named_list(nested.first().unwrap()) == ValueKind::List {
let mut lits = Vec::with_capacity(nested.len()); let mut lits = Vec::with_capacity(nested.len());
for meta in nested { for meta in nested {
match meta { match meta {
NestedMeta::Lit(l) => lits.push(l), // catch if the nested value is a literal value
NestedMeta::Lit(l) => lits.push((None, l)),
// catch if the nested value is a meta value
NestedMeta::Meta(m) => match m { NestedMeta::Meta(m) => match m {
// path => some quoted value
Meta::Path(path) => { Meta::Path(path) => {
let i = to_ident(path)?; let i = to_ident(path)?;
lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span()))) lits.push((None, Lit::Str(LitStr::new(&i.to_string(), i.span()))))
} }
Meta::List(_) | Meta::NameValue(_) => { Meta::List(_) | Meta::NameValue(_) => {
return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level"))
@ -120,12 +152,43 @@ pub fn parse_values(attr: &Attribute) -> Result<Values> {
}; };
Ok(Values::new(name, kind, lits, attr.span())) Ok(Values::new(name, kind, lits, attr.span()))
} else {
let mut lits = Vec::with_capacity(nested.len());
for meta in nested {
match meta {
// catch if the nested value is a literal value
NestedMeta::Lit(_) => {
return Err(Error::new(attr.span(), "key-value pairs expected"))
}
// catch if the nested value is a meta value
NestedMeta::Meta(m) => match m {
Meta::NameValue(n) => {
let name = to_ident(n.path)?.to_string();
let value = n.lit;
lits.push((Some(name), value));
}
Meta::List(_) | Meta::Path(_) => {
return Err(Error::new(attr.span(), "key-value pairs expected"))
}
},
}
}
Ok(Values::new(name, ValueKind::EqualsList, lits, attr.span()))
}
} }
Meta::NameValue(meta) => { Meta::NameValue(meta) => {
let name = to_ident(meta.path)?; let name = to_ident(meta.path)?;
let lit = meta.lit; let lit = meta.lit;
Ok(Values::new(name, ValueKind::Equals, vec![lit], attr.span())) Ok(Values::new(
name,
ValueKind::Equals,
vec![(None, lit)],
attr.span(),
))
} }
} }
} }
@ -194,7 +257,7 @@ impl AttributeOption for Vec<String> {
Ok(values Ok(values
.literals .literals
.into_iter() .into_iter()
.map(|lit| lit.to_str()) .map(|(_, l)| l.to_str())
.collect()) .collect())
} }
} }
@ -204,7 +267,7 @@ impl AttributeOption for String {
fn parse(values: Values) -> Result<Self> { fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?; validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
Ok(values.literals[0].to_str()) Ok(values.literals[0].1.to_str())
} }
} }
@ -213,7 +276,7 @@ impl AttributeOption for bool {
fn parse(values: Values) -> Result<Self> { fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::Name, ValueKind::SingleList])?; validate(&values, &[ValueKind::Name, ValueKind::SingleList])?;
Ok(values.literals.get(0).map_or(true, |l| l.to_bool())) Ok(values.literals.get(0).map_or(true, |(_, l)| l.to_bool()))
} }
} }
@ -222,7 +285,7 @@ impl AttributeOption for Ident {
fn parse(values: Values) -> Result<Self> { fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::SingleList])?; validate(&values, &[ValueKind::SingleList])?;
Ok(values.literals[0].to_ident()) Ok(values.literals[0].1.to_ident())
} }
} }
@ -231,15 +294,22 @@ impl AttributeOption for Vec<Ident> {
fn parse(values: Values) -> Result<Self> { fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::List])?; validate(&values, &[ValueKind::List])?;
Ok(values.literals.into_iter().map(|l| l.to_ident()).collect()) Ok(values
.literals
.into_iter()
.map(|(_, l)| l.to_ident())
.collect())
} }
} }
impl AttributeOption for Option<String> { impl AttributeOption for Option<String> {
fn parse(values: Values) -> Result<Self> { fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?; validate(
&values,
&[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList],
)?;
Ok(values.literals.get(0).map(|l| l.to_str())) Ok(values.literals.get(0).map(|(_, l)| l.to_str()))
} }
} }
@ -247,7 +317,44 @@ impl AttributeOption for PermissionLevel {
fn parse(values: Values) -> Result<Self> { fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::SingleList])?; validate(&values, &[ValueKind::SingleList])?;
Ok(values.literals.get(0).map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap()).unwrap()) Ok(values
.literals
.get(0)
.map(|(_, l)| PermissionLevel::from_str(&*l.to_str()).unwrap())
.unwrap())
}
}
impl AttributeOption for Arg {
fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::EqualsList])?;
let mut arg: Arg = Default::default();
for (key, value) in &values.literals {
match key {
Some(s) => match s.as_str() {
"name" => {
arg.name = value.to_str();
}
"description" => {
arg.description = value.to_str();
}
"required" => {
arg.required = value.to_bool();
}
"kind" => arg.kind = ApplicationCommandOptionType::from_str(value.to_str()),
_ => {
return Err(Error::new(key.span(), "unexpected attribute"));
}
},
_ => {
return Err(Error::new(key.span(), "unnamed attribute"));
}
}
}
Ok(arg)
} }
} }
@ -265,7 +372,7 @@ macro_rules! attr_option_num {
fn parse(values: Values) -> Result<Self> { fn parse(values: Values) -> Result<Self> {
validate(&values, &[ValueKind::SingleList])?; validate(&values, &[ValueKind::SingleList])?;
Ok(match &values.literals[0] { Ok(match &values.literals[0].1 {
Lit::Int(l) => l.base10_parse::<$n>()?, Lit::Int(l) => l.base10_parse::<$n>()?,
l => { l => {
let s = l.to_str(); let s = l.to_str();

View File

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

View File

@ -1,14 +1,10 @@
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
// FIXME: Remove this in a foreseeable future. #![deny(broken_intra_doc_links)]
// Currently exists for backwards compatibility to previous Rust versions.
#![recursion_limit = "128"]
#[allow(unused_extern_crates)]
extern crate proc_macro;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote; use quote::quote;
use syn::{parse::Error, parse_macro_input, spanned::Spanned, Lit}; use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit, Type};
pub(crate) mod attributes; pub(crate) mod attributes;
pub(crate) mod consts; pub(crate) mod consts;
@ -41,7 +37,7 @@ macro_rules! match_options {
pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
let mut fun = parse_macro_input!(input as CommandFun); let mut fun = parse_macro_input!(input as CommandFun);
let lit_name = if !attr.is_empty() { let _name = if !attr.is_empty() {
parse_macro_input!(attr as Lit).to_str() parse_macro_input!(attr as Lit).to_str()
} else { } else {
fun.name.to_string() fun.name.to_string()
@ -56,17 +52,40 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
let name = values.name.to_string(); let name = values.name.to_string();
let name = &name[..]; let name = &name[..];
match name {
"arg" => options
.cmd_args
.push(propagate_err!(attributes::parse(values))),
"example" => {
options
.examples
.push(propagate_err!(attributes::parse(values)));
}
"description" => {
let line: String = propagate_err!(attributes::parse(values));
util::append_line(&mut options.description, line);
}
_ => {
match_options!(name, values, options, span => [ match_options!(name, values, options, span => [
permission_level; aliases;
supports_dm; group;
can_blacklist required_permissions;
can_blacklist;
supports_dm
]); ]);
} }
}
}
let Options { let Options {
permission_level, aliases,
supports_dm, description,
group,
examples,
required_permissions,
can_blacklist, can_blacklist,
supports_dm,
mut cmd_args,
} = options; } = options;
let visibility = fun.visibility; let visibility = fun.visibility;
@ -78,25 +97,88 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
let cooked = fun.cooked.clone(); let cooked = fun.cooked.clone();
let command_path = quote!(crate::framework::Command); let command_path = quote!(crate::framework::Command);
let arg_path = quote!(crate::framework::Arg);
populate_fut_lifetimes_on_refs(&mut fun.args); populate_fut_lifetimes_on_refs(&mut fun.args);
let args = fun.args; let args = fun.args;
(quote! { let arg_idents = cmd_args
.iter()
.map(|arg| {
n.with_suffix(arg.name.replace(" ", "_").replace("-", "_").as_str())
.with_suffix(ARG)
})
.collect::<Vec<Ident>>();
let mut tokens = cmd_args
.iter_mut()
.map(|arg| {
let Arg {
name,
description,
kind,
required,
} = arg;
let an = n.with_suffix(name.as_str()).with_suffix(ARG);
quote! {
#(#cooked)* #(#cooked)*
pub static #n: #command_path = #command_path { #[allow(missing_docs)]
func: #name, pub static #an: #arg_path = #arg_path {
name: #lit_name, name: #name,
required_perms: #permission_level, description: #description,
supports_dm: #supports_dm, kind: #kind,
can_blacklist: #can_blacklist, required: #required,
};
}
})
.fold(quote! {}, |mut a, b| {
a.extend(b);
a
});
let variant = if args.len() == 2 {
quote!(crate::framework::CommandFnType::Multi)
} else {
let string: Type = parse_quote!(std::string::String);
let final_arg = args.get(2).unwrap();
if final_arg.kind == string {
quote!(crate::framework::CommandFnType::Text)
} else {
quote!(crate::framework::CommandFnType::Slash)
}
}; };
tokens.extend(quote! {
#(#cooked)*
#[allow(missing_docs)]
pub static #n: #command_path = #command_path {
fun: #variant(#name),
names: &[#_name, #(#aliases),*],
desc: #description,
group: #group,
examples: &[#(#examples),*],
required_permissions: #required_permissions,
can_blacklist: #can_blacklist,
supports_dm: #supports_dm,
args: &[#(&#arg_idents),*],
};
});
tokens.extend(quote! {
#(#cooked)*
#[allow(missing_docs)]
#visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> { #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
use ::serenity::futures::future::FutureExt; use ::serenity::futures::future::FutureExt;
async move { #(#body)* }.boxed() async move {
#(#body)*;
}.boxed()
} }
}) });
.into()
tokens.into()
} }

View File

@ -1,14 +1,14 @@
use crate::util::{Argument, Parenthesised};
use proc_macro2::Span;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use syn::{ use syn::{
braced, braced,
parse::{Error, Parse, ParseStream, Result}, parse::{Error, Parse, ParseStream, Result},
spanned::Spanned, spanned::Spanned,
Attribute, Block, FnArg, Ident, Pat, Path, PathSegment, Stmt, Token, Visibility, Attribute, Block, FnArg, Ident, Pat, Stmt, Token, Visibility,
}; };
use crate::util::{Argument, Parenthesised};
fn parse_argument(arg: FnArg) -> Result<Argument> { fn parse_argument(arg: FnArg) -> Result<Argument> {
match arg { match arg {
FnArg::Typed(typed) => { FnArg::Typed(typed) => {
@ -53,7 +53,7 @@ fn parse_argument(arg: FnArg) -> Result<Argument> {
/// Test if the attribute is cooked. /// Test if the attribute is cooked.
fn is_cooked(attr: &Attribute) -> bool { fn is_cooked(attr: &Attribute) -> bool {
const COOKED_ATTRIBUTE_NAMES: &[&str] = &[ const COOKED_ATTRIBUTE_NAMES: &[&str] = &[
"cfg", "cfg_attr", "doc", "derive", "inline", "allow", "warn", "deny", "forbid", "cfg", "cfg_attr", "derive", "inline", "allow", "warn", "deny", "forbid",
]; ];
COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n)) COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
@ -98,17 +98,6 @@ impl Parse for CommandFun {
fn parse(input: ParseStream<'_>) -> Result<Self> { fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut attributes = input.call(Attribute::parse_outer)?; let mut attributes = input.call(Attribute::parse_outer)?;
// `#[doc = "..."]` is a cooked attribute but it is special-cased for commands.
for attr in &mut attributes {
// Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`.
if attr.path.is_ident("doc") {
attr.path = Path::from(PathSegment::from(Ident::new(
"description",
Span::call_site(),
)));
}
}
let cooked = remove_cooked(&mut attributes); let cooked = remove_cooked(&mut attributes);
let visibility = input.parse::<Visibility>()?; let visibility = input.parse::<Visibility>()?;
@ -155,7 +144,7 @@ impl ToTokens for CommandFun {
stream.extend(quote! { stream.extend(quote! {
#(#cooked)* #(#cooked)*
#visibility async fn #name (#(#args),*) -> () { #visibility async fn #name (#(#args),*) {
#(#body)* #(#body)*
} }
}); });
@ -211,21 +200,98 @@ impl ToTokens for PermissionLevel {
} }
} }
#[derive(Debug)]
pub(crate) enum ApplicationCommandOptionType {
SubCommand,
SubCommandGroup,
String,
Integer,
Boolean,
User,
Channel,
Role,
Mentionable,
Unknown,
}
impl ApplicationCommandOptionType {
pub fn from_str(s: String) -> Self {
match s.as_str() {
"SubCommand" => Self::SubCommand,
"SubCommandGroup" => Self::SubCommandGroup,
"String" => Self::String,
"Integer" => Self::Integer,
"Boolean" => Self::Boolean,
"User" => Self::User,
"Channel" => Self::Channel,
"Role" => Self::Role,
"Mentionable" => Self::Mentionable,
_ => Self::Unknown,
}
}
}
impl ToTokens for ApplicationCommandOptionType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let path = quote!(
serenity::model::interactions::application_command::ApplicationCommandOptionType
);
let variant = match self {
ApplicationCommandOptionType::SubCommand => quote!(SubCommand),
ApplicationCommandOptionType::SubCommandGroup => quote!(SubCommandGroup),
ApplicationCommandOptionType::String => quote!(String),
ApplicationCommandOptionType::Integer => quote!(Integer),
ApplicationCommandOptionType::Boolean => quote!(Boolean),
ApplicationCommandOptionType::User => quote!(User),
ApplicationCommandOptionType::Channel => quote!(Channel),
ApplicationCommandOptionType::Role => quote!(Role),
ApplicationCommandOptionType::Mentionable => quote!(Mentionable),
ApplicationCommandOptionType::Unknown => quote!(Unknown),
};
stream.extend(quote! {
#path::#variant
});
}
}
#[derive(Debug)]
pub(crate) struct Arg {
pub name: String,
pub description: String,
pub kind: ApplicationCommandOptionType,
pub required: bool,
}
impl Default for Arg {
fn default() -> Self {
Self {
name: String::new(),
description: String::new(),
kind: ApplicationCommandOptionType::String,
required: false,
}
}
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Options { pub(crate) struct Options {
pub permission_level: PermissionLevel, pub aliases: Vec<String>,
pub supports_dm: bool, pub description: String,
pub group: String,
pub examples: Vec<String>,
pub required_permissions: PermissionLevel,
pub can_blacklist: bool, pub can_blacklist: bool,
pub supports_dm: bool,
pub cmd_args: Vec<Arg>,
} }
impl Options { impl Options {
#[inline] #[inline]
pub fn new() -> Self { pub fn new() -> Self {
let mut options = Self::default(); Self {
group: "Other".to_string(),
options.can_blacklist = true; ..Default::default()
options.supports_dm = true; }
options
} }
} }

View File

@ -1,6 +1,5 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::Span; use proc_macro2::{Span, TokenStream as TokenStream2};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, ToTokens}; use quote::{format_ident, quote, ToTokens};
use syn::{ use syn::{
braced, bracketed, parenthesized, braced, bracketed, parenthesized,
@ -158,3 +157,20 @@ pub fn populate_fut_lifetimes_on_refs(args: &mut Vec<Argument>) {
} }
} }
} }
pub fn append_line(desc: &mut String, mut line: String) {
if line.starts_with(' ') {
line.remove(0);
}
match line.rfind("\\$") {
Some(i) => {
desc.push_str(line[..i].trim_end());
desc.push(' ');
}
None => {
desc.push_str(&line);
desc.push('\n');
}
}
}

2
rustfmt.toml Normal file
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::{ use std::{
sync::Arc, sync::Arc,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
#[command] use chrono::offset::Utc;
#[can_blacklist(false)] use regex_command_attr::command;
async fn ping(ctx: &Context, msg: &Message, _args: String) { use serenity::{builder::CreateEmbedFooter, client::Context, model::channel::Message};
let now = SystemTime::now();
let since_epoch = now
.duration_since(UNIX_EPOCH)
.expect("Time calculated as going backwards. Very bad");
let delta = since_epoch.as_millis() as i64 - msg.timestamp.timestamp_millis(); use crate::{
consts::DEFAULT_PREFIX,
framework::{CommandInvoke, CreateGenericResponse},
models::{user_data::UserData, CtxData},
FrameworkCtx, THEME_COLOR,
};
let _ = msg fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
.channel_id
.say(&ctx, format!("Time taken to receive message: {}ms", delta))
.await;
}
async fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
let shard_count = ctx.cache.shard_count(); let shard_count = ctx.cache.shard_count();
let shard = ctx.shard_id; let shard = ctx.shard_id;
@ -49,173 +29,105 @@ async fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut Cr
} }
#[command] #[command]
#[can_blacklist(false)] #[aliases("invite")]
async fn help(ctx: &Context, msg: &Message, args: String) { #[description("Get information about the bot")]
async fn default_help( #[group("Info")]
ctx: &Context, async fn info(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) {
msg: &Message, let prefix = ctx.prefix(invoke.guild_id()).await;
lm: Arc<LanguageManager>,
prefix: &str,
language: &str,
) {
let desc = lm.get(language, "help/desc").replace("{prefix}", prefix);
let footer = footer(ctx).await;
let _ = msg
.channel_id
.send_message(ctx, |m| {
m.embed(move |e| {
e.title("Help Menu")
.description(desc)
.field(
lm.get(language, "help/setup_title"),
"`lang` `timezone` `meridian`",
true,
)
.field(
lm.get(language, "help/mod_title"),
"`prefix` `blacklist` `restrict` `alias`",
true,
)
.field(
lm.get(language, "help/reminder_title"),
"`remind` `interval` `natural` `look` `countdown`",
true,
)
.field(
lm.get(language, "help/reminder_mod_title"),
"`del` `offset` `pause` `nudge`",
true,
)
.field(
lm.get(language, "help/info_title"),
"`help` `info` `donate` `clock`",
true,
)
.field(
lm.get(language, "help/todo_title"),
"`todo` `todos` `todoc`",
true,
)
.field(lm.get(language, "help/other_title"), "`timer`", true)
.footer(footer)
.color(*THEME_COLOR)
})
})
.await;
}
let (pool, lm) = get_ctx_data(&ctx).await;
let language = UserData::language_of(&msg.author, &pool);
let prefix = ctx.prefix(msg.guild_id);
if !args.is_empty() {
let framework = ctx
.data
.read()
.await
.get::<FrameworkCtx>()
.cloned()
.expect("Could not get FrameworkCtx from data");
let matched = framework
.commands
.get(args.as_str())
.map(|inner| inner.name);
if let Some(command_name) = matched {
command_help(ctx, msg, lm, &prefix.await, &language.await, command_name).await
} else {
default_help(ctx, msg, lm, &prefix.await, &language.await).await;
}
} else {
default_help(ctx, msg, lm, &prefix.await, &language.await).await;
}
}
#[command]
async fn info(ctx: &Context, msg: &Message, _args: String) {
let (pool, lm) = get_ctx_data(&ctx).await;
let language = UserData::language_of(&msg.author, &pool);
let prefix = ctx.prefix(msg.guild_id);
let current_user = ctx.cache.current_user(); let current_user = ctx.cache.current_user();
let footer = footer(ctx).await; let footer = footer(ctx);
let desc = lm invoke
.get(&language.await, "info") .respond(
.replacen("{user}", &current_user.name, 1) ctx.http.clone(),
.replace("{default_prefix}", &*DEFAULT_PREFIX) CreateGenericResponse::new().embed(|e| {
.replace("{prefix}", &prefix.await);
let _ = msg
.channel_id
.send_message(ctx, |m| {
m.embed(move |e| {
e.title("Info") e.title("Info")
.description(desc) .description(format!(
"Default prefix: `{default_prefix}`
Reset prefix: `@{user} prefix {default_prefix}`
Help: `{prefix}help`
**Welcome to Reminder Bot!**
Developer: <@203532103185465344>
Icon: <@253202252821430272>
Find me on https://discord.jellywx.com and on https://github.com/JellyWX :)
Invite the bot: https://invite.reminder-bot.com/
Use our dashboard: https://reminder-bot.com/",
default_prefix = *DEFAULT_PREFIX,
user = current_user.name,
prefix = prefix
))
.footer(footer) .footer(footer)
.color(*THEME_COLOR) .color(*THEME_COLOR)
}) }),
}) )
.await; .await;
} }
#[command] #[command]
async fn donate(ctx: &Context, msg: &Message, _args: String) { #[description("Details on supporting the bot and Patreon benefits")]
let (pool, lm) = get_ctx_data(&ctx).await; #[group("Info")]
async fn donate(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) {
let language = UserData::language_of(&msg.author, &pool).await; let footer = footer(ctx);
let desc = lm.get(&language, "donate");
let footer = footer(ctx).await; invoke
.respond(
let _ = msg ctx.http.clone(),
.channel_id CreateGenericResponse::new().embed(|e| {
.send_message(ctx, |m| { e.title("Donate")
m.embed(move |e| { .description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :)
e.title("Donate")
.description(desc) **https://www.patreon.com/jellywx/**
.footer(footer) **https://discord.jellywx.com/**
.color(*THEME_COLOR)
}) When you subscribe, Patreon will automatically rank you up on our Discord server (make sure you link your Patreon and Discord accounts!)
}) With your new rank, you'll be able to:
.await; Set repeating reminders with `interval`, `natural` or the dashboard
} Use unlimited uploads on SoundFX
#[command] (Also, members of servers you __own__ will be able to set repeating reminders via commands)
async fn dashboard(ctx: &Context, msg: &Message, _args: String) {
let footer = footer(ctx).await; Just $2 USD/month!
let _ = msg *Please note, you must be in the JellyWX Discord server to receive Patreon features*")
.channel_id .footer(footer)
.send_message(ctx, |m| { .color(*THEME_COLOR)
m.embed(move |e| { }),
e.title("Dashboard") )
.description("https://reminder-bot.com/dashboard") .await;
.footer(footer) }
.color(*THEME_COLOR)
}) #[command]
}) #[description("Get the link to the online dashboard")]
.await; #[group("Info")]
} async fn dashboard(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) {
let footer = footer(ctx);
#[command]
async fn clock(ctx: &Context, msg: &Message, _args: String) { invoke
let (pool, lm) = get_ctx_data(&ctx).await; .respond(
ctx.http.clone(),
let language = UserData::language_of(&msg.author, &pool).await; CreateGenericResponse::new().embed(|e| {
let timezone = UserData::timezone_of(&msg.author, &pool).await; e.title("Dashboard")
.description("**https://reminder-bot.com/dashboard**")
let now = Utc::now().with_timezone(&timezone); .footer(footer)
.color(*THEME_COLOR)
let clock_display = lm.get(&language, "clock/time"); }),
)
let _ = msg .await;
.channel_id }
.say(
&ctx, #[command]
clock_display.replacen("{}", &now.format("%H:%M").to_string(), 1), #[description("View the current time in your selected timezone")]
#[group("Info")]
async fn clock(ctx: &Context, invoke: &(dyn CommandInvoke + Send + Sync)) {
let ud = ctx.user_data(&msg.author).await.unwrap();
let now = Utc::now().with_timezone(ud.timezone());
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(format!("Current time: {}", now.format("%H:%M"))),
) )
.await; .await;
} }

View File

@ -1,4 +1,4 @@
pub mod info_cmds; pub mod info_cmds;
pub mod moderation_cmds; //pub mod moderation_cmds;
pub mod reminder_cmds; //pub mod reminder_cmds;
pub mod todo_cmds; //pub mod todo_cmds;

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::{ use serenity::{
builder::CreateActionRow, builder::CreateActionRow,
client::Context, client::Context,
@ -11,14 +16,6 @@ use serenity::{
}, },
}; };
use chrono_tz::{Tz, TZ_VARIANTS};
use chrono::offset::Utc;
use inflector::Inflector;
use levenshtein::levenshtein;
use crate::{ use crate::{
command_help, command_help,
consts::{REGEX_ALIAS, REGEX_CHANNEL, REGEX_COMMANDS, REGEX_ROLE, THEME_COLOR}, consts::{REGEX_ALIAS, REGEX_CHANNEL, REGEX_COMMANDS, REGEX_ROLE, THEME_COLOR},
@ -28,8 +25,6 @@ use crate::{
FrameworkCtx, PopularTimezones, FrameworkCtx, PopularTimezones,
}; };
use std::{collections::HashMap, iter};
#[command] #[command]
#[supports_dm(false)] #[supports_dm(false)]
#[permission_level(Restricted)] #[permission_level(Restricted)]

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::{ use serenity::{
client::Context, client::Context,
model::{channel::Channel, channel::Message}, model::channel::{Channel, Message},
}; };
use crate::{ use crate::{
@ -16,7 +23,12 @@ use crate::{
models::{ models::{
channel_data::ChannelData, channel_data::ChannelData,
guild_data::GuildData, guild_data::GuildData,
reminder::{builder::ReminderScope, content::Content, look_flags::LookFlags, Reminder}, reminder::{
builder::{MultiReminderBuilder, ReminderScope},
content::Content,
look_flags::LookFlags,
Reminder,
},
timer::Timer, timer::Timer,
user_data::UserData, user_data::UserData,
CtxData, CtxData,
@ -24,17 +36,6 @@ use crate::{
time_parser::{natural_parser, TimeParser}, time_parser::{natural_parser, TimeParser},
}; };
use chrono::NaiveDateTime;
use num_integer::Integer;
use crate::models::reminder::builder::MultiReminderBuilder;
use std::{
default::Default,
string::ToString,
time::{SystemTime, UNIX_EPOCH},
};
#[command] #[command]
#[supports_dm(false)] #[supports_dm(false)]
#[permission_level(Restricted)] #[permission_level(Restricted)]

View File

@ -1,5 +1,6 @@
use regex_command_attr::command; use std::{convert::TryFrom, fmt};
use regex_command_attr::command;
use serenity::{ use serenity::{
async_trait, async_trait,
client::Context, client::Context,
@ -9,15 +10,12 @@ use serenity::{
id::{ChannelId, GuildId, UserId}, id::{ChannelId, GuildId, UserId},
}, },
}; };
use sqlx::MySqlPool;
use std::fmt;
use crate::{ use crate::{
command_help, get_ctx_data, command_help, get_ctx_data,
models::{user_data::UserData, CtxData}, models::{user_data::UserData, CtxData},
}; };
use sqlx::MySqlPool;
use std::convert::TryFrom;
#[derive(Debug)] #[derive(Debug)]
struct TodoNotFound; struct TodoNotFound;

View File

@ -74,9 +74,6 @@ lazy_static! {
pub static ref LOCAL_TIMEZONE: String = pub static ref LOCAL_TIMEZONE: String =
env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string()); env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
pub static ref LOCAL_LANGUAGE: String =
env::var("LOCAL_LANGUAGE").unwrap_or_else(|_| "EN".to_string());
pub static ref DEFAULT_PREFIX: String = pub static ref DEFAULT_PREFIX: String =
env::var("DEFAULT_PREFIX").unwrap_or_else(|_| "$".to_string()); env::var("DEFAULT_PREFIX").unwrap_or_else(|_| "$".to_string());

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::{ use serenity::{
async_trait, async_trait,
builder::{CreateComponents, CreateEmbed},
cache::Cache,
client::Context, client::Context,
constants::MESSAGE_CODE_LIMIT,
framework::Framework, framework::Framework,
futures::prelude::future::BoxFuture, futures::prelude::future::BoxFuture,
http::Http, http::Http,
model::{ model::{
channel::{Channel, GuildChannel, Message}, channel::{Channel, GuildChannel, Message},
guild::{Guild, Member}, guild::{Guild, Member},
id::{ChannelId, MessageId}, id::{ChannelId, GuildId, MessageId, UserId},
interactions::{
application_command::{ApplicationCommandInteraction, ApplicationCommandOptionType},
InteractionResponseType,
}, },
Result as SerenityResult, },
FutureExt, Result as SerenityResult,
}; };
use log::{error, info, warn};
use regex::{Match, Regex, RegexBuilder};
use std::{collections::HashMap, fmt};
use crate::{ use crate::{
language_manager::LanguageManager, models::{channel_data::ChannelData, guild_data::GuildData, CtxData},
models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData, CtxData},
LimitExecutors, SQLPool, LimitExecutors, SQLPool,
}; };
type CommandFn = for<'fut> fn(&'fut Context, &'fut Message, String) -> BoxFuture<'fut, ()>;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum PermissionLevel { pub enum PermissionLevel {
Unrestricted, Unrestricted,
@ -34,29 +38,334 @@ pub enum PermissionLevel {
Restricted, Restricted,
} }
pub struct Command { pub struct Args {
pub name: &'static str, pub args: HashMap<String, String>,
pub required_perms: PermissionLevel,
pub supports_dm: bool,
pub can_blacklist: bool,
pub func: CommandFn,
} }
impl Args {
pub fn named<D: ToString>(&self, name: D) -> Option<&String> {
let name = name.to_string();
self.args.get(&name)
}
}
pub struct CreateGenericResponse {
content: String,
embed: Option<CreateEmbed>,
components: Option<CreateComponents>,
}
impl CreateGenericResponse {
pub fn new() -> Self {
Self {
content: "".to_string(),
embed: None,
components: None,
}
}
pub fn content<D: ToString>(mut self, content: D) -> Self {
self.content = content.to_string();
self
}
pub fn embed<F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed>(mut self, f: F) -> Self {
let mut embed = CreateEmbed::default();
f(&mut embed);
self.embed = Some(embed);
self
}
pub fn components<F: FnOnce(&mut CreateComponents) -> &mut CreateComponents>(
mut self,
f: F,
) -> Self {
let mut components = CreateComponents::default();
f(&mut components);
self.components = Some(components);
self
}
}
#[async_trait]
pub trait CommandInvoke {
fn channel_id(&self) -> ChannelId;
fn guild_id(&self) -> Option<GuildId>;
fn guild(&self, cache: Arc<Cache>) -> Option<Guild>;
fn author_id(&self) -> UserId;
async fn member(&self, context: &Context) -> SerenityResult<Member>;
fn msg(&self) -> Option<Message>;
fn interaction(&self) -> Option<ApplicationCommandInteraction>;
async fn respond(
&self,
http: Arc<Http>,
generic_response: CreateGenericResponse,
) -> SerenityResult<()>;
async fn followup(
&self,
http: Arc<Http>,
generic_response: CreateGenericResponse,
) -> SerenityResult<()>;
}
#[async_trait]
impl CommandInvoke for Message {
fn channel_id(&self) -> ChannelId {
self.channel_id
}
fn guild_id(&self) -> Option<GuildId> {
self.guild_id
}
fn guild(&self, cache: Arc<Cache>) -> Option<Guild> {
self.guild(cache)
}
fn author_id(&self) -> UserId {
self.author.id
}
async fn member(&self, context: &Context) -> SerenityResult<Member> {
self.member(context).await
}
fn msg(&self) -> Option<Message> {
Some(self.clone())
}
fn interaction(&self) -> Option<ApplicationCommandInteraction> {
None
}
async fn respond(
&self,
http: Arc<Http>,
generic_response: CreateGenericResponse,
) -> SerenityResult<()> {
self.channel_id
.send_message(http, |m| {
m.content(generic_response.content);
if let Some(embed) = generic_response.embed {
m.set_embed(embed.clone());
}
if let Some(components) = generic_response.components {
m.components(|c| {
*c = components;
c
});
}
m
})
.await
.map(|_| ())
}
async fn followup(
&self,
http: Arc<Http>,
generic_response: CreateGenericResponse,
) -> SerenityResult<()> {
self.channel_id
.send_message(http, |m| {
m.content(generic_response.content);
if let Some(embed) = generic_response.embed {
m.set_embed(embed.clone());
}
if let Some(components) = generic_response.components {
m.components(|c| {
*c = components;
c
});
}
m
})
.await
.map(|_| ())
}
}
#[async_trait]
impl CommandInvoke for ApplicationCommandInteraction {
fn channel_id(&self) -> ChannelId {
self.channel_id
}
fn guild_id(&self) -> Option<GuildId> {
self.guild_id
}
fn guild(&self, cache: Arc<Cache>) -> Option<Guild> {
if let Some(guild_id) = self.guild_id {
guild_id.to_guild_cached(cache)
} else {
None
}
}
fn author_id(&self) -> UserId {
self.member.as_ref().unwrap().user.id
}
async fn member(&self, _: &Context) -> SerenityResult<Member> {
Ok(self.member.clone().unwrap())
}
fn msg(&self) -> Option<Message> {
None
}
fn interaction(&self) -> Option<ApplicationCommandInteraction> {
Some(self.clone())
}
async fn respond(
&self,
http: Arc<Http>,
generic_response: CreateGenericResponse,
) -> SerenityResult<()> {
self.create_interaction_response(http, |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.content(generic_response.content);
if let Some(embed) = generic_response.embed {
d.add_embed(embed.clone());
}
if let Some(components) = generic_response.components {
d.components(|c| {
*c = components;
c
});
}
d
})
})
.await
.map(|_| ())
}
async fn followup(
&self,
http: Arc<Http>,
generic_response: CreateGenericResponse,
) -> SerenityResult<()> {
self.create_followup_message(http, |d| {
d.content(generic_response.content);
if let Some(embed) = generic_response.embed {
d.add_embed(embed.clone());
}
if let Some(components) = generic_response.components {
d.components(|c| {
*c = components;
c
});
}
d
})
.await
.map(|_| ())
}
}
#[derive(Debug)]
pub struct Arg {
pub name: &'static str,
pub description: &'static str,
pub kind: ApplicationCommandOptionType,
pub required: bool,
}
type SlashCommandFn = for<'fut> fn(
&'fut Context,
&'fut (dyn CommandInvoke + Sync + Send),
Args,
) -> BoxFuture<'fut, ()>;
type TextCommandFn = for<'fut> fn(
&'fut Context,
&'fut (dyn CommandInvoke + Sync + Send),
String,
) -> BoxFuture<'fut, ()>;
type MultiCommandFn =
for<'fut> fn(&'fut Context, &'fut (dyn CommandInvoke + Sync + Send)) -> BoxFuture<'fut, ()>;
pub enum CommandFnType {
Slash(SlashCommandFn),
Text(TextCommandFn),
Multi(MultiCommandFn),
}
impl CommandFnType {
pub fn text(&self) -> Option<&TextCommandFn> {
match self {
CommandFnType::Text(t) => Some(t),
_ => None,
}
}
}
pub struct Command {
pub fun: CommandFnType,
pub names: &'static [&'static str],
pub desc: &'static str,
pub examples: &'static [&'static str],
pub group: &'static str,
pub required_permissions: PermissionLevel,
pub args: &'static [&'static Arg],
pub can_blacklist: bool,
pub supports_dm: bool,
}
impl Hash for Command {
fn hash<H: Hasher>(&self, state: &mut H) {
self.names[0].hash(state)
}
}
impl PartialEq for Command {
fn eq(&self, other: &Self) -> bool {
self.names[0] == other.names[0]
}
}
impl Eq for Command {}
impl Command { impl Command {
async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool { async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool {
if self.required_perms == PermissionLevel::Unrestricted { if self.required_permissions == PermissionLevel::Unrestricted {
true true
} else { } else {
let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap(); let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap();
if permissions.manage_guild() if permissions.manage_guild()
|| (permissions.manage_messages() || (permissions.manage_messages()
&& self.required_perms == PermissionLevel::Managed) && self.required_permissions == PermissionLevel::Managed)
{ {
return true; return true;
} }
if self.required_perms == PermissionLevel::Managed { if self.required_permissions == PermissionLevel::Managed {
let pool = ctx let pool = ctx
.data .data
.read() .read()
@ -83,7 +392,7 @@ WHERE
WHERE WHERE
guild = ?) guild = ?)
", ",
self.name, self.names[0],
guild.id.as_u64() guild.id.as_u64()
) )
.fetch_all(&pool) .fetch_all(&pool)
@ -123,62 +432,9 @@ WHERE
} }
} }
impl fmt::Debug for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Command")
.field("name", &self.name)
.field("required_perms", &self.required_perms)
.field("supports_dm", &self.supports_dm)
.field("can_blacklist", &self.can_blacklist)
.finish()
}
}
#[async_trait]
pub trait SendIterator {
async fn say_lines(
self,
http: impl AsRef<Http> + Send + Sync + 'async_trait,
content: impl Iterator<Item = String> + Send + 'async_trait,
) -> SerenityResult<()>;
}
#[async_trait]
impl SendIterator for ChannelId {
async fn say_lines(
self,
http: impl AsRef<Http> + Send + Sync + 'async_trait,
content: impl Iterator<Item = String> + Send + 'async_trait,
) -> SerenityResult<()> {
let mut current_content = String::new();
for line in content {
if current_content.len() + line.len() > MESSAGE_CODE_LIMIT as usize {
self.send_message(&http, |m| {
m.allowed_mentions(|am| am.empty_parse())
.content(&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 struct RegexFramework {
pub commands: HashMap<String, &'static Command>, pub commands_map: HashMap<String, &'static Command>,
pub commands: HashSet<&'static Command>,
command_matcher: Regex, command_matcher: Regex,
dm_regex_matcher: Regex, dm_regex_matcher: Regex,
default_prefix: String, default_prefix: String,
@ -186,12 +442,23 @@ pub struct RegexFramework {
ignore_bots: bool, ignore_bots: bool,
case_insensitive: bool, case_insensitive: bool,
dm_enabled: bool, dm_enabled: bool,
default_text_fun: TextCommandFn,
}
fn drop_text<'fut>(
_: &'fut Context,
_: &'fut (dyn CommandInvoke + Sync + Send),
_: String,
) -> std::pin::Pin<std::boxed::Box<(dyn std::future::Future<Output = ()> + std::marker::Send + 'fut)>>
{
async move {}.boxed()
} }
impl RegexFramework { impl RegexFramework {
pub fn new<T: Into<u64>>(client_id: T) -> Self { pub fn new<T: Into<u64>>(client_id: T) -> Self {
Self { Self {
commands: HashMap::new(), commands_map: HashMap::new(),
commands: HashSet::new(),
command_matcher: Regex::new(r#"^$"#).unwrap(), command_matcher: Regex::new(r#"^$"#).unwrap(),
dm_regex_matcher: Regex::new(r#"^$"#).unwrap(), dm_regex_matcher: Regex::new(r#"^$"#).unwrap(),
default_prefix: "".to_string(), default_prefix: "".to_string(),
@ -199,6 +466,7 @@ impl RegexFramework {
ignore_bots: true, ignore_bots: true,
case_insensitive: true, case_insensitive: true,
dm_enabled: true, dm_enabled: true,
default_text_fun: drop_text,
} }
} }
@ -226,8 +494,12 @@ impl RegexFramework {
self self
} }
pub fn add_command<S: ToString>(mut self, name: S, command: &'static Command) -> Self { pub fn add_command(mut self, command: &'static Command) -> Self {
self.commands.insert(name.to_string(), command); self.commands.insert(command);
for name in command.names {
self.commands_map.insert(name.to_string(), command);
}
self self
} }
@ -237,8 +509,11 @@ impl RegexFramework {
let command_names; let command_names;
{ {
let mut command_names_vec = let mut command_names_vec = self
self.commands.keys().map(|k| &k[..]).collect::<Vec<&str>>(); .commands_map
.keys()
.map(|k| &k[..])
.collect::<Vec<&str>>();
command_names_vec.sort_unstable_by_key(|a| a.len()); command_names_vec.sort_unstable_by_key(|a| a.len());
@ -265,7 +540,7 @@ impl RegexFramework {
{ {
let mut command_names_vec = self let mut command_names_vec = self
.commands .commands_map
.iter() .iter()
.filter_map(|(key, command)| { .filter_map(|(key, command)| {
if command.supports_dm { if command.supports_dm {
@ -359,15 +634,11 @@ impl Framework for RegexFramework {
if let Some(full_match) = self.command_matcher.captures(&msg.content) { if let Some(full_match) = self.command_matcher.captures(&msg.content) {
if check_prefix(&ctx, &guild, full_match.name("prefix")).await { if check_prefix(&ctx, &guild, full_match.name("prefix")).await {
let lm = data.get::<LanguageManager>().unwrap();
let language = UserData::language_of(&msg.author, &pool);
match check_self_permissions(&ctx, &guild, &channel).await { match check_self_permissions(&ctx, &guild, &channel).await {
Ok(perms) => match perms { Ok(perms) => match perms {
PermissionCheck::All => { PermissionCheck::All => {
let command = self let command = self
.commands .commands_map
.get( .get(
&full_match &full_match
.name("cmd") .name("cmd")
@ -394,8 +665,6 @@ impl Framework for RegexFramework {
let member = guild.member(&ctx, &msg.author).await.unwrap(); let member = guild.member(&ctx, &msg.author).await.unwrap();
if command.check_permissions(&ctx, &guild, &member).await { if command.check_permissions(&ctx, &guild, &member).await {
dbg!(command.name);
{ {
let guild_id = guild.id.as_u64().to_owned(); let guild_id = guild.id.as_u64().to_owned();
@ -413,30 +682,34 @@ impl Framework for RegexFramework {
|| !ctx.check_executing(msg.author.id).await || !ctx.check_executing(msg.author.id).await
{ {
ctx.set_executing(msg.author.id).await; ctx.set_executing(msg.author.id).await;
(command.func)(&ctx, &msg, args).await;
match command.fun {
CommandFnType::Text(t) => t(&ctx, &msg, args),
CommandFnType::Multi(m) => m(&ctx, &msg),
_ => (self.default_text_fun)(&ctx, &msg, args),
}
.await;
ctx.drop_executing(msg.author.id).await; ctx.drop_executing(msg.author.id).await;
} }
} else if command.required_perms } else if command.required_permissions
== PermissionLevel::Restricted == PermissionLevel::Restricted
{ {
let _ = msg let _ = msg
.channel_id .channel_id
.say( .say(
&ctx, &ctx,
lm.get(&language.await, "no_perms_restricted"), "You must have the `Manage Server` permission to use this command.",
) )
.await; .await;
} else if command.required_perms == PermissionLevel::Managed } else if command.required_permissions
== PermissionLevel::Managed
{ {
let _ = msg let _ = msg
.channel_id .channel_id
.say( .say(
&ctx, &ctx,
lm.get(&language.await, "no_perms_managed") "You must have `Manage Messages` or have a role capable of sending reminders to that channel. Please talk to your server admin, and ask them to use the `/restrict` command to specify allowed roles.",
.replace(
"{prefix}",
&ctx.prefix(msg.guild_id).await,
),
) )
.await; .await;
} }
@ -444,18 +717,21 @@ impl Framework for RegexFramework {
} }
PermissionCheck::Basic(manage_webhooks, embed_links) => { PermissionCheck::Basic(manage_webhooks, embed_links) => {
let response = lm let _ = msg
.get(&language.await, "no_perms_general") .channel_id
.replace( .say(
"{manage_webhooks}", &ctx,
if manage_webhooks { "" } else { "" }, format!(
) "Please ensure the bot has the correct permissions:
.replace(
"{embed_links}",
if embed_links { "" } else { "" },
);
let _ = msg.channel_id.say(&ctx, response).await; **Send Message**
{} **Embed Links**
{} **Manage Webhooks**",
if manage_webhooks { "" } else { "" },
if embed_links { "" } else { "" },
),
)
.await;
} }
PermissionCheck::None => { PermissionCheck::None => {
@ -477,7 +753,7 @@ impl Framework for RegexFramework {
else if self.dm_enabled { else if self.dm_enabled {
if let Some(full_match) = self.dm_regex_matcher.captures(&msg.content[..]) { if let Some(full_match) = self.dm_regex_matcher.captures(&msg.content[..]) {
let command = self let command = self
.commands .commands_map
.get(&full_match.name("cmd").unwrap().as_str().to_lowercase()) .get(&full_match.name("cmd").unwrap().as_str().to_lowercase())
.unwrap(); .unwrap();
let args = full_match let args = full_match
@ -486,11 +762,16 @@ impl Framework for RegexFramework {
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
dbg!(command.name);
if msg.id == MessageId(0) || !ctx.check_executing(msg.author.id).await { if msg.id == MessageId(0) || !ctx.check_executing(msg.author.id).await {
ctx.set_executing(msg.author.id).await; ctx.set_executing(msg.author.id).await;
(command.func)(&ctx, &msg, args).await;
match command.fun {
CommandFnType::Text(t) => t(&ctx, &msg, args),
CommandFnType::Multi(m) => m(&ctx, &msg),
_ => (self.default_text_fun)(&ctx, &msg, args),
}
.await;
ctx.drop_executing(msg.author.id).await; ctx.drop_executing(msg.author.id).await;
} }
} }

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 commands;
mod consts; mod consts;
mod framework; mod framework;
mod language_manager;
mod models; mod models;
mod time_parser; mod time_parser;
use std::{collections::HashMap, env, sync::Arc, time::Instant};
use chrono::Utc;
use chrono_tz::Tz;
use dashmap::DashMap;
use dotenv::dotenv;
use inflector::Inflector;
use log::info;
use serenity::{ use serenity::{
async_trait, async_trait,
cache::Cache, cache::Cache,
@ -15,8 +22,7 @@ use serenity::{
futures::TryFutureExt, futures::TryFutureExt,
http::{client::Http, CacheHttp}, http::{client::Http, CacheHttp},
model::{ model::{
channel::GuildChannel, channel::{GuildChannel, Message},
channel::Message,
guild::{Guild, GuildUnavailable}, guild::{Guild, GuildUnavailable},
id::{GuildId, UserId}, id::{GuildId, UserId},
interactions::{ interactions::{
@ -26,18 +32,13 @@ use serenity::{
prelude::{Context, EventHandler, TypeMapKey}, prelude::{Context, EventHandler, TypeMapKey},
utils::shard_id, utils::shard_id,
}; };
use sqlx::mysql::MySqlPool; use sqlx::mysql::MySqlPool;
use tokio::sync::RwLock;
use dotenv::dotenv;
use std::{collections::HashMap, env, sync::Arc, time::Instant};
use crate::{ use crate::{
commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds}, commands::info_cmds,
consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR}, consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR},
framework::RegexFramework, framework::RegexFramework,
language_manager::LanguageManager,
models::{ models::{
guild_data::GuildData, guild_data::GuildData,
reminder::{Reminder, ReminderAction}, reminder::{Reminder, ReminderAction},
@ -45,17 +46,6 @@ use crate::{
}, },
}; };
use inflector::Inflector;
use log::info;
use dashmap::DashMap;
use tokio::sync::RwLock;
use chrono::Utc;
use chrono_tz::Tz;
struct GuildDataCache; struct GuildDataCache;
impl TypeMapKey for GuildDataCache { impl TypeMapKey for GuildDataCache {
@ -266,128 +256,6 @@ DELETE FROM guilds WHERE guild = ?
.await .await
.unwrap(); .unwrap();
} }
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
let (pool, lm) = get_ctx_data(&&ctx).await;
match interaction {
Interaction::MessageComponent(component) => {
if component.data.custom_id.starts_with("timezone:") {
let mut user_data = UserData::from_user(&component.user, &ctx, &pool)
.await
.unwrap();
let new_timezone = component
.data
.custom_id
.replace("timezone:", "")
.parse::<Tz>();
if let Ok(timezone) = new_timezone {
user_data.timezone = timezone.to_string();
user_data.commit_changes(&pool).await;
let _ = component.create_interaction_response(&ctx, |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
let footer_text = lm.get(&user_data.language, "timezone/footer").replacen(
"{timezone}",
&user_data.timezone,
1,
);
let now = Utc::now().with_timezone(&user_data.timezone());
let content = lm
.get(&user_data.language, "timezone/set_p")
.replacen("{timezone}", &user_data.timezone, 1)
.replacen(
"{time}",
&now.format("%H:%M").to_string(),
1,
);
d.create_embed(|e| e.title(lm.get(&user_data.language, "timezone/set_p_title"))
.color(*THEME_COLOR)
.description(content)
.footer(|f| f.text(footer_text)))
.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
d
})
}).await;
}
} else if component.data.custom_id.starts_with("lang:") {
let mut user_data = UserData::from_user(&component.user, &ctx, &pool)
.await
.unwrap();
let lang_code = component.data.custom_id.replace("lang:", "");
if let Some(lang) = lm.get_language(&lang_code) {
user_data.language = lang.to_string();
user_data.commit_changes(&pool).await;
let _ = component
.create_interaction_response(&ctx, |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.create_embed(|e| {
e.title(
lm.get(&user_data.language, "lang/set_p_title"),
)
.color(*THEME_COLOR)
.description(
lm.get(&user_data.language, "lang/set_p"),
)
})
.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
})
})
.await;
}
} else {
match Reminder::from_interaction(
&ctx,
component.user.id,
component.data.custom_id.clone(),
)
.await
{
Ok((reminder, action)) => {
let response = match action {
ReminderAction::Delete => {
reminder.delete(&ctx).await;
"Reminder has been deleted"
}
};
let _ = component
.create_interaction_response(&ctx, |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| d
.content(response)
.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
)
})
.await;
}
Err(ie) => {
let _ = component
.create_interaction_response(&ctx, |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| d
.content(ie.to_string())
.flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
)
})
.await;
}
}
}
}
_ => {}
}
}
} }
#[tokio::main] #[tokio::main]
@ -414,14 +282,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1")) .ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1"))
.dm_enabled(dm_enabled) .dm_enabled(dm_enabled)
// info commands // info commands
.add_command("ping", &info_cmds::PING_COMMAND) //.add_command("help", &info_cmds::HELP_COMMAND)
.add_command("help", &info_cmds::HELP_COMMAND) .add_command(&info_cmds::INFO_COMMAND)
.add_command("info", &info_cmds::INFO_COMMAND) .add_command(&info_cmds::DONATE_COMMAND)
.add_command("invite", &info_cmds::INFO_COMMAND) //.add_command("dashboard", &info_cmds::DASHBOARD_COMMAND)
.add_command("donate", &info_cmds::DONATE_COMMAND) //.add_command("clock", &info_cmds::CLOCK_COMMAND)
.add_command("dashboard", &info_cmds::DASHBOARD_COMMAND)
.add_command("clock", &info_cmds::CLOCK_COMMAND)
// reminder commands // reminder commands
/*
.add_command("timer", &reminder_cmds::TIMER_COMMAND) .add_command("timer", &reminder_cmds::TIMER_COMMAND)
.add_command("remind", &reminder_cmds::REMIND_COMMAND) .add_command("remind", &reminder_cmds::REMIND_COMMAND)
.add_command("r", &reminder_cmds::REMIND_COMMAND) .add_command("r", &reminder_cmds::REMIND_COMMAND)
@ -452,6 +319,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.add_command("nudge", &reminder_cmds::NUDGE_COMMAND) .add_command("nudge", &reminder_cmds::NUDGE_COMMAND)
.add_command("alias", &moderation_cmds::ALIAS_COMMAND) .add_command("alias", &moderation_cmds::ALIAS_COMMAND)
.add_command("a", &moderation_cmds::ALIAS_COMMAND) .add_command("a", &moderation_cmds::ALIAS_COMMAND)
*/
.build(); .build();
let framework_arc = Arc::new(framework); let framework_arc = Arc::new(framework);
@ -460,13 +328,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.intents(if dm_enabled { .intents(if dm_enabled {
GatewayIntents::GUILD_MESSAGES GatewayIntents::GUILD_MESSAGES
| GatewayIntents::GUILDS | GatewayIntents::GUILDS
| GatewayIntents::GUILD_MESSAGE_REACTIONS
| GatewayIntents::DIRECT_MESSAGES | GatewayIntents::DIRECT_MESSAGES
| GatewayIntents::DIRECT_MESSAGE_REACTIONS
} else { } else {
GatewayIntents::GUILD_MESSAGES GatewayIntents::GUILD_MESSAGES | GatewayIntents::GUILDS
| GatewayIntents::GUILDS
| GatewayIntents::GUILD_MESSAGE_REACTIONS
}) })
.application_id(application_id.0) .application_id(application_id.0)
.event_handler(Handler) .event_handler(Handler)
@ -483,13 +347,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
.await .await
.unwrap(); .unwrap();
let language_manager = LanguageManager::from_compiled(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/",
env!("STRINGS_FILE")
)))
.unwrap();
let popular_timezones = sqlx::query!( let popular_timezones = sqlx::query!(
"SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21" "SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
) )
@ -508,7 +365,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
data.insert::<PopularTimezones>(Arc::new(popular_timezones)); data.insert::<PopularTimezones>(Arc::new(popular_timezones));
data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new())); data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
data.insert::<FrameworkCtx>(framework_arc.clone()); data.insert::<FrameworkCtx>(framework_arc.clone());
data.insert::<LanguageManager>(Arc::new(language_manager))
} }
if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| { if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
@ -585,54 +441,3 @@ pub async fn check_subscription_on_message(
false false
} }
} }
pub async fn get_ctx_data(ctx: &&Context) -> (MySqlPool, Arc<LanguageManager>) {
let pool;
let lm;
{
let data = ctx.data.read().await;
pool = data
.get::<SQLPool>()
.cloned()
.expect("Could not get SQLPool");
lm = data
.get::<LanguageManager>()
.cloned()
.expect("Could not get LanguageManager");
}
(pool, lm)
}
async fn command_help(
ctx: &Context,
msg: &Message,
lm: Arc<LanguageManager>,
prefix: &str,
language: &str,
command_name: &str,
) {
let _ = msg
.channel_id
.send_message(ctx, |m| {
m.embed(move |e| {
e.title(format!("{} Help", command_name.to_title_case()))
.description(
lm.get(language, &format!("help/{}", command_name))
.replace("{prefix}", prefix),
)
.footer(|f| {
f.text(concat!(
env!("CARGO_PKG_NAME"),
" ver ",
env!("CARGO_PKG_VERSION")
))
})
.color(*THEME_COLOR)
})
})
.await;
}

View File

@ -1,8 +1,6 @@
use serenity::model::channel::Channel;
use sqlx::MySqlPool;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serenity::model::channel::Channel;
use sqlx::MySqlPool;
pub struct ChannelData { pub struct ChannelData {
pub id: u32, pub id: u32,

View File

@ -1,8 +1,6 @@
use serenity::model::guild::Guild;
use sqlx::MySqlPool;
use log::error; use log::error;
use serenity::model::guild::Guild;
use sqlx::MySqlPool;
use crate::consts::DEFAULT_PREFIX; use crate::consts::DEFAULT_PREFIX;

View File

@ -4,22 +4,18 @@ pub mod reminder;
pub mod timer; pub mod timer;
pub mod user_data; pub mod user_data;
use std::sync::Arc;
use guild_data::GuildData;
use serenity::{ use serenity::{
async_trait, async_trait,
model::id::{GuildId, UserId}, model::id::{GuildId, UserId},
prelude::Context, prelude::Context,
}; };
use crate::{consts::DEFAULT_PREFIX, GuildDataCache, SQLPool};
use guild_data::GuildData;
use crate::models::user_data::UserData;
use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{consts::DEFAULT_PREFIX, models::user_data::UserData, GuildDataCache, SQLPool};
#[async_trait] #[async_trait]
pub trait CtxData { pub trait CtxData {
async fn guild_data<G: Into<GuildId> + Send + Sync>( async fn guild_data<G: Into<GuildId> + Send + Sync>(

View File

@ -1,3 +1,7 @@
use std::{collections::HashSet, fmt::Display};
use chrono::{Duration, NaiveDateTime, Utc};
use chrono_tz::Tz;
use serenity::{ use serenity::{
client::Context, client::Context,
http::CacheHttp, http::CacheHttp,
@ -8,9 +12,7 @@ use serenity::{
}, },
Result as SerenityResult, Result as SerenityResult,
}; };
use sqlx::MySqlPool;
use chrono::{Duration, NaiveDateTime, Utc};
use chrono_tz::Tz;
use crate::{ use crate::{
consts::{MAX_TIME, MIN_INTERVAL}, consts::{MAX_TIME, MIN_INTERVAL},
@ -23,10 +25,6 @@ use crate::{
SQLPool, SQLPool,
}; };
use sqlx::MySqlPool;
use std::{collections::HashSet, fmt::Display};
async fn create_webhook( async fn create_webhook(
ctx: impl CacheHttp, ctx: impl CacheHttp,
channel: GuildChannel, channel: GuildChannel,

View File

@ -1,6 +1,5 @@
use serenity::model::{channel::Message, guild::Guild, misc::Mentionable};
use regex::Captures; use regex::Captures;
use serenity::model::{channel::Message, guild::Guild, misc::Mentionable};
use crate::{consts::REGEX_CONTENT_SUBSTITUTION, models::reminder::errors::ContentError}; use crate::{consts::REGEX_CONTENT_SUBSTITUTION, models::reminder::errors::ContentError};

View File

@ -1,9 +1,8 @@
use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE};
use num_integer::Integer; use num_integer::Integer;
use rand::{rngs::OsRng, seq::IteratorRandom}; use rand::{rngs::OsRng, seq::IteratorRandom};
use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE};
pub fn longhand_displacement(seconds: u64) -> String { pub fn longhand_displacement(seconds: u64) -> String {
let (days, seconds) = seconds.div_rem(&DAY); let (days, seconds) = seconds.div_rem(&DAY);
let (hours, seconds) = seconds.div_rem(&HOUR); let (hours, seconds) = seconds.div_rem(&HOUR);

View File

@ -4,13 +4,19 @@ pub mod errors;
mod helper; mod helper;
pub mod look_flags; pub mod look_flags;
use serenity::{ use std::{
client::Context, convert::{TryFrom, TryInto},
model::id::{ChannelId, GuildId, UserId}, env,
}; };
use chrono::{NaiveDateTime, TimeZone}; use chrono::{NaiveDateTime, TimeZone};
use chrono_tz::Tz; use chrono_tz::Tz;
use ring::hmac;
use serenity::{
client::Context,
model::id::{ChannelId, GuildId, UserId},
};
use sqlx::MySqlPool;
use crate::{ use crate::{
models::reminder::{ models::reminder::{
@ -21,14 +27,6 @@ use crate::{
SQLPool, SQLPool,
}; };
use ring::hmac;
use sqlx::MySqlPool;
use std::{
convert::{TryFrom, TryInto},
env,
};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum ReminderAction { pub enum ReminderAction {
Delete, Delete,

View File

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

View File

@ -1,47 +1,22 @@
use chrono_tz::Tz;
use log::error;
use serenity::{ use serenity::{
http::CacheHttp, http::CacheHttp,
model::{id::UserId, user::User}, model::{id::UserId, user::User},
}; };
use sqlx::MySqlPool; use sqlx::MySqlPool;
use chrono_tz::Tz; use crate::consts::LOCAL_TIMEZONE;
use log::error;
use crate::consts::{LOCAL_LANGUAGE, LOCAL_TIMEZONE};
pub struct UserData { pub struct UserData {
pub id: u32, pub id: u32,
pub user: u64, pub user: u64,
pub name: String, pub name: String,
pub dm_channel: u32, pub dm_channel: u32,
pub language: String,
pub timezone: String, pub timezone: String,
} }
impl UserData { impl UserData {
pub async fn language_of<U>(user: U, pool: &MySqlPool) -> String
where
U: Into<UserId>,
{
let user_id = user.into().as_u64().to_owned();
match sqlx::query!(
"
SELECT language FROM users WHERE user = ?
",
user_id
)
.fetch_one(pool)
.await
{
Ok(r) => r.language,
Err(_) => LOCAL_LANGUAGE.clone(),
}
}
pub async fn timezone_of<U>(user: U, pool: &MySqlPool) -> Tz pub async fn timezone_of<U>(user: U, pool: &MySqlPool) -> Tz
where where
U: Into<UserId>, U: Into<UserId>,
@ -75,9 +50,9 @@ SELECT timezone FROM users WHERE user = ?
match sqlx::query_as_unchecked!( match sqlx::query_as_unchecked!(
Self, Self,
" "
SELECT id, user, name, dm_channel, IF(language IS NULL, ?, language) AS language, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ? SELECT id, user, name, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
", ",
*LOCAL_LANGUAGE, *LOCAL_TIMEZONE, user_id *LOCAL_TIMEZONE, user_id
) )
.fetch_one(pool) .fetch_one(pool)
.await .await
@ -101,15 +76,15 @@ INSERT IGNORE INTO channels (channel) VALUES (?)
sqlx::query!( sqlx::query!(
" "
INSERT INTO users (user, name, dm_channel, language, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?, ?) INSERT INTO users (user, name, dm_channel, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?)
", user_id, user.name, dm_id, *LOCAL_LANGUAGE, *LOCAL_TIMEZONE) ", user_id, user.name, dm_id, *LOCAL_TIMEZONE)
.execute(&pool_c) .execute(&pool_c)
.await?; .await?;
Ok(sqlx::query_as_unchecked!( Ok(sqlx::query_as_unchecked!(
Self, Self,
" "
SELECT id, user, name, dm_channel, language, timezone FROM users WHERE user = ? SELECT id, user, name, dm_channel, timezone FROM users WHERE user = ?
", ",
user_id user_id
) )
@ -128,10 +103,9 @@ SELECT id, user, name, dm_channel, language, timezone FROM users WHERE user = ?
pub async fn commit_changes(&self, pool: &MySqlPool) { pub async fn commit_changes(&self, pool: &MySqlPool) {
sqlx::query!( sqlx::query!(
" "
UPDATE users SET name = ?, language = ?, timezone = ? WHERE id = ? UPDATE users SET name = ?, timezone = ? WHERE id = ?
", ",
self.name, self.name,
self.language,
self.timezone, self.timezone,
self.id self.id
) )

View File

@ -1,15 +1,16 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
convert::TryFrom,
use std::fmt::{Display, Formatter, Result as FmtResult}; fmt::{Display, Formatter, Result as FmtResult},
str::from_utf8,
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION}; time::{SystemTime, UNIX_EPOCH},
};
use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono::{DateTime, Datelike, Timelike, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use std::convert::TryFrom;
use std::str::from_utf8;
use tokio::process::Command; use tokio::process::Command;
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
#[derive(Debug)] #[derive(Debug)]
pub enum InvalidTime { pub enum InvalidTime {
ParseErrorDMY, ParseErrorDMY,