From d2d480921ba23f91d0006028d7c00945c3264d0a Mon Sep 17 00:00:00 2001 From: Headline Date: Sun, 5 Mar 2023 19:07:31 -0800 Subject: [PATCH] Create "View on Godbolt" button (#200) * Implement View on Godbolt feature * Bump version --- Cargo.toml | 2 +- src/commands/asm.rs | 32 ++++++++++++++++++--- src/commands/compile.rs | 45 ++++++++++++++++++++--------- src/commands/cpp.rs | 11 +++---- src/events.rs | 29 ++++++++++++++++--- src/managers/compilation.rs | 40 ++++++++++++++++---------- src/slashcmds/cpp.rs | 2 +- src/utls/discordhelpers/embeds.rs | 48 ++++++++++++++++++++++--------- src/utls/discordhelpers/mod.rs | 24 ++++++++-------- 9 files changed, 163 insertions(+), 70 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a03f410..1b0700f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "discord-compiler-bot" description = "Discord bot to compile your spaghetti code." -version = "3.5.3" +version = "3.5.4" authors = ["Michael Flaherty (Headline#9999)"] edition = "2021" build = "src/build.rs" diff --git a/src/commands/asm.rs b/src/commands/asm.rs index dd06b26..5240645 100644 --- a/src/commands/asm.rs +++ b/src/commands/asm.rs @@ -10,7 +10,9 @@ use crate::utls::constants::*; use crate::utls::discordhelpers; use crate::utls::discordhelpers::embeds; +use crate::managers::compilation::CompilationDetails; use serenity::builder::CreateEmbed; +use serenity::model::application::component::ButtonStyle; use serenity::model::channel::{Message, ReactionType}; use serenity::model::user::User; @@ -19,8 +21,30 @@ use crate::utls::parser; #[command] #[bucket = "nospam"] pub async fn asm(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let emb = handle_request(ctx.clone(), msg.content.clone(), msg.author.clone(), msg).await?; - let asm_embed = embeds::dispatch_embed(&ctx.http, msg.channel_id, emb).await?; + let (embed, compilation_details) = + handle_request(ctx.clone(), msg.content.clone(), msg.author.clone(), msg).await?; + + // Send our final embed + let mut new_msg = embeds::embed_message(embed); + if let Some(b64) = compilation_details.base64 { + new_msg.components(|cmp| { + cmp.create_action_row(|row| { + row.create_button(|btn| { + btn.style(ButtonStyle::Link) + .url(format!("https://godbolt.org/clientstate/{}", b64)) + .label("View on godbolt.org") + }) + }) + }); + } + + let asm_embed = msg + .channel_id + .send_message(&ctx.http, |e| { + *e = new_msg.clone(); + e + }) + .await?; // Success/fail react let compilation_successful = asm_embed.embeds[0].colour.unwrap().0 == COLOR_OKAY; @@ -41,7 +65,7 @@ pub async fn handle_request( mut content: String, author: User, msg: &Message, -) -> Result { +) -> Result<(CreateEmbed, CompilationDetails), CommandError> { let data_read = ctx.data.read().await; let loading_reaction = { let botinfo_lock = data_read.get::().unwrap(); @@ -102,5 +126,5 @@ pub async fn handle_request( // remove our loading emote discordhelpers::delete_bot_reacts(&ctx, msg, loading_reaction).await?; - Ok(response.1) + Ok((response.1, response.0)) } diff --git a/src/commands/compile.rs b/src/commands/compile.rs index 0285ae2..ab2a218 100644 --- a/src/commands/compile.rs +++ b/src/commands/compile.rs @@ -12,11 +12,12 @@ use tokio::sync::RwLockReadGuard; use serenity::builder::CreateEmbed; use serenity::client::Context; use serenity::framework::standard::CommandError; +use serenity::model::application::component::ButtonStyle; use serenity::model::channel::{Message, ReactionType}; use serenity::model::user::User; use crate::cache::{CompilerCache, ConfigCache, StatsManagerCache}; -use crate::managers::compilation::CompilationManager; +use crate::managers::compilation::{CompilationDetails, CompilationManager}; #[command] #[bucket = "nospam"] @@ -24,20 +25,37 @@ pub async fn compile(ctx: &Context, msg: &Message, _args: Args) -> CommandResult let data_read = ctx.data.read().await; // Handle wandbox request logic - let embed = handle_request(ctx.clone(), msg.content.clone(), msg.author.clone(), msg).await?; + let (embed, compilation_details) = + handle_request(ctx.clone(), msg.content.clone(), msg.author.clone(), msg).await?; // Send our final embed - let compilation_embed = embeds::dispatch_embed(&ctx.http, msg.channel_id, embed).await?; + let mut new_msg = embeds::embed_message(embed); + if let Some(b64) = compilation_details.base64 { + new_msg.components(|cmp| { + cmp.create_action_row(|row| { + row.create_button(|btn| { + btn.style(ButtonStyle::Link) + .url(format!("https://godbolt.org/clientstate/{}", b64)) + .label("View on godbolt.org") + }) + }) + }); + } + + let sent = msg + .channel_id + .send_message(&ctx.http, |e| { + *e = new_msg.clone(); + e + }) + .await?; // Success/fail react - let compilation_successful = compilation_embed.embeds[0].colour.unwrap().0 == COLOR_OKAY; - discordhelpers::send_completion_react(ctx, &compilation_embed, compilation_successful).await?; + let compilation_successful = sent.embeds[0].colour.unwrap().0 == COLOR_OKAY; + discordhelpers::send_completion_react(ctx, &sent, compilation_successful).await?; let mut delete_cache = data_read.get::().unwrap().lock().await; - delete_cache.insert( - msg.id.0, - MessageCacheEntry::new(compilation_embed, msg.clone()), - ); + delete_cache.insert(msg.id.0, MessageCacheEntry::new(sent, msg.clone())); debug!("Command executed"); Ok(()) } @@ -47,7 +65,7 @@ pub async fn handle_request( mut content: String, author: User, msg: &Message, -) -> Result { +) -> Result<(CreateEmbed, CompilationDetails), CommandError> { let data_read = ctx.data.read().await; let loading_reaction = { let botinfo_lock = data_read.get::().unwrap(); @@ -66,7 +84,6 @@ pub async fn handle_request( // Try to load in an attachment let (code, ext) = parser::get_message_attachment(&msg.attachments).await?; if !code.is_empty() { - // content.push_str(&format!("\n```{}\n{}\n```\n", ext, code)); writeln!(&mut content, "\n```{}\n{}\n```\n", ext, code).unwrap(); } @@ -94,10 +111,10 @@ pub async fn handle_request( // dispatch our req let compilation_manager_lock: RwLockReadGuard = compilation_manager.read().await; - let awd = compilation_manager_lock + let compilation_result = compilation_manager_lock .compile(&parse_result, &author) .await; - let result = match awd { + let result = match compilation_result { Ok(r) => r, Err(e) => { // we failed, lets remove the loading react so it doesn't seem like we're still processing @@ -139,5 +156,5 @@ pub async fn handle_request( } } - Ok(result.1) + Ok((result.1, result.0)) } diff --git a/src/commands/cpp.rs b/src/commands/cpp.rs index 9391bb3..79ca73b 100644 --- a/src/commands/cpp.rs +++ b/src/commands/cpp.rs @@ -5,6 +5,7 @@ use serenity::{ prelude::*, }; +use crate::managers::compilation::CompilationDetails; use crate::utls::discordhelpers::embeds::EmbedOptions; use crate::{ cache::{CompilerCache, ConfigCache, MessageCache, MessageCacheEntry}, @@ -19,7 +20,8 @@ use crate::{ #[aliases("c++")] #[bucket = "nospam"] pub async fn cpp(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let emb = handle_request(ctx.clone(), msg.content.clone(), msg.author.clone(), msg).await?; + let (emb, _) = + handle_request(ctx.clone(), msg.content.clone(), msg.author.clone(), msg).await?; // Dispatch our request let compilation_embed = embeds::dispatch_embed(&ctx.http, msg.channel_id, emb).await?; @@ -40,7 +42,7 @@ pub async fn handle_request( content: String, author: User, msg: &Message, -) -> std::result::Result { +) -> std::result::Result<(CreateEmbed, CompilationDetails), CommandError> { let loading_reaction = { let data_read = ctx.data.read().await; let botinfo_lock = data_read.get::().unwrap(); @@ -98,7 +100,6 @@ pub async fn handle_request( // remove our loading emote discordhelpers::delete_bot_reacts(&ctx, msg, loading_reaction).await?; - let options = EmbedOptions::new(false, fake_parse.target.clone(), String::default()); - - Ok(result.1.to_embed(&author, &options)) + let options = EmbedOptions::new(false, result.0.clone()); + Ok((result.1.to_embed(&author, &options), result.0)) } diff --git a/src/events.rs b/src/events.rs index 972cf49..50b3d60 100644 --- a/src/events.rs +++ b/src/events.rs @@ -14,6 +14,7 @@ use std::env; use tokio::sync::MutexGuard; use chrono::{DateTime, Utc}; +use serenity::model::application::component::ButtonStyle; use crate::{ cache::*, @@ -198,7 +199,7 @@ impl EventHandler for Handler { let _ = new_message.delete_reactions(&ctx.http).await; if collector.is_some() { let prefix = env::var("BOT_PREFIX").expect("Bot prefix is not set!"); - let emb = match handle_request( + let (emb, details) = match handle_request( ctx.clone(), format!("{}compile\n```{}\n{}\n```", prefix, language, code), new_message.author.clone(), @@ -206,7 +207,7 @@ impl EventHandler for Handler { ) .await { - Ok(emb) => emb, + Ok((emb, details)) => (emb, details), Err(e) => { let emb = embeds::build_fail_embed( &new_message.author, @@ -227,8 +228,28 @@ impl EventHandler for Handler { return; } }; - let _ = - embeds::dispatch_embed(&ctx.http, new_message.channel_id, emb).await; + + // Send our final embed + let mut new_msg = embeds::embed_message(emb); + if let Some(b64) = details.base64 { + new_msg.components(|cmp| { + cmp.create_action_row(|row| { + row.create_button(|btn| { + btn.style(ButtonStyle::Link) + .url(format!("https://godbolt.org/clientstate/{}", b64)) + .label("View on godbolt.org") + }) + }) + }); + } + + let _ = new_message + .channel_id + .send_message(&ctx.http, |e| { + *e = new_msg.clone(); + e + }) + .await; } } } diff --git a/src/managers/compilation.rs b/src/managers/compilation.rs index 813b029..dd087c5 100644 --- a/src/managers/compilation.rs +++ b/src/managers/compilation.rs @@ -13,10 +13,11 @@ use crate::utls::discordhelpers::embeds::{EmbedOptions, ToEmbed}; use crate::utls::parser::ParserResult; // struct containing any information resolved during the compilation step -#[derive(Default)] +#[derive(Default, Clone)] pub struct CompilationDetails { pub language: String, pub compiler: String, + pub base64: Option, } //Traits for compiler lookup @@ -82,15 +83,13 @@ impl CompilationManager { RequestHandler::CompilerExplorer => { let result = self.compiler_explorer(parser_result).await?; - let options = - EmbedOptions::new(false, result.0.language.clone(), result.0.compiler.clone()); + let options = EmbedOptions::new(false, result.0.clone()); Ok((result.0, result.1.to_embed(author, &options))) } RequestHandler::WandBox => { let result = self.wandbox(parser_result).await?; - let options = - EmbedOptions::new(false, result.0.language.clone(), result.0.compiler.clone()); + let options = EmbedOptions::new(false, result.0.clone()); Ok((result.0, result.1.to_embed(author, &options))) } RequestHandler::None => { @@ -110,7 +109,7 @@ impl CompilationManager { &self, parse_result: &ParserResult, author: &User, - ) -> Result<(String, CreateEmbed), CommandError> { + ) -> Result<(CompilationDetails, CreateEmbed), CommandError> { let gbolt = match &self.gbolt { Some(gbolt) => gbolt, None => { @@ -151,9 +150,17 @@ impl CompilationManager { Err(CommandError::from(format!("Target '{}' either does not produce assembly or is not currently supported on godbolt.org", target))) } Some(compiler) => { - let response = Godbolt::send_request(&compiler, &parse_result.code, options, USER_AGENT).await?; - let options = EmbedOptions::new(true, target.to_string(), compiler.name); - Ok((compiler.lang, response.to_embed(author, &options))) + let response = Godbolt::send_request(&compiler, &parse_result.code, options.clone(), USER_AGENT).await?; + let base64 = Godbolt::get_base64(&compiler, &parse_result.code, options)?; + + let details = CompilationDetails { + language: target.to_string(), + compiler: compiler.name, + base64: Some(base64) + }; + + let options = EmbedOptions::new(true, details.clone()); + Ok((details, response.to_embed(author, &options))) } } } @@ -201,12 +208,6 @@ impl CompilationManager { }; let compiler = gbolt.resolve(target).unwrap(); - // report discovered information - let details = CompilationDetails { - compiler: compiler.name.clone(), - language: compiler.lang.clone(), - }; - // add boilerplate code if needed & fix common mistakes let mut code = parse_result.code.clone(); { @@ -217,7 +218,16 @@ impl CompilationManager { code = fix_common_problems(&compiler.lang, code); } + let base64 = Godbolt::get_base64(&compiler, &code, options.clone())?; let response = Godbolt::send_request(&compiler, &code, options, USER_AGENT).await?; + + // report discovered information + let details = CompilationDetails { + compiler: compiler.name.clone(), + language: compiler.lang.clone(), + base64: Some(base64), + }; + Ok((details, response)) } diff --git a/src/slashcmds/cpp.rs b/src/slashcmds/cpp.rs index e34a4c8..e558d4c 100644 --- a/src/slashcmds/cpp.rs +++ b/src/slashcmds/cpp.rs @@ -60,7 +60,7 @@ pub async fn cpp(ctx: &Context, msg: &ApplicationCommandInteraction) -> CommandR let data_read = ctx.data.read().await; let compiler_lock = data_read.get::().unwrap().read().await; let result = compiler_lock.compiler_explorer(&fake_parse).await?; - let options = EmbedOptions::new(false, fake_parse.target.clone(), String::default()); + let options = EmbedOptions::new(false, result.0); msg.edit_original_interaction_response(&ctx.http, |resp| { resp.add_embed(result.1.to_embed(&msg.user, &options)) diff --git a/src/utls/discordhelpers/embeds.rs b/src/utls/discordhelpers/embeds.rs index 73d56e3..8a39b5a 100644 --- a/src/utls/discordhelpers/embeds.rs +++ b/src/utls/discordhelpers/embeds.rs @@ -2,12 +2,14 @@ use std::fmt::Write as _; use std::{env, str}; use serenity::http::Http; +use serenity::model::application::component::ButtonStyle; use serenity::{ builder::{CreateEmbed, CreateMessage}, client::Context, model::prelude::*, }; +use crate::managers::compilation::CompilationDetails; use wandbox::*; use crate::utls::constants::*; @@ -16,15 +18,14 @@ use crate::utls::discordhelpers; #[derive(Default)] pub struct EmbedOptions { pub is_assembly: bool, - pub lang: String, - pub compiler: String, + pub compilation_info: CompilationDetails, } + impl EmbedOptions { - pub fn new(is_assembly: bool, lang: String, compiler: String) -> Self { + pub fn new(is_assembly: bool, compilation_info: CompilationDetails) -> Self { EmbedOptions { is_assembly, - lang, - compiler, + compilation_info, } } } @@ -67,11 +68,11 @@ impl ToEmbed for wandbox::CompilationResult { embed.footer(|f| { let mut text = author.tag(); - if !options.lang.is_empty() { - text = format!("{} | {}", text, options.lang); + if !options.compilation_info.language.is_empty() { + text = format!("{} | {}", text, options.compilation_info.language); } - if !options.compiler.is_empty() { - text = format!("{} | {}", text, options.compiler); + if !options.compilation_info.compiler.is_empty() { + text = format!("{} | {}", text, options.compilation_info.compiler); } text = format!("{} | wandbox.org", text); @@ -197,11 +198,11 @@ impl ToEmbed for godbolt::GodboltResponse { if let Some(time) = self.execution_time { appendstr = format!("{} | {}ms", appendstr, time); } - if !options.lang.is_empty() { - appendstr = format!("{} | {}", appendstr, options.lang); + if !options.compilation_info.language.is_empty() { + appendstr = format!("{} | {}", appendstr, options.compilation_info.language); } - if !options.compiler.is_empty() { - appendstr = format!("{} | {}", appendstr, options.compiler); + if !options.compilation_info.compiler.is_empty() { + appendstr = format!("{} | {}", appendstr, options.compilation_info.compiler); } embed.footer(|f| f.text(format!("{} | godbolt.org", appendstr))); @@ -209,9 +210,28 @@ impl ToEmbed for godbolt::GodboltResponse { } } -pub async fn edit_message_embed(ctx: &Context, old: &mut Message, emb: CreateEmbed) { +pub async fn edit_message_embed( + ctx: &Context, + old: &mut Message, + emb: CreateEmbed, + compilation_details: Option, +) { let _ = old .edit(ctx, |m| { + if let Some(details) = compilation_details { + if let Some(b64) = details.base64 { + m.components(|cmp| { + cmp.create_action_row(|row| { + row.create_button(|btn| { + btn.style(ButtonStyle::Link) + .url(format!("https://godbolt.org/clientstate/{}", b64)) + .label("View on godbolt.org") + }) + }) + }); + } + } + m.embed(|e| { e.0 = emb.0; e diff --git a/src/utls/discordhelpers/mod.rs b/src/utls/discordhelpers/mod.rs index 13daba1..c9465bb 100644 --- a/src/utls/discordhelpers/mod.rs +++ b/src/utls/discordhelpers/mod.rs @@ -15,6 +15,7 @@ use serenity::client::Context; use serenity::framework::standard::CommandResult; use tokio::sync::MutexGuard; +use crate::commands::compile; use std::fmt::Write as _; pub fn build_menu_items( @@ -121,7 +122,7 @@ pub async fn handle_edit( .await { let err = embeds::build_fail_embed(&author, &e.to_string()); - embeds::edit_message_embed(ctx, &mut old, err).await; + embeds::edit_message_embed(ctx, &mut old, err, None).await; } } else if content.starts_with(&format!("{}compile", prefix)) { if let Err(e) = handle_edit_compile( @@ -134,7 +135,7 @@ pub async fn handle_edit( .await { let err = embeds::build_fail_embed(&author, &e.to_string()); - embeds::edit_message_embed(ctx, &mut old, err).await; + embeds::edit_message_embed(ctx, &mut old, err, None).await; } } else if content.starts_with(&format!("{}cpp", prefix)) { if let Err(e) = handle_edit_cpp( @@ -147,11 +148,11 @@ pub async fn handle_edit( .await { let err = embeds::build_fail_embed(&author, &e.to_string()); - embeds::edit_message_embed(ctx, &mut old, err).await; + embeds::edit_message_embed(ctx, &mut old, err, None).await; } } else { let err = embeds::build_fail_embed(&author, "Invalid command for edit functionality!"); - embeds::edit_message_embed(ctx, &mut old, err).await; + embeds::edit_message_embed(ctx, &mut old, err, None).await; } } @@ -162,13 +163,13 @@ pub async fn handle_edit_cpp( mut old: Message, original_msg: Message, ) -> CommandResult { - let embed = + let (embed, details) = crate::commands::cpp::handle_request(ctx.clone(), content, author, &original_msg).await?; let compilation_successful = embed.0.get("color").unwrap() == COLOR_OKAY; discordhelpers::send_completion_react(ctx, &old, compilation_successful).await?; - embeds::edit_message_embed(ctx, &mut old, embed).await; + embeds::edit_message_embed(ctx, &mut old, embed, Some(details)).await; Ok(()) } @@ -179,14 +180,13 @@ pub async fn handle_edit_compile( mut old: Message, original_msg: Message, ) -> CommandResult { - let embed = - crate::commands::compile::handle_request(ctx.clone(), content, author, &original_msg) - .await?; + let (embed, compilation_details) = + compile::handle_request(ctx.clone(), content, author, &original_msg).await?; let compilation_successful = embed.0.get("color").unwrap() == COLOR_OKAY; discordhelpers::send_completion_react(ctx, &old, compilation_successful).await?; - embeds::edit_message_embed(ctx, &mut old, embed).await; + embeds::edit_message_embed(ctx, &mut old, embed, Some(compilation_details)).await; Ok(()) } @@ -197,11 +197,11 @@ pub async fn handle_edit_asm( mut old: Message, original_msg: Message, ) -> CommandResult { - let emb = + let (emb, details) = crate::commands::asm::handle_request(ctx.clone(), content, author, &original_msg).await?; let success = emb.0.get("color").unwrap() == COLOR_OKAY; - embeds::edit_message_embed(ctx, &mut old, emb).await; + embeds::edit_message_embed(ctx, &mut old, emb, Some(details)).await; send_completion_react(ctx, &old, success).await?; Ok(())