Skip to content

Commit

Permalink
Feature/jshell rework (#998)
Browse files Browse the repository at this point in the history
* [Feature/JShell] Using the new reworked jshell api

* [Feature/JShell] Reworked renderer

* [feature/JShell] Running Spotless/Sonar

* [Feature/JShell] RenderResult deleted

* [Feature/JShell] Added doc on JShellEval

* [Feature/JShell] Added braces

* Rebase and rename user to member

* [Feature/JShell] Fixing checks

* [Feature/JShell] Fixing from some feedback

* [Feature/JShell] Replaced if elses with a switch thanks to java 21

---------

Co-authored-by: Connor Schweighoefer <[email protected]>
  • Loading branch information
2 people authored and Taz03 committed Mar 6, 2024
1 parent a207172 commit 74a4ad9
Show file tree
Hide file tree
Showing 18 changed files with 643 additions and 187 deletions.
1 change: 1 addition & 0 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private Features() {
*/
public static Collection<Feature> createFeatures(JDA jda, Database database, Config config) {
FeatureBlacklistConfig blacklistConfig = config.getFeatureBlacklistConfig();
JShellEval jshellEval = new JShellEval(config.getJshell());
JShellEval jshellEval = new JShellEval(config.getJshell(), config.getGitHubApiKey());

TagSystem tagSystem = new TagSystem(database);
BookmarksSystem bookmarksSystem = new BookmarksSystem(config, database);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.InteractionHook;
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -103,7 +105,7 @@ public void onModalSubmitted(ModalInteractionEvent event, List<String> 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);
}
}

Expand All @@ -125,7 +127,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);
}
}

Expand All @@ -145,78 +147,80 @@ 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 User user, boolean showCode,
private void handleEval(IReplyCallback replyCallback, @Nullable Member member, boolean showCode,
String code, boolean startupScript) {
replyCallback.deferReply().queue(interactionHook -> {
try {
interactionHook
.editOriginalEmbeds(
jshellEval.evaluateAndRespond(user, code, showCode, startupScript))
.queue();
MessageEmbed messageEmbed =
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();
}
});
}

