Skip to content

Commit ff3612d

Browse files
committed
1158: Add boilerplate for the slash command
1 parent 465125a commit ff3612d

File tree

10 files changed

+357
-1
lines changed

10 files changed

+357
-1
lines changed

application/config.json.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"jshellAwsApiUrl": "<put_your_jshell_aws_api_url>",
23
"token": "<put_your_token_here>",
34
"githubApiKey": "<your_github_personal_access_token>",
45
"databasePath": "local-database.db",

application/src/main/java/org/togetherjava/tjbot/config/Config.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* Configuration of the application. Create instances using {@link #load(Path)}.
1717
*/
1818
public final class Config {
19+
private final String jShellAwsApiUrl;
1920
private final String token;
2021
private final String githubApiKey;
2122
private final String databasePath;
@@ -49,7 +50,8 @@ public final class Config {
4950

5051
@SuppressWarnings("ConstructorWithTooManyParameters")
5152
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
52-
private Config(@JsonProperty(value = "token", required = true) String token,
53+
private Config(@JsonProperty(value = "jshellAwsApiUrl", required = true) String jShellAwsApiUrl,
54+
@JsonProperty(value = "token", required = true) String token,
5355
@JsonProperty(value = "githubApiKey", required = true) String githubApiKey,
5456
@JsonProperty(value = "databasePath", required = true) String databasePath,
5557
@JsonProperty(value = "projectWebsite", required = true) String projectWebsite,
@@ -95,6 +97,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
9597
@JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig,
9698
@JsonProperty(value = "selectRolesChannelPattern",
9799
required = true) String selectRolesChannelPattern) {
100+
this.jShellAwsApiUrl = Objects.requireNonNull(jShellAwsApiUrl);
98101
this.token = Objects.requireNonNull(token);
99102
this.githubApiKey = Objects.requireNonNull(githubApiKey);
100103
this.databasePath = Objects.requireNonNull(databasePath);
@@ -418,4 +421,8 @@ public String getMemberCountCategoryPattern() {
418421
public RSSFeedsConfig getRSSFeedsConfig() {
419422
return rssFeedsConfig;
420423
}
424+
425+
public String getjShellAwsApiUrl() {
426+
return jShellAwsApiUrl;
427+
}
421428
}

application/src/main/java/org/togetherjava/tjbot/features/Features.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import org.togetherjava.tjbot.features.javamail.RSSHandlerRoutine;
3737
import org.togetherjava.tjbot.features.jshell.JShellCommand;
3838
import org.togetherjava.tjbot.features.jshell.JShellEval;
39+
import org.togetherjava.tjbot.features.jshell.aws.JShellAWSCommand;
40+
import org.togetherjava.tjbot.features.jshell.aws.JShellService;
3941
import org.togetherjava.tjbot.features.mathcommands.TeXCommand;
4042
import org.togetherjava.tjbot.features.mathcommands.wolframalpha.WolframAlphaCommand;
4143
import org.togetherjava.tjbot.features.mediaonly.MediaOnlyChannelListener;
@@ -192,6 +194,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
192194
features.add(new BookmarksCommand(bookmarksSystem));
193195
features.add(new ChatGptCommand(chatGptService, helpSystemHelper));
194196
features.add(new JShellCommand(jshellEval));
197+
features.add(new JShellAWSCommand(new JShellService(config.getjShellAwsApiUrl())));
195198

196199
FeatureBlacklist<Class<?>> blacklist = blacklistConfig.normal();
197200
return blacklist.filterStream(features.stream(), Object::getClass).toList();
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package org.togetherjava.tjbot.features.jshell.aws;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import net.dv8tion.jda.api.EmbedBuilder;
6+
import net.dv8tion.jda.api.entities.Member;
7+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
8+
import net.dv8tion.jda.api.interactions.InteractionHook;
9+
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
10+
import net.dv8tion.jda.api.interactions.commands.OptionType;
11+
import org.apache.logging.log4j.LogManager;
12+
import org.apache.logging.log4j.Logger;
13+
14+
import org.togetherjava.tjbot.features.CommandVisibility;
15+
import org.togetherjava.tjbot.features.SlashCommandAdapter;
16+
import org.togetherjava.tjbot.features.jshell.aws.exceptions.JShellAPIException;
17+
18+
import java.awt.*;
19+
20+
public class JShellAWSCommand extends SlashCommandAdapter {
21+
private static final Logger logger = LogManager.getLogger(JShellAWSCommand.class);
22+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
23+
private static final String CODE_PARAMETER = "code";
24+
private final JShellService jShellService;
25+
26+
public JShellAWSCommand(JShellService jShellService) {
27+
super("jshell-aws", "Execute Java code in Discord!", CommandVisibility.GUILD);
28+
getData().addOption(OptionType.STRING, CODE_PARAMETER, "The code to execute using JShell");
29+
this.jShellService = jShellService;
30+
}
31+
32+
@Override
33+
public void onSlashCommand(SlashCommandInteractionEvent event) {
34+
Member member = event.getMember();
35+
36+
if (member == null) {
37+
event.reply("Member that executed the command is no longer available, won't execute")
38+
.queue();
39+
return;
40+
}
41+
42+
logger.info("JShell AWS invoked by {} in channel {}", member.getAsMention(), event.getChannelId());
43+
44+
OptionMapping input = event.getOption(CODE_PARAMETER);
45+
46+
if (input == null) {
47+
EmbedBuilder eb = new EmbedBuilder();
48+
eb.setDescription(member.getAsMention() + ", you forgot to provide the code for JShell to evaluate!\nTry running the command again and make sure to select the code option");
49+
eb.setColor(Color.ORANGE);
50+
event.replyEmbeds(eb.build()).queue();
51+
return;
52+
}
53+
54+
event.deferReply().queue();
55+
56+
InteractionHook hook = event.getHook();
57+
58+
try {
59+
respondWithJShellOutput(hook,
60+
jShellService.sendRequest(new JShellRequest(input.getAsString())), input.getAsString());
61+
} catch (JShellAPIException jShellAPIException) {
62+
if (jShellAPIException.getStatusCode() == 400) {
63+
logger.warn("HTTP 400 error occurred with the JShell AWS API {}",
64+
jShellAPIException.getBody());
65+
respondWithInputError(hook, jShellAPIException.getBody());
66+
} else if (jShellAPIException.getStatusCode() == 408) {
67+
respondWithTimeout(hook, member, input.getAsString());
68+
} else {
69+
logger.error("HTTP {} received from JShell AWS API {}",
70+
jShellAPIException.getStatusCode(), jShellAPIException.getBody());
71+
respondWithSevereAPIError(hook);
72+
}
73+
} catch (Exception e) {
74+
logger.error(
75+
"An error occurred while sending/receiving request from the AWS JShell API", e);
76+
respondWithSevereAPIError(hook);
77+
}
78+
}
79+
80+
private static void respondWithJShellOutput(InteractionHook hook,
81+
JShellResponse response, String code) {
82+
// Extracted as fields to be compliant with Sonar
83+
final String SNIPPET_SECTION_TITLE = "## Snippets\n";
84+
final String BACKTICK = "`";
85+
final String NEWLINE = "\n";
86+
final String DOUBLE_NEWLINE = "\n\n";
87+
final String STATUS = "**Status**: ";
88+
final String OUTPUT_SECTION_TITLE = "**Output**\n";
89+
final String JAVA_CODE_BLOCK_START = "```java\n";
90+
final String CODE_BLOCK_END = "```\n";
91+
final String DIAGNOSTICS_SECTION_TITLE = "**Diagnostics**\n";
92+
final String CONSOLE_OUTPUT_SECTION_TITLE = "## Console Output\n";
93+
final String ERROR_OUTPUT_SECTION_TITLE = "## Error Output\n";
94+
95+
StringBuilder sb = new StringBuilder();
96+
sb.append(SNIPPET_SECTION_TITLE);
97+
98+
for (JShellSnippet snippet : response.events()) {
99+
sb.append(BACKTICK);
100+
sb.append(snippet.statement());
101+
sb.append(BACKTICK).append(DOUBLE_NEWLINE);
102+
sb.append(STATUS);
103+
sb.append(snippet.status());
104+
sb.append(NEWLINE);
105+
106+
if (snippet.value() != null && !snippet.value().isEmpty()) {
107+
sb.append(OUTPUT_SECTION_TITLE);
108+
sb.append(JAVA_CODE_BLOCK_START);
109+
sb.append(snippet.value());
110+
sb.append(CODE_BLOCK_END);
111+
}
112+
113+
if (!snippet.diagnostics().isEmpty()) {
114+
sb.append(DIAGNOSTICS_SECTION_TITLE);
115+
for (String diagnostic : snippet.diagnostics()) {
116+
sb.append(BACKTICK).append(diagnostic).append(BACKTICK).append(NEWLINE);
117+
}
118+
}
119+
}
120+
121+
if (response.outputStream() != null && !response.outputStream().isEmpty()) {
122+
sb.append(CONSOLE_OUTPUT_SECTION_TITLE);
123+
sb.append(JAVA_CODE_BLOCK_START);
124+
sb.append(response.outputStream());
125+
sb.append(CODE_BLOCK_END);
126+
}
127+
128+
if (response.errorStream() != null && !response.errorStream().isEmpty()) {
129+
sb.append(ERROR_OUTPUT_SECTION_TITLE);
130+
sb.append(JAVA_CODE_BLOCK_START);
131+
sb.append(response.errorStream());
132+
sb.append(CODE_BLOCK_END);
133+
}
134+
135+
EmbedBuilder eb = new EmbedBuilder();
136+
eb.setFooter("Code that was executed:\n" + code);
137+
if (sb.length() > 4000) {
138+
eb.setDescription(sb.substring(0, 500) + "\n... truncated " + (sb.length() - 500) + " characters\n```");
139+
} else {
140+
eb.setDescription(sb.toString());
141+
}
142+
eb.setColor(Color.GREEN);
143+
144+
hook.editOriginalEmbeds(eb.build()).queue();
145+
}
146+
147+
148+
private static void respondWithInputError(InteractionHook hook, String response) {
149+
JShellErrorResponse errorResponse;
150+
try {
151+
errorResponse = OBJECT_MAPPER.readValue(response, JShellErrorResponse.class);
152+
} catch (JsonProcessingException e) {
153+
errorResponse = new JShellErrorResponse(
154+
"There was a problem with the input you provided, please check and try again");
155+
}
156+
EmbedBuilder eb = new EmbedBuilder();
157+
eb.setDescription(errorResponse.error());
158+
eb.setColor(Color.ORANGE);
159+
hook.editOriginalEmbeds(eb.build()).queue();
160+
}
161+
162+
private static void respondWithTimeout(InteractionHook hook, Member member, String code) {
163+
EmbedBuilder eb = new EmbedBuilder();
164+
eb.setDescription(member.getAsMention() + " the code you provided took too long and the request has timed out! Consider tweaking your code to run a little faster.");
165+
eb.setColor(Color.ORANGE);
166+
eb.setFooter("Code that was executed:\n" + code);
167+
hook.editOriginalEmbeds(eb.build()).queue();
168+
}
169+
170+
private static void respondWithSevereAPIError(InteractionHook hook) {
171+
EmbedBuilder eb = new EmbedBuilder();
172+
eb.setDescription("An internal error occurred, please try again later");
173+
eb.setColor(Color.RED);
174+
hook.editOriginalEmbeds(eb.build()).queue();
175+
}
176+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.togetherjava.tjbot.features.jshell.aws;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
/**
7+
* Represents a response from JShell that contains an error key.
8+
*
9+
* @author Suraj Kuamr
10+
*/
11+
@JsonIgnoreProperties(ignoreUnknown = true)
12+
public record JShellErrorResponse(@JsonProperty("error") String error) {
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.togetherjava.tjbot.features.jshell.aws;
2+
3+
/**
4+
* A record containing the code snippet to be evaluated by the AWS JShell API
5+
*
6+
* @param code The Java code snippet to execute
7+
*
8+
* @author Suraj Kumar
9+
*/
10+
public record JShellRequest(String code) {
11+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.togetherjava.tjbot.features.jshell.aws;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
import java.util.List;
7+
8+
/**
9+
* A record containing the AWS JShell API response.
10+
*
11+
* @param errorStream The content in JShells error stream
12+
* @param outputStream The content in JShells standard output stream
13+
* @param events A list of snippets that were evaluated
14+
*
15+
* @author Suraj Kumar
16+
*/
17+
@JsonIgnoreProperties(ignoreUnknown = true)
18+
public record JShellResponse(@JsonProperty("errorStream") String errorStream,
19+
@JsonProperty("outputStream") String outputStream,
20+
@JsonProperty("events") List<JShellSnippet> events) {
21+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.togetherjava.tjbot.features.jshell.aws;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import org.apache.logging.log4j.LogManager;
6+
import org.apache.logging.log4j.Logger;
7+
8+
import org.togetherjava.tjbot.features.jshell.aws.exceptions.JShellAPIException;
9+
10+
import java.io.IOException;
11+
import java.net.URI;
12+
import java.net.URISyntaxException;
13+
import java.net.http.HttpClient;
14+
import java.net.http.HttpRequest;
15+
import java.net.http.HttpResponse;
16+
17+
/**
18+
* The JShellService class is used to interact with the AWS JShell API.
19+
*
20+
* @author Suraj Kumar
21+
*/
22+
public class JShellService {
23+
private static final Logger LOGGER = LogManager.getLogger(JShellService.class);
24+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
25+
private final String apiURL;
26+
private final HttpClient httpClient;
27+
28+
/**
29+
* Constructs a JShellService.
30+
*
31+
* @param apiURl The Lambda Function URL to send API requests to
32+
*/
33+
public JShellService(String apiURl) {
34+
this.apiURL = apiURl;
35+
this.httpClient = HttpClient.newHttpClient();
36+
}
37+
38+
/**
39+
* Sends an HTTP request to the AWS JShell API.
40+
*
41+
* @param jShellRequest The request object containing the code to evaluate
42+
* @return The API response as a JShellResponse object
43+
* @throws URISyntaxException If the API URL is invalid
44+
* @throws JsonProcessingException If the API response failed to get parsed by Jackson to our
45+
* mapping.
46+
*/
47+
public JShellResponse sendRequest(JShellRequest jShellRequest)
48+
throws URISyntaxException, JsonProcessingException {
49+
HttpRequest request = HttpRequest.newBuilder()
50+
.uri(new URI(apiURL))
51+
.header("Content-Type", "application/json")
52+
.POST(HttpRequest.BodyPublishers
53+
.ofString(OBJECT_MAPPER.writeValueAsString(jShellRequest)))
54+
.build();
55+
56+
try {
57+
HttpResponse<String> response =
58+
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
59+
60+
if (response.statusCode() != 200) {
61+
throw new JShellAPIException(response.statusCode(), response.body());
62+
}
63+
64+
String body = response.body();
65+
LOGGER.trace("Received the following body from the AWS JShell API: {}", body);
66+
67+
return OBJECT_MAPPER.readValue(response.body(), JShellResponse.class);
68+
69+
} catch (IOException | InterruptedException e) {
70+
LOGGER.error("Failed to send http request to the AWS JShell API", e);
71+
Thread.currentThread().interrupt();
72+
}
73+
74+
return null;
75+
}
76+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.togetherjava.tjbot.features.jshell.aws;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
import java.util.List;
7+
8+
/**
9+
* A JShell snippet is a statement that is to be executed. This record is used to hold information
10+
* about a statement that was provided by the AWS JShell API
11+
*
12+
* @param statement The statement that was executed
13+
* @param value The return value of the statement
14+
* @param status The status from evaluating the statement e.g. "VALID", "INVALID"
15+
* @param diagnostics A list of diagnostics such as error messages provided by JShell
16+
*
17+
* @author Suraj Kumar
18+
*/
19+
@JsonIgnoreProperties(ignoreUnknown = true)
20+
public record JShellSnippet(@JsonProperty("statement") String statement,
21+
@JsonProperty("value") String value, @JsonProperty("status") String status,
22+
List<String> diagnostics) {
23+
}

0 commit comments

Comments
 (0)