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 | |
| 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')
45 files changed, 7905 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 + })}) +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..78f6030 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,511 @@ +//! A set of abstractions for manipulating a PgSQL database relevant to Wisp's stored data. +pub mod models; +mod schema; + +use chrono::offset::Utc; +use diesel::pg::PgConnection; +use diesel::pg::upsert::excluded; +use diesel::prelude::*; +use diesel::r2d2::{ + ConnectionManager, + Pool, + PooledConnection +}; +use diesel; +use self::models::*; +use self::schema::*; +use std::env; +use std::ops::Deref; + +/// While the struct itself and the connection are public, Database cannot be manually +/// instantiated. Use Database::connect() to start it. +pub struct Database { + pub pool: Pool<ConnectionManager<PgConnection>>, + _hidden: (), +} + +impl Database { + /// Create a new database with a connection. + /// Returns a new Database. + pub fn connect() -> Self { + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let manager = ConnectionManager::<PgConnection>::new(database_url); + let pool = Pool::builder() + .max_size(10) + .build(manager) + .expect("Failed to make connection pool"); + + Database { + pool, + _hidden: (), + } + } + + /// Request a connection from the connection pool + fn conn(&self) -> PooledConnection<ConnectionManager<PgConnection>> { + self.pool.clone().get().expect("Attempt to get connection timed out") + } + + // Guild Tools + /// Add a guild with a given ID. + /// Returns the Ok(Some(Guild)) on success or Ok(None) if there is a conflict. + /// May return Err(DatabaseError) in the event of some other failure. + pub fn new_guild(&self, id: i64) -> QueryResult<Option<Guild>> { + let guild = NewGuild { + id, + }; + diesel::insert_into(guilds::table) + .values(&guild) + .on_conflict_do_nothing() + .get_result(self.conn().deref()) + .optional() + } + /// Add multiple guilds with a vector of IDs + /// Does nothing on conflict + /// Returns Result<count, err> + pub fn new_guilds(&self, ids: &[i64]) -> QueryResult<usize> { + let guilds = { + ids.iter().map(|e| { + NewGuild { + id: *e, + } + }).collect::<Vec<NewGuild>>() + }; + diesel::insert_into(guilds::table) + .values(&guilds) + .on_conflict_do_nothing() + .execute(self.conn().deref()) + } + /// Delete a guild by the ID. + /// Returns Result<guild_id, err> + pub fn del_guild(&self, g_id: i64) -> QueryResult<i64> { + use crate::db::schema::guilds::columns::id; + diesel::delete(guilds::table) + .filter(id.eq(&g_id)) + .returning(id) + .get_result(self.conn().deref()) + } + /// Select a guild + /// Returns Result<Guild, Err> + pub fn get_guild(&self, g_id: i64) -> QueryResult<Guild> { + guilds::table.find(&g_id) + .first(self.conn().deref()) + } + /// Update a guild + /// Returns Result<Guild, Err> + pub fn update_guild(&self, g_id: i64, guild: Guild) -> QueryResult<Guild> { + let target = guilds::table.find(&g_id); + diesel::update(target) + .set(&guild) + .get_result(self.conn().deref()) + } + /// Get the count of guilds in the database + pub fn count_guilds(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + guilds::table.select(count_star()) + .get_result(self.conn().deref()) + } + + // User Tools + /// Add a user with a given user ID and guild ID. + /// Returns the User on success. + pub fn new_user(&self, id: i64, guild_id: i64) -> QueryResult<User<Utc>> { + let user = NewUser { + id, + guild_id, + }; + diesel::insert_into(users::table) + .values(&user) + .get_result(self.conn().deref()) + } + /// Delete a user by user ID and guild ID. + /// Returns the ID on success. + pub fn del_user(&self, u_id: i64, g_id: i64) -> QueryResult<i64> { + use crate::db::schema::users::columns::{id, guild_id}; + diesel::delete(users::table) + .filter(id.eq(&u_id)) + .filter(guild_id.eq(&g_id)) + .returning(id) + .get_result(self.conn().deref()) + } + /// Select a user + /// Returns the user on success + pub fn get_user(&self, u_id: i64, g_id: i64) -> QueryResult<User<Utc>> { + users::table.find((u_id, g_id)) + .first(self.conn().deref()) + } + /// Select all users in a guild + /// Returns a vector of users on success + pub fn get_users(&self, g_id: i64) -> QueryResult<Vec<User<Utc>>> { + use crate::db::schema::users::columns::guild_id; + users::table.filter(guild_id.eq(&g_id)) + .get_results(self.conn().deref()) + } + /// Update a user + /// Returns the new user on success + pub fn update_user(&self, u_id: i64, g_id: i64, user: User<Utc>) -> QueryResult<User<Utc>> { + let target = users::table.find((u_id, g_id)); + diesel::update(target) + .set(&user) + .get_result(self.conn().deref()) + } + /// Upsert a user + /// Returns the new user on success + pub fn upsert_user(&self, user: UserUpdate) -> QueryResult<User<Utc>> { + use crate::db::schema::users::columns::{id, guild_id}; + diesel::insert_into(users::table) + .values(&user) + .on_conflict((id, guild_id)) + .do_update() + .set(&user) + .get_result(self.conn().deref()) + } + /// Upserts multiple users with a vector of UserUpdates + /// Returns Result<count, err> + pub fn upsert_users(&self, users: &[UserUpdate]) -> QueryResult<usize> { + use crate::db::schema::users::columns::*; + diesel::insert_into(users::table) + .values(users) + .on_conflict((id, guild_id)) + .do_update() + .set((nickname.eq(excluded(nickname)), + username.eq(excluded(username)), + roles.eq(excluded(roles)))) + .execute(self.conn().deref()) + } + /// Get the count of users in the database + pub fn count_users(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + users::table.select(count_star()) + .get_result(self.conn().deref()) + } + + // Role Tools + /// Add a role with the given role ID, guild ID, and optionally a category and aliases. + /// Returns the Role on success. + pub fn new_role(&self, id: i64, guild_id: i64, category: Option<String>, aliases: Option<Vec<String>>) -> QueryResult<Role> { + let role = NewRole { + id, + guild_id, + category, + aliases, + }; + diesel::insert_into(roles::table) + .values(&role) + .get_result(self.conn().deref()) + } + /// Delete a role by role ID and guild ID. + /// Returns the ID on success. + pub fn del_role(&self, r_id: i64, g_id: i64) -> QueryResult<i64> { + use crate::db::schema::roles::columns::{id, guild_id}; + diesel::delete(roles::table) + .filter(id.eq(&r_id)) + .filter(guild_id.eq(&g_id)) + .returning(id) + .get_result(self.conn().deref()) + } + /// Select a role + /// Returns the role on success + pub fn get_role(&self, r_id: i64, g_id: i64) -> QueryResult<Role> { + roles::table.find((r_id, g_id)) + .first(self.conn().deref()) + } + /// Select all roles by guild id + /// Returns a vector of roles on success + pub fn get_roles(&self, g_id: i64) -> QueryResult<Vec<Role>> { + use crate::db::schema::roles::columns::guild_id; + roles::table.filter(guild_id.eq(&g_id)) + .get_results(self.conn().deref()) + } + /// Update a role + /// Returns the new role on success + pub fn update_role(&self, r_id: i64, g_id: i64, role: Role) -> QueryResult<Role> { + let target = roles::table.find((r_id, g_id)); + diesel::update(target) + .set(&role) + .get_result(self.conn().deref()) + } + /// Get the count of roles in the database + pub fn count_roles(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + roles::table.select(count_star()) + .get_result(self.conn().deref()) + } + + // Note Tools + /// Add a note to the given user in the given guild by a given moderator + /// Returns the Note on success. + pub fn new_note(&self, user_id: i64, guild_id: i64, note: String, moderator: i64) -> QueryResult<Note<Utc>> { + let note = NewNote { + user_id, + guild_id, + note, + moderator, + }; + diesel::insert_into(notes::table) + .values(¬e) + .get_result(self.conn().deref()) + } + /// Delete a note by index, user ID, and guild ID. + /// Returns the Note.note on success. + pub fn del_note(&self, n_id: i32, u_id: i64, g_id: i64) -> QueryResult<String> { + use crate::db::schema::notes::columns::{user_id, guild_id, id, note}; + diesel::delete(notes::table) + .filter(user_id.eq(&u_id)) + .filter(guild_id.eq(&g_id)) + .filter(id.eq(&n_id)) + .returning(note) + .get_result(self.conn().deref()) + } + /* + /// Select a note + /// Returns the note on success + pub fn get_note(&self, n_id: i32, u_id: i64, g_id: i64) -> QueryResult<Note<Utc>> { + notes::table.find((n_id, u_id, g_id)) + .first(self.conn().deref()) + }*/ + /// Select all notes for a user + /// Returns a vec of notes on success + pub fn get_notes(&self, u_id: i64, g_id: i64) -> QueryResult<Vec<Note<Utc>>> { + use crate::db::schema::notes::columns::{user_id, guild_id}; + notes::table.filter(user_id.eq(&u_id)) + .filter(guild_id.eq(&g_id)) + .get_results(self.conn().deref()) + } + /// Get the count of notes in the database + pub fn count_notes(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + notes::table.select(count_star()) + .get_result(self.conn().deref()) + } + + // Timer Tools + /// Add a timer + /// Returns the timer on success. + pub fn new_timer(&self, starttime: i64, endtime: i64, data: String) -> QueryResult<Timer> { + let timer = NewTimer { + starttime, + endtime, + data, + }; + diesel::insert_into(timers::table) + .values(&timer) + .get_result(self.conn().deref()) + } + /// Delete a timer with the given ID. + /// Returns the note data on success. + pub fn del_timer(&self, t_id: i32) -> QueryResult<String> { + use crate::db::schema::timers::columns::{id, data}; + diesel::delete(timers::table) + .filter(id.eq(&t_id)) + .returning(data) + .get_result(self.conn().deref()) + } + /* + /// Select a timer + /// Returns the timer on success + pub fn get_timer(&self, t_id: i32) -> QueryResult<Timer> { + timers::table.find(t_id) + .first(self.conn().deref()) + }*/ + /// Select all timers + /// Returns a vec of timers on success + pub fn get_timers(&self) -> QueryResult<Vec<Timer>> { + timers::table.get_results(self.conn().deref()) + } + /// Get the count of timers in the database + pub fn count_timers(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + timers::table.select(count_star()) + .get_result(self.conn().deref()) + } + /// Get the timer with the closest expiration time to the present + pub fn get_earliest_timer(&self) -> QueryResult<Timer> { + use crate::db::schema::timers::{all_columns, columns::endtime}; + timers::table.select(all_columns) + .order(endtime.asc()) + .first(self.conn().deref()) + } + + // Case Tools + /// Add a Case + /// Returns the Case on success + pub fn new_case(&self, user_id: i64, guild_id: i64, casetype: String, reason: Option<String>, moderator: i64) -> QueryResult<Case<Utc>> { + let case = NewCase { + user_id, + guild_id, + casetype, + reason, + moderator, + }; + diesel::insert_into(cases::table) + .values(&case) + .get_result(self.conn().deref()) + } + /* + /// Delete a case + /// Returns the case on success. + pub fn del_case(&self, c_id: i32, u_id: i64, g_id: i64) -> QueryResult<Case<Utc>> { + use db::schema::cases::columns::{id, user_id, guild_id}; + diesel::delete(cases) + .filter(id.eq(&c_id)) + .filter(user_id.eq(&u_id)) + .filter(guild_id.eq(&g_id)) + .get_result(self.conn().deref()) + } + /// Select a case + /// Returns the case on success + pub fn get_case(&self, c_id: i32, u_id: i64, g_id: i64) -> QueryResult<Case<Utc>> { + cases::table.find((c_id, u_id, g_id)) + .first(self.conn().deref()) + }*/ + /// Select all cases for a user + /// Returns a vector of cases on success + pub fn get_cases(&self, u_id: i64, g_id: i64) -> QueryResult<Vec<Case<Utc>>> { + use crate::db::schema::cases::columns::{guild_id, user_id}; + cases::table.filter(user_id.eq(&u_id)) + .filter(guild_id.eq(&g_id)) + .get_results(self.conn().deref()) + } + /// Get the count of cases in the database + pub fn count_cases(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + cases::table.select(count_star()) + .get_result(self.conn().deref()) + } + + // Tag Tools + /// Add a Tag + /// Returns the Tag on success + pub fn new_tag(&self, author: i64, guild_id: i64, name: String, data: String) -> QueryResult<Tag> { + let tag = NewTag { + author, + guild_id, + name, + data, + }; + diesel::insert_into(tags::table) + .values(&tag) + .get_result(self.conn().deref()) + } + /// Delete a Tag + /// Returns the Tag on success. + pub fn del_tag(&self, g_id: i64, nm: String) -> QueryResult<Tag> { + use crate::db::schema::tags::columns::{name, guild_id}; + diesel::delete(tags::table) + .filter(name.eq(&nm)) + .filter(guild_id.eq(&g_id)) + .get_result(self.conn().deref()) + } + /// Select a Tag + /// Returns the Tag on success + pub fn get_tag(&self, g_id: i64, nm: String) -> QueryResult<Tag> { + tags::table.find((g_id, nm)) + .first(self.conn().deref()) + } + /// Select all tags by guild + /// Returns Vec<Tag> on success on success + pub fn get_tags(&self, g_id: i64) -> QueryResult<Vec<Tag>> { + use crate::db::schema::tags::columns::guild_id; + tags::table.filter(guild_id.eq(&g_id)) + .get_results(self.conn().deref()) + } + /// Update a tag + /// Returns the new tag on success + pub fn update_tag(&self, g_id: i64, nm: String, tag: Tag) -> QueryResult<Tag> { + let target = tags::table.find((g_id, nm)); + diesel::update(target) + .set(&tag) + .get_result(self.conn().deref()) + } + /// Get the count of tags in the database + pub fn count_tags(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + tags::table.select(count_star()) + .get_result(self.conn().deref()) + } + + // Premium Tools + /// Add premium with a given guild ID. + /// Returns the PremiumSettings on success. + pub fn new_premium(&self, id: i64) -> QueryResult<PremiumSettings> { + let prem = NewPremium { + id, + }; + diesel::insert_into(premium::table) + .values(&prem) + .get_result(self.conn().deref()) + } + /// Delete premium by a guild ID. + /// Returns the ID on success. + pub fn del_premium(&self, g_id: i64) -> QueryResult<i64> { + use crate::db::schema::premium::columns::id; + diesel::delete(premium::table) + .filter(id.eq(&g_id)) + .returning(id) + .get_result(self.conn().deref()) + } + /// Select PremiumSettings by guild ID + /// Returns the settings on success + /// Will return Err if the guild is not premium + pub fn get_premium(&self, g_id: i64) -> QueryResult<PremiumSettings> { + premium::table.find(&g_id) + .first(self.conn().deref()) + } + /// Update PremiumSettings + /// Returns the new settings on success + pub fn update_premium(&self, g_id: i64, settings: PremiumSettings) -> QueryResult<PremiumSettings> { + let target = premium::table.find(&g_id); + diesel::update(target) + .set(&settings) + .get_result(self.conn().deref()) + } + /// Get the count of guilds with premium in the database + pub fn count_premium(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + premium::table.select(count_star()) + .get_result(self.conn().deref()) + } + + // Tag Tools + /// Add a Hackban + /// Returns the Hackban on success + pub fn new_hackban(&self, id: i64, guild_id: i64, reason: Option<String>) -> QueryResult<Hackban> { + let hb = Hackban { + id, + guild_id, + reason, + }; + diesel::insert_into(hackbans::table) + .values(&hb) + .get_result(self.conn().deref()) + } + /// Delete a Hackban + /// Returns the Hackban on success. + pub fn del_hackban(&self, h_id: i64, g_id: i64) -> QueryResult<Hackban> { + use crate::db::schema::hackbans::columns::{id, guild_id}; + diesel::delete(hackbans::table) + .filter(id.eq(&h_id)) + .filter(guild_id.eq(&g_id)) + .get_result(self.conn().deref()) + } + /// Select a Hackban + /// Returns the Hackban on success + pub fn get_hackban(&self, id: i64, g_id: i64) -> QueryResult<Hackban> { + hackbans::table.find((id, g_id)) + .first(self.conn().deref()) + } + /// Select all hackbans by guild + /// Returns Vec<Hackban> on success on success + pub fn get_hackbans(&self, g_id: i64) -> QueryResult<Vec<Hackban>> { + use crate::db::schema::hackbans::columns::guild_id; + hackbans::table.filter(guild_id.eq(&g_id)) + .get_results(self.conn().deref()) + } + /// Get the count of hackbans in the database + pub fn count_hackbans(&self) -> QueryResult<i64> { + use diesel::dsl::count_star; + hackbans::table.select(count_star()) + .get_result(self.conn().deref()) + } +} diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100644 index 0000000..2e6a460 --- /dev/null +++ b/src/db/models.rs @@ -0,0 +1,253 @@ +use chrono::{DateTime, TimeZone, Utc}; +use serenity::model::id::{UserId, RoleId}; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use super::schema::*; + +// Query-ables +#[derive(Queryable, Identifiable, AsChangeset, Debug)] +#[primary_key(id)] +pub struct Guild { + pub id: i64, + pub admin_roles: Vec<i64>, + pub audit: bool, + pub audit_channel: i64, + pub audit_threshold: i16, + pub autorole: bool, + pub autoroles: Vec<i64>, + pub ignored_channels: Vec<i64>, + pub ignore_level: i16, + pub introduction: bool, + pub introduction_channel: i64, + pub introduction_message: String, + pub introduction_type: String, + pub mod_roles: Vec<i64>, + pub modlog: bool, + pub modlog_channel: i64, + pub mute_setup: bool, + pub prefix: String, + pub welcome: bool, + pub welcome_channel: i64, + pub welcome_message: String, + pub welcome_type: String, + pub goodbye: bool, + pub goodbye_channel: i64, + pub goodbye_message: String, + pub goodbye_type: String, + pub commands: Vec<String>, + pub logging: Vec<String>, +} + +// Deprecated fields: nickname, roles +#[derive(Queryable, Identifiable, AsChangeset, Debug)] +#[primary_key(id, guild_id)] +pub struct User<Tz: TimeZone> { + pub id: i64, + pub guild_id: i64, + pub username: String, + pub nickname: String, + pub roles: Vec<i64>, + pub watchlist: bool, + pub xp: i64, + pub last_message: DateTime<Tz>, + pub registered: Option<DateTime<Tz>>, +} + +#[derive(Queryable, Identifiable, AsChangeset, Debug)] +#[primary_key(id, user_id, guild_id)] +pub struct Note<Tz: TimeZone> { + pub id: i32, + pub user_id: i64, + pub guild_id: i64, + pub note: String, + pub moderator: i64, + pub timestamp: DateTime<Tz>, +} + +#[derive(Queryable, Identifiable, AsChangeset, Debug)] +#[primary_key(id, guild_id)] +pub struct Role { + pub id: i64, + pub guild_id: i64, + pub category: String, + pub aliases: Vec<String>, + pub required_roles: Vec<i64>, + pub forbidden_roles: Vec<i64>, +} + +#[derive(Queryable, Identifiable, AsChangeset, Debug)] +#[primary_key(id)] +pub struct Timer { + pub id: i32, + pub starttime: i64, + pub endtime: i64, + pub data: String, +} + +#[derive(Queryable, Identifiable, AsChangeset, Debug)] +#[primary_key(id, user_id, guild_id)] +pub struct Case<Tz: TimeZone> { + pub id: i32, + pub user_id: i64, + pub guild_id: i64, + pub casetype: String, + pub reason: String, + pub moderator: i64, + pub timestamp: DateTime<Tz> +} + +#[derive(Queryable, Identifiable, AsChangeset, Debug)] +#[primary_key(guild_id, name)] +pub struct Tag { + pub author: i64, + pub guild_id: i64, + pub name: String, + pub data: String, +} + +#[derive(Queryable, Identifiable, AsChangeset, Debug)] +#[table_name="premium"] +pub struct PremiumSettings { + pub id: i64, + pub tier: i32, + pub register_member_role: Option<i64>, + pub register_cooldown_role: Option<i64>, + pub register_cooldown_duration: Option<i32>, + pub cooldown_restricted_roles: Vec<i64>, +} + +// This one would be the same for insertable or queryable, so it has both +#[derive(Queryable, Identifiable, AsChangeset, Insertable, Clone, Debug)] +#[primary_key(id, guild_id)] +pub struct Hackban { + pub id: i64, + pub guild_id: i64, + pub reason: Option<String>, +} +// End Query-ables + +// Insertables +#[derive(Insertable)] +#[table_name="guilds"] +pub struct NewGuild { + pub id: i64, +} + +#[derive(Insertable)] +#[table_name="users"] +pub struct NewUser { + pub id: i64, + pub guild_id: i64, +} + +#[derive(Insertable)] +#[table_name="notes"] +pub struct NewNote { + pub user_id: i64, + pub guild_id: i64, + pub note: String, + pub moderator: i64, +} + +#[derive(Insertable)] +#[table_name="roles"] +pub struct NewRole { + pub id: i64, + pub guild_id: i64, + pub category: Option<String>, + pub aliases: Option<Vec<String>>, +} + +#[derive(Insertable)] +#[table_name="timers"] +pub struct NewTimer { + pub starttime: i64, + pub endtime: i64, + pub data: String, +} + +#[derive(Insertable)] +#[table_name="cases"] +pub struct NewCase { + pub user_id: i64, + pub guild_id: i64, + pub casetype: String, + pub reason: Option<String>, + pub moderator: i64, +} + +#[derive(Insertable)] +#[table_name="tags"] +pub struct NewTag { + pub author: i64, + pub guild_id: i64, + pub name: String, + pub data: String, +} + +#[derive(Insertable, Debug)] +#[table_name="premium"] +pub struct NewPremium { + pub id: i64, +} +// End Insertables + +// Other Stuff +#[derive(Insertable, AsChangeset, Debug)] +#[table_name="users"] +#[primary_key(id, guild_id)] +pub struct UserUpdate { + pub id: i64, + pub guild_id: i64, + pub username: String, +} + +impl Display for Guild { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "**Admin Roles:** {}\n**Audit:** {}\n**Audit Channel:** {}\n**Audit Threshold:** {}\n**Autorole:** {}\n**Autoroles:** {}\n**Ignored Channels:** {}\n**Ignore Level:** {}\n**Introduction:** {}\n**Introduction Channel:** {}\n**Introduction Type:** {}\n**Introduction Message:** {}\n**Mod Roles: ** {}\n**Modlog:** {}\n**Modlog Channel:** {}\n**Mute Setup:** {}\n**Prefix:** {}\n**Welcome:** {}\n**Welcome Channel:** {}\n**Welcome Type:** {}\n**Welcome Message:** {}\n**Disabled Commands:** {}\n**Disabled Log Types:** {}", + self.admin_roles.iter().map(|e| match RoleId(*e as u64).to_role_cached() { + Some(role) => role.name, + None => format!("{}", e), + }).collect::<Vec<String>>().join(", "), + self.audit, + format!("<#{}>", self.audit_channel), + self.audit_threshold, + self.autorole, + self.autoroles.iter().map(|e| match RoleId(*e as u64).to_role_cached() { + Some(role) => role.name, + None => format!("{}", e), + }).collect::<Vec<String>>().join(", "), + self.ignored_channels.iter().map(|e| format!("<#{}>", e)).collect::<Vec<String>>().join(", "), + self.ignore_level, + self.introduction, + format!("<#{}>", self.introduction_channel), + self.introduction_type, + self.introduction_message, + self.mod_roles.iter().map(|e| match RoleId(*e as u64).to_role_cached() { + Some(role) => role.name, + None => format!("{}", e), + }).collect::<Vec<String>>().join(", "), + self.modlog, + format!("<#{}>", self.modlog_channel), + self.mute_setup, + self.prefix, + self.welcome, + format!("<#{}>", self.welcome_channel), + self.welcome_type, + self.welcome_message, + self.commands.join(", "), + self.logging.join(", ") + )} +} + +impl Display for Note<Utc> { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{} wrote on {} (ID: {})\n`{}`", + match UserId(self.moderator as u64).to_user() { + Ok(user) => user.tag(), + Err(_) => format!("{}", self.moderator), + }, + self.timestamp.format("%a, %d %h %Y @ %H:%M:%S").to_string(), + self.id, + self.note) + } +} diff --git a/src/db/schema.rs b/src/db/schema.rs new file mode 100644 index 0000000..25f16ab --- /dev/null +++ b/src/db/schema.rs @@ -0,0 +1,129 @@ +table! { + cases (id, user_id, guild_id) { + id -> Int4, + user_id -> Int8, + guild_id -> Int8, + casetype -> Text, + reason -> Text, + moderator -> Int8, + timestamp -> Timestamptz, + } +} + +table! { + guilds (id) { + id -> Int8, + admin_roles -> Array<Int8>, + audit -> Bool, + audit_channel -> Int8, + audit_threshold -> Int2, + autorole -> Bool, + autoroles -> Array<Int8>, + ignored_channels -> Array<Int8>, + ignore_level -> Int2, + introduction -> Bool, + introduction_channel -> Int8, + introduction_message -> Text, + introduction_type -> Text, + mod_roles -> Array<Int8>, + modlog -> Bool, + modlog_channel -> Int8, + mute_setup -> Bool, + prefix -> Text, + welcome -> Bool, + welcome_channel -> Int8, + welcome_message -> Text, + welcome_type -> Text, + goodbye -> Bool, + goodbye_channel -> Int8, + goodbye_message -> Text, + goodbye_type -> Text, + commands -> Array<Text>, + logging -> Array<Text>, + } +} + +table! { + hackbans (id, guild_id) { + id -> Int8, + guild_id -> Int8, + reason -> Nullable<Text>, + } +} + +table! { + notes (id, user_id, guild_id) { + id -> Int4, + user_id -> Int8, + guild_id -> Int8, + note -> Text, + moderator -> Int8, + timestamp -> Timestamptz, + } +} + +table! { + premium (id) { + id -> Int8, + tier -> Int4, + register_member_role -> Nullable<Int8>, + register_cooldown_role -> Nullable<Int8>, + register_cooldown_duration -> Nullable<Int4>, + cooldown_restricted_roles -> Array<Int8>, + } +} + +table! { + roles (id, guild_id) { + id -> Int8, + guild_id -> Int8, + category -> Text, + aliases -> Array<Text>, + required_roles -> Array<Int8>, + forbidden_roles -> Array<Int8>, + } +} + +table! { + tags (guild_id, name) { + author -> Int8, + guild_id -> Int8, + name -> Text, + data -> Text, + } +} + +table! { + timers (id) { + id -> Int4, + starttime -> Int8, + endtime -> Int8, + data -> Text, + } +} + +table! { + users (id, guild_id) { + id -> Int8, + guild_id -> Int8, + username -> Text, + nickname -> Text, + roles -> Array<Int8>, + watchlist -> Bool, + xp -> Int8, + last_message -> Timestamptz, + registered -> Nullable<Timestamptz>, + } +} + +allow_tables_to_appear_in_same_query!( + cases, + guilds, + hackbans, + notes, + premium, + roles, + tags, + timers, + users, +); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0970dcf --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,32 @@ +#![recursion_limit = "128"] +#![allow(proc_macro_derive_resolution_fallback)] + +#[macro_use] extern crate diesel; +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate log; +#[macro_use] extern crate serde_derive; +extern crate serenity; +extern crate chrono; +extern crate forecast; +extern crate fuzzy_match; +extern crate geocoding; +extern crate kitsu; +extern crate levenshtein; +extern crate rand; +extern crate regex; +extern crate reqwest; +extern crate serde; +extern crate serde_json; +extern crate sys_info; +extern crate sysinfo; +extern crate threadpool; +extern crate typemap; +extern crate urbandictionary; + +pub mod macros; +pub mod core; +pub mod db; +pub mod modules; +pub mod wisp; // Client + +pub use crate::wisp::WispClient; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..8b4d3af --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,19 @@ +#![macro_use] + +#[macro_export] +macro_rules! check_error { + ($e:expr) => { + if let Err(err) = $e { + warn!("ERROR [{}:{}] {:?}", line!(), column!(), err); + } + }; +} + +macro_rules! failed { + ($e:expr) => { warn!("[{}:{}] {}", line!(), column!(), $e); }; + ($e:expr, $w:expr) => { warn!("[{}:{}] {} | {}", line!(), column!(), $e, $w); }; +} + +macro_rules! now { + () => { Utc::now().format("%FT%T").to_string() }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e4235a0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,63 @@ +#[macro_use] extern crate log; +#[macro_use] extern crate wisp; +extern crate chrono; +extern crate fern; +extern crate kankyo; + +use fern::colors::{Color, ColoredLevelConfig}; +use wisp::WispClient; + +fn main() { + kankyo::load().expect("Failed to load .env file."); + fern_setup().expect("Failed to apply Fern settings."); + let mut client = WispClient::new(); + check_error!(client.start()); +} + +fn fern_setup() -> Result<(), log::SetLoggerError> { + let colours = ColoredLevelConfig::new() + .trace(Color::Magenta) + .debug(Color::Cyan) + .info(Color::Green) + .warn(Color::Yellow) + .error(Color::Red); + + + let term_out = fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "{time} {level:level_width$}{target:target_width$}> {msg}", + time = chrono::Utc::now().format("%F %T"), + level = colours.color(record.level()), + target = format!("{}:{}", record.target(), record.line().unwrap_or(0)), + msg = message, + level_width = 8, + target_width = 60 + )) + }) + .chain(std::io::stdout()) + .into_shared(); + + let file_out = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{time} {level:level_width$}{target:target_width$}> {msg}", + time = chrono::Utc::now().format("%F %T"), + level = record.level(), + target = format!("{}:{}", record.target(), record.line().unwrap_or(0)), + msg = message, + level_width = 8, + target_width = 60 + )) + }) + .chain(fern::log_file("output.log").expect("Failed to load log file")) + .into_shared(); + + fern::Dispatch::new() + .level(log::LevelFilter::Info) + .level_for("serenity", log::LevelFilter::Debug) + .level_for("wisp", log::LevelFilter::Debug) + .chain(term_out) + .chain(file_out) + .apply() +} diff --git a/src/modules/commands/.todo b/src/modules/commands/.todo new file mode 100644 index 0000000..487377b --- /dev/null +++ b/src/modules/commands/.todo @@ -0,0 +1,52 @@ +Key: + * - Basically in. + / - Maybe... + +Commands: + Anime: + - [/] Darling + - [*] Waifu + Bot: + - [*] Donate + - [*] Suggest + Emma: + - [/] Fan Art + - [/] Ugly Cat + - [/] Verify + Fun: + - [/] Advice + - [/] Date Fact + - [/] Day Fact + - [/] FML + - [/] Fact + - [/] GitHub Zen + - [/] Insult + - [/] Minecraft Server Status + - [/] Number Fact + - [/] Onion + - [/] Spoiler + - [/] Year Fact + Moderation: + - [ ] Slow-mode + NSFW: + - [ ] Danbooru + - [ ] Gelbooru + - [ ] Rule 34 + Management: + - [ ] DM + - [/] Database + - [/] IP + - [ ] Leave Server + - [ ] Reload + - [/] Server Count + - [/] Status + - [/] Username + Reaction: + - [/] List + - [/] New + - [/] Remove + Server: + - [ ] Goodbye + - [ ] Oldest Member + - [/] Poll + - [ ] Random Member diff --git a/src/modules/commands/admins/config.rs b/src/modules/commands/admins/config.rs new file mode 100644 index 0000000..a26fc3d --- /dev/null +++ b/src/modules/commands/admins/config.rs @@ -0,0 +1,756 @@ +use crate::core::colours; +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct ConfigRaw; +impl Command for ConfigRaw { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Oh, you want it, raw...".to_string()), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let guild_data = db.get_guild(guild_id.0 as i64)?; + message.channel_id.say(format!("{:?}", guild_data))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigList; +impl Command for ConfigList { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Yeah, sure, here's my config.".to_string()), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let guild_data = db.get_guild(guild_id.0 as i64)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .colour(*colours::MAIN) + .description(format!("{}", guild_data)) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigPrefix; +impl Command for ConfigPrefix { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Change it, go ahead.".to_string()), + usage: Some("<prefix>".to_string()), + example: Some("!!".to_string()), + min_args: Some(1), + max_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let pre = args.single::<String>()?; + guild_data.prefix = pre; + match db.update_guild(guild_id.0 as i64, guild_data) { + Ok(guild) => { + message.channel_id.say(format!("Set prefix to {}", guild.prefix))?; + }, + Err(_) => { + message.channel_id.say("Failed to change prefix")?; + }, + } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigAutorole; +impl Command for ConfigAutorole { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Want to mess with my auto-role settings ? (A role must be provided for `add` or `remove`.)".to_string()), + usage: Some("<add|remove|enable|disable> <role_resolvable|_>".to_string()), + example: Some("add member".to_string()), + min_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let mut val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "add" => { + match parse_role(val.to_string(), guild_id) { + Some((role_id, role)) => { + guild_data.autoroles.push(role_id.0 as i64); + val = format!("{} ({})", role.name, role_id.0); + }, + None => { + message.channel_id.say("I couldn't find that role.")?; + return Ok(()) + }, + } + }, + "remove" => { + match parse_role(val.to_string(), guild_id) { + Some((role_id, role)) => { + guild_data.autoroles.retain(|e| *e != role_id.0 as i64); + val = format!("{} ({})", role.name, role_id.0); + }, + None => { + message.channel_id.say("I couldn't find that role.")?; + return Ok(()) + }, + } + }, + "enable" => { + guild_data.autorole = true; + }, + "disable" => { + guild_data.autorole = false; + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `add`, `remove`, `enable`, `disable`. For more information see `help config autorole`")?; + return Ok(()) + }, + } + let guild = db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Config Autorole Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + if val.is_empty() { guild.autorole.to_string() } else { val } , + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigAdmin; +impl Command for ConfigAdmin { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Who do you want me to consider an admin ?".to_string()), + usage: Some("<add|remove> <role_resolvable>".to_string()), + example: Some("add admin".to_string()), + min_args: Some(2), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let mut val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "add" => { + match parse_role(val.to_string(), guild_id) { + Some((role_id, role)) => { + guild_data.admin_roles.push(role_id.0 as i64); + val = format!("{} ({})", role.name, role_id.0); + }, + None => { + message.channel_id.say("I couldn't find that role.")?; + return Ok(()) + }, + } + }, + "remove" => { + match parse_role(val.to_string(), guild_id) { + Some((role_id, role)) => { + guild_data.admin_roles.retain(|e| *e != role_id.0 as i64); + val = format!("{} ({})", role.name, role_id.0); + }, + None => { + message.channel_id.say("I couldn't find that role.")?; + return Ok(()) + }, + } + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `add`, `remove`. For more information see `help config admin`")?; + return Ok(()) + }, + } + db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Config Admin Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + val, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigMod; +impl Command for ConfigMod { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Welcome to moderation !".to_string()), + usage: Some("<add|remove> <role_resolvable>".to_string()), + example: Some("add staff".to_string()), + min_args: Some(2), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let mut val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "add" => { + match parse_role(val.to_string(), guild_id) { + Some((role_id, role)) => { + guild_data.mod_roles.push(role_id.0 as i64); + val = format!("{} ({})", role.name, role_id.0); + }, + None => { + message.channel_id.say("I couldn't find that role.")?; + return Ok(()) + }, + } + }, + "remove" => { + match parse_role(val.to_string(), guild_id) { + Some((role_id, role)) => { + guild_data.mod_roles.retain(|e| *e != role_id.0 as i64); + val = format!("{} ({})", role.name, role_id.0); + }, + None => { + message.channel_id.say("I couldn't find that role.")?; + return Ok(()) + }, + } + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `add`, `remove`. For more information see `help config mod`")?; + return Ok(()) + }, + } + db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Config Mod Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + val, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigAudit; +impl Command for ConfigAudit { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Amping up security around here ? Change audit log settings. (A channel must be provided for `channel`.)".to_string()), + usage: Some("<enable|disable|channel> <channel_resolvable>".to_string()), + example: Some("channel #audit-logs".to_string()), + min_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let mut val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "enable" => { + guild_data.audit = true; + }, + "disable" => { + guild_data.audit = false; + }, + "channel" => { + match parse_channel(val.to_string(), guild_id) { + Some((channel_id, channel)) => { + guild_data.audit_channel = channel_id.0 as i64; + val = format!("{} ({})", channel.name, channel_id.0); + }, + None => { + message.channel_id.say("I couldn't find that channel.")?; + return Ok(()) + }, + } + }, + "threshold" => { + match val.parse::<i16>() { + Ok(th) => { + guild_data.audit_threshold = th; + val = th.to_string(); + }, + Err(_) => { message.channel_id.say("Please input a number as the threshold")?; } + } + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `enable`, `disable`, `channel`, `threshold`. For more information see `help config audit`")?; + return Ok(()) + }, + } + let guild = db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Config Audit Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + if val.is_empty() { format!("{}", guild.audit) } else { val }, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigModlog; +impl Command for ConfigModlog { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Moderation log settings, move along. (A channel must be provided for `channel`.)".to_string()), + usage: Some("<enable|disable|channel> <channel_resolvable>".to_string()), + example: Some("channel #mod-logs".to_string()), + min_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let mut val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "enable" => { + guild_data.modlog = true; + }, + "disable" => { + guild_data.modlog = false; + }, + "channel" => { + match parse_channel(val.to_string(), guild_id) { + Some((channel_id, channel)) => { + guild_data.modlog_channel = channel_id.0 as i64; + val = format!("{} ({})", channel.name, channel_id.0); + }, + None => { + message.channel_id.say("I couldn't find that channel.")?; + return Ok(()) + }, + } + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `enable`, `disable`, `channel`. For more information see `help config modlog`")?; + return Ok(()) + }, + } + let guild = db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Config Modlog Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + if val.is_empty() { guild.modlog.to_string() } else { val }, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigWelcome; +impl Command for ConfigWelcome { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + // desc: Some("Change welcome message settings.\nOption is one of enable, disable, channel, message, type and the respective values should be none, none, channel_resolvable, desired message.\nType designates if the message is plain or embed. Anything other than embed will result in plain.".to_string()), + desc: Some("Want a sick, custom welcome message !?\n\nOptions:\n`enable` - none.\n`disable` - none.\n`channel` - `channel_resolvable` (Mentionable).\n`message` - Your desired welcome message.\n`type` - `plain` or `embed`.".to_string()), + usage: Some("<option> <value>".to_string()), + example: Some("message Hey {user} ! Welcome to {guild} !".to_string()), + min_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let mut val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "enable" => { + guild_data.welcome = true; + }, + "disable" => { + guild_data.welcome = false; + }, + "channel" => { + match parse_channel(val.to_string(), guild_id) { + Some((channel_id, channel)) => { + guild_data.welcome_channel = channel_id.0 as i64; + val = format!("{} ({})", channel.name, channel_id.0); + }, + None => { + message.channel_id.say("I couldn't find that channel.")?; + return Ok(()) + }, + } + }, + "message" => { + guild_data.welcome_message = val.to_string(); + }, + "type" => { + guild_data.welcome_type = val.to_string(); + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `enable`, `disable`, `channel`, `message`, `type`. For more information see `help config welcome`")?; + return Ok(()) + }, + } + let guild = db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Welcome Configuration Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + if val.is_empty() { guild.welcome.to_string() } else { val }, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigGoodbye; +impl Command for ConfigGoodbye { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + // desc: Some("Change welcome message settings.\nOption is one of enable, disable, channel, message, type and the respective values should be none, none, channel_resolvable, desired message.\nType designates if the message is plain or embed. Anything other than embed will result in plain.".to_string()), + desc: Some("People leaving you ? At least make it cool and custom.\n\nOptions:\n`enable` - none.\n`disable` - none.\n`channel` - `channel_resolvable` (Mentionable).\n`message` - Your desired welcome message.\n`type` - `plain` or `embed`.".to_string()), + usage: Some("<option> <value>".to_string()), + example: Some("message {user} has left {guild}...".to_string()), + min_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let mut val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "enable" => { + guild_data.welcome = true; + }, + "disable" => { + guild_data.welcome = false; + }, + "channel" => { + match parse_channel(val.to_string(), guild_id) { + Some((channel_id, channel)) => { + guild_data.welcome_channel = channel_id.0 as i64; + val = format!("{} ({})", channel.name, channel_id.0); + }, + None => { + message.channel_id.say("I couldn't find that channel.")?; + return Ok(()) + }, + } + }, + "message" => { + guild_data.welcome_message = val.to_string(); + }, + "type" => { + guild_data.welcome_type = val.to_string(); + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `enable`, `disable`, `channel`, `message`, `type`. For more information see `help config goodbye`")?; + return Ok(()) + }, + } + let guild = db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Goodbye Configuration Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + if val.is_empty() { guild.welcome.to_string() } else { val }, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigIntroduction; +impl Command for ConfigIntroduction { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Change introduction message settings. This is exactly like welcome: `help config welcome` for more info. This is a premium only feature related to the Register command.".to_string()), + usage: Some("<option> <value>".to_string()), + example: Some("message Hey there {user}, mind introducting yourself?".to_string()), + min_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let mut val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "enable" => { + guild_data.introduction = true; + }, + "disable" => { + guild_data.introduction = false; + }, + "channel" => { + match parse_channel(val.to_string(), guild_id) { + Some((channel_id, channel)) => { + guild_data.introduction_channel = channel_id.0 as i64; + val = format!("{} ({})", channel.name, channel_id.0); + }, + None => { + message.channel_id.say("I couldn't find that channel.")?; + return Ok(()) + }, + } + }, + "message" => { + guild_data.introduction_message = val.to_string(); + }, + "type" => { + guild_data.introduction_type = val.to_string(); + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `enable`, `disable`, `channel`, `message`, `type`. For more information see `help config introduction`")?; + return Ok(()) + }, + } + let guild = db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Config Introduction Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + if val.is_empty() { guild.introduction.to_string() } else { val }, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigCommands; +impl Command for ConfigCommands { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Find something annoying ? Disable it ! (A command name must be provided.)".to_string()), + usage: Some("<enable|disable> <command_name>".to_string()), + example: Some("disable e621".to_string()), + min_args: Some(2), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "enable" => { + guild_data.commands.retain(|e| *e != val); + }, + "disable" => { + if !val.starts_with("conf") { + guild_data.commands.push(val.clone()); + } else { + message.channel_id.say("Config commands cannot be disabled.")?; + return Ok(()); + } + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `enable`, `disable`. For more information see `help config command`")?; + return Ok(()) + }, + } + db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Config Command Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + val, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct ConfigLogs; +impl Command for ConfigLogs { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Change which log messages are disabled. A log type must be provided.".to_string()), + usage: Some("<enable|disable|types> [type]".to_string()), + example: Some("disable message_edit".to_string()), + min_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let op = args.single::<String>().unwrap_or(String::new()); + let val = args.rest().to_string(); + match op.to_lowercase().as_str() { + "enable" => { + guild_data.logging.retain(|e| *e != val); + }, + "disable" => { + if LOG_TYPES.contains(&val.as_str()) { + guild_data.logging.push(val.clone()); + } else { + message.channel_id.say("Invalid log type. See `config log types` for valid types.")?; + return Ok(()); + } + }, + "types" => { + message.channel_id.say(LOG_TYPES.iter() + .map(|e| format!("`{}`", e)) + .collect::<Vec<String>>() + .join(", "))?; + return Ok(()); + }, + _ => { + message.channel_id.say("I didn't understand that option. Valid options are: `enable`, `disable`. For more information see `help config log`")?; + return Ok(()) + }, + } + db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Config Log Summary") + .colour(*colours::MAIN) + .description(format!("**Operation:** {}\n**Value:** {}", + op, + val, + )) + ))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} diff --git a/src/modules/commands/admins/ignore.rs b/src/modules/commands/admins/ignore.rs new file mode 100644 index 0000000..efafdac --- /dev/null +++ b/src/modules/commands/admins/ignore.rs @@ -0,0 +1,168 @@ +use crate::core::colours; +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct IgnoreAdd; +impl Command for IgnoreAdd { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Want me to ignore a channel ? Don't worry, I ain't seen a thing.".to_string()), + usage: Some("<channel_resolvable>".to_string()), + example: Some("#general".to_string()), + min_args: Some(1), + max_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + if let Some((channel_id, channel)) = parse_channel(args.full().to_string(), guild_id) { + if !guild_data.ignored_channels.contains(&(channel_id.0 as i64)) { + guild_data.ignored_channels.push(channel_id.0 as i64); + db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.say(format!( + "I will now ignore messages in {}", + channel.name + ))?; + } else { + message.channel_id.say("That channel is already being ignored.")?; + } + } else { + message.channel_id.say("I couldn't find that channel.")?; + } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct IgnoreRemove; +impl Command for IgnoreRemove { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Finally, I can see.".to_string()), + usage: Some("<channel_resolvable>".to_string()), + example: Some("#general".to_string()), + min_args: Some(1), + max_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + if let Some((channel_id, channel)) = parse_channel(args.full().to_string(), guild_id) { + if guild_data.ignored_channels.contains(&(channel_id.0 as i64)) { + guild_data.ignored_channels.retain(|e| *e != channel_id.0 as i64); + db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.say(format!( + "I will no longer ignore messages in {}", + channel.name + ))?; + } else { + message.channel_id.say("That channel isn't being ignored.")?; + } + } else { + message.channel_id.say("I couldn't find that channel.")?; + } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct IgnoreList; +impl Command for IgnoreList { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("You want ME to tell YOU want channels I'm ignoring ?.".to_string()), + required_permissions: Permissions::MANAGE_GUILD, + max_args: Some(0), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let guild_data = db.get_guild(guild_id.0 as i64)?; + if !guild_data.ignored_channels.is_empty() { + let channel_out = guild_data.ignored_channels.clone() + .iter() + .map(|c| format!("<#{}>", c)) + .collect::<Vec<String>>() + .join("\n"); + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Ignored Channels") + .description(channel_out) + .colour(*colours::MAIN) + ))?; + } else { + message.channel_id.say("I'm not ignoring any channels.")?; + } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct IgnoreLevel; +impl Command for IgnoreLevel { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Change which ranks can bypass ignored channels, epic.\n\nValues:\n`4` = Bot Owner, `3` = Guild Owner, `2` = Guild Admin, `1` = Guild Mod, `0` = Everyone.".to_string()), + usage: Some("<0..4>".to_string()), + example: Some("2".to_string()), + min_args: Some(1), + max_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + match args.single::<i16>() { + Ok(level) => { + guild_data.ignore_level = level; + db.update_guild(guild_id.0 as i64, guild_data)?; + message.channel_id.say(format!("Successfully set ignore level to {}", level))?; + }, + Err(_) => { + message.channel_id.say("Please enter an integer between 0 and 4.")?; + }, + } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} diff --git a/src/modules/commands/admins/management.rs b/src/modules/commands/admins/management.rs new file mode 100644 index 0000000..cad1365 --- /dev/null +++ b/src/modules/commands/admins/management.rs @@ -0,0 +1,317 @@ +use chrono::Utc; +use crate::core::colours; +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::builder::GetMessages; +use serenity::CACHE; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::{ + Message, + PermissionOverwrite, + PermissionOverwriteType +}; +use serenity::model::id::{ + ChannelId, + GuildId, + MessageId +}; +use serenity::model::Permissions; +use serenity::prelude::{ + Context, + Mentionable +}; +use std::sync::Arc; + +pub struct Prune; +impl Command for Prune { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + // desc: Some("Bulk delete messages. Filter is one of bot, attachment, !pin, mention, or a user_resolvable.\n`bot` will prune only messages from bots.\n`attachment` will prune only messages with attachments.\n`!pin` will prune all but pinned messages.\n`mention` will prune only messages that mention a user or everyone.\nMentioning a user will prune only that user's messages.".to_string()), + desc: Some("Someone causing havoc ?\n\nFilters:\n`bot` - Prune messages from bots.\n`attachment` - Prune message with attachments.\n`!pin` - Prune messages, excluding pinned messages.\n`mention` - Prune messages that mention a user(s).\n`user` - Prune messages that were sent by that user.".to_string()), + usage: Some("<count> [filter]".to_string()), + example: Some("20 bot".to_string()), + min_args: Some(1), + aliases: vec!["purge", "clear"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + message.delete()?; + if let Some(guild_id) = message.guild_id { + let count = args.single::<usize>().unwrap_or(0); + if count<=1000 { + let guild_data = db.get_guild(guild_id.0 as i64)?; + let fsel = args.single::<String>().unwrap_or(String::new()); + let mut filter = get_filter(fsel, guild_id); + let mut deletions = message.channel_id.messages(|_| re_retriever(u64::min(100, count as u64)))?; + let mut next_deletions; + let mut deleted_messages = Vec::new(); + while deleted_messages.len() < count { + next_deletions = message.channel_id + .messages(|_| be_retriever(deletions[deletions.len() - 1].id, u64::min(100, (count - deleted_messages.len()) as u64))) + .ok(); + deletions.retain(|m| filter(m) && is_deletable(m)); + match (deletions.len(), count - deleted_messages.len()) { + (n,_) if n <= 0 => { break; }, + (n,c) if n > c => { + deletions.truncate(c); + }, + _ => (), + } + match message.channel_id.delete_messages(&deletions) { + Ok(_) => { + deleted_messages.append(&mut deletions); + deletions = match next_deletions { + Some(s) => if s.is_empty() { break; } else { s }, + None => { break; } + } + }, + Err(why) => { + error!("PRUNE ERROR: {:?}", why); + break; + }, + } + } + if deleted_messages.len() > 0 { + if guild_data.modlog { + let channel = { + let cache = CACHE.read(); + cache.guild_channel(message.channel_id) + }; + ChannelId(guild_data.modlog_channel as u64).send_message(|m| m + .embed(|e| e + .title("Messages Pruned") + .description(format!( + "**Count:** {}\n**Moderator:** {} ({})\n**Channel:** {}" + ,deleted_messages.len() + ,message.author.mention() + ,message.author.tag() + ,match channel { + Some(ch) => { + let ch = ch.read(); + format!( + "{} ({})", + ch.mention(), + ch.name) + }, + None => message.channel_id.0.to_string(), + })) + .timestamp(now!()) + .colour(*colours::RED) + ))?; + } else { + message.channel_id.say(format!("Pruned {} message!", deleted_messages.len()))?; + } + if guild_data.audit { + deleted_messages.reverse(); + let prune_log = deleted_messages.iter() + .map(|m| format!( + "[{}] {} ({}): {}" + ,m.timestamp.with_timezone(&Utc).format("%F %T") + ,m.author.tag() + ,m.author.id.0 + ,m.content_safe() + )) + .collect::<Vec<String>>() + .join("\r\n"); + ChannelId(guild_data.audit_channel as u64).send_files(vec![ + (prune_log.as_bytes() + ,format!("prune-log-{}.txt", Utc::now().format("%FT%T")).as_str())] + ,|m| m)?; + } + } else { + message.channel_id.say("I wasn't able to delete any messages.")?; + } + } else { + message.channel_id.say("Please enter a number no greater than 1000.")?; + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct Clean; // Cleanup +impl Command for Clean { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + // desc: Some("Cleans up all commands and responses for Wisp sent in the past 10 minutes in the current channel.".to_string()), + desc: Some("Need to tidy up ? This'll delete all of Wisp's messages from the last ten minutes.".to_string()), + required_permissions: Permissions::MANAGE_GUILD, + max_args: Some(0), + aliases: vec!["cleanup"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let guild_data = db.get_guild(guild_id.0 as i64)?; + let user = CACHE.read().user.clone(); + let mut deletions = message.channel_id.messages(|_| re_retriever(100))?; + let mut next_deletions; + let mut num_del = 0; + message.delete()?; + loop { + deletions.retain(|m| + (Utc::now() - m.timestamp.with_timezone(&Utc)).num_seconds() <= 10*MIN as i64 + && (m.author.id == user.id + || m.content.starts_with(&guild_data.prefix) + || m.content.starts_with(&user.mention())) + ); + let len = deletions.len(); // mut + if len<=0 { break; } + next_deletions = message.channel_id.messages(|_| be_retriever(deletions[0].id, 100)).ok(); + match message.channel_id.delete_messages(deletions) { + Ok(_) => { + num_del += len; + deletions = match next_deletions { + Some(s) => s, + None => { break; }, + } + }, + Err(why) => { + error!("{:?}", why); + break; + }, + } + } + if num_del > 0 { + if guild_data.modlog { + let channel = { + let cache = CACHE.read(); + cache.guild_channel(message.channel_id) + }; + ChannelId(guild_data.modlog_channel as u64).send_message(|m| m + .embed(|e| e + .title("Messages Pruned") + .description(format!("**Count:** {}\n**Moderator:** {} ({})\n**Channel:** {}", + num_del, + message.author.mention(), + message.author.tag(), + match channel { + Some(ch) => { + let ch = ch.read(); + format!( + "{} ({})", + ch.mention(), + ch.name) + }, + None => message.channel_id.0.to_string(), + })) + .timestamp(now!()) + .colour(*colours::RED) + ))?; + } else { + message.channel_id.say(format!("Pruned {} message!", num_del))?; + } + } else { + message.channel_id.say("I wasn't able to delete any messages.")?; + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct SetupMute; +impl Command for SetupMute { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Sets up mute for the server. This command requires the Manage Channels and Manage Roles permissions. It creates the Muted role if it doesn't exist, then iterates through every channel and category to disable Send Messages, Speak, and Add Reactions. Add `bypass` as an arg to skip permission setting.".to_string()), + max_args: Some(1), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let guild = { + let cache = CACHE.read(); + cache.guild(guild_id) + }; + if let Some(guild_lock) = guild { + let guild = guild_lock.read().clone(); + let mut guild_data = db.get_guild(guild_id.0 as i64)?; + let bypass = args.single::<String>().unwrap_or("".to_string()); + let mute_role = match guild.roles.values().find(|e| e.name.to_lowercase() == "muted") { + Some(role) => role.clone(), + None => { + message.channel_id.say("Role `Muted` created")?; + guild.create_role(|r| r.name("Muted"))? + }, + }; + if bypass != "bypass" { + let allow = Permissions::empty(); + let deny = Permissions::SEND_MESSAGES | Permissions::ADD_REACTIONS | Permissions::SPEAK; + let overwrite = PermissionOverwrite { + allow, + deny, + kind: PermissionOverwriteType::Role(mute_role.id), + }; + for channel in guild.channels.values() { + let channel = channel.read(); // mut + channel.create_permission(&overwrite)?; + } + } + guild_data.mute_setup = true; + db.update_guild(guild.id.0 as i64, guild_data)?; + message.channel_id.say(format!("Setup permissions for {} channels.", guild.channels.len()))?; + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +// Helper functions for commands::prune +fn re_retriever(limit: u64) -> GetMessages { + GetMessages::default() + .limit(limit) +} + +fn be_retriever(id: MessageId, limit: u64) -> GetMessages { + GetMessages::default() + .before(id) + .limit(limit) +} + +fn is_deletable(message: &Message) -> bool { + let now = Utc::now() + .timestamp(); + let then = message.timestamp + .with_timezone(&Utc) + .timestamp(); + now - then < (WEEK as i64)*2 +} + +fn get_filter(input: String, guild_id: GuildId) -> Box<dyn FnMut(&Message) -> bool> { // + dyn + match input.as_str() { + "bot" => Box::new(|m| m.author.bot), + "mention" => Box::new(|m| !m.mentions.is_empty() || m.mention_everyone), + "attachment" => Box::new(|m| !m.attachments.is_empty()), + "!pin" => Box::new(|m| !m.pinned), + _ => { + match parse_user(input.to_string(), guild_id) { + Some((user_id, _)) => { + Box::new(move |m| m.author.id == user_id) + }, + None => { + Box::new(|_| true) + }, + } + }, + } +} diff --git a/src/modules/commands/admins/mod.rs b/src/modules/commands/admins/mod.rs new file mode 100644 index 0000000..441ecab --- /dev/null +++ b/src/modules/commands/admins/mod.rs @@ -0,0 +1,85 @@ +pub mod config; +pub mod ignore; +pub mod management; +// pub mod premium; +// pub mod roles; +pub mod tests; + +use self::config::*; +use self::ignore::*; +use self::management::*; +// use self::premium::*; +// use self::roles::*; +use self::tests::*; +use serenity::framework::standard::CreateGroup; + +pub fn init_config() -> CreateGroup { + CreateGroup::default() + .help_available(true) + .guild_only(true) + .prefixes(vec!["config", "cfg"]) + .default_cmd(ConfigList) + .cmd("admin", ConfigAdmin) + .cmd("audit", ConfigAudit) + .cmd("autorole", ConfigAutorole) + .cmd("cmd", ConfigCommands) + // .cmd("goodbye", ConfigGoodbye) + // .cmd("introduction", ConfigIntroduction) + .cmd("log", ConfigLogs) + .cmd("list", ConfigList) + .cmd("mod", ConfigMod) + .cmd("modlog", ConfigModlog) + .cmd("prefix", ConfigPrefix) + .cmd("raw", ConfigRaw) + .cmd("welcome", ConfigWelcome) +} + +pub fn init_ignore() -> CreateGroup { + CreateGroup::default() + .guild_only(true) + .help_available(true) + .prefix("ignore") + .default_cmd(IgnoreList) + .cmd("add", IgnoreAdd) + .cmd("remove", IgnoreRemove) + .cmd("list", IgnoreList) + .cmd("level", IgnoreLevel) +} + +pub fn init_management() -> CreateGroup { + CreateGroup::default() + .guild_only(true) + .help_available(true) + .cmd("clean", Clean) + .cmd("prune", Prune) + // .cmd("setup", SetupMute) +} + +/* pub fn init_premium() -> CreateGroup { + CreateGroup::default() + .guild_only(true) + .help_available(true) + .prefixes(vec!["p", "premium", "prem"]) + .cmd("register_member", PRegisterMember) + .cmd("register_cooldown", PRegisterCooldown) + .cmd("register_duration", PRegisterDuration) + .cmd("register_roles", PRegisterRestrictions) +} */ + +/* pub fn init_roles() -> CreateGroup { + CreateGroup::default() + .help_available(true) + .guild_only(true) + .cmd("csr", CreateSelfRole) + .cmd("dsr", DeleteSelfRole) + .cmd("esr", EditSelfRole) +} */ + +pub fn init_tests() -> CreateGroup { + CreateGroup::default() + .guild_only(true) + .help_available(true) + .prefix("test") + .cmd("welcome", TestWelcome) + // .cmd("intro", TestIntro) +} diff --git a/src/modules/commands/admins/premium.rs b/src/modules/commands/admins/premium.rs new file mode 100644 index 0000000..b6859bd --- /dev/null +++ b/src/modules/commands/admins/premium.rs @@ -0,0 +1,165 @@ +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct PRegisterMember; +impl Command for PRegisterMember { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Set the member role used by register. This role is automatically either after cooldown, if cooldown is set, or right away.".to_string()), + usage: Some("<role_resolvable>".to_string()), + example: Some("member".to_string()), + aliases: vec!["reg_m", "reg_member"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut settings = db.get_premium(guild_id.0 as i64)?; + if let Some((role_id, role)) = parse_role(args.full().to_string(), guild_id) { + settings.register_member_role = Some(role_id.0 as i64); + db.update_premium(guild_id.0 as i64, settings)?; + message.channel_id.say(format!("Set member role to {}", role.name))?; + } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct PRegisterCooldown; +impl Command for PRegisterCooldown { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Set the cooldown role used by register. This is applied automatically before member and removed after cooldown_duration".to_string()), + usage: Some("<role_resolvable>".to_string()), + example: Some("cooldown".to_string()), + aliases: vec!["reg_c", "reg_cooldown"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut settings = db.get_premium(guild_id.0 as i64)?; + if let Some((role_id, role)) = parse_role(args.full().to_string(), guild_id) { + settings.register_cooldown_role = Some(role_id.0 as i64); + db.update_premium(guild_id.0 as i64, settings)?; + message.channel_id.say(format!("Set cooldown role to {}", role.name))?; + } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct PRegisterDuration; +impl Command for PRegisterDuration { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Set the duration cooldown is applied for. Default value is 24 hours.".to_string()), + usage: Some("<time_resolvable>".to_string()), + example: Some("24h".to_string()), + aliases: vec!["reg_dur", "reg_duration"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut settings = db.get_premium(guild_id.0 as i64)?; + if let Ok(dur) = args.full().parse::<String>() { + let dur = hrtime_to_seconds(dur); + settings.register_cooldown_duration = Some(dur as i32); + db.update_premium(guild_id.0 as i64, settings)?; + message.channel_id.say(format!("Set duration of cooldown to {}", seconds_to_hrtime(dur as usize)))?; + } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct PRegisterRestrictions; +impl Command for PRegisterRestrictions { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Manage the roles people on cooldown cannot self-assign. These are also ignored in register command usage. Valid options: `add`, `remove`, `set`".to_string()), + usage: Some("<option> [values]".to_string()), + example: Some("set selfies, nsfw".to_string()), + aliases: vec!["reg_roles", "reg_restrict"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let op = args.single::<String>().unwrap_or(String::new()); + let mut sec = ""; + let mut val = String::new(); + let mut settings = db.get_premium(guild_id.0 as i64)?; + match op.as_str() { + "add" => { + if let Some((role_id, role)) = parse_role(args.rest().to_string(), guild_id) { + settings.cooldown_restricted_roles.push(role_id.0 as i64); + sec = "Added"; + val = role.name; + } + }, + "remove" => { + if let Some((role_id, role)) = parse_role(args.rest().to_string(), guild_id) { + settings.cooldown_restricted_roles.push(role_id.0 as i64); + sec = "Removed"; + val = role.name; + } + }, + "set" => { + let list = args.rest().split(",").map(|s| s.trim().to_string()); + let mut roles = Vec::new(); + let mut role_names = Vec::new(); + for role in list { + if let Some((role_id, role)) = parse_role(role, guild_id) { + roles.push(role_id.0 as i64); + role_names.push(role.name); + } + } + settings.cooldown_restricted_roles = roles; + sec = "Set to"; + val = role_names.join(", "); + }, + _ => { message.channel_id.say("I didn't understand that option. Valid options are: `add`, `remove`, `set`. For more information see `help p reg_roles`")?; }, + } + db.update_premium(guild_id.0 as i64, settings)?; + message.channel_id.say(format!("Successfully modified restricted roles. {} {}", sec, val))?; + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} diff --git a/src/modules/commands/admins/roles.rs b/src/modules/commands/admins/roles.rs new file mode 100644 index 0000000..bbe2195 --- /dev/null +++ b/src/modules/commands/admins/roles.rs @@ -0,0 +1,160 @@ +se crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct CreateSelfRole; +impl Command for CreateSelfRole { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Create a self role from a discord role. Also optionally takes a category and/or aliases.".to_string()), + usage: Some("<role_resolvable> [/c category] [/a aliases as CSV]".to_string()), + example: Some("NSFW /c Opt-in /a porn, lewd".to_string()), + aliases: vec!["createselfrole".to_string()], + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let switches = get_switches(args + .full() + .to_string()); + let backup = String::new(); + let rest = switches + .get("rest") + .unwrap_or(&backup); + if let Some((role_id, role)) = parse_role(rest.clone(), guild_id) { + let category = switches + .get("c") + .cloned(); + let aliases: Option<Vec<String>> = switches + .get("a") + .map(|s| s + .split(",") + .map(|c| c + .trim() + .to_string() + .to_lowercase()) + .collect()); + let data = db.new_role( + role_id.0 as i64, + guild_id.0 as i64, + category, + aliases)?; + message.channel_id.say(format!( + "Successfully added role {} to category {} {}" + ,role.name + ,data.category + ,if !data.aliases.is_empty() { + format!("with aliases {}", data.aliases.join(",")) + } else { + String::new() + } + ))?; + } else { message.channel_id.say("I couldn't find that role.")?; } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct DeleteSelfRole; +impl Command for DeleteSelfRole { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Delete a self role.".to_string()), + usage: Some("<role_resolvable>".to_string()), + example: Some("NSFW".to_string()), + aliases: vec!["deleteselfrole".to_string()], + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Some((role_id, role)) = parse_role(args.full().to_string(), guild_id) { + db.del_role(role_id.0 as i64, guild_id.0 as i64)?; + message.channel_id.say(format!("Successfully deleted role {}", role.name))?; + } else { message.channel_id.say("I couldn't find that role.")?; } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} + +pub struct EditSelfRole; +impl Command for EditSelfRole { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Edit a self role. Optionally takes a category and/or aliases. This operation is lazy and won't change anything you don't specify. Replace switch tells the bot to override aliases instead of append.".to_string()), + usage: Some("<role_resolvable> [/c category] [/a aliases as CSV] [/replace]".to_string()), + example: Some("NSFW /c Opt-in /a porn, lewd /replace".to_string()), + aliases: vec!["editselfrole".to_string()], + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let switches = get_switches(args.full().to_string()); + let backup = String::new(); + let rest = switches.get("rest").unwrap_or(&backup); + if let Some((role_id, d_role)) = parse_role(rest.clone(), guild_id) { + let category = switches + .get("c") + .cloned(); + let aliases: Option<Vec<String>> = switches + .get("a") + .map(|s| s + .split(",") + .map(|c| c + .trim() + .to_string() + .to_lowercase()) + .collect()); + let mut role = db.get_role(role_id.0 as i64, guild_id.0 as i64)?; + if let Some(s) = category { role.category = s; } + if let Some(mut a) = aliases { + match switches.get("replace") { + Some(_) => { role.aliases = a; }, + None => { role.aliases.append(&mut a); }, + } + } + let data = db.update_role(role_id.0 as i64, guild_id.0 as i64, role)?; + message.channel_id.say(format!("Successfully update role {} in category {} {}", + d_role.name, + data.category, + if !data.aliases.is_empty() { + format!("with aliases {}", data.aliases.join(",")) + } else { + String::new() + } + ))?; + } else { message.channel_id.say("I couldn't find that role.")?; } + } else { + failed!(GUILDID_FAIL); + } + Ok(()) + } +} diff --git a/src/modules/commands/admins/tests.rs b/src/modules/commands/admins/tests.rs new file mode 100644 index 0000000..d4c8692 --- /dev/null +++ b/src/modules/commands/admins/tests.rs @@ -0,0 +1,77 @@ +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::id::ChannelId; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct TestWelcome; +impl Command for TestWelcome { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Feeling superstitious ? I'll check if your welcome configuration is correct.".to_string()), + max_args: Some(0), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Some(member) = message.member() { + let guild_data = db.get_guild(guild_id.0 as i64)?; + if guild_data.welcome { + let channel = ChannelId(guild_data.welcome_channel as u64); + if guild_data.welcome_type.as_str() == "embed" { + send_welcome_embed(guild_data.welcome_message, &member, channel)?; + } else { + channel.say(parse_welcome_items(guild_data.welcome_message, &member))?; + } + } + } else { failed!(MEMBER_FAIL); } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct TestIntro; +impl Command for TestIntro { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Generates an introduction message to test your current setup.".to_string()), + aliases: vec!["introduction"].iter().map(|e| e.to_string()).collect(), + max_args: Some(0), + required_permissions: Permissions::MANAGE_GUILD, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Some(member) = message.member() { + let guild_data = db.get_guild(guild_id.0 as i64)?; + if guild_data.welcome { + let channel = ChannelId(guild_data.introduction_channel as u64); + if guild_data.introduction_type.as_str() == "embed" { + send_welcome_embed(guild_data.introduction_message, &member, channel)?; + } else { + channel.say(parse_welcome_items(guild_data.introduction_message, &member))?; + } + } + } else { failed!(MEMBER_FAIL); } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} diff --git a/src/modules/commands/general/animals.rs b/src/modules/commands/general/animals.rs new file mode 100644 index 0000000..42817ab --- /dev/null +++ b/src/modules/commands/general/animals.rs @@ -0,0 +1,183 @@ +// use crate::core::colours; +use crate::core::consts::*; +use crate::core::model::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::prelude::Context; +use std::sync::Arc; + +// pub struct Bunny; +// impl Command for Bunny { +// fn options(&self) -> Arc<CommandOptions> { +// let default = CommandOptions::default(); +// let options = CommandOptions { +// desc: Some("One bouncy animal for you.".to_string()), +// ..default +// }; +// Arc::new(options) +// } + +// fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { +// let data = ctx.data.lock(); +// if let Some(api) = data.get::<ApiClient>() { +// let res = api.bunny()?; +// message.channel_id.send_message(|m| m +// .embed(|e| e +// .image(&res.media[0]) +// ))?; +// } else { failed!(API_FAIL); } + +// Ok(()) +// } +// } + +pub struct Cat; +impl Command for Cat { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Enjoy a random cat.".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(api) = data.get::<ApiClient>() { + let res = api.cat()?; + message.channel_id.send_message(|m| m + .embed(|e| e + .image(res.file) + ))?; + } else { failed!(API_FAIL); } + + Ok(()) + } +} + +pub struct Dog; +impl Command for Dog { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Cats aren't enough for you ? Have a dog !".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(api) = data.get::<ApiClient>() { + let res = api.dog()?; + message.channel_id.send_message(|m| m + .embed(|e| e + .image(res.message) + ))?; + } else { failed!(API_FAIL); } + + Ok(()) + } +} + +pub struct Duck; +impl Command for Duck { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Have a duck, pog.".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(api) = data.get::<ApiClient>() { + let res = api.duck()?; + message.channel_id.send_message(|m| m + .embed(|e| e + .image(res.url) + ))?; + } else { failed!(API_FAIL); } + + Ok(()) + } +} + +pub struct Fox; +impl Command for Fox { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("A fox, thats it.".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(api) = data.get::<ApiClient>() { + let res = api.fox()?; + message.channel_id.send_message(|m| m + .embed(|e| e + .image(res.image) + ))?; + } else { failed!(API_FAIL); } + + Ok(()) + } +} + +pub struct Owl; +impl Command for Owl { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Why an owl ?".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(api) = data.get::<ApiClient>() { + let res = api.owl()?; + message.channel_id.send_message(|m| m + .embed(|e| e + .image(res.image) + ))?; + } else { failed!(API_FAIL); } + + Ok(()) + } +} + +pub struct UglyCat; +impl Command for UglyCat { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("UGLY CAT !".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + message.channel_id.send_message(|m| m + .embed(|e| e + .image("https://i.pinimg.com/originals/4d/19/0f/4d190f1307b35e7155bb4b898e19d545.jpg") + ))?; + + Ok(()) + } +} diff --git a/src/modules/commands/general/fun.rs b/src/modules/commands/general/fun.rs new file mode 100644 index 0000000..79f37c9 --- /dev/null +++ b/src/modules/commands/general/fun.rs @@ -0,0 +1,301 @@ +use crate::core::model::ApiClient; +use crate::core::colours; +use crate::core::consts::*; +use rand::prelude::*; +use regex::Regex; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::prelude::Context; +use std::sync::Arc; + +lazy_static! { + static ref DICE_MATCH: Regex = Regex::new(r"(?P<count>\d+)d?(?P<sides>\d*)").expect("Failed to create Regex"); +} + +pub struct Clapify; +impl Command for Clapify { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Clap.. Clap... Clap....".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + let to_say = args; + let clapped = to_say.replace(" ", "🙏"); + message.channel_id.say(clapped)?; + + Ok(()) + } +} + +pub struct CoinFlip; +impl Command for CoinFlip { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("A simple game, one in the chamber, who gets splattered ?".to_string()), + aliases: vec!["flipcoin"].iter().map(|e| e.to_string()).collect(), + // usage: Some("[tags]".to_string()), + // example: Some("minecraft".to_string()), + // aliases: vec!["furry"].iter().map(|e| e.to_string()).collect(), + // owner_privileges: false, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, _args: Args) -> Result<(), CommandError> { + let random = thread_rng().gen_bool(5.4); + // TODO: Make this an embed eventually. + if random { + message.channel_id.say("Looks like a heads to me !")?; + } else { + message.channel_id.say("Tails seems to be the winner.")?; + } + + Ok(()) + } +} + +// TODO: eval expressions such as "2d10 + 5" +pub struct Dice; +impl Command for Dice { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Toss em up. (Defaults to 6-sided.)".to_string()), + usage: Some("<Nd>[X]".to_string()), + example: Some("2d10".to_string()), + aliases: vec!["roll"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + let expr = args.single::<String>().unwrap_or(String::new()); + if let Some(caps) = DICE_MATCH.captures(expr.as_str()) { + let count: u32 = caps["count"].parse().unwrap_or(1); + let sides: u32 = caps["sides"].parse().unwrap_or(6); + if count > 0 && count <= 1000 { + if sides > 0 && sides <= 100 { + let mut total = 0; + for _ in 1..&count+1 { + let r = thread_rng().gen_range(1,&sides+1); + total += r; + } + message.channel_id.send_message(|m| m + .embed(|e| e + .colour(*colours::MAIN) + .field(format!("{} 🎲 [1-{}]", count, sides), format!("You rolled {}", total), true) + ))?; + } else { message.channel_id.say("Sides out of bounds. Max: 100")?; } + } else { message.channel_id.say("Count out of bounds. Max: 1000")?; } + } else { message.channel_id.say("Sorry, I didn't understand your input.")?; } + Ok(()) + } +} + +pub struct EightBall; +impl Command for EightBall { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("You have a lot of trust seen as you've put your faith in me.".to_string()), + aliases: vec!["8b"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _args: Args) -> Result<(), CommandError> { + let random_side = thread_rng().gen_range(1, 20); + + let responses = vec![ + "As I see it, yes.", + "Ask again later.", + "Better not tell you now.", + "Cannot predict now.", + "Concentrate and ask again.", + "Don't count on it.", + "It is certain.", + "It is decidedly so.", + "Most likely.", + "My reply is no.", + "My sources say no.", + "Outlook not so good.", + "Outlook good.", + "Reply hazy, try again.", + "Signs point to yes.", + "Very doubtful.", + "Without a doubt.", + "Yes.", + "Yes - definetely.", + "You may rely on it." + ]; + + message.channel_id.say(responses[random_side])?; + + Ok(()) + } +} + +pub struct PayRespects; +impl Command for PayRespects { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Press F to Pay Respects".to_string()), + // usage: Some("[tags]".to_string()), + // example: Some("minecraft".to_string()), + aliases: vec!["payrespects"].iter().map(|e| e.to_string()).collect(), + // owner_privileges: false, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, _args: Args) -> Result<(), CommandError> { + message.channel_id.say("Press F to Pay Respects")?.react("🇫")?; + + Ok(()) + } +} + +pub struct Opinion; +impl Command for Opinion { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Yeah I'll give you my opinion, just don't take me seriously.".to_string()), + usage: Some("[something you want my opinion on]".to_string()), + example: Some("onions".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + let random = thread_rng().gen_bool(5.4); + // TODO: Make this an embed eventually. + if random { + message.channel_id.say(format!( + "{:?} ? Yeah, I'll give it a thumbs up.", args.to_string()))?; + } else { + message.channel_id.say(format!( + "{:?} ? That's a thumbs down from me.", args.to_string()))?; + } + + Ok(()) + } +} + +pub struct Rate; +impl Command for Rate { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("I'll rate it, don't take it seriously though.".to_string()), + usage: Some("[something you want my rating on]".to_string()), + example: Some("salads".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + let random = thread_rng().gen_range(1, 10); + // TODO: Make this an embed eventually. + message.channel_id.say(format!( + "I'll give {:?} a {} out of 10.", args.to_string(), random))?; + + Ok(()) + } +} + +pub struct RussianRoulette; +impl Command for RussianRoulette { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("A simple game, one in the chamber, who gets splattered ?".to_string()), + // usage: Some("[tags]".to_string()), + // example: Some("minecraft".to_string()), + // aliases: vec!["furry"].iter().map(|e| e.to_string()).collect(), + // owner_privileges: false, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, _args: Args) -> Result<(), CommandError> { + let random = thread_rng().gen_range(1, 6); + if random == 4 { + message.channel_id.say("Boom ! Better luck next time...")?; + } else { + message.channel_id.say("Click ! You survived, for now...")?; + } + + Ok(()) + } +} + +pub struct Uwufy; +impl Command for Uwufy { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Can't get enough of that \"uwu\" goodness ?".to_string()), + aliases: vec!["owofy"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + let to_say = args; + + let r_re = Regex::new(r"/(?:l|r)/g").unwrap(); + let l_re = Regex::new(r"/(?:L|R)/g").unwrap(); + let face_re = Regex::new(r"/!+/g").unwrap(); + + let replaced1 = r_re.replace_all(&to_say, "w"); + let replaced2 = l_re.replace_all(&replaced1, "W"); + let uwufied = face_re.replace_all(&replaced2, " >w< "); + + message.channel_id.say(uwufied)?; + + Ok(()) + } +} + +pub struct YoMomma; +impl Command for YoMomma { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Yo momma so...".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(api) = data.get::<ApiClient>() { + let res = api.yo_momma()?; + message.channel_id.say(res.joke)?; + } else { failed!(API_FAIL); } + + Ok(()) + } +} diff --git a/src/modules/commands/general/misc.rs b/src/modules/commands/general/misc.rs new file mode 100644 index 0000000..f342cd0 --- /dev/null +++ b/src/modules/commands/general/misc.rs @@ -0,0 +1,320 @@ +// 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 forecast::Icon::*; +// use forecast::Units; +// use rand::prelude::*; +// use serenity::CACHE; +// use serenity::client::bridge::gateway::ShardId; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::{ + channel::Message, + // guild::Role, +}; +use serenity::prelude::{ + Context, + // Mentionable +}; +// use std::f64::NAN; +use std::sync::Arc; +// use sys_info; +// use sysinfo::{ +// ProcessExt, +// SystemExt, +// System, +// get_current_pid +// }; + +pub struct Anime; +impl Command for Anime { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Want to share an anime ? Results provided by kitsu.io.".to_string()), + usage: Some("<anime title>".to_string()), + example: Some("darling in the franxx".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + use kitsu::model::Status::*; + let data = ctx.data.lock(); + message.channel_id.broadcast_typing()?; + if let Some(api) = data.get::<ApiClient>() { + let res = api.anime(args.full())?; + if let Some(anime) = res.data.first() { + let status = match anime.attributes.status { + Some(stat) => { match stat { + Current => "Current", + Finished => "Complete", + TBA => "To Be Announced", + Unreleased => "Unreleased", + Upcoming => "Upcoming", + }}, + None => "Status Not Found", + }; + let cover_url = match anime.attributes.cover_image.clone() { + Some(cover) => { match cover.original { + Some(url) => url, + None => String::new(), + }}, + None => String::new(), + }; + message.channel_id.send_message(|m| m + .embed(|e| e + .title(anime.attributes.canonical_title.clone()) + .url(anime.url()) + .description(format!("{}\n\n{}\n**Score:** {}\n**Status:** {}", + anime.attributes.synopsis, + if let Some(count) = anime.attributes.episode_count { + let mut out = format!("**Episodes:** {}", count); + if let Some(length) = anime.attributes.episode_length { + out.push_str(format!(" ({} min/ep)", length).as_str()); + } + out + } else { String::from("Episode Information Not Found") }, + anime.attributes.average_rating.clone().unwrap_or(String::from("Not Found")), + status + )) + .thumbnail(cover_url) + .colour(*colours::MAIN) + ))?; + } + } else { failed!(API_FAIL); } + Ok(()) + } +} + +pub struct DadJoke; +impl Command for DadJoke { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Who would voluntarily want a dad joke...".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(api) = data.get::<ApiClient>() { + let res = api.joke()?; + message.channel_id.say(res)?; + } else { failed!(API_FAIL); } + Ok(()) + } +} + +pub struct Douse; +impl Command for Douse { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + message.channel_id.send_message(|m| m + .embed(|e| e + .colour(*colours::MAIN) + .image("https://i.pinimg.com/originals/6a/c8/26/6ac826e3d0cbd64eb4f42c12a73fcdb8.gif") + ))?; + + Ok(()) + } +} + +pub struct Manga; +impl Command for Manga { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Need info about a manga ? Results provided by kitsu.io".to_string()), + usage: Some("<anime title>".to_string()), + example: Some("deathnote".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + use kitsu::model::Status::*; + let data = ctx.data.lock(); + message.channel_id.broadcast_typing()?; + if let Some(api) = data.get::<ApiClient>() { + let res = api.manga(args.full())?; + if let Some(manga) = res.data.first() { + let status = match manga.attributes.status { + Some(stat) => { match stat { + Current => "Current", + Finished => "Complete", + TBA => "To Be Announced", + Unreleased => "Unreleased", + Upcoming => "Upcoming", + }}, + None => "Status Not Found", + }; + let cover_url = match manga.attributes.cover_image.clone() { + Some(cover) => { match cover.original { + Some(url) => url, + None => String::new(), + }}, + None => String::new(), + }; + message.channel_id.send_message(|m| m + .embed(|e| e + .title(manga.attributes.canonical_title.clone()) + .url(manga.url()) + .description(format!("{}\n\n**Volumes:** {}\n**Chapters:** {}\n**Score:** {}\n**Status:** {}", + manga.attributes.synopsis, + manga.attributes.volume_count.map_or(String::from("Not Found"), |count| count.to_string()), + manga.attributes.chapter_count.map_or(String::from("Not Found"), |count| count.to_string()), + manga.attributes.average_rating.clone().unwrap_or(String::from("Not Found")), + status + )) + .thumbnail(cover_url) + .colour(*colours::MAIN) + ))?; + } + } else { failed!(API_FAIL); } + Ok(()) + } +} + +pub struct UrbanDictionary; // Urban +impl Command for UrbanDictionary { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Hmm, be responsible.".to_string()), + usage: Some(r#"<"term"> [count]"#.to_string()), + example: Some(r#""boku no pico" 5"#.to_string()), + aliases: vec!["urban", "ud", "urbandict"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + let api = { + let data = ctx.data.lock(); + data.get::<ApiClient>().cloned() + }; + if let Some(api) = api { + let term = args.single_quoted::<String>().unwrap_or(String::new()); + let res = api.urban(term.as_str())?; + if !res.definitions.is_empty() { + let count = args.single::<u32>().unwrap_or(1); + let mut tags: Vec<String> = Vec::new(); + if let Some(res_tags) = &res.tags { + tags = res_tags.clone(); + tags.sort(); + tags.dedup(); + } + if count == 1 { + let item = &res.definitions[0]; + let tags_list = { + let list = tags.iter().map(|t| "#".to_string()+t).collect::<Vec<String>>().join(", "); + if !list.is_empty() { + list + } else { + "None".to_string() + } + }; + let definition = { + let mut i = item.definition.clone(); + if i.len() > 1000 { + i.truncate(997); + i += "..."; + } + i + }; + message.channel_id.send_message(|m| m + .embed(|e| e + .colour(*colours::MAIN) + .field(format!(r#"Definition of "{}" by {}"#, item.word, item.author), &item.permalink, false) + .field("Thumbs Up", &item.thumbs_up, true) + .field("Thumbs Down", &item.thumbs_down, true) + .field("Definition", definition, false) + .field("Example", &item.example, false) + .field("Tags", tags_list, false) + ))?; + } else { + let mut list = res.definitions; + list.truncate(count as usize); + let list = list.iter() + .map(|c| format!(r#""{}" by {}: {}"#, c.word, c.author, c.permalink)) + .collect::<Vec<String>>() + .join("\n"); + message.channel_id.send_message(|m| m + .embed(|e| e + .title(format!("Top {} results for {}", count, term)) + .description(list) + .colour(*colours::MAIN) + ))?; + } + } + } else { failed!(API_FAIL); } + + Ok(()) + } +} + +// pub struct Waifu; +// impl Command for Waifu { +// fn options(&self) -> Arc<CommandOptions> { +// let default = CommandOptions::default(); +// let options = CommandOptions { +// desc: Some("I'll give you random waifu and backstory generated by a neural network, so don't get too attached.".to_string()), +// ..default +// }; +// Arc::new(options) +// } + +// fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { +// let data = ctx.data.lock(); +// if let Some(api) = data.get::<ApiClient>() { +// let res = api.waifu_backstory()?; +// message.channel_id.send_message(|m| m +// .embed(|e| e +// // .image(res.image) +// .description(res) +// ))?; +// } else { failed!(API_FAIL); } + +// Ok(()) +// } +// } + +// pub struct DateFact; +// impl Command for DateFact { +// fn options(&self) -> Arc<CommandOptions> { +// let default = CommandOptions::default(); +// let options = CommandOptions { +// desc: Some("A fun fact about a fun date.".to_string()), +// ..default +// }; +// Arc::new(options) +// } + +// fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { +// let to_say = args; + +// message.channel_id.say(uwufied)?; + +// Ok(()) +// } +// } diff --git a/src/modules/commands/general/mod.rs b/src/modules/commands/general/mod.rs new file mode 100644 index 0000000..acff760 --- /dev/null +++ b/src/modules/commands/general/mod.rs @@ -0,0 +1,122 @@ +pub mod misc; +pub mod nsfw; +// pub mod roles; +// pub mod tags; +pub mod fun; +pub mod utilities; +pub mod animals; + +use self::misc::*; +use self::nsfw::*; +// use self::roles::*; +// use self::tags::*; +use self::fun::*; +use self::utilities::*; +use self::animals::*; +use serenity::framework::standard::CreateGroup; + +pub fn init_animals() -> CreateGroup { + CreateGroup::default() + .help_available(true) + // .cmd("bunny", Bunny) // TODO: Add bunny + .cmd("cat", Cat) + .cmd("dog", Dog) + .cmd("duck", Duck) + .cmd("fox", Fox) + .cmd("owl", Owl) + .cmd("uglycat", UglyCat) +} + +pub fn init_anime() -> CreateGroup { + CreateGroup::default() + .help_available(true) + .cmd("anime", Anime) + .cmd("manga", Manga) + // .cmd("waifu", Waifu) +} + +pub fn init_fun() -> CreateGroup { + CreateGroup::default() + .help_available(true) + .cmd("clapify", Clapify) + .cmd("joke", DadJoke) + // .cmd("opinion", Opinion) + .cmd("rate", Rate) + .cmd("urbandictionary", UrbanDictionary) + .cmd("f", PayRespects) + .cmd("8ball", EightBall) + .cmd("uwufy", Uwufy) + .cmd("yomomma", YoMomma) +} + +/* pub fn init_misc() -> CreateGroup { + CreateGroup::default() + .help_available(true) +} */ + +pub fn init_minigames() -> CreateGroup { + CreateGroup::default() + .help_available(true) + // .cmd("coinflip", CoinFlip) + .cmd("dice", Dice) + .cmd("russianroulette", RussianRoulette) +} + +pub fn init_nsfw() -> CreateGroup { + CreateGroup::default() + .help_available(true) + .check(|_,message,_,_| { + if let Ok(channel) = message.channel_id.to_channel() { + if channel.is_nsfw() { + true + } else { + check_error!(message.channel_id.say("Woah there, you can only use this command in NSFW marked channels !")); + false + } + } else { + check_error!(message.channel_id.say("Weird, I can't get the channel information, I can't tell if it's NSFW.")); + false + }}) + .cmd("e621", Furry) +} + +/* pub fn init_roles() -> CreateGroup { + CreateGroup::default() + .help_available(true) + .guild_only(true) + .cmd("role", AddSelfRole) + .cmd("derole", RemoveSelfRole) + .cmd("roles", ListSelfRoles) +} + +pub fn init_tags() -> CreateGroup { + CreateGroup::default() + .help_available(true) + .guild_only(true) + .prefix("tag") + .default_cmd(TagSingle) + .cmd("show", TagSingle) + .cmd("add", TagAdd) + .cmd("del", TagRemove) + .cmd("edit", TagEdit) + .cmd("list", TagList) +} */ + +pub fn init_utilities() -> CreateGroup { + CreateGroup::default() + .help_available(true) + .cmd("id", UserId) + .cmd("membercount", MemberCount) + .cmd("pfp", PFP) + .cmd("ping", Ping) + .cmd("prefix", Prefix) + .cmd("remind", Reminder) + .cmd("role", RoleInfo) + .cmd("server", ServerInfo) + // .cmd("stats", Stats) + // .cmd("tags", TagList) + .cmd("time", Time) + .cmd("user", UserInfo) + // .cmd("weather", Weather) + .cmd("wisp", Wisp) +} diff --git a/src/modules/commands/general/nsfw.rs b/src/modules/commands/general/nsfw.rs new file mode 100644 index 0000000..1a699d0 --- /dev/null +++ b/src/modules/commands/general/nsfw.rs @@ -0,0 +1,50 @@ +use crate::core::model::ApiClient; +use crate::core::consts::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct Furry; +impl Command for Furry { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("I see your a individual of culture...".to_string()), + usage: Some("[tags]".to_string()), + example: Some("minecraft".to_string()), + aliases: vec!["furry"].iter().map(|e| e.to_string()).collect(), + owner_privileges: false, + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + message.channel_id.broadcast_typing()?; + if let Some(api) = data.get::<ApiClient>() { + let res = api.furry(args.full(), 1)?; + let post = &res[0]; + message.channel_id.send_message(|m| m + .embed(|e| e + .image(&post.file_url) + .description(format!("**Tags:** {}\n**Post:** [{}]({})\n**Artist:** {}\n**Score::** {}", + &post.tags.replace("_", "\\_"), + &post.id, + format!("https://e621.net/post/show/{}", &post.id), + &post.artist[0], + &post.score + ) + ) + ))?; + } else { failed!(API_FAIL); } + + Ok(()) + } +} diff --git a/src/modules/commands/general/roles.rs b/src/modules/commands/general/roles.rs new file mode 100644 index 0000000..b6cd240 --- /dev/null +++ b/src/modules/commands/general/roles.rs @@ -0,0 +1,278 @@ +use crate::core::colours; +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use fuzzy_match::fuzzy_match; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::guild::Role; +use serenity::model::id::RoleId; +use serenity::prelude::Context; +use std::collections::BTreeMap; +use std::sync::Arc; + +pub struct AddSelfRole; +impl Command for AddSelfRole { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Add roles to yourself provided they are on the self role list.".to_string()), + usage: Some("<role_resolvables as CSV>".to_string()), + example: Some("red, green".to_string()), + aliases: vec!["addselfrole", "asr"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Some(mut member) = message.member() { + let roles = db.get_roles(guild_id.0 as i64)?; + let (restricted_roles, has_cooldown) = match db.get_premium(guild_id.0 as i64) { + Ok(data) => { + let has_cooldown = if let Some(cooldown_role) = data.register_cooldown_role { + member.roles.contains(&RoleId(cooldown_role as u64)) + } else { + false + }; + (data.cooldown_restricted_roles, has_cooldown) + }, + Err(_) => { + (Vec::new(), false) + }, + }; + if !roles.is_empty() { + let list = args.rest().split(",").map(|s| s.trim().to_string()); + let mut to_add = Vec::new(); + let mut failed = Vec::new(); + let role_names = roles.iter().filter_map(|r| match RoleId(r.id as u64).to_role_cached() { + Some(role) => Some(role.clone()), + None => None, + }).collect::<Vec<Role>>(); + for r1 in list { + if let Some((r, r2)) = parse_role(r1.clone(), guild_id) { + if has_cooldown && restricted_roles.contains(&(r.0 as i64)) { + failed.push(format!("{} is not available on cooldown", r2.name)); + continue; + } + if let Some(_) = roles.iter().find(|e| e.id == r.0 as i64) { + to_add.push(r); + } else { failed.push(format!("{} is a role, but it isn't self-assignable", r2.name)); } + } else if let Some(i) = roles.iter().position(|r| r.aliases.contains(&r1.to_lowercase())) { + if has_cooldown && restricted_roles.contains(&(roles[i].id)) { + failed.push(format!("{} is not available on cooldown", match RoleId(roles[i].id as u64).to_role_cached() { + Some(role) => role.name, + None => roles[i].id.to_string(), + })); + continue; + } + to_add.push(RoleId(roles[i].id as u64)); + } else { + failed.push(format!("Failed to find match \"{}\". {}", r1, + if let Some(i) = fuzzy_match(&r1, role_names.iter().enumerate().map(|(i,r)| (r.name.as_str(), i)).collect()) { + let ref val = role_names[i]; + format!("Closest match: {}", val.name.clone()) + } else { String::new() } + )); + } + } + for (i, role_id) in to_add.clone().iter().enumerate() { + if member.roles.contains(role_id) { + to_add.remove(i); + failed.push(format!("You already have {}", match role_names.iter().find(|r| &r.id == role_id) { + Some(s) => s.name.clone(), + None => role_id.0.to_string(), + })); + } + if let Err(_) = member.add_role(*role_id) { + to_add.remove(i); + failed.push(format!("Failed to add {}", match role_names.iter().find(|r| &r.id == role_id) { + Some(s) => s.name.clone(), + None => role_id.0.to_string(), + })); + }; + } + let mut fields = Vec::new(); + if !to_add.is_empty() { + fields.push(("Added Roles", to_add.iter().filter_map(|r| match r.to_role_cached() { + Some(r) => Some(r.name.clone()), + None => None, + }).collect::<Vec<String>>().join("\n").to_string(), false)); + } + if !failed.is_empty() { + fields.push(("Failed to Add", failed.join("\n"), false)); + } + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Add Self Role Summary") + .fields(fields) + .colour(member.colour().unwrap_or(*colours::GREEN)) + ))?; + } else { + message.channel_id.say("There are no self roles.")?; + } + } else { failed!(MEMBER_FAIL); } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct RemoveSelfRole; +impl Command for RemoveSelfRole { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Remove roles from yourself provided they are on the self role list.".to_string()), + usage: Some("<role_resolvables as CSV>".to_string()), + example: Some("red, green".to_string()), + aliases: vec!["removeselfrole", "rsr"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Some(mut member) = message.member() { + let roles = db.get_roles(guild_id.0 as i64)?; + if !roles.is_empty() { + let list = args.rest().split(",").map(|s| s.trim().to_string()); + let mut to_remove = Vec::new(); + let mut failed = Vec::new(); + let role_names = roles.iter().filter_map(|r| match RoleId(r.id as u64).to_role_cached() { + Some(role) => Some(role.clone()), + None => None, + }).collect::<Vec<Role>>(); + for r1 in list { + if let Some((r, r2)) = parse_role(r1.clone(), guild_id) { + if let Some(_) = roles.iter().find(|e| e.id == r.0 as i64) { + to_remove.push(r); + } else { failed.push(format!("{} is a role, but it isn't self-assignable", r2.name)); } + } else if let Some(i) = roles.iter().position(|r| r.aliases.contains(&r1.to_lowercase())) { + to_remove.push(RoleId(roles[i].id as u64)); + } else { + failed.push(format!("Failed to find match \"{}\". {}", r1, + if let Some(i) = fuzzy_match(&r1, role_names.iter().enumerate().map(|(i,r)| (r.name.as_str(), i)).collect()) { + let ref val = role_names[i]; + format!("Closest match: {}", val.name.clone()) + } else { String::new() } + )); + } + } + for (i, role_id) in to_remove.clone().iter().enumerate() { + if !member.roles.contains(role_id) { + to_remove.remove(i); + failed.push(format!("You don't have {}", match role_names.iter().find(|r| &r.id == role_id) { + Some(s) => s.name.clone(), + None => role_id.0.to_string(), + })); + } + if let Err(_) = member.remove_role(*role_id) { + to_remove.remove(i); + failed.push(format!("Failed to remove {}", match role_names.iter().find(|r| &r.id == role_id) { + Some(s) => s.name.clone(), + None => role_id.0.to_string(), + })); + }; + } + let mut fields = Vec::new(); + if !to_remove.is_empty() { + fields.push(("Removed Roles", to_remove.iter().filter_map(|r| match r.to_role_cached() { + Some(r) => Some(r.name.clone()), + None => None, + }).collect::<Vec<String>>().join("\n").to_string(), false)); + } + if !failed.is_empty() { + fields.push(("Failed to Remove", failed.join("\n"), false)); + } + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Remove Self Role Summary") + .fields(fields) + .colour(member.colour().unwrap_or(*colours::RED)) + ))?; + } else { + message.channel_id.say("There are no self roles.")?; + } + } else { failed!(MEMBER_FAIL); } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct ListSelfRoles; +impl Command for ListSelfRoles { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("List all the self roles for the current server. Optionally, you can view a single category.".to_string()), + usage: Some("[category]".to_string()), + aliases: vec!["listselfroles", "lsr"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let mut roles = db.get_roles(guild_id.0 as i64)?; + if !roles.is_empty() { + if args.is_empty() { + let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new(); + for role in roles.iter() { + match RoleId(role.id as u64).to_role_cached() { + Some(r) => { + map.entry(role.category.clone()).or_insert(Vec::new()).push(r.name); + }, + None => { + // Clean up roles that don't exist + db.del_role(role.id, guild_id.0 as i64)?; + }, + } + } + let mut fields = Vec::new(); + for (key, val) in map.iter_mut() { + val.sort(); + fields.push((key, val.join("\n"), true)); + } + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Self Roles") + .fields(fields) + .colour(*colours::MAIN) + ))?; + } else { + let category = args.full().to_string(); + roles.retain(|e| *e.category.to_lowercase() == category.to_lowercase()); + if !roles.is_empty() { + let mut roles = roles + .iter() + .map(|e| match RoleId(e.id as u64).to_role_cached() { + Some(r) => r.name, + None => e.id.to_string(), + }) + .collect::<Vec<String>>(); + roles.sort(); + message.channel_id.send_message(|m| m + .embed(|e| e + .title(category) + .description(roles.join("\n")) + .colour(*colours::MAIN) + ))?; + } else { + message.channel_id.say(format!("The category `{}` does not exist.", category))?; + } + } + } else { + message.channel_id.say("There are no self roles.")?; + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} diff --git a/src/modules/commands/general/tags.rs b/src/modules/commands/general/tags.rs new file mode 100644 index 0000000..fc364c0 --- /dev/null +++ b/src/modules/commands/general/tags.rs @@ -0,0 +1,168 @@ +use crate::core::consts::*; +use crate::core::consts::DB as db; +use fuzzy_match::algorithms::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::prelude::Context; +use std::cmp::Ordering; +use std::sync::Arc; + +pub struct TagList; +impl Command for TagList { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Alias to `tag list`".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let tags = db.get_tags(guild_id.0 as i64)?; + if !tags.is_empty() { + message.channel_id.say(tags.iter().map(|e| e.name.as_str()).collect::<Vec<&str>>().join("\n"))?; + } else { + message.channel_id.say("No tags founds.")?; + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct TagSingle; +impl Command for TagSingle { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("View a tag.".to_string()), + usage: Some("<tag name>".to_string()), + example: Some("foobar".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let tag_input = args.full().trim().to_string(); + let tags = db.get_tags(guild_id.0 as i64)?; + if !tags.is_empty() { + if let Some(tag) = tags.iter().find(|e| e.name == tag_input) { + message.channel_id.say(&tag.data)?; + } else { + let mut sdc = SorensenDice::new(); + let mut matches = Vec::new(); + for tag in tags.iter() { + let dist = sdc.get_similarity(tag.name.as_str(), &tag_input); + matches.push((tag, dist)); + } + matches.retain(|e| e.1 > 0.2); + matches.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)); + matches.truncate(5); + let matches = matches.iter().map(|e| e.0.name.clone()).collect::<Vec<String>>(); + message.channel_id.say(format!("No tag found. Did you mean...\n{}", matches.join("\n")))?; + } + } else { message.channel_id.say("There are no tags yet.")?; } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct TagAdd; +impl Command for TagAdd { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Create a new tag.".to_string()), + usage: Some("<tag name, quoted> <tag value>".to_string()), + example: Some(r#""my new tag" look, I made a tag!"#.to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let tag_input = args.single_quoted::<String>()?; + let value = args.rest().to_string(); + let tag = db.new_tag(message.author.id.0 as i64, guild_id.0 as i64, tag_input.clone(), value)?; + message.channel_id.say(format!("Successfully created tag `{}`", tag.name))?; + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct TagRemove; +impl Command for TagRemove { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Delete a tag.".to_string()), + usage: Some("<tag name>".to_string()), + example: Some("foobar".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let tag_input = args.single_quoted::<String>()?; + let tag = db.get_tag(guild_id.0 as i64, tag_input.clone())?; + let check = guild_id + .member(message.author.id) + .and_then(|m| m + .permissions() + .map(|p| p + .manage_messages())) + .unwrap_or(false); + if message.author.id.0 as i64 == tag.author || check { + let tag = db.del_tag(guild_id.0 as i64, tag_input.clone())?; + message.channel_id.say(format!("Successfully deleted tag `{}`", tag.name))?; + } else { message.channel_id.say("You must own this tag in order to delete it.")?; } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct TagEdit; +impl Command for TagEdit { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Edit a tag. Only works if you are the author.".to_string()), + usage: Some("<tag name, quoted> <new value>".to_string()), + example: Some(r#""my edited tag" I had to edit this tag"#.to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let tag_input = args.single_quoted::<String>()?; + let value = args.rest().to_string(); + let mut tag = db.get_tag(guild_id.0 as i64, tag_input.clone())?; + let check = guild_id + .member(message.author.id) + .and_then(|m| m + .permissions() + .map(|p| p + .manage_messages())) + .unwrap_or(false); + if message.author.id.0 as i64 == tag.author || check { + tag.data = value.clone(); + let t = db.update_tag(guild_id.0 as i64, tag_input.clone(), tag)?; + message.channel_id.say(format!("Successfully edited tag `{}`", t.name))?; + } else { message.channel_id.say("You must own this tag in order to edit it.")?; } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} diff --git a/src/modules/commands/general/utilities.rs b/src/modules/commands/general/utilities.rs new file mode 100644 index 0000000..fc6fc36 --- /dev/null +++ b/src/modules/commands/general/utilities.rs @@ -0,0 +1,761 @@ +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 forecast::Icon::*; +// use forecast::Units; +// use rand::prelude::*; +use serenity::CACHE; +use serenity::client::bridge::gateway::ShardId; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::{ + channel::Message, + guild::Role, +}; +use serenity::prelude::{ + Context, + Mentionable +}; +// use std::f64::NAN; +use std::sync::Arc; +use sys_info; +use sysinfo::{ + ProcessExt, + SystemExt, + System, + get_current_pid +}; + +// TODO: Get member count working. +pub struct MemberCount; +impl Command for MemberCount { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Wanna check how many cool people are a part of your server ?".to_string()), + guild_only: true, + aliases: vec!["mc"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, _args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let member_count = CACHE.read().guild(guild_id).unwrap().read().member_count; + message.channel_id.say(format!( + "There are {:?} members in this guild !", member_count))?; + } + + Ok(()) + } +} + +pub struct UserId; +impl Command for UserId { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Need your ID ?".to_string()), + usage: Some("[user_resolvable]".to_string()), + example: Some("@fun".to_string()), + guild_only: true, + aliases: vec!["userid", "uid"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Some((id,_)) = parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + message.channel_id.say(format!("{}", id.0))?; + } else { + message.channel_id.say("I couldn't find that user.")?; + } + } + + Ok(()) + } +} + +pub struct PFP; +impl Command for PFP { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Need a close up ? No creeping ! (Defaults to the author of the command.)".to_string()), + usage: Some("[user_resolvable]".to_string()), + example: Some("@fun".to_string()), + guild_only: true, + aliases: vec!["avi", "avatar"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _ctx: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let (user, member) = match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((id, member)) => (id.to_user()?, member), + None => (message.author.clone(), message.member().ok_or("Failed to get member.")?), + }; + message.channel_id.send_message(|m| m + .embed(|e| e + .colour(member.colour().unwrap_or(*colours::MAIN)) + .image(user.face()) + ))?; + } + Ok(()) + } +} + +pub struct Ping; +impl Command for Ping { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("No, I will not play ping-pong with you...".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + let mut lat = 0; + if let Some(sm_lock) = data.get::<SerenityShardManager>() { + let sm = sm_lock.lock(); + let runners = sm.runners.lock(); + if let Some(shard_runner) = runners.get(&ShardId(ctx.shard_id)) { + if let Some(la) = shard_runner.latency { + lat = la.as_secs() as u32 + la.subsec_millis(); + } + } + } + let mut m = message.channel_id.send_message(|m| m.embed(|e| e.title("Pong!")))?; + let t = m.timestamp.timestamp_millis() - message.timestamp.timestamp_millis(); + m.edit(|m| m.embed(|e| e + .title("Pong!") + .description(format!("**Shard Latency:** {}\n**Response Time:** {} ms", if lat==0 { String::from("Failed to retrieve") } else { format!("{} ms", lat) }, t)) + .colour(*colours::MAIN) + ))?; + Ok(()) + } +} + +pub struct Prefix; +impl Command for Prefix { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Need the prefix of the current guild ?".to_string()), + guild_only: true, + aliases: vec!["pre"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Ok(settings) = db.get_guild(guild_id.0 as i64) { + message.channel_id.say(format!("The prefix for this guild is `{}`", settings.prefix))?; + } else { + message.channel_id.say("Failed to get guild data.")?; + } + } + Ok(()) + } +} + +pub struct Reminder; +impl Command for Reminder { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Like a calender, but cooler. (Sent to whatever channel the reminder was created in.)".to_string()), + usage: Some("<reminder text> </t time_resolvable>".to_string()), + example: Some("do the thing /t 1 day 10 min 25 s".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(tc_lock) = data.get::<TC>() { + let tc = tc_lock.lock(); + let channel_id = message.channel_id; + let user_id = message.author.id; + let switches = get_switches(args.rest().to_string()); + let reminder = match switches.get("rest") { + Some(s) => s.clone(), + None => String::new(), + }; + let start_time = Utc::now().timestamp(); + let dur = hrtime_to_seconds(match switches.get("t") { + Some(s) => s.clone(), + None => String::new(), + }); + if dur>0 { + let end_time = start_time + dur; + let reminder_fmt = format!("REMINDER||{}||{}||{}||{}", channel_id.0, user_id.0, dur, reminder); // mut + db.new_timer(start_time, end_time, reminder_fmt.clone())?; + tc.request(); + message.channel_id.say(format!("Got it! I'll remind you to {} in {}", + reminder, + seconds_to_hrtime(dur as usize) + ))?; + } else { + message.channel_id.say("Sorry, I wasn't able to find a time there. Make sure you to add `/t time_resolvable` after your reminder text.")?; + } + } else { failed!(TC_FAIL); } + Ok(()) + } +} + +pub struct RoleInfo; +impl Command for RoleInfo { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Mods lacking ? Don't worry, I got you.".to_string()), + usage: Some("<role_resolvable>".to_string()), + example: Some("@example role".to_string()), + guild_only: true, + aliases: vec!["roleinfo", "ri", "rinfo"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + match parse_role(args.rest().to_string(), guild_id) { + Some((role_id, role)) => { + let role_data = db.get_role(role_id.0 as i64, guild_id.0 as i64).ok(); + let mut fields = vec![ + ("Name", role.name.clone(), true), + ("ID", role_id.0.to_string(), true), + ("Hex", format!("#{}", role.colour.hex()), true), + ("Hoisted", String::from(if role.hoist { "Yes" } else { "No" }), true), + ("Mentionable", String::from(if role.mentionable { "Yes" } else { "No" }), true), + ("Position", role.position.to_string(), true), + ]; + match role_data { + Some(r) => { + fields.push(("Self Assignable", String::from("Yes"), true)); + if !r.aliases.is_empty() { + fields.push(("Self Role Aliases", r.aliases.join(", "), true)); + } + }, + None => { + fields.push(("Self Assignable", String::from("No"), true)); + } + } + message.channel_id.send_message(|m| m + .embed(|e| e + .thumbnail(format!("https://www.colorhexa.com/{}.png", role.colour.hex().to_lowercase())) + .colour(role.colour) + .fields(fields) + ))?; + }, + None => { message.channel_id.say("Unable to find that role.")?; } + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct ServerInfo; +impl Command for ServerInfo { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Need some quick info about the current guild ?".to_string()), + guild_only: true, + aliases: vec!["serverinfo", "si", "sinfo"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + use serenity::model::channel::ChannelType::*; + use serenity::model::user::OnlineStatus::*; + + let switches = get_switches(args.full().to_string()); + let g = match switches.get("rest") { + Some(s) => { + if let Some((_, lock)) = parse_guild(s.to_string()) { + Some(lock) + } else { + None + } + }, + None => message.guild() + }; + if let Some(guild_lock) = g { + let guild = guild_lock.read().clone(); + match switches.get("roles") { + None => { + let mut channels = (0,0,0); + for (_, channel_lock) in guild.channels.iter() { + let channel = channel_lock.read(); // mut + match channel.kind { + Text => { channels.0 += 1; }, + Voice => { channels.1 += 1; }, + Category => { channels.2 += 1; }, + Group => {}, + Private => {}, + } + } + let mut members = (0,0,0); + for (user_id, _) in guild.members.iter() { + match user_id.to_user() { + Ok(u) => { + if u.bot { + members.1 += 1; + } else { + members.0 += 1; + } + }, + Err(_) => {}, + } + } + for (_, presence) in guild.presences.iter() { + match presence.status { + DoNotDisturb => { members.2 += 1; }, + Idle => { members.2 += 1; }, + Invisible => {}, + Offline => {}, + Online => { members.2 += 1; }, + } + } + message.channel_id.send_message(|m| m + .embed(|e| e + .thumbnail(guild.icon_url().unwrap_or("https://cdn.discordapp.com/embed/avatars/0.png".to_string())) + .color(*colours::MAIN) + .field("ID", guild.id, true) + .field("Name", &guild.name, true) + .field("Owner", guild.owner_id.mention(), true) + .field("Region", guild.region, true) + .field(format!("Channels [{}]", guild.channels.len()), format!("Categories: {}\nText: {}\nVoice: {}", channels.2, channels.0, channels.1), true) + .field(format!("Members [{}/{}]", members.2, guild.members.len()), format!("Humans: {}\nBots: {}", members.0, members.1), true) + .field("Created", guild.id.created_at().format("%a, %d %h %Y @ %H:%M:%S").to_string(), false) + .field("Roles", guild.roles.len(), true) + .field("Emojis", guild.emojis.len(), true) + .title(guild.name) + ))?; + }, + Some(_) => { + let mut roles_raw = guild.roles.values().collect::<Vec<&Role>>(); + roles_raw.sort_by(|a, b| b.position.cmp(&a.position)); + let roles = roles_raw.iter().map(|e| e.name.clone()).collect::<Vec<String>>(); + message.channel_id.send_message(|m| m + .embed(|e| e + .title(format!("Roles for {}. Count: {}", guild.name, roles.len())) + .description(roles.join("\n")) + .colour(*colours::BLUE) + ))?; + }, + } + } else { message.channel_id.say("Could not find that guild.")?; } + Ok(()) + } +} + +pub struct Stats; +impl Command for Stats { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("You wanna get to know me more ? How sweet.".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let (cached_guilds + ,cached_channels + ,cached_users + ,cached_messages) = { + let cache = CACHE.read(); + (cache.guilds.len() + ,cache.channels.len() + ,cache.users.len() + ,cache.messages.values() + .fold(0, |a,m| { + a + m.len() + })) + }; + let (db_guilds + ,db_users + ,db_notes + ,db_roles + ,db_timers + ,db_cases + ,db_tags + ,db_hackbans + ,db_premium) = { + (db.count_guilds().unwrap_or(-1) + ,db.count_users().unwrap_or(-1) + ,db.count_notes().unwrap_or(-1) + ,db.count_roles().unwrap_or(-1) + ,db.count_timers().unwrap_or(-1) + ,db.count_cases().unwrap_or(-1) + ,db.count_tags().unwrap_or(-1) + ,db.count_hackbans().unwrap_or(-1) + ,db.count_premium().unwrap_or(-1) + ) + }; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Bot Stats") + .field("Cache", format!( + "Guilds: {}\nChannels: {}\nUsers: {}\nMessages: {}" + ,cached_guilds + ,cached_channels + ,cached_users + ,cached_messages + ), false) + .field("Database", format!( + "Guilds: {}\nUsers: {}\nNotes: {}\nSelf Roles: {}\nTimers: {}\nCases: {}\nTags: {}\nHackbans: {}\nPremium Guilds: {}" + ,db_guilds + ,db_users + ,db_notes + ,db_roles + ,db_timers + ,db_cases + ,db_tags + ,db_hackbans + ,db_premium + ), false) + .field("More coming soon", "...", false) + .colour(*colours::MAIN) + .timestamp(now!()) + ))?; + Ok(()) + } +} + +pub struct Time; // Now +impl Command for Time { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Don't have a clock ? (Optionally, you can provide an amount of hours to offset the time by.".to_string()), + usage: Some("[hour]".to_string()), + example: Some("-5".to_string()), + aliases: vec!["now"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + use chrono::offset::FixedOffset; + let utc = Utc::now(); + let datetime = match args.single::<i32>() { + Ok(data) => { + let tz = FixedOffset::east(data * 3600); + utc.with_timezone(&tz) + }, + Err(_) => { + let tz = FixedOffset::east(0); + utc.with_timezone(&tz) + }, + }; + + let time = datetime.format("%H:%M").to_string(); + let date = datetime.format("%A %e %B %Y").to_string(); + message.channel_id.send_message(|m| m + .embed(|e| e + .colour(*colours::MAIN) + .description(format!("**Time:** {}\n**Date:** {}\n**Timezone:** UTC{}", time, date, datetime.timezone())) + ))?; + Ok(()) + } +} + +pub struct UserInfo; +impl Command for UserInfo { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Yea I'll bring something up for you, just don't be a creep. (Defaults to the author of the command.)".to_string()), + usage: Some("[user_resolvable]".to_string()), + example: Some("@fun".to_string()), + guild_only: true, + aliases: vec!["userinfo", "ui", "uinfo", "whois"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let (user, member) = match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((id, member)) => (id.to_user()?, member), + None => (message.author.clone(), message.member().ok_or("Failed to get member.")?), + }; + let user_data = db.get_user(user.id.0 as i64, guild_id.0 as i64)?; + let mut roles = member.roles.iter() + .map(|c| match c.to_role_cached() { + Some(r) => r.name, + None => c.0.to_string(), + }) + .collect::<Vec<String>>(); + roles.sort(); + let roles = if roles.is_empty() { + "None".to_string() + } else { + roles.join(", ") + }; + let dates = format!( + "Created: {}\nJoined: {}{}", + user.created_at() + .format("%a, %d %h %Y @ %T") + .to_string(), + member.joined_at + .and_then(|t| Some(t.with_timezone(&Utc))) + .unwrap_or(Utc::now()) + .format("%a, %d %h %Y @ %T") + .to_string(), + user_data.registered.map_or(String::new(), |r| { + format!("\nRegistered: {}", r + .format("%a, %d %h %Y @ %T") + .to_string()) + }) + ); + message.channel_id.send_message(|m| m + .embed(|e| e + .colour(member.colour().unwrap_or(*colours::MAIN)) + .thumbnail(user.face()) + .title(&user.tag()) + .field("ID", user.id, true) + .field("Mention", user.mention(), true) + .field("Nickname", member.display_name().into_owned(), true) + .field("Dates", dates, false) + .field(format!("Roles [{}]", member.roles.len()), roles, false) + ))?; + } + Ok(()) + } +} + +/* pub struct Weather; +impl Command for Weather { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + bucket: Some("weather".to_string()), + desc: Some("Check on the current weather at a given city. By default this will use the units used at that location, but units can be manually selected. Options are si, us, uk, ca".to_string()), + usage: Some("<city name> [/unit]".to_string()), + example: Some("london /us".to_string()), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + let data = ctx.data.lock(); + if let Some(api) = data.get::<ApiClient>() { + let switches = get_switches(args.full().to_string()); + let rest = switches.get("rest"); + // TODO: Refactor this + let mut units = Units::Auto; + if switches.len() > 1 { + switches.keys().for_each(|k| { + match k.as_str() { + "uk" => { units = Units::UK; }, + "c" | "ca" => { units = Units::CA; }, + "si" => { units = Units::SI; }, + "us" => { units = Units::Imperial; }, + _ => {}, + } + }); + } + message.channel_id.broadcast_typing()?; + if let Some(loc) = rest { + match api.weather(loc, units) { + Some((city_info, Ok(body))) => { + if let Some(current) = body.currently { + if let Some(daily_data) = body.daily { + let daily = &daily_data.data[0]; + let temp = current.temperature.unwrap_or(NAN); + let temp_high = current.temperature_high.unwrap_or(daily.temperature_high.unwrap_or(NAN)); + let temp_low = current.temperature_low.unwrap_or(daily.temperature_low.unwrap_or(NAN)); + let feels_like = current.apparent_temperature.unwrap_or(NAN); + let wind = current.wind_speed.unwrap_or(NAN); + let visi = current.visibility.unwrap_or(NAN); + let pressure = current.pressure.unwrap_or(NAN); + let humidity = current.humidity.unwrap_or(NAN)*100.0; + let icon = match current.icon { + Some(ic) => { + match ic { + ClearDay => "The sky is clear", + ClearNight => "The sky is clear", + Rain => "It is raining", + Snow => "It is snowing", + Sleet => "It is sleeting", + Wind => "It is windy", + Fog => "It is foggy", + Cloudy => "The sky is cloudy", + PartlyCloudyDay => "The sky is partly cloudy", + PartlyCloudyNight => "The sky is partly cloudy", + Hail => "It is hailing", + Thunderstorm => "There is a thunderstorm", + Tornado => "There is a tornado", + } + }, + None => "The sky is clear", + }; + let response_units = body.flags.and_then(|e| Some(e.units)).unwrap_or(Units::Imperial); + let (temp_unit, speed_unit, dist_unit) = match response_units { + Units::SI => { ("C", "m/s", "km") }, + Units::CA => { ("C", "kmph", "km") }, + Units::UK => { ("C", "mph", "mi") }, + _ => { ("F", "mph", "mi") }, + }; + message.channel_id.send_message(|m| m + .embed(|e| e + .title(format!("Weather in {}", city_info)) + .description(format!("_It is currently **{}°{temp}** with wind of **{} {speed}** making it feel like **{}°{temp}**. {} with a visibility of about **{} {dist}**._", + temp, + wind, + feels_like, + icon, + visi, + temp = temp_unit, + speed = speed_unit, + dist = dist_unit + )) + .field("Temperature", format!( + "Current: **{}°{temp}**\nLow/High: **{}°{temp} / {}°{temp}**", + temp, + temp_low, + temp_high, + temp = temp_unit + ), true) + .field("Wind Chill", format!( + "Feels Like: **{}°{temp}**\nWind Speed: **{} {speed}**", + feels_like, + wind, + temp = temp_unit, + speed = speed_unit + ), true) + .field("Atmosphere", format!( + "Humidity: **{}%**\nPressure: **{} mbar**", + humidity, + pressure, + ), true) + .colour(*colours::MAIN) + .timestamp(now!()) + .footer(|f| f.text("Forecast by Dark Sky")) + ))?; + } + } + }, + Some((_, Err(why))) => { + message.channel_id.say(format!("Something went wrong while getting the forecast.\n{}", why))?; + }, + None => { + message.channel_id.say("An error occurred while resolving the location.")?; + }, + } + } else { message.channel_id.say("Please enter a location.")?; } + } else { failed!(API_FAIL); } + Ok(()) + } +} */ + +pub struct Wisp; // BotInfo +impl Command for Wisp { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Information about me !".to_string()), + usage: Some("".to_string()), + aliases: vec!["botinfo", "bi", "binfo", "bot", "invite"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + use serenity::builder::CreateEmbed; + + let data = ctx.data.lock(); + // let (guild_count, shard_count, thumbnail) = { + // let cache = CACHE.read(); + // (cache.guilds.len(), cache.shard_count, cache.user.face()) + // }; + let thumbnail = CACHE.read().user.face(); + let owner = data.get::<Owner>().expect("Failed to get owner").to_user()?; + let sys = System::new(); + let embed = CreateEmbed::default() + // .description("Hi! I'm Wisp, a general purpose bot created in [Rust](http://www.rust-lang.org/) using [Serenity](https://github.com/serenity-rs/serenity).") + .description("Hey ! I'm Wisp, I was written in [Rust](https://www.rust-lang.org/) using [Serenity](https://github.com/serenity-rs/serenity).") + .field("Owner/ Developer", format!( + "Name: {}\nID: {}" + ,owner.tag() + ,owner.id) + ,true) + // .field("Links", format!( + // "[Support Server]({})\n[Invite]({})\n[GitLab]({})\n[Patreon]({})" + // ,SUPPORT_SERV_INVITE + // ,BOT_INVITE + // ,GITLAB_LINK + // ,PATREON_LINK) + // ,true) + .field("Useful Links", format!( + // "[Support Server]({})\n[Invite]({})\n[GitLab]({})\n[Patreon]({})" + "[Invite]({})" + // ,SUPPORT_SERV_INVITE + ,BOT_INVITE) + // ,GITLAB_LINK + // ,PATREON_LINK) + ,true) // true + // .field("Counts", format!( + // "Guilds: {}\nShards: {}" + // ,guild_count + // ,shard_count) + // ,false) + .footer(|f| f + .text("Want to request a feature ? Submit one over at [email protected] !")) + .thumbnail(thumbnail) + .colour(*colours::MAIN); + if let Some(process) = sys.get_process(get_current_pid()) { + message.channel_id.send_message(|m| m + .embed(|_| embed + .field("System Info", format!( + "Type: {} {}\nUptime: {}" + ,sys_info::os_type().unwrap_or(String::from("OS Not Found")) + ,sys_info::os_release().unwrap_or(String::from("Release Not Found")) + ,seconds_to_hrtime(sys.get_uptime() as usize)) + ,false) + .field("Process Info", format!( + "Memory Usage: {} MB\nCPU Usage {}%\nUptime: Temporarily Disabled" + ,process.memory()/1000 // convert to MB + ,(process.cpu_usage()*100.0).round()/100.0 // round to 2 decimals + /* ,seconds_to_hrtime((sys.get_uptime() - process.start_time()) as usize) */) + ,true) + ))?; + } else { + message.channel_id.send_message(|m| m + .embed(|_| embed + ))?; + } + Ok(()) + } +} diff --git a/src/modules/commands/mod.rs b/src/modules/commands/mod.rs new file mode 100644 index 0000000..76dca00 --- /dev/null +++ b/src/modules/commands/mod.rs @@ -0,0 +1,4 @@ +pub mod admins; +pub mod general; +pub mod mods; +pub mod owners; diff --git a/src/modules/commands/mods/hackbans.rs b/src/modules/commands/mods/hackbans.rs new file mode 100644 index 0000000..5cf8030 --- /dev/null +++ b/src/modules/commands/mods/hackbans.rs @@ -0,0 +1,117 @@ +use crate::core::colours; +use crate::core::consts::DB as db; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::id::UserId; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct HackbanAdd; +impl Command for HackbanAdd { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + // desc: Some("Adds a user to the hackban list. Users on this list will be banned on joining.".to_string()), + desc: Some("This ? It'll add someone to the \"hackban\" list, basically, they'll be banned upon joining.".to_string()), + usage: Some("<user_id> [reason]".to_string()), + example: Some("217348698294714370 spamming images in general".to_string()), + required_permissions: Permissions::BAN_MEMBERS, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + let guild_id = message.guild_id.unwrap(); + let hackbans = db.get_hackbans(guild_id.0 as i64)?; + let user_id = UserId(args.single::<u64>()?); + match hackbans.iter().find(|e| e.id as u64 == user_id.0) { + Some(_) => { message.channel_id.say("User is already hackbanned.")?; }, + None => { + let reason = args.single::<String>().ok(); + db.new_hackban(user_id.0 as i64, guild_id.0 as i64, reason.clone())?; + message.channel_id.say(format!( + "Added {} to the hackban list{}", + user_id.0, + match reason { + Some(r) => format!(" with reason `{}`", r), + None => String::new(), + } + ))?; + } + } + Ok(()) + } +} + +pub struct HackbanRemove; +impl Command for HackbanRemove { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Feeling forgiveful ? Take someone off the \"hackban\" list !".to_string()), + usage: Some("<user_id>".to_string()), + example: Some("217348698294714370".to_string()), + required_permissions: Permissions::BAN_MEMBERS, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + let guild_id = message.guild_id.unwrap(); + let hackbans = db.get_hackbans(guild_id.0 as i64)?; + let user_id = UserId(args.single::<u64>()?); + match hackbans.iter().find(|e| e.id as u64 == user_id.0) { + None => { message.channel_id.say("User isn't hackbanned.")?; }, + Some(_) => { + db.del_hackban(user_id.0 as i64, guild_id.0 as i64)?; + message.channel_id.say(format!( + "Removed {} from the hackban list", + user_id.0 + ))?; + } + } + Ok(()) + } +} + +pub struct HackbanList; +impl Command for HackbanList { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + // desc: Some("Lets users on the hackban list along with their reasons, if provided.".to_string()), + desc: Some("Checking up ? See who's on the \"hackban\" list.".to_string()), + required_permissions: Permissions::BAN_MEMBERS, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + let guild_id = message.guild_id.unwrap(); + let hackbans = db.get_hackbans(guild_id.0 as i64)?; + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Hackbans") + .description( + hackbans.iter().cloned().map(|e| format!( + "{}{}", + e.id, + format!(": `{}`", e.reason.unwrap_or(String::new())) + )) + .collect::<Vec<String>>() + .join("\n") + ) + .colour(*colours::MAIN) + ))?; + Ok(()) + } +} diff --git a/src/modules/commands/mods/info.rs b/src/modules/commands/mods/info.rs new file mode 100644 index 0000000..11c2119 --- /dev/null +++ b/src/modules/commands/mods/info.rs @@ -0,0 +1,49 @@ +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct ModInfo; +impl Command for ModInfo { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Someone acting up ? Look em' up.".to_string()), + usage: Some("<user_resolvable>".to_string()), + example: Some("@fun".to_string()), + aliases: vec!["mi", "minfo"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_MESSAGES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((user_id, _)) => { + let user = db.get_user(user_id.0 as i64, guild_id.0 as i64)?; + let cases = db.get_cases(user_id.0 as i64, guild_id.0 as i64)?; + let case_fmt = cases.iter().map(|c| format!("Type: {}\nModerator: {}\nTimestamp: {}", c.casetype, c.moderator, c.timestamp)).collect::<Vec<String>>().join("\n"); + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Moderator info") + .field("Watchlist", { if user.watchlist { "Yes" } else { "No" } }, false) + .field("Cases", if case_fmt.is_empty() { "None" } else { case_fmt.as_str() }, false) + ))?; + }, + None => { message.channel_id.say("I couldn't find that user.")?; } + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} diff --git a/src/modules/commands/mods/kickbans.rs b/src/modules/commands/mods/kickbans.rs new file mode 100644 index 0000000..ca26350 --- /dev/null +++ b/src/modules/commands/mods/kickbans.rs @@ -0,0 +1,97 @@ +use crate::core::consts::DB as db; +use crate::core::utils::parse_user; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +// TODO: add case entry on successful kick/ban + +pub struct BanUser; +impl Command for BanUser { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + // desc: Some("Bans a user. If unable to resolve to a bannable user, the user is hackbanned instead.".to_string()), + desc: Some("Wield this with power... (If I am unable to get the user, they will be \"hackbanned\" instead)".to_string()), + usage: Some("<user_resolvable> [time_resolvable] [reason]".to_string()), + example: Some("217348698294714370 spamming images in general".to_string()), + required_permissions: Permissions::BAN_MEMBERS, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + let guild_id = message.guild_id.ok_or("Failed to get guild_id")?; + let arg = args.single::<String>()?; + if let Some((_, member)) = parse_user(arg.clone(), guild_id) { + let (days, reason) = { + match args.single_quoted_n::<u8>().ok() { + None => { + (None, Some(args.rest())) + }, + days => { + args.skip(); + (days, Some(args.rest())) + } + } + }; + match (days, reason) { + (Some(d), Some(r)) => { member.ban(&(d,r))?; }, + (Some(d), None) => { member.ban(&d)?; }, + (None, Some(r)) => { member.ban(&r)?; }, + (None, None) => { member.ban(&0)?; }, + } + message.channel_id.say(format!( + "Banned {} successfully{}." + ,member.user.read().tag() + ,if let Some(r) = reason { format!("with reason {}", r) } else { "".to_string() } + ))?; + } else { + if let Ok(id) = arg.parse::<i64>() { + db.new_hackban(id, guild_id.0 as i64, args.single::<String>().ok())?; + } else { + message.channel_id.say("User does not exist in guild and argument is not a valid ID.")?; + } + } + Ok(()) + } +} + +pub struct KickUser; +impl Command for KickUser { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + // desc: Some("Kicks a user. Reason is discarded due to a limitation in Serenity but will be implemented at a later date.".to_string()), + desc: Some("Want to temporarily set-back someone ? (Reason is currentaly disabled.)".to_string()), + usage: Some("<user_resolvable> [reason]".to_string()), + example: Some("217348698294714370 spamming images in general".to_string()), + required_permissions: Permissions::KICK_MEMBERS, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + let guild_id = message.guild_id.ok_or("Failed to get guild_id")?; + if let Some((_, member)) = parse_user(args.single::<String>()?, guild_id) { + let _reason = args.rest(); + member.kick()?; + message.channel_id.say(format!( + "Kicked {} successfully." + ,member.user.read().tag() + ))?; + } else { + message.channel_id.say("User does not exist in guild.")?; + } + Ok(()) + } +} diff --git a/src/modules/commands/mods/mod.rs b/src/modules/commands/mods/mod.rs new file mode 100644 index 0000000..8a852cf --- /dev/null +++ b/src/modules/commands/mods/mod.rs @@ -0,0 +1,80 @@ +// pub mod hackbans; +// pub mod info; +pub mod kickbans; +// pub mod mute; +pub mod notes; +// pub mod roles; +pub mod watchlist; + +// use self::hackbans::*; +// use self::info::*; +use self::kickbans::*; +// use self::mute::*; +use self::notes::*; +// use self::roles::*; +use self::watchlist::*; +use serenity::framework::standard::CreateGroup; + +/* pub fn init_hackbans() -> CreateGroup { + CreateGroup::default() + .prefixes(vec!["hackban", "hb"]) + .guild_only(true) + .help_available(true) + .cmd("add", HackbanAdd) + .cmd("remove", HackbanRemove) + .cmd("list", HackbanList) +} */ + +/* pub fn init_info() -> CreateGroup { + CreateGroup::default() + .guild_only(true) + .help_available(true) + .cmd("modinfo", ModInfo) +} */ + +pub fn init_kickbans() -> CreateGroup { + CreateGroup::default() + .guild_only(true) + .help_available(true) + .cmd("kick", KickUser) + .cmd("ban", BanUser) +} + +/* pub fn init_mute() -> CreateGroup { + CreateGroup::default() + .guild_only(true) + .help_available(true) + .cmd("mute", Mute) + .cmd("unmute", Unmute) +} */ + +pub fn init_notes() -> CreateGroup { + CreateGroup::default() + .prefix("note") + .guild_only(true) + .help_available(true) + .cmd("add", NoteAdd) + .cmd("delete", NoteRemove) + .cmd("list", NoteList) +} + +/* pub fn init_roles() -> CreateGroup { + CreateGroup::default() + .guild_only(true) + .help_available(true) + .cmd("register", Register) + .cmd("addrole", AddRole) + .cmd("removerole", RemoveRole) + .cmd("rolecolour", RoleColour) +} */ + +pub fn init_watchlist() -> CreateGroup { + CreateGroup::default() + .prefixes(vec!["watchlist", "wl"]) + .guild_only(true) + .help_available(true) + .default_cmd(WatchlistList) + .cmd("add", WatchlistAdd) + .cmd("delete", WatchlistRemove) + .cmd("list", WatchlistList) +} diff --git a/src/modules/commands/mods/mute.rs b/src/modules/commands/mods/mute.rs new file mode 100644 index 0000000..12ee36d --- /dev/null +++ b/src/modules/commands/mods/mute.rs @@ -0,0 +1,179 @@ +use chrono::Utc; +use crate::core::colours; +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::model::TC; +use crate::core::utils::*; +use serenity::builder::CreateMessage; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::id::ChannelId; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct Mute; +impl Command for Mute { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Someone being annoyin ? Mute em' ! (Optionally, include a reason and/ or time for the mute to expire).".to_string()), + usage: Some("<user_resolvable> [/t time] [/r reason]".to_string()), + example: Some("@fun /t 1day /r spam".to_string()), + required_permissions: Permissions::MANAGE_ROLES | Permissions::MUTE_MEMBERS, + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_lock) = message.guild() { + let guild = { + guild_lock.read().clone() + }; + if let Some((_, mut member)) = parse_user(args.single::<String>().unwrap_or(String::new()), guild.id) { + let guild_data = db.get_guild(guild.id.0 as i64)?; + if guild_data.mute_setup { + let switches = get_switches(args.rest().to_string()); + let time = match switches.get("t") { + Some(s) => hrtime_to_seconds(s.clone()), + None => 0, + }; + let reason = match switches.get("r") { + Some(s) => s.clone(), + None => String::new(), + }; + if let Some(mute_role) = guild.roles.values().find(|e| e.name.to_lowercase() == "muted") { + if member.roles.contains(&mute_role.id) { + message.channel_id.say("Member already muted.")?; + } else { + member.add_role(mute_role)?; + let user = { + member.user.read().clone() + }; + let case = db.new_case(user.id.0 as i64, guild.id.0 as i64, "Mute".to_string(), Some(reason.clone()), message.author.id.0 as i64)?; + let mut description = format!( + "**User:** {} ({})\n**Moderator:** {} ({})" + ,user.tag() + ,user.id.0 + ,message.author.tag() + ,message.author.id.0); + if time != 0 { + let data = ctx.data.lock(); + if let Some(tc_lock) = data.get::<TC>() { + let tc = tc_lock.lock(); + let data = format!("UNMUTE||{}||{}||{}||{}||{}||{}", + user.id.0, + guild.id.0, + mute_role.id.0, + if guild_data.modlog && guild_data.modlog_channel > 0 { + guild_data.modlog_channel + } else { message.channel_id.0 as i64 }, + time, + case.id); + let start_time = Utc::now().timestamp(); + let end_time = start_time + time; + db.new_timer(start_time, end_time, data)?; + tc.request(); + description = format!( + "{}\n**Duration:** {}" + ,description + ,seconds_to_hrtime(time as usize)); + } else { + message.channel_id.say("Something went wrong with the timer.")?; + } + } + if !reason.is_empty() { + description = format!( + "{}\n**Reason:** {}" + ,description + ,reason.to_string()); + } + let response = CreateMessage::default() + .embed(|e| e + .title("Member Muted") + .colour(*colours::RED) + .description(description) + .timestamp(now!())); + + if guild_data.modlog && guild_data.modlog_channel > 0 { + let channel = ChannelId(guild_data.modlog_channel as u64); + channel.send_message(|_| response)?; + } else { + message.channel_id.send_message(|_| response)?; + } + } + } else { message.channel_id.say("No mute role")?; } + } else { + message.channel_id.say("Please run `setup` before using this command. Without it, muting may not work right.")?; + } + } else { message.channel_id.say("I couldn't find that user.")?; } + } else { failed!(GUILD_FAIL); } + Ok(()) + } +} + +pub struct Unmute; +impl Command for Unmute { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("How nice, you'll unmute them !".to_string()), + usage: Some("<user_resolvable>".to_string()), + example: Some("@fun".to_string()), + required_permissions: Permissions::MANAGE_ROLES | Permissions::MUTE_MEMBERS, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_lock) = message.guild() { + let guild = { + guild_lock.read().clone() + }; + if let Some((_, mut member)) = parse_user(args.single::<String>().unwrap_or(String::new()), guild.id) { + let guild_data = db.get_guild(guild.id.0 as i64)?; + if guild_data.mute_setup { + if let Some(mute_role) = guild.roles.values().find(|e| e.name.to_lowercase() == "muted") { + let user = { + member.user.read().clone() + }; + let mut description = format!( + "**User:** {} ({})\n**Moderator:** {} ({})" + ,user.tag() + ,user.id.0 + ,message.author.tag() + ,message.author.id.0); + let response = CreateMessage::default() + .embed(|e| e + .title("Member Unmuted") + .colour(*colours::GREEN) + .description(description) + .timestamp(now!())); + + if member.roles.contains(&mute_role.id) { + member.remove_role(mute_role)?; + if guild_data.modlog && guild_data.modlog_channel > 0 { + let channel = ChannelId(guild_data.modlog_channel as u64); + channel.send_message(|_| response)?; + } else { + message.channel_id.send_message(|_| response)?; + } + } else { + message.channel_id.say("Member was not muted.")?; + } + } else { message.channel_id.say("No mute role")?; } + } else { + message.channel_id.say("Please run `setup` before using this command. Without it, muting may not work right.")?; + } + } else { message.channel_id.say("I couldn't find that user.")?; } + } else { failed!(GUILD_FAIL); } + Ok(()) + } +} diff --git a/src/modules/commands/mods/notes.rs b/src/modules/commands/mods/notes.rs new file mode 100644 index 0000000..495e93d --- /dev/null +++ b/src/modules/commands/mods/notes.rs @@ -0,0 +1,108 @@ +use crate::core::colours; +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct NoteAdd; +impl Command for NoteAdd { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Of the forgetful kind ? Add a note to someone !".to_string()), + usage: Some("<user_resolvable> <note>".to_string()), + example: Some("@fun test note".to_string()), + required_permissions: Permissions::MANAGE_MESSAGES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((user,_)) => { + let note = args.rest().to_string(); + let data = db.new_note(user.0 as i64, guild_id.0 as i64, note, message.author.id.0 as i64)?; + message.channel_id.say(format!("Added note `{}`.", data.note))?; + }, + None => { message.channel_id.say("I couldn't find that user")?; } + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct NoteRemove; +impl Command for NoteRemove { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Oh, you remembered...".to_string()), + usage: Some("<user_resolvable> <index>".to_string()), + example: Some("@fun 3".to_string()), + aliases: vec!["del", "rm"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_MESSAGES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((user,_)) => { + let index = args.single::<i32>().unwrap_or(0); + let data = db.del_note(index, user.0 as i64, guild_id.0 as i64)?; + message.channel_id.say(format!("Deleted note `{}`.", data))?; + }, + None => { message.channel_id.say("I couldn't find that user")?; } + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct NoteList; +impl Command for NoteList { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Checking in ? See all the notes on someone.".to_string()), + usage: Some("<user_resolvable>".to_string()), + example: Some("@fun".to_string()), + aliases: vec!["ls"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_MESSAGES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((user_id, member)) => { + let notes = db.get_notes(user_id.0 as i64, guild_id.0 as i64)?; + let notes_fmt = notes.iter().map(|n| n.to_string()).collect::<Vec<String>>().join("\n\n"); + message.channel_id.send_message(|m| m + .embed(|e| e + .colour(*colours::MAIN) + .title(format!("Notes for {}", member.display_name().into_owned())) + .description(notes_fmt) + ))?; + }, + None => { message.channel_id.say("I couldn't find that user")?; } + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} diff --git a/src/modules/commands/mods/roles.rs b/src/modules/commands/mods/roles.rs new file mode 100644 index 0000000..1d5e8d5 --- /dev/null +++ b/src/modules/commands/mods/roles.rs @@ -0,0 +1,338 @@ +use chrono::Utc; +use crate::core::colours; +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::model::TC; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::id::{ + ChannelId, + RoleId +}; +use serenity::model::guild::Member; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct Register; +impl Command for Register { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("A premium command that adds roles to a user (from the self roles list only), and depending on the settings for the command, will apply either a member role or a cooldown role with a timer. When the timer ends, cooldown is removed and member is added. In order for the switch to occur automatically, this command must be used. See the premium commands for more information on configuring this command.".to_string()), + usage: Some("<user_resolvable> <role_resolvables as CSV>".to_string()), + example: Some("@fun gamer, techie".to_string()), + aliases: vec!["reg"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_ROLES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, ctx: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + debug!("REGISTER TRACE: Begin register for user: {}", message.author.id.0); + if let Some(guild_id) = message.guild_id { + let settings = db.get_premium(guild_id.0 as i64).map_err(|_| "Premium is required to use this command.")?; + let guild_data = db.get_guild(guild_id.0 as i64)?; + let roles = db.get_roles(guild_id.0 as i64)?; + debug!("REGISTER TRACE: Settings obtained"); + match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((user_id, mut member)) => { + debug!("REGISTER TRACE: User matched"); + let channel = if guild_data.modlog { + ChannelId(guild_data.modlog_channel as u64) + } else { message.channel_id }; + let list = args.rest().split(",").map(|s| s.trim().to_string()); + let mut to_add = Vec::new(); + for r1 in list { + if let Some((r, _)) = parse_role(r1.clone(), guild_id) { + if settings.cooldown_restricted_roles.contains(&(r.0 as i64)) { continue; } + to_add.push(r); + } else if let Some(i) = roles.iter().position(|r| r.aliases.contains(&r1)) { + if settings.cooldown_restricted_roles.contains(&(roles[i].id)) { continue; } + to_add.push(RoleId(roles[i].id as u64)); + } + } + debug!("REGISTER TRACE: Resolved roles"); + let mut to_add = filter_roles(to_add, guild_id.member(&message.author)?); + debug!("REGISTER TRACE: Filtered roles"); + for (i, role_id) in to_add.clone().iter().enumerate() { + if member.roles.contains(role_id) { + to_add.remove(i); + continue; + } + if let Err(_) = member.add_role(*role_id) { + to_add.remove(i); + }; + } + debug!("REGISTER TRACE: Roles added"); + if let Some(role) = settings.register_cooldown_role { + member.add_role(RoleId(role as u64))?; + debug!("REGISTER TRACE: Added cooldown role"); + if let Some(member_role) = settings.register_member_role { + debug!("REGISTER TRACE: Added member role"); + let data = ctx.data.lock(); + let tc_lock = data.get::<TC>().ok_or("Failed to obtain timer client.")?; + let tc = tc_lock.lock(); + let dur = match settings.register_cooldown_duration { + Some(dur) => dur, + None => DAY as i32, + }; + let data = format!("COOLDOWN||{}||{}||{}||{}", + user_id.0, + guild_id.0, + member_role, + role); + let start_time = Utc::now().timestamp(); + let end_time = start_time + dur as i64; + check_error!(db.new_timer(start_time, end_time, data)); + tc.request(); + debug!("REGISTER TRACE: Timer registered"); + } + } else if let Some(role) = settings.register_member_role { + member.add_role(RoleId(role as u64))?; + debug!("REGISTER TRACE: Added member role (No cooldown role)"); + } + let desc = if !to_add.is_empty() { + to_add.iter().map(|r| match r.to_role_cached() { + Some(role) => role.name, + None => r.0.to_string(), + }) + .collect::<Vec<String>>() + .join("\n") + } else { String::new() }; + debug!("REGISTER TRACE: Built log message"); + channel.send_message(|m| m + .embed(|e| e + .title(format!( + "Registered {} with the following roles:", + member.user.read().tag() + )) + .description(desc) + .colour(member.colour().unwrap_or(*colours::MAIN)) + .timestamp(now!()) + ))?; + debug!("REGISTER TRACE: Sent log message"); + if guild_data.introduction && guild_data.introduction_channel>0 { + let channel = ChannelId(guild_data.introduction_channel as u64); + if guild_data.introduction_type == "embed" { + send_welcome_embed(guild_data.introduction_message, &member, channel)?; + } else { + channel.say(parse_welcome_items(guild_data.introduction_message, &member))?; + } + debug!("REGISTER TRACE: Sent introduction message"); + } + }, + None => { message.channel_id.say("I couldn't find that user.")?; } + } + } else { failed!(GUILDID_FAIL); } + debug!("REGISTER TRACE: Register completed successfully"); + Ok(()) + } +} + +pub struct AddRole; +impl Command for AddRole { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Welcome to the club ! Add a role(s) to someone !".to_string()), + usage: Some("<user_resolvable> <role_resolvables as CSV>".to_string()), + example: Some("@fun red, green".to_string()), + aliases: vec!["ar"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_ROLES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Some((_, mut member)) = parse_user(args.single::<String>()?, guild_id) { + let list = args.rest().split(",").map(|s| s.trim().to_string()); + let mut to_add = Vec::new(); + let mut failed = Vec::new(); + for r1 in list { + if let Some((s,_)) = parse_role(r1.clone(), guild_id) { + to_add.push(s); + } else { + failed.push(format!("Could not locate {}", r1)); + } + } + let mut to_add = filter_roles(to_add, guild_id.member(&message.author)?); + for (i, role_id) in to_add.clone().iter().enumerate() { + if member.roles.contains(role_id) { + to_add.remove(i); + failed.push(format!( + "You already have {}", + match role_id.to_role_cached() { + Some(role) => role.name, + None => role_id.0.to_string(), + })); + } + if let Err(_) = member.add_role(*role_id) { + to_add.remove(i); + failed.push(format!( + "Failed to add {}", + match role_id.to_role_cached() { + Some(role) => role.name, + None => role_id.0.to_string(), + })); + }; + } + let mut fields = Vec::new(); + if !to_add.is_empty() { + fields.push(("Added Roles", to_add.iter() + .map(|r| match r.to_role_cached() { + Some(role) => role.name, + None => r.0.to_string(), + }) + .collect::<Vec<String>>() + .join("\n"), + false)); + } + if !failed.is_empty() { + fields.push(("Failed to Add", failed.join("\n"), false)); + } + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Add Role Summary") + .fields(fields) + .colour(member.colour().unwrap_or(*colours::MAIN)) + ))?; + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct RemoveRole; +impl Command for RemoveRole { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("It doesn't have to end like this... Remove someone's role(s).".to_string()), + usage: Some("<user_resolvable> <role_resolvables as CSV>".to_string()), + example: Some("@fun red, green".to_string()), + aliases: vec!["rr"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_ROLES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + if let Some((_, mut member)) = parse_user(args.single::<String>()?, guild_id) { + let list = args.rest().split(",").map(|s| s.trim().to_string()); + let mut to_remove = Vec::new(); + let mut failed = Vec::new(); + for r1 in list { + if let Some((s,_)) = parse_role(r1.clone(), guild_id) { + to_remove.push(s); + } else { + failed.push(format!("Could not locate {}", r1)); + } + } + let mut to_remove = filter_roles(to_remove, guild_id.member(&message.author)?); + for (i, role_id) in to_remove.clone().iter().enumerate() { + if !member.roles.contains(role_id) { + to_remove.remove(i); + failed.push(format!( + "You don't have {}", + match role_id.to_role_cached() { + Some(role) => role.name, + None => role_id.0.to_string(), + })); + } + if let Err(_) = member.remove_role(*role_id) { + to_remove.remove(i); + failed.push(format!( + "Failed to remove {}", + match role_id.to_role_cached() { + Some(role) => role.name, + None => role_id.0.to_string(), + })); + }; + } + let mut fields = Vec::new(); + if !to_remove.is_empty() { + fields.push(("Removed Roles", to_remove.iter() + .map(|r| match r.to_role_cached() { + Some(role) => role.name, + None => r.0.to_string(), + }) + .collect::<Vec<String>>() + .join("\n"), + false)); + } + if !failed.is_empty() { + fields.push(("Failed to Remove", failed.join("\n"), false)); + } + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Remove Role Summary") + .fields(fields) + .colour(member.colour().unwrap_or(*colours::MAIN)) + ))?; + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct RoleColour; +impl Command for RoleColour { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Feeling \"colourful\" ? Change the colour of a role !".to_string()), + usage: Some("<role_resolvable> <colour>".to_string()), + example: Some("418130449089691658 e0ffff".to_string()), + aliases: vec!["rc", "rolecolor"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_ROLES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + match parse_role(args.single_quoted::<String>().unwrap_or(String::new()), guild_id) { + Some((_, mut role)) => { + let input = args.single::<String>()?; + let colour_as_hex = if input.starts_with("#") { + &input[1..] + } else { input.as_str() }; + let colour = u64::from_str_radix(colour_as_hex, 16)?; + role.edit(|r| r.colour(colour))?; + message.channel_id.say(format!("Colour of `{}` changed to `#{:06X}`", role.name, colour))?; + }, + None => { message.channel_id.say("I couldn't find that role")?; }, + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +fn filter_roles(roles: Vec<RoleId>, member: Member) -> Vec<RoleId> { + let highest = match member.highest_role_info() { + Some((_,h)) => h, + None => -1, + }; + roles.into_iter() + .filter_map(|r| { + let role = r.to_role_cached()?; + match role.position >= highest { + true => None, + false => Some(r), + } + }) + .collect() +} diff --git a/src/modules/commands/mods/watchlist.rs b/src/modules/commands/mods/watchlist.rs new file mode 100644 index 0000000..b4470a5 --- /dev/null +++ b/src/modules/commands/mods/watchlist.rs @@ -0,0 +1,113 @@ +use crate::core::colours; +use crate::core::consts::*; +use crate::core::consts::DB as db; +use crate::core::utils::*; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::model::id::UserId; +use serenity::model::Permissions; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct WatchlistAdd; +impl Command for WatchlistAdd { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("What have they done now... Add someone the the watchlist.".to_string()), + usage: Some("<user_resolvable>".to_string()), + example: Some("@fun".to_string()), + required_permissions: Permissions::MANAGE_MESSAGES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((user_id, member)) => { + let mut user_data = db.get_user(user_id.0 as i64, guild_id.0 as i64)?; + user_data.watchlist = true; + db.update_user(user_id.0 as i64, guild_id.0 as i64, user_data)?; + message.channel_id.say(format!("Set {} to watchlist status.", member.display_name().into_owned()))?; + }, + None => { message.channel_id.say("I couldn't find that user")?; } + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct WatchlistRemove; +impl Command for WatchlistRemove { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Given up, have you ?".to_string()), + usage: Some("<user_resolvable>".to_string()), + example: Some("@fun".to_string()), + aliases: vec!["del", "rm"].iter().map(|e| e.to_string()).collect(), + required_permissions: Permissions::MANAGE_MESSAGES, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + match parse_user(args.single::<String>().unwrap_or(String::new()), guild_id) { + Some((user_id, member)) => { + let mut user_data = db.get_user(user_id.0 as i64, guild_id.0 as i64)?; + user_data.watchlist = false; + db.update_user(user_id.0 as i64, guild_id.0 as i64, user_data)?; + message.channel_id.say(format!("Unset {} from watchlist status.", member.display_name().into_owned()))?; + }, + None => { message.channel_id.say("I couldn't find that user")?; } + } + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} + +pub struct WatchlistList; +impl Command for WatchlistList { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + desc: Some("Yeah, I'll tell you who's on the watchlist.".to_string()), + required_permissions: Permissions::MANAGE_MESSAGES, + aliases: vec!["ls"].iter().map(|e| e.to_string()).collect(), + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + if let Some(guild_id) = message.guild_id { + let users = db.get_users(guild_id.0 as i64)?; + let user_map = users.iter() + .filter(|e| e.watchlist) + .map(|u| { + match UserId(u.id as u64).to_user() { + Ok(user) => user.tag(), + Err(_) => format!("<#{}>", u.id), + } + }) + .collect::<Vec<String>>() + .join("\n"); + message.channel_id.send_message(|m| m + .embed(|e| e + .title("Watchlist") + .description(user_map) + .colour(*colours::MAIN) + ))?; + } else { failed!(GUILDID_FAIL); } + Ok(()) + } +} diff --git a/src/modules/commands/owners/log.rs b/src/modules/commands/owners/log.rs new file mode 100644 index 0000000..67a8156 --- /dev/null +++ b/src/modules/commands/owners/log.rs @@ -0,0 +1,27 @@ +use std::path::Path; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct Log; +impl Command for Log { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + owners_only: true, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, _: Args) -> Result<(), CommandError> { + message.channel_id.send_files(vec![Path::new("./output.log")], |m| m)?; + Ok(()) + } +} diff --git a/src/modules/commands/owners/mod.rs b/src/modules/commands/owners/mod.rs new file mode 100644 index 0000000..55105c3 --- /dev/null +++ b/src/modules/commands/owners/mod.rs @@ -0,0 +1,14 @@ +pub mod log; +pub mod premium; + +use self::log::*; +use self::premium::*; +use serenity::framework::standard::CreateGroup; + +pub fn init() -> CreateGroup { + CreateGroup::default() + .help_available(false) + .cmd("log", Log) + .cmd("premium", Premium) // op + .cmd("say", Say) +} diff --git a/src/modules/commands/owners/premium.rs b/src/modules/commands/owners/premium.rs new file mode 100644 index 0000000..4719faa --- /dev/null +++ b/src/modules/commands/owners/premium.rs @@ -0,0 +1,76 @@ +use crate::core::consts::DB as db; +use crate::core::utils::parse_guild; +use serenity::framework::standard::{ + Args, + Command, + CommandError, + CommandOptions +}; +use serenity::model::channel::Message; +use serenity::prelude::Context; +use std::sync::Arc; + +pub struct Say; +impl Command for Say { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + owners_only: true, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, args: Args) -> Result<(), CommandError> { + message.delete()?; + message.channel_id.say(args.to_string())?; + + Ok(()) + } +} + +pub struct Premium; +impl Command for Premium { + fn options(&self) -> Arc<CommandOptions> { + let default = CommandOptions::default(); + let options = CommandOptions { + owners_only: true, + ..default + }; + Arc::new(options) + } + + fn execute(&self, _: &mut Context, message: &Message, mut args: Args) -> Result<(), CommandError> { + let op = args.single::<String>()?; + let g = args.single_quoted::<String>()?; + if let Some((guild_id, guild_lock)) = parse_guild(g) { + let guild = guild_lock.read(); + match op.to_lowercase().as_str() { + "enable" => { + if let Ok(_) = db.new_premium(guild_id.0 as i64) { + message.channel_id.say(format!("{} is now premium !", guild.name))?; + } + }, + "disable" => { + if let Ok(_) = db.del_premium(guild_id.0 as i64) { + message.channel_id.say(format!("{} is no longer premium...", guild.name))?; + } + }, + "set" => { + let mut prem = db.get_premium(guild_id.0 as i64)?; + prem.tier = args.single::<i32>()?; + let pr = db.update_premium(guild_id.0 as i64, prem)?; + message.channel_id.say(format!("Updated premium tier for {} to {}.", guild.name, pr.tier))?; + }, + "show" => { + if let Ok(prem) = db.get_premium(guild_id.0 as i64) { // mut + // TODO: add impl Display for PremiumSettings + message.channel_id.say(format!("{:?}", prem))?; + } + }, + _ => {}, + } + } + Ok(()) + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..82b6da3 --- /dev/null +++ b/src/modules/mod.rs @@ -0,0 +1 @@ +pub mod commands; diff --git a/src/wisp.rs b/src/wisp.rs new file mode 100644 index 0000000..f3ffd9b --- /dev/null +++ b/src/wisp.rs @@ -0,0 +1,60 @@ +use crate::core::{ + api, + handler::Handler, + model::*, + framework::WispFramework, + timers +}; +use serenity::Error as SerenityError; +use serenity::http; +use serenity::model::id::UserId; +use serenity::prelude::{Client, Mutex}; +use std::collections::HashSet; +use std::env; +use std::sync::Arc; + +pub struct WispClient(Client); +impl WispClient { + pub fn new() -> Self { + let token = env::var("DISCORD_TOKEN").expect("Expected Discord bot token in environment."); + let mut client = Client::new(&token, Handler).expect("Unable to initialize Wisp client."); + { + let mut data = client.data.lock(); + let api_client = api::ApiClient::new(); + let tc = timers::TimerClient::new(); + data.insert::<SerenityShardManager>(Arc::clone(&client.shard_manager)); + data.insert::<ApiClient>(Arc::new(api_client)); + data.insert::<TC>(Arc::new(Mutex::new(tc))); + } + let owners = match http::get_current_application_info() { + Ok(info) => { + let mut set = HashSet::new(); + let mut data = client.data.lock(); + data.insert::<Owner>(info.owner.id); + set.insert(info.owner.id); + set + }, + Err(why) => panic!("Couldn't get the application info: {:?}", why), + }; + client.with_framework(WispFramework::new(owners)); + WispClient(client) + } + + pub fn new_with_owners(owners: HashSet<UserId>) -> Self { + let token = env::var("DISCORD_TOKEN").expect("Expected Discord bot token in environment."); + let mut client = Client::new(&token, Handler).expect("Unable to initialize Wisp client."); + { + let mut data = client.data.lock(); + let api_client = api::ApiClient::new(); + let tc = timers::TimerClient::new(); + data.insert::<SerenityShardManager>(Arc::clone(&client.shard_manager)); + data.insert::<ApiClient>(Arc::new(api_client)); + data.insert::<TC>(Arc::new(Mutex::new(tc))); + } + client.with_framework(WispFramework::new(owners)); + WispClient(client) + } + + pub fn start(&mut self) -> Result<(), SerenityError> { self.start_autosharded() } + pub fn start_autosharded(&mut self) -> Result<(), SerenityError> { self.0.start_autosharded() } +} |