diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a37a39b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "error-codes"] + path = error-codes + url = https://github.com/AToska21/error-codes.git diff --git a/Cargo.lock b/Cargo.lock index d27d7b9..550fa45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -85,6 +94,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -200,6 +215,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -259,6 +280,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -279,6 +327,16 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -316,6 +374,30 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -356,6 +438,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.0.35" @@ -988,6 +1076,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1017,6 +1115,10 @@ name = "professor" version = "0.1.0" dependencies = [ "dotenv", + "once_cell", + "regex", + "serde", + "serde_json", "serenity", "tokio", ] @@ -1080,6 +1182,35 @@ dependencies = [ "bitflags 2.8.0", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "reqwest" version = "0.11.27" @@ -1145,6 +1276,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1343,6 +1483,7 @@ dependencies = [ "chrono", "command_attr", "dashmap", + "ed25519-dalek", "flate2", "futures", "fxhash", @@ -1377,12 +1518,32 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "skeptic" version = "0.13.7" @@ -1429,6 +1590,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index e39e733..af0705d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,13 +5,22 @@ edition = "2021" [dependencies] dotenv = "0.15.0" +regex = "1.11.1" +once_cell = "1.20.3" + [dependencies.tokio] version = "1.43.0" features = [ - "rt-multi-thread", + "rt", "macros", ] + [dependencies.serenity] -version = "0.12.4" \ No newline at end of file +version = "0.12.4" +features = ["interactions_endpoint"] + +[build-dependencies] +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.138" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..de2a671 --- /dev/null +++ b/build.rs @@ -0,0 +1,201 @@ +use std::fmt::Write; +use std::{env, fs}; +use std::fs::FileType; +use std::iter::Map; +use serde::Deserialize; +use serde_json::Value; + +#[derive(Deserialize)] +struct CategoryInfo{ + name: String, + description: String, + system: String, +} + +#[derive(Deserialize)] +struct ErrorInfo{ + name: String, + message: String, + short_description: String, + long_description: String, + short_solution: String, + long_solution: String, + support_link: String, +} + +fn main(){ + let mut code = "pub fn get_error_code_and_category(category: u16, code: u16) -> (super::CategoryInfo, super::ErrorInfo) {\ + let category_result = match category{\ + ".to_owned(); + + for category_dir in fs::read_dir("./error-codes/data").expect("unable to read dir"){ + let category_dir = category_dir.expect("unable to read category"); + + let mut path = category_dir.path(); + path.push("en_US.json"); + + let json_raw = fs::read_to_string(path).expect("unable to read category json"); + let category: Value = serde_json::from_str(&json_raw).expect("unable to parse json file"); + + let Value::Object(categories) = category else { + panic!("unable to parse category json"); + }; + + let mut iter = categories.iter(); + + let first_category = iter.next().unwrap(); + + assert_eq!(iter.next(), None); + + let Value::Object(category_info) = first_category.1 else { + panic!("unable to parse category json"); + }; + + let Value::String(name) = &category_info["name"] else { + panic!("unable to parse category json"); + }; + + let Value::String(description) = &category_info["description"] else { + panic!("unable to parse category json"); + }; + + let Value::String(system) = &category_info["system"] else { + panic!("unable to parse category json"); + }; + + code.write_str( + &format!("\ + {} => super::CategoryInfo{{\ + name: \"{}\", + description: \"{}\", + system: \"{}\", + }},\ + ", first_category.0, name, description, system) + ).expect("unable to write"); + + } + + code.write_str("\ + _ => super::CategoryInfo::default()\ + };\ + let error_result = match ((category as u32) << 16) | code as u32{\ + \ + ").expect("unable to write"); + + for category_dir in fs::read_dir("./error-codes/data").expect("unable to read dir"){ + let category_dir = category_dir.expect("unable to read category"); + + for error_dir in fs::read_dir(category_dir.path()).expect("unable to read dir") { + let error_dir = error_dir.expect("unable to read error"); + + if !error_dir.file_type().unwrap().is_dir(){ + continue; + } + + let mut path = error_dir.path(); + path.push("en_US.json"); + + let json_raw = fs::read_to_string(path).expect("unable to read error json"); + let category: Value = serde_json::from_str(&json_raw).expect("unable to parse json file"); + + let Value::Object(categories) = category else { + panic!("unable to parse category json"); + }; + + let mut iter = categories.iter(); + + let first_category = iter.next().unwrap(); + + assert_eq!(iter.next(), None); + + let Value::Object(category) = first_category.1 else { + panic!("unable to parse category json"); + }; + + let mut iter = category.iter(); + + let first_error = iter.next().unwrap(); + + assert_eq!(iter.next(), None); + + let Value::Object(error) = first_error.1 else { + panic!("unable to parse category json"); + }; + + if first_error.0.contains("X"){ + continue; + } + + let category_num: u32 = first_category.0.parse().expect("unable to parse category"); + let error_num: u32 = first_error.0.parse().expect("unable to parse error"); + + let search_val: u32 = category_num << 16 | error_num; + + let Value::String(name) = &error["name"] else { + panic!("unable to parse error json"); + }; + + let name = name.replace("\"", "\\\""); + + let Value::String(message) = &error["message"] else { + panic!("unable to parse error json"); + }; + + let message = message.replace("\"", "\\\""); + + let Value::String(short_description) = &error["short_description"] else { + panic!("unable to parse error json"); + }; + + let short_description = short_description.replace("\"", "\\\""); + + let Value::String(long_description) = &error["long_description"] else { + panic!("unable to parse error json"); + }; + + let long_description = long_description.replace("\"", "\\\""); + + let Value::String(short_solution) = &error["short_solution"] else { + panic!("unable to parse error json"); + }; + + let short_solution = short_solution.replace("\"", "\\\""); + + let Value::String(long_solution) = &error["long_solution"] else { + panic!("unable to parse error json"); + }; + + let long_solution = long_solution.replace("\"", "\\\""); + + let Value::String(support_link) = &error["support_link"] else { + panic!("unable to parse error json"); + }; + + let support_link = support_link.replace("\"", "\\\""); + + code.write_str( + &format!("\ + {} => super::ErrorInfo{{\ + name: \"{}\", + message: \"{}\", + short_description: \"{}\", + long_description: \"{}\", + short_solution: \"{}\", + long_solution: \"{}\", + support_link: \"{}\", + }},\ + ", search_val, name, message, short_description, long_description, short_solution, long_solution, support_link) + ).expect("unable to write"); + } + } + + code.write_str("\ + _ => super::ErrorInfo::default()\ + };\ + (category_result, error_result)\ + }\ + ").expect("unable to write"); + let mut path = env::var("OUT_DIR").unwrap(); + path += "/errors.rs"; + fs::write(path, code).expect("unable to write generated code") +} \ No newline at end of file diff --git a/error-codes b/error-codes new file mode 160000 index 0000000..2902808 --- /dev/null +++ b/error-codes @@ -0,0 +1 @@ +Subproject commit 2902808d5c80fd8bf6afcf198949c861199b987d diff --git a/src/error_codes.rs b/src/error_codes.rs new file mode 100644 index 0000000..1a752cf --- /dev/null +++ b/src/error_codes.rs @@ -0,0 +1,173 @@ +use std::collections::BTreeMap; +use std::sync::Arc; +use std::time::Duration; +use once_cell::sync::Lazy; +use regex::Regex; +use serenity::all::{Context, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedFooter, CreateMessage, EditInteractionResponse, EditMessage, EventHandler, Http, Interaction, Message}; +use serenity::all::CreateActionRow::Buttons; +use serenity::async_trait; +use tokio::time::sleep; + +mod errors{ + include!(concat!(env!("OUT_DIR"), "/errors.rs")); +} + +struct CategoryInfo{ + name: &'static str, + description: &'static str, + system: &'static str, +} + +impl Default for CategoryInfo{ + fn default() -> Self { + Self{ + name: "unknown", + description: "unknown", + system: "unknown" + } + } +} + +struct ErrorInfo{ + name: &'static str, + message: &'static str, + short_description: &'static str, + long_description: &'static str, + short_solution: &'static str, + long_solution: &'static str, + support_link: &'static str, +} + +impl Default for ErrorInfo{ + fn default() -> Self { + Self{ + name: "unknown", + message: "unknown", + long_description: "unknown", + long_solution: "unknown", + short_description: "unknown", + short_solution: "unknown", + support_link: "unknown", + } + } +} + +static ERROR_CODE_REGEX: Lazy = + Lazy::new(|| Regex::new("\\d\\d\\d-\\d\\d\\d\\d").expect("invalid regex")); + +pub struct ErrorCodeHandler; + +fn create_error_explain_message(str_code: &str, expanded: bool) -> Option{ + let Ok(category) = str_code[0..3].parse() else { + return None; + }; + + let Ok(error) = str_code[5..].parse() else { + return None; + }; + + let (category_info, error_info) = errors::get_error_code_and_category(category, error); + + let mut embed = CreateEmbed::new() + .title(str_code); + + if expanded{ + embed = embed.field("Module", category_info.name, true) + .field("System", category_info.system, true) + .field("Module Description", category_info.description, true) + .field("Name", error_info.name, false) + .field("Explanation", error_info.message, true) + .field("Description", error_info.long_description, true) + .field("Solution", error_info.long_solution, true) + ; + } else { + embed = embed.field("Name", error_info.name, true) + .field("Description", error_info.short_description, true) + .field("Solution", error_info.short_solution, true) + } + + let expand_button = + CreateButton::new(format!("ERROR_EXPLAIN:{}", str_code)) + .label("Expand") + .disabled(expanded); + + let message = EditMessage::new() + .embed(embed) + .components(vec![ + Buttons( + vec![ + expand_button + ] + ) + ]) + .content(""); + + Some(message) +} + +fn start_timed_explanation_collapse(mut message: Message, http: Arc, error_code: &str){ + let error_code: Box = error_code.into(); + + tokio::spawn(async move { + sleep(Duration::from_secs(30)).await; + + let Some(new_message) = create_error_explain_message(&error_code, false) else { + return + }; + + message.edit(&http, new_message).await.ok(); + }); +} + +#[async_trait] +impl EventHandler for ErrorCodeHandler { + async fn message(&self, ctx: Context, msg: Message) { + if msg.author.bot{ + return; + } + + if let Some(err_code) = ERROR_CODE_REGEX.find(&msg.content){ + let str_code = err_code.as_str(); + + let Some(message) = create_error_explain_message(str_code, true) else { + return + }; + + let response = CreateMessage::new() + .content("loading") + .reference_message(&msg); + + let Ok(mut msg) = msg.channel_id.send_message(&ctx.http, response).await else { + return + }; + + let Some(edited) = create_error_explain_message(str_code, true) else { + return; + }; + + msg.edit(&ctx.http, edited).await.ok(); + + start_timed_explanation_collapse(msg, ctx.http.clone(), str_code); + } + } + + async fn interaction_create(&self, ctx: Context, interaction: Interaction) { + if let Interaction::Component(mut component) = interaction{ + if component.data.custom_id.starts_with("ERROR_EXPLAIN"){ + let Some((_, error_code)) = component.data.custom_id.split_once(":") else{ + return; + }; + + let Some(msg) = create_error_explain_message(error_code, true) else { + return + }; + + component.message.edit(&ctx.http, msg).await.ok(); + + component.defer(&ctx.http).await.ok(); + + start_timed_explanation_collapse(*component.message, ctx.http.clone(), error_code); + } + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5e753ff..59b867b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,27 @@ -#[tokio::main] -async fn main() { +mod error_codes; +use std::env; +use once_cell::sync::Lazy; +use regex::Regex; +use serenity::async_trait; +use serenity::prelude::*; + + + + + +#[tokio::main(flavor = "current_thread")] +async fn main() { + dotenv::dotenv().ok(); + + let token = env::var("PROFESSOR_TOKEN").expect("Token not specified"); + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::MESSAGE_CONTENT; + + + let mut client = Client::builder(&token, intents) + .event_handler(error_codes::ErrorCodeHandler) + .await.expect("unable to create client"); + + client.start().await.expect("error running bot"); }