Move to poise, add slash commands

late-submissions
Elnu 2 years ago
parent d397bb7117
commit 9e9eb48025

140
Cargo.lock generated

@ -124,17 +124,6 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "command_attr"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d999d4e7731150ee14aee8f619c7a9aa9a4385bca0606c4fa95aa2f36a05d9a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.3" version = "0.9.3"
@ -223,6 +212,41 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "darling"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.4.0" version = "5.4.0"
@ -237,6 +261,17 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "deunicode" name = "deunicode"
version = "0.4.3" version = "0.4.3"
@ -551,6 +586,12 @@ dependencies = [
"cxx-build", "cxx-build",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.3.0" version = "0.3.0"
@ -598,10 +639,10 @@ version = "0.1.0"
dependencies = [ dependencies = [
"dotenv", "dotenv",
"fs_extra", "fs_extra",
"poise",
"rand", "rand",
"reqwest", "reqwest",
"serde_json", "serde_json",
"serenity",
"slug", "slug",
"tokio", "tokio",
"tracing", "tracing",
@ -624,12 +665,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "levenshtein"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.137" version = "0.2.137"
@ -877,6 +912,37 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "poise"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aee439543df35482730552e7c9ed0c45a5f1d521548e6c0249967c4ba8828f60"
dependencies = [
"async-trait",
"derivative",
"futures-core",
"futures-util",
"log",
"once_cell",
"parking_lot",
"poise_macros",
"regex",
"serenity",
"tokio",
]
[[package]]
name = "poise_macros"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d21213ff7aeef5ab69729a5cddfb351a84a9bf3dadf9f470032440d43746c2"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -940,6 +1006,21 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "regex"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.3" version = "0.5.3"
@ -1029,6 +1110,12 @@ dependencies = [
"base64", "base64",
] ]
[[package]]
name = "rustversion"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.11" version = "1.0.11"
@ -1156,26 +1243,23 @@ dependencies = [
"bytes", "bytes",
"cfg-if", "cfg-if",
"chrono", "chrono",
"command_attr",
"dashmap", "dashmap",
"flate2", "flate2",
"futures", "futures",
"levenshtein",
"mime", "mime",
"mime_guess", "mime_guess",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"reqwest", "reqwest",
"rustversion",
"serde", "serde",
"serde-value", "serde-value",
"serde_json", "serde_json",
"static_assertions",
"time", "time",
"tokio", "tokio",
"tracing", "tracing",
"typemap_rev", "typemap_rev",
"url", "url",
"uwl",
] ]
[[package]] [[package]]
@ -1248,10 +1332,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "static_assertions" name = "strsim"
version = "1.1.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
@ -1593,12 +1677,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uwl"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

@ -15,11 +15,8 @@ reqwest = "0.11"
slug = "0.1" slug = "0.1"
unicode_hfwidth = "0.2" unicode_hfwidth = "0.2"
fs_extra = "1.2" fs_extra = "1.2"
poise = "0.4"
[dependencies.tokio] [dependencies.tokio]
version = "1.0" version = "1.0"
features = ["macros", "signal", "rt-multi-thread"] features = ["macros", "signal", "rt-multi-thread"]
[dependencies.serenity]
version = "0.11"
features = ["cache", "framework", "standard_framework", "rustls_backend"]

@ -1,20 +1,20 @@
use crate::utils::*; use crate::utils::*;
use serde_json::Map; use serde_json::Map;
use serenity::framework::standard::{macros::command, Args, CommandResult};
use serenity::http::typing::Typing; use crate::serenity;
use serenity::model::prelude::*; use crate::Error;
use serenity::prelude::*; use crate::{Context, PrefixContext};
use poise::command;
use slug::slugify; use slug::slugify;
use std::env;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
#[command] #[command(prefix_command, slash_command, description_localized("en-US", "View the latest handwriting challenge info."))]
async fn challenge(ctx: &Context, msg: &Message) -> CommandResult { pub async fn challenge(ctx: Context<'_>) -> Result<(), Error> {
msg.reply( ctx.say(
&ctx.http,
format!( format!(
"Tegaki Tuesday #{n}: <https://tegakituesday.com/{n}>", "Tegaki Tuesday #{n}: <https://tegakituesday.com/{n}>",
n = get_challenge_number() n = get_challenge_number()
@ -24,27 +24,26 @@ async fn challenge(ctx: &Context, msg: &Message) -> CommandResult {
Ok(()) Ok(())
} }
#[command] #[command(prefix_command, broadcast_typing, description_localized("en-US", "Submit to the latest handwriting challenge."))]
async fn submit(ctx: &Context, msg: &Message) -> CommandResult { pub async fn submit(ctx: PrefixContext<'_>) -> Result<(), Error> {
// TODO: The code for this command needs to be refactored, // TODO: The code for this command needs to be refactored,
// there are large duplicated sections that need to be merged somehow. // there are large duplicated sections that need to be merged somehow.
let guild_data = get_guild_data(); let guild_data = get_guild_data();
let guild = msg.guild_id.unwrap().as_u64().to_string(); let guild = ctx.msg.guild_id.unwrap().as_u64().to_string();
if !guild_data.contains_key(&guild) if !guild_data.contains_key(&guild)
|| !&guild_data[&guild] || !&guild_data[&guild]
.as_object() .as_object()
.unwrap() .unwrap()
.contains_key("submissionChannel") .contains_key("submissionChannel")
{ {
msg.reply(&ctx.http, "Submissions aren't enabled for this server yet.") ctx.msg.reply(&ctx.discord.http, "Submissions aren't enabled for this server yet.").await?;
.await?;
return Ok(()); return Ok(());
} }
let current_guild_data = &guild_data[&guild].as_object().unwrap(); let current_guild_data = &guild_data[&guild].as_object().unwrap();
let submission_channel = current_guild_data["submissionChannel"].as_str().unwrap(); let submission_channel = current_guild_data["submissionChannel"].as_str().unwrap();
if submission_channel != &msg.channel_id.as_u64().to_string() { if submission_channel != ctx.msg.channel_id.as_u64().to_string() {
msg.reply( ctx.msg.reply(
&ctx.http, &ctx.discord.http,
format!( format!(
"Sorry, submissions aren't permitted here. Please go to <#{}>. Thanks!", "Sorry, submissions aren't permitted here. Please go to <#{}>. Thanks!",
guild guild
@ -53,12 +52,10 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult {
.await?; .await?;
return Ok(()); return Ok(());
} }
if msg.attachments.len() == 0 { if ctx.msg.attachments.len() == 0 {
msg.reply(&ctx.http, "Please attach at least one image.") ctx.msg.reply(&ctx.discord.http, "Please attach at least one image.").await?;
.await?;
return Ok(()); return Ok(());
} }
let typing = Typing::start(ctx.http.clone(), *msg.channel_id.as_u64()).unwrap();
let challenge_number = get_challenge_number(); let challenge_number = get_challenge_number();
let submission_images_dir = get_submission_images_dir(); let submission_images_dir = get_submission_images_dir();
@ -71,10 +68,10 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult {
let mut invalid_types = false; let mut invalid_types = false;
let mut requires_rebuild = false; let mut requires_rebuild = false;
for (i, submission) in submission_data.iter_mut().enumerate() { for (i, submission) in submission_data.iter_mut().enumerate() {
if is_matching_submission(&submission, &msg) { if is_matching_submission(&submission, &ctx.msg.author) {
existing_submitter = true; existing_submitter = true;
let mut images = submission["images"].as_array_mut().unwrap().clone(); let mut images = submission["images"].as_array_mut().unwrap().clone();
for attachment in msg.attachments.iter() { for attachment in ctx.msg.attachments.iter() {
let extension; let extension;
if let Some(content_type) = &attachment.content_type { if let Some(content_type) = &attachment.content_type {
if content_type == "image/png" { if content_type == "image/png" {
@ -93,8 +90,8 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult {
let file_name = format!( let file_name = format!(
"{}-{}-{}-{}.{}", "{}-{}-{}-{}.{}",
i + 1, i + 1,
slugify(&msg.author.name), slugify(&ctx.msg.author.name),
msg.author.discriminator, ctx.msg.author.discriminator,
images.len() + 1, images.len() + 1,
extension extension
); );
@ -113,10 +110,10 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult {
let mut submitter_data = Map::new(); let mut submitter_data = Map::new();
submitter_data.insert( submitter_data.insert(
String::from("username"), String::from("username"),
format!("{}#{}", msg.author.name, msg.author.discriminator).into(), format!("{}#{}", ctx.msg.author.name, ctx.msg.author.discriminator).into(),
); );
let mut images: Vec<String> = Vec::new(); let mut images: Vec<String> = Vec::new();
for attachment in msg.attachments.iter() { for attachment in ctx.msg.attachments.iter() {
let extension; let extension;
if let Some(content_type) = &attachment.content_type { if let Some(content_type) = &attachment.content_type {
if content_type == "image/png" { if content_type == "image/png" {
@ -135,8 +132,8 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult {
let file_name = format!( let file_name = format!(
"{}-{}-{}{}.{}", "{}-{}-{}{}.{}",
submission_data.len() + 1, submission_data.len() + 1,
slugify(&msg.author.name), slugify(&ctx.msg.author.name),
msg.author.discriminator, ctx.msg.author.discriminator,
if images.len() == 0 { if images.len() == 0 {
String::from("") String::from("")
} else { } else {
@ -153,7 +150,7 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult {
submitter_data.insert(String::from("images"), images.into()); submitter_data.insert(String::from("images"), images.into());
submitter_data.insert( submitter_data.insert(
String::from("id"), String::from("id"),
msg.author.id.as_u64().to_string().into(), ctx.msg.author.id.as_u64().to_string().into(),
); );
submission_data.push(submitter_data.into()); submission_data.push(submitter_data.into());
} }
@ -169,18 +166,17 @@ async fn submit(ctx: &Context, msg: &Message) -> CommandResult {
} else if invalid_types { } else if invalid_types {
message.push_str("Sorry, your submission could not be uploaded; only **.png**, **.jpg**, and **.jpeg** files are permitted."); message.push_str("Sorry, your submission could not be uploaded; only **.png**, **.jpg**, and **.jpeg** files are permitted.");
} }
let _ = typing.stop(); ctx.msg.reply(&ctx.discord.http, message).await?;
msg.reply(&ctx.http, message).await?;
Ok(()) Ok(())
} }
#[command] #[command(prefix_command, slash_command, description_localized("en-US", "List images in your current submission, if available."))]
async fn images(ctx: &Context, msg: &Message) -> CommandResult { pub async fn images(ctx: Context<'_>) -> Result<(), Error> {
let submission_data = get_current_submission_data(); let submission_data = get_current_submission_data();
let images: Vec<String> = { let images: Vec<String> = {
let mut images = Vec::new(); let mut images = Vec::new();
for submission in submission_data.iter() { for submission in submission_data.iter() {
if is_matching_submission(&submission, &msg) { if is_matching_submission(&submission, &ctx.author()) {
for image in submission["images"].as_array().unwrap().iter() { for image in submission["images"].as_array().unwrap().iter() {
images.push(String::from(image.as_str().unwrap())); images.push(String::from(image.as_str().unwrap()));
} }
@ -191,8 +187,7 @@ async fn images(ctx: &Context, msg: &Message) -> CommandResult {
}; };
let challenge_number = get_challenge_number(); let challenge_number = get_challenge_number();
if images.len() == 0 { if images.len() == 0 {
msg.reply( ctx.say(
&ctx.http,
format!( format!(
"You haven't submitted anything for Tegaki Tuesday #{}.", "You haven't submitted anything for Tegaki Tuesday #{}.",
challenge_number challenge_number
@ -213,41 +208,29 @@ async fn images(ctx: &Context, msg: &Message) -> CommandResult {
image image
)); ));
} }
msg.reply(&ctx.http, message).await?; ctx.say(message).await?;
Ok(()) Ok(())
} }
#[command] #[command(prefix_command, slash_command, description_localized("en-US", "Delete images from your current submission using image numbers from the images command."))]
// imageDelete instead of image_delete to keep command naming from Python version pub async fn imagedelete(
#[allow(non_snake_case)] ctx: Context<'_>,
async fn imageDelete(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { number: i32,
let number; ) -> Result<(), Error> {
match args.single::<i32>() {
Ok(value) => number = value,
Err(_) => {
msg.reply(&ctx.http, format!("Please provide the image number you want to delete. You can get a list of your submitted images using `{}images`.", env::var("PREFIX").unwrap())).await?;
return Ok(());
}
}
if number < 1 { if number < 1 {
msg.reply( ctx.say("That isn't a valid image number. Image numbers start at 1.").await?;
&ctx.http,
"That isn't a valid image number. Image numbers start at 1.",
)
.await?;
return Ok(()); return Ok(());
} }
let challenge_number = get_challenge_number(); let challenge_number = get_challenge_number();
let mut submission_data = get_current_submission_data(); let mut submission_data = get_current_submission_data();
for (i, submission) in submission_data.iter_mut().enumerate() { for (i, submission) in submission_data.iter_mut().enumerate() {
if !is_matching_submission(&submission, &msg) { if !is_matching_submission(&submission, &ctx.author()) {
continue; continue;
} }
let mut images = submission["images"].as_array().unwrap().clone(); let mut images = submission["images"].as_array().unwrap().clone();
let image_count = images.len(); let image_count = images.len();
if image_count < number.try_into().unwrap() { if image_count < number.try_into().unwrap() {
msg.reply( ctx.say(
&ctx.http,
if image_count == 0 { if image_count == 0 {
// This is an edge case that should never happen. // This is an edge case that should never happen.
// In this scenario, there is submission data with an empty image list. // In this scenario, there is submission data with an empty image list.
@ -296,11 +279,12 @@ async fn imageDelete(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes
for (j, image) in images.iter_mut().enumerate() { for (j, image) in images.iter_mut().enumerate() {
let old = image.as_str().unwrap(); let old = image.as_str().unwrap();
let from = format!("{}/{}", submission_images_dir, old); let from = format!("{}/{}", submission_images_dir, old);
let author = ctx.author();
let new = format!( let new = format!(
"{}-{}-{}{}.{}", "{}-{}-{}{}.{}",
i + 1, i + 1,
slugify(&msg.author.name), slugify(&author.name),
msg.author.discriminator, author.discriminator,
if j == 0 { if j == 0 {
String::from("") String::from("")
} else { } else {
@ -317,11 +301,10 @@ async fn imageDelete(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes
} }
set_submission_data(submission_data); set_submission_data(submission_data);
rebuild_site(); rebuild_site();
msg.reply(&ctx.http, message).await?; ctx.say(message).await?;
return Ok(()); return Ok(());
} }
msg.reply( ctx.say(
&ctx.http,
format!( format!(
"You haven't submitted anything for Tegaki Tuesday #{}.", "You haven't submitted anything for Tegaki Tuesday #{}.",
challenge_number challenge_number
@ -331,11 +314,15 @@ async fn imageDelete(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes
Ok(()) Ok(())
} }
#[command] // TODO: make also slash command
#[allow(non_snake_case)] #[command(prefix_command, description_localized("en-US", "Make a suggestion for future challenge prompts!"))]
async fn suggest(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn suggest(
ctx: PrefixContext<'_>,
#[description = "Suggestion text. Please include passage and source."]
suggestion: String,
) -> Result<(), Error> {
let guild_data = get_guild_data(); let guild_data = get_guild_data();
let channel = ChannelId( let channel = serenity::ChannelId(
guild_data["suggestionChannel"] guild_data["suggestionChannel"]
.as_str() .as_str()
.unwrap() .unwrap()
@ -344,33 +331,32 @@ async fn suggest(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
); );
// User::accent_colour is only available via the REST API // User::accent_colour is only available via the REST API
// If we just do msg.author.accent_colour here, we will get None // If we just do msg.author.accent_colour here, we will get None
let accent_color = ctx let author = &ctx.msg.author;
.http let accent_color = ctx.discord.http
.get_user(*msg.author.id.as_u64()) .get_user(*author.id.as_u64())
.await .await
.unwrap() .unwrap()
.accent_colour; .accent_colour;
channel channel
.send_message(&ctx.http, |m| { .send_message(&ctx.discord.http, |m| {
m.allowed_mentions(|am| { m.allowed_mentions(|am| {
am.empty_parse(); am.empty_parse();
am am
}); });
m.embed(|e| { m.embed(|e| {
let username = format!("{}#{}", msg.author.name, msg.author.discriminator); let username = format!("{}#{}", author.name, author.discriminator);
e.title("New suggestion"); e.title("New suggestion");
e.description(format!( e.description(format!(
"{}\n\n[See original message]({}) ({})", "{}\n\n[See original message]({})",
args.rest(), suggestion,
msg.link(), ctx.msg.link(),
msg.guild(&ctx).unwrap().name
)); ));
let mut author = serenity::builder::CreateEmbedAuthor::default(); let mut embed_author = serenity::builder::CreateEmbedAuthor::default();
author embed_author
.icon_url(get_avatar(&msg.author)) .icon_url(get_avatar(&author))
.name(username) .name(username)
.url(format!("https://discord.com/users/{}", msg.author.id)); .url(format!("https://discord.com/users/{}", author.id));
e.set_author(author); e.set_author(embed_author);
if let Some(accent_color) = accent_color { if let Some(accent_color) = accent_color {
e.color(accent_color); e.color(accent_color);
} }
@ -380,6 +366,6 @@ async fn suggest(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
}) })
.await .await
.unwrap(); .unwrap();
msg.reply(&ctx.http, "Suggestion sent! Thank you for making a suggestion. If it is chosen to be used in a future challenge, you will be mentioned in the challenge description!").await?; ctx.msg.reply(&ctx.discord.http, "Suggestion sent! Thank you for making a suggestion. If it is chosen to be used in a future challenge, you will be mentioned in the challenge description!").await?;
Ok(()) Ok(())
} }

@ -1,11 +1,16 @@
use crate::utils::*; use crate::utils::*;
use serenity::framework::standard::{macros::command, Args, CommandResult};
use serenity::model::prelude::*;
use serenity::prelude::*;
#[command] use crate::serenity;
async fn i(ctx: &Context, msg: &Message, args: Args) -> CommandResult { use crate::Error;
let input = args.rest(); use crate::Context;
use poise::command;
#[command(slash_command, prefix_command, description_localized("en-US", "Get category info and links to Jisho for character(s), with stroke order for single characters."))]
pub async fn i(
ctx: Context<'_>,
#[description = "Input kanji to get info for"]
input: String,
) -> Result<(), Error> {
let chars = input.chars(); let chars = input.chars();
let mut message = String::from(""); let mut message = String::from("");
let mut covered_chars: Vec<char> = Vec::new(); let mut covered_chars: Vec<char> = Vec::new();
@ -40,9 +45,7 @@ async fn i(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
if skipped_chars == 1 { "" } else { "s" } if skipped_chars == 1 { "" } else { "s" }
)); ));
} }
msg.channel_id ctx.send(|m| {
.send_message(&ctx.http, |m| {
m.reference_message(msg);
m.allowed_mentions(|am| { m.allowed_mentions(|am| {
am.empty_parse(); am.empty_parse();
am am
@ -69,50 +72,61 @@ async fn i(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
Ok(()) Ok(())
} }
#[command] #[command(slash_command, prefix_command, description_localized("en-US", "Random Jōyō kanji"))]
async fn joyo(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn joyo(
random_kanji("JOYO", ctx, msg, args).await ctx: Context<'_>,
) -> Result<(), Error> {
random_kanji("JOYO", ctx, None).await
} }
#[command] #[command(slash_command, prefix_command, description_localized("en-US", "Random Jinmeiyō kanji"))]
async fn jinmeiyo(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn jinmeiyo(
random_kanji("JINMEIYO", ctx, msg, args).await ctx: Context<'_>,
)-> Result<(), Error> {
random_kanji("JINMEIYO", ctx, None).await
} }
#[command] #[command(slash_command, prefix_command, description_localized("en-US", "Random Kyōiku kanji"))]
async fn kyoiku(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn kyoiku(
random_kanji("KYOIKU", ctx, msg, args).await ctx: Context<'_>,
#[description = "Kyōiku subcategory. GRADE1, GRADE2, GRADE3, GRADE4, GRADE5, GRADE6, or ALL."]
subcategory: Option<String>,
) -> Result<(), Error> {
random_kanji("KYOIKU", ctx, subcategory).await
} }
#[command] #[command(slash_command, prefix_command, description_localized("en-US", "Random JLPT kanji"))]
async fn jlpt(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn jlpt(
random_kanji("JLPT", ctx, msg, args).await ctx: Context<'_>,
#[description = "JLPT subcategory. N1, N2, N3, N4, N5, or ALL"]
subcategory: Option<String>
) -> Result<(), Error> {
random_kanji("JLPT", ctx, subcategory).await
} }
#[command] #[command(slash_command, prefix_command, description_localized("en-US", "Random Hyōgai kanji"))]
async fn hyogai(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn hyogai(
random_kanji("HYOGAI", ctx, msg, args).await ctx: Context<'_>,
#[description = "Hyōgai subcategory. ELEMENTS, MAIN, or ALL."]
subcategory: Option<String>,
) -> Result<(), Error> {
random_kanji("HYOGAI", ctx, subcategory).await
} }
#[command] #[command(slash_command, prefix_command, description_localized("en-US", "Get stroke order diagrams for character(s), maximum 4"))]
async fn so(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn so(
ctx: Context<'_>,
#[description = "Input characters to get stroke order for"]
text: String,
) -> Result<(), Error> {
const MAX_CHARS: i32 = 4; const MAX_CHARS: i32 = 4;
let text = args.rest();
if text.is_empty() {
msg.reply(
&ctx.http,
"Please provide some text you want the stroke order for.",
)
.await?;
return Ok(());
}
let mut displayed_characters: Vec<char> = Vec::new(); let mut displayed_characters: Vec<char> = Vec::new();
let mut displayed_character_count = 0; let mut displayed_character_count = 0;
for character in text.chars() { for character in text.chars() {
if displayed_character_count >= MAX_CHARS { if displayed_character_count >= MAX_CHARS {
msg.channel_id ctx.channel_id()
.say( .say(
&ctx.http, &ctx.discord().http,
":warning: Maximum number of stroke order diagrams per command reached.", ":warning: Maximum number of stroke order diagrams per command reached.",
) )
.await?; .await?;
@ -124,7 +138,7 @@ async fn so(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
// Don't show same character twice // Don't show same character twice
displayed_characters.push(character); displayed_characters.push(character);
displayed_character_count += 1; displayed_character_count += 1;
display_kanji(&ctx, &msg, character, "").await?; display_kanji(ctx, character, "").await?;
} }
Ok(()) Ok(())
} }

@ -1,13 +1,14 @@
use serenity::framework::standard::macros::hook; use crate::Error;
use serenity::framework::standard::{macros::command, CommandResult}; use crate::Context;
use serenity::model::prelude::*; use poise::command;
use serenity::prelude::*;
use std::env; use std::env;
#[command] // TODO: Implement proper help text for command-specific help,
async fn help(ctx: &Context, msg: &Message) -> CommandResult { // see https://github.com/kangalioo/poise/blob/90ac24a8ef621ec6dc3fc452762dc9cfa144f693/examples/framework_usage/main.rs#L18-L38
let prefix = env::var("PREFIX").unwrap(); #[command(prefix_command, slash_command, description_localized("en-US", "Get help for the 字ちゃん Tegaki Tuesday bot"))]
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
let p = env::var("PREFIX").unwrap();
let message = format!( let message = format!(
"<:jichan:943336845637480478> Hello! I'm (Ji-chan), the Tegaki Tuesday bot (and mascot!). "<:jichan:943336845637480478> Hello! I'm (Ji-chan), the Tegaki Tuesday bot (and mascot!).
For more information about the challenge, check out the website at <https://tegakituesday.com> For more information about the challenge, check out the website at <https://tegakituesday.com>
@ -26,18 +27,8 @@ __**Kanji 漢字**__
:game_die: `{p}joyo` Random Jōyō kanji :game_die: `{p}joyo` Random Jōyō kanji
:game_die: `{p}kyoiku <grade|all>` Random Kyōiku kanji :game_die: `{p}kyoiku <grade|all>` Random Kyōiku kanji
:game_die: `{p}jlpt <level|all>` Random JLPT kanji :game_die: `{p}jlpt <level|all>` Random JLPT kanji
:game_die: `{p}hyogai <group|all>` Random Hyōgai kanji", :game_die: `{p}hyogai <group|all>` Random Hyōgai kanji"
p = prefix
); );
msg.reply(&ctx.http, message).await?; ctx.say(message).await?;
Ok(()) 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();
}

@ -1,38 +1,26 @@
use serenity::framework::standard::{macros::command, Args, CommandResult}; #![allow(non_snake_case)]
use serenity::model::prelude::*;
use serenity::prelude::*; use crate::Context;
use crate::Error;
use poise::command;
use serde_json::json; use serde_json::json;
use std::env; use std::env;
use crate::utils::*; use crate::utils::*;
use crate::ShardManagerContainer;
#[command]
#[owners_only]
async fn sleep(ctx: &Context, msg: &Message) -> CommandResult {
let data = ctx.data.read().await;
if let Some(manager) = data.get::<ShardManagerContainer>() {
msg.reply(ctx, "Good night!").await?;
manager.lock().await.shutdown_all().await;
} else {
msg.reply(ctx, "There was a problem getting the shard manager")
.await?;
return Ok(());
}
#[command(prefix_command, hide_in_help, owners_only)]
pub async fn sleep(ctx: Context<'_>) -> Result<(), crate::Error> {
ctx.say("Good night!").await?;
ctx.framework().shard_manager.lock().await.shutdown_all().await;
Ok(()) Ok(())
} }
#[command] #[command(prefix_command, hide_in_help, owners_only)]
#[owners_only] pub async fn setsubmissionchannel(ctx: Context<'_>) -> Result<(), Error> {
#[allow(non_snake_case)]
async fn setSubmissionChannel(ctx: &Context, msg: &Message) -> CommandResult {
let mut guild_data = get_guild_data(); let mut guild_data = get_guild_data();
let guild = msg.guild_id.unwrap().as_u64().to_string(); let guild = ctx.guild_id().unwrap().as_u64().to_string();
let channel = msg.channel_id.as_u64().to_string(); let channel = ctx.channel_id().as_u64().to_string();
if guild_data.contains_key(&guild) { if guild_data.contains_key(&guild) {
let mut current_guild_data = guild_data[&guild].as_object().unwrap().clone(); let mut current_guild_data = guild_data[&guild].as_object().unwrap().clone();
if current_guild_data.contains_key("submissionChannel") { if current_guild_data.contains_key("submissionChannel") {
@ -45,23 +33,21 @@ async fn setSubmissionChannel(ctx: &Context, msg: &Message) -> CommandResult {
guild_data.insert(guild, json!({ "submissionChannel": channel })); guild_data.insert(guild, json!({ "submissionChannel": channel }));
} }
set_guild_data(guild_data); set_guild_data(guild_data);
msg.reply( ctx.say(
&ctx.http,
format!( format!(
"Submission channel for **{}** set to <#{}>.", "Submission channel for **{}** set to <#{}>.",
msg.guild(&ctx).unwrap().name, ctx.guild().unwrap().name,
msg.channel_id ctx.channel_id()
), ),
) )
.await?; .await?;
Ok(()) Ok(())
} }
#[command] #[command(prefix_command, hide_in_help, owners_only)]
#[owners_only] pub async fn setsuggestionchannel(ctx: Context<'_>) -> Result<(), Error> {
#[allow(non_snake_case)] let channel = ctx.channel_id().as_u64().to_string();
async fn setSuggestionChannel(ctx: &Context, msg: &Message) -> CommandResult { let message = format!("Submission channel set to <#{}>.", channel);
let channel = msg.channel_id.as_u64().to_string();
let mut guild_data = get_guild_data(); let mut guild_data = get_guild_data();
if guild_data.contains_key("submissionChannel") { if guild_data.contains_key("submissionChannel") {
guild_data["suggestionChannel"] = channel.into(); guild_data["suggestionChannel"] = channel.into();
@ -69,29 +55,19 @@ async fn setSuggestionChannel(ctx: &Context, msg: &Message) -> CommandResult {
guild_data.insert(String::from("suggestionChannel"), channel.into()); guild_data.insert(String::from("suggestionChannel"), channel.into());
} }
set_guild_data(guild_data); set_guild_data(guild_data);
msg.reply( ctx.say(message).await?;
&ctx.http,
format!("Submission channel set to <#{}>.", msg.channel_id),
)
.await?;
Ok(()) Ok(())
} }
#[command] #[command(prefix_command, hide_in_help, owners_only)]
#[owners_only]
#[allow(non_snake_case)] #[allow(non_snake_case)]
async fn setAnnouncementRole(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn setannouncementrole(
let role; ctx: Context<'_>,
match args.single::<u64>() { #[description = "Announcement role ID"]
Ok(id) => role = id.to_string(), role: u64,
Err(_) => { ) -> Result<(), Error> {
msg.reply(&ctx.http, "Please provide an announcement role ID.")
.await?;
return Ok(());
}
}
let mut guild_data = get_guild_data(); let mut guild_data = get_guild_data();
let guild = msg.guild_id.unwrap().as_u64().to_string(); let guild = ctx.guild_id().unwrap().as_u64().to_string();
if guild_data.contains_key(&guild) { if guild_data.contains_key(&guild) {
let mut current_guild_data = guild_data[&guild].as_object().unwrap().clone(); let mut current_guild_data = guild_data[&guild].as_object().unwrap().clone();
if current_guild_data.contains_key("announcementRole") { if current_guild_data.contains_key("announcementRole") {
@ -104,20 +80,21 @@ async fn setAnnouncementRole(ctx: &Context, msg: &Message, mut args: Args) -> Co
guild_data.insert(guild, json!({ "announcementRole": role })); guild_data.insert(guild, json!({ "announcementRole": role }));
} }
set_guild_data(guild_data); set_guild_data(guild_data);
msg.reply(&ctx.http, "Announcement role set.").await?; ctx.say("Announcement role set.").await?;
Ok(()) Ok(())
} }
#[command] #[command(prefix_command, hide_in_help, owners_only)]
#[owners_only] pub async fn announce(
async fn announce(ctx: &Context, msg: &Message, args: Args) -> CommandResult { ctx: Context<'_>,
send(ctx, msg, args.rest(), true, false).await #[description = "Announcement text"]
announcement: String,
) -> Result<(), Error> {
send(ctx, &announcement, true, false).await
} }
#[command] #[command(prefix_command, hide_in_help, owners_only)]
#[owners_only] pub async fn announcechallenge(ctx: Context<'_>) -> Result<(), Error> {
#[allow(non_snake_case)]
async fn announceChallenge(ctx: &Context, msg: &Message) -> CommandResult {
let challenge_number = get_challenge_number(); 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 <https://tegakituesday.com/{n}>. 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 <https://tegakituesday.com/{n}>.
@ -131,15 +108,14 @@ You can make submissions in both languages, but please submit in your target lan
}, },
p = env::var("PREFIX").unwrap() p = env::var("PREFIX").unwrap()
); );
send(ctx, msg, &message, true, false).await send(ctx, &message, true, false).await?;
ctx.say("Announced!").await?;
Ok(())
} }
#[command] #[command(prefix_command, hide_in_help, owners_only)]
#[owners_only] pub async fn rebuildsite(ctx: Context<'_>) -> Result<(), Error> {
#[allow(non_snake_case)]
async fn rebuildSite(ctx: &Context, msg: &Message) -> CommandResult {
rebuild_site(); rebuild_site();
msg.reply(&ctx.http, "Started site rebuild process!") ctx.say("Started site rebuild process!").await?;
.await?;
Ok(()) Ok(())
} }

@ -1,69 +1,26 @@
mod commands; mod commands;
mod utils; mod utils;
use std::{collections::HashSet, env, sync::Arc}; use std::env;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
type PrefixContext<'a> = poise::PrefixContext<'a, Data, Error>;
// User data, which is stored and accessible in all command invocations
pub struct Data {}
use commands::{challenge::*, kanji::*, meta::*, owner::*}; use commands::{challenge::*, kanji::*, meta::*, owner::*};
use serenity::model::gateway::Activity; use poise::serenity_prelude::{
use serenity::{ model::gateway::GatewayIntents
async_trait,
client::bridge::gateway::ShardManager,
framework::{standard::macros::group, StandardFramework},
http::Http,
model::{
event::ResumedEvent,
gateway::{GatewayIntents, Ready},
},
prelude::*,
}; };
use tracing::{error, info}; use poise::serenity_prelude as serenity;
pub struct ShardManagerContainer;
impl TypeMapKey for ShardManagerContainer {
type Value = Arc<Mutex<ShardManager>>;
}
struct Handler;
#[async_trait] #[poise::command(prefix_command)]
impl EventHandler for Handler { async fn register(ctx: Context<'_>) -> Result<(), Error> {
async fn ready(&self, ctx: Context, ready: Ready) { poise::builtins::register_application_commands_buttons(ctx).await?;
info!("Connected as {}", ready.user.name); Ok(())
let activity = Activity::watching("for new submissions");
ctx.set_activity(activity).await;
} }
async fn resume(&self, _: Context, _: ResumedEvent) {
info!("Resumed");
}
}
#[group]
#[commands(
i,
joyo,
jinmeiyo,
kyoiku,
jlpt,
hyogai,
so,
rebuildSite,
challenge,
submit,
images,
imageDelete,
help,
sleep,
setSubmissionChannel,
setSuggestionChannel,
setAnnouncementRole,
suggest,
announce,
announceChallenge
)]
struct General;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// This will load the environment variables located at `./.env`, relative to // This will load the environment variables located at `./.env`, relative to
@ -74,55 +31,52 @@ async fn main() {
// //
// In this case, a good default is setting the environment variable // In this case, a good default is setting the environment variable
// `RUST_LOG` to `debug`. // `RUST_LOG` to `debug`.
tracing_subscriber::fmt::init();
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let prefix = env::var("PREFIX").expect("Expected a prefix in the environment");
let http = Http::new(&token);
// We will fetch your bot's owners and id let framework = poise::Framework::builder()
let (owners, _bot_id) = match http.get_current_application_info().await { .options(poise::FrameworkOptions {
Ok(info) => { commands: vec![
let mut owners = HashSet::new(); register(),
owners.insert(info.owner.id);
// owner
(owners, info.id) sleep(),
} setsubmissionchannel(),
Err(why) => panic!("Could not access application info: {:?}", why), setsuggestionchannel(),
}; setannouncementrole(),
announce(),
// Create the framework announcechallenge(),
let framework = StandardFramework::new() rebuildsite(),
.configure(|c| c.owners(owners).prefix(prefix))
.group(&GENERAL_GROUP) // challenge
.unrecognised_command(commands::meta::unrecognised_command_hook); challenge(),
submit(),
let intents = GatewayIntents::GUILD_MESSAGES images(),
imagedelete(),
suggest(),
// meta
help(),
// kanji
i(),
joyo(),
jinmeiyo(),
kyoiku(),
jlpt(),
hyogai(),
so()
],
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some(env::var("PREFIX").expect("Expected a prefix in the environment")),
..Default::default()
},
..Default::default()
})
.token(std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"))
.intents(GatewayIntents::GUILD_MESSAGES
| GatewayIntents::DIRECT_MESSAGES | GatewayIntents::DIRECT_MESSAGES
| GatewayIntents::MESSAGE_CONTENT | GatewayIntents::MESSAGE_CONTENT
| GatewayIntents::GUILDS; // required for getting guild name like in setSubmissionChannel | GatewayIntents::GUILDS)
let mut client = Client::builder(&token, intents) .user_data_setup(move |_ctx, _ready, _framework| Box::pin(async move { Ok(Data {}) }));
.framework(framework)
.event_handler(Handler)
.await
.expect("Err creating client");
{ framework.run().await.unwrap();
let mut data = client.data.write().await;
data.insert::<ShardManagerContainer>(client.shard_manager.clone());
}
let shard_manager = client.shard_manager.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c()
.await
.expect("Could not register ctrl+c handler");
shard_manager.lock().await.shutdown_all().await;
});
if let Err(why) = client.start().await {
error!("Client error: {:?}", why);
}
} }

@ -1,9 +1,6 @@
use rand::seq::IteratorRandom; use rand::seq::IteratorRandom;
use serde_json::Map; use serde_json::Map;
use serde_json::Value; use serde_json::Value;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::prelude::*;
use serenity::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
use std::fs; use std::fs;
@ -12,6 +9,9 @@ use std::fs::OpenOptions;
use std::io::Read; use std::io::Read;
use std::io::Write; use std::io::Write;
use std::process::Command; use std::process::Command;
use crate::serenity;
use crate::Error;
use crate::{Context, PrefixContext};
pub fn get_challenge_number() -> i32 { pub fn get_challenge_number() -> i32 {
let challenge_dir = format!("{}/content/challenges", env::var("HUGO").unwrap()); let challenge_dir = format!("{}/content/challenges", env::var("HUGO").unwrap());
@ -90,8 +90,8 @@ pub fn set_submission_data(submission_data: Vec<Value>) {
.unwrap(); .unwrap();
} }
pub fn is_matching_submission(submission: &Value, msg: &Message) -> bool { pub fn is_matching_submission(submission: &Value, author: &serenity::User) -> bool {
submission["id"].as_str().unwrap() == msg.author.id.as_u64().to_string() submission["id"].as_str().unwrap() == author.id.as_u64().to_string()
} }
pub fn to_fullwidth(string: &str) -> String { pub fn to_fullwidth(string: &str) -> String {
@ -153,12 +153,11 @@ pub fn set_guild_data(guild_data: Map<String, Value>) {
} }
pub async fn send( pub async fn send(
ctx: &Context, ctx: Context<'_>,
msg: &Message,
message: &str, message: &str,
ping: bool, ping: bool,
pin: bool, pin: bool,
) -> CommandResult { ) -> Result<(), Error> {
let guild_data = get_guild_data(); let guild_data = get_guild_data();
let mut announcements_count = 0; let mut announcements_count = 0;
for (_guild, data) in guild_data.iter() { for (_guild, data) in guild_data.iter() {
@ -166,7 +165,7 @@ pub async fn send(
if !data.contains_key("submissionChannel") { if !data.contains_key("submissionChannel") {
continue; continue;
} }
let channel = ChannelId( let channel = serenity::ChannelId(
data["submissionChannel"] data["submissionChannel"]
.as_str() .as_str()
.unwrap() .unwrap()
@ -181,6 +180,7 @@ pub async fn send(
)); ));
} }
message_to_send.push_str(message); message_to_send.push_str(message);
let ctx = ctx.discord();
let sent_message = channel let sent_message = channel
.send_message(&ctx.http, |e| { .send_message(&ctx.http, |e| {
e.content(message_to_send); e.content(message_to_send);
@ -198,8 +198,7 @@ pub async fn send(
} }
announcements_count += 1; announcements_count += 1;
} }
msg.reply( ctx.say(
&ctx.http,
format!( format!(
"Announced to {} server{}!", "Announced to {} server{}!",
announcements_count, announcements_count,
@ -222,21 +221,20 @@ pub fn get_so_diagram(kanji: char) -> String {
} }
pub async fn display_kanji( pub async fn display_kanji(
ctx: &Context, ctx: Context<'_>,
msg: &Message,
kanji: char, kanji: char,
comment: &str, comment: &str,
) -> CommandResult { ) -> Result<(), Error> {
msg.reply(&ctx.http, format!("{}{}", kanji, comment)) ctx.say(format!("{}{}", kanji, comment))
.await?; .await?;
let url = get_so_diagram(kanji); let url = get_so_diagram(kanji);
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let request = client.head(&url).build().unwrap(); let request = client.head(&url).build().unwrap();
let response = client.execute(request).await?.status(); let response = client.execute(request).await?.status();
let link_validated = response != reqwest::StatusCode::NOT_FOUND; let link_validated = response != reqwest::StatusCode::NOT_FOUND;
msg.channel_id ctx.channel_id()
.say( .say(
&ctx.http, &ctx.discord().http,
if link_validated { if link_validated {
&url &url
} else { } else {
@ -301,10 +299,9 @@ pub fn get_kanji_info(kanji: char) -> String {
pub async fn random_kanji( pub async fn random_kanji(
category: &str, category: &str,
ctx: &Context, ctx: Context<'_>,
msg: &Message, subcategory: Option<String>,
mut args: Args, ) -> Result<(), Error> {
) -> CommandResult {
let lists_data = get_lists_data(); let lists_data = get_lists_data();
let category = &lists_data[category]; let category = &lists_data[category];
let default_version = category["default"].as_str().unwrap(); let default_version = category["default"].as_str().unwrap();
@ -312,10 +309,9 @@ pub async fn random_kanji(
match list.as_str() { match list.as_str() {
Some(string) => { Some(string) => {
let kanji = random_from_string(string); let kanji = random_from_string(string);
display_kanji(&ctx, &msg, kanji, "").await?; display_kanji(ctx, kanji, "").await?;
} }
None => { None => {
let subcategory = args.single::<String>();
let subcategories = list.as_object().unwrap(); let subcategories = list.as_object().unwrap();
let subcategory_list = { let subcategory_list = {
let mut string = String::from("\n"); let mut string = String::from("\n");
@ -335,49 +331,40 @@ pub async fn random_kanji(
)); ));
string string
}; };
match subcategory { let subcategory = subcategory.unwrap().to_uppercase();
Ok(string) => { if subcategory == "ALL" {
let string = string.to_uppercase();
if string == "ALL" {
let subcategory_key = subcategories let subcategory_key = subcategories
.keys() .keys()
.choose(&mut rand::thread_rng()) .choose(&mut rand::thread_rng())
.unwrap(); .unwrap();
let list = subcategories[subcategory_key].as_str().unwrap(); let list = subcategories[subcategory_key].as_str().unwrap();
let kanji = random_from_string(&list); let kanji = random_from_string(&list);
display_kanji(&ctx, &msg, kanji, &format!(", **{}**", subcategory_key)) display_kanji(ctx, kanji, &format!(", **{}**", subcategory_key))
.await?; .await?;
} else if subcategories.contains_key(&string) { } else if subcategories.contains_key(&subcategory) {
let list = list[&string].as_str().unwrap(); let list = list[&subcategory].as_str().unwrap();
let kanji = random_from_string(&list); let kanji = random_from_string(&list);
display_kanji(&ctx, &msg, kanji, "").await?; display_kanji(ctx, kanji, "").await?;
} else { } else {
let message = format!( let message = format!(
"That is an invalid subcategory. Please use {}.", "That is an invalid subcategory. Please use {}.",
&subcategory_list &subcategory_list
); );
msg.reply(&ctx.http, message).await?; ctx.say(message).await?;
}
}
Err(_) => {
let mut message = String::from("Please specify a subcategory: ");
message.push_str(&subcategory_list);
msg.reply(&ctx.http, message).await?;
}
} }
} }
} }
Ok(()) Ok(())
} }
pub fn get_avatar(user: &User) -> String { pub fn get_avatar(user: &serenity::User) -> String {
match user.avatar_url() { match user.avatar_url() {
Some(avatar_url) => avatar_url, Some(avatar_url) => avatar_url,
None => user.default_avatar_url(), None => user.default_avatar_url(),
} }
} }
pub async fn leaderboard(ctx: &Context) -> CommandResult { pub async fn leaderboard(ctx: &PrefixContext<'_>) -> Result<(), Error> {
const LENGTH: usize = 10; const LENGTH: usize = 10;
let mut submission_counts: HashMap<String, u32> = HashMap::new(); let mut submission_counts: HashMap<String, u32> = HashMap::new();
for challenge in 1..get_challenge_number() + 1 { for challenge in 1..get_challenge_number() + 1 {
@ -396,12 +383,10 @@ pub async fn leaderboard(ctx: &Context) -> CommandResult {
let mut leaderboard_html = String::from("<table id=\"leaderboard\">"); let mut leaderboard_html = String::from("<table id=\"leaderboard\">");
for (i, (id, count)) in top_submitters[0..LENGTH].iter().enumerate() { for (i, (id, count)) in top_submitters[0..LENGTH].iter().enumerate() {
let place = i + 1; let place = i + 1;
let user = UserId(id.parse::<u64>().unwrap()) let user = &ctx.msg.author;
.to_user(&ctx.http)
.await?;
let avatar = get_avatar(&user); let avatar = get_avatar(&user);
let profile = format!("https://discord.com/users/{id}"); let profile = format!("https://discord.com/users/{id}");
let name = user.name; let name = &user.name;
let discriminator = user.discriminator; let discriminator = user.discriminator;
leaderboard_html.push_str(&format!("<tr><td>{place}</td><td><a href=\"{profile}\" target=\"_blank\" class=\"no-underline\"><img src=\"{avatar}\" alt=\"avatar\"> <span class=\"underline\">{name}<span class=\"muted\">#{:0>4}</span></span></a></td><td>{count}</td></tr>", discriminator)); leaderboard_html.push_str(&format!("<tr><td>{place}</td><td><a href=\"{profile}\" target=\"_blank\" class=\"no-underline\"><img src=\"{avatar}\" alt=\"avatar\"> <span class=\"underline\">{name}<span class=\"muted\">#{:0>4}</span></span></a></td><td>{count}</td></tr>", discriminator));
} }

Loading…
Cancel
Save