private void handleSnippetsCommand(SlashCommandInteractionEvent event) {
event.deferReply().queue(interactionHook -> {
OptionMapping userOption = event.getOption(USER_PARAMETER);
User user = userOption == null ? event.getUser() : userOption.getAsUser();
Member member = Objects
.requireNonNull(userOption == null ? event.getMember() : userOption.getAsMember());
OptionMapping includeStartupScriptOption =
event.getOption(INCLUDE_STARTUP_SCRIPT_PARAMETER);
boolean includeStartupScript =
includeStartupScriptOption != null && includeStartupScriptOption.getAsBoolean();
List<String> 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, User user, List<String> snippets) {
private void sendSnippets(InteractionHook interactionHook, Member member,
List<String> 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);
}
}

private boolean canBeSentAsEmbed(List<String> 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;
}

private void sendSnippetsAsEmbed(InteractionHook interactionHook, User user,
private void sendSnippetsAsEmbed(InteractionHook interactionHook, Member member,
List<String> snippets) {
EmbedBuilder builder = new EmbedBuilder().setColor(Colors.SUCCESS_COLOR)
.setAuthor(user.getName())
.setTitle(snippetsTitle(user));
.setAuthor(member.getEffectiveName())
.setTitle(snippetsTitle(member));
int i = 1;
for (String snippet : snippets) {
builder.addField("Snippet " + i, "```java\n" + snippet + "```", false);
Expand All @@ -227,35 +231,45 @@ private void sendSnippetsAsEmbed(InteractionHook interactionHook, User user,

private boolean canBeSentAsFile(List<String> snippets) {
return snippets.stream()
.mapToInt(s -> (s + "// Snippet 10").getBytes().length)
.mapToInt(s -> (s + MAX_SNIPPETS_FILE_PREFIX).getBytes().length)
.sum() < Message.MAX_FILE_SIZE;
}

private void sendSnippetsAsFile(InteractionHook interactionHook, User user,
private void sendSnippetsAsFile(InteractionHook interactionHook, Member member,
List<String> 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())
.setTitle(snippetsTitle(user))
.setAuthor(member.getEffectiveName())
.setTitle(snippetsTitle(member))
.build())
.setFiles(FileUpload.fromData(sb.toString().getBytes(), snippetsTitle(user)))
.setFiles(
FileUpload.fromData(sb.toString().getBytes(), snippetsTitle(member) + ".java"))
.queue();
}

private String snippetsTitle(User user) {
return user.getName() + "'s snippets";
private String snippetsTitle(Member member) {
return member.getEffectiveName() + "'s snippets";
}

private void sendSnippetsTooLong(InteractionHook interactionHook, User user) {
private void sendSnippetsTooLong(InteractionHook interactionHook, Member member) {
interactionHook
.editOriginalEmbeds(new EmbedBuilder().setColor(Colors.ERROR_COLOR)
.setAuthor(user.getName())
.setAuthor(member.getEffectiveName())
.setTitle("Too much code to send...")
.build())
.queue();
Expand All @@ -266,13 +280,16 @@ 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;
}

Expand All @@ -296,23 +313,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 member) {
return new EmbedBuilder().setAuthor(member.getEffectiveName() + "'s result")
.setColor(Colors.ERROR_COLOR)
.setDescription("Could not find session for user " + user.getName())
.setDescription("Could not find session for member " + member.getEffectiveName())
.build();
}

private MessageEmbed createUnexpectedErrorEmbed(@Nullable User 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.getName() + "'s result");
if (member != null) {
embedBuilder.setAuthor(member.getEffectiveName() + "'s result");
}
return embedBuilder.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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;

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.ResultRenderer;
import org.togetherjava.tjbot.features.utils.Colors;
import org.togetherjava.tjbot.features.utils.ConnectionFailedException;
import org.togetherjava.tjbot.features.utils.RateLimiter;
Expand All @@ -24,6 +26,7 @@
* including JShell commands and JShell code actions.
*/
public class JShellEval {
private final String gistApiToken;
private final JShellApi api;

private final ResultRenderer renderer;
Expand All @@ -33,9 +36,12 @@ 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) {
this.api = new JShellApi(new ObjectMapper(), config.baseUrl());
public JShellEval(JShellConfig config, String gistApiToken) {
this.gistApiToken = gistApiToken;
this.api = new JShellApi(new ObjectMapper().registerModule(new Jdk17SealedClassesModule()),
config.baseUrl());
this.renderer = new ResultRenderer();

this.rateLimiter = new RateLimiter(Duration.ofSeconds(config.rateLimitWindowSeconds()),
Expand All @@ -49,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
Expand All @@ -58,26 +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 User 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
.renderToEmbed(user, showCode ? code : null, user != null, result, new EmbedBuilder())
.build();
return renderer.render(gistApiToken, member, showCode, result);
}

@Nullable
private MessageEmbed wasRateLimited(@Nullable User user, Instant checkTime) {
private MessageEmbed wasRateLimited(@Nullable Member member, Instant checkTime) {
if (rateLimiter.allowRequest(checkTime)) {
return null;
}
Expand All @@ -88,8 +92,8 @@ private MessageEmbed wasRateLimited(@Nullable User user, Instant checkTime) {
.setDescription(
"You are currently rate-limited. Please try again " + nextAllowedTime + ".")
.setColor(Colors.ERROR_COLOR);
if (user != null) {
embedBuilder.setAuthor(user.getName() + "'s result");
if (member != null) {
embedBuilder.setAuthor(member.getEffectiveName() + "'s result");
}
return embedBuilder.build();
}
Expand Down
Loading

0 comments on commit 74a4ad9

Please sign in to comment.