Skip to content
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

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
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
44 changes: 44 additions & 0 deletions .github/workflows/deploy-jshell-aws.yaml
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
1 change: 1 addition & 0 deletions application/config.json.template
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"jshellAwsApiUrl": "<put_your_jshell_aws_api_url>",
"token": "<put_your_token_here>",
"githubApiKey": "<your_github_personal_access_token>",
"databasePath": "local-database.db",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Configuration of the application. Create instances using {@link #load(Path)}.
*/
public final class Config {
private final String jShellAwsApiUrl;
private final String token;
private final String githubApiKey;
private final String databasePath;
Expand Down Expand Up @@ -49,7 +50,8 @@ public final class Config {

@SuppressWarnings("ConstructorWithTooManyParameters")
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
private Config(@JsonProperty(value = "token", required = true) String token,
private Config(@JsonProperty(value = "jshellAwsApiUrl", required = true) String jShellAwsApiUrl,
@JsonProperty(value = "token", required = true) String token,
@JsonProperty(value = "githubApiKey", required = true) String githubApiKey,
@JsonProperty(value = "databasePath", required = true) String databasePath,
@JsonProperty(value = "projectWebsite", required = true) String projectWebsite,
Expand Down Expand Up @@ -95,6 +97,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
@JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig,
@JsonProperty(value = "selectRolesChannelPattern",
required = true) String selectRolesChannelPattern) {
this.jShellAwsApiUrl = Objects.requireNonNull(jShellAwsApiUrl);
this.token = Objects.requireNonNull(token);
this.githubApiKey = Objects.requireNonNull(githubApiKey);
this.databasePath = Objects.requireNonNull(databasePath);
Expand Down Expand Up @@ -418,4 +421,8 @@ public String getMemberCountCategoryPattern() {
public RSSFeedsConfig getRSSFeedsConfig() {
return rssFeedsConfig;
}

public String getjShellAwsApiUrl() {
return jShellAwsApiUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import org.togetherjava.tjbot.features.javamail.RSSHandlerRoutine;
import org.togetherjava.tjbot.features.jshell.JShellCommand;
import org.togetherjava.tjbot.features.jshell.JShellEval;
import org.togetherjava.tjbot.features.jshell.aws.JShellAWSCommand;
import org.togetherjava.tjbot.features.jshell.aws.JShellService;
import org.togetherjava.tjbot.features.mathcommands.TeXCommand;
import org.togetherjava.tjbot.features.mathcommands.wolframalpha.WolframAlphaCommand;
import org.togetherjava.tjbot.features.mediaonly.MediaOnlyChannelListener;
Expand Down Expand Up @@ -192,6 +194,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new BookmarksCommand(bookmarksSystem));
features.add(new ChatGptCommand(chatGptService, helpSystemHelper));
features.add(new JShellCommand(jshellEval));
features.add(new JShellAWSCommand(new JShellService(config.getjShellAwsApiUrl())));

FeatureBlacklist<Class<?>> blacklist = blacklistConfig.normal();
return blacklist.filterStream(features.stream(), Object::getClass).toList();
Expand Down
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;
}
Comment on lines +58 to +65
Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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

Copy link
Member

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
Screenshot 2024-09-14 at 4 37 42 PM

Since you made code param as mandatory, you no longer need to check for input being null or empty string


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();
}
}
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) {
}
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) {
}
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) {
}
Loading
Loading