From c30c649886c29a16b32f23a3b57092a2717fc1b2 Mon Sep 17 00:00:00 2001 From: Alathreon Date: Sat, 23 Dec 2023 17:19:51 +0100 Subject: [PATCH 01/10] [Feature/JShell] Using the new reworked jshell api --- application/build.gradle | 1 + .../tjbot/features/code/EvalCodeCommand.java | 10 +- .../tjbot/features/jshell/JShellCommand.java | 76 +++++---- .../tjbot/features/jshell/JShellEval.java | 21 +-- .../tjbot/features/jshell/ResultRenderer.java | 89 ---------- .../backend/dto/JShellEvalAbortion.java | 10 ++ .../backend/dto/JShellEvalAbortionCause.java | 45 ++++++ .../backend/dto/JShellExceptionResult.java | 10 -- .../jshell/backend/dto/JShellResult.java | 36 ++--- .../backend/dto/JShellSnippetResult.java | 23 +++ .../jshell/backend/dto/SnippetStatus.java | 14 +- .../jshell/renderer/RenderResult.java | 11 ++ .../jshell/renderer/RendererUtils.java | 47 ++++++ .../jshell/renderer/ResultEmbedRenderer.java | 149 +++++++++++++++++ .../jshell/renderer/ResultEmbedRenderer2.java | 152 ++++++++++++++++++ .../jshell/renderer/ResultFileRenderer.java | 92 +++++++++++ .../jshell/renderer/ResultRenderer.java | 32 ++++ .../jshell/renderer/package-info.java | 11 ++ .../tjbot/features/utils/Colors.java | 3 +- 19 files changed, 663 insertions(+), 169 deletions(-) delete mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/ResultRenderer.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortion.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortionCause.java delete mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellExceptionResult.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellSnippetResult.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer2.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultFileRenderer.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/package-info.java diff --git a/application/build.gradle b/application/build.gradle index a83ab2c422..48009b9ac5 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -67,6 +67,7 @@ dependencies { implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jacksonVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + implementation "com.sigpwned:jackson-modules-java17-sealed-classes:0.0.0" implementation 'com.github.freva:ascii-table:1.8.0' diff --git a/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java index ad6cc91a3c..787caa1ddb 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java @@ -4,6 +4,7 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import org.togetherjava.tjbot.features.jshell.JShellEval; +import org.togetherjava.tjbot.features.jshell.renderer.RenderResult; import org.togetherjava.tjbot.features.utils.CodeFence; import org.togetherjava.tjbot.features.utils.Colors; import org.togetherjava.tjbot.features.utils.ConnectionFailedException; @@ -34,7 +35,14 @@ public MessageEmbed apply(CodeFence codeFence) { .build(); } try { - return jshellEval.evaluateAndRespond(null, codeFence.code(), false, false); + RenderResult renderResult = jshellEval.evaluateAndRespond(null, codeFence.code(), false, false); + if(renderResult instanceof RenderResult.EmbedResult em) { + return em.embeds().get(0); //TODO -_- + } else { + return new EmbedBuilder().setColor(Colors.ERROR_COLOR) + .setDescription("The result was too big to be displayed") //TODO, maybe return the description only ? + .build(); + } } catch (RequestFailedException | ConnectionFailedException e) { return new EmbedBuilder().setColor(Colors.ERROR_COLOR) .setDescription("Request failed: " + e.getMessage()) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java index 44bb6ba37f..10654208f5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java @@ -1,6 +1,7 @@ package org.togetherjava.tjbot.features.jshell; import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.User; @@ -15,11 +16,13 @@ import net.dv8tion.jda.api.interactions.components.text.TextInput; import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; import net.dv8tion.jda.api.interactions.modals.ModalMapping; +import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.FileUpload; import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; import org.togetherjava.tjbot.features.jshell.backend.JShellApi; +import org.togetherjava.tjbot.features.jshell.renderer.RenderResult; import org.togetherjava.tjbot.features.utils.Colors; import org.togetherjava.tjbot.features.utils.ConnectionFailedException; import org.togetherjava.tjbot.features.utils.MessageUtils; @@ -27,6 +30,7 @@ import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; @@ -103,7 +107,7 @@ public void onModalSubmitted(ModalInteractionEvent event, List args) { mapping = event.getValue(TEXT_INPUT_PART_ID); } if (mapping != null) { - handleEval(event, event.getUser(), true, mapping.getAsString(), startupScript); + handleEval(event, event.getMember(), true, mapping.getAsString(), startupScript); } } @@ -125,7 +129,7 @@ private void handleEvalCommand(SlashCommandInteractionEvent event) { if (code == null) { sendEvalModal(event, startupScript); } else { - handleEval(event, event.getUser(), true, code.getAsString(), startupScript); + handleEval(event, event.getMember(), true, code.getAsString(), startupScript); } } @@ -150,14 +154,20 @@ private void sendEvalModal(SlashCommandInteractionEvent event, boolean startupSc * @param startupScript if the startup script should be used or not * @param code the code */ - private void handleEval(IReplyCallback replyCallback, @Nullable User user, boolean showCode, - String code, boolean startupScript) { + private void handleEval(IReplyCallback replyCallback, @Nullable Member user, boolean showCode, + String code, boolean startupScript) { replyCallback.deferReply().queue(interactionHook -> { try { - interactionHook - .editOriginalEmbeds( - jshellEval.evaluateAndRespond(user, code, showCode, startupScript)) - .queue(); + RenderResult renderResult = jshellEval.evaluateAndRespond(user, code, showCode, startupScript); + if(renderResult instanceof RenderResult.EmbedResult em) { + em.embeds().forEach(e -> interactionHook.sendMessageEmbeds(e).queue()); + } else if(renderResult instanceof RenderResult.FileResult file) { + interactionHook + .editOriginalAttachments(AttachedFile.fromData(file.content().getBytes(StandardCharsets.UTF_8), "Result.java")) + .queue(); + } else { + throw new AssertionError(); + } } catch (RequestFailedException | ConnectionFailedException e) { interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(user, e)).queue(); } @@ -167,7 +177,7 @@ private void handleEval(IReplyCallback replyCallback, @Nullable User user, boole private void handleSnippetsCommand(SlashCommandInteractionEvent event) { event.deferReply().queue(interactionHook -> { OptionMapping userOption = event.getOption(USER_PARAMETER); - User user = userOption == null ? event.getUser() : userOption.getAsUser(); + Member user = Objects.requireNonNull(userOption == null ? event.getMember() : userOption.getAsMember()); OptionMapping includeStartupScriptOption = event.getOption(INCLUDE_STARTUP_SCRIPT_PARAMETER); boolean includeStartupScript = @@ -194,7 +204,7 @@ private void handleSnippetsCommand(SlashCommandInteractionEvent event) { }); } - private void sendSnippets(InteractionHook interactionHook, User user, List snippets) { + private void sendSnippets(InteractionHook interactionHook, Member user, List snippets) { if (canBeSentAsEmbed(snippets)) { sendSnippetsAsEmbed(interactionHook, user, snippets); } else if (canBeSentAsFile(snippets)) { @@ -212,10 +222,10 @@ private boolean canBeSentAsEmbed(List snippets) { && snippets.size() <= MessageUtils.MAXIMUM_VISIBLE_EMBEDS; } - private void sendSnippetsAsEmbed(InteractionHook interactionHook, User user, + private void sendSnippetsAsEmbed(InteractionHook interactionHook, Member user, List snippets) { EmbedBuilder builder = new EmbedBuilder().setColor(Colors.SUCCESS_COLOR) - .setAuthor(user.getName()) + .setAuthor(user.getEffectiveName()) .setTitle(snippetsTitle(user)); int i = 1; for (String snippet : snippets) { @@ -227,35 +237,41 @@ private void sendSnippetsAsEmbed(InteractionHook interactionHook, User user, private boolean canBeSentAsFile(List snippets) { return snippets.stream() - .mapToInt(s -> (s + "// Snippet 10").getBytes().length) + .mapToInt(s -> (s + " // Snippet 1000").getBytes().length) .sum() < Message.MAX_FILE_SIZE; } - private void sendSnippetsAsFile(InteractionHook interactionHook, User user, + private void sendSnippetsAsFile(InteractionHook interactionHook, Member user, List snippets) { StringBuilder sb = new StringBuilder(); int i = 1; for (String snippet : snippets) { - sb.append("// Snippet ").append(i).append("\n").append(snippet); + snippet = snippet.replaceAll("^\n+", ""); + if(!snippet.endsWith("\n")) { + snippet += "\n"; + } + int idxOf = snippet.indexOf("\n"); + int insertIndex = idxOf != -1 ? idxOf : snippet.length(); + sb.append(snippet, 0, insertIndex).append(" // Snippet ").append(i).append(snippet.substring(insertIndex)); i++; } interactionHook .editOriginalEmbeds(new EmbedBuilder().setColor(Colors.SUCCESS_COLOR) - .setAuthor(user.getName()) + .setAuthor(user.getEffectiveName()) .setTitle(snippetsTitle(user)) .build()) - .setFiles(FileUpload.fromData(sb.toString().getBytes(), snippetsTitle(user))) + .setFiles(FileUpload.fromData(sb.toString().getBytes(), snippetsTitle(user) + ".java")) .queue(); } - private String snippetsTitle(User user) { - return user.getName() + "'s snippets"; + private String snippetsTitle(Member user) { + return user.getEffectiveName() + "'s snippets"; } - private void sendSnippetsTooLong(InteractionHook interactionHook, User user) { + private void sendSnippetsTooLong(InteractionHook interactionHook, Member user) { interactionHook .editOriginalEmbeds(new EmbedBuilder().setColor(Colors.ERROR_COLOR) - .setAuthor(user.getName()) + .setAuthor(user.getEffectiveName()) .setTitle("Too much code to send...") .build()) .queue(); @@ -266,13 +282,13 @@ private void handleCloseCommand(SlashCommandInteractionEvent event) { jshellEval.getApi().closeSession(event.getUser().getId()); } catch (RequestFailedException e) { if (e.getStatus() == JShellApi.SESSION_NOT_FOUND) { - event.replyEmbeds(createSessionNotFoundErrorEmbed(event.getUser())).queue(); + event.replyEmbeds(createSessionNotFoundErrorEmbed(Objects.requireNonNull(event.getMember()))).queue(); } else { - event.replyEmbeds(createUnexpectedErrorEmbed(event.getUser(), e)).queue(); + event.replyEmbeds(createUnexpectedErrorEmbed(event.getMember(), e)).queue(); } return; } catch (ConnectionFailedException e) { - event.replyEmbeds(createUnexpectedErrorEmbed(event.getUser(), e)).queue(); + event.replyEmbeds(createUnexpectedErrorEmbed(event.getMember(), e)).queue(); return; } @@ -296,23 +312,23 @@ private void handleStartupScriptCommand(SlashCommandInteractionEvent event) { .build()) .queue(); } catch (RequestFailedException | ConnectionFailedException e) { - event.replyEmbeds(createUnexpectedErrorEmbed(event.getUser(), e)).queue(); + event.replyEmbeds(createUnexpectedErrorEmbed(event.getMember(), e)).queue(); } }); } - private MessageEmbed createSessionNotFoundErrorEmbed(User user) { - return new EmbedBuilder().setAuthor(user.getName() + "'s result") + private MessageEmbed createSessionNotFoundErrorEmbed(Member user) { + return new EmbedBuilder().setAuthor(user.getEffectiveName() + "'s result") .setColor(Colors.ERROR_COLOR) - .setDescription("Could not find session for user " + user.getName()) + .setDescription("Could not find session for user " + user.getEffectiveName()) .build(); } - private MessageEmbed createUnexpectedErrorEmbed(@Nullable User user, Exception e) { + private MessageEmbed createUnexpectedErrorEmbed(@Nullable Member user, Exception e) { EmbedBuilder embedBuilder = new EmbedBuilder().setColor(Colors.ERROR_COLOR) .setDescription("Request failed: " + e.getMessage()); if (user != null) { - embedBuilder.setAuthor(user.getName() + "'s result"); + embedBuilder.setAuthor(user.getEffectiveName() + "'s result"); } return embedBuilder.build(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java index 381c787cd0..cea97c2af7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java @@ -1,7 +1,9 @@ package org.togetherjava.tjbot.features.jshell; import com.fasterxml.jackson.databind.ObjectMapper; +import com.sigpwned.jackson.modules.jdk17.sealedclasses.Jdk17SealedClassesModule; import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.utils.TimeFormat; @@ -9,6 +11,8 @@ import org.togetherjava.tjbot.config.JShellConfig; import org.togetherjava.tjbot.features.jshell.backend.JShellApi; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; +import org.togetherjava.tjbot.features.jshell.renderer.RenderResult; +import org.togetherjava.tjbot.features.jshell.renderer.ResultRenderer; import org.togetherjava.tjbot.features.utils.Colors; import org.togetherjava.tjbot.features.utils.ConnectionFailedException; import org.togetherjava.tjbot.features.utils.RateLimiter; @@ -18,6 +22,7 @@ import java.time.Duration; import java.time.Instant; +import java.util.List; /** * Provides a mid-ground between JDA and JShell API which can be used from many places in the bot, @@ -35,7 +40,7 @@ public class JShellEval { * @param config the JShell configuration to use */ public JShellEval(JShellConfig config) { - this.api = new JShellApi(new ObjectMapper(), config.baseUrl()); + this.api = new JShellApi(new ObjectMapper().registerModule(new Jdk17SealedClassesModule()), config.baseUrl()); this.renderer = new ResultRenderer(); this.rateLimiter = new RateLimiter(Duration.ofSeconds(config.rateLimitWindowSeconds()), @@ -58,11 +63,11 @@ public JShellApi getApi() { * @throws ConnectionFailedException if the connection to the API couldn't be made at the first * place */ - public MessageEmbed evaluateAndRespond(@Nullable User user, String code, boolean showCode, - boolean startupScript) throws RequestFailedException, ConnectionFailedException { + public RenderResult evaluateAndRespond(@Nullable Member user, String code, boolean showCode, + boolean startupScript) throws RequestFailedException, ConnectionFailedException { MessageEmbed rateLimitedMessage = wasRateLimited(user, Instant.now()); if (rateLimitedMessage != null) { - return rateLimitedMessage; + return new RenderResult.EmbedResult(List.of(rateLimitedMessage)); } JShellResult result; if (user == null) { @@ -71,13 +76,11 @@ public MessageEmbed evaluateAndRespond(@Nullable User user, String code, boolean result = api.evalSession(code, user.getId(), startupScript); } - return renderer - .renderToEmbed(user, showCode ? code : null, user != null, result, new EmbedBuilder()) - .build(); + return renderer.render(user, showCode, result); } @Nullable - private MessageEmbed wasRateLimited(@Nullable User user, Instant checkTime) { + private MessageEmbed wasRateLimited(@Nullable Member user, Instant checkTime) { if (rateLimiter.allowRequest(checkTime)) { return null; } @@ -89,7 +92,7 @@ private MessageEmbed wasRateLimited(@Nullable User user, Instant checkTime) { "You are currently rate-limited. Please try again " + nextAllowedTime + ".") .setColor(Colors.ERROR_COLOR); if (user != null) { - embedBuilder.setAuthor(user.getName() + "'s result"); + embedBuilder.setAuthor(user.getEffectiveName() + "'s result"); } return embedBuilder.build(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/ResultRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/ResultRenderer.java deleted file mode 100644 index 9d930249d9..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/ResultRenderer.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.togetherjava.tjbot.features.jshell; - -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.User; - -import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; -import org.togetherjava.tjbot.features.jshell.backend.dto.SnippetStatus; -import org.togetherjava.tjbot.features.utils.MessageUtils; - -import javax.annotation.Nullable; - -import java.awt.Color; - -import static org.togetherjava.tjbot.features.utils.Colors.*; - -/** - * Allows to render JShell results. - */ -class ResultRenderer { - - /** - * Renders a JShell result to an embed. - * - * @param originator the user from who to display snippet ownership, won't be displayed if null - * @param originalCode the original code to display, won't be displayed if null - * @param partOfSession if it was part of a regular session, or a one time session - * @param result the JShell result - * @param builder the embed builder - * @return the ember builder, for chaining - */ - public EmbedBuilder renderToEmbed(@Nullable User originator, @Nullable String originalCode, - boolean partOfSession, JShellResult result, EmbedBuilder builder) { - if (originator != null) { - builder.setAuthor(originator.getName() + "'s result"); - } - builder.setColor(color(result.status())); - - if (originalCode != null - && originalCode.length() + "```\n```".length() < MessageEmbed.VALUE_MAX_LENGTH) { - builder.setDescription("```java\n" + originalCode + "```"); - builder.addField( - originator == null ? "Original code" : (originator.getName() + "'s code"), - "```java\n" + originalCode + "```", false); - } - - if (result.result() != null && !result.result().isBlank()) { - builder.addField("Snippet result", result.result(), false); - } - if (result.status() == SnippetStatus.ABORTED) { - builder.setTitle("Request timed out"); - } - - String description = getDescriptionFromResult(result); - description = MessageUtils.abbreviate(description, MessageEmbed.DESCRIPTION_MAX_LENGTH); - if (result.stdoutOverflow() && !description.endsWith(MessageUtils.ABBREVIATION)) { - description += MessageUtils.ABBREVIATION; - } - builder.setDescription(description); - - if (partOfSession) { - builder.setFooter("Snippet " + result.id() + " of current session"); - } else { - builder.setFooter("This result is not part of a session"); - } - - return builder; - } - - private String getDescriptionFromResult(JShellResult result) { - if (result.exception() != null) { - return result.exception().exceptionClass() + ":" - + result.exception().exceptionMessage(); - } - if (!result.errors().isEmpty()) { - return String.join(", ", result.errors()); - } - return result.stdout(); - } - - private Color color(SnippetStatus status) { - return switch (status) { - case VALID -> SUCCESS_COLOR; - case RECOVERABLE_DEFINED, RECOVERABLE_NOT_DEFINED -> WARNING_COLOR; - case REJECTED, ABORTED -> ERROR_COLOR; - }; - } - -} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortion.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortion.java new file mode 100644 index 0000000000..9bc8829b56 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortion.java @@ -0,0 +1,10 @@ +package org.togetherjava.tjbot.features.jshell.backend.dto; + +/** + * Represents an abortion of a JShell snippet. + * @param sourceCause the source which caused this abortion + * @param remainingSource the remaining source code which couldn't be executed because of this abortion + * @param cause the cause of this abortion + */ +public record JShellEvalAbortion(String sourceCause, String remainingSource, JShellEvalAbortionCause cause) { +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortionCause.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortionCause.java new file mode 100644 index 0000000000..bd25877d0b --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortionCause.java @@ -0,0 +1,45 @@ +package org.togetherjava.tjbot.features.jshell.backend.dto; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.List; + +/** + * The cause of an abortion, see the implementations of this sealed interface for the possible causes. + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME) +public sealed interface JShellEvalAbortionCause { + + /** + * The timeout was exceeded. + */ + @JsonTypeName("TIMEOUT") + record TimeoutAbortionCause() implements JShellEvalAbortionCause { + } + + /** + * An exception was thrown but never caught. + * + * @param exceptionClass the class of the exception + * @param exceptionMessage the message of the exception + */ + @JsonTypeName("UNCAUGHT_EXCEPTION") + record UnhandledExceptionAbortionCause(String exceptionClass, String exceptionMessage) implements JShellEvalAbortionCause { + } + + /** + * The code doesn't compile, but at least the syntax is correct. + * @param errors the compilation errors + */ + @JsonTypeName("COMPILE_TIME_ERROR") + record CompileTimeErrorAbortionCause(List errors) implements JShellEvalAbortionCause { + } + + /** + * The code doesn't compile, and the syntax itself isn't correct. + */ + @JsonTypeName("SYNTAX_ERROR") + record SyntaxErrorAbortionCause() implements JShellEvalAbortionCause { + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellExceptionResult.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellExceptionResult.java deleted file mode 100644 index dbffefda69..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellExceptionResult.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.togetherjava.tjbot.features.jshell.backend.dto; - -/** - * The thrown exception. - * - * @param exceptionClass the class of the exception - * @param exceptionMessage the message of the exception - */ -public record JShellExceptionResult(String exceptionClass, String exceptionMessage) { -} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellResult.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellResult.java index 11be982c91..e468dfc391 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellResult.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellResult.java @@ -1,43 +1,31 @@ package org.togetherjava.tjbot.features.jshell.backend.dto; -import jdk.jshell.SnippetEvent; - import javax.annotation.Nullable; - import java.util.List; /** * Result of a JShell eval. - * - * @param status {@link SnippetStatus status} of the snippet - * @param type {@link SnippetType type} of the snippet - * @param id {@link jdk.jshell.Snippet#id() id} of the snippet - * @param source source code of the snippet - * @param result {@link SnippetEvent#value() result} of the snippet, usually null if the source code - * wasn't executed or if an exception happened during the execution, see related doc - * @param exception exception thrown by the executed code, null if no exception was thrown + * + * @param snippetsResults the results of each individual snippet + * @param abortion represents an abortion, if any of the snippet couldn't end properly, preventing the following snippets to execute, can be null * @param stdoutOverflow if stdout has overflowed and was truncated * @param stdout what was printed by the snippet - * @param errors the compilations errors of the snippet */ -public record JShellResult(SnippetStatus status, SnippetType type, String id, String source, - @Nullable String result, @Nullable JShellExceptionResult exception, boolean stdoutOverflow, - String stdout, List errors) { +public record JShellResult( + List snippetsResults, + @Nullable JShellEvalAbortion abortion, + boolean stdoutOverflow, + String stdout) { /** * The JShell result. - * - * @param status status of the snippet - * @param type type of the snippet - * @param id id of the snippet - * @param source source code of the snippet - * @param result result of the snippet, nullable - * @param exception thrown exception, nullable + * + * @param snippetsResults the results of each individual snippet + * @param abortion represents an abortion, if any of the snippet couldn't end properly, preventing the following snippets to execute, can be null * @param stdoutOverflow if stdout has overflowed and was truncated * @param stdout what was printed by the snippet - * @param errors the compilations errors of the snippet */ public JShellResult { - errors = List.copyOf(errors); + snippetsResults = List.copyOf(snippetsResults); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellSnippetResult.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellSnippetResult.java new file mode 100644 index 0000000000..6d43c1c422 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellSnippetResult.java @@ -0,0 +1,23 @@ +package org.togetherjava.tjbot.features.jshell.backend.dto; + +import jdk.jshell.SnippetEvent; + +import javax.annotation.Nullable; + +/** + * Result of a JShell eval of a snippet. + * + * @param status {@link SnippetStatus status} of the snippet + * @param type {@link SnippetType type} of the snippet + * @param id {@link jdk.jshell.Snippet#id() id} of the snippet + * @param source source code of the snippet + * @param result {@link SnippetEvent#value() result} of the snippet, usually null if the source code + * wasn't executed or if an exception happened during the execution, see related doc + */ +public record JShellSnippetResult( + SnippetStatus status, + SnippetType type, + int id, + String source, + @Nullable String result) { +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/SnippetStatus.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/SnippetStatus.java index 63306ab0b2..530d64b1fd 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/SnippetStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/SnippetStatus.java @@ -1,8 +1,7 @@ package org.togetherjava.tjbot.features.jshell.backend.dto; /** - * The status of the snippet, see {@link jdk.jshell.Snippet.Status} for most of them, and evaluation - * timeout of the JShell REST API for {@link SnippetStatus#ABORTED ABORTED}. + * The status of the snippet, see {@link jdk.jshell.Snippet.Status}. */ public enum SnippetStatus { /** @@ -20,9 +19,14 @@ public enum SnippetStatus { /** * See {@link jdk.jshell.Snippet.Status#REJECTED}. */ - REJECTED, + REJECTED; + /** - * Used when the timeout of an evaluation is reached. + * Returns the name of the constant, with _ replaced by spaces. + * @return the name of the constant, with _ replaced by spaces */ - ABORTED + @Override + public String toString() { + return name().replace('_', ' '); + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java new file mode 100644 index 0000000000..9122867b8e --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java @@ -0,0 +1,11 @@ +package org.togetherjava.tjbot.features.jshell.renderer; + +import net.dv8tion.jda.api.entities.MessageEmbed; + +import java.util.List; + +// TODO delete ? +public sealed interface RenderResult { + record EmbedResult(List embeds) implements RenderResult {} + record FileResult(String content) implements RenderResult {} +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java new file mode 100644 index 0000000000..8a584bd8f2 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java @@ -0,0 +1,47 @@ +package org.togetherjava.tjbot.features.jshell.renderer; + +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortionCause; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; +import org.togetherjava.tjbot.features.jshell.backend.dto.SnippetStatus; + +class RendererUtils { + private RendererUtils() { + } + + static String abortionCauseToString(JShellEvalAbortionCause abortionCause) { + if (abortionCause instanceof JShellEvalAbortionCause.TimeoutAbortionCause) { + return "Allowed time exceeded."; + } else if (abortionCause instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause c) { + return "Uncaught exception:\n" + c.exceptionClass() + ":" + c.exceptionMessage(); + } else if (abortionCause instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause c) { + return "The code doesn't compile:\n" + String.join("\n", c.errors()); + } else if (abortionCause instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause) { + return "The code doesn't compile, there are syntax errors in this code."; + } + throw new AssertionError(); + } + + enum GeneralStatus { + SUCCESS, PARTIAL_SUCCESS, ERROR + } + static GeneralStatus getGeneralStatus(JShellResult result) { + if (result.snippetsResults().isEmpty() && result.abortion() == null) return GeneralStatus.SUCCESS; // Empty = success + if (result.snippetsResults().isEmpty()) + return GeneralStatus.ERROR; // Only abortion = failure, special case for syntax error + if (result.snippetsResults().size() == 1 && result.abortion() != null // Only abortion = failure, case for all except syntax error + && !(result.abortion().cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause)) + return GeneralStatus.ERROR; + + if (result.abortion() != null) return GeneralStatus.PARTIAL_SUCCESS; // At least one snippet is a success + + return getGeneralStatus(result.snippetsResults().get(result.snippetsResults().size() - 1).status()); + } + + private static GeneralStatus getGeneralStatus(SnippetStatus status) { + return switch (status) { + case VALID -> GeneralStatus.SUCCESS; + case RECOVERABLE_DEFINED, RECOVERABLE_NOT_DEFINED -> GeneralStatus.PARTIAL_SUCCESS; + case REJECTED -> GeneralStatus.ERROR; + }; + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java new file mode 100644 index 0000000000..3f51c2e4c0 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java @@ -0,0 +1,149 @@ +package org.togetherjava.tjbot.features.jshell.renderer; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.User; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortion; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellSnippetResult; +import org.togetherjava.tjbot.features.utils.MessageUtils; + +import javax.annotation.Nullable; +import java.awt.*; + +import static org.togetherjava.tjbot.features.utils.Colors.*; + +/** + * Allows to render JShell results. + */ +class ResultEmbedRenderer { + + /** + * Renders a JShell result to an embed. + * + * @param originator the user from who to display snippet ownership, won't be displayed if null + * @param showCode if the original should be displayed + * @param result the JShell result + * @return the ember builder + */ + public MessageEmbed renderToEmbed(@Nullable User originator, boolean showCode, JShellResult result) { + EmbedBuilder builder = new EmbedBuilder(); + if (originator != null) { + builder.setAuthor(originator.getName() + "'s result"); + } + builder.setColor(getStatusColor(result)); + + setResultToEmbed(showCode, builder, result); + + return builder.build(); + } + boolean canBeSentAsEmbed(@Nullable User originator, boolean showCode, JShellResult result) { + if(showCode) { + int descLength = "```java\n".length(); + for (JShellSnippetResult r : result.snippetsResults()) { + descLength += """ + // Snippet %d, %s + %s + jshell> %s + """.formatted(r.id(), r.status(), r.source(), r.result() == null ? "" : r.result()).length(); + } + descLength += "```".length(); + if(descLength > MessageEmbed.DESCRIPTION_MAX_LENGTH) { + return false; + } + int abortionLength = 0; + if(result.abortion() != null) { + abortionLength += abortionToString(result.abortion()).length(); + if(abortionLength > MessageEmbed.VALUE_MAX_LENGTH) { + return false; + } + abortionLength += "[WARNING] The code couldn't end properly...".length(); + } + int resultLength = getResultLength(result); + return authorLength(originator) + descLength + abortionLength + resultLength <= MessageEmbed.EMBED_MAX_LENGTH_BOT; + } else { + int abortionLength = 0; + if(result.abortion() != null) { + abortionLength += ("[WARNING] The code couldn't end properly...\n" + abortionToString(result.abortion())).length(); + if(abortionLength > MessageEmbed.DESCRIPTION_MAX_LENGTH) { + return false; + } + } + int resultLength = getResultLength(result); + return authorLength(originator) + abortionLength + resultLength <= MessageEmbed.EMBED_MAX_LENGTH_BOT; + } + } + private int getResultLength(JShellResult result) { + return result.stdout().isEmpty() + ? "No result".length() + : ("Result" + getSdtOut(result)).length(); + } + private int authorLength(@Nullable User originator) { + return originator == null ? 0 : (originator.getName() + "'s result").length(); + } + + private void setResultToEmbed(boolean showCode, EmbedBuilder builder, JShellResult result) { + if (showCode) { + StringBuilder sb = new StringBuilder("```java\n"); + for (JShellSnippetResult r : result.snippetsResults()) { + sb.append(""" + // Snippet %d, %s + %s + jshell> %s + """.formatted(r.id(), r.status(), r.source(), r.result() == null ? "" : r.result())); + } + sb.append("```"); + builder.setDescription(sb.toString()); + if(result.abortion() != null) { + builder.addField("[WARNING] The code couldn't end properly...", abortionToString(result.abortion()), false); + } + } else { + if(result.abortion() != null) { + builder.setDescription("[WARNING] The code couldn't end properly...\n" + abortionToString(result.abortion())); + } + } + setStdoutToEmbed(builder, result); + } + private void setStdoutToEmbed(EmbedBuilder builder, JShellResult result) { + if(result.stdout().isEmpty()) { + builder.addField("No result", "", false); + } else { + builder.addField("Result", getSdtOut(result), false); + } + } + + private String getSdtOut(JShellResult result) { + String stdout = result.stdout(); + if(result.stdoutOverflow()) { + stdout += MessageUtils.ABBREVIATION; + } + return MessageUtils.abbreviate(stdout, MessageEmbed.VALUE_MAX_LENGTH); + } + + private String abortionToString(JShellEvalAbortion abortion) { + String s = """ + Problematic source code: + ```java + %s + ``` + Cause: + %s + + """.formatted(abortion.sourceCause(), RendererUtils.abortionCauseToString(abortion.cause())); + if(!abortion.remainingSource().isEmpty()) { + s += """ + Remaining code: + ```java + %s```""".formatted( abortion.remainingSource()); + } + return s; + } + + private Color getStatusColor(JShellResult result) { + return switch (RendererUtils.getGeneralStatus(result)) { + case SUCCESS -> SUCCESS_COLOR; + case PARTIAL_SUCCESS -> PARTIAL_SUCCESS_COLOR; + case ERROR -> ERROR_COLOR; + }; + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer2.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer2.java new file mode 100644 index 0000000000..36ab86a27c --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer2.java @@ -0,0 +1,152 @@ +package org.togetherjava.tjbot.features.jshell.renderer; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortion; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellSnippetResult; +import org.togetherjava.tjbot.features.utils.MessageUtils; + +import javax.annotation.Nullable; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.togetherjava.tjbot.features.utils.Colors.*; + +/** + * Allows to render JShell results. + */ +class ResultEmbedRenderer2 { + private static final int ABORTION_CODE_SIZE_LIMIT = 2000; + + /** + * Renders a JShell result to an embed. + * + * @param originator the user from who to display snippet ownership, won't be displayed if null + * @param showCode if the original should be displayed + * @param result the JShell result + * @return the embeds + */ + public List renderToEmbed(@Nullable Member originator, boolean showCode, JShellResult result) { + List builder = new ArrayList<>(); + + setResultToEmbed(showCode, builder, result); + + String author = originator == null ? null : originator.getEffectiveName() + "'s result"; + Color color = getStatusColor(result); + + return setMetadataAndBuild(generateEmbeds(builder).toList(), author, color); + } + + private Stream generateEmbeds(List segments) { + List embeds = new ArrayList<>(); + StringBuilder currentEmbedDescription = new StringBuilder(); + for(String segment : segments) { + if(currentEmbedDescription.length() + "\n".length() + segment.length() < MessageEmbed.DESCRIPTION_MAX_LENGTH) { + currentEmbedDescription.append(segment).append("\n"); + } else { + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setDescription(currentEmbedDescription); + embeds.add(embedBuilder); + currentEmbedDescription = new StringBuilder(segment); + } + } + if(!currentEmbedDescription.isEmpty()) { + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setDescription(currentEmbedDescription); + embeds.add(embedBuilder); + } + return embeds.stream(); + } private List setMetadataAndBuild(List embedBuilders, @Nullable String author, Color color) { + int i = 1; + List embeds = new ArrayList<>(); + for(EmbedBuilder segment : embedBuilders) { + setMetadata(segment, author, embedBuilders.size() > 1 ? "Part %d / %d".formatted(i, embedBuilders.size()) : null, color); + embeds.add(segment.build()); + i++; + } + return embeds; + } + + /** + * Sets metadata like author and color to an embed. + * @param embedBuilder the embed + * @param author the author to set + * @param footer the footer to set + * @param color the color to set + */ + private void setMetadata(EmbedBuilder embedBuilder, @Nullable String author, @Nullable String footer, Color color) { + if(author != null) { + embedBuilder.setAuthor(author); + } + if(footer != null) { + embedBuilder.setFooter(footer); + } + embedBuilder.setColor(color); + } + + private void setResultToEmbed(boolean showCode, List builder, JShellResult result) { + if (showCode) { + builder.add("## Snippets\n"); + for (JShellSnippetResult r : result.snippetsResults()) { + builder.add(""" + ### Snippet %d, %s + ```java + %s```%s""".formatted(r.id(), r.status(), r.source(), resultToString(r.result()))); + } + } + if(result.abortion() != null) { + builder.add("## [WARNING] The code couldn't end properly...\n" + abortionToString(result.abortion())); + } + builder.add(stdoutToString(result)); + } + private String resultToString(@Nullable String result) { + if(result == null || result.isEmpty()) return ""; + if(!result.contains("\n")) return "\njshell> `" + result + "`"; + return "\njshell> ↓```\n" + result + "```"; + } + private String stdoutToString(JShellResult result) { + if(result.stdout().isEmpty()) { + return "## System out\n[Nothing]\n"; + } else { + return "## System out\n```\n" + getSdtOut(result) + "```"; + } + } + + private String getSdtOut(JShellResult result) { + String stdout = result.stdout(); + if(result.stdoutOverflow()) { + stdout += MessageUtils.ABBREVIATION; + } + return stdout; + } + + private String abortionToString(JShellEvalAbortion abortion) { + String s = """ + Problematic source code: + ```java + %s``` + Cause: + %s + """.formatted(MessageUtils.abbreviate(abortion.sourceCause(), ABORTION_CODE_SIZE_LIMIT), RendererUtils.abortionCauseToString(abortion.cause())); + if(!abortion.remainingSource().isEmpty()) { + s += """ + + Remaining code: + ```java + %s```""".formatted(MessageUtils.abbreviate(abortion.remainingSource(), ABORTION_CODE_SIZE_LIMIT)); + } + return s; + } + + private Color getStatusColor(JShellResult result) { + return switch (RendererUtils.getGeneralStatus(result)) { + case SUCCESS -> SUCCESS_COLOR; + case PARTIAL_SUCCESS -> PARTIAL_SUCCESS_COLOR; + case ERROR -> ERROR_COLOR; + }; + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultFileRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultFileRenderer.java new file mode 100644 index 0000000000..0c66099e97 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultFileRenderer.java @@ -0,0 +1,92 @@ +package org.togetherjava.tjbot.features.jshell.renderer; + +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortion; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellSnippetResult; +import org.togetherjava.tjbot.features.utils.MessageUtils; + +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; + +class ResultFileRenderer { + private static final String SUCCESS = "SUCCESS!"; + private static final String PARTIAL_SUCCESS = "PARTIAL SUCCESS -_-"; + private static final String ERROR = "ERROR... :(!"; + + /** + * Renders a JShell result to a file. + * + * @param originator the user from who to display snippet ownership, won't be displayed if null + * @param showCode if the original should be displayed + * @param result the JShell result + * @return the content + */ + public String renderToFileString(@Nullable Member originator, boolean showCode, JShellResult result) { + StringBuilder builder = new StringBuilder(); + builder.append("// ").append(getGeneralStatus(result)).append("\n"); + + if (originator != null) { + builder.append("// ").append(originator.getEffectiveName()).append("'s result\n\n"); + } + + setResultToBuilder(showCode, builder, result); + + return builder.toString(); + } + + private void setResultToBuilder(boolean showCode, StringBuilder builder, JShellResult result) { + if (showCode) { + for (JShellSnippetResult r : result.snippetsResults()) { + builder.append(""" + // Snippet %d, %s + %s + jshell> %s + """.formatted(r.id(), r.status(), r.source(), r.result() == null ? "" : r.result())); + } + } + if (result.abortion() != null) { + builder.append('\n'); + builder.append("// [WARNING] The code couldn't end properly...\n"); + builder.append(abortionToString(result.abortion())); + } + setStdoutToBuilder(builder, result); + } + + private void setStdoutToBuilder(StringBuilder builder, JShellResult result) { + if (result.stdout().isEmpty()) { + builder.append("// No result"); + } else { + builder.append("// Result:\n"); + builder.append(getSdtOut(result)); + } + } + + private String getSdtOut(JShellResult result) { + String stdout = result.stdout(); + stdout = "// " + stdout.replace("\n", "\n// "); + if (result.stdoutOverflow()) { + stdout += MessageUtils.ABBREVIATION; + } + return stdout; + } + + private String abortionToString(JShellEvalAbortion abortion) { + return """ + // Cause: + // %s + // + // Remaining code: + %s + """.formatted(RendererUtils.abortionCauseToString(abortion.cause()), abortion.remainingSource()); + } + + private String getGeneralStatus(JShellResult result) { + return switch (RendererUtils.getGeneralStatus(result)) { + case SUCCESS -> SUCCESS; + case PARTIAL_SUCCESS -> PARTIAL_SUCCESS; + case ERROR -> ERROR; + }; + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java new file mode 100644 index 0000000000..6803de9e52 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java @@ -0,0 +1,32 @@ +package org.togetherjava.tjbot.features.jshell.renderer; + +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; + +import javax.annotation.Nullable; + +/** + * Allows to render JShell results. + */ +public class ResultRenderer { + private final ResultEmbedRenderer2 embedRenderer = new ResultEmbedRenderer2();//TODO + private final ResultFileRenderer fileRenderer = new ResultFileRenderer(); + + /** + * Renders a JShell result. + * + * @param originator the user from who to display snippet ownership, won't be displayed if null + * @param showCode if the original should be displayed + * @param result the JShell result + * @return the result + */ + public RenderResult render(@Nullable Member originator, boolean showCode, JShellResult result) { + if(true/*embedRenderer.canBeSentAsEmbed(originator, showCode, result) TODO*/) { + return new RenderResult.EmbedResult(embedRenderer.renderToEmbed(originator, showCode, result)); + } else { + return new RenderResult.FileResult(fileRenderer.renderToFileString(originator, showCode, result)); + } + } + +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/package-info.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/package-info.java new file mode 100644 index 0000000000..f9fc5adcd8 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/package-info.java @@ -0,0 +1,11 @@ +/** + * This packages is used to render results from JShell. The core class is + * {@link org.togetherjava.tjbot.features.jshell.renderer.ResultRenderer}. + */ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package org.togetherjava.tjbot.features.jshell.renderer; + +import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/features/utils/Colors.java b/application/src/main/java/org/togetherjava/tjbot/features/utils/Colors.java index 8222bf3c52..9c364fc4fc 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/utils/Colors.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/utils/Colors.java @@ -12,6 +12,7 @@ private Colors() { public static final Color ERROR_COLOR = new Color(255, 99, 71); public static final Color SUCCESS_COLOR = new Color(118, 255, 0); - public static final Color WARNING_COLOR = new Color(255, 181, 71); + public static final Color WARNING_COLOR = new Color(255, 255, 0); + public static final Color PARTIAL_SUCCESS_COLOR = new Color(255, 140, 71); } From 4b5746149f8f798d3eb9de4330610e9f5e35900f Mon Sep 17 00:00:00 2001 From: Alathreon Date: Sat, 23 Dec 2023 17:20:02 +0100 Subject: [PATCH 02/10] [Feature/JShell] Reworked renderer --- .../togetherjava/tjbot/features/Features.java | 2 +- .../tjbot/features/code/EvalCodeCommand.java | 11 +- .../tjbot/features/jshell/JShellCommand.java | 18 +-- .../tjbot/features/jshell/JShellEval.java | 10 +- .../jshell/renderer/RendererUtils.java | 53 ++++++ .../jshell/renderer/ResultEmbedRenderer.java | 132 ++++----------- .../jshell/renderer/ResultEmbedRenderer2.java | 152 ------------------ ...eRenderer.java => ResultGistRenderer.java} | 58 ++++++- .../renderer/ResultMinimalEmbedRenderer.java | 46 ++++++ .../jshell/renderer/ResultRenderer.java | 31 ++-- 10 files changed, 212 insertions(+), 301 deletions(-) delete mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer2.java rename application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/{ResultFileRenderer.java => ResultGistRenderer.java} (56%) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultMinimalEmbedRenderer.java diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 9a48a3159e..5d18b2ca64 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -79,7 +79,7 @@ private Features() { */ public static Collection createFeatures(JDA jda, Database database, Config config) { FeatureBlacklistConfig blacklistConfig = config.getFeatureBlacklistConfig(); - JShellEval jshellEval = new JShellEval(config.getJshell()); + JShellEval jshellEval = new JShellEval(config.getJshell(), config.getGistApiKey()); TagSystem tagSystem = new TagSystem(database); BookmarksSystem bookmarksSystem = new BookmarksSystem(config, database); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java index 787caa1ddb..3de78f0d2d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java @@ -2,9 +2,7 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; - import org.togetherjava.tjbot.features.jshell.JShellEval; -import org.togetherjava.tjbot.features.jshell.renderer.RenderResult; import org.togetherjava.tjbot.features.utils.CodeFence; import org.togetherjava.tjbot.features.utils.Colors; import org.togetherjava.tjbot.features.utils.ConnectionFailedException; @@ -35,14 +33,7 @@ public MessageEmbed apply(CodeFence codeFence) { .build(); } try { - RenderResult renderResult = jshellEval.evaluateAndRespond(null, codeFence.code(), false, false); - if(renderResult instanceof RenderResult.EmbedResult em) { - return em.embeds().get(0); //TODO -_- - } else { - return new EmbedBuilder().setColor(Colors.ERROR_COLOR) - .setDescription("The result was too big to be displayed") //TODO, maybe return the description only ? - .build(); - } + return jshellEval.evaluateAndRespond(null, codeFence.code(), false, false); } catch (RequestFailedException | ConnectionFailedException e) { return new EmbedBuilder().setColor(Colors.ERROR_COLOR) .setDescription("Request failed: " + e.getMessage()) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java index 10654208f5..9c90e89a67 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java @@ -4,7 +4,6 @@ import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.InteractionHook; @@ -16,21 +15,16 @@ import net.dv8tion.jda.api.interactions.components.text.TextInput; import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; import net.dv8tion.jda.api.interactions.modals.ModalMapping; -import net.dv8tion.jda.api.utils.AttachedFile; import net.dv8tion.jda.api.utils.FileUpload; - import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; import org.togetherjava.tjbot.features.jshell.backend.JShellApi; -import org.togetherjava.tjbot.features.jshell.renderer.RenderResult; import org.togetherjava.tjbot.features.utils.Colors; import org.togetherjava.tjbot.features.utils.ConnectionFailedException; import org.togetherjava.tjbot.features.utils.MessageUtils; import org.togetherjava.tjbot.features.utils.RequestFailedException; import javax.annotation.Nullable; - -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; @@ -158,16 +152,8 @@ private void handleEval(IReplyCallback replyCallback, @Nullable Member user, boo String code, boolean startupScript) { replyCallback.deferReply().queue(interactionHook -> { try { - RenderResult renderResult = jshellEval.evaluateAndRespond(user, code, showCode, startupScript); - if(renderResult instanceof RenderResult.EmbedResult em) { - em.embeds().forEach(e -> interactionHook.sendMessageEmbeds(e).queue()); - } else if(renderResult instanceof RenderResult.FileResult file) { - interactionHook - .editOriginalAttachments(AttachedFile.fromData(file.content().getBytes(StandardCharsets.UTF_8), "Result.java")) - .queue(); - } else { - throw new AssertionError(); - } + MessageEmbed messageEmbed = jshellEval.evaluateAndRespond(user, code, showCode, startupScript); + interactionHook.sendMessageEmbeds(messageEmbed).queue(); } catch (RequestFailedException | ConnectionFailedException e) { interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(user, e)).queue(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java index cea97c2af7..ff7e5abf5a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java @@ -29,6 +29,7 @@ * including JShell commands and JShell code actions. */ public class JShellEval { + private final String gistApiToken; private final JShellApi api; private final ResultRenderer renderer; @@ -39,7 +40,8 @@ public class JShellEval { * * @param config the JShell configuration to use */ - public JShellEval(JShellConfig config) { + public JShellEval(JShellConfig config, String gistApiToken) { + this.gistApiToken = gistApiToken; this.api = new JShellApi(new ObjectMapper().registerModule(new Jdk17SealedClassesModule()), config.baseUrl()); this.renderer = new ResultRenderer(); @@ -63,11 +65,11 @@ public JShellApi getApi() { * @throws ConnectionFailedException if the connection to the API couldn't be made at the first * place */ - public RenderResult evaluateAndRespond(@Nullable Member user, String code, boolean showCode, + public MessageEmbed evaluateAndRespond(@Nullable Member user, String code, boolean showCode, boolean startupScript) throws RequestFailedException, ConnectionFailedException { MessageEmbed rateLimitedMessage = wasRateLimited(user, Instant.now()); if (rateLimitedMessage != null) { - return new RenderResult.EmbedResult(List.of(rateLimitedMessage)); + return rateLimitedMessage; } JShellResult result; if (user == null) { @@ -76,7 +78,7 @@ public RenderResult evaluateAndRespond(@Nullable Member user, String code, boole result = api.evalSession(code, user.getId(), startupScript); } - return renderer.render(user, showCode, result); + return renderer.render(gistApiToken, user, showCode, result); } @Nullable diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java index 8a584bd8f2..dd708a00f5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java @@ -1,8 +1,18 @@ package org.togetherjava.tjbot.features.jshell.renderer; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortionCause; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import org.togetherjava.tjbot.features.jshell.backend.dto.SnippetStatus; +import org.togetherjava.tjbot.features.utils.MessageUtils; + +import javax.annotation.Nullable; +import java.awt.*; +import java.util.List; +import java.util.Optional; + +import static org.togetherjava.tjbot.features.utils.Colors.*; class RendererUtils { private RendererUtils() { @@ -36,6 +46,13 @@ static GeneralStatus getGeneralStatus(JShellResult result) { return getGeneralStatus(result.snippetsResults().get(result.snippetsResults().size() - 1).status()); } + static Color getStatusColor(JShellResult result) { + return switch (RendererUtils.getGeneralStatus(result)) { + case SUCCESS -> SUCCESS_COLOR; + case PARTIAL_SUCCESS -> PARTIAL_SUCCESS_COLOR; + case ERROR -> ERROR_COLOR; + }; + } private static GeneralStatus getGeneralStatus(SnippetStatus status) { return switch (status) { @@ -44,4 +61,40 @@ private static GeneralStatus getGeneralStatus(SnippetStatus status) { case REJECTED -> GeneralStatus.ERROR; }; } + + static Optional generateEmbed(List segments) { + StringBuilder currentEmbedDescription = new StringBuilder(); + for(String segment : segments) { + if(currentEmbedDescription.length() + "\n".length() + segment.length() < MessageEmbed.DESCRIPTION_MAX_LENGTH) { + currentEmbedDescription.append(segment).append("\n"); + } else { + return Optional.empty(); + } + } + return Optional.of(new EmbedBuilder().setDescription(currentEmbedDescription)); + } + + static MessageEmbed setMetadataAndBuild(EmbedBuilder embedBuilder, @Nullable String author, Color color) { + if(author != null) { + embedBuilder.setAuthor(author); + } + embedBuilder.setColor(color); + return embedBuilder.build(); + } + + static String stdoutToMarkdownString(JShellResult result) { + if(result.stdout().isEmpty()) { + return "## System out\n[Nothing]\n"; + } else { + return "## System out\n```\n" + getSdtOut(result) + "```"; + } + } + + static String getSdtOut(JShellResult result) { + String stdout = result.stdout(); + if(result.stdoutOverflow()) { + stdout += MessageUtils.ABBREVIATION; + } + return stdout; + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java index 3f51c2e4c0..cef2cd0a0b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java @@ -1,8 +1,7 @@ package org.togetherjava.tjbot.features.jshell.renderer; -import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.User; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortion; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellSnippetResult; @@ -10,140 +9,71 @@ import javax.annotation.Nullable; import java.awt.*; - -import static org.togetherjava.tjbot.features.utils.Colors.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; /** * Allows to render JShell results. */ class ResultEmbedRenderer { + private static final int ABORTION_CODE_SIZE_LIMIT = 2000; /** - * Renders a JShell result to an embed. + * Renders a JShell result to an embed, empty if too big. * * @param originator the user from who to display snippet ownership, won't be displayed if null * @param showCode if the original should be displayed * @param result the JShell result - * @return the ember builder + * @return the embed, or empty if too big */ - public MessageEmbed renderToEmbed(@Nullable User originator, boolean showCode, JShellResult result) { - EmbedBuilder builder = new EmbedBuilder(); - if (originator != null) { - builder.setAuthor(originator.getName() + "'s result"); - } - builder.setColor(getStatusColor(result)); + public Optional renderToEmbed(@Nullable Member originator, boolean showCode, JShellResult result) { + List builder = new ArrayList<>(); setResultToEmbed(showCode, builder, result); - return builder.build(); - } - boolean canBeSentAsEmbed(@Nullable User originator, boolean showCode, JShellResult result) { - if(showCode) { - int descLength = "```java\n".length(); - for (JShellSnippetResult r : result.snippetsResults()) { - descLength += """ - // Snippet %d, %s - %s - jshell> %s - """.formatted(r.id(), r.status(), r.source(), r.result() == null ? "" : r.result()).length(); - } - descLength += "```".length(); - if(descLength > MessageEmbed.DESCRIPTION_MAX_LENGTH) { - return false; - } - int abortionLength = 0; - if(result.abortion() != null) { - abortionLength += abortionToString(result.abortion()).length(); - if(abortionLength > MessageEmbed.VALUE_MAX_LENGTH) { - return false; - } - abortionLength += "[WARNING] The code couldn't end properly...".length(); - } - int resultLength = getResultLength(result); - return authorLength(originator) + descLength + abortionLength + resultLength <= MessageEmbed.EMBED_MAX_LENGTH_BOT; - } else { - int abortionLength = 0; - if(result.abortion() != null) { - abortionLength += ("[WARNING] The code couldn't end properly...\n" + abortionToString(result.abortion())).length(); - if(abortionLength > MessageEmbed.DESCRIPTION_MAX_LENGTH) { - return false; - } - } - int resultLength = getResultLength(result); - return authorLength(originator) + abortionLength + resultLength <= MessageEmbed.EMBED_MAX_LENGTH_BOT; - } - } - private int getResultLength(JShellResult result) { - return result.stdout().isEmpty() - ? "No result".length() - : ("Result" + getSdtOut(result)).length(); - } - private int authorLength(@Nullable User originator) { - return originator == null ? 0 : (originator.getName() + "'s result").length(); + String author = originator == null ? null : originator.getEffectiveName() + "'s result"; + Color color = RendererUtils.getStatusColor(result); + + return RendererUtils.generateEmbed(builder).map(e -> RendererUtils.setMetadataAndBuild(e, author, color)); } - private void setResultToEmbed(boolean showCode, EmbedBuilder builder, JShellResult result) { + private void setResultToEmbed(boolean showCode, List builder, JShellResult result) { if (showCode) { - StringBuilder sb = new StringBuilder("```java\n"); + builder.add("## Snippets\n"); for (JShellSnippetResult r : result.snippetsResults()) { - sb.append(""" - // Snippet %d, %s - %s - jshell> %s - """.formatted(r.id(), r.status(), r.source(), r.result() == null ? "" : r.result())); - } - sb.append("```"); - builder.setDescription(sb.toString()); - if(result.abortion() != null) { - builder.addField("[WARNING] The code couldn't end properly...", abortionToString(result.abortion()), false); - } - } else { - if(result.abortion() != null) { - builder.setDescription("[WARNING] The code couldn't end properly...\n" + abortionToString(result.abortion())); + builder.add(""" + ### Snippet %d, %s + ```java + %s```%s""".formatted(r.id(), r.status(), r.source(), resultToString(r.result()))); } } - setStdoutToEmbed(builder, result); - } - private void setStdoutToEmbed(EmbedBuilder builder, JShellResult result) { - if(result.stdout().isEmpty()) { - builder.addField("No result", "", false); - } else { - builder.addField("Result", getSdtOut(result), false); + if(result.abortion() != null) { + builder.add("## [WARNING] The code couldn't end properly...\n" + abortionToString(result.abortion())); } + builder.add(RendererUtils.stdoutToMarkdownString(result)); } - - private String getSdtOut(JShellResult result) { - String stdout = result.stdout(); - if(result.stdoutOverflow()) { - stdout += MessageUtils.ABBREVIATION; - } - return MessageUtils.abbreviate(stdout, MessageEmbed.VALUE_MAX_LENGTH); + private String resultToString(@Nullable String result) { + if(result == null || result.isEmpty()) return ""; + if(!result.contains("\n")) return "\njshell> `" + result + "`"; + return "\njshell> ↓```\n" + result + "```"; } private String abortionToString(JShellEvalAbortion abortion) { String s = """ Problematic source code: ```java - %s - ``` + %s``` Cause: %s - - """.formatted(abortion.sourceCause(), RendererUtils.abortionCauseToString(abortion.cause())); + """.formatted(MessageUtils.abbreviate(abortion.sourceCause(), ABORTION_CODE_SIZE_LIMIT), RendererUtils.abortionCauseToString(abortion.cause())); if(!abortion.remainingSource().isEmpty()) { s += """ - Remaining code: + + Remaining code: ```java - %s```""".formatted( abortion.remainingSource()); + %s```""".formatted(MessageUtils.abbreviate(abortion.remainingSource(), ABORTION_CODE_SIZE_LIMIT)); } return s; } - - private Color getStatusColor(JShellResult result) { - return switch (RendererUtils.getGeneralStatus(result)) { - case SUCCESS -> SUCCESS_COLOR; - case PARTIAL_SUCCESS -> PARTIAL_SUCCESS_COLOR; - case ERROR -> ERROR_COLOR; - }; - } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer2.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer2.java deleted file mode 100644 index 36ab86a27c..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer2.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.togetherjava.tjbot.features.jshell.renderer; - -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.MessageEmbed; -import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortion; -import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; -import org.togetherjava.tjbot.features.jshell.backend.dto.JShellSnippetResult; -import org.togetherjava.tjbot.features.utils.MessageUtils; - -import javax.annotation.Nullable; -import java.awt.*; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -import static org.togetherjava.tjbot.features.utils.Colors.*; - -/** - * Allows to render JShell results. - */ -class ResultEmbedRenderer2 { - private static final int ABORTION_CODE_SIZE_LIMIT = 2000; - - /** - * Renders a JShell result to an embed. - * - * @param originator the user from who to display snippet ownership, won't be displayed if null - * @param showCode if the original should be displayed - * @param result the JShell result - * @return the embeds - */ - public List renderToEmbed(@Nullable Member originator, boolean showCode, JShellResult result) { - List builder = new ArrayList<>(); - - setResultToEmbed(showCode, builder, result); - - String author = originator == null ? null : originator.getEffectiveName() + "'s result"; - Color color = getStatusColor(result); - - return setMetadataAndBuild(generateEmbeds(builder).toList(), author, color); - } - - private Stream generateEmbeds(List segments) { - List embeds = new ArrayList<>(); - StringBuilder currentEmbedDescription = new StringBuilder(); - for(String segment : segments) { - if(currentEmbedDescription.length() + "\n".length() + segment.length() < MessageEmbed.DESCRIPTION_MAX_LENGTH) { - currentEmbedDescription.append(segment).append("\n"); - } else { - EmbedBuilder embedBuilder = new EmbedBuilder(); - embedBuilder.setDescription(currentEmbedDescription); - embeds.add(embedBuilder); - currentEmbedDescription = new StringBuilder(segment); - } - } - if(!currentEmbedDescription.isEmpty()) { - EmbedBuilder embedBuilder = new EmbedBuilder(); - embedBuilder.setDescription(currentEmbedDescription); - embeds.add(embedBuilder); - } - return embeds.stream(); - } private List setMetadataAndBuild(List embedBuilders, @Nullable String author, Color color) { - int i = 1; - List embeds = new ArrayList<>(); - for(EmbedBuilder segment : embedBuilders) { - setMetadata(segment, author, embedBuilders.size() > 1 ? "Part %d / %d".formatted(i, embedBuilders.size()) : null, color); - embeds.add(segment.build()); - i++; - } - return embeds; - } - - /** - * Sets metadata like author and color to an embed. - * @param embedBuilder the embed - * @param author the author to set - * @param footer the footer to set - * @param color the color to set - */ - private void setMetadata(EmbedBuilder embedBuilder, @Nullable String author, @Nullable String footer, Color color) { - if(author != null) { - embedBuilder.setAuthor(author); - } - if(footer != null) { - embedBuilder.setFooter(footer); - } - embedBuilder.setColor(color); - } - - private void setResultToEmbed(boolean showCode, List builder, JShellResult result) { - if (showCode) { - builder.add("## Snippets\n"); - for (JShellSnippetResult r : result.snippetsResults()) { - builder.add(""" - ### Snippet %d, %s - ```java - %s```%s""".formatted(r.id(), r.status(), r.source(), resultToString(r.result()))); - } - } - if(result.abortion() != null) { - builder.add("## [WARNING] The code couldn't end properly...\n" + abortionToString(result.abortion())); - } - builder.add(stdoutToString(result)); - } - private String resultToString(@Nullable String result) { - if(result == null || result.isEmpty()) return ""; - if(!result.contains("\n")) return "\njshell> `" + result + "`"; - return "\njshell> ↓```\n" + result + "```"; - } - private String stdoutToString(JShellResult result) { - if(result.stdout().isEmpty()) { - return "## System out\n[Nothing]\n"; - } else { - return "## System out\n```\n" + getSdtOut(result) + "```"; - } - } - - private String getSdtOut(JShellResult result) { - String stdout = result.stdout(); - if(result.stdoutOverflow()) { - stdout += MessageUtils.ABBREVIATION; - } - return stdout; - } - - private String abortionToString(JShellEvalAbortion abortion) { - String s = """ - Problematic source code: - ```java - %s``` - Cause: - %s - """.formatted(MessageUtils.abbreviate(abortion.sourceCause(), ABORTION_CODE_SIZE_LIMIT), RendererUtils.abortionCauseToString(abortion.cause())); - if(!abortion.remainingSource().isEmpty()) { - s += """ - - Remaining code: - ```java - %s```""".formatted(MessageUtils.abbreviate(abortion.remainingSource(), ABORTION_CODE_SIZE_LIMIT)); - } - return s; - } - - private Color getStatusColor(JShellResult result) { - return switch (RendererUtils.getGeneralStatus(result)) { - case SUCCESS -> SUCCESS_COLOR; - case PARTIAL_SUCCESS -> PARTIAL_SUCCESS_COLOR; - case ERROR -> ERROR_COLOR; - }; - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultFileRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultGistRenderer.java similarity index 56% rename from application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultFileRenderer.java rename to application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultGistRenderer.java index 0c66099e97..eaff4645f5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultFileRenderer.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultGistRenderer.java @@ -1,29 +1,39 @@ package org.togetherjava.tjbot.features.jshell.renderer; +import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.MessageEmbed; +import org.kohsuke.github.GHGist; +import org.kohsuke.github.GHGistBuilder; +import org.kohsuke.github.GitHubBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.features.filesharing.FileSharingMessageListener; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortion; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellSnippetResult; import org.togetherjava.tjbot.features.utils.MessageUtils; import javax.annotation.Nullable; -import java.nio.charset.StandardCharsets; +import java.io.IOException; +import java.util.Optional; -class ResultFileRenderer { +class ResultGistRenderer { + private static final Logger logger = LoggerFactory.getLogger(ResultGistRenderer.class); private static final String SUCCESS = "SUCCESS!"; private static final String PARTIAL_SUCCESS = "PARTIAL SUCCESS -_-"; private static final String ERROR = "ERROR... :(!"; /** - * Renders a JShell result to a file. + * Renders a JShell result to a gist. * + * @param gistApiToken the token to use to send the result to gist * @param originator the user from who to display snippet ownership, won't be displayed if null * @param showCode if the original should be displayed * @param result the JShell result * @return the content */ - public String renderToFileString(@Nullable Member originator, boolean showCode, JShellResult result) { + public Optional renderToGist(String gistApiToken, @Nullable Member originator, boolean showCode, JShellResult result) { StringBuilder builder = new StringBuilder(); builder.append("// ").append(getGeneralStatus(result)).append("\n"); @@ -33,7 +43,33 @@ public String renderToFileString(@Nullable Member originator, boolean showCode, setResultToBuilder(showCode, builder, result); - return builder.toString(); + String text = builder.toString(); + + GHGist gist; + try { + GHGistBuilder gistBuilder = new GitHubBuilder().withOAuthToken(gistApiToken) + .build() + .createGist() + .public_(false); + if (originator != null) { + gistBuilder.description("Uploaded by " + originator.getEffectiveName()); + } + gistBuilder.file((originator == null ? "JShell" : originator.getEffectiveName()) + "'s result.java", text); + + gist = gistBuilder.create(); + } catch (IOException ex) { + logger.error("Couldn't send JShell result to Gist", ex); + return Optional.empty(); + } + + EmbedBuilder embedBuilder = new EmbedBuilder() + .setTitle("The result was too big and so was uploaded to Gist: " + gist.getHtmlUrl() + "\n↓This is the output of the JShell execution.↓") + .setDescription(getSdtOut(result).transform(s -> s.isBlank() ? "Nothing" : s)) + .setColor(RendererUtils.getStatusColor(result)); + if(originator != null) { + embedBuilder.setAuthor(originator.getEffectiveName() + "'s result"); + } + return Optional.of(embedBuilder.build()); } private void setResultToBuilder(boolean showCode, StringBuilder builder, JShellResult result) { @@ -59,11 +95,19 @@ private void setStdoutToBuilder(StringBuilder builder, JShellResult result) { builder.append("// No result"); } else { builder.append("// Result:\n"); - builder.append(getSdtOut(result)); + builder.append(getSdtOutAsComment(result)); } } private String getSdtOut(JShellResult result) { + String stdout = result.stdout().replace("\n", "\n// "); + if (result.stdoutOverflow()) { + stdout += MessageUtils.ABBREVIATION; + } + return stdout; + } + + private String getSdtOutAsComment(JShellResult result) { String stdout = result.stdout(); stdout = "// " + stdout.replace("\n", "\n// "); if (result.stdoutOverflow()) { diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultMinimalEmbedRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultMinimalEmbedRenderer.java new file mode 100644 index 0000000000..bc7266cb70 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultMinimalEmbedRenderer.java @@ -0,0 +1,46 @@ +package org.togetherjava.tjbot.features.jshell.renderer; + +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; + +import javax.annotation.Nullable; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Allows to render JShell results in a minimal way. + */ +class ResultMinimalEmbedRenderer { + + /** + * Renders a JShell result to an embed, empty if too big. + * + * @param originator the user from who to display snippet ownership, won't be displayed if null + * @param result the JShell result + * @return the embed, or empty if too big + */ + public Optional renderToEmbed(@Nullable Member originator, JShellResult result) { + List builder = new ArrayList<>(); + + setResultToEmbed(builder, result); + + String author = originator == null ? null : originator.getEffectiveName() + "'s result"; + Color color = RendererUtils.getStatusColor(result); + + return RendererUtils.generateEmbed(builder).map(e -> RendererUtils.setMetadataAndBuild(e, author, color)); + } + + private void setResultToEmbed(List builder, JShellResult result) { + if(result.abortion() != null) { + builder.add(""" + ## [WARNING] The code couldn't end properly + Cause: + %s + """.formatted(RendererUtils.abortionCauseToString(result.abortion().cause()))); + } + builder.add(RendererUtils.stdoutToMarkdownString(result)); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java index 6803de9e52..48055a6680 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java @@ -1,32 +1,43 @@ package org.togetherjava.tjbot.features.jshell.renderer; +import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.MessageEmbed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import javax.annotation.Nullable; +import java.awt.*; /** * Allows to render JShell results. */ public class ResultRenderer { - private final ResultEmbedRenderer2 embedRenderer = new ResultEmbedRenderer2();//TODO - private final ResultFileRenderer fileRenderer = new ResultFileRenderer(); + private static final Logger logger = LoggerFactory.getLogger(ResultRenderer.class); + private final ResultEmbedRenderer embedRenderer = new ResultEmbedRenderer(); + private final ResultGistRenderer gistRenderer = new ResultGistRenderer(); + private final ResultMinimalEmbedRenderer minimalEmbedRenderer = new ResultMinimalEmbedRenderer(); /** * Renders a JShell result. - * + * + * @param gistApiToken the token to use to send the result to gist, in case it is too big for an embed * @param originator the user from who to display snippet ownership, won't be displayed if null * @param showCode if the original should be displayed * @param result the JShell result * @return the result */ - public RenderResult render(@Nullable Member originator, boolean showCode, JShellResult result) { - if(true/*embedRenderer.canBeSentAsEmbed(originator, showCode, result) TODO*/) { - return new RenderResult.EmbedResult(embedRenderer.renderToEmbed(originator, showCode, result)); - } else { - return new RenderResult.FileResult(fileRenderer.renderToFileString(originator, showCode, result)); - } + public MessageEmbed render(String gistApiToken, @Nullable Member originator, boolean showCode, JShellResult result) { + return embedRenderer.renderToEmbed(originator, showCode, result) + .or(() -> gistRenderer.renderToGist(gistApiToken, originator, showCode, result)) + .or(() -> minimalEmbedRenderer.renderToEmbed(originator, result)) + .orElseGet(() -> renderFailure(result)); + } + + private MessageEmbed renderFailure(JShellResult result) { + logger.error("Couldn't render JShell result {} ", result); + return new EmbedBuilder().setTitle("Couldn't render the result, please contact a moderator.").setColor(Color.RED).build(); } } From 4fe716397507492e26e19a3bb0d0042ed9e1eb2e Mon Sep 17 00:00:00 2001 From: Alathreon Date: Sat, 23 Dec 2023 17:33:32 +0100 Subject: [PATCH 03/10] [feature/JShell] Running Spotless/Sonar --- .../tjbot/features/code/EvalCodeCommand.java | 1 + .../tjbot/features/jshell/JShellCommand.java | 22 ++++++--- .../tjbot/features/jshell/JShellEval.java | 8 ++-- .../backend/dto/JShellEvalAbortion.java | 7 ++- .../backend/dto/JShellEvalAbortionCause.java | 3 +- .../jshell/backend/dto/JShellResult.java | 14 +++--- .../backend/dto/JShellSnippetResult.java | 6 +-- .../jshell/backend/dto/SnippetStatus.java | 1 + .../jshell/renderer/RenderResult.java | 4 +- .../jshell/renderer/RendererUtils.java | 44 +++++++++++------ .../jshell/renderer/ResultEmbedRenderer.java | 48 ++++++++++++------- .../jshell/renderer/ResultGistRenderer.java | 34 +++++++------ .../renderer/ResultMinimalEmbedRenderer.java | 15 +++--- .../jshell/renderer/ResultRenderer.java | 22 ++++++--- 14 files changed, 142 insertions(+), 87 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java index 3de78f0d2d..ad6cc91a3c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/code/EvalCodeCommand.java @@ -2,6 +2,7 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; + import org.togetherjava.tjbot.features.jshell.JShellEval; import org.togetherjava.tjbot.features.utils.CodeFence; import org.togetherjava.tjbot.features.utils.Colors; diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java index 9c90e89a67..c064990c29 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java @@ -16,6 +16,7 @@ import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; import net.dv8tion.jda.api.interactions.modals.ModalMapping; import net.dv8tion.jda.api.utils.FileUpload; + import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; import org.togetherjava.tjbot.features.jshell.backend.JShellApi; @@ -25,6 +26,7 @@ import org.togetherjava.tjbot.features.utils.RequestFailedException; import javax.annotation.Nullable; + import java.util.List; import java.util.Objects; @@ -149,10 +151,11 @@ private void sendEvalModal(SlashCommandInteractionEvent event, boolean startupSc * @param code the code */ private void handleEval(IReplyCallback replyCallback, @Nullable Member user, boolean showCode, - String code, boolean startupScript) { + String code, boolean startupScript) { replyCallback.deferReply().queue(interactionHook -> { try { - MessageEmbed messageEmbed = jshellEval.evaluateAndRespond(user, code, showCode, startupScript); + MessageEmbed messageEmbed = + jshellEval.evaluateAndRespond(user, code, showCode, startupScript); interactionHook.sendMessageEmbeds(messageEmbed).queue(); } catch (RequestFailedException | ConnectionFailedException e) { interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(user, e)).queue(); @@ -163,7 +166,8 @@ private void handleEval(IReplyCallback replyCallback, @Nullable Member user, boo private void handleSnippetsCommand(SlashCommandInteractionEvent event) { event.deferReply().queue(interactionHook -> { OptionMapping userOption = event.getOption(USER_PARAMETER); - Member user = Objects.requireNonNull(userOption == null ? event.getMember() : userOption.getAsMember()); + Member user = Objects + .requireNonNull(userOption == null ? event.getMember() : userOption.getAsMember()); OptionMapping includeStartupScriptOption = event.getOption(INCLUDE_STARTUP_SCRIPT_PARAMETER); boolean includeStartupScript = @@ -233,12 +237,15 @@ private void sendSnippetsAsFile(InteractionHook interactionHook, Member user, int i = 1; for (String snippet : snippets) { snippet = snippet.replaceAll("^\n+", ""); - if(!snippet.endsWith("\n")) { + if (!snippet.endsWith("\n")) { snippet += "\n"; } int idxOf = snippet.indexOf("\n"); int insertIndex = idxOf != -1 ? idxOf : snippet.length(); - sb.append(snippet, 0, insertIndex).append(" // Snippet ").append(i).append(snippet.substring(insertIndex)); + sb.append(snippet, 0, insertIndex) + .append(" // Snippet ") + .append(i) + .append(snippet.substring(insertIndex)); i++; } interactionHook @@ -268,7 +275,10 @@ private void handleCloseCommand(SlashCommandInteractionEvent event) { jshellEval.getApi().closeSession(event.getUser().getId()); } catch (RequestFailedException e) { if (e.getStatus() == JShellApi.SESSION_NOT_FOUND) { - event.replyEmbeds(createSessionNotFoundErrorEmbed(Objects.requireNonNull(event.getMember()))).queue(); + event + .replyEmbeds(createSessionNotFoundErrorEmbed( + Objects.requireNonNull(event.getMember()))) + .queue(); } else { event.replyEmbeds(createUnexpectedErrorEmbed(event.getMember(), e)).queue(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java index ff7e5abf5a..a32b2a72d1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java @@ -5,13 +5,11 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.utils.TimeFormat; import org.togetherjava.tjbot.config.JShellConfig; import org.togetherjava.tjbot.features.jshell.backend.JShellApi; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; -import org.togetherjava.tjbot.features.jshell.renderer.RenderResult; import org.togetherjava.tjbot.features.jshell.renderer.ResultRenderer; import org.togetherjava.tjbot.features.utils.Colors; import org.togetherjava.tjbot.features.utils.ConnectionFailedException; @@ -22,7 +20,6 @@ import java.time.Duration; import java.time.Instant; -import java.util.List; /** * Provides a mid-ground between JDA and JShell API which can be used from many places in the bot, @@ -42,7 +39,8 @@ public class JShellEval { */ public JShellEval(JShellConfig config, String gistApiToken) { this.gistApiToken = gistApiToken; - this.api = new JShellApi(new ObjectMapper().registerModule(new Jdk17SealedClassesModule()), config.baseUrl()); + this.api = new JShellApi(new ObjectMapper().registerModule(new Jdk17SealedClassesModule()), + config.baseUrl()); this.renderer = new ResultRenderer(); this.rateLimiter = new RateLimiter(Duration.ofSeconds(config.rateLimitWindowSeconds()), @@ -66,7 +64,7 @@ public JShellApi getApi() { * place */ public MessageEmbed evaluateAndRespond(@Nullable Member user, String code, boolean showCode, - boolean startupScript) throws RequestFailedException, ConnectionFailedException { + boolean startupScript) throws RequestFailedException, ConnectionFailedException { MessageEmbed rateLimitedMessage = wasRateLimited(user, Instant.now()); if (rateLimitedMessage != null) { return rateLimitedMessage; diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortion.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortion.java index 9bc8829b56..0b9fa93ff3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortion.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortion.java @@ -2,9 +2,12 @@ /** * Represents an abortion of a JShell snippet. + * * @param sourceCause the source which caused this abortion - * @param remainingSource the remaining source code which couldn't be executed because of this abortion + * @param remainingSource the remaining source code which couldn't be executed because of this + * abortion * @param cause the cause of this abortion */ -public record JShellEvalAbortion(String sourceCause, String remainingSource, JShellEvalAbortionCause cause) { +public record JShellEvalAbortion(String sourceCause, String remainingSource, + JShellEvalAbortionCause cause) { } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortionCause.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortionCause.java index bd25877d0b..a696c685e3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortionCause.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellEvalAbortionCause.java @@ -6,7 +6,8 @@ import java.util.List; /** - * The cause of an abortion, see the implementations of this sealed interface for the possible causes. + * The cause of an abortion, see the implementations of this sealed interface for the possible + * causes. */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) public sealed interface JShellEvalAbortionCause { diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellResult.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellResult.java index e468dfc391..ecc44e3905 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellResult.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellResult.java @@ -1,27 +1,27 @@ package org.togetherjava.tjbot.features.jshell.backend.dto; import javax.annotation.Nullable; + import java.util.List; /** * Result of a JShell eval. * * @param snippetsResults the results of each individual snippet - * @param abortion represents an abortion, if any of the snippet couldn't end properly, preventing the following snippets to execute, can be null + * @param abortion represents an abortion, if any of the snippet couldn't end properly, preventing + * the following snippets to execute, can be null * @param stdoutOverflow if stdout has overflowed and was truncated * @param stdout what was printed by the snippet */ -public record JShellResult( - List snippetsResults, - @Nullable JShellEvalAbortion abortion, - boolean stdoutOverflow, - String stdout) { +public record JShellResult(List snippetsResults, + @Nullable JShellEvalAbortion abortion, boolean stdoutOverflow, String stdout) { /** * The JShell result. * * @param snippetsResults the results of each individual snippet - * @param abortion represents an abortion, if any of the snippet couldn't end properly, preventing the following snippets to execute, can be null + * @param abortion represents an abortion, if any of the snippet couldn't end properly, + * preventing the following snippets to execute, can be null * @param stdoutOverflow if stdout has overflowed and was truncated * @param stdout what was printed by the snippet */ diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellSnippetResult.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellSnippetResult.java index 6d43c1c422..87ab95684e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellSnippetResult.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/JShellSnippetResult.java @@ -14,10 +14,6 @@ * @param result {@link SnippetEvent#value() result} of the snippet, usually null if the source code * wasn't executed or if an exception happened during the execution, see related doc */ -public record JShellSnippetResult( - SnippetStatus status, - SnippetType type, - int id, - String source, +public record JShellSnippetResult(SnippetStatus status, SnippetType type, int id, String source, @Nullable String result) { } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/SnippetStatus.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/SnippetStatus.java index 530d64b1fd..4d61b09616 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/SnippetStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/backend/dto/SnippetStatus.java @@ -23,6 +23,7 @@ public enum SnippetStatus { /** * Returns the name of the constant, with _ replaced by spaces. + * * @return the name of the constant, with _ replaced by spaces */ @Override diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java index 9122867b8e..ec9ea10266 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java @@ -5,7 +5,9 @@ import java.util.List; // TODO delete ? -public sealed interface RenderResult { +public sealed + +interface RenderResult { record EmbedResult(List embeds) implements RenderResult {} record FileResult(String content) implements RenderResult {} } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java index dd708a00f5..9453e66309 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java @@ -2,12 +2,14 @@ import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; + import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortionCause; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import org.togetherjava.tjbot.features.jshell.backend.dto.SnippetStatus; import org.togetherjava.tjbot.features.utils.MessageUtils; import javax.annotation.Nullable; + import java.awt.*; import java.util.List; import java.util.Optional; @@ -15,8 +17,7 @@ import static org.togetherjava.tjbot.features.utils.Colors.*; class RendererUtils { - private RendererUtils() { - } + private RendererUtils() {} static String abortionCauseToString(JShellEvalAbortionCause abortionCause) { if (abortionCause instanceof JShellEvalAbortionCause.TimeoutAbortionCause) { @@ -32,20 +33,31 @@ static String abortionCauseToString(JShellEvalAbortionCause abortionCause) { } enum GeneralStatus { - SUCCESS, PARTIAL_SUCCESS, ERROR + SUCCESS, + PARTIAL_SUCCESS, + ERROR } + static GeneralStatus getGeneralStatus(JShellResult result) { - if (result.snippetsResults().isEmpty() && result.abortion() == null) return GeneralStatus.SUCCESS; // Empty = success + if (result.snippetsResults().isEmpty() && result.abortion() == null) + return GeneralStatus.SUCCESS; // Empty = success if (result.snippetsResults().isEmpty()) - return GeneralStatus.ERROR; // Only abortion = failure, special case for syntax error - if (result.snippetsResults().size() == 1 && result.abortion() != null // Only abortion = failure, case for all except syntax error - && !(result.abortion().cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause)) + return GeneralStatus.ERROR; // Only abortion = failure, special case for syntax error + if (result.snippetsResults().size() == 1 && result.abortion() != null // Only abortion = + // failure, case for + // all except syntax + // error + && !(result.abortion() + .cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause)) return GeneralStatus.ERROR; - if (result.abortion() != null) return GeneralStatus.PARTIAL_SUCCESS; // At least one snippet is a success + if (result.abortion() != null) + return GeneralStatus.PARTIAL_SUCCESS; // At least one snippet is a success - return getGeneralStatus(result.snippetsResults().get(result.snippetsResults().size() - 1).status()); + return getGeneralStatus( + result.snippetsResults().get(result.snippetsResults().size() - 1).status()); } + static Color getStatusColor(JShellResult result) { return switch (RendererUtils.getGeneralStatus(result)) { case SUCCESS -> SUCCESS_COLOR; @@ -64,8 +76,9 @@ private static GeneralStatus getGeneralStatus(SnippetStatus status) { static Optional generateEmbed(List segments) { StringBuilder currentEmbedDescription = new StringBuilder(); - for(String segment : segments) { - if(currentEmbedDescription.length() + "\n".length() + segment.length() < MessageEmbed.DESCRIPTION_MAX_LENGTH) { + for (String segment : segments) { + if (currentEmbedDescription.length() + "\n".length() + + segment.length() < MessageEmbed.DESCRIPTION_MAX_LENGTH) { currentEmbedDescription.append(segment).append("\n"); } else { return Optional.empty(); @@ -74,8 +87,9 @@ static Optional generateEmbed(List segments) { return Optional.of(new EmbedBuilder().setDescription(currentEmbedDescription)); } - static MessageEmbed setMetadataAndBuild(EmbedBuilder embedBuilder, @Nullable String author, Color color) { - if(author != null) { + static MessageEmbed setMetadataAndBuild(EmbedBuilder embedBuilder, @Nullable String author, + Color color) { + if (author != null) { embedBuilder.setAuthor(author); } embedBuilder.setColor(color); @@ -83,7 +97,7 @@ static MessageEmbed setMetadataAndBuild(EmbedBuilder embedBuilder, @Nullable Str } static String stdoutToMarkdownString(JShellResult result) { - if(result.stdout().isEmpty()) { + if (result.stdout().isEmpty()) { return "## System out\n[Nothing]\n"; } else { return "## System out\n```\n" + getSdtOut(result) + "```"; @@ -92,7 +106,7 @@ static String stdoutToMarkdownString(JShellResult result) { static String getSdtOut(JShellResult result) { String stdout = result.stdout(); - if(result.stdoutOverflow()) { + if (result.stdoutOverflow()) { stdout += MessageUtils.ABBREVIATION; } return stdout; diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java index cef2cd0a0b..6b3da5bc62 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java @@ -2,12 +2,14 @@ import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageEmbed; + import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortion; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellSnippetResult; import org.togetherjava.tjbot.features.utils.MessageUtils; import javax.annotation.Nullable; + import java.awt.*; import java.util.ArrayList; import java.util.List; @@ -27,7 +29,8 @@ class ResultEmbedRenderer { * @param result the JShell result * @return the embed, or empty if too big */ - public Optional renderToEmbed(@Nullable Member originator, boolean showCode, JShellResult result) { + public Optional renderToEmbed(@Nullable Member originator, boolean showCode, + JShellResult result) { List builder = new ArrayList<>(); setResultToEmbed(showCode, builder, result); @@ -35,7 +38,8 @@ public Optional renderToEmbed(@Nullable Member originator, boolean String author = originator == null ? null : originator.getEffectiveName() + "'s result"; Color color = RendererUtils.getStatusColor(result); - return RendererUtils.generateEmbed(builder).map(e -> RendererUtils.setMetadataAndBuild(e, author, color)); + return RendererUtils.generateEmbed(builder) + .map(e -> RendererUtils.setMetadataAndBuild(e, author, color)); } private void setResultToEmbed(boolean showCode, List builder, JShellResult result) { @@ -45,34 +49,42 @@ private void setResultToEmbed(boolean showCode, List builder, JShellResu builder.add(""" ### Snippet %d, %s ```java - %s```%s""".formatted(r.id(), r.status(), r.source(), resultToString(r.result()))); + %s```%s""".formatted(r.id(), r.status(), r.source(), + resultToString(r.result()))); } } - if(result.abortion() != null) { - builder.add("## [WARNING] The code couldn't end properly...\n" + abortionToString(result.abortion())); + if (result.abortion() != null) { + builder.add("## [WARNING] The code couldn't end properly...\n" + + abortionToString(result.abortion())); } builder.add(RendererUtils.stdoutToMarkdownString(result)); } + private String resultToString(@Nullable String result) { - if(result == null || result.isEmpty()) return ""; - if(!result.contains("\n")) return "\njshell> `" + result + "`"; + if (result == null || result.isEmpty()) + return ""; + if (!result.contains("\n")) + return "\njshell> `" + result + "`"; return "\njshell> ↓```\n" + result + "```"; } private String abortionToString(JShellEvalAbortion abortion) { String s = """ - Problematic source code: - ```java - %s``` - Cause: - %s - """.formatted(MessageUtils.abbreviate(abortion.sourceCause(), ABORTION_CODE_SIZE_LIMIT), RendererUtils.abortionCauseToString(abortion.cause())); - if(!abortion.remainingSource().isEmpty()) { - s += """ - - Remaining code: + Problematic source code: ```java - %s```""".formatted(MessageUtils.abbreviate(abortion.remainingSource(), ABORTION_CODE_SIZE_LIMIT)); + %s``` + Cause: + %s + """.formatted( + MessageUtils.abbreviate(abortion.sourceCause(), ABORTION_CODE_SIZE_LIMIT), + RendererUtils.abortionCauseToString(abortion.cause())); + if (!abortion.remainingSource().isEmpty()) { + s += """ + + Remaining code: + ```java + %s```""".formatted( + MessageUtils.abbreviate(abortion.remainingSource(), ABORTION_CODE_SIZE_LIMIT)); } return s; } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultGistRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultGistRenderer.java index eaff4645f5..3ca6498aaf 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultGistRenderer.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultGistRenderer.java @@ -8,13 +8,14 @@ import org.kohsuke.github.GitHubBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.togetherjava.tjbot.features.filesharing.FileSharingMessageListener; + import org.togetherjava.tjbot.features.jshell.backend.dto.JShellEvalAbortion; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import org.togetherjava.tjbot.features.jshell.backend.dto.JShellSnippetResult; import org.togetherjava.tjbot.features.utils.MessageUtils; import javax.annotation.Nullable; + import java.io.IOException; import java.util.Optional; @@ -29,11 +30,12 @@ class ResultGistRenderer { * * @param gistApiToken the token to use to send the result to gist * @param originator the user from who to display snippet ownership, won't be displayed if null - * @param showCode if the original should be displayed - * @param result the JShell result + * @param showCode if the original should be displayed + * @param result the JShell result * @return the content */ - public Optional renderToGist(String gistApiToken, @Nullable Member originator, boolean showCode, JShellResult result) { + public Optional renderToGist(String gistApiToken, @Nullable Member originator, + boolean showCode, JShellResult result) { StringBuilder builder = new StringBuilder(); builder.append("// ").append(getGeneralStatus(result)).append("\n"); @@ -48,13 +50,14 @@ public Optional renderToGist(String gistApiToken, @Nullable Member GHGist gist; try { GHGistBuilder gistBuilder = new GitHubBuilder().withOAuthToken(gistApiToken) - .build() - .createGist() - .public_(false); + .build() + .createGist() + .public_(false); if (originator != null) { gistBuilder.description("Uploaded by " + originator.getEffectiveName()); } - gistBuilder.file((originator == null ? "JShell" : originator.getEffectiveName()) + "'s result.java", text); + gistBuilder.file((originator == null ? "JShell" : originator.getEffectiveName()) + + "'s result.java", text); gist = gistBuilder.create(); } catch (IOException ex) { @@ -63,10 +66,11 @@ public Optional renderToGist(String gistApiToken, @Nullable Member } EmbedBuilder embedBuilder = new EmbedBuilder() - .setTitle("The result was too big and so was uploaded to Gist: " + gist.getHtmlUrl() + "\n↓This is the output of the JShell execution.↓") - .setDescription(getSdtOut(result).transform(s -> s.isBlank() ? "Nothing" : s)) - .setColor(RendererUtils.getStatusColor(result)); - if(originator != null) { + .setTitle("The result was too big and so was uploaded to Gist: " + gist.getHtmlUrl() + + "\n↓This is the output of the JShell execution.↓") + .setDescription(getSdtOut(result).transform(s -> s.isBlank() ? "Nothing" : s)) + .setColor(RendererUtils.getStatusColor(result)); + if (originator != null) { embedBuilder.setAuthor(originator.getEffectiveName() + "'s result"); } return Optional.of(embedBuilder.build()); @@ -79,7 +83,8 @@ private void setResultToBuilder(boolean showCode, StringBuilder builder, JShellR // Snippet %d, %s %s jshell> %s - """.formatted(r.id(), r.status(), r.source(), r.result() == null ? "" : r.result())); + """.formatted(r.id(), r.status(), r.source(), + r.result() == null ? "" : r.result())); } } if (result.abortion() != null) { @@ -123,7 +128,8 @@ private String abortionToString(JShellEvalAbortion abortion) { // // Remaining code: %s - """.formatted(RendererUtils.abortionCauseToString(abortion.cause()), abortion.remainingSource()); + """.formatted(RendererUtils.abortionCauseToString(abortion.cause()), + abortion.remainingSource()); } private String getGeneralStatus(JShellResult result) { diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultMinimalEmbedRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultMinimalEmbedRenderer.java index bc7266cb70..333d3bd69f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultMinimalEmbedRenderer.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultMinimalEmbedRenderer.java @@ -2,9 +2,11 @@ import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageEmbed; + import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import javax.annotation.Nullable; + import java.awt.*; import java.util.ArrayList; import java.util.List; @@ -30,16 +32,17 @@ public Optional renderToEmbed(@Nullable Member originator, JShellR String author = originator == null ? null : originator.getEffectiveName() + "'s result"; Color color = RendererUtils.getStatusColor(result); - return RendererUtils.generateEmbed(builder).map(e -> RendererUtils.setMetadataAndBuild(e, author, color)); + return RendererUtils.generateEmbed(builder) + .map(e -> RendererUtils.setMetadataAndBuild(e, author, color)); } private void setResultToEmbed(List builder, JShellResult result) { - if(result.abortion() != null) { + if (result.abortion() != null) { builder.add(""" - ## [WARNING] The code couldn't end properly - Cause: - %s - """.formatted(RendererUtils.abortionCauseToString(result.abortion().cause()))); + ## [WARNING] The code couldn't end properly + Cause: + %s + """.formatted(RendererUtils.abortionCauseToString(result.abortion().cause()))); } builder.add(RendererUtils.stdoutToMarkdownString(result)); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java index 48055a6680..d264b9b8c1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultRenderer.java @@ -5,9 +5,11 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.togetherjava.tjbot.features.jshell.backend.dto.JShellResult; import javax.annotation.Nullable; + import java.awt.*; /** @@ -17,27 +19,33 @@ public class ResultRenderer { private static final Logger logger = LoggerFactory.getLogger(ResultRenderer.class); private final ResultEmbedRenderer embedRenderer = new ResultEmbedRenderer(); private final ResultGistRenderer gistRenderer = new ResultGistRenderer(); - private final ResultMinimalEmbedRenderer minimalEmbedRenderer = new ResultMinimalEmbedRenderer(); + private final ResultMinimalEmbedRenderer minimalEmbedRenderer = + new ResultMinimalEmbedRenderer(); /** * Renders a JShell result. * - * @param gistApiToken the token to use to send the result to gist, in case it is too big for an embed + * @param gistApiToken the token to use to send the result to gist, in case it is too big for an + * embed * @param originator the user from who to display snippet ownership, won't be displayed if null * @param showCode if the original should be displayed * @param result the JShell result * @return the result */ - public MessageEmbed render(String gistApiToken, @Nullable Member originator, boolean showCode, JShellResult result) { + public MessageEmbed render(String gistApiToken, @Nullable Member originator, boolean showCode, + JShellResult result) { return embedRenderer.renderToEmbed(originator, showCode, result) - .or(() -> gistRenderer.renderToGist(gistApiToken, originator, showCode, result)) - .or(() -> minimalEmbedRenderer.renderToEmbed(originator, result)) - .orElseGet(() -> renderFailure(result)); + .or(() -> gistRenderer.renderToGist(gistApiToken, originator, showCode, result)) + .or(() -> minimalEmbedRenderer.renderToEmbed(originator, result)) + .orElseGet(() -> renderFailure(result)); } private MessageEmbed renderFailure(JShellResult result) { logger.error("Couldn't render JShell result {} ", result); - return new EmbedBuilder().setTitle("Couldn't render the result, please contact a moderator.").setColor(Color.RED).build(); + return new EmbedBuilder() + .setTitle("Couldn't render the result, please contact a moderator.") + .setColor(Color.RED) + .build(); } } From 437e6671f2442ec4c3f46208abbc2cc029de5808 Mon Sep 17 00:00:00 2001 From: Alathreon Date: Sat, 23 Dec 2023 17:44:01 +0100 Subject: [PATCH 04/10] [Feature/JShell] RenderResult deleted --- .../features/jshell/renderer/RenderResult.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java deleted file mode 100644 index ec9ea10266..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RenderResult.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.togetherjava.tjbot.features.jshell.renderer; - -import net.dv8tion.jda.api.entities.MessageEmbed; - -import java.util.List; - -// TODO delete ? -public sealed - -interface RenderResult { - record EmbedResult(List embeds) implements RenderResult {} - record FileResult(String content) implements RenderResult {} -} From c4ae6152a79ace719bd2f1a1b283cbc9efeec8af Mon Sep 17 00:00:00 2001 From: Alathreon Date: Sat, 23 Dec 2023 17:47:06 +0100 Subject: [PATCH 05/10] [Feature/JShell] Added doc on JShellEval --- .../java/org/togetherjava/tjbot/features/jshell/JShellEval.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java index a32b2a72d1..f12a2daf34 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java @@ -36,6 +36,7 @@ public class JShellEval { * Creates a JShell evaluation instance * * @param config the JShell configuration to use + * @param gistApiToken token of Gist api in case a JShell result is uploaded here */ public JShellEval(JShellConfig config, String gistApiToken) { this.gistApiToken = gistApiToken; From 9d5de870b559593bfdb667e10ca9b21f05ae747c Mon Sep 17 00:00:00 2001 From: Alathreon Date: Sat, 20 Jan 2024 18:02:40 +0100 Subject: [PATCH 06/10] [Feature/JShell] Added braces --- .../features/jshell/renderer/RendererUtils.java | 12 ++++++++---- .../jshell/renderer/ResultEmbedRenderer.java | 9 ++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java index 9453e66309..4989ad1053 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java @@ -39,20 +39,24 @@ enum GeneralStatus { } static GeneralStatus getGeneralStatus(JShellResult result) { - if (result.snippetsResults().isEmpty() && result.abortion() == null) + if (result.snippetsResults().isEmpty() && result.abortion() == null) { return GeneralStatus.SUCCESS; // Empty = success - if (result.snippetsResults().isEmpty()) + } + if (result.snippetsResults().isEmpty()) { return GeneralStatus.ERROR; // Only abortion = failure, special case for syntax error + } if (result.snippetsResults().size() == 1 && result.abortion() != null // Only abortion = // failure, case for // all except syntax // error && !(result.abortion() - .cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause)) + .cause() instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause)) { return GeneralStatus.ERROR; + } - if (result.abortion() != null) + if (result.abortion() != null) { return GeneralStatus.PARTIAL_SUCCESS; // At least one snippet is a success + } return getGeneralStatus( result.snippetsResults().get(result.snippetsResults().size() - 1).status()); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java index 6b3da5bc62..ecbd215eb4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/ResultEmbedRenderer.java @@ -61,11 +61,14 @@ private void setResultToEmbed(boolean showCode, List builder, JShellResu } private String resultToString(@Nullable String result) { - if (result == null || result.isEmpty()) + if (result == null || result.isEmpty()) { return ""; - if (!result.contains("\n")) + } + if (!result.contains("\n")) { return "\njshell> `" + result + "`"; - return "\njshell> ↓```\n" + result + "```"; + } else { + return "\njshell> ↓```\n" + result + "```"; + } } private String abortionToString(JShellEvalAbortion abortion) { From f6959a2b93dbc269095aaf029f73940ed4ed926e Mon Sep 17 00:00:00 2001 From: Connor Schweighoefer Date: Thu, 15 Feb 2024 23:32:44 +0100 Subject: [PATCH 07/10] Rebase and rename user to member --- .../togetherjava/tjbot/features/Features.java | 2 +- .../tjbot/features/jshell/JShellCommand.java | 62 +++++++++---------- .../tjbot/features/jshell/JShellEval.java | 18 +++--- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 5d18b2ca64..7dbc19d447 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -79,7 +79,7 @@ private Features() { */ public static Collection createFeatures(JDA jda, Database database, Config config) { FeatureBlacklistConfig blacklistConfig = config.getFeatureBlacklistConfig(); - JShellEval jshellEval = new JShellEval(config.getJshell(), config.getGistApiKey()); + JShellEval jshellEval = new JShellEval(config.getJshell(), config.getGitHubApiKey()); TagSystem tagSystem = new TagSystem(database); BookmarksSystem bookmarksSystem = new BookmarksSystem(config, database); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java index c064990c29..800e746ba2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java @@ -145,20 +145,20 @@ private void sendEvalModal(SlashCommandInteractionEvent event, boolean startupSc * Handle evaluation of code. * * @param replyCallback the callback to reply to - * @param user the user, if null, will create a single use session + * @param member the member, if null, will create a single use session * @param showCode if the embed should contain the original code * @param startupScript if the startup script should be used or not * @param code the code */ - private void handleEval(IReplyCallback replyCallback, @Nullable Member user, boolean showCode, + private void handleEval(IReplyCallback replyCallback, @Nullable Member member, boolean showCode, String code, boolean startupScript) { replyCallback.deferReply().queue(interactionHook -> { try { MessageEmbed messageEmbed = - jshellEval.evaluateAndRespond(user, code, showCode, startupScript); + jshellEval.evaluateAndRespond(member, code, showCode, startupScript); interactionHook.sendMessageEmbeds(messageEmbed).queue(); } catch (RequestFailedException | ConnectionFailedException e) { - interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(user, e)).queue(); + interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(member, e)).queue(); } }); } @@ -166,7 +166,7 @@ private void handleEval(IReplyCallback replyCallback, @Nullable Member user, boo private void handleSnippetsCommand(SlashCommandInteractionEvent event) { event.deferReply().queue(interactionHook -> { OptionMapping userOption = event.getOption(USER_PARAMETER); - Member user = Objects + Member member = Objects .requireNonNull(userOption == null ? event.getMember() : userOption.getAsMember()); OptionMapping includeStartupScriptOption = event.getOption(INCLUDE_STARTUP_SCRIPT_PARAMETER); @@ -175,32 +175,32 @@ private void handleSnippetsCommand(SlashCommandInteractionEvent event) { List snippets; try { snippets = jshellEval.getApi() - .snippetsSession(user.getId(), includeStartupScript) + .snippetsSession(member.getId(), includeStartupScript) .snippets(); } catch (RequestFailedException e) { if (e.getStatus() == JShellApi.SESSION_NOT_FOUND) { - interactionHook.editOriginalEmbeds(createSessionNotFoundErrorEmbed(user)) + interactionHook.editOriginalEmbeds(createSessionNotFoundErrorEmbed(member)) .queue(); } else { - interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(user, e)).queue(); + interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(member, e)).queue(); } return; } catch (ConnectionFailedException e) { - interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(user, e)).queue(); + interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(member, e)).queue(); return; } - sendSnippets(interactionHook, user, snippets); + sendSnippets(interactionHook, member, snippets); }); } - private void sendSnippets(InteractionHook interactionHook, Member user, List snippets) { + private void sendSnippets(InteractionHook interactionHook, Member member, List snippets) { if (canBeSentAsEmbed(snippets)) { - sendSnippetsAsEmbed(interactionHook, user, snippets); + sendSnippetsAsEmbed(interactionHook, member, snippets); } else if (canBeSentAsFile(snippets)) { - sendSnippetsAsFile(interactionHook, user, snippets); + sendSnippetsAsFile(interactionHook, member, snippets); } else { - sendSnippetsTooLong(interactionHook, user); + sendSnippetsTooLong(interactionHook, member); } } @@ -212,11 +212,11 @@ private boolean canBeSentAsEmbed(List snippets) { && snippets.size() <= MessageUtils.MAXIMUM_VISIBLE_EMBEDS; } - private void sendSnippetsAsEmbed(InteractionHook interactionHook, Member user, + private void sendSnippetsAsEmbed(InteractionHook interactionHook, Member member, List snippets) { EmbedBuilder builder = new EmbedBuilder().setColor(Colors.SUCCESS_COLOR) - .setAuthor(user.getEffectiveName()) - .setTitle(snippetsTitle(user)); + .setAuthor(member.getEffectiveName()) + .setTitle(snippetsTitle(member)); int i = 1; for (String snippet : snippets) { builder.addField("Snippet " + i, "```java\n" + snippet + "```", false); @@ -231,7 +231,7 @@ private boolean canBeSentAsFile(List snippets) { .sum() < Message.MAX_FILE_SIZE; } - private void sendSnippetsAsFile(InteractionHook interactionHook, Member user, + private void sendSnippetsAsFile(InteractionHook interactionHook, Member member, List snippets) { StringBuilder sb = new StringBuilder(); int i = 1; @@ -250,21 +250,21 @@ private void sendSnippetsAsFile(InteractionHook interactionHook, Member user, } interactionHook .editOriginalEmbeds(new EmbedBuilder().setColor(Colors.SUCCESS_COLOR) - .setAuthor(user.getEffectiveName()) - .setTitle(snippetsTitle(user)) + .setAuthor(member.getEffectiveName()) + .setTitle(snippetsTitle(member)) .build()) - .setFiles(FileUpload.fromData(sb.toString().getBytes(), snippetsTitle(user) + ".java")) + .setFiles(FileUpload.fromData(sb.toString().getBytes(), snippetsTitle(member) + ".java")) .queue(); } - private String snippetsTitle(Member user) { - return user.getEffectiveName() + "'s snippets"; + private String snippetsTitle(Member member) { + return member.getEffectiveName() + "'s snippets"; } - private void sendSnippetsTooLong(InteractionHook interactionHook, Member user) { + private void sendSnippetsTooLong(InteractionHook interactionHook, Member member) { interactionHook .editOriginalEmbeds(new EmbedBuilder().setColor(Colors.ERROR_COLOR) - .setAuthor(user.getEffectiveName()) + .setAuthor(member.getEffectiveName()) .setTitle("Too much code to send...") .build()) .queue(); @@ -313,18 +313,18 @@ private void handleStartupScriptCommand(SlashCommandInteractionEvent event) { }); } - private MessageEmbed createSessionNotFoundErrorEmbed(Member user) { - return new EmbedBuilder().setAuthor(user.getEffectiveName() + "'s result") + private MessageEmbed createSessionNotFoundErrorEmbed(Member member) { + return new EmbedBuilder().setAuthor(member.getEffectiveName() + "'s result") .setColor(Colors.ERROR_COLOR) - .setDescription("Could not find session for user " + user.getEffectiveName()) + .setDescription("Could not find session for member " + member.getEffectiveName()) .build(); } - private MessageEmbed createUnexpectedErrorEmbed(@Nullable Member user, Exception e) { + private MessageEmbed createUnexpectedErrorEmbed(@Nullable Member member, Exception e) { EmbedBuilder embedBuilder = new EmbedBuilder().setColor(Colors.ERROR_COLOR) .setDescription("Request failed: " + e.getMessage()); - if (user != null) { - embedBuilder.setAuthor(user.getEffectiveName() + "'s result"); + if (member != null) { + embedBuilder.setAuthor(member.getEffectiveName() + "'s result"); } return embedBuilder.build(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java index f12a2daf34..cd965d128d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellEval.java @@ -55,7 +55,7 @@ public JShellApi getApi() { /** * Evaluate code and return a message containing the response. * - * @param user the user, if null, will create a single use session + * @param member the member, if null, will create a single use session * @param code the code * @param showCode if the original code should be displayed * @param startupScript if the startup script should be used or not @@ -64,24 +64,24 @@ public JShellApi getApi() { * @throws ConnectionFailedException if the connection to the API couldn't be made at the first * place */ - public MessageEmbed evaluateAndRespond(@Nullable Member user, String code, boolean showCode, + public MessageEmbed evaluateAndRespond(@Nullable Member member, String code, boolean showCode, boolean startupScript) throws RequestFailedException, ConnectionFailedException { - MessageEmbed rateLimitedMessage = wasRateLimited(user, Instant.now()); + MessageEmbed rateLimitedMessage = wasRateLimited(member, Instant.now()); if (rateLimitedMessage != null) { return rateLimitedMessage; } JShellResult result; - if (user == null) { + if (member == null) { result = api.evalOnce(code, startupScript); } else { - result = api.evalSession(code, user.getId(), startupScript); + result = api.evalSession(code, member.getId(), startupScript); } - return renderer.render(gistApiToken, user, showCode, result); + return renderer.render(gistApiToken, member, showCode, result); } @Nullable - private MessageEmbed wasRateLimited(@Nullable Member user, Instant checkTime) { + private MessageEmbed wasRateLimited(@Nullable Member member, Instant checkTime) { if (rateLimiter.allowRequest(checkTime)) { return null; } @@ -92,8 +92,8 @@ private MessageEmbed wasRateLimited(@Nullable Member user, Instant checkTime) { .setDescription( "You are currently rate-limited. Please try again " + nextAllowedTime + ".") .setColor(Colors.ERROR_COLOR); - if (user != null) { - embedBuilder.setAuthor(user.getEffectiveName() + "'s result"); + if (member != null) { + embedBuilder.setAuthor(member.getEffectiveName() + "'s result"); } return embedBuilder.build(); } From 4dd8176eb60941098249a93315f6adb283203964 Mon Sep 17 00:00:00 2001 From: Alathreon Date: Tue, 20 Feb 2024 20:19:48 +0100 Subject: [PATCH 08/10] [Feature/JShell] Fixing checks --- .../tjbot/features/jshell/JShellCommand.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java index 800e746ba2..4222d28e52 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java @@ -182,7 +182,8 @@ private void handleSnippetsCommand(SlashCommandInteractionEvent event) { interactionHook.editOriginalEmbeds(createSessionNotFoundErrorEmbed(member)) .queue(); } else { - interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(member, e)).queue(); + interactionHook.editOriginalEmbeds(createUnexpectedErrorEmbed(member, e)) + .queue(); } return; } catch (ConnectionFailedException e) { @@ -194,7 +195,8 @@ private void handleSnippetsCommand(SlashCommandInteractionEvent event) { }); } - private void sendSnippets(InteractionHook interactionHook, Member member, List snippets) { + private void sendSnippets(InteractionHook interactionHook, Member member, + List snippets) { if (canBeSentAsEmbed(snippets)) { sendSnippetsAsEmbed(interactionHook, member, snippets); } else if (canBeSentAsFile(snippets)) { @@ -253,7 +255,8 @@ private void sendSnippetsAsFile(InteractionHook interactionHook, Member member, .setAuthor(member.getEffectiveName()) .setTitle(snippetsTitle(member)) .build()) - .setFiles(FileUpload.fromData(sb.toString().getBytes(), snippetsTitle(member) + ".java")) + .setFiles( + FileUpload.fromData(sb.toString().getBytes(), snippetsTitle(member) + ".java")) .queue(); } From 719cd758375726ef7560fa37da7856b686679163 Mon Sep 17 00:00:00 2001 From: Alathreon Date: Tue, 20 Feb 2024 21:25:50 +0100 Subject: [PATCH 09/10] [Feature/JShell] Fixing from some feedback --- .../togetherjava/tjbot/features/jshell/JShellCommand.java | 6 ++++-- .../tjbot/features/jshell/renderer/RendererUtils.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java index 4222d28e52..b0eae75704 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/JShellCommand.java @@ -52,6 +52,8 @@ public class JShellCommand extends SlashCommandAdapter { private static final int MIN_MESSAGE_INPUT_LENGTH = 0; private static final int MAX_MESSAGE_INPUT_LENGTH = TextInput.MAX_VALUE_LENGTH; + private static final String MAX_SNIPPETS_FILE_PREFIX = " // Snippet 1000"; + private static final String MAX_SNIPPETS_EMBED_PREFIX = "Snippet 10```java\n```"; private final JShellEval jshellEval; /** @@ -209,7 +211,7 @@ private void sendSnippets(InteractionHook interactionHook, Member member, private boolean canBeSentAsEmbed(List snippets) { return snippets.stream().noneMatch(s -> s.length() >= MessageEmbed.VALUE_MAX_LENGTH) && snippets.stream() - .mapToInt(s -> (s + "Snippet 10```java\n```").length()) + .mapToInt(s -> (s + MAX_SNIPPETS_EMBED_PREFIX).length()) .sum() < MessageEmbed.EMBED_MAX_LENGTH_BOT - 100 && snippets.size() <= MessageUtils.MAXIMUM_VISIBLE_EMBEDS; } @@ -229,7 +231,7 @@ private void sendSnippetsAsEmbed(InteractionHook interactionHook, Member member, private boolean canBeSentAsFile(List snippets) { return snippets.stream() - .mapToInt(s -> (s + " // Snippet 1000").getBytes().length) + .mapToInt(s -> (s + MAX_SNIPPETS_FILE_PREFIX).getBytes().length) .sum() < Message.MAX_FILE_SIZE; } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java index 4989ad1053..d896452527 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java @@ -10,7 +10,7 @@ import javax.annotation.Nullable; -import java.awt.*; +import java.awt.Color; import java.util.List; import java.util.Optional; From 466b950ec50ad01f243268bba0c7c530d97563c8 Mon Sep 17 00:00:00 2001 From: Alathreon Date: Tue, 20 Feb 2024 21:32:18 +0100 Subject: [PATCH 10/10] [Feature/JShell] Replaced if elses with a switch thanks to java 21 --- .../features/jshell/renderer/RendererUtils.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java index d896452527..48dfe416e1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/jshell/renderer/RendererUtils.java @@ -20,16 +20,12 @@ class RendererUtils { private RendererUtils() {} static String abortionCauseToString(JShellEvalAbortionCause abortionCause) { - if (abortionCause instanceof JShellEvalAbortionCause.TimeoutAbortionCause) { - return "Allowed time exceeded."; - } else if (abortionCause instanceof JShellEvalAbortionCause.UnhandledExceptionAbortionCause c) { - return "Uncaught exception:\n" + c.exceptionClass() + ":" + c.exceptionMessage(); - } else if (abortionCause instanceof JShellEvalAbortionCause.CompileTimeErrorAbortionCause c) { - return "The code doesn't compile:\n" + String.join("\n", c.errors()); - } else if (abortionCause instanceof JShellEvalAbortionCause.SyntaxErrorAbortionCause) { - return "The code doesn't compile, there are syntax errors in this code."; - } - throw new AssertionError(); + return switch (abortionCause) { + case JShellEvalAbortionCause.TimeoutAbortionCause ignored -> "Allowed time exceeded."; + case JShellEvalAbortionCause.UnhandledExceptionAbortionCause c -> "Uncaught exception:\n" + c.exceptionClass() + ":" + c.exceptionMessage(); + case JShellEvalAbortionCause.CompileTimeErrorAbortionCause c -> "The code doesn't compile:\n" + String.join("\n", c.errors()); + case JShellEvalAbortionCause.SyntaxErrorAbortionCause ignored -> "The code doesn't compile, there are syntax errors in this code."; + }; } enum GeneralStatus {