diff --git a/application/src/main/java/org/togetherjava/tjbot/config/JShellConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/JShellConfig.java index 2337c6e8d4..850ca9f4c3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/JShellConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/JShellConfig.java @@ -2,21 +2,43 @@ import com.linkedin.urls.Url; +import org.togetherjava.tjbot.features.utils.RateLimiter; + import java.net.MalformedURLException; +/** + * JShell config. + * + * @param baseUrl the base url of the JShell REST API + * @param rateLimitWindowSeconds the number of seconds of the {@link RateLimiter rate limiter} for + * jshell commands and code actions + * @param rateLimitRequestsInWindow the number of requests of the {@link RateLimiter rate limiter} + * for jshell commands and code actions + */ public record JShellConfig(String baseUrl, int rateLimitWindowSeconds, int rateLimitRequestsInWindow) { + /** + * Creates a JShell config. + * + * @param baseUrl the base url of the JShell REST API, must be valid + * @param rateLimitWindowSeconds the number of seconds of the {@link RateLimiter rate limiter} + * for jshell commands and code actions, must be higher than 0 + * @param rateLimitRequestsInWindow the number of requests of the {@link RateLimiter rate + * limiter} for jshell commands and code actions, must be higher than 0 + */ public JShellConfig { try { Url.create(baseUrl); } catch (MalformedURLException e) { throw new IllegalArgumentException(e); } - if (rateLimitWindowSeconds <= 0) + if (rateLimitWindowSeconds < 0) { throw new IllegalArgumentException( "Illegal rateLimitWindowSeconds : " + rateLimitWindowSeconds); - if (rateLimitRequestsInWindow <= 0) + } + if (rateLimitRequestsInWindow < 0) { throw new IllegalArgumentException( "Illegal rateLimitRequestsInWindow : " + rateLimitRequestsInWindow); + } } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java b/application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java index cd849926fd..30ae6bdfc2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java @@ -63,6 +63,8 @@ public final class CodeMessageHandler extends MessageReceiverAdapter implements /** * Creates a new instance. + * + * @param jshellEval the jshell evaluation instance used in the code actions */ public CodeMessageHandler(JShellEval jshellEval) { componentIdInteractor = new ComponentIdInteractor(getInteractionType(), getName()); 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 23aae33824..1dfd6a7f8c 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,12 +4,14 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import org.togetherjava.tjbot.features.jshell.JShellEval; -import org.togetherjava.tjbot.features.jshell.render.Colors; import org.togetherjava.tjbot.features.utils.CodeFence; +import org.togetherjava.tjbot.features.utils.Colors; import org.togetherjava.tjbot.features.utils.RequestFailedException; /** - * Evaluates the given code. + * Evaluates the given code with jshell. + *
+ * It will not work of the code isn't valid java or jshell compatible code.
*/
final class EvalCodeCommand implements CodeAction {
private final JShellEval jshellEval;
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 dbf13f5176..92905b6756 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
@@ -20,7 +20,8 @@
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.render.Colors;
+import org.togetherjava.tjbot.features.utils.Colors;
+import org.togetherjava.tjbot.features.utils.MessageUtils;
import org.togetherjava.tjbot.features.utils.RequestFailedException;
import javax.annotation.Nullable;
@@ -28,18 +29,21 @@
import java.util.List;
import java.util.Objects;
+/**
+ * The JShell command, provide functionalities to create JShell sessions, evaluate code, etc.
+ */
public class JShellCommand extends SlashCommandAdapter {
- private static final String JSHELL_TEXT_INPUT_ID = "jshell";
+ private static final String TEXT_INPUT_PART_ID = "jshell";
private static final String JSHELL_COMMAND = "jshell";
- private static final String JSHELL_VERSION_SUBCOMMAND = "version";
- private static final String JSHELL_EVAL_SUBCOMMAND = "eval";
- private static final String JSHELL_SNIPPETS_SUBCOMMAND = "snippets";
- private static final String JSHELL_CLOSE_SUBCOMMAND = "shutdown";
- private static final String JSHELL_STARTUP_SCRIPT_SUBCOMMAND = "startup-script";
- private static final String JSHELL_CODE_PARAMETER = "code";
- private static final String JSHELL_STARTUP_SCRIPT_PARAMETER = "startup-script";
- private static final String JSHELL_USER_PARAMETER = "user";
- private static final String JSHELL_INCLUDE_STARTUP_SCRIPT_PARAMETER = "include-startup-script";
+ private static final String VERSION_SUBCOMMAND = "version";
+ private static final String EVAL_SUBCOMMAND = "eval";
+ private static final String SNIPPETS_SUBCOMMAND = "snippets";
+ private static final String CLOSE_SUBCOMMAND = "shutdown";
+ private static final String STARTUP_SCRIPT_SUBCOMMAND = "startup-script";
+ private static final String CODE_PARAMETER = "code";
+ private static final String STARTUP_SCRIPT_PARAMETER = "startup-script";
+ private static final String USER_PARAMETER = "user";
+ private static final String INCLUDE_STARTUP_SCRIPT_PARAMETER = "include-startup-script";
private static final int MIN_MESSAGE_INPUT_LENGTH = 0;
private static final int MAX_MESSAGE_INPUT_LENGTH = TextInput.MAX_VALUE_LENGTH;
@@ -48,39 +52,42 @@ public class JShellCommand extends SlashCommandAdapter {
/**
* Creates an instance of the command.
+ *
+ * @param jshellEval the jshell evaluation instance used in the jshell command
*/
public JShellCommand(JShellEval jshellEval) {
- super(JSHELL_COMMAND, "JShell as a command.", CommandVisibility.GUILD);
+ super(JSHELL_COMMAND,
+ "JShell as a command, a Read–eval–print loop which allows to rapidly and simply execute java code.",
+ CommandVisibility.GUILD);
this.jshellEval = jshellEval;
getData().addSubcommands(
- new SubcommandData(JSHELL_VERSION_SUBCOMMAND, "Get the version of JShell"),
- new SubcommandData(JSHELL_EVAL_SUBCOMMAND,
- "Evaluate java code in JShell, don't fill the optional parameter to access a bigger input box.")
- .addOption(OptionType.STRING, JSHELL_CODE_PARAMETER,
- "Code to evaluate. If not supplied, open an inout box.")
- .addOption(OptionType.BOOLEAN, JSHELL_STARTUP_SCRIPT_PARAMETER,
+ new SubcommandData(VERSION_SUBCOMMAND, "Get the version of JShell"),
+ new SubcommandData(EVAL_SUBCOMMAND,
+ "Evaluate java code in JShell, submit the command without code for inputting longer, multi-line code.")
+ .addOption(OptionType.STRING, CODE_PARAMETER,
+ "Code to evaluate. Leave empty to input longer, multi-line code.")
+ .addOption(OptionType.BOOLEAN, STARTUP_SCRIPT_PARAMETER,
"If the startup script should be loaded, true by default."),
- new SubcommandData(JSHELL_SNIPPETS_SUBCOMMAND,
- "Get the evaluated snippets of the user who sent the command, or the user specified user if any.")
- .addOption(OptionType.USER, JSHELL_USER_PARAMETER,
- "User to get the snippets from. If null, get the snippets of the user who sent the command.")
- .addOption(OptionType.BOOLEAN, JSHELL_INCLUDE_STARTUP_SCRIPT_PARAMETER,
+ new SubcommandData(SNIPPETS_SUBCOMMAND,
+ "Display your snippets, or the snippets of the specified user if any.")
+ .addOption(OptionType.USER, USER_PARAMETER,
+ "User to get the snippets from. If null, get your snippets.")
+ .addOption(OptionType.BOOLEAN, INCLUDE_STARTUP_SCRIPT_PARAMETER,
"if the startup script should be included, false by default."),
- new SubcommandData(JSHELL_CLOSE_SUBCOMMAND, "Close your session."),
- new SubcommandData(JSHELL_STARTUP_SCRIPT_SUBCOMMAND,
- "Display the startup script."));
+ new SubcommandData(CLOSE_SUBCOMMAND, "Close your session."),
+ new SubcommandData(STARTUP_SCRIPT_SUBCOMMAND, "Display the startup script."));
}
@Override
public void onSlashCommand(SlashCommandInteractionEvent event) {
switch (Objects.requireNonNull(event.getSubcommandName())) {
- case JSHELL_VERSION_SUBCOMMAND -> handleVersionCommand(event);
- case JSHELL_EVAL_SUBCOMMAND -> handleEvalCommand(event);
- case JSHELL_SNIPPETS_SUBCOMMAND -> handleSnippetsCommand(event);
- case JSHELL_CLOSE_SUBCOMMAND -> handleCloseCommand(event);
- case JSHELL_STARTUP_SCRIPT_SUBCOMMAND -> handleStartupScriptCommand(event);
+ case VERSION_SUBCOMMAND -> handleVersionCommand(event);
+ case EVAL_SUBCOMMAND -> handleEvalCommand(event);
+ case SNIPPETS_SUBCOMMAND -> handleSnippetsCommand(event);
+ case CLOSE_SUBCOMMAND -> handleCloseCommand(event);
+ case STARTUP_SCRIPT_SUBCOMMAND -> handleStartupScriptCommand(event);
default -> throw new AssertionError(
"Unexpected Subcommand: " + event.getSubcommandName());
}
@@ -88,11 +95,10 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
@Override
public void onModalSubmitted(ModalInteractionEvent event, List
+ * Each method may do a blocking HTTP request and may throw a RequestFailedException if status code
+ * isn't 200 or 204.
+ *
+ * When startup script boolean argument is asked, true means {@link JShellApi#STARTUP_SCRIPT_ID} and
+ * false means Together-Java JShell backend's default startup script.
+ *
+ * For more information, check the Together-Java JShell backend project.
+ */
public class JShellApi {
public static final int SESSION_NOT_FOUND = 404;
+ /**
+ * The startup script to use when startup script boolean argument is true.
+ */
private static final String STARTUP_SCRIPT_ID = "CUSTOM_DEFAULT";
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
private final String baseUrl;
+ /**
+ * Creates a JShellAPI
+ *
+ * @param objectMapper the json mapper to use
+ * @param baseUrl the base url of the JShell REST API
+ */
public JShellApi(ObjectMapper objectMapper, String baseUrl) {
this.objectMapper = objectMapper;
this.baseUrl = baseUrl;
@@ -34,6 +55,15 @@ public JShellApi(ObjectMapper objectMapper, String baseUrl) {
this.httpClient = HttpClient.newBuilder().build();
}
+ /**
+ * Evaluates the code in a one time only session, will block until the request is over.
+ *
+ * @param code the code to evaluate
+ * @param startupScript if the {@link JShellApi#STARTUP_SCRIPT_ID startup script} should be
+ * executed at the start of the session
+ * @return the result of the evaluation
+ * @throws RequestFailedException if the status code is not 200 or 204
+ */
public JShellResult evalOnce(String code, boolean startupScript) throws RequestFailedException {
return send(
baseUrl + "single-eval"
@@ -42,6 +72,15 @@ public JShellResult evalOnce(String code, boolean startupScript) throws RequestF
ResponseUtils.ofJson(JShellResult.class, objectMapper)).body();
}
+ /**
+ * Evaluates the code in a regular session, will block until the request is over.
+ *
+ * @param code the code to evaluate
+ * @param startupScript if the {@link JShellApi#STARTUP_SCRIPT_ID startup script} should be
+ * executed at the start of the session
+ * @return the result of the evaluation
+ * @throws RequestFailedException if the status code is not 200 or 204
+ */
public JShellResult evalSession(String code, String sessionId, boolean startupScript)
throws RequestFailedException {
return send(
@@ -51,6 +90,14 @@ public JShellResult evalSession(String code, String sessionId, boolean startupSc
ResponseUtils.ofJson(JShellResult.class, objectMapper)).body();
}
+ /**
+ * Gets and return the snippets for the given session id, will block until the request is over.
+ *
+ * @param sessionId the id of the session to get the snippets from
+ * @param includeStartupScript if the startup script should be included in the returned snippets
+ * @return the snippets of the session
+ * @throws RequestFailedException if the status code is not 200 or 204
+ */
public SnippetList snippetsSession(String sessionId, boolean includeStartupScript)
throws RequestFailedException {
return send(
@@ -59,11 +106,24 @@ public SnippetList snippetsSession(String sessionId, boolean includeStartupScrip
ResponseUtils.ofJson(SnippetList.class, objectMapper)).body();
}
+ /**
+ * Closes the given session.
+ *
+ * @param sessionId the id of the session to close
+ * @throws RequestFailedException if the status code is not 200 or 204
+ */
public void closeSession(String sessionId) throws RequestFailedException {
send(baseUrl + sessionId, HttpRequest.newBuilder().DELETE(), BodyHandlers.discarding())
.body();
}
+ /**
+ * Gets and return the {@link JShellApi#STARTUP_SCRIPT_ID startup script}, will block until the
+ * request is over.
+ *
+ * @return the startup script
+ * @throws RequestFailedException if the status code is not 200 or 204
+ */
public String startupScript() throws RequestFailedException {
return send(baseUrl + "startup_script/" + STARTUP_SCRIPT_ID, HttpRequest.newBuilder().GET(),
BodyHandlers.ofString()).body();
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
index 1cbb548d6e..dbffefda69 100644
--- 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
@@ -1,4 +1,10 @@
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 afa83ed1b3..830eb17429 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,10 +1,25 @@
package org.togetherjava.tjbot.features.jshell.backend.dto;
+import javax.annotation.Nullable;
+
import java.util.List;
+/**
+ * Result of a JShell eval.
+ *
+ * @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
+ * @param exception thrown exception
+ * @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,
- String result, JShellExceptionResult exception, boolean stdoutOverflow, String stdout,
- List
+ * Defines a window and a number of request, for example, if 10 requests should be allowed per 5
+ * seconds, so 10/5s, the following should be called: {@snippet java: new
+ * RateLimit(Duration.of(5, TimeUnit.SECONDS), 10) }
+ *
+ * @param duration the duration of window
+ * @param allowedRequests the number of requests to allow in the window
+ */
public RateLimiter(Duration duration, int allowedRequests) {
this.duration = duration;
this.allowedRequests = allowedRequests;
@@ -23,6 +34,12 @@ public RateLimiter(Duration duration, int allowedRequests) {
this.lastUses = List.of();
}
+ /**
+ * Tries to allow the request. If it is allowed, the time is registered.
+ *
+ * @param time the time of the request
+ * @return if the request was allowed
+ */
public boolean allowRequest(Instant time) {
synchronized (this) {
List