Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9c50dbd
used github api for creating gists (#796)
Taz03 Mar 8, 2023
6869def
Bump org.jooq:jooq from 3.17.2 to 3.18.0 (#799)
dependabot[bot] Mar 9, 2023
6d5234f
Bump org.mockito:mockito-core from 5.1.0 to 5.2.0 (#801)
dependabot[bot] Mar 10, 2023
5dbe008
add modal support to /modmail command (#800)
hamzanaciri99 Mar 11, 2023
411aa57
Bump com.diffplug.spotless from 6.16.0 to 6.17.0 (#803)
dependabot[bot] Mar 14, 2023
037a02b
remove bootstrap launcher (#802)
Taz03 Mar 14, 2023
5ccb89f
Bump gradle.plugin.org.flywaydb:gradle-plugin-publishing (#805)
dependabot[bot] Mar 16, 2023
beb7164
Bump org.flywaydb:flyway-core from 9.15.0 to 9.16.0 (#806)
dependabot[bot] Mar 16, 2023
cb3abfc
Logo Update (#807)
Zabuzard Mar 17, 2023
09d4ce3
Update to SlashCommandEducator to exclude strings that end with ")" o…
tmcdonnell2 Mar 21, 2023
23a97eb
ChatGPT Intialization (#810)
tmcdonnell2 Mar 25, 2023
9fa86e0
Update build.gradle (#811)
Zabuzard Mar 25, 2023
2257253
Improved chatGpt slash command (#818)
tmcdonnell2 Apr 15, 2023
5c54d3c
Bump chatGPTVersion from 0.11.1 to 0.12.0 (#817)
dependabot[bot] Apr 17, 2023
1a66ff5
Bump com.diffplug.spotless from 6.17.0 to 6.18.0 (#819)
dependabot[bot] Apr 17, 2023
e62b4c2
Bump nu.studer:gradle-jooq-plugin from 8.1 to 8.2 (#823)
dependabot[bot] Apr 17, 2023
db568d4
Bump jacksonVersion from 2.14.0 to 2.15.0 (#825)
dependabot[bot] Apr 24, 2023
381f237
Bump org.mockito:mockito-core from 5.2.0 to 5.3.1 (#826)
dependabot[bot] Apr 24, 2023
a5ca31d
Bump name.remal.sonarlint from 3.0.7 to 3.1.0 (#827)
dependabot[bot] Apr 27, 2023
6271904
Bump org.flywaydb:flyway-core from 9.16.0 to 9.17.0 (#828)
dependabot[bot] Apr 28, 2023
8921b3b
Bump org.jsoup:jsoup from 1.15.3 to 1.16.1 (#829)
dependabot[bot] May 1, 2023
54b59da
Bump gradle.plugin.org.flywaydb:gradle-plugin-publishing (#830)
dependabot[bot] May 3, 2023
4d3c61e
Bump gradle.plugin.org.flywaydb:gradle-plugin-publishing (#835)
dependabot[bot] May 16, 2023
cf910d9
Bump org.flywaydb:flyway-core from 9.17.0 to 9.18.0 (#834)
dependabot[bot] May 16, 2023
c1d6b9a
Remove website module (#833)
marko-radosavljevic May 16, 2023
f7675d1
Bump name.remal.sonarlint from 3.1.0 to 3.2.0 (#836)
dependabot[bot] May 18, 2023
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ logviewer/webpack.generated.js
.DS_Store

# VisualStudioCode
.vscode
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
Expand Down
13 changes: 9 additions & 4 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jib {
}
}
container {
mainClass = 'org.togetherjava.tjbot.BootstrapLauncher'
mainClass = 'org.togetherjava.tjbot.Application'
setCreationTime(java.time.Instant.now().toString())
}
}
Expand Down Expand Up @@ -57,7 +57,7 @@ dependencies {

implementation 'io.mikael:urlbuilder:2.0.9'

implementation 'org.jsoup:jsoup:1.15.3'
implementation 'org.jsoup:jsoup:1.16.1'

implementation 'org.scilab.forge:jlatexmath:1.0.7'
implementation 'org.scilab.forge:jlatexmath-font-greek:1.0.7'
Expand All @@ -73,13 +73,18 @@ dependencies {
implementation 'io.github.url-detector:url-detector:0.1.23'

implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1'

implementation 'org.kohsuke:github-api:1.314'

testImplementation 'org.mockito:mockito-core:5.1.0'
testImplementation 'org.mockito:mockito-core:5.3.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'

implementation "com.theokanning.openai-gpt3-java:api:$chatGPTVersion"
implementation "com.theokanning.openai-gpt3-java:service:$chatGPTVersion"
}

application {
mainClass = 'org.togetherjava.tjbot.BootstrapLauncher'
mainClass = 'org.togetherjava.tjbot.Application'
}
1 change: 1 addition & 0 deletions application/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,5 @@
],
"logInfoChannelWebhook": "<put_your_webhook_here>",
"logErrorChannelWebhook": "<put_your_webhook_here>"
"openaiApiKey": "<check pins in #tjbot_discussion for the key>"
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public final class Config {
private final String mediaOnlyChannelPattern;
private final String logInfoChannelWebhook;
private final String logErrorChannelWebhook;
private final String openaiApiKey;

@SuppressWarnings("ConstructorWithTooManyParameters")
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
Expand Down Expand Up @@ -70,7 +71,8 @@ private Config(@JsonProperty(value = "token", required = true) String token,
@JsonProperty(value = "logInfoChannelWebhook",
required = true) String logInfoChannelWebhook,
@JsonProperty(value = "logErrorChannelWebhook",
required = true) String logErrorChannelWebhook) {
required = true) String logErrorChannelWebhook,
@JsonProperty(value = "openaiApiKey", required = true) String openaiApiKey) {
this.token = Objects.requireNonNull(token);
this.gistApiKey = Objects.requireNonNull(gistApiKey);
this.databasePath = Objects.requireNonNull(databasePath);
Expand All @@ -93,6 +95,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
this.blacklistedFileExtension = Objects.requireNonNull(blacklistedFileExtension);
this.logInfoChannelWebhook = Objects.requireNonNull(logInfoChannelWebhook);
this.logErrorChannelWebhook = Objects.requireNonNull(logErrorChannelWebhook);
this.openaiApiKey = Objects.requireNonNull(openaiApiKey);
}

/**
Expand Down Expand Up @@ -304,4 +307,13 @@ public String getLogInfoChannelWebhook() {
public String getLogErrorChannelWebhook() {
return logErrorChannelWebhook;
}

/**
* The OpenAI token needed for communicating with OpenAI ChatGPT.
*
* @return the OpenAI API Token
*/
public String getOpenaiApiKey() {
return openaiApiKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.features.basic.*;
import org.togetherjava.tjbot.features.bookmarks.*;
import org.togetherjava.tjbot.features.chaptgpt.ChatGptCommand;
import org.togetherjava.tjbot.features.chaptgpt.ChatGptService;
import org.togetherjava.tjbot.features.code.CodeMessageAutoDetection;
import org.togetherjava.tjbot.features.code.CodeMessageHandler;
import org.togetherjava.tjbot.features.code.CodeMessageManualDetection;
Expand Down Expand Up @@ -72,6 +74,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
ScamHistoryStore scamHistoryStore = new ScamHistoryStore(database);
HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database);
CodeMessageHandler codeMessageHandler = new CodeMessageHandler();
ChatGptService chatGptService = new ChatGptService(config);

// NOTE The system can add special system relevant commands also by itself,
// hence this list may not necessarily represent the full list of all commands actually
Expand Down Expand Up @@ -139,7 +142,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new HelpThreadCommand(config, helpSystemHelper));
features.add(new ReportCommand(config));
features.add(new BookmarksCommand(bookmarksSystem));

features.add(new ChatGptCommand(chatGptService));
return features;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public final class SlashCommandEducator extends MessageReceiverAdapter {
private static final Predicate<String> IS_MESSAGE_COMMAND = Pattern.compile("""
[.!?] #Start of message command
[a-zA-Z]{2,15} #Name of message command, e.g. 'close'
.* #Rest of the message
.*[^);] #Rest of the message (don't end with code stuff)
""", Pattern.COMMENTS).asMatchPredicate();

@Override
Expand All @@ -48,24 +48,25 @@ private void sendAdvice(Message message) {
Try starting your message with a forward-slash `/` and Discord should open a popup showing you all available commands.
A command might then look like `/foo` 👍""";

createReply(message, content, SLASH_COMMAND_POPUP_ADVICE_PATH).queue();
createReply(message, content).queue();
}

private static MessageCreateAction createReply(Message messageToReplyTo, String content,
String imagePath) {
private static MessageCreateAction createReply(Message messageToReplyTo, String content) {
boolean useImage = true;
InputStream imageData = HelpSystemHelper.class.getResourceAsStream("/" + imagePath);
InputStream imageData =
HelpSystemHelper.class.getResourceAsStream("/" + SLASH_COMMAND_POPUP_ADVICE_PATH);
if (imageData == null) {
useImage = false;
}

MessageEmbed embed = new EmbedBuilder().setDescription(content)
.setImage(useImage ? "attachment://" + imagePath : null)
.setImage(useImage ? "attachment://" + SLASH_COMMAND_POPUP_ADVICE_PATH : null)
.build();

MessageCreateAction action = messageToReplyTo.replyEmbeds(embed);
if (useImage) {
action = action.addFiles(FileUpload.fromData(imageData, imagePath));
action = action
.addFiles(FileUpload.fromData(imageData, SLASH_COMMAND_POPUP_ADVICE_PATH));
}

return action;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.togetherjava.tjbot.features.chaptgpt;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.components.Modal;
import net.dv8tion.jda.api.interactions.components.text.TextInput;
import net.dv8tion.jda.api.interactions.components.text.TextInputStyle;

import org.togetherjava.tjbot.features.CommandVisibility;
import org.togetherjava.tjbot.features.SlashCommandAdapter;

import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;

/**
* The implemented command is {@code /chatgpt}, which allows users to ask ChatGPT a question, upon
* which it will respond with an AI generated answer.
*/
public final class ChatGptCommand extends SlashCommandAdapter {
private static final String QUESTION_INPUT = "question";
private static final int MAX_MESSAGE_INPUT_LENGTH = 200;
private static final int MIN_MESSAGE_INPUT_LENGTH = 4;
private static final Duration COMMAND_COOLDOWN = Duration.of(10, ChronoUnit.SECONDS);
private final ChatGptService chatGptService;

private final Cache<String, Instant> userIdToAskedAtCache =
Caffeine.newBuilder().maximumSize(1_000).expireAfterWrite(COMMAND_COOLDOWN).build();

/**
* Creates an instance of the chatgpt command.
*
* @param chatGptService ChatGptService - Needed to make calls to ChatGPT API
*/
public ChatGptCommand(ChatGptService chatGptService) {
super("chatgpt", "Ask the ChatGPT AI a question!", CommandVisibility.GUILD);

this.chatGptService = chatGptService;
}

@Override
public void onSlashCommand(SlashCommandInteractionEvent event) {
Instant previousAskTime = userIdToAskedAtCache.getIfPresent(event.getMember().getId());
if (previousAskTime != null) {
long timeRemainingUntilNextAsk =
COMMAND_COOLDOWN.minus(Duration.between(previousAskTime, Instant.now()))
.toSeconds();

event
.reply("Sorry, you need to wait another " + timeRemainingUntilNextAsk
+ " second(s) before asking chatGPT another question.")
.setEphemeral(true)
.queue();
return;
}

TextInput body = TextInput
.create(QUESTION_INPUT, "Ask ChatGPT a question or get help with code",
TextInputStyle.PARAGRAPH)
.setPlaceholder("Put your question for ChatGPT here")
.setRequiredRange(MIN_MESSAGE_INPUT_LENGTH, MAX_MESSAGE_INPUT_LENGTH)
.build();

Modal modal = Modal.create(generateComponentId(), "ChatGPT").addActionRow(body).build();
event.replyModal(modal).queue();
}

@Override
public void onModalSubmitted(ModalInteractionEvent event, List<String> args) {
event.deferReply().queue();

Optional<String> optional =
chatGptService.ask(event.getValue(QUESTION_INPUT).getAsString());
if (optional.isPresent()) {
userIdToAskedAtCache.put(event.getMember().getId(), Instant.now());
}

String response = optional.orElse(
"An error has occurred while trying to communicate with ChatGPT. Please try again later");
event.getHook().sendMessage(response).queue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.togetherjava.tjbot.features.chaptgpt;

import com.theokanning.openai.OpenAiHttpException;
import com.theokanning.openai.completion.chat.ChatCompletionRequest;
import com.theokanning.openai.completion.chat.ChatMessage;
import com.theokanning.openai.completion.chat.ChatMessageRole;
import com.theokanning.openai.service.OpenAiService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.togetherjava.tjbot.config.Config;

import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
* Service used to communicate to OpenAI API to generate responses.
*/
public class ChatGptService {
private static final Logger logger = LoggerFactory.getLogger(ChatGptService.class);
private static final Duration TIMEOUT = Duration.ofSeconds(10);
private static final int MAX_TOKENS = 3_000;
private boolean isDisabled = false;
private final OpenAiService openAiService;

/**
* Creates instance of ChatGPTService
*
* @param config needed for token to OpenAI API.
*/
public ChatGptService(Config config) {
String apiKey = config.getOpenaiApiKey();
if (apiKey.isBlank()) {
isDisabled = true;
}

openAiService = new OpenAiService(apiKey, TIMEOUT);
}

/**
* Prompt ChatGPT with a question and receive a response.
*
* @param question The question being asked of ChatGPT. Max is {@value MAX_TOKENS} tokens.
* @see <a href="https://platform.openai.com/docs/guides/chat/managing-tokens">ChatGPT
* Tokens</a>.
* @return response from ChatGPT as a String.
*/
public Optional<String> ask(String question) {
if (isDisabled) {
return Optional.empty();
}

try {
ChatMessage chatMessage =
new ChatMessage(ChatMessageRole.USER.value(), Objects.requireNonNull(question));
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo")
.messages(List.of(chatMessage))
.frequencyPenalty(0.5)
.temperature(0.7)
.maxTokens(MAX_TOKENS)
.n(1)
.build();
return Optional.ofNullable(openAiService.createChatCompletion(chatCompletionRequest)
.getChoices()
.get(0)
.getMessage()
.getContent());
} catch (OpenAiHttpException openAiHttpException) {
logger.warn(
"There was an error using the OpenAI API: {} Code: {} Type: {} Status Code: {}",
openAiHttpException.getMessage(), openAiHttpException.code,
openAiHttpException.type, openAiHttpException.statusCode);
} catch (RuntimeException runtimeException) {
logger.warn("There was an error using the OpenAI API: {}",
runtimeException.getMessage());
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* This package contains the functionality to connect with ChatGPT via API calls.
*/
@MethodsReturnNonnullByDefault
@ParametersAreNonnullByDefault
package org.togetherjava.tjbot.features.chaptgpt;

import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault;

import javax.annotation.ParametersAreNonnullByDefault;
Loading