Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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