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

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

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,