From 8cdc6462223dca8a88133d9c10a5166f67610d50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:00:25 +0100 Subject: [PATCH 01/23] build(deps): bump com.openai:openai-java from 4.18.0 to 4.19.0 (#1412) Bumps [com.openai:openai-java](https://github.com/openai/openai-java) from 4.18.0 to 4.19.0. - [Release notes](https://github.com/openai/openai-java/releases) - [Changelog](https://github.com/openai/openai-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/openai-java/compare/v4.18.0...v4.19.0) --- updated-dependencies: - dependency-name: com.openai:openai-java dependency-version: 4.19.0 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> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 19cdc35324..5928d023a5 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ version '1.0-SNAPSHOT' ext { jooqVersion = '3.20.5' jacksonVersion = '2.19.1' - chatGPTVersion = '4.18.0' + chatGPTVersion = '4.19.0' junitVersion = '6.0.0' } From 7f63240ef5661e49a00e63336f0c17ca5ddc2c0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 08:26:38 +0100 Subject: [PATCH 02/23] build(deps): bump com.openai:openai-java from 4.19.0 to 4.20.0 (#1413) Bumps [com.openai:openai-java](https://github.com/openai/openai-java) from 4.19.0 to 4.20.0. - [Release notes](https://github.com/openai/openai-java/releases) - [Changelog](https://github.com/openai/openai-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/openai-java/compare/v4.19.0...v4.20.0) --- updated-dependencies: - dependency-name: com.openai:openai-java dependency-version: 4.20.0 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> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5928d023a5..e84ca7ee21 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ version '1.0-SNAPSHOT' ext { jooqVersion = '3.20.5' jacksonVersion = '2.19.1' - chatGPTVersion = '4.19.0' + chatGPTVersion = '4.20.0' junitVersion = '6.0.0' } From e6a2657a9af4b6d4a9fc256e220550012d15b6db Mon Sep 17 00:00:00 2001 From: Bryce <86679354+Bryce72@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:33:53 -0500 Subject: [PATCH 03/23] fix: Handle UNKNOWN_MESSAGE exception in SuggestionsUpDownVoter (#1415) * SuggestionsUpDownVoter should have UNKNOWN_MESSAGE exception gracefully with an INFO log instead #1414 * SuggestionsUpDownVoter should have UNKNOWN_MESSAGE exception gracefully with an INFO log instead #1414 Refactored and idomaticized * SuggestionsUpDownVoter should have UNKNOWN_MESSAGE exception gracefully with an INFO log instead #1414 Refactored and idomaticized -- oops updated to make sure its similar via both react and create thread * updated info log strings --- .../basic/SuggestionsUpDownVoter.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java index 5dbbdbf8b2..e07682725f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java @@ -50,13 +50,22 @@ public void onMessageReceived(MessageReceivedEvent event) { Message message = event.getMessage(); createThread(message); + reactWith(config.getUpVoteEmoteName(), FALLBACK_UP_VOTE, guild, message); reactWith(config.getDownVoteEmoteName(), FALLBACK_DOWN_VOTE, guild, message); } private static void createThread(Message message) { String threadTitle = generateThreadTitle(message); - message.createThreadChannel(threadTitle).queue(); + message.createThreadChannel(threadTitle).queue(_ -> { + }, exception -> { + if (exception instanceof ErrorResponseException responseException + && responseException.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) { + logger.info("Failed to start suggestion thread: source message deleted"); + return; + + } + }); } /** @@ -91,19 +100,25 @@ private static void reactWith(String emojiName, Emoji fallbackEmoji, Guild guild "Unable to vote on a suggestion with the configured emoji ('{}'), using fallback instead.", emojiName); return message.addReaction(fallbackEmoji); - }).queue(ignored -> { - }, exception -> { - if (exception instanceof ErrorResponseException responseException - && responseException.getErrorResponse() == ErrorResponse.REACTION_BLOCKED) { + }).queue(_ -> { + }, SuggestionsUpDownVoter::handleReactionFailure); + } + + private static void handleReactionFailure(Throwable exception) { + if (exception instanceof ErrorResponseException responseException) { + if (responseException.getErrorResponse() == ErrorResponse.REACTION_BLOCKED) { // User blocked the bot, hence the bot can not add reactions to their messages. - // Nothing we can do here. return; } - - logger.error("Attempted to react to a suggestion, but failed", exception); - }); + if (responseException.getErrorResponse() == ErrorResponse.UNKNOWN_MESSAGE) { + logger.info("Failed to react to suggestion: source message deleted"); + return; + } + } + logger.error("Attempted to react to a suggestion, but failed", exception); } + private static Optional getEmojiByName(String name, Guild guild) { return guild.getEmojisByName(name, false).stream().findAny(); } From bf239309f34a67a39077bdce8970d2a53040a1dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:37:39 +0100 Subject: [PATCH 04/23] build(deps): bump org.jetbrains:annotations from 26.0.1 to 26.1.0 (#1417) Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 26.0.1 to 26.1.0. - [Release notes](https://github.com/JetBrains/java-annotations/releases) - [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md) - [Commits](https://github.com/JetBrains/java-annotations/compare/26.0.1...26.1.0) --- updated-dependencies: - dependency-name: org.jetbrains:annotations dependency-version: 26.1.0 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> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index 280d482687..c49e518948 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -40,7 +40,7 @@ shadowJar { dependencies { implementation 'com.google.code.findbugs:jsr305:3.0.2' - implementation 'org.jetbrains:annotations:26.0.1' + implementation 'org.jetbrains:annotations:26.1.0' implementation project(':database') implementation project(':utils') From 3292631c731dd0d3b33f2c8386137cc276085920 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:47:12 +0100 Subject: [PATCH 05/23] build(deps): bump com.openai:openai-java from 4.20.0 to 4.22.0 (#1418) Bumps [com.openai:openai-java](https://github.com/openai/openai-java) from 4.20.0 to 4.22.0. - [Release notes](https://github.com/openai/openai-java/releases) - [Changelog](https://github.com/openai/openai-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/openai-java/compare/v4.20.0...v4.22.0) --- updated-dependencies: - dependency-name: com.openai:openai-java dependency-version: 4.22.0 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> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e84ca7ee21..2f39fafb0a 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ version '1.0-SNAPSHOT' ext { jooqVersion = '3.20.5' jacksonVersion = '2.19.1' - chatGPTVersion = '4.20.0' + chatGPTVersion = '4.22.0' junitVersion = '6.0.0' } From 1b44779297ee9eaf170f824741060389e056dac3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:50:26 +0100 Subject: [PATCH 06/23] build(deps): bump com.openai:openai-java from 4.22.0 to 4.23.0 (#1421) Bumps [com.openai:openai-java](https://github.com/openai/openai-java) from 4.22.0 to 4.23.0. - [Release notes](https://github.com/openai/openai-java/releases) - [Changelog](https://github.com/openai/openai-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/openai-java/compare/v4.22.0...v4.23.0) --- updated-dependencies: - dependency-name: com.openai:openai-java dependency-version: 4.23.0 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> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2f39fafb0a..8e8dee54ca 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ version '1.0-SNAPSHOT' ext { jooqVersion = '3.20.5' jacksonVersion = '2.19.1' - chatGPTVersion = '4.22.0' + chatGPTVersion = '4.23.0' junitVersion = '6.0.0' } From 1070f233ef07b901010841de6376a6d0e72d1949 Mon Sep 17 00:00:00 2001 From: VlaM5 Date: Thu, 26 Feb 2026 15:17:57 +0200 Subject: [PATCH 07/23] fix: allow forwarded messages with media in media-only channels (#1419) * fix: allow forwarded messages with media in media-only channels * fix: address review comments * fix: remove unused import * fix: apply code formatting --- .../mediaonly/MediaOnlyChannelListener.java | 35 ++++++++++++++- .../MediaOnlyChannelListenerTest.java | 45 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListener.java b/application/src/main/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListener.java index 11f666beaa..083c3add07 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListener.java @@ -13,6 +13,7 @@ import org.togetherjava.tjbot.features.MessageReceiverAdapter; import java.awt.Color; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -51,9 +52,39 @@ public void onMessageReceived(MessageReceivedEvent event) { } } + /** + * Checks whether the given message has no media attached. + *

+ * A message is considered to have media if it contains attachments, embeds, or a URL in its + * text content. For forwarded messages, the snapshots are also checked for media. + * + * @param message the message to check + * @return {@code true} if the message has no media, {@code false} otherwise + */ private boolean messageHasNoMediaAttached(Message message) { - return message.getAttachments().isEmpty() && message.getEmbeds().isEmpty() - && !message.getContentRaw().contains("http"); + if (hasMedia(message.getAttachments(), message.getEmbeds(), message.getContentRaw())) { + return false; + } + + return message.getMessageSnapshots() + .stream() + .noneMatch(snapshot -> hasMedia(snapshot.getAttachments(), snapshot.getEmbeds(), + snapshot.getContentRaw())); + } + + /** + * Checks whether the given content contains any media. + *

+ * Media is considered present if there are attachments, embeds, or a URL (identified by + * {@code "http"}) in the text content. + * + * @param attachments the attachments of the message or snapshot + * @param embeds the embeds of the message or snapshot + * @param content the raw text content of the message or snapshot + */ + private boolean hasMedia(List attachments, List embeds, + String content) { + return !attachments.isEmpty() || !embeds.isEmpty() || content.contains("http"); } private MessageCreateData createNotificationMessage(Message message) { diff --git a/application/src/test/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListenerTest.java b/application/src/test/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListenerTest.java index c42bba88bf..89d36fcdf4 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListenerTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListenerTest.java @@ -4,6 +4,7 @@ import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.messages.MessageSnapshot; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; import net.dv8tion.jda.api.utils.messages.MessageCreateData; @@ -112,4 +113,48 @@ private MessageReceivedEvent sendMessage(MessageCreateData message, mediaOnlyChannelListener.onMessageReceived(event); return event; } + + + @Test + void keepsForwardedMessageWithAttachment() { + // GIVEN a forwarded message that contains an attachment inside the snapshot + MessageCreateData message = new MessageCreateBuilder().setContent("any").build(); + + MessageSnapshot snapshot = mock(MessageSnapshot.class); + when(snapshot.getAttachments()).thenReturn(List.of(mock(Message.Attachment.class))); + when(snapshot.getEmbeds()).thenReturn(List.of()); + when(snapshot.getContentRaw()).thenReturn(""); + + // WHEN sending the forwarded message + MessageReceivedEvent event = sendMessageWithSnapshots(message, List.of(snapshot)); + + // THEN it does not get deleted + verify(event.getMessage(), never()).delete(); + } + + @Test + void deletesForwardedMessageWithoutMedia() { + // GIVEN a forwarded message that contains no media inside the snapshot + MessageCreateData message = new MessageCreateBuilder().setContent("any").build(); + + MessageSnapshot snapshot = mock(MessageSnapshot.class); + when(snapshot.getAttachments()).thenReturn(List.of()); + when(snapshot.getEmbeds()).thenReturn(List.of()); + when(snapshot.getContentRaw()).thenReturn("just some text, no media"); + + // WHEN sending the forwarded message + MessageReceivedEvent event = sendMessageWithSnapshots(message, List.of(snapshot)); + + // THEN it gets deleted + verify(event.getMessage()).delete(); + } + + private MessageReceivedEvent sendMessageWithSnapshots(MessageCreateData message, + List snapshots) { + MessageReceivedEvent event = + jdaTester.createMessageReceiveEvent(message, List.of(), ChannelType.TEXT); + when(event.getMessage().getMessageSnapshots()).thenReturn(snapshots); + mediaOnlyChannelListener.onMessageReceived(event); + return event; + } } From e7c8c3f07cbee66fd68b1d88ad320f0169af730c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:41:24 +0100 Subject: [PATCH 08/23] build(deps): bump org.mockito:mockito-core from 5.21.0 to 5.22.0 (#1424) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.21.0 to 5.22.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.21.0...v5.22.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.22.0 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> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index c49e518948..b0af6d1bf0 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation 'org.apache.commons:commons-text:1.15.0' implementation 'com.apptasticsoftware:rssreader:3.12.0' - testImplementation 'org.mockito:mockito-core:5.21.0' + testImplementation 'org.mockito:mockito-core:5.22.0' testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly 'org.junit.platform:junit-platform-launcher' From 4367c2d50ad68a433f2bdc6a389ccef08cba56dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:00:04 +0100 Subject: [PATCH 09/23] build(deps): bump com.diffplug.spotless from 8.2.0 to 8.3.0 (#1426) Bumps com.diffplug.spotless from 8.2.0 to 8.3.0. --- updated-dependencies: - dependency-name: com.diffplug.spotless dependency-version: 8.3.0 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> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8e8dee54ca..2cc986a351 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id "com.diffplug.spotless" version "8.2.0" + id "com.diffplug.spotless" version "8.3.0" id "org.sonarqube" version "7.2.0.6526" id "name.remal.sonarlint" version "7.0.0" } From 6ebffd9a9579028a1ee47d753c31a97f93a6dfd5 Mon Sep 17 00:00:00 2001 From: "barsh(404)" Date: Tue, 3 Mar 2026 10:01:01 +0300 Subject: [PATCH 10/23] Add utilities to detect and replace broken links. (#1366) * Add utilities to detect and replace broken links. * style: run spotlessApply * Add utilities to detect and replace broken links V2 * Add utilities to detect and replace broken links V2 * Add utilities to detect and replace broken links V2 * Add utilities to detect and replace broken links V2 * Fixed link detection to handle 3xx redirects properly Updated isLinkBroken() to only treat 4xx/5xx status codes as broken. Previously 3xx redirects were incorrectly marked as broken links also improved javadoc clarity throughout LinkDetection class * Apply Spotless formatting and regenerate jOOQ sources * commit:Broken links resolve requested changes - Rename replaceDeadLinks to replaceBrokenLinks for consistency - Use Optional instead of null values in stream processing - Add convenience overload for extractLinks with default filters - Update javadocs to be more generic and future-proof - Move implementation details from javadoc to inline comments - Replace 'ignored' lambda params with '_' Resolves the review comments from @Zabuzard * refactor: apply review feedback from @Zabuzard * New fixed and changes * refactor: simplify link filtering with helper method Use streams thraughout and extract replacement logic into a separate method for better readbility * style(javadoc): remove gap between JavaDoc and method signature Remove a reoccuring line gap introduced between each method's JavaDoc and its corresponding method signatures to adhere to the overall style of the project. Signed-off-by: Chris Sdogkos --------- Signed-off-by: Chris Sdogkos Co-authored-by: Chris Sdogkos --- .../tjbot/features/utils/LinkDetection.java | 219 ++++++++++++++++-- 1 file changed, 204 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/utils/LinkDetection.java b/application/src/main/java/org/togetherjava/tjbot/features/utils/LinkDetection.java index 3b6dc18112..8b66c0b7cd 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/utils/LinkDetection.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/utils/LinkDetection.java @@ -4,27 +4,61 @@ import com.linkedin.urls.detection.UrlDetector; import com.linkedin.urls.detection.UrlDetectorOptions; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; /** - * Utility class to detect links. + * Utility methods for working with links inside arbitrary text. + * + *

+ * This class can: + *

    + *
  • Extract HTTP(S) links from text
  • + *
  • Check whether a link is reachable via HTTP
  • + *
  • Replace broken links asynchronously
  • + *
+ * + *

+ * It is intentionally stateless and uses asynchronous HTTP requests to avoid blocking calling + * threads. */ public class LinkDetection { + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); /** - * Possible ways to filter a link. + * Default filters applied when extracting links from text. * - * @see LinkDetection + *

+ * Links to intentionally ignore in order to reduce false positives when scanning chat messages + * or source-code snippets. + */ + private static final Set DEFAULT_FILTERS = + Set.of(LinkFilter.SUPPRESSED, LinkFilter.NON_HTTP_SCHEME); + + /** + * Filters that control which detected URLs are returned by {@link #extractLinks}. */ public enum LinkFilter { /** - * Filters links suppressed with {@literal }. + * Ignores URLs that are wrapped in angle brackets, e.g. {@code }. + * + *

+ * Such links are often intentionally suppressed in chat platforms. */ SUPPRESSED, /** - * Filters links that are not using http scheme. + * Ignores URLs that do not use the HTTP or HTTPS scheme. + * + *

+ * This helps avoid false positives such as {@code ftp://}, {@code file://}, or scheme-less + * matches. */ NON_HTTP_SCHEME } @@ -34,11 +68,24 @@ private LinkDetection() { } /** - * Extracts all links from the given content. + * Extracts links from the given text. + * + *

+ * The text is scanned using a URL detector, then filtered and normalized according to the + * provided {@link LinkFilter}s. + * + *

+ * Example: + * + *

{@code
+     * Set filters = Set.of(LinkFilter.SUPPRESSED, LinkFilter.NON_HTTP_SCHEME);
+     * extractLinks("Visit https://example.com and ", filters)
+     * // returns ["https://example.com"]
+     * }
* - * @param content the content to search through - * @param filter the filters applied to the urls - * @return a list of all found links, can be empty + * @param content the text to scan for links + * @param filter a set of filters controlling which detected links are returned + * @return a list of extracted links in the order they appear in the text */ public static List extractLinks(String content, Set filter) { return new UrlDetector(content, UrlDetectorOptions.BRACKET_MATCH).detect() @@ -49,22 +96,166 @@ public static List extractLinks(String content, Set filter) } /** - * Checks whether the given content contains a link. + * Extracts links from the given text using default filters. * - * @param content the content to search through - * @return true if the content contains at least one link + *

+ * This is a convenience method that uses {@link #DEFAULT_FILTERS}. + * + * @param content the text to scan for links + * @return a list of extracted links in the order they appear in the text + * @see #extractLinks(String, Set) + */ + public static List extractLinks(String content) { + return extractLinks(content, DEFAULT_FILTERS); + } + + /** + * Checks whether the given text contains at least one detectable URL. + * + *

+ * This method performs a lightweight detection only and does not apply any {@link LinkFilter}s. + * + * @param content the text to scan + * @return {@code true} if at least one URL-like pattern is detected */ public static boolean containsLink(String content) { return !(new UrlDetector(content, UrlDetectorOptions.BRACKET_MATCH).detect().isEmpty()); } + /** + * Asynchronously checks whether a URL is considered broken. + * + *

+ * A link is considered broken if: + *

    + *
  • The URL is malformed or unreachable
  • + *
  • The HTTP request fails with an exception
  • + *
  • The response status code is 4xx (client error) or 5xx (server error)
  • + *
+ * + *

+ * Successful responses (2xx) and redirects (3xx) are considered valid links. The response body + * is never inspected. + * + * @param url the URL to check + * @return a {@code CompletableFuture} completing with {@code true} if the link is broken, + * {@code false} otherwise + */ + public static CompletableFuture isLinkBroken(String url) { + // Try HEAD request first (cheap and fast) + HttpRequest headRequest = HttpRequest.newBuilder(URI.create(url)) + .method("HEAD", HttpRequest.BodyPublishers.noBody()) + .build(); + + return HTTP_CLIENT.sendAsync(headRequest, HttpResponse.BodyHandlers.discarding()) + .thenApply(response -> { + int status = response.statusCode(); + // 2xx and 3xx are success, 4xx and 5xx are errors + return status >= 400; + }) + .exceptionally(_ -> true) + .thenCompose(result -> { + if (!Boolean.TRUE.equals(result)) { + return CompletableFuture.completedFuture(false); + } + // If HEAD fails, fall back to GET request (some servers don't support HEAD) + HttpRequest fallbackGetRequest = + HttpRequest.newBuilder(URI.create(url)).GET().build(); + return HTTP_CLIENT + .sendAsync(fallbackGetRequest, HttpResponse.BodyHandlers.discarding()) + .thenApply(resp -> resp.statusCode() >= 400) + .exceptionally(_ -> true); + }); + } + + /** + * Replaces all broken links in the given text. + * + *

+ * Each detected link is checked asynchronously using {@link #isLinkBroken(String)}. Only links + * confirmed as broken are replaced. Duplicate URLs are checked only once and all occurrences + * are replaced if found to be broken. + * + *

+ * This method does not block - all link checks are performed asynchronously and combined into a + * single {@code CompletableFuture}. + * + *

+ * Example: + * + *

{@code
+     * replaceBrokenLinks("""
+     *           Test
+     *           http://deadlink/1
+     *           http://workinglink/1
+     *         """, "(broken link)")
+     * }
+ * + *

+ * Results in: + * + *

{@code
+     * Test
+     * (broken link)
+     * http://workinglink/1
+     * }
+ * + * @param text the input text containing URLs + * @param replacement the string used to replace broken links + * @return a {@code CompletableFuture} that completes with the modified text, or the original + * text if no broken links were found + */ + public static CompletableFuture replaceBrokenLinks(String text, String replacement) { + List links = extractLinks(text, DEFAULT_FILTERS); + + if (links.isEmpty()) { + return CompletableFuture.completedFuture(text); + } + + // Can't filter yet - we won't know which links are broken until the futures complete + List> brokenLinkFutures = links.stream() + .distinct() + .map(link -> isLinkBroken(link) + .thenApply(isBroken -> Boolean.TRUE.equals(isBroken) ? link : null)) + .toList(); + + return CompletableFuture.allOf(brokenLinkFutures.toArray(CompletableFuture[]::new)) + .thenApply(_ -> brokenLinkFutures.stream() + .map(CompletableFuture::join) + .filter(Objects::nonNull) + .toList()) + .thenApply(brokenLinks -> replaceLinks(brokenLinks, text, replacement)); + } + + private static String replaceLinks(List linksToReplace, String text, + String replacement) { + String result = text; + for (String link : linksToReplace) { + result = result.replace(link, replacement); + } + return result; + } + + /** + * Converts a detected {@link Url} into a normalized link string. + * + *

+ * Applies the provided {@link LinkFilter}s. Additionally removes trailing punctuation such as + * commas or periods from the detected URL. + * + * @param url the detected URL + * @param filter active link filters to apply + * @return an {@link Optional} containing the normalized link, or {@code Optional.empty()} if + * the link should be filtered out + */ private static Optional toLink(Url url, Set filter) { String raw = url.getOriginalUrl(); if (filter.contains(LinkFilter.SUPPRESSED) && raw.contains(">")) { // URL escapes, such as "" should be skipped return Optional.empty(); } - // Not interested in other schemes, also to filter out matches without scheme. + // Not interested in other schemes, also to filter out matches without scheme (Skip non-HTTP + // schemes) // It detects a lot of such false-positives in Java snippets if (filter.contains(LinkFilter.NON_HTTP_SCHEME) && !raw.startsWith("http")) { return Optional.empty(); @@ -76,8 +267,6 @@ private static Optional toLink(Url url, Set filter) { // Remove trailing punctuation link = link.substring(0, link.length() - 1); } - return Optional.of(link); } - } From 90158700aae93660b8c30d9f3b96b50ba4c797dd Mon Sep 17 00:00:00 2001 From: Chirag Date: Tue, 3 Mar 2026 15:42:40 +0530 Subject: [PATCH 11/23] Added "Mark Active" button to help-thread "inactivity-closed" message (#1423) * feat(help): add manual reactivation button for inactive threads * feat(help): add manual reactivation button for inactive threads * refactor(help): consolidate interactor and cleanup reactivation logic * refactor(help): share ComponentIdInteractor between listener and archiver * refactor(help):minor formatting * refactor: move thread reactivation logic to HelpThreadAutoArchiver - Make HelpThreadAutoArchiver implement UserInteractor (name: thread-inactivity) - Own ComponentIdInteractor for mark-active button ID generation - Move onButtonClick/onInactivityButton from listener to archiver - Remove mark-active routing and getComponentIdInteractor from listener - Remove unused generateMarkActiveId from HelpSystemHelper - Extract mark-active literal to constant (SonarLint S1192) * refactor: minor cleanups as requested * refactor: formatting as requested by christolis. * add Ephermeral message on thread re-Activation --- .../togetherjava/tjbot/features/Features.java | 4 +- .../features/help/HelpThreadAutoArchiver.java | 66 +++++++++++++++++-- .../help/HelpThreadCreatedListener.java | 6 +- 3 files changed, 67 insertions(+), 9 deletions(-) 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 6febd433b6..65a9996842 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -125,6 +125,8 @@ public static Collection createFeatures(JDA jda, Database database, Con HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database, chatGptService); HelpThreadLifecycleListener helpThreadLifecycleListener = new HelpThreadLifecycleListener(helpSystemHelper, database); + HelpThreadCreatedListener helpThreadCreatedListener = + new HelpThreadCreatedListener(helpSystemHelper); TopHelpersService topHelpersService = new TopHelpersService(database); TopHelpersAssignmentRoutine topHelpersAssignmentRoutine = new TopHelpersAssignmentRoutine(config, topHelpersService); @@ -173,7 +175,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new RejoinModerationRoleListener(actionsStore, config)); features.add(new GuildLeaveCloseThreadListener(config)); features.add(new LeftoverBookmarksListener(bookmarksSystem)); - features.add(new HelpThreadCreatedListener(helpSystemHelper)); + features.add(helpThreadCreatedListener); features.add(new HelpThreadLifecycleListener(helpSystemHelper, database)); features.add(new ProjectsThreadCreatedListener(config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadAutoArchiver.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadAutoArchiver.java index 41792957ff..d4537cdab5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadAutoArchiver.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadAutoArchiver.java @@ -8,12 +8,18 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.TimeUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.features.Routine; +import org.togetherjava.tjbot.features.UserInteractionType; +import org.togetherjava.tjbot.features.UserInteractor; +import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator; +import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor; import java.time.Duration; import java.time.Instant; @@ -27,12 +33,16 @@ * Routine, which periodically checks all help threads and archives them if there has not been any * recent activity. */ -public final class HelpThreadAutoArchiver implements Routine { +public final class HelpThreadAutoArchiver implements Routine, UserInteractor { private static final Logger logger = LoggerFactory.getLogger(HelpThreadAutoArchiver.class); private static final int SCHEDULE_MINUTES = 60; private static final Duration ARCHIVE_AFTER_INACTIVITY_OF = Duration.ofHours(12); + private static final String MARK_ACTIVE_LABEL = "Mark Active"; + private static final String MARK_ACTIVE_ID = "mark-active"; private final HelpSystemHelper helper; + private final ComponentIdInteractor inactivityInteractor = + new ComponentIdInteractor(getInteractionType(), getName()); /** * Creates a new instance. @@ -43,6 +53,41 @@ public HelpThreadAutoArchiver(HelpSystemHelper helper) { this.helper = helper; } + @Override + public String getName() { + return "help-thread-auto-archiver"; + } + + @Override + public UserInteractionType getInteractionType() { + return UserInteractionType.OTHER; + } + + @Override + public void acceptComponentIdGenerator(ComponentIdGenerator generator) { + inactivityInteractor.acceptComponentIdGenerator(generator); + } + + @Override + public void onButtonClick(ButtonInteractionEvent event, List args) { + onMarkActiveButton(event); + } + + private void onMarkActiveButton(ButtonInteractionEvent event) { + event.reply("You have marked the thread as active.").setEphemeral(true).queue(); + + ThreadChannel thread = event.getChannel().asThreadChannel(); + Message botClosedThreadMessage = event.getMessage(); + + thread.getManager() + .setArchived(false) + .flatMap(_ -> botClosedThreadMessage.delete()) + .queue(); + + logger.debug("Thread {} was manually reactivated via button by user {}", thread.getId(), + event.getUser().getId()); + } + @Override public Schedule createSchedule() { return new Schedule(ScheduleMode.FIXED_RATE, 0, SCHEDULE_MINUTES, TimeUnit.MINUTES); @@ -88,8 +133,8 @@ private void autoArchiveForThread(ThreadChannel threadChannel) { """ Your question has been closed due to inactivity. - If it was not resolved yet, feel free to just post a message below - to reopen it, or create a new thread. + If it was not resolved yet, **click the button below** to keep it + open, or feel free to create a new thread. Note that usually the reason for nobody calling back is that your question may have been not well asked and hence no one felt confident @@ -131,11 +176,16 @@ private void handleArchiveFlow(ThreadChannel threadChannel, MessageEmbed embed) private void triggerArchiveFlow(ThreadChannel threadChannel, long authorId, MessageEmbed embed) { + String markActiveId = inactivityInteractor.generateComponentId(MARK_ACTIVE_ID); + Function> sendEmbedWithMention = - member -> threadChannel.sendMessage(member.getAsMention()).addEmbeds(embed); + member -> threadChannel.sendMessage(member.getAsMention()) + .addEmbeds(embed) + .addActionRow(Button.primary(markActiveId, MARK_ACTIVE_LABEL)); Supplier> sendEmbedWithoutMention = - () -> threadChannel.sendMessageEmbeds(embed); + () -> threadChannel.sendMessageEmbeds(embed) + .addActionRow(Button.primary(markActiveId, MARK_ACTIVE_LABEL)); threadChannel.getGuild() .retrieveMemberById(authorId) @@ -161,8 +211,12 @@ private void triggerAuthorIdNotFoundArchiveFlow(ThreadChannel threadChannel, logger.info( "Was unable to find a matching thread for id: {} in DB, archiving thread without mentioning OP", threadChannel.getId()); + + String markActiveId = inactivityInteractor.generateComponentId(MARK_ACTIVE_ID); + threadChannel.sendMessageEmbeds(embed) - .flatMap(sentEmbed -> threadChannel.getManager().setArchived(true)) + .addActionRow(Button.primary(markActiveId, MARK_ACTIVE_LABEL)) + .flatMap(_ -> threadChannel.getManager().setArchived(true)) .queue(); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index bbf8490a2c..246af1ffcf 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -187,7 +187,10 @@ private Consumer handleParentMessageDeleted(Member user, ThreadChanne @Override public void onButtonClick(ButtonInteractionEvent event, List args) { - // This method handles chatgpt's automatic response "dismiss" button + onAiHelpDismissButton(event, args); + } + + private void onAiHelpDismissButton(ButtonInteractionEvent event, List args) { event.deferEdit().queue(); ThreadChannel channel = event.getChannel().asThreadChannel(); @@ -197,7 +200,6 @@ public void onButtonClick(ButtonInteractionEvent event, List args) { .queue(forumPostMessage -> handleDismiss(interactionUser, channel, forumPostMessage, event, args), handleParentMessageDeleted(interactionUser, channel, event, args)); - } private boolean isPostAuthor(Member interactionUser, Message message) { From 0af5cd08502e3fc8bdcc56dd36901938e67c5238 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 08:33:18 +0100 Subject: [PATCH 12/23] build(deps): bump com.openai:openai-java from 4.23.0 to 4.24.1 (#1429) Bumps [com.openai:openai-java](https://github.com/openai/openai-java) from 4.23.0 to 4.24.1. - [Release notes](https://github.com/openai/openai-java/releases) - [Changelog](https://github.com/openai/openai-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/openai-java/compare/v4.23.0...v4.24.1) --- updated-dependencies: - dependency-name: com.openai:openai-java dependency-version: 4.24.1 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> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2cc986a351..e94db5ff4c 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ version '1.0-SNAPSHOT' ext { jooqVersion = '3.20.5' jacksonVersion = '2.19.1' - chatGPTVersion = '4.23.0' + chatGPTVersion = '4.24.1' junitVersion = '6.0.0' } From 9383845527809870787b403e7919aceb1cad3a9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 08:33:33 +0100 Subject: [PATCH 13/23] build(deps): bump gradle-wrapper from 9.2.0 to 9.4.0 (#1428) Bumps gradle-wrapper from 9.2.0 to 9.4.0. --- updated-dependencies: - dependency-name: gradle-wrapper dependency-version: 9.4.0 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> --- gradle/wrapper/gradle-wrapper.jar | Bin 45633 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee3125fe0768e9a76ee977ac089eb657005e..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39855 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 N2q(zEpY4qk{{xkq;s5{u diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bad7c2462f..dbc3ce4a04 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index adff685a03..0262dcbd52 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. From 692453c316c1183ed7fa56b8684fadb4c65e8867 Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Thu, 5 Mar 2026 22:51:24 +0100 Subject: [PATCH 14/23] removed slash command educator (#1427) --- .../togetherjava/tjbot/features/Features.java | 2 - .../features/basic/SlashCommandEducator.java | 74 ----------------- .../resources/slashCommandPopupAdvice.png | Bin 50963 -> 0 bytes .../basic/SlashCommandEducatorTest.java | 75 ------------------ 4 files changed, 151 deletions(-) delete mode 100644 application/src/main/java/org/togetherjava/tjbot/features/basic/SlashCommandEducator.java delete mode 100644 application/src/main/resources/slashCommandPopupAdvice.png delete mode 100644 application/src/test/java/org/togetherjava/tjbot/features/basic/SlashCommandEducatorTest.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 65a9996842..1af25522e9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -10,7 +10,6 @@ import org.togetherjava.tjbot.features.basic.PingCommand; import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder; import org.togetherjava.tjbot.features.basic.RoleSelectCommand; -import org.togetherjava.tjbot.features.basic.SlashCommandEducator; import org.togetherjava.tjbot.features.basic.SuggestionsUpDownVoter; import org.togetherjava.tjbot.features.bookmarks.BookmarksCommand; import org.togetherjava.tjbot.features.bookmarks.BookmarksSystem; @@ -164,7 +163,6 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(codeMessageHandler); features.add(new CodeMessageAutoDetection(config, codeMessageHandler)); features.add(new CodeMessageManualDetection(codeMessageHandler)); - features.add(new SlashCommandEducator()); features.add(new PinnedNotificationRemover(config)); features.add(new QuoteBoardForwarder(config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/SlashCommandEducator.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/SlashCommandEducator.java deleted file mode 100644 index f2c0d7e24b..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/SlashCommandEducator.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.togetherjava.tjbot.features.basic; - -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; -import net.dv8tion.jda.api.utils.FileUpload; - -import org.togetherjava.tjbot.features.MessageReceiverAdapter; -import org.togetherjava.tjbot.features.help.HelpSystemHelper; - -import java.io.InputStream; -import java.util.function.Predicate; -import java.util.regex.Pattern; - -/** - * Listens to messages that are likely supposed to be message commands, such as {@code !foo} and - * then educates the user about using slash commands, such as {@code /foo} instead. - */ -public final class SlashCommandEducator extends MessageReceiverAdapter { - private static final int MAX_COMMAND_LENGTH = 30; - private static final String SLASH_COMMAND_POPUP_ADVICE_PATH = "slashCommandPopupAdvice.png"; - private static final Predicate 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 (don't end with code stuff) - """, Pattern.COMMENTS).asMatchPredicate(); - - @Override - public void onMessageReceived(MessageReceivedEvent event) { - if (event.getAuthor().isBot() || event.isWebhookMessage()) { - return; - } - - String content = event.getMessage().getContentRaw(); - - if (IS_MESSAGE_COMMAND.test(content) && content.length() < MAX_COMMAND_LENGTH) { - sendAdvice(event.getMessage()); - } - } - - private void sendAdvice(Message message) { - String content = - """ - Looks like you attempted to use a command? Please note that we only use **slash-commands** on this server 🙂 - - 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).queue(); - } - - private static MessageCreateAction createReply(Message messageToReplyTo, String content) { - boolean useImage = true; - InputStream imageData = - HelpSystemHelper.class.getResourceAsStream("/" + SLASH_COMMAND_POPUP_ADVICE_PATH); - if (imageData == null) { - useImage = false; - } - - MessageEmbed embed = new EmbedBuilder().setDescription(content) - .setImage(useImage ? "attachment://" + SLASH_COMMAND_POPUP_ADVICE_PATH : null) - .build(); - - MessageCreateAction action = messageToReplyTo.replyEmbeds(embed); - if (useImage) { - action = action - .addFiles(FileUpload.fromData(imageData, SLASH_COMMAND_POPUP_ADVICE_PATH)); - } - - return action; - } -} diff --git a/application/src/main/resources/slashCommandPopupAdvice.png b/application/src/main/resources/slashCommandPopupAdvice.png deleted file mode 100644 index 6284b8566e9ef48f8051ac31ee544ff1adee3a0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50963 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 X(lv!zyk{@~@JmucPP|6cAmIN1w7{*W diff --git a/application/src/test/java/org/togetherjava/tjbot/features/basic/SlashCommandEducatorTest.java b/application/src/test/java/org/togetherjava/tjbot/features/basic/SlashCommandEducatorTest.java deleted file mode 100644 index 03d07c1a2c..0000000000 --- a/application/src/test/java/org/togetherjava/tjbot/features/basic/SlashCommandEducatorTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.togetherjava.tjbot.features.basic; - -import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.channel.ChannelType; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; -import net.dv8tion.jda.api.utils.messages.MessageCreateData; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -import org.togetherjava.tjbot.features.MessageReceiver; -import org.togetherjava.tjbot.jda.JdaTester; - -import java.util.List; -import java.util.stream.Stream; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -final class SlashCommandEducatorTest { - private JdaTester jdaTester; - private MessageReceiver messageReceiver; - - @BeforeEach - void setUp() { - jdaTester = new JdaTester(); - messageReceiver = new SlashCommandEducator(); - } - - private MessageReceivedEvent sendMessage(String content) { - MessageCreateData message = new MessageCreateBuilder().setContent(content).build(); - MessageReceivedEvent event = - jdaTester.createMessageReceiveEvent(message, List.of(), ChannelType.TEXT); - - messageReceiver.onMessageReceived(event); - - return event; - } - - @ParameterizedTest - @MethodSource("provideMessageCommands") - void sendsAdviceOnMessageCommand(String message) { - // GIVEN a message containing a message command - // WHEN the message is sent - MessageReceivedEvent event = sendMessage(message); - - // THEN the system replies to it with an advice - verify(event.getMessage(), times(1)).replyEmbeds(any(MessageEmbed.class)); - } - - @ParameterizedTest - @MethodSource("provideOtherMessages") - void ignoresOtherMessages(String message) { - // GIVEN a message that is not a message command - // WHEN the message is sent - MessageReceivedEvent event = sendMessage(message); - - // THEN the system ignores the message and does not reply to it - verify(event.getMessage(), never()).replyEmbeds(any(MessageEmbed.class)); - } - - private static Stream provideMessageCommands() { - return Stream.of("!foo", ".foo", "?foo", ".test", "!whatever", "!this is a test"); - } - - private static Stream provideOtherMessages() { - return Stream.of(" a ", "foo", "#foo", "/foo", "!!!", "?!?!?", "?", ".,-", "!f", "! foo", - "thisIsAWordWhichLengthIsMoreThanThirtyLetterSoItShouldNotReply", - ".isLetter and .isNumber are available", ".toString()", ".toString();", - "this is a test;"); - } -} From afd94248e8ee5aff89e513f9c5fdb2998b7b6cfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 07:51:33 +0100 Subject: [PATCH 15/23] build(deps): bump com.openai:openai-java from 4.24.1 to 4.26.0 (#1430) Bumps [com.openai:openai-java](https://github.com/openai/openai-java) from 4.24.1 to 4.26.0. - [Release notes](https://github.com/openai/openai-java/releases) - [Changelog](https://github.com/openai/openai-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/openai-java/compare/v4.24.1...v4.26.0) --- updated-dependencies: - dependency-name: com.openai:openai-java dependency-version: 4.26.0 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> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e94db5ff4c..a60efc7e75 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ version '1.0-SNAPSHOT' ext { jooqVersion = '3.20.5' jacksonVersion = '2.19.1' - chatGPTVersion = '4.24.1' + chatGPTVersion = '4.26.0' junitVersion = '6.0.0' } From f27f66aefc12e20cdc1f741a8ca19e2b8a21ed5e Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Fri, 6 Mar 2026 10:03:42 +0100 Subject: [PATCH 16/23] Reduced log-level of RSS-Feed Circuit Breaker (#1431) --- .../org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java index 1d89896038..eac3b9ef31 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java @@ -432,7 +432,7 @@ private List fetchRSSItemsFromURL(String rssUrl) { long blacklistedHours = calculateWaitHours(newCount); - logger.warn( + logger.debug( "RSS fetch failed for {} (Attempt #{}). Backing off for {} hours. Reason: {}", rssUrl, newCount, blacklistedHours, e.getMessage(), e); From c686756e1d6231de42dda370b83439a0fc5a6ebe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:57:16 +0100 Subject: [PATCH 17/23] build(deps): bump org.flywaydb:flyway-core from 12.0.0 to 12.1.0 (#1432) Bumps [org.flywaydb:flyway-core](https://github.com/flyway/flyway) from 12.0.0 to 12.1.0. - [Release notes](https://github.com/flyway/flyway/releases) - [Commits](https://github.com/flyway/flyway/compare/flyway-12.0.0...flyway-12.1.0) --- updated-dependencies: - dependency-name: org.flywaydb:flyway-core dependency-version: 12.1.0 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 8ef3ef97cd..2d0e9fbbcf 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -7,7 +7,7 @@ var sqliteVersion = "3.51.0.0" dependencies { implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation "org.xerial:sqlite-jdbc:${sqliteVersion}" - implementation 'org.flywaydb:flyway-core:12.0.0' + implementation 'org.flywaydb:flyway-core:12.1.0' implementation "org.jooq:jooq:$jooqVersion" implementation project(':utils') From bb7164ad45359535eb95cf28b5a39e5387fbea2d Mon Sep 17 00:00:00 2001 From: Firas Regaieg Date: Wed, 11 Mar 2026 08:40:06 +0100 Subject: [PATCH 18/23] Basic Bot Analytics System (#1425) * Analytics service setup with first use in Ping command * Analytics: remove AnalyticsService injection in PingCommand and using it from BotCore; * feat: update command usage analytics to use generated record methods * Analytics: applies changes for zabuzard 1st CR; * Analytics - Metrics > persist(): rename argument moment to happenedAt * Analytics update - Metrics: renaming persist() to processEvent(); - resources/db: V16 update, removing default value for happened_at column --- .../org/togetherjava/tjbot/Application.java | 5 +- .../togetherjava/tjbot/features/Features.java | 8 ++- .../tjbot/features/analytics/Metrics.java | 56 +++++++++++++++++++ .../features/analytics/package-info.java | 13 +++++ .../tjbot/features/system/BotCore.java | 35 +++++++----- .../db/V16__Add_Analytics_System.sql | 6 ++ 6 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/analytics/Metrics.java create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/analytics/package-info.java create mode 100644 application/src/main/resources/db/V16__Add_Analytics_System.sql diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index 4c228cb02a..ec5e92a2f3 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -12,6 +12,7 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.features.Features; import org.togetherjava.tjbot.features.SlashCommandAdapter; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.system.BotCore; import org.togetherjava.tjbot.logging.LogMarkers; import org.togetherjava.tjbot.logging.discord.DiscordLogging; @@ -82,13 +83,15 @@ public static void runBot(Config config) { } Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath()); + Metrics metrics = new Metrics(database); + JDA jda = JDABuilder.createDefault(config.getToken()) .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT) .build(); jda.awaitReady(); - BotCore core = new BotCore(jda, database, config); + BotCore core = new BotCore(jda, database, config, metrics); CommandReloading.reloadCommands(jda, core); core.scheduleRoutines(jda); 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 1af25522e9..bd2575d407 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -6,6 +6,7 @@ import org.togetherjava.tjbot.config.FeatureBlacklist; import org.togetherjava.tjbot.config.FeatureBlacklistConfig; import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine; import org.togetherjava.tjbot.features.basic.PingCommand; import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder; @@ -90,7 +91,7 @@ * it with the system. *

* To add a new slash command, extend the commands returned by - * {@link #createFeatures(JDA, Database, Config)}. + * {@link #createFeatures(JDA, Database, Config, Metrics)}. */ public class Features { private Features() { @@ -106,9 +107,12 @@ private Features() { * @param jda the JDA instance commands will be registered at * @param database the database of the application, which features can use to persist data * @param config the configuration features should use + * @param metrics the metrics service for tracking analytics * @return a collection of all features */ - public static Collection createFeatures(JDA jda, Database database, Config config) { + @SuppressWarnings("unused") + public static Collection createFeatures(JDA jda, Database database, Config config, + Metrics metrics) { FeatureBlacklistConfig blacklistConfig = config.getFeatureBlacklistConfig(); JShellEval jshellEval = new JShellEval(config.getJshell(), config.getGitHubApiKey()); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/analytics/Metrics.java b/application/src/main/java/org/togetherjava/tjbot/features/analytics/Metrics.java new file mode 100644 index 0000000000..9aa0c797fe --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/analytics/Metrics.java @@ -0,0 +1,56 @@ +package org.togetherjava.tjbot.features.analytics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.generated.tables.MetricEvents; + +import java.time.Instant; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Service for tracking and recording events for analytics purposes. + */ +public final class Metrics { + private static final Logger logger = LoggerFactory.getLogger(Metrics.class); + + private final Database database; + + private final ExecutorService service = Executors.newSingleThreadExecutor(); + + /** + * Creates a new instance. + * + * @param database the database to use for storing and retrieving analytics data + */ + public Metrics(Database database) { + this.database = database; + } + + /** + * Track an event execution. + * + * @param event the event to save + */ + public void count(String event) { + logger.debug("Counting new record for event: {}", event); + Instant moment = Instant.now(); + service.submit(() -> processEvent(event, moment)); + + } + + /** + * + * @param event the event to save + * @param happenedAt the moment when the event is dispatched + */ + private void processEvent(String event, Instant happenedAt) { + database.write(context -> context.newRecord(MetricEvents.METRIC_EVENTS) + .setEvent(event) + .setHappenedAt(happenedAt) + .insert()); + } + +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/analytics/package-info.java b/application/src/main/java/org/togetherjava/tjbot/features/analytics/package-info.java new file mode 100644 index 0000000000..d06d76f93d --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/analytics/package-info.java @@ -0,0 +1,13 @@ +/** + * Analytics system for collecting and persisting bot activity metrics. + *

+ * This package provides services and components that record events for later analysis and reporting + * across multiple feature areas. + */ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package org.togetherjava.tjbot.features.analytics; + +import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java b/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java index e9d99bc4d1..da4e47fcd8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java @@ -23,7 +23,6 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.components.ComponentInteraction; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.slf4j.Logger; @@ -42,6 +41,7 @@ import org.togetherjava.tjbot.features.UserInteractionType; import org.togetherjava.tjbot.features.UserInteractor; import org.togetherjava.tjbot.features.VoiceReceiver; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.componentids.ComponentId; import org.togetherjava.tjbot.features.componentids.ComponentIdParser; import org.togetherjava.tjbot.features.componentids.ComponentIdStore; @@ -79,13 +79,13 @@ public final class BotCore extends ListenerAdapter implements CommandProvider { private static final ExecutorService COMMAND_SERVICE = Executors.newCachedThreadPool(); private static final ScheduledExecutorService ROUTINE_SERVICE = Executors.newScheduledThreadPool(5); - private final Config config; private final Map prefixedNameToInteractor; private final List routines; private final ComponentIdParser componentIdParser; private final ComponentIdStore componentIdStore; private final Map channelNameToMessageReceiver = new HashMap<>(); private final Map channelNameToVoiceReceiver = new HashMap<>(); + private final Metrics metrics; /** * Creates a new command system which uses the given database to allow commands to persist data. @@ -95,10 +95,11 @@ public final class BotCore extends ListenerAdapter implements CommandProvider { * @param jda the JDA instance that this command system will be used with * @param database the database that commands may use to persist data * @param config the configuration to use for this system + * @param metrics the metrics service for tracking analytics */ - public BotCore(JDA jda, Database database, Config config) { - this.config = config; - Collection features = Features.createFeatures(jda, database, config); + public BotCore(JDA jda, Database database, Config config, Metrics metrics) { + this.metrics = metrics; + Collection features = Features.createFeatures(jda, database, config, metrics); // Message receivers features.stream() @@ -300,14 +301,14 @@ private Optional selectPreferredAudioChannel(@Nullable AudioChannelUnio } @Override - public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) { + public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) { selectPreferredAudioChannel(event.getChannelJoined(), event.getChannelLeft()) .ifPresent(channel -> getVoiceReceiversSubscribedTo(channel) .forEach(voiceReceiver -> voiceReceiver.onVoiceUpdate(event))); } @Override - public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) { + public void onGuildVoiceVideo(GuildVoiceVideoEvent event) { AudioChannelUnion channel = event.getVoiceState().getChannel(); if (channel == null) { @@ -319,7 +320,7 @@ public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) { } @Override - public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) { + public void onGuildVoiceStream(GuildVoiceStreamEvent event) { AudioChannelUnion channel = event.getVoiceState().getChannel(); if (channel == null) { @@ -331,7 +332,7 @@ public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) { } @Override - public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) { + public void onGuildVoiceMute(GuildVoiceMuteEvent event) { AudioChannelUnion channel = event.getVoiceState().getChannel(); if (channel == null) { @@ -343,7 +344,7 @@ public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) { } @Override - public void onGuildVoiceDeafen(@NotNull GuildVoiceDeafenEvent event) { + public void onGuildVoiceDeafen(GuildVoiceDeafenEvent event) { AudioChannelUnion channel = event.getVoiceState().getChannel(); if (channel == null) { @@ -380,10 +381,16 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { logger.debug("Received slash command '{}' (#{}) on guild '{}'", name, event.getId(), event.getGuild()); - COMMAND_SERVICE.execute( - () -> requireUserInteractor(UserInteractionType.SLASH_COMMAND.getPrefixedName(name), - SlashCommand.class) - .onSlashCommand(event)); + COMMAND_SERVICE.execute(() -> { + + SlashCommand interactor = requireUserInteractor( + UserInteractionType.SLASH_COMMAND.getPrefixedName(name), SlashCommand.class); + + metrics.count("slash-" + name); + + interactor.onSlashCommand(event); + + }); } @Override diff --git a/application/src/main/resources/db/V16__Add_Analytics_System.sql b/application/src/main/resources/db/V16__Add_Analytics_System.sql new file mode 100644 index 0000000000..a29a62e513 --- /dev/null +++ b/application/src/main/resources/db/V16__Add_Analytics_System.sql @@ -0,0 +1,6 @@ +CREATE TABLE metric_events +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event TEXT NOT NULL, + happened_at TIMESTAMP NOT NULL +); From 799008947621f91367c720cd1bc707cd3f02afa2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:59:57 +0100 Subject: [PATCH 19/23] build(deps): bump org.mockito:mockito-core from 5.22.0 to 5.23.0 (#1436) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.22.0 to 5.23.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.22.0...v5.23.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-version: 5.23.0 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> --- application/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/build.gradle b/application/build.gradle index b0af6d1bf0..a387e78a87 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation 'org.apache.commons:commons-text:1.15.0' implementation 'com.apptasticsoftware:rssreader:3.12.0' - testImplementation 'org.mockito:mockito-core:5.22.0' + testImplementation 'org.mockito:mockito-core:5.23.0' testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly 'org.junit.platform:junit-platform-launcher' From 5d819b36fc131e1aed8602263d5df59a9e78c038 Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Thu, 12 Mar 2026 11:12:43 +0100 Subject: [PATCH 20/23] Fixed Slashcommand-Metrics missing subcommands (#1434) --- .../org/togetherjava/tjbot/features/system/BotCore.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java b/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java index da4e47fcd8..c87153428b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java @@ -382,14 +382,16 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) { logger.debug("Received slash command '{}' (#{}) on guild '{}'", name, event.getId(), event.getGuild()); COMMAND_SERVICE.execute(() -> { - SlashCommand interactor = requireUserInteractor( UserInteractionType.SLASH_COMMAND.getPrefixedName(name), SlashCommand.class); - metrics.count("slash-" + name); + String eventName = "slash-" + name; + if (event.getSubcommandName() != null) { + eventName += "_" + event.getSubcommandName(); + } + metrics.count(eventName); interactor.onSlashCommand(event); - }); } From ddabd5ea83c79d00557366096c724869bfa04b57 Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Thu, 12 Mar 2026 11:13:02 +0100 Subject: [PATCH 21/23] Fixed /github-search command failing in some cases (#1435) * fixed github command * CR firas --- .../tjbot/features/github/GitHubCommand.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/github/GitHubCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/github/GitHubCommand.java index b52e79550d..4091912755 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/github/GitHubCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/github/GitHubCommand.java @@ -2,6 +2,7 @@ import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.commands.OptionType; import org.kohsuke.github.GHIssue; import org.slf4j.Logger; @@ -44,6 +45,7 @@ public final class GitHubCommand extends SlashCommandAdapter { private static final String TITLE_OPTION = "title"; private static final Logger logger = LoggerFactory.getLogger(GitHubCommand.class); + private static final int MAX_SUGGESTED_CHOICES = 25; private final GitHubReference reference; @@ -98,10 +100,13 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { String[] issueData = titleOption.split(" ", 2); String targetIssueTitle = issueData[1]; + event.deferReply().queue(); + InteractionHook hook = event.getHook(); + reference.findIssue(issueId, targetIssueTitle) - .ifPresentOrElse(issue -> event.replyEmbeds(reference.generateReply(issue)).queue(), - () -> event.reply("Could not find the issue you are looking for.") - .setEphemeral(true) + .ifPresentOrElse( + issue -> hook.editOriginalEmbeds(reference.generateReply(issue)).queue(), + () -> hook.editOriginal("Could not find the issue you are looking for.") .queue()); } @@ -109,17 +114,21 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { public void onAutoComplete(CommandAutoCompleteInteractionEvent event) { String title = event.getOption(TITLE_OPTION).getAsString(); + List choices; if (title.isEmpty()) { - event.replyChoiceStrings(autocompleteGHIssueCache.stream().limit(25).toList()).queue(); + choices = autocompleteGHIssueCache.stream().limit(MAX_SUGGESTED_CHOICES).toList(); } else { Queue closestSuggestions = new PriorityQueue<>(Comparator.comparingInt(suggestionScorer(title))); - closestSuggestions.addAll(autocompleteGHIssueCache); + choices = + Stream.generate(closestSuggestions::poll).limit(MAX_SUGGESTED_CHOICES).toList(); + } - List choices = Stream.generate(closestSuggestions::poll).limit(25).toList(); - event.replyChoiceStrings(choices).queue(); + if (choices.isEmpty()) { + choices = List.of("No issues found"); } + event.replyChoiceStrings(choices).queue(); if (isCacheExpired()) { updateCache(); From e64fd0b6ab97c572393fd80537cb92a38a6d9bfa Mon Sep 17 00:00:00 2001 From: Daniel Tischner Date: Thu, 12 Mar 2026 18:08:09 +0100 Subject: [PATCH 22/23] Added metric events (#1433) * added a bunch of metric events * adjusted tests * (merge conflict) --- .../togetherjava/tjbot/features/Features.java | 36 +++++++++---------- .../basic/SuggestionsUpDownVoter.java | 7 +++- .../features/chatgpt/ChatGptService.java | 8 ++++- .../features/code/CodeMessageHandler.java | 8 ++++- .../FileSharingMessageListener.java | 8 ++++- .../features/github/GitHubReference.java | 7 +++- .../features/help/AutoPruneHelperRoutine.java | 7 +++- .../help/GuildLeaveCloseThreadListener.java | 15 +++++--- .../tjbot/features/help/HelpSystemHelper.java | 1 - .../features/help/HelpThreadCommand.java | 7 +++- .../help/HelpThreadCreatedListener.java | 18 +++++++--- .../mediaonly/MediaOnlyChannelListener.java | 35 ++++++++++-------- .../RejoinModerationRoleListener.java | 11 ++++-- .../BlacklistedAttachmentListener.java | 8 ++++- .../features/moderation/scam/ScamBlocker.java | 7 +++- .../tjbot/features/rss/RSSHandlerRoutine.java | 8 ++++- .../tjbot/features/system/BotCore.java | 22 +++++++----- .../tjbot/features/tags/TagCommand.java | 7 +++- .../TopHelpersAssignmentRoutine.java | 9 ++++- .../features/voicechat/DynamicVoiceChat.java | 13 ++++--- .../MediaOnlyChannelListenerTest.java | 3 +- .../tjbot/features/tags/TagCommandTest.java | 6 ++-- .../tjbot/features/tags/TagSystemTest.java | 10 +++--- 23 files changed, 184 insertions(+), 77 deletions(-) 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 bd2575d407..1f6acbef2e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -121,18 +121,18 @@ public static Collection createFeatures(JDA jda, Database database, Con ModerationActionsStore actionsStore = new ModerationActionsStore(database); ModAuditLogWriter modAuditLogWriter = new ModAuditLogWriter(config); ScamHistoryStore scamHistoryStore = new ScamHistoryStore(database); - GitHubReference githubReference = new GitHubReference(config); + GitHubReference githubReference = new GitHubReference(config, metrics); CodeMessageHandler codeMessageHandler = - new CodeMessageHandler(blacklistConfig.special(), jshellEval); - ChatGptService chatGptService = new ChatGptService(config); + new CodeMessageHandler(blacklistConfig.special(), jshellEval, metrics); + ChatGptService chatGptService = new ChatGptService(config, metrics); HelpSystemHelper helpSystemHelper = new HelpSystemHelper(config, database, chatGptService); HelpThreadLifecycleListener helpThreadLifecycleListener = new HelpThreadLifecycleListener(helpSystemHelper, database); HelpThreadCreatedListener helpThreadCreatedListener = - new HelpThreadCreatedListener(helpSystemHelper); + new HelpThreadCreatedListener(helpSystemHelper, metrics); TopHelpersService topHelpersService = new TopHelpersService(database); TopHelpersAssignmentRoutine topHelpersAssignmentRoutine = - new TopHelpersAssignmentRoutine(config, topHelpersService); + new TopHelpersAssignmentRoutine(config, topHelpersService, metrics); // 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 @@ -147,22 +147,22 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new ScamHistoryPurgeRoutine(scamHistoryStore)); features.add(new HelpThreadMetadataPurger(database)); features.add(new HelpThreadActivityUpdater(helpSystemHelper)); - features - .add(new AutoPruneHelperRoutine(config, helpSystemHelper, modAuditLogWriter, database)); + features.add(new AutoPruneHelperRoutine(config, helpSystemHelper, modAuditLogWriter, + database, metrics)); features.add(new HelpThreadAutoArchiver(helpSystemHelper)); features.add(new LeftoverBookmarksCleanupRoutine(bookmarksSystem)); features.add(new MarkHelpThreadCloseInDBRoutine(database, helpThreadLifecycleListener)); features.add(new MemberCountDisplayRoutine(config)); - features.add(new RSSHandlerRoutine(config, database)); + features.add(new RSSHandlerRoutine(config, database, metrics)); features.add(topHelpersAssignmentRoutine); // Message receivers features.add(new TopHelpersMessageListener(database, config)); - features.add(new SuggestionsUpDownVoter(config)); - features.add(new ScamBlocker(actionsStore, scamHistoryStore, config)); - features.add(new MediaOnlyChannelListener(config)); - features.add(new FileSharingMessageListener(config)); - features.add(new BlacklistedAttachmentListener(config, modAuditLogWriter)); + features.add(new SuggestionsUpDownVoter(config, metrics)); + features.add(new ScamBlocker(actionsStore, scamHistoryStore, config, metrics)); + features.add(new MediaOnlyChannelListener(config, metrics)); + features.add(new FileSharingMessageListener(config, metrics)); + features.add(new BlacklistedAttachmentListener(config, modAuditLogWriter, metrics)); features.add(githubReference); features.add(codeMessageHandler); features.add(new CodeMessageAutoDetection(config, codeMessageHandler)); @@ -171,11 +171,11 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new QuoteBoardForwarder(config)); // Voice receivers - features.add(new DynamicVoiceChat(config)); + features.add(new DynamicVoiceChat(config, metrics)); // Event receivers - features.add(new RejoinModerationRoleListener(actionsStore, config)); - features.add(new GuildLeaveCloseThreadListener(config)); + features.add(new RejoinModerationRoleListener(actionsStore, config, metrics)); + features.add(new GuildLeaveCloseThreadListener(config, metrics)); features.add(new LeftoverBookmarksListener(bookmarksSystem)); features.add(helpThreadCreatedListener); features.add(new HelpThreadLifecycleListener(helpSystemHelper, database)); @@ -190,7 +190,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new LogLevelCommand()); features.add(new PingCommand()); features.add(new TeXCommand()); - features.add(new TagCommand(tagSystem)); + features.add(new TagCommand(tagSystem, metrics)); features.add(new TagManageCommand(tagSystem, modAuditLogWriter)); features.add(new TagsCommand(tagSystem)); features.add(new WarnCommand(actionsStore)); @@ -210,7 +210,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new WolframAlphaCommand(config)); features.add(new GitHubCommand(githubReference)); features.add(new ModMailCommand(jda, config)); - features.add(new HelpThreadCommand(config, helpSystemHelper)); + features.add(new HelpThreadCommand(config, helpSystemHelper, metrics)); features.add(new ReportCommand(config)); features.add(new BookmarksCommand(bookmarksSystem)); features.add(new ChatGptCommand(chatGptService, helpSystemHelper)); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java index e07682725f..861454b0a6 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java @@ -13,6 +13,7 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.SuggestionsConfig; import org.togetherjava.tjbot.features.MessageReceiverAdapter; +import org.togetherjava.tjbot.features.analytics.Metrics; import java.util.Optional; import java.util.regex.Pattern; @@ -28,16 +29,19 @@ public final class SuggestionsUpDownVoter extends MessageReceiverAdapter { private static final int THREAD_TITLE_MAX_LENGTH = 60; private final SuggestionsConfig config; + private final Metrics metrics; /** * Creates a new listener to receive all message sent in suggestion channels. * * @param config the config to use for this + * @param metrics to track events */ - public SuggestionsUpDownVoter(Config config) { + public SuggestionsUpDownVoter(Config config, Metrics metrics) { super(Pattern.compile(config.getSuggestions().getChannelPattern())); this.config = config.getSuggestions(); + this.metrics = metrics; } @Override @@ -49,6 +53,7 @@ public void onMessageReceived(MessageReceivedEvent event) { Guild guild = event.getGuild(); Message message = event.getMessage(); + metrics.count("suggestion"); createThread(message); reactWith(config.getUpVoteEmoteName(), FALLBACK_UP_VOTE, guild, message); 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 08ddbee729..3b2e78af73 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 @@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.features.analytics.Metrics; import javax.annotation.Nullable; @@ -28,14 +29,18 @@ public class ChatGptService { private boolean isDisabled = false; private OpenAIClient openAIClient; + private Metrics metrics; /** * Creates instance of ChatGPTService * * @param config needed for token to OpenAI API. + * @param metrics to track events */ - public ChatGptService(Config config) { + public ChatGptService(Config config, Metrics metrics) { String apiKey = config.getOpenaiApiKey(); + this.metrics = metrics; + boolean keyIsDefaultDescription = apiKey.startsWith("<") && apiKey.endsWith(">"); if (apiKey.isBlank() || keyIsDefaultDescription) { isDisabled = true; @@ -111,6 +116,7 @@ private Optional sendPrompt(String prompt, ChatGptModel chatModel) { .build(); Response chatGptResponse = openAIClient.responses().create(params); + metrics.count("chatgpt-prompted"); String response = chatGptResponse.output() .stream() diff --git a/application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java b/application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java index 601f91663f..38b2c44164 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java @@ -17,6 +17,7 @@ import org.togetherjava.tjbot.features.MessageReceiverAdapter; import org.togetherjava.tjbot.features.UserInteractionType; import org.togetherjava.tjbot.features.UserInteractor; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator; import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor; import org.togetherjava.tjbot.features.jshell.JShellEval; @@ -52,6 +53,7 @@ public final class CodeMessageHandler extends MessageReceiverAdapter implements static final Color AMBIENT_COLOR = Color.decode("#FDFD96"); private final ComponentIdInteractor componentIdInteractor; + private final Metrics metrics; private final Map labelToCodeAction; /** @@ -71,9 +73,12 @@ public final class CodeMessageHandler extends MessageReceiverAdapter implements * @param blacklist the feature blacklist, used to test if certain code actions should be * disabled * @param jshellEval used to execute java code and build visual result + * @param metrics to track events */ - public CodeMessageHandler(FeatureBlacklist blacklist, JShellEval jshellEval) { + public CodeMessageHandler(FeatureBlacklist blacklist, JShellEval jshellEval, + Metrics metrics) { componentIdInteractor = new ComponentIdInteractor(getInteractionType(), getName()); + this.metrics = metrics; List codeActions = blacklist .filterStream(Stream.of(new FormatCodeCommand(), new EvalCodeCommand(jshellEval)), @@ -183,6 +188,7 @@ public void onButtonClick(ButtonInteractionEvent event, List args) { CodeFence code = extractCodeOrFallback(originalMessage.get().getContentRaw()); // Apply the selected action + metrics.count("code_action-" + codeAction.getLabel()); return event.getHook() .editOriginalEmbeds(codeAction.apply(code)) .setActionRow(createButtons(originalMessageId, codeAction)); 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 c040eaf065..bdc4f13c38 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 @@ -18,6 +18,7 @@ import org.togetherjava.tjbot.features.MessageReceiverAdapter; import org.togetherjava.tjbot.features.UserInteractionType; import org.togetherjava.tjbot.features.UserInteractor; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator; import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor; import org.togetherjava.tjbot.features.utils.Guilds; @@ -46,6 +47,7 @@ public final class FileSharingMessageListener extends MessageReceiverAdapter new ComponentIdInteractor(getInteractionType(), getName()); private final String githubApiKey; + private final Metrics metrics; private final Set extensionFilter = Set.of("txt", "java", "gradle", "xml", "kt", "json", "fxml", "css", "c", "h", "cpp", "py", "yml"); @@ -56,11 +58,13 @@ public final class FileSharingMessageListener extends MessageReceiverAdapter * Creates a new instance. * * @param config used to get api key and channel names. + * @param metrics to track events * @see org.togetherjava.tjbot.features.Features */ - public FileSharingMessageListener(Config config) { + public FileSharingMessageListener(Config config, Metrics metrics) { super(Pattern.compile(".*")); githubApiKey = config.getGitHubApiKey(); + this.metrics = metrics; isHelpForumName = Pattern.compile(config.getHelpSystem().getHelpForumPattern()).asMatchPredicate(); isSoftModRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); @@ -112,6 +116,7 @@ public void onButtonClick(ButtonInteractionEvent event, List args) { new GitHubBuilder().withOAuthToken(githubApiKey).build().getGist(gistId).delete(); event.deferEdit().queue(); event.getHook().deleteOriginal().queue(); + metrics.count("file_sharing-deleted"); } catch (IOException e) { logger.warn("Failed to delete gist with id {}", gistId, e); } @@ -190,6 +195,7 @@ private void sendResponse(MessageReceivedEvent event, String url, String gistId) componentIdInteractor.generateComponentId(message.getAuthor().getId(), gistId), "Delete"); + metrics.count("file_sharing-uploaded"); message.reply(messageContent).setActionRow(gist, delete).queue(); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/github/GitHubReference.java b/application/src/main/java/org/togetherjava/tjbot/features/github/GitHubReference.java index 960587e8a4..0b48799806 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/github/GitHubReference.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/github/GitHubReference.java @@ -21,6 +21,7 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.features.MessageReceiverAdapter; +import org.togetherjava.tjbot.features.analytics.Metrics; import java.awt.Color; import java.io.FileNotFoundException; @@ -67,6 +68,7 @@ public final class GitHubReference extends MessageReceiverAdapter { DateTimeFormatter.ofPattern("dd MMM, yyyy").withZone(ZoneOffset.UTC); private final Predicate hasGithubIssueReferenceEnabled; private final Config config; + private final Metrics metrics; /** * The repositories that are searched when looking for an issue. @@ -80,9 +82,11 @@ public final class GitHubReference extends MessageReceiverAdapter { * a predicate for matching allowed channels for feature and acquires repositories. * * @param config The Config to get allowed channel pattern for feature. + * @param metrics to track events */ - public GitHubReference(Config config) { + public GitHubReference(Config config, Metrics metrics) { this.config = config; + this.metrics = metrics; this.hasGithubIssueReferenceEnabled = Pattern.compile(config.getGitHubReferencingEnabledChannelPattern()) .asMatchPredicate(); @@ -142,6 +146,7 @@ private void replyBatchEmbeds(List embeds, Message message, ? message.getChannel().asThreadChannel() : message.getChannel().asTextChannel(); + metrics.count("gh_reference"); for (List messageEmbeds : partition) { if (isFirstBatch) { message.replyEmbeds(messageEmbeds).mentionRepliedUser(mentionRepliedUser).queue(); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/AutoPruneHelperRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/help/AutoPruneHelperRoutine.java index 6ed381a36e..34ac5140ba 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/AutoPruneHelperRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/AutoPruneHelperRoutine.java @@ -12,6 +12,7 @@ import org.togetherjava.tjbot.config.HelperPruneConfig; import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.features.Routine; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.moderation.audit.ModAuditLogWriter; import javax.annotation.Nullable; @@ -45,6 +46,7 @@ public final class AutoPruneHelperRoutine implements Routine { private final HelpSystemHelper helper; private final ModAuditLogWriter modAuditLogWriter; private final Database database; + private final Metrics metrics; private final List allCategories; private final Predicate selectYourRolesChannelNamePredicate; @@ -55,13 +57,15 @@ public final class AutoPruneHelperRoutine implements Routine { * @param helper the helper to use * @param modAuditLogWriter to inform mods when manual pruning becomes necessary * @param database to determine whether a user is inactive + * @param metrics to track events */ public AutoPruneHelperRoutine(Config config, HelpSystemHelper helper, - ModAuditLogWriter modAuditLogWriter, Database database) { + ModAuditLogWriter modAuditLogWriter, Database database, Metrics metrics) { allCategories = config.getHelpSystem().getCategories(); this.helper = helper; this.modAuditLogWriter = modAuditLogWriter; this.database = database; + this.metrics = metrics; HelperPruneConfig helperPruneConfig = config.getHelperPruneConfig(); roleFullLimit = helperPruneConfig.roleFullLimit(); @@ -108,6 +112,7 @@ private void pruneRoleIfFull(List members, Role targetRole, if (isRoleFull(withRole)) { logger.debug("Helper role {} is full, starting to prune.", targetRole.getName()); + metrics.count("autoprune_helper-" + targetRole.getName()); pruneRole(targetRole, withRole, selectRoleChannel, when); } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/GuildLeaveCloseThreadListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/GuildLeaveCloseThreadListener.java index 5d1a4d7260..a0a7facd88 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/GuildLeaveCloseThreadListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/GuildLeaveCloseThreadListener.java @@ -7,20 +7,24 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.features.EventReceiver; +import org.togetherjava.tjbot.features.analytics.Metrics; /** * Remove all thread channels associated to a user when they leave the guild. */ public final class GuildLeaveCloseThreadListener extends ListenerAdapter implements EventReceiver { private final String helpForumPattern; + private final Metrics metrics; /** * Creates a new instance. * * @param config the config to get help forum channel pattern from + * @param metrics to track events */ - public GuildLeaveCloseThreadListener(Config config) { + public GuildLeaveCloseThreadListener(Config config, Metrics metrics) { this.helpForumPattern = config.getHelpSystem().getHelpForumPattern(); + this.metrics = metrics; } @Override @@ -35,8 +39,11 @@ public void onGuildMemberRemove(GuildMemberRemoveEvent event) { .queue(threads -> threads.stream() .filter(thread -> thread.getOwnerIdLong() == event.getUser().getIdLong()) .filter(thread -> thread.getParentChannel().getName().matches(helpForumPattern)) - .forEach(thread -> thread.sendMessageEmbeds(embed) - .flatMap(_ -> thread.getManager().setArchived(true)) - .queue())); + .forEach(thread -> { + metrics.count("op_left_thread"); + thread.sendMessageEmbeds(embed) + .flatMap(_ -> thread.getManager().setArchived(true)) + .queue(); + })); } } 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 edf217f1ea..5f88ff1019 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 @@ -236,7 +236,6 @@ private RestAction useChatGptFallbackMessage(ThreadChannel threadChanne } void writeHelpThreadToDatabase(long authorId, ThreadChannel threadChannel) { - Instant createdAt = threadChannel.getTimeCreated().toInstant(); String appliedTags = threadChannel.getAppliedTags() diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCommand.java index fd6b264e0c..459fc5a904 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCommand.java @@ -20,6 +20,7 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; +import org.togetherjava.tjbot.features.analytics.Metrics; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -56,6 +57,7 @@ public final class HelpThreadCommand extends SlashCommandAdapter { public static final String COMMAND_NAME = "help-thread"; private final HelpSystemHelper helper; + private final Metrics metrics; private final Map nameToSubcommand; private final Map> subcommandToCooldownCache; private final Map> subcommandToEventHandler; @@ -65,8 +67,9 @@ public final class HelpThreadCommand extends SlashCommandAdapter { * * @param config the config to use * @param helper the helper to use + * @param metrics to track events */ - public HelpThreadCommand(Config config, HelpSystemHelper helper) { + public HelpThreadCommand(Config config, HelpSystemHelper helper, Metrics metrics) { super(COMMAND_NAME, "Help thread specific commands", CommandVisibility.GUILD); OptionData categoryChoices = @@ -93,6 +96,7 @@ public HelpThreadCommand(Config config, HelpSystemHelper helper) { getData().addSubcommands(Subcommand.RESET_ACTIVITY.toSubcommandData()); this.helper = helper; + this.metrics = metrics; Supplier> createCooldownCache = () -> Caffeine.newBuilder() .maximumSize(1_000) @@ -158,6 +162,7 @@ private void changeCategory(SlashCommandInteractionEvent event, ThreadChannel he event.deferReply().queue(); refreshCooldownFor(Subcommand.CHANGE_CATEGORY, helpThread); + metrics.count("help-category-" + category); helper.changeChannelCategory(helpThread, category) .flatMap(_ -> sendCategoryChangedMessage(helpThread.getGuild(), event.getHook(), helpThread, category)) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index 246af1ffcf..c0f01b8245 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -22,6 +22,7 @@ import org.togetherjava.tjbot.features.EventReceiver; import org.togetherjava.tjbot.features.UserInteractionType; import org.togetherjava.tjbot.features.UserInteractor; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator; import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor; import org.togetherjava.tjbot.features.utils.LinkDetection; @@ -31,6 +32,7 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -46,6 +48,7 @@ public final class HelpThreadCreatedListener extends ListenerAdapter implements EventReceiver, UserInteractor { private static final Logger log = LoggerFactory.getLogger(HelpThreadCreatedListener.class); private final HelpSystemHelper helper; + private final Metrics metrics; private final Cache threadIdToCreatedAtCache = Caffeine.newBuilder() .maximumSize(1_000) @@ -58,9 +61,11 @@ public final class HelpThreadCreatedListener extends ListenerAdapter * Creates a new instance. * * @param helper to work with the help threads + * @param metrics to track events */ - public HelpThreadCreatedListener(HelpSystemHelper helper) { + public HelpThreadCreatedListener(HelpSystemHelper helper, Metrics metrics) { this.helper = helper; + this.metrics = metrics; } @Override @@ -88,6 +93,7 @@ private boolean wasThreadAlreadyHandled(long threadChannelId) { } private void handleHelpThreadCreated(ThreadChannel threadChannel) { + metrics.count("help-question_posted"); threadChannel.retrieveStartMessage().flatMap(message -> { registerThreadDataInDB(message, threadChannel); return sendHelperHeadsUp(threadChannel) @@ -121,10 +127,11 @@ private RestAction pinOriginalQuestion(Message message) { private RestAction sendHelperHeadsUp(ThreadChannel threadChannel) { String alternativeMention = "Helper"; - String helperMention = helper.getCategoryTagOfChannel(threadChannel) - .map(ForumTag::getName) - .flatMap(category -> helper.handleFindRoleForCategory(category, - threadChannel.getGuild())) + Optional forumTagName = + helper.getCategoryTagOfChannel(threadChannel).map(ForumTag::getName); + forumTagName.ifPresent(name -> metrics.count("help-category-" + name)); + String helperMention = forumTagName.flatMap( + category -> helper.handleFindRoleForCategory(category, threadChannel.getGuild())) .map(Role::getAsMention) .orElse(alternativeMention); @@ -233,6 +240,7 @@ private void handleDismiss(Member interactionUser, ThreadChannel channel, return; } + metrics.count("help-ai_dismiss"); RestAction deleteMessages = event.getMessage().delete(); for (String id : args) { deleteMessages = deleteMessages.and(channel.deleteMessageById(id)); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListener.java b/application/src/main/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListener.java index 083c3add07..ab9eedcc7b 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListener.java @@ -11,6 +11,7 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.features.MessageReceiverAdapter; +import org.togetherjava.tjbot.features.analytics.Metrics; import java.awt.Color; import java.util.List; @@ -26,13 +27,18 @@ */ public final class MediaOnlyChannelListener extends MessageReceiverAdapter { + private final Metrics metrics; + /** * Creates a MediaOnlyChannelListener to receive all message sent in MediaOnly channel. * * @param config to find MediaOnly channels + * @param metrics metrics */ - public MediaOnlyChannelListener(Config config) { + public MediaOnlyChannelListener(Config config, Metrics metrics) { super(Pattern.compile(config.getMediaOnlyChannelPattern())); + + this.metrics = metrics; } @Override @@ -47,6 +53,7 @@ public void onMessageReceived(MessageReceivedEvent event) { } if (messageHasNoMediaAttached(message)) { + metrics.count("media_only_channel-msg_deleted"); message.delete().flatMap(_ -> dmUser(message)).queue(_ -> { }, failure -> tempNotifyUserInChannel(message)); } @@ -87,6 +94,19 @@ private boolean hasMedia(List attachments, List dmUser(Message message) { + return message.getAuthor() + .openPrivateChannel() + .flatMap(channel -> channel.sendMessage(createNotificationMessage(message))); + } + + private void tempNotifyUserInChannel(Message message) { + message.getChannel() + .sendMessage(createNotificationMessage(message)) + .queue(notificationMessage -> notificationMessage.delete() + .queueAfter(1, TimeUnit.MINUTES)); + } + private MessageCreateData createNotificationMessage(Message message) { String originalMessageContent = message.getContentRaw(); @@ -100,17 +120,4 @@ private MessageCreateData createNotificationMessage(Message message) { .setEmbeds(originalMessageEmbed) .build(); } - - private RestAction dmUser(Message message) { - return message.getAuthor() - .openPrivateChannel() - .flatMap(channel -> channel.sendMessage(createNotificationMessage(message))); - } - - private void tempNotifyUserInChannel(Message message) { - message.getChannel() - .sendMessage(createNotificationMessage(message)) - .queue(notificationMessage -> notificationMessage.delete() - .queueAfter(1, TimeUnit.MINUTES)); - } } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/moderation/RejoinModerationRoleListener.java b/application/src/main/java/org/togetherjava/tjbot/features/moderation/RejoinModerationRoleListener.java index 7312a9104a..5f0f7c740f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/moderation/RejoinModerationRoleListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/moderation/RejoinModerationRoleListener.java @@ -11,6 +11,7 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.features.EventReceiver; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.logging.LogMarkers; import java.util.List; @@ -32,6 +33,7 @@ public final class RejoinModerationRoleListener implements EventReceiver { private final ModerationActionsStore actionsStore; private final List moderationRoles; + private final Metrics metrics; /** * Constructs an instance. @@ -39,9 +41,12 @@ public final class RejoinModerationRoleListener implements EventReceiver { * @param actionsStore used to store actions issued by this command and to retrieve whether a * user should be e.g. muted * @param config the config to use for this + * @param metrics to track events */ - public RejoinModerationRoleListener(ModerationActionsStore actionsStore, Config config) { + public RejoinModerationRoleListener(ModerationActionsStore actionsStore, Config config, + Metrics metrics) { this.actionsStore = actionsStore; + this.metrics = metrics; moderationRoles = List.of( new ModerationRole("mute", ModerationAction.MUTE, ModerationAction.UNMUTE, @@ -93,8 +98,10 @@ private boolean shouldApplyModerationRole(ModerationRole moderationRole, return false; } - private static void applyModerationRole(ModerationRole moderationRole, Member member) { + private void applyModerationRole(ModerationRole moderationRole, Member member) { Guild guild = member.getGuild(); + + metrics.count("mod-user_rejoined_reapplied_role"); logger.info(LogMarkers.SENSITIVE, "Reapplied existing {} to user '{}' ({}) in guild '{}' after rejoining.", moderationRole.actionName, member.getUser().getName(), member.getId(), diff --git a/application/src/main/java/org/togetherjava/tjbot/features/moderation/attachment/BlacklistedAttachmentListener.java b/application/src/main/java/org/togetherjava/tjbot/features/moderation/attachment/BlacklistedAttachmentListener.java index c7cd224f18..b043152e5e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/moderation/attachment/BlacklistedAttachmentListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/moderation/attachment/BlacklistedAttachmentListener.java @@ -11,6 +11,7 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.features.MessageReceiverAdapter; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.moderation.audit.ModAuditLogWriter; import org.togetherjava.tjbot.features.moderation.modmail.ModMailCommand; import org.togetherjava.tjbot.features.utils.MessageUtils; @@ -25,6 +26,7 @@ */ public final class BlacklistedAttachmentListener extends MessageReceiverAdapter { private final ModAuditLogWriter modAuditLogWriter; + private final Metrics metrics; private final List blacklistedFileExtensions; /** @@ -32,9 +34,12 @@ public final class BlacklistedAttachmentListener extends MessageReceiverAdapter * * @param config to find the blacklisted media attachments * @param modAuditLogWriter to inform the mods about the suspicious attachment + * @param metrics to track events */ - public BlacklistedAttachmentListener(Config config, ModAuditLogWriter modAuditLogWriter) { + public BlacklistedAttachmentListener(Config config, ModAuditLogWriter modAuditLogWriter, + Metrics metrics) { this.modAuditLogWriter = modAuditLogWriter; + this.metrics = metrics; blacklistedFileExtensions = config.getBlacklistedFileExtensions(); } @@ -49,6 +54,7 @@ public void onMessageReceived(MessageReceivedEvent event) { } private void handleBadMessage(Message message) { + metrics.count("blacklisted_attachment-deleted"); message.delete().flatMap(_ -> dmUser(message)).queue(_ -> warnMods(message)); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java b/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java index 094d05b5ce..a1314c5eb5 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java @@ -27,6 +27,7 @@ import org.togetherjava.tjbot.features.MessageReceiverAdapter; import org.togetherjava.tjbot.features.UserInteractionType; import org.togetherjava.tjbot.features.UserInteractor; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator; import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor; import org.togetherjava.tjbot.features.moderation.ModerationAction; @@ -75,6 +76,7 @@ public final class ScamBlocker extends MessageReceiverAdapter implements UserInt private final ScamHistoryStore scamHistoryStore; private final Predicate isRequiredRole; + private final Metrics metrics; private final ComponentIdInteractor componentIdInteractor; /** @@ -83,9 +85,10 @@ public final class ScamBlocker extends MessageReceiverAdapter implements UserInt * @param actionsStore to store quarantine actions in * @param scamHistoryStore to store and retrieve scam history from * @param config the config to use for this + * @param metrics to track events */ public ScamBlocker(ModerationActionsStore actionsStore, ScamHistoryStore scamHistoryStore, - Config config) { + Config config, Metrics metrics) { this.actionsStore = actionsStore; this.scamHistoryStore = scamHistoryStore; this.config = config; @@ -102,6 +105,7 @@ public ScamBlocker(ModerationActionsStore actionsStore, ScamHistoryStore scamHis isRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate(); + this.metrics = metrics; componentIdInteractor = new ComponentIdInteractor(getInteractionType(), getName()); } @@ -164,6 +168,7 @@ private void takeActionWasAlreadyReported(MessageReceivedEvent event) { } private void takeAction(MessageReceivedEvent event) { + metrics.count("scam-detected"); switch (mode) { case OFF -> throw new AssertionError( "The OFF-mode should be detected earlier already to prevent expensive computation"); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java index eac3b9ef31..d00bb54af7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/rss/RSSHandlerRoutine.java @@ -24,6 +24,7 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.records.RssFeedRecord; import org.togetherjava.tjbot.features.Routine; +import org.togetherjava.tjbot.features.analytics.Metrics; import javax.annotation.Nonnull; @@ -85,6 +86,7 @@ public final class RSSHandlerRoutine implements Routine { private final Map> targetChannelPatterns; private final int interval; private final Database database; + private final Metrics metrics; private final Cache circuitBreaker = Caffeine.newBuilder().expireAfterWrite(7, TimeUnit.DAYS).maximumSize(500).build(); @@ -99,11 +101,14 @@ public final class RSSHandlerRoutine implements Routine { * * @param config The configuration containing RSS feed details. * @param database The database for storing RSS feed data. + * @param metrics to track events */ - public RSSHandlerRoutine(Config config, Database database) { + public RSSHandlerRoutine(Config config, Database database, Metrics metrics) { this.config = config.getRSSFeedsConfig(); this.interval = this.config.pollIntervalInMinutes(); this.database = database; + this.metrics = metrics; + this.fallbackChannelPattern = Pattern.compile(this.config.fallbackChannelPattern()).asMatchPredicate(); isVideoLink = Pattern.compile(this.config.videoLinkPattern()).asMatchPredicate(); @@ -260,6 +265,7 @@ private Optional getLatestPostDateFromItems(List items, * @param feedConfig the RSS feed configuration */ private void postItem(List textChannels, Item rssItem, RSSFeed feedConfig) { + metrics.count("rss-item_posted"); MessageCreateData message = constructMessage(rssItem, feedConfig); textChannels.forEach(channel -> channel.sendMessage(message).queue()); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java b/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java index c87153428b..5dcdc0a81a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java @@ -459,10 +459,13 @@ public void onMessageContextInteraction(final MessageContextInteractionEvent eve logger.debug("Received message context command '{}' (#{}) on guild '{}'", name, event.getId(), event.getGuild()); - COMMAND_SERVICE.execute(() -> requireUserInteractor( - UserInteractionType.MESSAGE_CONTEXT_COMMAND.getPrefixedName(name), - MessageContextCommand.class) - .onMessageContext(event)); + COMMAND_SERVICE.execute(() -> { + MessageContextCommand userInteractor = requireUserInteractor( + UserInteractionType.MESSAGE_CONTEXT_COMMAND.getPrefixedName(name), + MessageContextCommand.class); + metrics.count("msg_ctx-" + name); + userInteractor.onMessageContext(event); + }); } @Override @@ -471,10 +474,13 @@ public void onUserContextInteraction(final UserContextInteractionEvent event) { logger.debug("Received user context command '{}' (#{}) on guild '{}'", name, event.getId(), event.getGuild()); - COMMAND_SERVICE.execute(() -> requireUserInteractor( - UserInteractionType.USER_CONTEXT_COMMAND.getPrefixedName(name), - UserContextCommand.class) - .onUserContext(event)); + COMMAND_SERVICE.execute(() -> { + UserContextCommand userInteractor = requireUserInteractor( + UserInteractionType.USER_CONTEXT_COMMAND.getPrefixedName(name), + UserContextCommand.class); + metrics.count("user_ctx-" + name); + userInteractor.onUserContext(event); + }); } /** diff --git a/application/src/main/java/org/togetherjava/tjbot/features/tags/TagCommand.java b/application/src/main/java/org/togetherjava/tjbot/features/tags/TagCommand.java index 1bf13983bf..7581c5c750 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/tags/TagCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/tags/TagCommand.java @@ -18,6 +18,7 @@ import org.togetherjava.tjbot.features.CommandVisibility; import org.togetherjava.tjbot.features.SlashCommandAdapter; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.utils.LinkDetection; import org.togetherjava.tjbot.features.utils.LinkPreview; import org.togetherjava.tjbot.features.utils.LinkPreviews; @@ -40,6 +41,7 @@ */ public final class TagCommand extends SlashCommandAdapter { private final TagSystem tagSystem; + private final Metrics metrics; private static final int MAX_SUGGESTIONS = 5; static final String ID_OPTION = "id"; static final String REPLY_TO_USER_OPTION = "reply-to"; @@ -48,11 +50,13 @@ public final class TagCommand extends SlashCommandAdapter { * Creates a new instance, using the given tag system as base. * * @param tagSystem the system providing the actual tag data + * @param metrics to track events */ - public TagCommand(TagSystem tagSystem) { + public TagCommand(TagSystem tagSystem, Metrics metrics) { super("tag", "Display a tags content", CommandVisibility.GUILD); this.tagSystem = tagSystem; + this.metrics = metrics; getData().addOptions( new OptionData(OptionType.STRING, ID_OPTION, "The id of the tag to display", true, @@ -87,6 +91,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event) { if (tagSystem.handleIsUnknownTag(id, event)) { return; } + metrics.count("tag-" + id); String tagContent = tagSystem.getTag(id).orElseThrow(); MessageEmbed contentEmbed = new EmbedBuilder().setDescription(tagContent) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/tophelper/TopHelpersAssignmentRoutine.java b/application/src/main/java/org/togetherjava/tjbot/features/tophelper/TopHelpersAssignmentRoutine.java index 76434af7e8..cb6449a24f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/tophelper/TopHelpersAssignmentRoutine.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/tophelper/TopHelpersAssignmentRoutine.java @@ -23,6 +23,7 @@ import org.togetherjava.tjbot.features.Routine; import org.togetherjava.tjbot.features.UserInteractionType; import org.togetherjava.tjbot.features.UserInteractor; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator; import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor; import org.togetherjava.tjbot.features.utils.Guilds; @@ -65,6 +66,7 @@ public final class TopHelpersAssignmentRoutine implements Routine, UserInteracto private final TopHelpersConfig config; private final TopHelpersService service; + private final Metrics metrics; private final Predicate roleNamePredicate; private final Predicate assignmentChannelNamePredicate; private final Predicate announcementChannelNamePredicate; @@ -75,10 +77,12 @@ public final class TopHelpersAssignmentRoutine implements Routine, UserInteracto * * @param config the config to use * @param service the service to use to compute Top Helpers + * @param metrics to track events */ - public TopHelpersAssignmentRoutine(Config config, TopHelpersService service) { + public TopHelpersAssignmentRoutine(Config config, TopHelpersService service, Metrics metrics) { this.config = config.getTopHelpers(); this.service = service; + this.metrics = metrics; roleNamePredicate = Pattern.compile(this.config.getRolePattern()).asMatchPredicate(); assignmentChannelNamePredicate = @@ -255,6 +259,9 @@ private void manageTopHelperRole(Collection currentTopHelpers, guild.addRoleToMember(UserSnowflake.fromId(userToAddRoleTo), topHelperRole).queue(); } + for (long topHelperUserId : selectedTopHelperIds) { + metrics.count("top_helper-" + topHelperUserId); + } reportRoleManageSuccess(event); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/voicechat/DynamicVoiceChat.java b/application/src/main/java/org/togetherjava/tjbot/features/voicechat/DynamicVoiceChat.java index 9294d6dcd0..03a001a604 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/voicechat/DynamicVoiceChat.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/voicechat/DynamicVoiceChat.java @@ -20,6 +20,7 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.DynamicVoiceChatConfig; import org.togetherjava.tjbot.features.VoiceReceiverAdapter; +import org.togetherjava.tjbot.features.analytics.Metrics; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -36,6 +37,7 @@ public final class DynamicVoiceChat extends VoiceReceiverAdapter { private final VoiceChatCleanupStrategy voiceChatCleanupStrategy; private final DynamicVoiceChatConfig dynamicVoiceChannelConfig; + private final Metrics metrics; private final Cache deletedChannels = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(); @@ -45,9 +47,11 @@ public final class DynamicVoiceChat extends VoiceReceiverAdapter { * * @param config the configurations needed for this feature. See: * {@link org.togetherjava.tjbot.config.DynamicVoiceChatConfig} + * @param metrics to track events */ - public DynamicVoiceChat(Config config) { + public DynamicVoiceChat(Config config, Metrics metrics) { this.dynamicVoiceChannelConfig = config.getDynamicVoiceChatConfig(); + this.metrics = metrics; this.voiceChatCleanupStrategy = new OldestVoiceChatCleanup(dynamicVoiceChannelConfig.cleanChannelsAmount(), @@ -128,9 +132,10 @@ private void createDynamicVoiceChannel(GuildVoiceUpdateEvent event, VoiceChannel moveMember(guild, member, newChannel); sendWarningEmbed(newChannel); }) - .queue(newChannel -> logger.trace("Successfully created {} voice channel.", - newChannel.getName()), - error -> logger.error("Failed to create dynamic voice channel", error)); + .queue(newChannel -> { + logger.trace("Successfully created {} voice channel.", newChannel.getName()); + metrics.count("dynamic_voice_channel-created"); + }, error -> logger.error("Failed to create dynamic voice channel", error)); } private void moveMember(Guild guild, Member member, AudioChannel channel) { diff --git a/application/src/test/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListenerTest.java b/application/src/test/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListenerTest.java index 89d36fcdf4..b814f0dab8 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListenerTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/mediaonly/MediaOnlyChannelListenerTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.jda.JdaTester; import java.util.List; @@ -34,7 +35,7 @@ void setUp() { Config config = mock(Config.class); when(config.getMediaOnlyChannelPattern()).thenReturn("any"); - mediaOnlyChannelListener = new MediaOnlyChannelListener(config); + mediaOnlyChannelListener = new MediaOnlyChannelListener(config, mock(Metrics.class)); } @Test diff --git a/application/src/test/java/org/togetherjava/tjbot/features/tags/TagCommandTest.java b/application/src/test/java/org/togetherjava/tjbot/features/tags/TagCommandTest.java index bb8f3cea5b..738c9177e8 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/tags/TagCommandTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/tags/TagCommandTest.java @@ -10,14 +10,14 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.Tags; import org.togetherjava.tjbot.features.SlashCommand; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.jda.JdaTester; import org.togetherjava.tjbot.jda.SlashCommandInteractionEventBuilder; import javax.annotation.Nullable; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; final class TagCommandTest { private TagSystem system; @@ -29,7 +29,7 @@ void setUp() { Database database = Database.createMemoryDatabase(Tags.TAGS); system = spy(new TagSystem(database)); jdaTester = new JdaTester(); - command = new TagCommand(system); + command = new TagCommand(system, mock(Metrics.class)); } private SlashCommandInteractionEvent triggerSlashCommand(String id, diff --git a/application/src/test/java/org/togetherjava/tjbot/features/tags/TagSystemTest.java b/application/src/test/java/org/togetherjava/tjbot/features/tags/TagSystemTest.java index 97b1b2ab3f..2f83607a3b 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/tags/TagSystemTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/tags/TagSystemTest.java @@ -6,6 +6,7 @@ import org.togetherjava.tjbot.db.Database; import org.togetherjava.tjbot.db.generated.tables.Tags; +import org.togetherjava.tjbot.features.analytics.Metrics; import org.togetherjava.tjbot.jda.JdaTester; import java.util.Optional; @@ -16,9 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; final class TagSystemTest { private TagSystem system; @@ -56,8 +55,9 @@ void createDeleteButton() { @Test void handleIsUnknownTag() { insertTagRaw("known", "foo"); - SlashCommandInteractionEvent event = - jdaTester.createSlashCommandInteractionEvent(new TagCommand(system)).build(); + SlashCommandInteractionEvent event = jdaTester + .createSlashCommandInteractionEvent(new TagCommand(system, mock(Metrics.class))) + .build(); assertFalse(system.handleIsUnknownTag("known", event)); verify(event, never()).reply(anyString()); From 70b5297b21087fbd478d24165258925b0fa83040 Mon Sep 17 00:00:00 2001 From: Suraj Kumar Date: Thu, 12 Mar 2026 17:30:38 +0000 Subject: [PATCH 23/23] docs: add pipeline badge to README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b1e65d81e4..39eb7edde9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Together-Java_TJ-Bot&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=Together-Java_TJ-Bot) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Together-Java_TJ-Bot&metric=security_rating)](https://sonarcloud.io/dashboard?id=Together-Java_TJ-Bot) +[![Pipeline](https://woodpecker.togetherjava.org/api/badges/1/status.svg)](https://woodpecker.togetherjava.org/repos/1) + + TJ-Bot is a Discord Bot used on the [Together Java](https://discord.com/invite/XXFUXzK) server. It is maintained by the community, anyone can contribute! ![bot says hello](https://i.imgur.com/FE1MJTV.png)