From 531cfedc171d3f2fbb2d5485c01072930858b725 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sat, 26 Feb 2022 18:18:52 -0800 Subject: [PATCH 1/8] Fix link in challenge announcement message --- src/commands/owner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/owner.rs b/src/commands/owner.rs index d6aced0..5ebf274 100644 --- a/src/commands/owner.rs +++ b/src/commands/owner.rs @@ -197,7 +197,7 @@ async fn announce(ctx: &Context, msg: &Message, args: Args) -> CommandResult { #[allow(non_snake_case)] async fn announceChallenge(ctx: &Context, msg: &Message) -> CommandResult { let challenge_number = get_challenge_number(); - let message = format!("Welcome to the **{n}{th}** weekly **Tegaki Tuesday** (手書きの火曜日) handwriting challenge! :pen_fountain: The prompt is available in both Japanese and English on the website at . + let message = format!("Welcome to the **{n}{th}** weekly **Tegaki Tuesday** (手書きの火曜日) handwriting challenge! :pen_fountain: The prompt is available in both Japanese and English on the website at . You can make submissions in both languages, but please submit in your target language first. Submissions can be submitted by uploading the image to this channel along with the `{p}submit` command. By submitting, you agree to having your work posted to the website under the Attribution-ShareAlike 4.0 Unported (CC BY-SA 4.0) license, attributed to your Discord account. ().", n = challenge_number, From 9295fb48f8cb55f7715706302012e97b53373797 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sat, 26 Feb 2022 18:20:12 -0800 Subject: [PATCH 2/8] Add unrecognised command message, closes #1 --- src/commands/meta.rs | 10 ++++++++++ src/main.rs | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/commands/meta.rs b/src/commands/meta.rs index a32fc8f..0bfc22b 100644 --- a/src/commands/meta.rs +++ b/src/commands/meta.rs @@ -1,6 +1,7 @@ use serenity::framework::standard::{macros::command, CommandResult}; use serenity::model::prelude::*; use serenity::prelude::*; +use serenity::framework::standard::macros::hook; use std::env; @@ -31,3 +32,12 @@ __**Kanji 漢字**__ msg.reply(&ctx.http, message).await?; Ok(()) } + +#[hook] +pub async fn unrecognised_command_hook( + ctx: &Context, + msg: &Message, + unrecognised_command_name: &str, +) { + msg.reply(&ctx.http, &format!("I don't understand the command '{}'. For a list of commands, see `{}help`. Commands are case-sensitive.", unrecognised_command_name, env::var("PREFIX").unwrap())).await.unwrap(); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 55e411e..ae713d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -97,7 +97,8 @@ async fn main() { // Create the framework let framework = StandardFramework::new() .configure(|c| c.owners(owners).prefix(prefix)) - .group(&GENERAL_GROUP); + .group(&GENERAL_GROUP) + .unrecognised_command(commands::meta::unrecognised_command_hook); let mut client = Client::builder(&token) .framework(framework) From a92872bdd0b1214a92b0b24722ac8f7a4dd593a8 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sat, 26 Feb 2022 18:34:39 -0800 Subject: [PATCH 3/8] Add typing status to submission command, closes #4 --- src/commands/challenge.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands/challenge.rs b/src/commands/challenge.rs index 378af13..7a09707 100644 --- a/src/commands/challenge.rs +++ b/src/commands/challenge.rs @@ -1,6 +1,7 @@ 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; @@ -186,6 +187,7 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult { .await?; return Ok(()); } + let typing = Typing::start(ctx.http.clone(), *msg.channel_id.as_u64()).unwrap(); let challenge_number = get_challenge_number(); let submission_images_dir = get_submission_images_dir(); @@ -295,6 +297,7 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult { } else if invalid_types { message.push_str("Sorry, your submission could not be uploaded; only **.png**, **.jpg**, and **.jpeg** files are permitted."); } + typing.stop(); msg.reply(&ctx.http, message).await?; Ok(()) } From aff6e7f2cf89c3558c16ef00c2aeec69e6011ff9 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sun, 27 Feb 2022 16:14:58 -0800 Subject: [PATCH 4/8] Set activity on start, closes #10 --- src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index ae713d8..bbbf9aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,8 @@ mod commands; use std::{collections::HashSet, env, sync::Arc}; use commands::{challenge::*, kanji::*, meta::*, owner::*}; +use serenity::model::gateway::Activity; +use serenity::model::gateway::ActivityEmoji; use serenity::{ async_trait, client::bridge::gateway::ShardManager, @@ -33,8 +35,10 @@ struct Handler; #[async_trait] impl EventHandler for Handler { - async fn ready(&self, _: Context, ready: Ready) { + async fn ready(&self, ctx: Context, ready: Ready) { info!("Connected as {}", ready.user.name); + let activity = Activity::watching("for new submissions"); + ctx.set_activity(activity).await; } async fn resume(&self, _: Context, _: ResumedEvent) { From 69e261bd2c78846309c76c218d82e4114a56bbf3 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sun, 27 Feb 2022 16:17:57 -0800 Subject: [PATCH 5/8] Remove unused use statement --- src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index bbbf9aa..080ad37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,6 @@ use std::{collections::HashSet, env, sync::Arc}; use commands::{challenge::*, kanji::*, meta::*, owner::*}; use serenity::model::gateway::Activity; -use serenity::model::gateway::ActivityEmoji; use serenity::{ async_trait, client::bridge::gateway::ShardManager, From 8d7cb72423a7c7d267e48c805e6c9701fafc6dd2 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sun, 27 Feb 2022 16:19:55 -0800 Subject: [PATCH 6/8] Remove pullAndRebuildSite command, Already up to date. can be run in site build script, closes #9 --- src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 080ad37..d4af6e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,7 +55,6 @@ impl EventHandler for Handler { hyogai, so, rebuildSite, - pullAndRebuildSite, challenge, submit, images, From 49f41d0770892bf654a25cc19e8591c8326a7b52 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sun, 27 Feb 2022 16:23:11 -0800 Subject: [PATCH 7/8] Move rebuildSite command into owner.rs --- src/commands/challenge.rs | 28 +--------------------------- src/commands/owner.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/commands/challenge.rs b/src/commands/challenge.rs index 7a09707..dd53755 100644 --- a/src/commands/challenge.rs +++ b/src/commands/challenge.rs @@ -107,39 +107,13 @@ fn to_fullwidth(string: &str) -> String { fullwidth } -fn rebuild_site() { +pub fn rebuild_site() { Command::new("./build.sh") .current_dir(get_hugo_path()) .spawn() .expect("Failed to rebuild site"); } -#[command] -#[owners_only] -#[allow(non_snake_case)] -async fn rebuildSite(ctx: &Context, msg: &Message) -> CommandResult { - rebuild_site(); - msg.reply(&ctx.http, "Started site rebuild process!") - .await?; - Ok(()) -} - -#[command] -#[owners_only] -#[allow(non_snake_case)] -async fn pullAndRebuildSite(ctx: &Context, msg: &Message) -> CommandResult { - Command::new("git") - .current_dir(get_hugo_path()) - .arg("pull") - .spawn() - .expect("Failed to git pull") - .wait()?; - rebuild_site(); - msg.reply(&ctx.http, "Pulled and started site rebuild process!") - .await?; - Ok(()) -} - #[command] async fn challenge(ctx: &Context, msg: &Message) -> CommandResult { msg.reply( diff --git a/src/commands/owner.rs b/src/commands/owner.rs index 5ebf274..de125e8 100644 --- a/src/commands/owner.rs +++ b/src/commands/owner.rs @@ -12,6 +12,7 @@ use std::io::Read; use std::io::Write; use crate::commands::challenge::get_challenge_number; +use crate::commands::challenge::rebuild_site; use crate::ShardManagerContainer; fn get_guild_data_path() -> String { @@ -211,3 +212,13 @@ You can make submissions in both languages, but please submit in your target lan ); send(ctx, msg, &message, true, true).await } + +#[command] +#[owners_only] +#[allow(non_snake_case)] +async fn rebuildSite(ctx: &Context, msg: &Message) -> CommandResult { + rebuild_site(); + msg.reply(&ctx.http, "Started site rebuild process!") + .await?; + Ok(()) +} \ No newline at end of file From faf2a38400c7580ac785e39cfe1626de74a9d338 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Sun, 27 Feb 2022 16:36:51 -0800 Subject: [PATCH 8/8] Move all non-command functions in commands folder into utils file, closes #11 --- src/commands/challenge.rs | 105 +---------- src/commands/kanji.rs | 163 +---------------- src/commands/owner.rs | 101 +---------- src/main.rs | 1 + src/utils.rs | 357 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 361 insertions(+), 366 deletions(-) create mode 100644 src/utils.rs 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(()) +}