diff options
| author | Fuwn <[email protected]> | 2020-10-26 19:03:53 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2020-10-26 19:03:53 -0700 |
| commit | 9742614a1dc4699c1f2c69d923d402237672335d (patch) | |
| tree | a49f7d834372f37cef06b30a28ff1b40bdfaa079 /src/core | |
| parent | Create README.md (diff) | |
| download | dep-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.rs | 332 | ||||
| -rw-r--r-- | src/core/colours.rs | 8 | ||||
| -rw-r--r-- | src/core/consts.rs | 51 | ||||
| -rw-r--r-- | src/core/framework.rs | 206 | ||||
| -rw-r--r-- | src/core/handler.rs | 572 | ||||
| -rw-r--r-- | src/core/mod.rs | 8 | ||||
| -rw-r--r-- | src/core/model.rs | 33 | ||||
| -rw-r--r-- | src/core/timers.rs | 160 | ||||
| -rw-r--r-- | src/core/utils.rs | 302 |
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 + })}) +} |