diff --git a/src/commands/challenge.rs b/src/commands/challenge.rs index dd53755..0e5414f 100644 --- a/src/commands/challenge.rs +++ b/src/commands/challenge.rs @@ -2,117 +2,14 @@ use serenity::framework::standard::{macros::command, Args, CommandResult}; use serenity::model::prelude::*; use serenity::prelude::*; use serenity::http::typing::Typing; - use std::env; use std::fs; - use serde_json::Map; -use serde_json::Value; use std::fs::File; -use std::fs::OpenOptions; -use std::io::Read; use std::io::Write; use std::path::Path; -use std::process::Command; - use slug::slugify; - -use crate::commands::owner::get_guild_data; - -pub fn get_challenge_number() -> i32 { - let challenge_dir = format!("{}/content/challenges", env::var("HUGO").unwrap()); - let paths = fs::read_dir(challenge_dir).unwrap(); - let mut max = 0; - for path in paths { - let number = path - .unwrap() - .path() - .file_stem() - .unwrap() - .to_str() - .unwrap() - .parse::() - .unwrap(); - if number > max { - max = number; - } - } - max -} - -fn get_hugo_path() -> String { - env::var("HUGO").unwrap() -} - -fn get_submission_images_dir() -> String { - format!("{}/assets/{}", get_hugo_path(), get_challenge_number()) -} - -fn get_submission_data_path() -> String { - format!( - "{}/data/challenges/{}.json", - get_hugo_path(), - get_challenge_number() - ) -} - -fn get_submission_data() -> Vec { - let submission_data_path = get_submission_data_path(); - let submission_data_json = match File::open(&submission_data_path) { - Ok(mut file) => { - let mut json = String::new(); - file.read_to_string(&mut json).unwrap(); - json - } - Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => { - let mut file = File::create(&submission_data_path).unwrap(); - file.write_all(b"[]").unwrap(); - file.flush().unwrap(); - String::from("[]") - } - Err(_) => panic!("Failed to open submission data file"), - }; - let mut submission_data: Value = serde_json::from_str(&submission_data_json).unwrap(); - submission_data.as_array_mut().unwrap().clone() -} - -fn set_submission_data(submission_data: Vec) { - let submission_data: Value = submission_data.into(); - let mut submission_data_file = OpenOptions::new() - .write(true) - .truncate(true) - .open(get_submission_data_path()) - .unwrap(); - submission_data_file - .write_all( - serde_json::to_string_pretty(&submission_data) - .unwrap() - .as_bytes(), - ) - .unwrap(); -} - -fn is_matching_submission(submission: &Value, msg: &Message) -> bool { - submission["id"].as_str().unwrap() == msg.author.id.as_u64().to_string() -} - -fn to_fullwidth(string: &str) -> String { - let mut fullwidth = String::new(); - for character in string.chars() { - fullwidth.push(match unicode_hfwidth::to_fullwidth(character) { - Some(character) => character, - None => character, - }); - } - fullwidth -} - -pub fn rebuild_site() { - Command::new("./build.sh") - .current_dir(get_hugo_path()) - .spawn() - .expect("Failed to rebuild site"); -} +use crate::utils::*; #[command] async fn challenge(ctx: &Context, msg: &Message) -> CommandResult { diff --git a/src/commands/kanji.rs b/src/commands/kanji.rs index e4b5822..3a1bce8 100644 --- a/src/commands/kanji.rs +++ b/src/commands/kanji.rs @@ -1,97 +1,7 @@ use serenity::framework::standard::{macros::command, Args, CommandResult}; use serenity::model::prelude::*; use serenity::prelude::*; - -use serde_json::Map; -use serde_json::Value; -use std::fs::File; -use std::io::Read; - -use rand::seq::IteratorRandom; - -fn random_from_string(string: &str) -> char { - string.chars().choose(&mut rand::thread_rng()).unwrap() -} - -fn get_so_diagram(kanji: char) -> String { - format!( - "https://raw.githubusercontent.com/mistval/kanji_images/master/gifs/{}.gif", - format!("{:x}", kanji as i32) - ) -} - -async fn display_kanji(ctx: &Context, msg: &Message, kanji: char, comment: &str) -> CommandResult { - msg.reply(&ctx.http, format!("{}{}", kanji, comment)) - .await?; - let url = get_so_diagram(kanji); - let client = reqwest::Client::new(); - let request = client.head(&url).build().unwrap(); - let response = client.execute(request).await?.status(); - let link_validated = response != reqwest::StatusCode::NOT_FOUND; - msg.channel_id - .say( - &ctx.http, - if link_validated { - &url - } else { - "The stroke order diagram for this kanji is unavailable." - }, - ) - .await?; - Ok(()) -} - -fn get_lists_data() -> Map { - let mut lists_file = File::open("kanji_lists.json").unwrap(); - let mut lists_json = String::new(); - lists_file.read_to_string(&mut lists_json).unwrap(); - let lists_data: Value = serde_json::from_str(&lists_json).unwrap(); - lists_data.as_object().unwrap().clone() -} - -fn get_kanji_info(kanji: char) -> String { - let lists_data = get_lists_data(); - let mut info = String::from(""); - for category_name in lists_data.keys() { - let category = &lists_data[category_name]; - let default_version = category["default"].as_str().unwrap(); - match &category["versions"][default_version]["characters"].as_str() { - // if no variants (string) - Some(list) => { - if list.contains(kanji) { - info.push_str(&format!("**{}**, ", category_name)); - } - } - // if variants (map) - None => { - let variants = &category["versions"][default_version]["characters"] - .as_object() - .unwrap(); - let mut in_variants = false; - for variant in variants.keys() { - let list = variants[variant].as_str().unwrap(); - if list.contains(kanji) { - if !in_variants { - info.push_str(&format!("**{}** (", category_name)); - in_variants = true; - } - info.push_str(&format!("{}, ", variant)); - } - } - if in_variants { - // Remove last two characters (comma and space) - info.pop(); - info.pop(); - info.push_str("), "); - } - } - }; - } - // Remove last two characters again - info.pop(); - info.pop(); - info -} +use crate::utils::*; #[command] async fn i(ctx: &Context, msg: &Message, args: Args) -> CommandResult { @@ -159,77 +69,6 @@ async fn i(ctx: &Context, msg: &Message, args: Args) -> CommandResult { Ok(()) } -async fn random_kanji( - category: &str, - ctx: &Context, - msg: &Message, - mut args: Args, -) -> CommandResult { - let lists_data = get_lists_data(); - let category = &lists_data[category]; - let default_version = category["default"].as_str().unwrap(); - let list = &category["versions"][default_version]["characters"]; - match list.as_str() { - Some(string) => { - let kanji = random_from_string(string); - display_kanji(&ctx, &msg, kanji, "").await?; - } - None => { - let subcategory = args.single::(); - let subcategories = list.as_object().unwrap(); - let subcategory_list = { - let mut string = String::from("\n"); - let mut total_count = 0; - for subcategory in subcategories.keys() { - let subcategory = subcategory.as_str(); - // One has to do .chars().count() as opposed to .len() here, - // because .len() returns the byte length NOT the number of characters. - // For kanji, they are always multi-byte Unicode characters. - let count = &subcategories[subcategory].as_str().unwrap().chars().count(); - total_count += count; - string.push_str(&format!("**{}** ({} kanji),\n", subcategory, count)); - } - string.push_str(&format!( - "or **all** ({} kanji total) for all subcategories", - total_count - )); - string - }; - match subcategory { - Ok(string) => { - let string = string.to_uppercase(); - if string == "ALL" { - let subcategory_key = subcategories - .keys() - .choose(&mut rand::thread_rng()) - .unwrap(); - let list = subcategories[subcategory_key].as_str().unwrap(); - let kanji = random_from_string(&list); - display_kanji(&ctx, &msg, kanji, &format!(", **{}**", subcategory_key)) - .await?; - } else if subcategories.contains_key(&string) { - let list = list[&string].as_str().unwrap(); - let kanji = random_from_string(&list); - display_kanji(&ctx, &msg, kanji, "").await?; - } else { - let message = format!( - "That is an invalid subcategory. Please use {}.", - &subcategory_list - ); - msg.reply(&ctx.http, message).await?; - } - } - Err(_) => { - let mut message = String::from("Please specify a subcategory: "); - message.push_str(&subcategory_list); - msg.reply(&ctx.http, message).await?; - } - } - } - } - Ok(()) -} - #[command] async fn joyo(ctx: &Context, msg: &Message, args: Args) -> CommandResult { random_kanji("JOYO", ctx, msg, args).await diff --git a/src/commands/owner.rs b/src/commands/owner.rs index de125e8..9543b3e 100644 --- a/src/commands/owner.rs +++ b/src/commands/owner.rs @@ -3,58 +3,11 @@ use serenity::model::prelude::*; use serenity::prelude::*; use serde_json::json; -use serde_json::Map; -use serde_json::Value; use std::env; -use std::fs::File; -use std::fs::OpenOptions; -use std::io::Read; -use std::io::Write; -use crate::commands::challenge::get_challenge_number; -use crate::commands::challenge::rebuild_site; +use crate::utils::*; use crate::ShardManagerContainer; -fn get_guild_data_path() -> String { - env::var("GUILD_DATA").unwrap() -} - -pub fn get_guild_data() -> Map { - let guild_data_path = get_guild_data_path(); - let guild_data_json = match File::open(&guild_data_path) { - Ok(mut file) => { - let mut json = String::new(); - file.read_to_string(&mut json).unwrap(); - json - } - Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => { - let mut file = File::create(&guild_data_path).unwrap(); - file.write_all(b"{}").unwrap(); - file.flush().unwrap(); - String::from("{}") - } - Err(_) => panic!("Failed to open guild data file"), - }; - let mut submission_data: Value = serde_json::from_str(&guild_data_json).unwrap(); - submission_data.as_object_mut().unwrap().clone() -} - -fn set_guild_data(guild_data: Map) { - let guild_data: Value = guild_data.into(); - let mut guild_data_file = OpenOptions::new() - .write(true) - .truncate(true) - .open(get_guild_data_path()) - .unwrap(); - guild_data_file - .write_all( - serde_json::to_string_pretty(&guild_data) - .unwrap() - .as_bytes(), - ) - .unwrap(); -} - #[command] #[owners_only] async fn sleep(ctx: &Context, msg: &Message) -> CommandResult { @@ -135,58 +88,6 @@ async fn setAnnouncementRole(ctx: &Context, msg: &Message, mut args: Args) -> Co Ok(()) } -async fn send(ctx: &Context, msg: &Message, message: &str, ping: bool, pin: bool) -> CommandResult { - let guild_data = get_guild_data(); - let mut announcements_count = 0; - for (_guild, data) in guild_data.iter() { - let data = data.as_object().unwrap(); - if !data.contains_key("submissionChannel") { - continue; - } - let channel = ChannelId( - data["submissionChannel"] - .as_str() - .unwrap() - .parse::() - .unwrap(), - ); - let mut message_to_send = String::from(""); - if ping && data.contains_key("announcementRole") { - message_to_send.push_str(&format!( - "<@&{}> ", - data["announcementRole"].as_str().unwrap() - )); - } - message_to_send.push_str(message); - let sent_message = channel - .send_message(&ctx.http, |e| { - e.content(message_to_send); - e - }) - .await - .unwrap(); - if pin { - // No need to do anything on error, - // it just means we don't have pin permissions - match sent_message.pin(&ctx.http).await { - Ok(_) => (), - Err(_) => (), - }; - } - announcements_count += 1; - } - msg.reply( - &ctx.http, - format!( - "Announced to {} server{}!", - announcements_count, - if announcements_count == 1 { "" } else { "s" } - ), - ) - .await?; - Ok(()) -} - #[command] #[owners_only] async fn announce(ctx: &Context, msg: &Message, args: Args) -> CommandResult { diff --git a/src/main.rs b/src/main.rs index d4af6e3..e111810 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ //! features = ["framework", "standard_framework"] //! ``` mod commands; +mod utils; use std::{collections::HashSet, env, sync::Arc}; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..1082c54 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,357 @@ +use serenity::framework::standard::{Args, CommandResult}; +use serenity::model::prelude::*; +use serenity::prelude::*; +use std::fs; +use serde_json::Map; +use serde_json::Value; +use std::fs::File; +use std::fs::OpenOptions; +use std::io::Read; +use std::io::Write; +use std::process::Command; +use rand::seq::IteratorRandom; +use std::env; + +pub fn get_challenge_number() -> i32 { + let challenge_dir = format!("{}/content/challenges", env::var("HUGO").unwrap()); + let paths = fs::read_dir(challenge_dir).unwrap(); + let mut max = 0; + for path in paths { + let number = path + .unwrap() + .path() + .file_stem() + .unwrap() + .to_str() + .unwrap() + .parse::() + .unwrap(); + if number > max { + max = number; + } + } + max +} + +pub fn get_hugo_path() -> String { + env::var("HUGO").unwrap() +} + +pub fn get_submission_images_dir() -> String { + format!("{}/assets/{}", get_hugo_path(), get_challenge_number()) +} + +pub fn get_submission_data_path() -> String { + format!( + "{}/data/challenges/{}.json", + get_hugo_path(), + get_challenge_number() + ) +} + +pub fn get_submission_data() -> Vec { + let submission_data_path = get_submission_data_path(); + let submission_data_json = match File::open(&submission_data_path) { + Ok(mut file) => { + let mut json = String::new(); + file.read_to_string(&mut json).unwrap(); + json + } + Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => { + let mut file = File::create(&submission_data_path).unwrap(); + file.write_all(b"[]").unwrap(); + file.flush().unwrap(); + String::from("[]") + } + Err(_) => panic!("Failed to open submission data file"), + }; + let mut submission_data: Value = serde_json::from_str(&submission_data_json).unwrap(); + submission_data.as_array_mut().unwrap().clone() +} + +pub fn set_submission_data(submission_data: Vec) { + let submission_data: Value = submission_data.into(); + let mut submission_data_file = OpenOptions::new() + .write(true) + .truncate(true) + .open(get_submission_data_path()) + .unwrap(); + submission_data_file + .write_all( + serde_json::to_string_pretty(&submission_data) + .unwrap() + .as_bytes(), + ) + .unwrap(); +} + +pub fn is_matching_submission(submission: &Value, msg: &Message) -> bool { + submission["id"].as_str().unwrap() == msg.author.id.as_u64().to_string() +} + +pub fn to_fullwidth(string: &str) -> String { + let mut fullwidth = String::new(); + for character in string.chars() { + fullwidth.push(match unicode_hfwidth::to_fullwidth(character) { + Some(character) => character, + None => character, + }); + } + fullwidth +} + +pub fn rebuild_site() { + Command::new("./build.sh") + .current_dir(get_hugo_path()) + .spawn() + .expect("Failed to rebuild site"); +} + + +pub fn get_guild_data_path() -> String { + env::var("GUILD_DATA").unwrap() +} + +pub fn get_guild_data() -> Map { + let guild_data_path = get_guild_data_path(); + let guild_data_json = match File::open(&guild_data_path) { + Ok(mut file) => { + let mut json = String::new(); + file.read_to_string(&mut json).unwrap(); + json + } + Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => { + let mut file = File::create(&guild_data_path).unwrap(); + file.write_all(b"{}").unwrap(); + file.flush().unwrap(); + String::from("{}") + } + Err(_) => panic!("Failed to open guild data file"), + }; + let mut submission_data: Value = serde_json::from_str(&guild_data_json).unwrap(); + submission_data.as_object_mut().unwrap().clone() +} + +pub fn set_guild_data(guild_data: Map) { + let guild_data: Value = guild_data.into(); + let mut guild_data_file = OpenOptions::new() + .write(true) + .truncate(true) + .open(get_guild_data_path()) + .unwrap(); + guild_data_file + .write_all( + serde_json::to_string_pretty(&guild_data) + .unwrap() + .as_bytes(), + ) + .unwrap(); +} + +pub async fn send(ctx: &Context, msg: &Message, message: &str, ping: bool, pin: bool) -> CommandResult { + let guild_data = get_guild_data(); + let mut announcements_count = 0; + for (_guild, data) in guild_data.iter() { + let data = data.as_object().unwrap(); + if !data.contains_key("submissionChannel") { + continue; + } + let channel = ChannelId( + data["submissionChannel"] + .as_str() + .unwrap() + .parse::() + .unwrap(), + ); + let mut message_to_send = String::from(""); + if ping && data.contains_key("announcementRole") { + message_to_send.push_str(&format!( + "<@&{}> ", + data["announcementRole"].as_str().unwrap() + )); + } + message_to_send.push_str(message); + let sent_message = channel + .send_message(&ctx.http, |e| { + e.content(message_to_send); + e + }) + .await + .unwrap(); + if pin { + // No need to do anything on error, + // it just means we don't have pin permissions + match sent_message.pin(&ctx.http).await { + Ok(_) => (), + Err(_) => (), + }; + } + announcements_count += 1; + } + msg.reply( + &ctx.http, + format!( + "Announced to {} server{}!", + announcements_count, + if announcements_count == 1 { "" } else { "s" } + ), + ) + .await?; + Ok(()) +} + +pub fn random_from_string(string: &str) -> char { + string.chars().choose(&mut rand::thread_rng()).unwrap() +} + +pub fn get_so_diagram(kanji: char) -> String { + format!( + "https://raw.githubusercontent.com/mistval/kanji_images/master/gifs/{}.gif", + format!("{:x}", kanji as i32) + ) +} + + +pub async fn display_kanji(ctx: &Context, msg: &Message, kanji: char, comment: &str) -> CommandResult { + msg.reply(&ctx.http, format!("{}{}", kanji, comment)) + .await?; + let url = get_so_diagram(kanji); + let client = reqwest::Client::new(); + let request = client.head(&url).build().unwrap(); + let response = client.execute(request).await?.status(); + let link_validated = response != reqwest::StatusCode::NOT_FOUND; + msg.channel_id + .say( + &ctx.http, + if link_validated { + &url + } else { + "The stroke order diagram for this kanji is unavailable." + }, + ) + .await?; + Ok(()) +} + +pub fn get_lists_data() -> Map { + let mut lists_file = File::open("kanji_lists.json").unwrap(); + let mut lists_json = String::new(); + lists_file.read_to_string(&mut lists_json).unwrap(); + let lists_data: Value = serde_json::from_str(&lists_json).unwrap(); + lists_data.as_object().unwrap().clone() +} + +pub fn get_kanji_info(kanji: char) -> String { + let lists_data = get_lists_data(); + let mut info = String::from(""); + for category_name in lists_data.keys() { + let category = &lists_data[category_name]; + let default_version = category["default"].as_str().unwrap(); + match &category["versions"][default_version]["characters"].as_str() { + // if no variants (string) + Some(list) => { + if list.contains(kanji) { + info.push_str(&format!("**{}**, ", category_name)); + } + } + // if variants (map) + None => { + let variants = &category["versions"][default_version]["characters"] + .as_object() + .unwrap(); + let mut in_variants = false; + for variant in variants.keys() { + let list = variants[variant].as_str().unwrap(); + if list.contains(kanji) { + if !in_variants { + info.push_str(&format!("**{}** (", category_name)); + in_variants = true; + } + info.push_str(&format!("{}, ", variant)); + } + } + if in_variants { + // Remove last two characters (comma and space) + info.pop(); + info.pop(); + info.push_str("), "); + } + } + }; + } + // Remove last two characters again + info.pop(); + info.pop(); + info +} + +pub async fn random_kanji( + category: &str, + ctx: &Context, + msg: &Message, + mut args: Args, +) -> CommandResult { + let lists_data = get_lists_data(); + let category = &lists_data[category]; + let default_version = category["default"].as_str().unwrap(); + let list = &category["versions"][default_version]["characters"]; + match list.as_str() { + Some(string) => { + let kanji = random_from_string(string); + display_kanji(&ctx, &msg, kanji, "").await?; + } + None => { + let subcategory = args.single::(); + let subcategories = list.as_object().unwrap(); + let subcategory_list = { + let mut string = String::from("\n"); + let mut total_count = 0; + for subcategory in subcategories.keys() { + let subcategory = subcategory.as_str(); + // One has to do .chars().count() as opposed to .len() here, + // because .len() returns the byte length NOT the number of characters. + // For kanji, they are always multi-byte Unicode characters. + let count = &subcategories[subcategory].as_str().unwrap().chars().count(); + total_count += count; + string.push_str(&format!("**{}** ({} kanji),\n", subcategory, count)); + } + string.push_str(&format!( + "or **all** ({} kanji total) for all subcategories", + total_count + )); + string + }; + match subcategory { + Ok(string) => { + let string = string.to_uppercase(); + if string == "ALL" { + let subcategory_key = subcategories + .keys() + .choose(&mut rand::thread_rng()) + .unwrap(); + let list = subcategories[subcategory_key].as_str().unwrap(); + let kanji = random_from_string(&list); + display_kanji(&ctx, &msg, kanji, &format!(", **{}**", subcategory_key)) + .await?; + } else if subcategories.contains_key(&string) { + let list = list[&string].as_str().unwrap(); + let kanji = random_from_string(&list); + display_kanji(&ctx, &msg, kanji, "").await?; + } else { + let message = format!( + "That is an invalid subcategory. Please use {}.", + &subcategory_list + ); + msg.reply(&ctx.http, message).await?; + } + } + Err(_) => { + let mut message = String::from("Please specify a subcategory: "); + message.push_str(&subcategory_list); + msg.reply(&ctx.http, message).await?; + } + } + } + } + Ok(()) +}