-
-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
1158: AWS version of JShell #1161
Closed
Closed
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2d3a235
1158: Add backend for AWS JShell
surajkumar 852adb8
1158: `/jshell-aws` slash command
surajkumar f82ce1f
1158: Add GitHub action to deploy JShell to AWS
surajkumar 86fc982
Added timeout to JShell API call
surajkumar b18ac90
Add `required` flag to the `code` parameter in the /jshell-aws command
surajkumar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
name: deploy | ||
on: | ||
push: | ||
branches: [ master ] | ||
paths: | ||
- 'jshell-aws-backend/**' | ||
workflow_dispatch: | ||
|
||
permissions: | ||
id-token: write | ||
contents: read | ||
|
||
jobs: | ||
deploy: | ||
name: Deploy to AWS | ||
runs-on: ubuntu-latest | ||
env: | ||
AWS_REGION: eu-west-2 | ||
steps: | ||
- name: Check out code | ||
uses: actions/checkout@v4 | ||
|
||
- name: Authenticate with AWS | ||
uses: aws-actions/configure-aws-credentials@v4 | ||
with: | ||
role-to-assume: ${{ secrets.GH_ACTIONS_ROLE_ARN }} | ||
aws-region: ${{ env.AWS_REGION }} | ||
|
||
- name: Install SAM CLI | ||
uses: aws-actions/setup-sam@v2 | ||
with: | ||
use-installer: true | ||
|
||
- name: Install Java | ||
uses: actions/setup-java@v3 | ||
with: | ||
java-version: '21' | ||
distribution: 'corretto' | ||
|
||
- name: Build Application | ||
run: sam build -t infrastructure/template.yaml | ||
|
||
- name: Deploy to AWS | ||
run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --config-file infrastructure/samconfig.toml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
199 changes: 199 additions & 0 deletions
199
application/src/main/java/org/togetherjava/tjbot/features/jshell/aws/JShellAWSCommand.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
package org.togetherjava.tjbot.features.jshell.aws; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import net.dv8tion.jda.api.EmbedBuilder; | ||
import net.dv8tion.jda.api.entities.Member; | ||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; | ||
import net.dv8tion.jda.api.interactions.InteractionHook; | ||
import net.dv8tion.jda.api.interactions.commands.OptionMapping; | ||
import net.dv8tion.jda.api.interactions.commands.OptionType; | ||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
|
||
import org.togetherjava.tjbot.features.CommandVisibility; | ||
import org.togetherjava.tjbot.features.SlashCommandAdapter; | ||
import org.togetherjava.tjbot.features.jshell.aws.exceptions.JShellAPIException; | ||
|
||
import java.awt.Color; | ||
|
||
/** | ||
* This class contains the complete logic for the /jshell-aws command. | ||
* | ||
* @author Suraj Kumar | ||
*/ | ||
public class JShellAWSCommand extends SlashCommandAdapter { | ||
private static final Logger logger = LogManager.getLogger(JShellAWSCommand.class); | ||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
private static final String CODE_PARAMETER = "code"; | ||
private final JShellService jShellService; | ||
|
||
/** | ||
* Constructs a new JShellAWSCommand | ||
* | ||
* @param jShellService The service class to make requests against AWS | ||
*/ | ||
public JShellAWSCommand(JShellService jShellService) { | ||
super("jshell-aws", "Execute Java code in Discord!", CommandVisibility.GUILD); | ||
getData().addOption(OptionType.STRING, CODE_PARAMETER, "The code to execute using JShell", | ||
true); | ||
this.jShellService = jShellService; | ||
} | ||
|
||
@Override | ||
public void onSlashCommand(SlashCommandInteractionEvent event) { | ||
Member member = event.getMember(); | ||
|
||
if (member == null) { | ||
event.reply("Member that executed the command is no longer available, won't execute") | ||
.queue(); | ||
return; | ||
} | ||
|
||
logger.info("JShell AWS invoked by {} in channel {}", member.getAsMention(), | ||
event.getChannelId()); | ||
|
||
OptionMapping input = event.getOption(CODE_PARAMETER); | ||
|
||
if (input == null || input.getAsString().isEmpty()) { | ||
EmbedBuilder eb = new EmbedBuilder(); | ||
eb.setDescription(member.getAsMention() | ||
+ ", you forgot to provide the code for JShell to evaluate or it was too short!\nTry running the command again and make sure to select the code option"); | ||
eb.setColor(Color.ORANGE); | ||
event.replyEmbeds(eb.build()).queue(); | ||
return; | ||
} | ||
|
||
event.deferReply().queue(); | ||
|
||
InteractionHook hook = event.getHook(); | ||
|
||
String code = input.getAsString(); | ||
|
||
try { | ||
respondWithJShellOutput(hook, jShellService.sendRequest(new JShellRequest(code)), code); | ||
} catch (JShellAPIException jShellAPIException) { | ||
handleJShellAPIException(hook, jShellAPIException, member, code); | ||
} catch (Exception e) { | ||
logger.error( | ||
"An error occurred while sending/receiving request from the AWS JShell API", e); | ||
respondWithSevereAPIError(hook, code); | ||
} | ||
} | ||
|
||
private static void handleJShellAPIException(InteractionHook hook, | ||
JShellAPIException jShellAPIException, Member member, String code) { | ||
switch (jShellAPIException.getStatusCode()) { | ||
case 400 -> { | ||
logger.warn("HTTP 400 error occurred with the JShell AWS API {}", | ||
jShellAPIException.getBody()); | ||
respondWithInputError(hook, jShellAPIException.getBody()); | ||
} | ||
case 408 -> respondWithTimeout(hook, member, code); | ||
default -> { | ||
logger.error("HTTP {} received from JShell AWS API {}", | ||
jShellAPIException.getStatusCode(), jShellAPIException.getBody()); | ||
respondWithSevereAPIError(hook, code); | ||
} | ||
} | ||
} | ||
|
||
private static void respondWithJShellOutput(InteractionHook hook, JShellResponse response, | ||
String code) { | ||
// Extracted as fields to be compliant with Sonar | ||
final String SNIPPET_SECTION_TITLE = "## Snippets\n"; | ||
final String BACKTICK = "`"; | ||
final String NEWLINE = "\n"; | ||
final String DOUBLE_NEWLINE = "\n\n"; | ||
final String STATUS = "**Status**: "; | ||
final String OUTPUT_SECTION_TITLE = "**Output**\n"; | ||
final String JAVA_CODE_BLOCK_START = "```java\n"; | ||
final String CODE_BLOCK_END = "```\n"; | ||
final String DIAGNOSTICS_SECTION_TITLE = "**Diagnostics**\n"; | ||
final String CONSOLE_OUTPUT_SECTION_TITLE = "## Console Output\n"; | ||
final String ERROR_OUTPUT_SECTION_TITLE = "## Error Output\n"; | ||
|
||
StringBuilder sb = new StringBuilder(); | ||
sb.append(SNIPPET_SECTION_TITLE); | ||
|
||
for (JShellSnippet snippet : response.events()) { | ||
sb.append(BACKTICK); | ||
sb.append(snippet.statement()); | ||
sb.append(BACKTICK).append(DOUBLE_NEWLINE); | ||
sb.append(STATUS); | ||
sb.append(snippet.status()); | ||
sb.append(NEWLINE); | ||
|
||
if (snippet.value() != null && !snippet.value().isEmpty()) { | ||
sb.append(OUTPUT_SECTION_TITLE); | ||
sb.append(JAVA_CODE_BLOCK_START); | ||
sb.append(snippet.value()); | ||
sb.append(CODE_BLOCK_END); | ||
} | ||
|
||
if (!snippet.diagnostics().isEmpty()) { | ||
sb.append(DIAGNOSTICS_SECTION_TITLE); | ||
for (String diagnostic : snippet.diagnostics()) { | ||
sb.append(BACKTICK).append(diagnostic).append(BACKTICK).append(NEWLINE); | ||
} | ||
} | ||
} | ||
|
||
if (response.outputStream() != null && !response.outputStream().isEmpty()) { | ||
sb.append(CONSOLE_OUTPUT_SECTION_TITLE); | ||
sb.append(JAVA_CODE_BLOCK_START); | ||
sb.append(response.outputStream()); | ||
sb.append(CODE_BLOCK_END); | ||
} | ||
|
||
if (response.errorStream() != null && !response.errorStream().isEmpty()) { | ||
sb.append(ERROR_OUTPUT_SECTION_TITLE); | ||
sb.append(JAVA_CODE_BLOCK_START); | ||
sb.append(response.errorStream()); | ||
sb.append(CODE_BLOCK_END); | ||
} | ||
|
||
String description; | ||
if (sb.length() > 4000) { | ||
description = sb.substring(0, 500) + "...``` truncated " + (sb.length() - 500) | ||
+ " characters"; | ||
} else { | ||
ankitsmt211 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
description = sb.toString(); | ||
} | ||
|
||
sendEmbed(hook, description, Color.GREEN, code); | ||
} | ||
|
||
private static void respondWithInputError(InteractionHook hook, String response) { | ||
JShellErrorResponse errorResponse; | ||
try { | ||
errorResponse = OBJECT_MAPPER.readValue(response, JShellErrorResponse.class); | ||
} catch (JsonProcessingException e) { | ||
errorResponse = new JShellErrorResponse( | ||
"There was a problem with the input you provided, please check and try again"); | ||
} | ||
EmbedBuilder eb = new EmbedBuilder(); | ||
eb.setDescription(errorResponse.error()); | ||
eb.setColor(Color.ORANGE); | ||
hook.editOriginalEmbeds(eb.build()).queue(); | ||
} | ||
|
||
private static void respondWithTimeout(InteractionHook hook, Member member, String code) { | ||
sendEmbed(hook, member.getAsMention() | ||
+ " the code you provided took too long and the request has timed out! Consider tweaking your code to run a little faster.", | ||
Color.ORANGE, code); | ||
} | ||
|
||
private static void respondWithSevereAPIError(InteractionHook hook, String code) { | ||
sendEmbed(hook, "An internal error occurred, please try again later", Color.RED, code); | ||
} | ||
|
||
private static void sendEmbed(InteractionHook hook, String description, Color color, | ||
String code) { | ||
EmbedBuilder eb = new EmbedBuilder(); | ||
eb.setDescription(description); | ||
eb.setColor(color); | ||
eb.setFooter("Code that was executed:\n" + code); | ||
ankitsmt211 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
hook.editOriginalEmbeds(eb.build()).queue(); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
...ication/src/main/java/org/togetherjava/tjbot/features/jshell/aws/JShellErrorResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package org.togetherjava.tjbot.features.jshell.aws; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
||
/** | ||
* Represents a response from JShell that contains an error key. | ||
* | ||
* @author Suraj Kuamr | ||
*/ | ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
public record JShellErrorResponse(@JsonProperty("error") String error) { | ||
} |
11 changes: 11 additions & 0 deletions
11
application/src/main/java/org/togetherjava/tjbot/features/jshell/aws/JShellRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package org.togetherjava.tjbot.features.jshell.aws; | ||
|
||
/** | ||
* A record containing the code snippet to be evaluated by the AWS JShell API | ||
* | ||
* @param code The Java code snippet to execute | ||
* | ||
* @author Suraj Kumar | ||
*/ | ||
public record JShellRequest(String code) { | ||
} |
21 changes: 21 additions & 0 deletions
21
application/src/main/java/org/togetherjava/tjbot/features/jshell/aws/JShellResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package org.togetherjava.tjbot.features.jshell.aws; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
||
import java.util.List; | ||
|
||
/** | ||
* A record containing the AWS JShell API response. | ||
* | ||
* @param errorStream The content in JShells error stream | ||
* @param outputStream The content in JShells standard output stream | ||
* @param events A list of snippets that were evaluated | ||
* | ||
* @author Suraj Kumar | ||
*/ | ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
public record JShellResponse(@JsonProperty("errorStream") String errorStream, | ||
@JsonProperty("outputStream") String outputStream, | ||
@JsonProperty("events") List<JShellSnippet> events) { | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when setting up code as command option, you can set required property to True thus user can't execute the command without passing in something as code to begin with
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check, but I think true is the default value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added it anyway, it enforces readability ig
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I double checked, required is false by default
Since you made code param as mandatory, you no longer need to check for input being null or empty string