From 1c173bd2823557e429f43e112616f072be573e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Connor=20Schweigh=C3=B6fer?= Date: Sat, 9 Mar 2024 02:29:46 -0500 Subject: [PATCH 1/6] Set encoding to UTF-8 (#1045) --- build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build.gradle b/build.gradle index 12857fc695..adeece2c8c 100644 --- a/build.gradle +++ b/build.gradle @@ -86,4 +86,13 @@ subprojects { test { useJUnitPlatform() } + + compileJava { + options.encoding = "UTF-8" + } + + compileTestJava { + options.encoding = "UTF-8" + } + } From c2645100e0c06381ebda9bb2ba2dd94a0ff2e0fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 08:36:11 +0100 Subject: [PATCH 2/6] Bump org.flywaydb:flyway-core from 10.8.1 to 10.9.1 (#1047) Bumps [org.flywaydb:flyway-core](https://github.com/flyway/flyway) from 10.8.1 to 10.9.1. - [Release notes](https://github.com/flyway/flyway/releases) - [Commits](https://github.com/flyway/flyway/compare/flyway-10.8.1...flyway-10.9.1) --- updated-dependencies: - dependency-name: org.flywaydb:flyway-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- database/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/build.gradle b/database/build.gradle index 598018b630..fc6da09e27 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -7,7 +7,7 @@ var sqliteVersion = "3.45.0.0" dependencies { implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation "org.xerial:sqlite-jdbc:${sqliteVersion}" - implementation 'org.flywaydb:flyway-core:10.8.1' + implementation 'org.flywaydb:flyway-core:10.9.1' implementation "org.jooq:jooq:$jooqVersion" implementation project(':utils') From 206f71cb37e71429522010a0be966b541a889871 Mon Sep 17 00:00:00 2001 From: vishv843 <55508798+vishv843@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:38:31 -0500 Subject: [PATCH 3/6] modified discriminator (#1050) refactor username to be displayed for gists --- .../tjbot/features/filesharing/FileSharingMessageListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/filesharing/FileSharingMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/features/filesharing/FileSharingMessageListener.java index 298452a40a..9a3524fbea 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/filesharing/FileSharingMessageListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/filesharing/FileSharingMessageListener.java @@ -134,7 +134,7 @@ private void processAttachments(MessageReceivedEvent event, .build() .createGist() .public_(false) - .description("Uploaded by " + event.getAuthor().getAsTag()); + .description("Uploaded by " + event.getAuthor().getName()); List> tasks = new ArrayList<>(); From a6ac1815d0250626ff0e0b453027515bf775abe6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:36:20 +0530 Subject: [PATCH 4/6] Bump org.mockito:mockito-core from 5.3.1 to 5.10.0 (#1015) * Bump org.mockito:mockito-core from 5.3.1 to 5.10.0 Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.3.1 to 5.10.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.3.1...v5.10.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Fix tests failing with new version --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Connor Schweighoefer --- application/build.gradle | 2 +- .../test/java/org/togetherjava/tjbot/jda/JdaTester.java | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index 7fd78cf1d2..a92f80e921 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -77,7 +77,7 @@ dependencies { implementation 'org.kohsuke:github-api:1.319' - testImplementation 'org.mockito:mockito-core:5.3.1' + testImplementation 'org.mockito:mockito-core:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java b/application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java index 30fc0c60f8..145e02f37b 100644 --- a/application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java +++ b/application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java @@ -45,6 +45,7 @@ import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.MockingDetails; +import org.mockito.internal.util.MockUtil; import org.mockito.stubbing.Answer; import org.togetherjava.tjbot.features.SlashCommand; @@ -247,9 +248,11 @@ public JdaTester() { public SlashCommandInteractionEventBuilder createSlashCommandInteractionEvent( SlashCommand command) { UnaryOperator mockOperator = event -> { - SlashCommandInteractionEvent SlashCommandInteractionEvent = spy(event); - mockInteraction(SlashCommandInteractionEvent); - return SlashCommandInteractionEvent; + if (!MockUtil.isMock(event)) { + event = spy(event); + } + mockInteraction(event); + return event; }; return new SlashCommandInteractionEventBuilder(jda, mockOperator).setCommand(command) From 6dd670a117b6d06f67487ddc4459e75187f60bf3 Mon Sep 17 00:00:00 2001 From: alphaBEE <61616007+ankitsmt211@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:24:53 +0530 Subject: [PATCH 5/6] AI response should be in embed (#1046) * AI response should be in embed * helper method to generate embed * remove AI parser class that partitioned responses & related tests * refactor dismiss button on generated responses * update docs * remove AIResponseParserTest --- .../togetherjava/tjbot/features/Features.java | 2 +- .../features/chatgpt/AIResponseParser.java | 82 ------------------- .../features/chatgpt/ChatGptCommand.java | 27 ++++-- .../features/chatgpt/ChatGptService.java | 6 +- .../tjbot/features/help/HelpSystemHelper.java | 54 +++++++----- .../chatgpt/AIResponseParserTest.java | 46 ----------- 6 files changed, 57 insertions(+), 160 deletions(-) delete mode 100644 application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java delete mode 100644 application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 7dbc19d447..1052381988 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -162,7 +162,7 @@ public static Collection 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)); + features.add(new ChatGptCommand(chatGptService, helpSystemHelper)); features.add(new JShellCommand(jshellEval)); FeatureBlacklist> blacklist = blacklistConfig.normal(); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java deleted file mode 100644 index 9dce43ff1c..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParser.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.togetherjava.tjbot.features.chatgpt; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Represents a class to partition long text blocks into smaller blocks which work with Discord's - * API. Initially constructed to partition text from AI text generation APIs. - */ -public class AIResponseParser { - private AIResponseParser() { - throw new UnsupportedOperationException("Utility class, construction not supported"); - } - - private static final Logger logger = LoggerFactory.getLogger(AIResponseParser.class); - private static final int RESPONSE_LENGTH_LIMIT = 2_000; - - /** - * Parses the response generated by AI. If response is longer than - * {@value RESPONSE_LENGTH_LIMIT}, then breaks apart the response into suitable lengths for - * Discords API. - * - * @param response The response from the AI which we want to send over Discord. - * @return An array potentially holding the original response split up into shorter than - * {@value RESPONSE_LENGTH_LIMIT} length pieces. - */ - public static String[] parse(String response) { - String[] partedResponse = new String[] {response}; - if (response.length() > RESPONSE_LENGTH_LIMIT) { - logger.debug("Response to parse:\n{}", response); - partedResponse = partitionAiResponse(response); - } - - return partedResponse; - } - - private static String[] partitionAiResponse(String response) { - List responseChunks = new ArrayList<>(); - String[] splitResponseOnMarks = response.split("```"); - - for (int i = 0; i < splitResponseOnMarks.length; i++) { - String split = splitResponseOnMarks[i]; - List chunks = new ArrayList<>(); - chunks.add(split); - - // Check each chunk for correct length. If over the length, split in two and check - // again. - while (!chunks.stream().allMatch(s -> s.length() < RESPONSE_LENGTH_LIMIT)) { - for (int j = 0; j < chunks.size(); j++) { - String chunk = chunks.get(j); - if (chunk.length() > RESPONSE_LENGTH_LIMIT) { - int midpointNewline = chunk.lastIndexOf("\n", chunk.length() / 2); - chunks.set(j, chunk.substring(0, midpointNewline)); - chunks.add(j + 1, chunk.substring(midpointNewline)); - } - } - } - - // Given the splitting on ```, the odd numbered entries need to have code marks - // restored. - if (i % 2 != 0) { - // We assume that everything after the ``` on the same line is the language - // declaration. Could be empty. - String lang = split.substring(0, split.indexOf(System.lineSeparator())); - chunks = chunks.stream() - .map(s -> ("```" + lang).concat(s).concat("```")) - // Handle case of doubling language declaration - .map(s -> s.replaceFirst("```" + lang + lang, "```" + lang)) - .collect(Collectors.toList()); - } - - List list = chunks.stream().filter(string -> !string.equals("")).toList(); - responseChunks.addAll(list); - } // end of for loop. - - return responseChunks.toArray(new String[0]); - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java index 30477ba74e..38bb5fc400 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java @@ -2,6 +2,8 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.SelfUser; 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; @@ -10,6 +12,7 @@ import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; +import org.togetherjava.tjbot.features.help.HelpSystemHelper; import java.time.Duration; import java.time.Instant; @@ -28,6 +31,7 @@ public final class ChatGptCommand extends SlashCommandAdapter { 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 HelpSystemHelper helper; private final Cache userIdToAskedAtCache = Caffeine.newBuilder().maximumSize(1_000).expireAfterWrite(COMMAND_COOLDOWN).build(); @@ -36,11 +40,13 @@ public final class ChatGptCommand extends SlashCommandAdapter { * Creates an instance of the chatgpt command. * * @param chatGptService ChatGptService - Needed to make calls to ChatGPT API + * @param helper HelpSystemHelper - Needed to generate response embed for prompt */ - public ChatGptCommand(ChatGptService chatGptService) { + public ChatGptCommand(ChatGptService chatGptService, HelpSystemHelper helper) { super(COMMAND_NAME, "Ask the ChatGPT AI a question!", CommandVisibility.GUILD); this.chatGptService = chatGptService; + this.helper = helper; } @Override @@ -75,20 +81,23 @@ public void onModalSubmitted(ModalInteractionEvent event, List args) { event.deferReply().queue(); String context = ""; - Optional optional = - chatGptService.ask(event.getValue(QUESTION_INPUT).getAsString(), context); + String question = event.getValue(QUESTION_INPUT).getAsString(); + + Optional optional = chatGptService.ask(question, context); if (optional.isPresent()) { userIdToAskedAtCache.put(event.getMember().getId(), Instant.now()); } - String[] errorResponse = {""" + String errorResponse = """ An error has occurred while trying to communicate with ChatGPT. Please try again later. - """}; + """; - String[] response = optional.orElse(errorResponse); - for (String message : response) { - event.getHook().sendMessage(message).queue(); - } + String response = optional.orElse(errorResponse); + SelfUser selfUser = event.getJDA().getSelfUser(); + + MessageEmbed responseEmbed = helper.generateGptResponseEmbed(response, selfUser, question); + + event.getHook().sendMessageEmbeds(responseEmbed).queue(); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java index a145b42139..e8b02d04bb 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptService.java @@ -87,11 +87,11 @@ public ChatGptService(Config config) { * @param question The question being asked of ChatGPT. Max is {@value MAX_TOKENS} tokens. * @param context The category of asked question, to set the context(eg. Java, Database, Other * etc). - * @return partitioned response from ChatGPT as a String array. + * @return response from ChatGPT as a String. * @see ChatGPT * Tokens. */ - public Optional ask(String question, String context) { + public Optional ask(String question, String context) { if (isDisabled) { return Optional.empty(); } @@ -121,7 +121,7 @@ public Optional ask(String question, String context) { return Optional.empty(); } - return Optional.of(AIResponseParser.parse(response)); + return Optional.of(response); } catch (OpenAiHttpException openAiHttpException) { logger.warn( "There was an error using the OpenAI API: {} Code: {} Type: {} Status Code: {}", diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java index 917595d80f..6a93aeadc1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java @@ -1,5 +1,6 @@ package org.togetherjava.tjbot.features.help; +import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer; import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel; @@ -9,7 +10,6 @@ import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.requests.RestAction; -import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +33,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -117,7 +116,7 @@ public HelpSystemHelper(Config config, Database database, ChatGptService chatGpt RestAction constructChatGptAttempt(ThreadChannel threadChannel, String originalQuestion, ComponentIdInteractor componentIdInteractor) { Optional questionOptional = prepareChatGptQuestion(threadChannel, originalQuestion); - Optional chatGPTAnswer; + Optional chatGPTAnswer; if (questionOptional.isEmpty()) { return useChatGptFallbackMessage(threadChannel); @@ -130,11 +129,12 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, String context = matchingTag.getName(); chatGPTAnswer = chatGptService.ask(question, context); + if (chatGPTAnswer.isEmpty()) { return useChatGptFallbackMessage(threadChannel); } - List ids = new CopyOnWriteArrayList<>(); + StringBuilder idForDismissButton = new StringBuilder(); RestAction message = mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) .map(""" @@ -143,27 +143,43 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, %s. """::formatted) .flatMap(threadChannel::sendMessage) - .onSuccess(m -> ids.add(m.getId())); - String[] answers = chatGPTAnswer.orElseThrow(); - - for (int i = 0; i < answers.length; i++) { - MessageCreateAction answer = threadChannel.sendMessage(answers[i]); + .onSuccess(m -> idForDismissButton.append(m.getId())); - if (i == answers.length - 1) { - message = message.flatMap(any -> answer - .addActionRow(generateDismissButton(componentIdInteractor, ids))); - continue; - } + String answer = chatGPTAnswer.orElseThrow(); + SelfUser selfUser = threadChannel.getJDA().getSelfUser(); - message = message.flatMap(ignored -> answer.onSuccess(m -> ids.add(m.getId()))); + int responseCharLimit = MessageEmbed.DESCRIPTION_MAX_LENGTH; + if (answer.length() > responseCharLimit) { + answer = answer.substring(0, responseCharLimit); } - return message; + MessageEmbed responseEmbed = generateGptResponseEmbed(answer, selfUser, originalQuestion); + return message.flatMap(any -> threadChannel.sendMessageEmbeds(responseEmbed) + .addActionRow( + generateDismissButton(componentIdInteractor, idForDismissButton.toString()))); + } + + public MessageEmbed generateGptResponseEmbed(String answer, SelfUser selfUser, String title) { + String responseByGptFooter = "- AI generated response"; + + int embedTitleLimit = MessageEmbed.TITLE_MAX_LENGTH; + String capitalizedTitle = Character.toUpperCase(title.charAt(0)) + title.substring(1); + + String titleForEmbed = capitalizedTitle.length() > embedTitleLimit + ? capitalizedTitle.substring(0, embedTitleLimit) + : capitalizedTitle; + + return new EmbedBuilder() + .setAuthor(selfUser.getName(), null, selfUser.getEffectiveAvatarUrl()) + .setTitle(titleForEmbed) + .setDescription(answer) + .setColor(Color.pink) + .setFooter(responseByGptFooter) + .build(); } - private Button generateDismissButton(ComponentIdInteractor componentIdInteractor, - List ids) { - String buttonId = componentIdInteractor.generateComponentId(ids.toArray(String[]::new)); + private Button generateDismissButton(ComponentIdInteractor componentIdInteractor, String id) { + String buttonId = componentIdInteractor.generateComponentId(id); return Button.danger(buttonId, "Dismiss"); } diff --git a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java b/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java deleted file mode 100644 index 715dc14f0c..0000000000 --- a/application/src/test/java/org/togetherjava/tjbot/features/chatgpt/AIResponseParserTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.togetherjava.tjbot.features.chatgpt; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Objects; - -class AIResponseParserTest { - private static final Logger logger = LoggerFactory.getLogger(AIResponseParserTest.class); - - @ParameterizedTest - @ValueSource(ints = {1, 2, 3, 4}) - void correctResponseLength(int fileNumber) { - try (InputStream in = getClass().getClassLoader() - .getResourceAsStream("AITestResponses/test" + fileNumber + ".txt")) { - String response = new String(Objects.requireNonNull(in).readAllBytes()); - String[] aiResponse = AIResponseParser.parse(response); - - testResponseLength(aiResponse); - toLog(aiResponse); - } catch (IOException | NullPointerException ex) { - logger.error("{}", ex.getMessage()); - Assertions.fail(); - } - } - - private void testResponseLength(String[] responses) { - int AI_RESPONSE_CHARACTER_LIMIT = 2000; - for (String response : responses) { - Assertions.assertTrue(response.length() <= AI_RESPONSE_CHARACTER_LIMIT, - "Response length is NOT within character limit: " + response.length()); - logger.warn("Response length was: {}", response.length()); - } - } - - private void toLog(String[] responses) { - for (String response : responses) { - logger.info(response); - } - } -} From 5ac1e07e010fb21a5709891be7b8a4ad8ecf74c4 Mon Sep 17 00:00:00 2001 From: Devansh Tiwari <65783463+devloves@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:51:51 +0530 Subject: [PATCH 6/6] Member Count Display (#1038) * Added MemberCounter Routine * feat: use pattern compile * removed debug lines * pattern fixes in config * refactor: change cat to category * feat: switch to findAny from forEach * perf: remove pattern compilation for each call Co-authored-by: Devansh Tiwari <65783463+devloves@users.noreply.github.com> * perf: fixed solar linting error * perf: made memberCountCategoryPattern required * Update application/config.json.template Co-authored-by: Chris Sdogkos * fixed: routine not updating channel name after first write * fixed comment --------- Co-authored-by: christolis Co-authored-by: Tanish Azad --- application/config.json.template | 1 + .../org/togetherjava/tjbot/config/Config.java | 13 +++++ .../togetherjava/tjbot/features/Features.java | 2 + .../basic/MemberCountDisplayRoutine.java | 53 +++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/basic/MemberCountDisplayRoutine.java diff --git a/application/config.json.template b/application/config.json.template index 23e8f72648..7958745d39 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -110,5 +110,6 @@ "special": [ ] }, + "memberCountCategoryPattern": "Info", "selectRolesChannelPattern": "select-your-roles" } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 57c3f98954..7d809b6c9a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -44,6 +44,7 @@ public final class Config { private final HelperPruneConfig helperPruneConfig; private final FeatureBlacklistConfig featureBlacklistConfig; private final String selectRolesChannelPattern; + private final String memberCountCategoryPattern; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -86,6 +87,8 @@ private Config(@JsonProperty(value = "token", required = true) String token, @JsonProperty(value = "openaiApiKey", required = true) String openaiApiKey, @JsonProperty(value = "sourceCodeBaseUrl", required = true) String sourceCodeBaseUrl, @JsonProperty(value = "jshell", required = true) JShellConfig jshell, + @JsonProperty(value = "memberCountCategoryPattern", + required = true) String memberCountCategoryPattern, @JsonProperty(value = "helperPruneConfig", required = true) HelperPruneConfig helperPruneConfig, @JsonProperty(value = "featureBlacklist", @@ -96,6 +99,7 @@ private Config(@JsonProperty(value = "token", required = true) String token, this.githubApiKey = Objects.requireNonNull(githubApiKey); this.databasePath = Objects.requireNonNull(databasePath); this.projectWebsite = Objects.requireNonNull(projectWebsite); + this.memberCountCategoryPattern = Objects.requireNonNull(memberCountCategoryPattern); this.discordGuildInvite = Objects.requireNonNull(discordGuildInvite); this.modAuditLogChannelPattern = Objects.requireNonNull(modAuditLogChannelPattern); this.modMailChannelPattern = Objects.requireNonNull(modMailChannelPattern); @@ -405,4 +409,13 @@ public FeatureBlacklistConfig getFeatureBlacklistConfig() { public String getSelectRolesChannelPattern() { return selectRolesChannelPattern; } + + /** + * Gets the pattern matching the category that is used to display the total member count. + * + * @return the categories name types + */ + public String getMemberCountCategoryPattern() { + return memberCountCategoryPattern; + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/Features.java b/application/src/main/java/org/togetherjava/tjbot/features/Features.java index 1052381988..38da09aca0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -8,6 +8,7 @@ import org.togetherjava.tjbot.config.FeatureBlacklist; import org.togetherjava.tjbot.config.FeatureBlacklistConfig; import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine; import org.togetherjava.tjbot.features.basic.PingCommand; import org.togetherjava.tjbot.features.basic.RoleSelectCommand; import org.togetherjava.tjbot.features.basic.SlashCommandEducator; @@ -109,6 +110,7 @@ public static Collection createFeatures(JDA jda, Database database, Con .add(new AutoPruneHelperRoutine(config, helpSystemHelper, modAuditLogWriter, database)); features.add(new HelpThreadAutoArchiver(helpSystemHelper)); features.add(new LeftoverBookmarksCleanupRoutine(bookmarksSystem)); + features.add(new MemberCountDisplayRoutine(config)); // Message receivers features.add(new TopHelpersMessageListener(database, config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/MemberCountDisplayRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/MemberCountDisplayRoutine.java new file mode 100644 index 0000000000..bb67396670 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/MemberCountDisplayRoutine.java @@ -0,0 +1,53 @@ +package org.togetherjava.tjbot.features.basic; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.channel.concrete.Category; + +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.features.Routine; + +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Shows the guild member count on selected category, which updates everyday. + */ +public class MemberCountDisplayRoutine implements Routine { + private final Predicate memberCountCategoryPredicate; + + /** + * Creates an instance on member count display routine. + * + * @param config the config to use + */ + public MemberCountDisplayRoutine(Config config) { + memberCountCategoryPredicate = + Pattern.compile(config.getMemberCountCategoryPattern() + "( - \\d+ Members)?") + .asMatchPredicate(); + } + + private void updateCategoryName(Category category) { + int totalMemberCount = category.getGuild().getMemberCount(); + String baseName = category.getName().split("-")[0].trim(); + + category.getManager() + .setName("%s - %d Members".formatted(baseName, totalMemberCount)) + .queue(); + } + + @Override + public Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_RATE, 0, 24, TimeUnit.HOURS); + } + + @Override + public void runRoutine(JDA jda) { + jda.getGuilds() + .forEach(guild -> guild.getCategories() + .stream() + .filter(category -> memberCountCategoryPredicate.test(category.getName())) + .findAny() + .ifPresent(this::updateCategoryName)); + } +}