diff --git a/.gitignore b/.gitignore index f9182ec..93449f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target Secrets*.toml .vscode -*.dca \ No newline at end of file +*.dca +.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d24ed4d..df57b29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,7 @@ dependencies = [ "symphonia", "tokio", "tracing", + "urlencoding", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2b8e889..65337f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ regex = "1.10.2" once_cell = "1.18.0" gen_welcome = { version = "0.1.0", path = "crates/gen_welcome" } +urlencoding = "2.1.3" [dependencies.parking_lot] version = "0.12" diff --git a/src/config/general_command_loader.rs b/src/config/general_command_loader.rs index f61004c..226c24a 100644 --- a/src/config/general_command_loader.rs +++ b/src/config/general_command_loader.rs @@ -1,6 +1,7 @@ use crate::events::join::guild_member_addition; use crate::general_commands::meetups::MEETUPS_COMMAND; use crate::general_commands::ping::PING_COMMAND; +use crate::general_commands::compile::COMPILE_COMMAND; use crate::general_commands::songbird_commands::DEAFEN_COMMAND; use crate::general_commands::songbird_commands::JOIN_COMMAND; use crate::general_commands::songbird_commands::LEAVE_COMMAND; @@ -13,7 +14,7 @@ use serenity::{async_trait, prelude::Context}; use serenity::{model::prelude::Member, prelude::EventHandler}; #[group] -#[commands(ping, meetups, deafen, join, leave, mute, ting, undeafen, unmute)] +#[commands(ping, meetups, deafen, join, leave, mute, ting, undeafen, unmute, compile)] pub struct General; #[async_trait] diff --git a/src/general_commands/compile.rs b/src/general_commands/compile.rs new file mode 100644 index 0000000..10ac4f0 --- /dev/null +++ b/src/general_commands/compile.rs @@ -0,0 +1,258 @@ +use std::time::Duration; +use regex::Regex; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serenity::all::{Context, CreateMessage, Message, ReactionType}; +use serenity::all::standard::CommandResult; +use serenity::all::standard::macros::command; +use serenity::builder::{CreateEmbed, CreateEmbedFooter}; +use urlencoding::encode; +use tokio::time::sleep; +use tracing::error; + +static LANGUAGE_ALIASES: &[(&str, &str)] = &[ + ("objc", "objective-c"), + ("kt", "kotlin"), + ("cs", "csharp"), + ("py", "python"), + ("py3", "python3"), + ("js", "javascript"), + ("coffee", "coffeescript"), + ("fs", "fsharp"), + ("rs", "rust"), + ("lisp", "commonlisp"), + ("ts", "typescript"), + ("bf", "brainfuck") +]; + + +static LANGUAGES: &[&str] = &["c", "cpp", "objective-c", "java", "kotlin", "scala", + "swift", "csharp", "go", "haskell", "erlang", "perl", "python", "python3", + "ruby", "php", "bash", "r", "javascript", "coffeescript", "vb", "cobol", "fsharp", "d", + "clojure", "elixir", "mysql", "rust", "scheme", "commonlisp", "nadesiko", "typescript", + "brainfuck", "plain" +]; + +static MISSING_CODE_BLOCK: &str + = "Falta un bloque de c贸digo, colocalo con \\`\\`\\` \\`\\`\\`."; +static MISSING_LANGUAGE: &str + = "Falta especificar un lenguaje a tu bloque de c贸digo, especificalo despu茅s de los \\`\\`\\`."; +static INVALID_LANGUAGE: &str + = "El lenguaje especificado es invalido, los lenguajes validos son: "; +static INVALID_RESPONSE: &str + = "La respuesta recibida del compilador no se pudo leer."; + +#[derive(Deserialize)] +struct RunnerResponse { + id: String, + status: String, +} + +#[derive(Deserialize, Serialize)] +struct RunnerDetails { + build_stderr: Option, + build_exit_code: Option, + build_result: Option, + + stdout: Option, + stderr: Option, + result: Option, + exit_code: Option +} + +async fn compile_code(language: String, code: String, args: String) -> Option { + Client::new() + .post(format!( + "https://api.paiza.io/runners/create?source_code={}&language={}&api_key=guest{}", + encode(&*code), + encode(&*language), + if args.is_empty() { "".to_string() } else { format!("&input={args}") } + )) + .send() + .await + .unwrap() + .json::() + .await + .inspect_err(|e| error!("Hubo un error: {e:?}")) + .ok() +} + +async fn check_status(runner_id: String) -> Option { + Client::new() + .get(format!( + "https://api.paiza.io/runners/get_status?id={}&api_key=guest", + encode(&*runner_id) + )) + .send() + .await + .unwrap() + .json::() + .await + .inspect_err(|e| error!("Hubo un error: {e:?}")) + .ok() +} + +async fn check_details(runner_id: String) -> Option { + Client::new() + .get(format!( + "https://api.paiza.io/runners/get_details?id={}&api_key=guest", + encode(&*runner_id) + )) + .send() + .await + .unwrap() + .json::() + .await + .inspect_err(|e| error!("Hubo un error: {e:?}")) + .ok() +} + +#[command] +pub async fn compile(ctx: &Context, msg: &Message) -> CommandResult { + msg.react(ctx, ReactionType::Unicode("馃".to_string())) + .await + .unwrap(); + + let parts: Vec<&str> = Regex::new(r"[ \n]") + .unwrap() + .splitn(&*msg.content, 2) + .collect(); + + if parts.len() < 2 { + msg.reply(ctx, MISSING_CODE_BLOCK).await?; + return Ok(()); + } + + let args_and_code = &parts[1..].join(" "); + let start_code = args_and_code + .find("```") + .map(|idx| idx + 3); + let end_code = args_and_code[start_code.unwrap_or(0)..] + .find("```") + .map(|idx| start_code.unwrap_or(0) + idx); + + let mut code_block + = if let (Some(start), Some(end)) = (start_code, end_code) { + Some(args_and_code[start..end].to_string()) + } else { + msg.reply(ctx, MISSING_CODE_BLOCK).await?; + return Ok(()); + }.unwrap(); + + let mut language = if let Some(start) = start_code { + let lang_end = args_and_code[start..] + .find('\n') + .unwrap_or(0); + &args_and_code[start..start + lang_end] + } else { + "" + } + .to_string() + .to_lowercase(); + + if language.is_empty() { + msg.reply(ctx, MISSING_LANGUAGE).await?; + return Ok(()); + } + + code_block = code_block[language.len()..].to_string(); + + language = LANGUAGE_ALIASES.iter() + .find_map(|(key, value)| + if key.to_string() == language { Some(value.to_string()) } else { None + }) + .unwrap_or(language); + + if language == "rust" { + msg.react(ctx, ReactionType::Unicode("馃".to_string())).await.unwrap(); + } + + if !LANGUAGES.contains(&&*language) { + msg.reply(ctx, format!( + "{INVALID_LANGUAGE} {}", + LANGUAGES.join(", ") + )).await?; + return Ok(()); + } + + let args = args_and_code[end_code.unwrap() + 3..] + .to_string() + .replace("\n", " "); + + let api_response = compile_code(language, code_block, args).await; + + if let Some(parsed_res) = api_response { + let mut response = parsed_res; + while response.status != "completed" { + sleep(Duration::from_secs(3)).await; + response = if let Some(new_status) + = check_status(response.id).await { + new_status + } else { + msg.reply(ctx, INVALID_RESPONSE).await?; + return Ok(()); + }; + } + + let mut response_embed = CreateEmbed::default(); + + if let Some(build_details) = check_details(response.id).await { + if build_details.build_result.unwrap_or("success".to_string()) != "success" { + response_embed = response_embed + .title("Error de build!") + .description(format!( + "```\n{}\n```", + build_details.build_stderr.unwrap_or( + "".to_string() + ) + )) + .color(0xFF0000) + .footer(CreateEmbedFooter::new(format!( + "El compilador salio con el c贸digo: {}", + build_details.build_exit_code.unwrap_or_default() + ))); + } else if build_details.result.unwrap_or("success".to_string()) != "success" { + response_embed = response_embed + .title("Error de ejecuci贸n!") + .description(format!( + "```\n{}\n```", + build_details.stderr.unwrap_or( + "".to_string() + ) + )) + .color(0xFF0000) + .footer(CreateEmbedFooter::new(format!( + "El programa salio con el c贸digo: {}", + build_details.exit_code.unwrap_or_default() + ))) + } else { + response_embed = response_embed + .title("El c贸digo se ejecuto correctamente") + .description(format!( + "```\n{}\n```", + build_details.stdout.unwrap_or( + "".to_string() + ) + )) + .color(0x00FF00) + .footer(CreateEmbedFooter::new(format!( + "El programa salio con el c贸digo: {}", + build_details.exit_code.unwrap_or_default() + ))) + } + + msg.channel_id + .send_message( + ctx, + CreateMessage::new().embed(response_embed).reference_message(msg) + ) + .await?; + } else { + msg.reply(ctx, INVALID_RESPONSE).await?; + } + } else { + msg.reply(ctx, INVALID_RESPONSE).await?; + } + + Ok(()) +} diff --git a/src/general_commands/mod.rs b/src/general_commands/mod.rs index 15594cf..bfcc530 100644 --- a/src/general_commands/mod.rs +++ b/src/general_commands/mod.rs @@ -1,3 +1,4 @@ pub mod meetups; pub mod ping; pub mod songbird_commands; +pub mod compile; diff --git a/src/slash_commands/mod.rs b/src/slash_commands/mod.rs index 196f187..5b6b87b 100644 --- a/src/slash_commands/mod.rs +++ b/src/slash_commands/mod.rs @@ -3,4 +3,4 @@ pub mod id; pub mod invite; pub mod crate_lib; pub mod ping; -pub mod sugerencia; +pub mod sugerencia; \ No newline at end of file