aboutsummaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
authorFuwn <[email protected]>2020-10-26 19:03:53 -0700
committerFuwn <[email protected]>2020-10-26 19:03:53 -0700
commit9742614a1dc4699c1f2c69d923d402237672335d (patch)
treea49f7d834372f37cef06b30a28ff1b40bdfaa079 /src/core
parentCreate README.md (diff)
downloaddep-core-next-9742614a1dc4699c1f2c69d923d402237672335d.tar.xz
dep-core-next-9742614a1dc4699c1f2c69d923d402237672335d.zip
repo: push main from local to remote
Diffstat (limited to 'src/core')
-rw-r--r--src/core/api.rs332
-rw-r--r--src/core/colours.rs8
-rw-r--r--src/core/consts.rs51
-rw-r--r--src/core/framework.rs206
-rw-r--r--src/core/handler.rs572
-rw-r--r--src/core/mod.rs8
-rw-r--r--src/core/model.rs33
-rw-r--r--src/core/timers.rs160
-rw-r--r--src/core/utils.rs302
9 files changed, 1672 insertions, 0 deletions
diff --git a/src/core/api.rs b/src/core/api.rs
new file mode 100644
index 0000000..0d28878
--- /dev/null
+++ b/src/core/api.rs
@@ -0,0 +1,332 @@
+use forecast::{
+ ApiClient as DSClient,
+ ApiResponse,
+ ForecastRequestBuilder,
+ Lang,
+ Units
+};
+use geocoding::Opencage;
+use kitsu::model::{
+ Response,
+ Anime,Manga
+};
+use kitsu::{
+ KitsuReqwestRequester,
+ Error as KitsuErr
+};
+use reqwest::header::{
+ Headers,
+ UserAgent,
+ ContentType,
+ Accept,
+ Authorization,
+ qitem
+};
+use reqwest::mime;
+use reqwest::{
+ Client,
+ Result as ReqwestResult,
+ Response as ReqwestResponse
+};
+use std::collections::HashMap;
+use std::env;
+use urbandictionary::{
+ ReqwestUrbanDictionaryRequester,
+ Result as UrbanResult,
+ model::Response as UrbanResponse
+};
+
+const UA: &str = "wisp-bot";
+
+// Endpoints
+const DOG: &str = "https://dog.ceo/api/breeds/image/random";
+const CAT: &str = "http://aws.random.cat/meow";
+const DAD_JOKE: &str = "https://icanhazdadjoke.com";
+const FURRY: &str = "https://e621.net/post/index.json";
+const DBOTS: &str = "http://discordbots.org/api/bots";
+// const BUNNY: &str = "https://api.bunnies.io/v2/loop/random/?media=gif,png";
+const DUCK: &str = "https://random-d.uk/api/v1/random?type=gif";
+const FOX: &str = "https://randomfox.ca/floof/";
+const OWL: &str = "https://pics.floofybot.moe/owl";
+const YO_MOMMA: &str = "https://api.yomomma.info/";
+// const WAIFU_BACKSTORY: &str = "https://www.thiswaifudoesnotexist.net/snippet-"; // .txt
+// const WAIFU_IMAGE: &str = "https://www.thiswaifudoesnotexist.net/example-"; // .jpg
+
+// Deserialization Structs
+// #[derive(Deserialize, Debug)]
+// pub struct Bunny {
+// pub id: String,
+// pub media: Vec<String>,
+// pub source: String,
+// pub thisServed: u32,
+// pub totalServed: u32
+// }
+
+#[derive(Deserialize, Debug)]
+pub struct Dog {
+ pub message: String,
+ pub status: String
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Duck {
+ pub message: String,
+ pub url: String
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Cat {
+ pub file: String
+}
+
+#[derive(Deserialize, Debug)]
+pub struct CreatedAt {
+ pub json_class: String,
+ pub s: usize,
+ pub n: usize
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Fox {
+ pub image: String,
+ pub link: String
+}
+
+#[derive(Deserialize, Debug)]
+pub struct FurPost {
+ pub id: usize,
+ pub tags: String,
+ pub locked_tags: Option<String>,
+ pub description: String,
+ pub created_at: CreatedAt,
+ pub creator_id: usize,
+ pub author: String,
+ pub change: usize,
+ pub source: Option<String>,
+ pub score: isize,
+ pub fav_count: usize,
+ pub md5: Option<String>,
+ pub file_size: Option<usize>,
+ pub file_url: String,
+ pub file_ext: Option<String>,
+ pub preview_url: String,
+ pub preview_width: Option<usize>,
+ pub preview_height: Option<usize>,
+ pub sample_url: Option<String>,
+ pub sample_width: Option<usize>,
+ pub sample_height: Option<usize>,
+ pub rating: String,
+ pub status: String,
+ pub width: usize,
+ pub height: usize,
+ pub has_comments: bool,
+ pub has_notes: bool,
+ pub has_children: Option<bool>,
+ pub children: Option<String>,
+ pub parent_id: Option<usize>,
+ pub artist: Vec<String>,
+ pub sources: Option<Vec<String>>
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Owl {
+ pub image: String
+}
+
+#[derive(Deserialize, Debug)]
+pub struct YoMomma {
+ pub joke: String
+}
+
+// The Client
+pub struct ApiClient {
+ pub client: Client,
+ pub oc_client: Opencage,
+}
+impl ApiClient {
+ pub fn new() -> Self {
+ let client = Client::new();
+ let oc_key = env::var("OPENCAGE_KEY").expect("No key for OpenCage in env");
+ let oc_client = Opencage::new(oc_key);
+ ApiClient {
+ client,
+ oc_client
+ }
+ }
+
+ pub fn stats_update(&self, bot_id: u64, server_count: usize) -> ReqwestResult<ReqwestResponse> {
+ let mut headers = Headers::new();
+ headers.set(ContentType::json());
+ headers.set(Authorization(env::var("DBOTS_TOKEN").expect("No DiscordBots.org token in env")));
+ let mut data = HashMap::new();
+ data.insert("server_count", server_count);
+
+ self.client.post(format!("{}/{}/stats", DBOTS, bot_id).as_str())
+ .json(&data)
+ .send()
+ }
+
+ // pub fn bunny(&self) -> ReqwestResult<Bunny> {
+ // match self.client.get(BUNNY).send() {
+ // Ok(mut res) => {
+ // res.json::<Bunny>()
+ // },
+ // Err(why) => {
+ // error!("{:?}", why);
+ // Err(why)
+ // },
+ // }
+ // }
+
+ pub fn dog(&self) -> ReqwestResult<Dog> {
+ match self.client.get(DOG).send() {
+ Ok(mut res) => {
+ res.json::<Dog>()
+ },
+ Err(why) => {
+ error!("{:?}", why);
+ Err(why)
+ },
+ }
+ }
+
+ pub fn duck(&self) -> ReqwestResult<Duck> {
+ match self.client.get(DUCK).send() {
+ Ok(mut res) => {
+ res.json::<Duck>()
+ },
+ Err(why) => {
+ error!("{:?}", why);
+ Err(why)
+ },
+ }
+ }
+
+ pub fn cat(&self) -> ReqwestResult<Cat> {
+ match self.client.get(CAT).send() {
+ Ok(mut res) => {
+ res.json::<Cat>()
+ },
+ Err(why) => {
+ error!("{:?}", why);
+ Err(why)
+ },
+
+ }
+ }
+
+ pub fn joke(&self) -> ReqwestResult<String> {
+ let mut headers = Headers::new();
+ headers.set(UserAgent::new(UA));
+ headers.set(Accept(vec![qitem(mime::TEXT_PLAIN)]));
+
+ match self.client.get(DAD_JOKE)
+ .headers(headers)
+ .send() {
+ Ok(mut res) => {
+ res.text()
+ },
+ Err(why) => {
+ error!("{:?}", why);
+ Err(why)
+ },
+ }
+ }
+
+ pub fn urban(&self, input: &str) -> UrbanResult<UrbanResponse> {
+ self.client.definitions(input)
+ }
+
+ pub fn furry<S: Into<String>>(&self, input: S, count: u32) -> ReqwestResult<Vec<FurPost>> {
+ let mut headers = Headers::new();
+ headers.set(UserAgent::new(UA));
+
+ match self.client.get(FURRY)
+ .headers(headers)
+ .query(&[("tags", input.into()+" order:random"), ("limit", count.to_string())])
+ .send() {
+ Ok(mut res) => {
+ res.json::<Vec<FurPost>>()
+ },
+ Err(why) => {
+ error!("{:?}", why);
+ Err(why)
+ },
+ }
+ }
+
+ pub fn fox(&self) -> ReqwestResult<Fox> {
+ match self.client.get(FOX).send() {
+ Ok(mut res) => {
+ res.json::<Fox>()
+ },
+ Err(why) => {
+ error!("{:?}", why);
+ Err(why)
+ },
+ }
+ }
+
+ pub fn owl(&self) -> ReqwestResult<Owl> {
+ match self.client.get(OWL).send() {
+ Ok(mut res) => {
+ res.json::<Owl>()
+ },
+ Err(why) => {
+ error!("{:?}", why);
+ Err(why)
+ },
+ }
+ }
+
+ pub fn yo_momma(&self) -> ReqwestResult<YoMomma> {
+ match self.client.get(YO_MOMMA).send() {
+ Ok(mut res) => {
+ res.json::<YoMomma>()
+ },
+ Err(why) => {
+ error!("{:?}", why);
+ Err(why)
+ },
+ }
+ }
+
+ pub fn anime<S: Into<String>>(&self, input: S) -> Result<Response<Vec<Anime>>, KitsuErr> {
+ self.client.search_anime(|f| f.filter("text", input.into().trim()))
+ }
+
+ pub fn manga<S: Into<String>>(&self, input: S) -> Result<Response<Vec<Manga>>, KitsuErr> {
+ self.client.search_manga(|f| f.filter("text", input.into().trim()))
+ }
+
+ pub fn weather(&self, input: &str, units: Units) -> Option<(String, ReqwestResult<ApiResponse>)> {
+ match self.oc_client.forward_full(input, &None) {
+ Ok(data) => {
+ if !data.results.is_empty() {
+ let first = data.results.first().unwrap();
+ let city_info = format!("{}, {}, {}",
+ first.components.get("city").unwrap(),
+ first.components.get("state").unwrap(),
+ first.components.get("country").unwrap()
+ );
+ let ds_key = env::var("DARKSKY_KEY").expect("No DarkSky API Key found in env");
+ let fc_req = Some(ForecastRequestBuilder::new(ds_key.as_str(), *first.geometry.get("lat").unwrap(), *first.geometry.get("lng").unwrap())
+ .lang(Lang::English)
+ .units(units)
+ .build());
+ if let Some(req) = fc_req {
+ let ds_client = DSClient::new(&self.client);
+ match ds_client.get_forecast(req) {
+ Ok(mut res) => {
+ return Some((city_info, res.json::<ApiResponse>()));
+ },
+ Err(why) => { return Some((city_info, Err(why))); },
+ }
+ }
+ }
+ },
+ Err(why) => { debug!("Failed to resolve location: {:?}", why); }
+ }
+ None
+ }
+}
diff --git a/src/core/colours.rs b/src/core/colours.rs
new file mode 100644
index 0000000..0d90e0e
--- /dev/null
+++ b/src/core/colours.rs
@@ -0,0 +1,8 @@
+use serenity::utils::Colour;
+
+lazy_static! {
+ pub static ref MAIN: Colour = Colour::new(0xfebe50); // 0x5da9ff
+ pub static ref BLUE: Colour = Colour::new(0x6969ff);
+ pub static ref RED: Colour = Colour::new(0xff4040);
+ pub static ref GREEN: Colour = Colour::new(0x00ff7f);
+}
diff --git a/src/core/consts.rs b/src/core/consts.rs
new file mode 100644
index 0000000..d76f2ff
--- /dev/null
+++ b/src/core/consts.rs
@@ -0,0 +1,51 @@
+use crate::db::Database;
+use serenity::model::id::{GuildId, ChannelId, RoleId};
+
+lazy_static!{
+ pub static ref DB: Database = Database::connect();
+ pub static ref LOG_TYPES: Vec<&'static str> = vec![
+ "member_ban",
+ "member_join",
+ "member_kick",
+ "member_leave",
+ "member_unban",
+ "message_delete",
+ "message_edit",
+ "nickname_change",
+ "role_change",
+ "username_change"];
+}
+
+pub const WEEK: usize = 60*60*24*7;
+pub const DAY: usize = 60*60*24;
+pub const HOUR: usize = 60*60;
+pub const MIN: usize = 60;
+
+pub const MESSAGE_CACHE: usize = 100;
+pub const SLICE_SIZE: usize = 65535;
+pub const USER_SLICE_SIZE: usize = 65535/5;
+
+pub const COMMAND_LOG: ChannelId = ChannelId(770117277416554526); // 376422940570419200
+pub const ERROR_LOG: ChannelId = ChannelId(770117277416554526); // 376422808852627457
+pub const GUILD_LOG: ChannelId = ChannelId(770117277416554526); // 406115496833056789
+pub const NOW_LIVE: RoleId = RoleId(370395740406546432);
+pub const SUPPORT_SERVER: GuildId = GuildId(704032355987488791); // 373561057639268352
+pub const TRANSCEND: GuildId = GuildId(348660188951216129);
+
+pub const SUPPORT_SERV_INVITE: &str = "https://discord.gg/ASrM7p9";
+pub const BOT_INVITE: &str = "https://discordapp.com/oauth2/authorize/?permissions=335670488&scope=bot&client_id=699473263998271489";
+// pub const GITLAB_LINK: &str = "https://gitlab.com/fuwn/wisp";
+// pub const PATREON_LINK: &str = "https://www.patreon.com/wisp";
+
+pub const API_FAIL: &str = "Failed to get API";
+pub const CACHE_CHANNEL_FAIL: &str = "Failed to get channel lock from CACHE";
+pub const CACHE_GUILD_FAIL: &str = "Failed to get guild lock from CACHE";
+pub const DB_GUILD_FAIL: &str = "Failed to select Guild";
+pub const DB_GUILD_DEL_FAIL: &str = "Failed to delete Guild";
+pub const DB_GUILD_ENTRY_FAIL: &str = "Failed to insert Guild";
+pub const DB_USER_ENTRY_FAIL: &str = "Failed to insert User";
+pub const GUILD_FAIL: &str = "Failed to get Guild";
+pub const GUILDID_FAIL: &str = "Failed to get GuildId";
+pub const MEMBER_FAIL: &str = "Failed to get member";
+pub const TC_FAIL: &str = "Failed to get TimerClient";
+pub const USER_FAIL: &str = "Failed to get user";
diff --git a/src/core/framework.rs b/src/core/framework.rs
new file mode 100644
index 0000000..901573f
--- /dev/null
+++ b/src/core/framework.rs
@@ -0,0 +1,206 @@
+use chrono::Utc;
+use crate::core::colours;
+use crate::core::consts::*;
+use crate::core::consts::DB as db;
+use crate::core::model::Owner;
+use crate::core::utils::check_rank;
+use crate::modules::commands::{
+ admins,
+ general,
+ mods,
+ owners
+};
+use serenity::framework::{
+ StandardFramework,
+ standard::{
+ help_commands,
+ HelpBehaviour,
+ }
+};
+use serenity::model::channel::{Channel, Message};
+use serenity::model::id::{GuildId, UserId};
+use serenity::prelude::Context;
+use std::collections::HashSet;
+
+pub struct WispFramework;
+impl WispFramework {
+ pub fn new(owners: HashSet<UserId>) -> StandardFramework {
+ StandardFramework::new()
+ .configure(|c| c
+ .allow_whitespace(true)
+ .allow_dm(true)
+ .on_mention(true)
+ .ignore_bots(true)
+ .case_insensitivity(true)
+ .delimiters(vec![","," "])
+ .owners(owners)
+ .prefix("w.")
+ .dynamic_prefix(|_, message| {
+ if message.is_private() {
+ return Some(String::new());
+ } else {
+ let guild_id = message.guild_id.unwrap_or(GuildId(0));
+ if let Ok(settings) = db.get_guild(guild_id.0 as i64) {
+ return Some(settings.prefix);
+ }
+ }
+ None
+ }))
+ .before(|ctx, message, command_name| {
+ if let false = message.is_private() {
+ let guild_id = message.guild_id.unwrap_or(GuildId(0));
+ if let Ok(guild_data) = db.get_guild(guild_id.0 as i64) {
+ if guild_data.ignored_channels.contains(&(message.channel_id.0 as i64)) {
+ if get_highest_rank(ctx, message) < guild_data.ignore_level {
+ return false;
+ }
+ }
+ if guild_data.commands.contains(&command_name.to_string()) {
+ return false;
+ }
+ }
+ }
+ true
+ })
+ .after(|_, message, cmd_name, error| {
+ let guild = match message.guild() {
+ Some(lock) => {
+ let g = lock.read();
+ format!("{} ({})", g.name, g.id.0)
+ },
+ None => String::from("Private"),
+ };
+ let channel = if let Some(ch) = message.channel() {
+ match ch {
+ Channel::Guild(c) => {
+ let c = c.read();
+ format!("{} ({})", c.name, c.id.0)
+ },
+ Channel::Private(_) => ch.id().0.to_string(),
+ _ => String::new(),
+ }
+ } else { String::new() };
+ check_error!(COMMAND_LOG.send_message(|m| m
+ .embed(|e| e
+ .description(format!("**Guild:** {}\n**Channel:** {}\n**Author:** {} ({})\n**ID:** {}",
+ guild,
+ channel,
+ message.author.tag(),
+ message.author.id.0,
+ message.id.0
+ ))
+ .field("Content", message.content_safe(), false)
+ .timestamp(now!())
+ .colour(*colours::MAIN)
+ )));
+ if let Err(why) = error {
+ // TODO do some actual matching here so you can provide more details
+ check_error!(message.channel_id.say(format!("Something went wrong with the command. Here's the error: {:?}", why)));
+ check_error!(ERROR_LOG.send_message(|m| m
+ .embed(|e| e
+ .description(format!("{:?}", why))
+ .field("Message", message.id.0.to_string(), true)
+ .field("Channel", message.channel_id.0.to_string(), true)
+ .field("Command", cmd_name, true)
+ .field("Message Content", message.content_safe(), false)
+ .timestamp(now!())
+ .colour(*colours::RED)
+ )));
+ }
+ })
+ .on_dispatch_error(|_, message, error| {
+ use serenity::framework::standard::DispatchError;
+ match error {
+ DispatchError::LackOfPermissions(perm) => check_error!(
+ message.channel_id.say(format!(
+ // "You lack the following permissions needed to execute this command: {:?}"
+ "Sorry, but you need the following permissions to use this command; {:?}"
+ ,perm))),
+ DispatchError::RateLimited(time) => check_error!(
+ message.channel_id.say(format!(
+ "Woah, that was fast. Please wait {} seconds before trying again."
+ ,time))),
+ DispatchError::NotEnoughArguments { min, given } => check_error!(
+ message.channel_id.say(format!(
+ "Too few arguments provided. {} given, {} minimum."
+ ,given
+ ,min))),
+ DispatchError::TooManyArguments { max, given } => check_error!(
+ message.channel_id.say(format!(
+ "Too many arguments provided. {} given, {} maximum."
+ ,given
+ ,max))),
+ DispatchError::OnlyForDM => check_error!(
+ message.channel_id.say("This command is only available in DMs.")), // private channels
+ DispatchError::OnlyForGuilds => check_error!(
+ message.channel_id.say("This command is only available in guilds.")),
+ _ => (),
+ }
+ })
+ .customised_help(help_commands::plain, |c| c
+ .no_help_available_text("No help is available on this command.")
+ .usage_label("Usage")
+ .usage_sample_label("Example")
+ .aliases_label("Aliases")
+ .guild_only_text("Guild only")
+ .dm_only_text("DM only")
+ .dm_and_guilds_text("DM or Guild")
+ .command_not_found_text("Command not found.")
+ .lacking_role(HelpBehaviour::Strike)
+ .lacking_permissions(HelpBehaviour::Strike)
+ .wrong_channel(HelpBehaviour::Strike)
+ .embed_success_colour(*colours::MAIN)
+ .embed_error_colour(*colours::RED))
+ .bucket("weather", 30, DAY as i64, 1000)
+ .group("Anime", |_| general::init_anime())
+ .group("Animals", |_| general::init_animals())
+ .group("Config", |_| admins::init_config())
+ .group("Fun", |_| general::init_fun())
+ // .group("Hackbans", |_| mods::init_hackbans())
+ .group("Ignore Channels Management", |_| admins::init_ignore())
+ .group("Kick and Ban", |_| mods::init_kickbans())
+ .group("Management", |_| admins::init_management())
+ .group("Minigames", |_| general::init_minigames())
+ // .group("Mod Info", |_| mods::init_info())
+ // .group("Mute", |_| mods::init_mute())
+ // .group("Notes", |_| mods::init_notes())
+ .group("NSFW", |_| general::init_nsfw())
+ .group("Owner/ Developer Only", |_| owners::init())
+ // .group("Premium", |_| admins::init_premium())
+ // .group("Role Management", |_| mods::init_roles())
+ // .group("Self Role Management", |_| admins::init_roles())
+ // .group("Self Roles", |_| general::init_roles())
+ // .group("Tags", |_| general::init_tags())
+ .group("Tests", |_| admins::init_tests())
+ .group("Utilities", |_| general::init_utilities())
+ .group("Watchlist Management", |_| mods::init_watchlist())
+ }
+}
+
+fn get_highest_rank(ctx: &mut Context, message: &Message) -> i16 {
+ {
+ let data = ctx.data.lock();
+ if let Some(owner) = data.get::<Owner>() {
+ if *owner == message.author.id {
+ return 4;
+ }
+ }
+ }
+ if let Some(guild_lock) = message.guild() {
+ let (guild_id, owner_id) = {
+ let guild = guild_lock.read();
+ (guild.id, guild.owner_id)
+ };
+ if message.author.id == owner_id { return 3; }
+ if let Ok(guild_data) = db.get_guild(guild_id.0 as i64) {
+ if let Ok(member) = guild_id.member(message.author.id.clone()) {
+ if check_rank(guild_data.admin_roles, &member.roles) {
+ return 2;
+ } else if check_rank(guild_data.mod_roles, &member.roles) {
+ return 1;
+ }
+ }
+ }
+ }
+ 0
+}
diff --git a/src/core/handler.rs b/src/core/handler.rs
new file mode 100644
index 0000000..5f499bf
--- /dev/null
+++ b/src/core/handler.rs
@@ -0,0 +1,572 @@
+use chrono::Utc;
+use crate::core::colours;
+use crate::core::consts::*;
+use crate::core::consts::DB as db;
+use crate::core::model::*;
+use crate::core::utils::*;
+use crate::db::models::UserUpdate;
+use levenshtein::levenshtein;
+use rand::prelude::*;
+use serenity::CACHE;
+use serenity::model::gateway::{Game, GameType, Ready};
+use serenity::model::channel::Message;
+use serenity::model::event::PresenceUpdateEvent;
+use serenity::model::guild::{Guild, Member, PartialGuild};
+use serenity::model::id::{
+ ChannelId,
+ GuildId,
+ MessageId,
+ RoleId
+};
+use serenity::model::user::User;
+use serenity::prelude::*;
+use std::sync::{Arc, Once};
+use std::thread;
+use std::time::Duration;
+
+static LOAD_TIMERS: Once = Once::new();
+
+pub struct Handler;
+
+impl EventHandler for Handler {
+ fn ready(&self, ctx: Context, ready: Ready) {
+ CACHE.write().settings_mut().max_messages(MESSAGE_CACHE);
+ let data = ctx.data.lock();
+ LOAD_TIMERS.call_once(|| {
+ if let Some(tc_lock) = data.get::<TC>() {
+ let tc = tc_lock.lock();
+ tc.request();
+ }
+ if let Some(api) = data.get::<ApiClient>().cloned() {
+ let bot_id = CACHE.read().user.id.0.clone();
+ thread::spawn(move || {
+ loop {
+ thread::sleep(Duration::from_secs(10 * (MIN as u64)));
+
+ // Change role colours for Transcend
+ if let Some(owner_role) = RoleId(348665099550195713).to_role_cached() {
+ check_error!(owner_role.edit(|r| r
+ .colour(thread_rng().gen_range(0,16777216))
+ ));
+ }
+ if let Some(first_role) = RoleId(363398104491229184).to_role_cached() {
+ check_error!(first_role.edit(|r| r
+ .colour(thread_rng().gen_range(0,16777216))
+ ));
+ }
+ }
+ });
+ thread::spawn(move || {
+ loop {
+ thread::sleep(Duration::from_secs(HOUR as u64));
+
+ // Update DBots stats
+ let count = CACHE.read().guilds.len();
+ check_error!(api.stats_update(bot_id, count));
+ }
+ });
+ } else { failed!(API_FAIL); }
+ });
+ info!("Logged in as {}", ready.user.name);
+ }
+
+ fn cached(&self, ctx: Context, guilds: Vec<GuildId>) {
+ let mut users = Vec::new();
+ for guild_id in guilds.iter() {
+ let guild_lock = CACHE.read().guild(guild_id);
+ if let Some(guild_lock) = guild_lock {
+ let guild = guild_lock.read();
+ let members = &guild.members;
+ for (_, member) in members.iter() {
+ let u = member.user.read();
+ users.push(UserUpdate {
+ id: u.id.0 as i64,
+ guild_id: guild_id.0 as i64,
+ username: u.tag()
+ });
+ }
+ } else { failed!(CACHE_GUILD_FAIL); }
+ }
+ for slice in guilds.iter().map(|e| e.0 as i64).collect::<Vec<i64>>().chunks(SLICE_SIZE) {
+ check_error!(db.new_guilds(slice));
+ }
+ for slice in users.chunks(USER_SLICE_SIZE) {
+ check_error!(db.upsert_users(slice));
+ }
+
+ // let guild_count = guilds.len();
+ // ctx.set_game(Game::listening(&format!("{} guilds | m!help", guild_count)));
+ ctx.set_game(Game::playing("w.help | v0.1.0"));
+ info!("Caching complete.");
+ }
+
+ fn message(&self, _: Context, message: Message) {
+ if message.content.contains("uwu!") {
+ check_error!(message.channel_id.say("Uwufier has been re-branded ! Please use the new prefix; `w.`!"));
+ }
+
+ if message.mention_everyone {
+ check_error!(message.react("👀"))
+ }
+ }
+
+ fn message_delete(&self, _: Context, channel_id: ChannelId, message_id: MessageId) {
+ let (channel_name, guild_id) = {
+ let channel_lock = CACHE.read().guild_channel(&channel_id);
+ if let Some(channel_lock) = channel_lock {
+ let ch = channel_lock.read();
+ (ch.name.clone(), ch.guild_id.clone())
+ } else {
+ ("unknown".to_string(), GuildId(0))
+ }
+ };
+ match db.get_guild(guild_id.0 as i64) {
+ Ok(guild_data) => {
+ if guild_data.logging.contains(&String::from("message_delete")) { return; }
+ let audit_channel = ChannelId(guild_data.audit_channel as u64);
+ if guild_data.audit && audit_channel.0 > 0 {
+ let message = CACHE.read().message(&channel_id, &message_id);
+ if let Some(message) = message {
+ if message.author.bot { return; }
+ check_error!(audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Message Deleted")
+ .colour(*colours::RED)
+ .footer(|f| f.text(format!("ID: {}", message_id.0)))
+ .description(format!("**Author:** {} ({}) - {}\n**Channel:** {} ({}) - <#{}>\n**Content:**\n{}",
+ message.author.tag(),
+ message.author.id.0,
+ message.author.mention(),
+ channel_name,
+ channel_id.0,
+ channel_id.0,
+ message.content_safe()))
+ .timestamp(now!())
+ )));
+ } else {
+ check_error!(audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Uncached Message Deleted")
+ .colour(*colours::RED)
+ .footer(|f| f.text(format!("ID: {}", message_id.0)))
+ .description(format!("**Channel:** {} ({}) - <#{}>",
+ channel_name,
+ channel_id.0,
+ channel_id.0))
+ .timestamp(now!())
+ )));
+ }
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_FAIL, why); },
+ }
+ }
+
+ // Edit logs
+ fn message_update(&self, _: Context, old: Option<Message>, new: Message) {
+ if new.author.bot { return; }
+ if let None = new.edited_timestamp { return; }
+ if let Some(message) = old {
+ if let Some(guild_id) = new.guild_id {
+ let channel_id = new.channel_id;
+ let channel_name = {
+ let channel_lock = CACHE.read().guild_channel(&channel_id);
+ if let Some(channel_lock) = channel_lock {
+ let ch = channel_lock.read();
+ ch.name.clone()
+ } else {
+ "unknown".to_string()
+ }
+ };
+ match db.get_guild(guild_id.0 as i64) {
+ Ok(guild_data) => {
+ if guild_data.logging.contains(&String::from("message_edit")) { return; }
+ let audit_channel = ChannelId(guild_data.audit_channel as u64);
+ let distance = levenshtein(message.content.as_str(), new.content.as_str());
+ if guild_data.audit && audit_channel.0 > 0 && distance >= guild_data.audit_threshold as usize {
+ check_error!(audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Message Edited")
+ .colour(*colours::MAIN)
+ .footer(|f| f.text(format!("ID: {}", message.id.0)))
+ .description(format!("**Author:** {} ({}) - {}\n**Channel:** {} ({}) - <#{}>\n**Old Content:**\n{}\n**New Content:**\n{}",
+ message.author.tag(),
+ message.author.id.0,
+ message.author.mention(),
+ channel_name,
+ channel_id.0,
+ channel_id.0,
+ message.content_safe(),
+ new.content))
+ .timestamp(now!())
+ )));
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_FAIL, why); },
+ }
+ } else { failed!(GUILDID_FAIL); }
+ }
+ }
+
+ // Username changes and Now Live! role
+ fn presence_update(&self, _: Context, event: PresenceUpdateEvent) {
+ if let Some(guild_id) = event.guild_id {
+ match event.presence.user {
+ Some(ref user_lock) => {
+ let (user_bot, user_tag, user_face) = {
+ let u = user_lock.read();
+ (u.bot, u.tag(), u.face())
+ };
+ if !user_bot {
+ if guild_id == TRANSCEND {
+ let member = CACHE.read().member(guild_id, event.presence.user_id);
+ if let Some(mut member) = member {
+ match event.presence.game {
+ Some(ref game) => {
+ if let GameType::Streaming = game.kind {
+ if member.roles.contains(&NOW_LIVE) {
+ let _ = member.add_role(NOW_LIVE);
+ }
+ }
+ },
+ None => {
+ if !member.roles.contains(&NOW_LIVE) {
+ let _ = member.remove_role(NOW_LIVE);
+ }
+ },
+ }
+ } else { failed!(MEMBER_FAIL); }
+ }
+ let user_update = UserUpdate {
+ id: event.presence.user_id.0 as i64,
+ guild_id: guild_id.0 as i64,
+ username: user_tag.clone()
+ };
+ if let Ok(mut user_data) = db.upsert_user(user_update) {
+ if user_tag != user_data.username && user_data.username != String::new() {
+ if let Ok(guild_data) = db.get_guild(guild_id.0 as i64) {
+ if guild_data.logging.contains(&String::from("username_change")) { return; }
+ if guild_data.audit && guild_data.audit_channel > 0 {
+ let audit_channel = ChannelId(guild_data.audit_channel as u64);
+ audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Username changed")
+ .colour(*colours::MAIN)
+ .thumbnail(user_face)
+ .description(format!("**Old:** {}\n**New:** {}", user_data.username, user_tag))
+ )).expect("Failed to send Message");
+ }
+ } else { failed!(DB_GUILD_FAIL); }
+ user_data.username = user_tag;
+ db.update_user(event.presence.user_id.0 as i64, guild_id.0 as i64, user_data).expect("Failed to update user");
+ }
+ }
+ }
+ },
+ None => {},
+ }
+ } else { failed!(GUILDID_FAIL); }
+ }
+
+ fn guild_create(&self, _: Context, guild: Guild, is_new: bool) {
+ if is_new {
+ match db.new_guild(guild.id.0 as i64) {
+ Ok(_) => {
+ match guild.owner_id.to_user() {
+ Ok(owner) => {
+ check_error!(GUILD_LOG.send_message(|m| m
+ .embed(|e| e
+ .title("Joined Guild")
+ .timestamp(now!())
+ .colour(*colours::GREEN)
+ .description(format!("**Name:** {}\n**ID:** {}\n**Owner:** {} ({})",
+ guild.name,
+ guild.id.0,
+ owner.tag(),
+ owner.id.0))
+ )));
+ },
+ Err(why) => { failed!(USER_FAIL, why); },
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_ENTRY_FAIL, why); }
+ }
+ }
+ }
+
+ fn guild_delete(&self, _: Context, partial_guild: PartialGuild, _: Option<Arc<RwLock<Guild>>>) {
+ match db.del_guild(partial_guild.id.0 as i64) {
+ Ok(_) => {
+ match partial_guild.owner_id.to_user() {
+ Ok(owner) => {
+ check_error!(GUILD_LOG.send_message(|m| m
+ .embed(|e| e
+ .title("Left Guild")
+ .timestamp(now!())
+ .colour(*colours::RED)
+ .description(format!("**Name:** {}\n**ID:** {}\n**Owner:** {} ({})",
+ partial_guild.name,
+ partial_guild.id.0,
+ owner.tag(),
+ owner.id.0))
+ )));
+ },
+ Err(why) => { failed!(USER_FAIL, why); },
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_DEL_FAIL, why); }
+ }
+ }
+
+ // Join log and welcome message
+ fn guild_member_addition(&self, _: Context, guild_id: GuildId, mut member: Member) {
+ let (banned, reason) = {
+ let user_id = member.user.read().id.0;
+ db.get_hackban(user_id as i64, guild_id.0 as i64)
+ .map(|ban| (true, ban.reason.clone()))
+ .unwrap_or((false, None))
+ };
+ if banned {
+ if let Some(ref r) = reason {
+ check_error!(member.ban::<String>(r));
+ } else {
+ check_error!(member.ban(&0));
+ }
+ } else {
+ match db.get_guild(guild_id.0 as i64) {
+ Ok(guild_data) => {
+ if guild_data.logging.contains(&String::from("member_join")) { return; }
+ let (user_id, user_face, user_tag, username) = {
+ let u = member.user.read();
+ (u.id, u.face(), u.tag(), u.name.clone())
+ };
+ let user_update = UserUpdate {
+ id: user_id.0 as i64,
+ guild_id: guild_id.0 as i64,
+ username
+ };
+ match db.upsert_user(user_update) {
+ Ok(mut user_data) => {
+ if guild_data.audit && guild_data.audit_channel > 0 {
+ let audit_channel = ChannelId(guild_data.audit_channel as u64);
+ check_error!(audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Member Joined")
+ .colour(*colours::GREEN)
+ .thumbnail(user_face)
+ .timestamp(now!())
+ .description(format!("<@{}>\n{}\n{}", user_id, user_tag, user_id))
+ )));
+ }
+ if guild_data.welcome && guild_data.welcome_channel > 0 {
+ let channel = ChannelId(guild_data.welcome_channel as u64);
+ if guild_data.welcome_type.as_str() == "embed" {
+ check_error!(send_welcome_embed(guild_data.welcome_message, &member, channel));
+ } else {
+ check_error!(channel.say(parse_welcome_items(guild_data.welcome_message, &member)));
+ }
+ }
+ if guild_data.autorole && !guild_data.autoroles.is_empty() {
+ for id in guild_data.autoroles.iter() {
+ check_error!(member.add_role(RoleId(*id as u64)));
+ }
+ }
+ user_data.username = user_tag;
+ user_data.nickname = member.display_name().into_owned();
+ user_data.roles = member.roles.iter().map(|e| e.0 as i64).collect::<Vec<i64>>();
+ check_error!(db.update_user(user_id.0 as i64, guild_id.0 as i64, user_data));
+ },
+ Err(why) => { failed!(DB_USER_ENTRY_FAIL, why); }
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_FAIL, why); }
+ }
+ }
+ }
+
+ // Leave and kick log
+ fn guild_member_removal(&self, _: Context, guild_id: GuildId, user: User, _: Option<Member>) {
+ match db.get_guild(guild_id.0 as i64) {
+ Ok(guild_data) => {
+ check_error!(db.del_user(user.id.0 as i64, guild_id.0 as i64));
+ if guild_data.logging.contains(&String::from("member_leave")) { return; }
+ if guild_data.audit && guild_data.audit_channel > 0 {
+ let audit_channel = ChannelId(guild_data.audit_channel as u64);
+ check_error!(audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Member Left")
+ .colour(*colours::RED)
+ .thumbnail(user.face())
+ .timestamp(now!())
+ .description(format!("<@{}>\n{}\n{}", user.id, user.tag(), user.id))
+ )));
+ }
+ if guild_data.logging.contains(&String::from("member_kick")) { return; }
+ thread::sleep(Duration::from_secs(3));
+ if let Ok(audits) = guild_id.audit_logs(Some(20), None, None, Some(1)) {
+ if let Some((audit_id, audit)) = audits.entries.iter().next() {
+ if guild_data.modlog && guild_data.modlog_channel > 0 && audit.target_id == user.id.0 && (Utc::now().timestamp()-audit_id.created_at().timestamp())<5 {
+ let modlog_channel = ChannelId(guild_data.modlog_channel as u64);
+ check_error!(modlog_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Member Kicked")
+ .colour(*colours::RED)
+ .thumbnail(user.face())
+ .timestamp(now!())
+ .description(format!("**Member:** {} ({}) - {}\n**Responsible Moderator:** {}\n**Reason:** {}",
+ user.tag(),
+ user.id.0,
+ user.mention(),
+ match audit.user_id.to_user() {
+ Ok(u) => u.tag(),
+ Err(_) => audit.user_id.0.to_string()
+ },
+ audit.reason.clone().unwrap_or("None".to_string())
+ ))
+ )));
+ }
+ }
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_FAIL, why); }
+ }
+ }
+
+ // Nickname and Role changes
+ fn guild_member_update(&self, _: Context, old: Option<Member>, new: Member) {
+ let guild_id = new.guild_id;
+ if let Some(old) = old {
+ match db.get_guild(guild_id.0 as i64) {
+ Ok(guild_data) => {
+ let (user_face, user_tag) = {
+ let u = new.user.read();
+ (u.face(), u.tag())
+ };
+ if guild_data.audit && guild_data.audit_channel > 0 {
+ if guild_data.logging.contains(&String::from("nickname_change")) { return; }
+ let audit_channel = ChannelId(guild_data.audit_channel as u64);
+ if new.nick != old.nick {
+ let old_nick = old.nick.clone().unwrap_or("None".to_string());
+ let new_nick = new.nick.clone().unwrap_or("None".to_string());
+ check_error!(audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Nickname changed")
+ .colour(*colours::MAIN)
+ .thumbnail(&user_face)
+ .description(format!(
+ "**User: ** {}\n**Old:** {}\n**New:** {}",
+ user_tag,
+ old_nick,
+ new_nick
+ ))
+ )));
+ };
+ if guild_data.logging.contains(&String::from("role_change")) { return; }
+ let mut roles_added = new.roles.clone();
+ roles_added.retain(|e| !old.roles.contains(e));
+ if !roles_added.is_empty() {
+ let roles_added = roles_added.iter()
+ .map(|r| match r.to_role_cached() {
+ Some(role) => role.name,
+ None => r.0.to_string(),
+ })
+ .collect::<Vec<String>>();
+ check_error!(audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Roles changed")
+ .colour(*colours::MAIN)
+ .thumbnail(&user_face)
+ .description(format!("**User: ** {}\n**Added:** {}", user_tag, roles_added.join(", ")))
+ )));
+ }
+ let mut roles_removed = old.roles.clone();
+ roles_removed.retain(|e| !new.roles.contains(e));
+ if !roles_removed.is_empty() {
+ let roles_removed = roles_removed.iter()
+ .map(|r| match r.to_role_cached() {
+ Some(role) => role.name,
+ None => r.0.to_string(),
+ })
+ .collect::<Vec<String>>();
+ check_error!(audit_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Roles changed")
+ .colour(*colours::MAIN)
+ .thumbnail(&user_face)
+ .description(format!("**User: ** {}\n**Removed:** {}", user_tag, roles_removed.join(", ")))
+ )));
+ }
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_FAIL, why); }
+ }
+ }
+ }
+
+ fn guild_ban_addition(&self, _: Context, guild_id: GuildId, user: User) {
+ thread::sleep(Duration::from_secs(3));
+ if let Ok(audits) = guild_id.audit_logs(Some(22), None, None, Some(1)) {
+ if let Some(audit) = audits.entries.values().next() {
+ match db.get_guild(guild_id.0 as i64) {
+ Ok(guild_data) => {
+ if guild_data.logging.contains(&String::from("member_ban")) { return; }
+ if guild_data.modlog && guild_data.modlog_channel > 0 && audit.target_id == user.id.0 {
+ let modlog_channel = ChannelId(guild_data.modlog_channel as u64);
+ check_error!(modlog_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Member Banned")
+ .colour(*colours::RED)
+ .thumbnail(user.face())
+ .timestamp(now!())
+ .description(format!("**Member:** {} ({}) - {}\n**Responsible Moderator:** {}\n**Reason:** {}",
+ user.tag(),
+ user.id.0,
+ user.mention(),
+ match audit.user_id.to_user() {
+ Ok(u) => u.tag(),
+ Err(_) => audit.user_id.0.to_string(),
+ },
+ audit.reason.clone().unwrap_or("None".to_string())
+ ))
+ )));
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_FAIL, why); }
+ }
+ }
+ }
+ }
+
+ fn guild_ban_removal(&self, _: Context, guild_id: GuildId, user: User) {
+ thread::sleep(Duration::from_secs(3));
+ if let Ok(audits) = guild_id.audit_logs(Some(23), None, None, Some(1)) {
+ if let Some(audit) = audits.entries.values().next() {
+ match db.get_guild(guild_id.0 as i64) {
+ Ok(guild_data) => {
+ if guild_data.logging.contains(&String::from("member_unban")) { return; }
+ if guild_data.modlog && guild_data.modlog_channel > 0 && audit.target_id == user.id.0 {
+ let modlog_channel = ChannelId(guild_data.modlog_channel as u64);
+ check_error!(modlog_channel.send_message(|m| m
+ .embed(|e| e
+ .title("Member Unbanned")
+ .colour(*colours::GREEN)
+ .thumbnail(user.face())
+ .timestamp(now!())
+ .description(format!("**Member:** {} ({}) - {}\n**Responsible Moderator:** {}",
+ user.tag(),
+ user.id.0,
+ user.mention(),
+ match audit.user_id.to_user() {
+ Ok(u) => u.tag(),
+ Err(_) => audit.user_id.0.to_string(),
+ }
+ ))
+ )));
+ }
+ },
+ Err(why) => { failed!(DB_GUILD_FAIL, why); }
+ }
+ }
+ }
+ }
+}
diff --git a/src/core/mod.rs b/src/core/mod.rs
new file mode 100644
index 0000000..2ddcba0
--- /dev/null
+++ b/src/core/mod.rs
@@ -0,0 +1,8 @@
+pub mod api;
+pub mod colours;
+pub mod consts;
+pub mod framework;
+pub mod handler;
+pub mod model;
+pub mod timers;
+pub mod utils;
diff --git a/src/core/model.rs b/src/core/model.rs
new file mode 100644
index 0000000..ade2930
--- /dev/null
+++ b/src/core/model.rs
@@ -0,0 +1,33 @@
+use crate::core::api;
+use crate::core::timers::TimerClient;
+use crate::db::Database;
+use serenity::client::bridge::gateway::ShardManager;
+use serenity::model::id::UserId;
+use serenity::prelude::Mutex;
+use std::sync::Arc;
+use typemap::Key;
+
+pub struct Owner;
+impl Key for Owner {
+ type Value = UserId;
+}
+
+pub struct SerenityShardManager;
+impl Key for SerenityShardManager {
+ type Value = Arc<Mutex<ShardManager>>;
+}
+
+pub struct ApiClient;
+impl Key for ApiClient {
+ type Value = Arc<api::ApiClient>;
+}
+
+pub struct DB;
+impl Key for DB {
+ type Value = Arc<Database>;
+}
+
+pub struct TC;
+impl Key for TC {
+ type Value = Arc<Mutex<TimerClient>>;
+}
diff --git a/src/core/timers.rs b/src/core/timers.rs
new file mode 100644
index 0000000..2420c48
--- /dev/null
+++ b/src/core/timers.rs
@@ -0,0 +1,160 @@
+use crate::core::colours;
+use crate::core::consts::*;
+use crate::core::consts::DB as db;
+use crate::core::utils::*;
+use chrono::Utc;
+use serenity::model::channel::Channel;
+use serenity::model::id::*;
+use serenity::prelude::{Mentionable, Mutex};
+use std::str::FromStr;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::mpsc::{Sender, channel};
+
+use std::thread;
+use std::time::Duration;
+
+fn reminder(channel_id: ChannelId, user_id: UserId, dur: String, reminder: &String) {
+ let check = match channel_id.to_channel() {
+ Ok(ch) => { match ch {
+ Channel::Private(_) => true,
+ _ => false,
+ }},
+ _ => false,
+ };
+ check_error!(channel_id.send_message(|m| m
+ .content(match check {
+ false => user_id.mention(),
+ true => String::new(),
+ })
+ .embed(|e| e
+ .title(format!("Reminder from {} ago", dur))
+ .colour(*colours::MAIN)
+ .description(reminder)
+ )));
+}
+
+fn unmute(user_id: UserId, guild_id: GuildId, channel_id: ChannelId, role_id: RoleId) {
+ match user_id.to_user() {
+ Ok(user) => {
+ if let Ok(mut member) = guild_id.member(user_id) {
+ if let Ok(_) = member.remove_role(role_id) {
+ check_error!(channel_id.send_message(|m| m
+ .embed(|e| e
+ .title("Member Unmuted Automatically")
+ .colour(*colours::GREEN)
+ .description(format!("**Member:** {} ({})", user.tag(), user_id.0))
+ )));
+ }
+ }
+ },
+ Err(why) => { failed!(USER_FAIL, why); }
+ }
+}
+
+fn cooldown(user_id: UserId, guild_id: GuildId, mrole_id: RoleId, crole_id: RoleId) {
+ if let Ok(mut member) = guild_id.member(user_id) {
+ check_error!(member.add_role(mrole_id));
+ check_error!(member.remove_role(crole_id));
+ debug!("Member removed from cooldown. User ID: {:?}, Guild: {:?}", user_id, guild_id);
+ }
+}
+
+pub struct TimerClient(Arc<Mutex<Sender<bool>>>);
+
+impl TimerClient {
+ pub fn new() -> Self {
+ let (tx, rx) = channel();
+ let tx = Arc::new(Mutex::new(tx));
+ let mtx = tx.clone();
+ thread::spawn(move || {
+ if let Ok(_) = rx.recv() {
+ thread::spawn(move || {
+ loop {
+ match db.get_earliest_timer() {
+ Ok(timer) => {
+ let dur = u64::checked_sub(timer.endtime as u64, Utc::now().timestamp() as u64).unwrap_or(0);
+ let itx = mtx.clone();
+ let cond = Arc::new(AtomicBool::new(true));
+ let mc = cond.clone();
+ thread::spawn(move || {
+ thread::sleep(Duration::from_secs(dur));
+ if mc.load(Ordering::Relaxed) {
+ let _ = itx.lock().send(true);
+ }
+ });
+ if let Ok(opt) = rx.recv() {
+ match opt {
+ false => {
+ cond.store(false, Ordering::Relaxed);
+ continue;
+ },
+ true => {
+ let parts = timer.data.split("||").map(|s| s.to_string()).collect::<Vec<String>>();
+ match parts[0].as_str() {
+ "REMINDER" => {
+ // type, channel_id, user_id, dur, reminder
+ let cid = ChannelId::from_str(parts[1].as_str()).ok();
+ let uid = UserId::from_str(parts[2].as_str()).ok();
+ let dur = seconds_to_hrtime(parts[3].parse::<usize>().unwrap_or(0));
+ let rem = &parts[4];
+ match (cid, uid) {
+ (Some(cid), Some(uid)) => { reminder(cid, uid, dur, rem); },
+ _ => (),
+ }
+ },
+ "UNMUTE" => {
+ // type, user_id, guild_id, mute_role, channel_id, dur
+ let uid = UserId::from_str(parts[1].as_str()).ok();
+ let gid = match parts[2].parse::<u64>() {
+ Ok(g) => Some(GuildId(g)),
+ _ => None,
+ };
+ let rid = RoleId::from_str(parts[3].as_str()).ok();
+ let cid = ChannelId::from_str(parts[4].as_str()).ok();
+ match (uid, gid, cid, rid) {
+ (Some(u), Some(g), Some(c), Some(r)) => { unmute(u,g,c,r); },
+ _ => (),
+ }
+ },
+ "COOLDOWN" => {
+ // type, user_id, guild_id, member_role_id, cooldown_role_id
+ let uid = UserId::from_str(parts[1].as_str()).ok();
+ let gid = match parts[2].parse::<u64>() {
+ Ok(g) => Some(GuildId(g)),
+ _ => None,
+ };
+ let mrid = RoleId::from_str(parts[3].as_str()).ok();
+ let crid = RoleId::from_str(parts[4].as_str()).ok();
+ match (uid, gid, mrid, crid) {
+ (Some(u), Some(g), Some(m), Some(c)) => { cooldown(u,g,m,c); },
+ _ => (),
+ }
+ },
+ _ => {},
+ }
+ check_error!(db.del_timer(timer.id));
+ },
+ }
+ }
+ },
+ Err(why) => {
+ debug!("{:?}", why);
+ use diesel::result::Error::*;
+ match why {
+ NotFound => { let _ = rx.recv(); },
+ _ => ()
+ }
+ },
+ }
+ }
+ });
+ }
+ });
+ TimerClient(tx)
+ }
+
+ pub fn request(&self) {
+ let _ = self.0.lock().send(false);
+ }
+}
diff --git a/src/core/utils.rs b/src/core/utils.rs
new file mode 100644
index 0000000..845b19f
--- /dev/null
+++ b/src/core/utils.rs
@@ -0,0 +1,302 @@
+use crate::core::consts::*;
+use regex::Regex;
+use serenity::CACHE;
+use serenity::Error;
+use serenity::model::channel::{GuildChannel, Message};
+use serenity::model::guild::{Guild, Role, Member};
+use serenity::model::id::*;
+use serenity::model::misc::Mentionable;
+use serenity::prelude::RwLock;
+use std::collections::HashMap;
+use std::str::FromStr;
+use std::sync::Arc;
+
+lazy_static! {
+ static ref CHANNEL_MATCH: Regex = Regex::new(r"(?:<#)?(\d{17,})>*?").expect("Failed to create Regex");
+ static ref EMBED_ITEM: Regex = Regex::new(r"\$[^\$]*").expect("Failed to create Regex");
+ static ref EMBED_PARTS: Regex = Regex::new(r"\$?(?P<field>\S+):(?P<value>.*)").expect("Failed to create Regex");
+ static ref GUILD_MATCH: Regex = Regex::new(r"\d{17,}").expect("Failed to create Regex");
+ static ref PLAIN_PARTS: Regex = Regex::new(r"\{.*?\}").expect("Failed to create Regex");
+ static ref ROLE_MATCH: Regex = Regex::new(r"(?:<@)?&?(\d{17,})>*?").expect("Failed to create Regex");
+ static ref SWITCH_PARTS: Regex = Regex::new(r"/\s*(\S+)([^/]*)").expect("Failed to create Regex");
+ static ref SWITCH_REST: Regex = Regex::new(r"^[^/]+").expect("Failed to create Regex");
+ static ref TIME: Regex = Regex::new(r"(\d+)\s*?(\w)").expect("Failed to create Regex");
+ static ref USER_MATCH: Regex = Regex::new(r"(?:<@)?!?(\d{17,})>*?").expect("Failed to create Regex");
+}
+
+/// Attempts to parse a role ID out of a string
+/// If the string does not contain a valid snowflake, attempt to match as name to cached roles
+/// This method is case insensitive
+pub fn parse_role(input: String, guild_id: GuildId) -> Option<(RoleId, Role)> {
+ match ROLE_MATCH.captures(input.as_str()) {
+ Some(s) => {
+ if let Ok(id) = RoleId::from_str(&s[1]) {
+ if let Some(role) = id.to_role_cached() {
+ return Some((id, role.clone()));
+ }
+ }
+ None
+ },
+ None => {
+ let guild_lock = CACHE.read().guild(&guild_id);
+ if let Some(guild_lock) = guild_lock {
+ let guild = guild_lock.read();
+ for (id, role) in guild.roles.iter() {
+ if role.name.to_lowercase() == input.to_lowercase() {
+ return Some((*id, role.clone()));
+ }
+ }
+ }
+ None
+ },
+ }
+}
+
+/// Attempts to parse a user ID out of a string
+/// If the string does not contain a valid snowflake, attempt to match as name to cached users
+/// This method is case insensitive
+pub fn parse_user(input: String, guild_id: GuildId) -> Option<(UserId, Member)> {
+ match USER_MATCH.captures(input.as_str()) {
+ Some(s) => {
+ if let Ok(id) = UserId::from_str(&s[1]) {
+ match CACHE.read().member(&guild_id, &id) {
+ Some(member) => {
+ return Some((id, member.clone()));
+ },
+ None => {
+ if let Ok(member) = guild_id.member(id) {
+ return Some((id, member));
+ }
+ },
+ }
+ }
+ None
+ },
+ None => {
+ let guild_lock = CACHE.read().guild(&guild_id);
+ if let Some(guild_lock) = guild_lock {
+ let guild = guild_lock.read();
+ for (id, member) in guild.members.iter() {
+ let user = member.user.read();
+ if user.name.to_lowercase() == input.to_lowercase()
+ || user.tag().to_lowercase() == input.to_lowercase()
+ || member.display_name().to_lowercase() == input.to_lowercase() {
+ return Some((*id, member.clone()));
+ }
+ }
+ }
+ None
+ },
+ }
+}
+
+/// Attempts to parse a channel ID out of a string
+/// If the string does not contain a valid snowflake, attempt to match as name to cached GuildChannels
+/// This method is case insensitive
+pub fn parse_channel(input: String, guild_id: GuildId) -> Option<(ChannelId, GuildChannel)> {
+ match CHANNEL_MATCH.captures(input.as_str()) {
+ Some(s) => {
+ if let Ok(id) = ChannelId::from_str(&s[1]) {
+ let ch_lock = CACHE.read().guild_channel(&id);
+ if let Some(ch_lock) = ch_lock {
+ let ch = ch_lock.read();
+ return Some((id, ch.clone()));
+ }
+ }
+ None
+ },
+ None => {
+ let guild_lock = CACHE.read().guild(&guild_id);
+ if let Some(guild_lock) = guild_lock {
+ let guild = guild_lock.read();
+ for (id, ch_lock) in guild.channels.iter() {
+ let ch = ch_lock.read();
+ if ch.name.to_lowercase() == input.to_lowercase() {
+ return Some((*id, ch.clone()));
+ }
+ }
+ }
+ None
+ },
+ }
+}
+
+/// Attempts to parse a guild ID out of a string
+/// If the string does not contain a valid snowflake, attempt to match as name to cached guild
+/// This method is case insensitive
+pub fn parse_guild(input: String) -> Option<(GuildId, Arc<RwLock<Guild>>)> {
+ match GUILD_MATCH.captures(input.as_str()) {
+ Some(s) => {
+ if let Ok(id) = s[0].parse::<u64>() {
+ let id = GuildId(id);
+ if let Some(g_lock) = id.to_guild_cached() {
+ return Some((id, g_lock));
+ }
+ }
+ None
+ },
+ None => {
+ let guilds = &CACHE.read().guilds;
+ for (id, g_lock) in guilds.iter() {
+ if g_lock.read().name.to_lowercase() == input.to_lowercase() {
+ return Some((*id, Arc::clone(g_lock)));
+ }
+ }
+ None
+ },
+ }
+}
+
+/// This is used for checking if a member has any roles that match the guild's configured mod_roles
+/// or admin_roles
+pub fn check_rank<T: AsRef<Vec<RoleId>>>(roles: Vec<i64>, member: T) -> bool {
+ for role in roles.iter() {
+ if member.as_ref().contains(&RoleId(*role as u64)) {
+ return true;
+ }
+ }
+ false
+}
+
+/// Parses a string for flags preceded by `/`
+/// The HashMap returned correlates to `/{key} {value}` where value may be an empty string.
+/// Additionally, the map will contain the key "rest" which contains anything in the string prior
+/// to any unescaped `/` appearing. If no unescaped `/` are present, this will also be the full
+/// string.
+pub fn get_switches(input: String) -> HashMap<String, String> {
+ let input = input.replace(r"\/", "∰"); // use an uncommon substitute because the regex crate doesn't support lookaround, we'll sub back after the regex does its thing
+ let mut map: HashMap<String, String> = HashMap::new();
+ if let Some(s) = SWITCH_REST.captures(input.as_str()) {
+ map.insert("rest".to_string(), s[0].replace("∰", "/").trim().to_string());
+ };
+ for s in SWITCH_PARTS.captures_iter(input.as_str()) {
+ map.insert(s[1].to_string(), s[2].replace("∰", "/").trim().to_string());
+ }
+ map
+}
+
+/// Converts a human-readable time to seconds
+/// Example inputs
+/// `3 days 2 hours 23 seconds`
+/// `7w2d4h`
+pub fn hrtime_to_seconds(time: String) -> i64 {
+ TIME.captures_iter(time.as_str())
+ .fold(0, |acc, s| {
+ match s[1].parse::<i64>() {
+ Err(_) => acc,
+ Ok(c) => {
+ match &s[2] {
+ "w" => acc + (c * WEEK as i64),
+ "d" => acc + (c * DAY as i64),
+ "h" => acc + (c * HOUR as i64),
+ "m" => acc + (c * MIN as i64),
+ "s" => acc + c,
+ _ => acc,
+ }
+ },
+ }
+ })
+}
+
+/// Converts a time in seconds to a human readable string
+pub fn seconds_to_hrtime(secs: usize) -> String {
+ let word = ["week", "day", "hour", "min", "sec"];
+ fn make_parts(t: usize, steps: &[usize], mut accum: Vec<usize>) -> Vec<usize> {
+ match steps.split_first() {
+ None => accum,
+ Some((s, steps)) => {
+ accum.push(t / *s);
+ make_parts(t % *s, steps, accum)
+ },
+ }
+ }
+
+ make_parts(secs, &[WEEK, DAY, HOUR, MIN, 1], Vec::new())
+ .iter()
+ .enumerate()
+ .filter_map(|(i, s)| {
+ if s > &0 {
+ if s > &1 {
+ Some(format!("{} {}s", s, word[i]))
+ } else {
+ Some(format!("{} {}", s, word[i]))
+ }
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<String>>()
+ .join(", ")
+}
+
+pub fn parse_welcome_items<S: Into<String>>(input: S, member: &Member) -> String {
+ let input = input.into();
+ let mut ret = input.clone();
+ let user = member.user.read();
+ for word in PLAIN_PARTS.captures_iter(input.as_str()) {
+ match word[0].to_lowercase().as_str() {
+ "{user}" => {
+ ret = ret.replace(&word[0], user.mention().as_str());
+ },
+ "{usertag}" => {
+ ret = ret.replace(&word[0], user.tag().as_str());
+ },
+ "{username}" => {
+ ret = ret.replace(&word[0], user.name.as_str());
+ },
+ "{guild}" => {
+ if let Ok(guild) = member.guild_id.to_partial_guild() {
+ ret = ret.replace(&word[0], guild.name.as_str());
+ }
+ },
+ "{membercount}" => {
+ if let Some(guild) = member.guild_id.to_guild_cached() {
+ ret = ret.replace(&word[0], guild.read().member_count.to_string().as_str());
+ }
+ },
+ _ => {},
+ }
+ }
+ ret
+}
+
+pub fn send_welcome_embed(input: String, member: &Member, channel: ChannelId) -> Result<Message, Error> {
+ let user = member.user.read();
+ channel.send_message(|m| { m .embed(|mut e| {
+ for item in EMBED_ITEM.captures_iter(input.as_str()) {
+ if let Some(caps) = EMBED_PARTS.captures(&item[0]) {
+ match caps["field"].to_lowercase().as_str() {
+ "title" => {
+ e = e.title(parse_welcome_items(&caps["value"], member));
+ },
+ "description" => {
+ e = e.description(parse_welcome_items(&caps["value"], member));
+ },
+ "thumbnail" => {
+ match caps["value"].to_lowercase().trim() {
+ "user" => {
+ e = e.thumbnail(user.face());
+ },
+ "member" => {
+ e = e.thumbnail(user.face());
+ },
+ "guild" => {
+ if let Ok(guild) = member.guild_id.to_partial_guild() {
+ if let Some(ref s) = guild.icon_url() {
+ e = e.thumbnail(s);
+ }
+ }
+ },
+ _ => {},
+ }
+ },
+ "color" | "colour" => {
+ e = e.colour(u64::from_str_radix(&caps["value"].trim().replace("#",""), 16).unwrap_or(0));
+ },
+ _ => {},
+ }
+ }
+ }
+ e
+ })})
+}