From e1dcd79de32f8ddb26f440e7e8ddacc6c779fcec Mon Sep 17 00:00:00 2001 From: christolis Date: Tue, 29 Oct 2024 21:47:10 +0200 Subject: [PATCH 1/5] feat(cool-messages): add configuration files --- application/config.json.template | 5 ++++ .../org/togetherjava/tjbot/config/Config.java | 15 +++++++++- .../tjbot/config/CoolMessagesBoardConfig.java | 28 +++++++++++++++++++ .../tjbot/features/MessageReceiver.java | 10 +++++++ .../features/MessageReceiverAdapter.java | 7 +++++ .../tjbot/features/system/BotCore.java | 9 ++++++ 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/togetherjava/tjbot/config/CoolMessagesBoardConfig.java diff --git a/application/config.json.template b/application/config.json.template index a32f8fd440..aa984fd06b 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -175,5 +175,10 @@ "fallbackChannelPattern": "java-news-and-changes", "pollIntervalInMinutes": 10 }, + "coolMessagesConfig": { + "minimumReactions": 5, + "boardChannelPattern": "quotes", + "reactionEmoji": "U+2B50" + }, "memberCountCategoryPattern": "Info" } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 79c04e6cad..51972751e4 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -48,6 +48,7 @@ public final class Config { private final RSSFeedsConfig rssFeedsConfig; private final String selectRolesChannelPattern; private final String memberCountCategoryPattern; + private final CoolMessagesBoardConfig coolMessagesConfig; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -100,7 +101,9 @@ private Config(@JsonProperty(value = "token", required = true) String token, required = true) FeatureBlacklistConfig featureBlacklistConfig, @JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig, @JsonProperty(value = "selectRolesChannelPattern", - required = true) String selectRolesChannelPattern) { + required = true) String selectRolesChannelPattern, + @JsonProperty(value = "coolMessagesConfig", + required = true) CoolMessagesBoardConfig coolMessagesConfig) { this.token = Objects.requireNonNull(token); this.githubApiKey = Objects.requireNonNull(githubApiKey); this.databasePath = Objects.requireNonNull(databasePath); @@ -135,6 +138,7 @@ private Config(@JsonProperty(value = "token", required = true) String token, this.featureBlacklistConfig = Objects.requireNonNull(featureBlacklistConfig); this.rssFeedsConfig = Objects.requireNonNull(rssFeedsConfig); this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern); + this.coolMessagesConfig = Objects.requireNonNull(coolMessagesConfig); } /** @@ -428,6 +432,15 @@ public String getSelectRolesChannelPattern() { return selectRolesChannelPattern; } + /** + * The configuration of the cool messages config. + * + * @return configuration of cool messages config + */ + public CoolMessagesBoardConfig getCoolMessagesConfig() { + return coolMessagesConfig; + } + /** * Gets the pattern matching the category that is used to display the total member count. * diff --git a/application/src/main/java/org/togetherjava/tjbot/config/CoolMessagesBoardConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/CoolMessagesBoardConfig.java new file mode 100644 index 0000000000..bf2d1b57be --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/config/CoolMessagesBoardConfig.java @@ -0,0 +1,28 @@ +package org.togetherjava.tjbot.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import java.util.Objects; + +/** + * Configuration for the cool messages board feature, see + * {@link org.togetherjava.tjbot.features.basic.CoolMessagesBoardManager}. + */ +@JsonRootName("coolMessagesConfig") +public record CoolMessagesBoardConfig( + @JsonProperty(value = "minimumReactions", required = true) int minimumReactions, + @JsonProperty(value = "boardChannelPattern", required = true) String boardChannelPattern, + @JsonProperty(value = "reactionEmoji", required = true) String reactionEmoji) { + + /** + * Creates a CoolMessagesBoardConfig. + * + * @param minimumReactions the minimum amount of reactions + * @param boardChannelPattern the pattern for the board channel + * @param reactionEmoji the emoji with which users should react to + */ + public CoolMessagesBoardConfig { + Objects.requireNonNull(boardChannelPattern); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/features/MessageReceiver.java b/application/src/main/java/org/togetherjava/tjbot/features/MessageReceiver.java index c5b6358434..18a1adb023 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/MessageReceiver.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/MessageReceiver.java @@ -3,6 +3,7 @@ import net.dv8tion.jda.api.events.message.MessageDeleteEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageUpdateEvent; +import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; import java.util.regex.Pattern; @@ -56,4 +57,13 @@ public interface MessageReceiver extends Feature { * message that was deleted */ void onMessageDeleted(MessageDeleteEvent event); + + /** + * Triggered by the core system whenever a new reaction was added to a message in a text channel + * of a guild the bot has been added to. + * + * @param event the event that triggered this, containing information about the corresponding + * reaction that was added + */ + void onMessageReactionAdd(MessageReactionAddEvent event); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/MessageReceiverAdapter.java b/application/src/main/java/org/togetherjava/tjbot/features/MessageReceiverAdapter.java index 05280c97ab..6ceee951b9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/MessageReceiverAdapter.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/MessageReceiverAdapter.java @@ -3,6 +3,7 @@ import net.dv8tion.jda.api.events.message.MessageDeleteEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageUpdateEvent; +import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; import java.util.regex.Pattern; @@ -57,4 +58,10 @@ public void onMessageUpdated(MessageUpdateEvent event) { public void onMessageDeleted(MessageDeleteEvent event) { // Adapter does not react by default, subclasses may change this behavior } + + @SuppressWarnings("NoopMethodInAbstractClass") + @Override + public void onMessageReactionAdd(MessageReactionAddEvent event) { + // Adapter does not react by default, subclasses may change this behavior + } } 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 869e978a17..bbf71c54f6 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 @@ -13,6 +13,7 @@ import net.dv8tion.jda.api.events.message.MessageDeleteEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageUpdateEvent; +import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; import net.dv8tion.jda.api.interactions.components.ComponentInteraction; @@ -238,6 +239,14 @@ public void onMessageDelete(final MessageDeleteEvent event) { } } + @Override + public void onMessageReactionAdd(final MessageReactionAddEvent event) { + if (event.isFromGuild()) { + getMessageReceiversSubscribedTo(event.getChannel()) + .forEach(messageReceiver -> messageReceiver.onMessageReactionAdd(event)); + } + } + private Stream getMessageReceiversSubscribedTo(Channel channel) { String channelName = channel.getName(); return channelNameToMessageReceiver.entrySet() From 26e7811af06d16b9b98b0d7d82c22c9385c0b935 Mon Sep 17 00:00:00 2001 From: christolis Date: Tue, 29 Oct 2024 21:48:36 +0200 Subject: [PATCH 2/5] feat(cool-messages): add primary logic --- .../togetherjava/tjbot/features/Features.java | 2 + .../basic/CoolMessagesBoardManager.java | 147 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.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 99241f5689..371e5e379d 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.basic.CoolMessagesBoardManager; import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine; import org.togetherjava.tjbot.features.basic.PingCommand; import org.togetherjava.tjbot.features.basic.RoleSelectCommand; @@ -154,6 +155,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new CodeMessageManualDetection(codeMessageHandler)); features.add(new SlashCommandEducator()); features.add(new PinnedNotificationRemover(config)); + features.add(new CoolMessagesBoardManager(config)); // Event receivers features.add(new RejoinModerationRoleListener(actionsStore, config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java new file mode 100644 index 0000000000..6ca62f8748 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java @@ -0,0 +1,147 @@ +package org.togetherjava.tjbot.features.basic; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.MessageReaction; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; +import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.config.CoolMessagesBoardConfig; +import org.togetherjava.tjbot.features.MessageReceiverAdapter; + +import java.awt.Color; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Manager for the cool messages board. It appends highly-voted text messages to a separate channel + * where members of the guild can see a list of all of them. + */ +public final class CoolMessagesBoardManager extends MessageReceiverAdapter { + + private static final Logger logger = LoggerFactory.getLogger(CoolMessagesBoardManager.class); + private final Emoji coolEmoji; + private final Predicate boardChannelNamePredicate; + private final CoolMessagesBoardConfig config; + + /** + * Constructs a new instance of CoolMessagesBoardManager. + * + * @param config the configuration containing settings specific to the cool messages board, + * including the reaction emoji and the pattern to match board channel names + */ + public CoolMessagesBoardManager(Config config) { + this.config = config.getCoolMessagesConfig(); + this.coolEmoji = Emoji.fromUnicode(this.config.reactionEmoji()); + + boardChannelNamePredicate = + Pattern.compile(this.config.boardChannelPattern()).asMatchPredicate(); + } + + @Override + public void onMessageReactionAdd(MessageReactionAddEvent event) { + final MessageReaction messageReaction = event.getReaction(); + int originalReactionsCount = messageReaction.hasCount() ? messageReaction.getCount() : 0; + boolean isCoolEmoji = messageReaction.getEmoji().equals(coolEmoji); + long guildId = event.getGuild().getIdLong(); + Optional boardChannel = getBoardChannel(event.getJDA(), guildId); + + if (boardChannel.isEmpty()) { + logger.warn( + "Could not find board channel with pattern '{}' in server with ID '{}'. Skipping reaction handling...", + this.config.boardChannelPattern(), guildId); + return; + } + + // If the bot has already reacted to this message, then this means that + // the message has been quoted to the cool messages board, so skip it. + if (hasBotReacted(event.getJDA(), messageReaction)) { + return; + } + + final int newReactionsCount = originalReactionsCount + 1; + if (isCoolEmoji && newReactionsCount >= config.minimumReactions()) { + event.retrieveMessage() + .queue(message -> message.addReaction(coolEmoji) + .flatMap(v -> insertCoolMessage(boardChannel.get(), message)) + .queue(), + e -> logger.warn("Tried to retrieve cool message but got: {}", + e.getMessage())); + } + } + + /** + * Gets the board text channel where the quotes go to, wrapped in an optional. + * + * @param jda the JDA + * @param guildId the guild ID + * @return the board text channel + */ + private Optional getBoardChannel(JDA jda, long guildId) { + return jda.getGuildById(guildId) + .getTextChannelCache() + .stream() + .filter(channel -> boardChannelNamePredicate.test(channel.getName())) + .findAny(); + } + + /** + * Inserts a message to the specified text channel + * + * @return a {@link MessageCreateAction} of the call to make + */ + private static MessageCreateAction insertCoolMessage(TextChannel boardChannel, + Message message) { + return boardChannel.sendMessageEmbeds(Collections.singleton(createQuoteEmbed(message))); + } + + /** + * Wraps a text message into a properly formatted quote message used for the board text channel. + */ + private static MessageEmbed createQuoteEmbed(Message message) { + final User author = message.getAuthor(); + EmbedBuilder embedBuilder = new EmbedBuilder(); + + // If the message contains image(s), include the first one + var firstImageAttachment = message.getAttachments() + .stream() + .parallel() + .filter(Message.Attachment::isImage) + .findAny() + .orElse(null); + + if (firstImageAttachment != null) { + embedBuilder.setThumbnail(firstImageAttachment.getUrl()); + } + + return embedBuilder.setDescription(message.getContentDisplay()) + .appendDescription("%n%n[Jump to Message](%s)".formatted(message.getJumpUrl())) + .setColor(Color.orange) + .setAuthor(author.getName(), null, author.getAvatarUrl()) + .setTimestamp(message.getTimeCreated()) + .build(); + } + + /** + * Checks a {@link MessageReaction} to see if the bot has reacted to it. + */ + private boolean hasBotReacted(JDA jda, MessageReaction messageReaction) { + if (!coolEmoji.equals(messageReaction.getEmoji())) { + return false; + } + + return messageReaction.retrieveUsers() + .parallelStream() + .anyMatch(user -> jda.getSelfUser().getIdLong() == user.getIdLong()); + } +} From fb4fb5d7ffaaba941671b980de4cb04e298ccbd2 Mon Sep 17 00:00:00 2001 From: christolis Date: Tue, 29 Oct 2024 21:49:12 +0200 Subject: [PATCH 3/5] feat(cool-message): forward messages instead of using embed For this feature, the version of JDA had to be bumped to 5.1.2 --- .../basic/CoolMessagesBoardManager.java | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java index 6ca62f8748..c0168f7b27 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java @@ -1,11 +1,8 @@ package org.togetherjava.tjbot.features.basic; -import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.MessageReaction; -import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; @@ -17,8 +14,6 @@ import org.togetherjava.tjbot.config.CoolMessagesBoardConfig; import org.togetherjava.tjbot.features.MessageReceiverAdapter; -import java.awt.Color; -import java.util.Collections; import java.util.Optional; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -102,34 +97,7 @@ private Optional getBoardChannel(JDA jda, long guildId) { */ private static MessageCreateAction insertCoolMessage(TextChannel boardChannel, Message message) { - return boardChannel.sendMessageEmbeds(Collections.singleton(createQuoteEmbed(message))); - } - - /** - * Wraps a text message into a properly formatted quote message used for the board text channel. - */ - private static MessageEmbed createQuoteEmbed(Message message) { - final User author = message.getAuthor(); - EmbedBuilder embedBuilder = new EmbedBuilder(); - - // If the message contains image(s), include the first one - var firstImageAttachment = message.getAttachments() - .stream() - .parallel() - .filter(Message.Attachment::isImage) - .findAny() - .orElse(null); - - if (firstImageAttachment != null) { - embedBuilder.setThumbnail(firstImageAttachment.getUrl()); - } - - return embedBuilder.setDescription(message.getContentDisplay()) - .appendDescription("%n%n[Jump to Message](%s)".formatted(message.getJumpUrl())) - .setColor(Color.orange) - .setAuthor(author.getName(), null, author.getAvatarUrl()) - .setTimestamp(message.getTimeCreated()) - .build(); + return message.forwardTo(boardChannel); } /** From 1ade40902b4acdfa185cd8959f1d2df1bd567a86 Mon Sep 17 00:00:00 2001 From: Surya Tejess Date: Sat, 28 Jun 2025 19:40:10 +0530 Subject: [PATCH 4/5] refactor: code review addressed by Zabuzard * minimumReactions-5, star symbol instead of encoding * requests changes by zabuzard except for moving getBoardChannel down and markMessageAsProcessed * Following JavaDocs guidelines of making the first letter capital * code refactoring * refactor: use correct method for reactionsCount It turns out that for each event fired, every *single* damn time, messageReaction.hasCount() would always return false. No matter what. Terrible documentation from JDA's side. As a result, because of the ternary operator: messageReaction.hasCount() ? messageReaction.getCount() + 1 : 1 the result of `reactionsCount` would always end up holding the value of one. In the following changes, we use `messageReaction.retrieveUsers()` to get a list of the people reacted, get a `Stream` from that and get its count. Much more reliable this way and it also happens to be more readable. Signed-off-by: Chris Sdogkos Co-authored-by: Chris Sdogkos Co-authored-by: Surya Tejess <74978874+suryatejess@users.noreply.github.com> --- application/config.json.template | 4 +- .../org/togetherjava/tjbot/config/Config.java | 6 +- ...BoardConfig.java => QuoteBoardConfig.java} | 9 +-- .../togetherjava/tjbot/features/Features.java | 4 +- ...dManager.java => QuoteBoardForwarder.java} | 76 +++++++++---------- 5 files changed, 48 insertions(+), 51 deletions(-) rename application/src/main/java/org/togetherjava/tjbot/config/{CoolMessagesBoardConfig.java => QuoteBoardConfig.java} (75%) rename application/src/main/java/org/togetherjava/tjbot/features/basic/{CoolMessagesBoardManager.java => QuoteBoardForwarder.java} (50%) diff --git a/application/config.json.template b/application/config.json.template index aa984fd06b..0fcc50cefb 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -176,9 +176,9 @@ "pollIntervalInMinutes": 10 }, "coolMessagesConfig": { - "minimumReactions": 5, + "minimumReactions": 2, "boardChannelPattern": "quotes", - "reactionEmoji": "U+2B50" + "reactionEmoji": "⭐" }, "memberCountCategoryPattern": "Info" } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 51972751e4..d5a36424f2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -48,7 +48,7 @@ public final class Config { private final RSSFeedsConfig rssFeedsConfig; private final String selectRolesChannelPattern; private final String memberCountCategoryPattern; - private final CoolMessagesBoardConfig coolMessagesConfig; + private final QuoteBoardConfig coolMessagesConfig; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -103,7 +103,7 @@ private Config(@JsonProperty(value = "token", required = true) String token, @JsonProperty(value = "selectRolesChannelPattern", required = true) String selectRolesChannelPattern, @JsonProperty(value = "coolMessagesConfig", - required = true) CoolMessagesBoardConfig coolMessagesConfig) { + required = true) QuoteBoardConfig coolMessagesConfig) { this.token = Objects.requireNonNull(token); this.githubApiKey = Objects.requireNonNull(githubApiKey); this.databasePath = Objects.requireNonNull(databasePath); @@ -437,7 +437,7 @@ public String getSelectRolesChannelPattern() { * * @return configuration of cool messages config */ - public CoolMessagesBoardConfig getCoolMessagesConfig() { + public QuoteBoardConfig getCoolMessagesConfig() { return coolMessagesConfig; } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/CoolMessagesBoardConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/QuoteBoardConfig.java similarity index 75% rename from application/src/main/java/org/togetherjava/tjbot/config/CoolMessagesBoardConfig.java rename to application/src/main/java/org/togetherjava/tjbot/config/QuoteBoardConfig.java index bf2d1b57be..d88420c8c1 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/CoolMessagesBoardConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/QuoteBoardConfig.java @@ -6,23 +6,22 @@ import java.util.Objects; /** - * Configuration for the cool messages board feature, see - * {@link org.togetherjava.tjbot.features.basic.CoolMessagesBoardManager}. + * Configuration for the cool messages board feature, see {@link ``QuoteBoardForwarder``}. */ @JsonRootName("coolMessagesConfig") -public record CoolMessagesBoardConfig( +public record QuoteBoardConfig( @JsonProperty(value = "minimumReactions", required = true) int minimumReactions, @JsonProperty(value = "boardChannelPattern", required = true) String boardChannelPattern, @JsonProperty(value = "reactionEmoji", required = true) String reactionEmoji) { /** - * Creates a CoolMessagesBoardConfig. + * Creates a QuoteBoardConfig. * * @param minimumReactions the minimum amount of reactions * @param boardChannelPattern the pattern for the board channel * @param reactionEmoji the emoji with which users should react to */ - public CoolMessagesBoardConfig { + public QuoteBoardConfig { Objects.requireNonNull(boardChannelPattern); } } 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 371e5e379d..831ea9e30e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/Features.java @@ -6,9 +6,9 @@ import org.togetherjava.tjbot.config.FeatureBlacklist; import org.togetherjava.tjbot.config.FeatureBlacklistConfig; import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.features.basic.CoolMessagesBoardManager; import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine; 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; @@ -155,7 +155,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new CodeMessageManualDetection(codeMessageHandler)); features.add(new SlashCommandEducator()); features.add(new PinnedNotificationRemover(config)); - features.add(new CoolMessagesBoardManager(config)); + features.add(new QuoteBoardForwarder(config)); // Event receivers features.add(new RejoinModerationRoleListener(actionsStore, config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/QuoteBoardForwarder.java similarity index 50% rename from application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java rename to application/src/main/java/org/togetherjava/tjbot/features/basic/QuoteBoardForwarder.java index c0168f7b27..77577a8665 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/QuoteBoardForwarder.java @@ -6,12 +6,12 @@ import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; -import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; +import net.dv8tion.jda.api.requests.RestAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.togetherjava.tjbot.config.Config; -import org.togetherjava.tjbot.config.CoolMessagesBoardConfig; +import org.togetherjava.tjbot.config.QuoteBoardConfig; import org.togetherjava.tjbot.features.MessageReceiverAdapter; import java.util.Optional; @@ -20,61 +20,63 @@ /** * Manager for the cool messages board. It appends highly-voted text messages to a separate channel - * where members of the guild can see a list of all of them. + * where members of the guild can see a list of all of them. User reacts to a message with a + * configured emoji it then forwards this message to the configured quote board channel */ -public final class CoolMessagesBoardManager extends MessageReceiverAdapter { +public final class QuoteBoardForwarder extends MessageReceiverAdapter { - private static final Logger logger = LoggerFactory.getLogger(CoolMessagesBoardManager.class); - private final Emoji coolEmoji; - private final Predicate boardChannelNamePredicate; - private final CoolMessagesBoardConfig config; + private static final Logger logger = LoggerFactory.getLogger(QuoteBoardForwarder.class); + private final Emoji triggerReaction; + private final Predicate isQuoteBoardChannelName; + private final QuoteBoardConfig config; /** - * Constructs a new instance of CoolMessagesBoardManager. + * Constructs a new instance of QuoteBoardForwarder. * * @param config the configuration containing settings specific to the cool messages board, * including the reaction emoji and the pattern to match board channel names */ - public CoolMessagesBoardManager(Config config) { + public QuoteBoardForwarder(Config config) { this.config = config.getCoolMessagesConfig(); - this.coolEmoji = Emoji.fromUnicode(this.config.reactionEmoji()); + this.triggerReaction = Emoji.fromUnicode(this.config.reactionEmoji()); - boardChannelNamePredicate = + isQuoteBoardChannelName = Pattern.compile(this.config.boardChannelPattern()).asMatchPredicate(); } @Override public void onMessageReactionAdd(MessageReactionAddEvent event) { final MessageReaction messageReaction = event.getReaction(); - int originalReactionsCount = messageReaction.hasCount() ? messageReaction.getCount() : 0; - boolean isCoolEmoji = messageReaction.getEmoji().equals(coolEmoji); + boolean isCoolEmoji = messageReaction.getEmoji().equals(triggerReaction); long guildId = event.getGuild().getIdLong(); - Optional boardChannel = getBoardChannel(event.getJDA(), guildId); - if (boardChannel.isEmpty()) { - logger.warn( - "Could not find board channel with pattern '{}' in server with ID '{}'. Skipping reaction handling...", - this.config.boardChannelPattern(), guildId); + if (hasAlreadyForwardedMessage(event.getJDA(), messageReaction)) { return; } - // If the bot has already reacted to this message, then this means that - // the message has been quoted to the cool messages board, so skip it. - if (hasBotReacted(event.getJDA(), messageReaction)) { - return; - } + final int reactionsCount = (int) messageReaction.retrieveUsers().stream().count(); + if (isCoolEmoji && reactionsCount >= config.minimumReactions()) { + Optional boardChannel = findQuoteBoardChannel(event.getJDA(), guildId); + + if (boardChannel.isEmpty()) { + logger.warn( + "Could not find board channel with pattern '{}' in server with ID '{}'. Skipping reaction handling...", + this.config.boardChannelPattern(), guildId); + return; + } - final int newReactionsCount = originalReactionsCount + 1; - if (isCoolEmoji && newReactionsCount >= config.minimumReactions()) { event.retrieveMessage() - .queue(message -> message.addReaction(coolEmoji) - .flatMap(v -> insertCoolMessage(boardChannel.get(), message)) - .queue(), - e -> logger.warn("Tried to retrieve cool message but got: {}", - e.getMessage())); + .queue(message -> markAsProcessed(message).flatMap(v -> message + .forwardTo(boardChannel.orElseThrow())).queue(), e -> logger.warn( + "Unknown error while attempting to retrieve and forward message for quote-board, message is ignored.", + e)); } } + private RestAction markAsProcessed(Message message) { + return message.addReaction(triggerReaction); + } + /** * Gets the board text channel where the quotes go to, wrapped in an optional. * @@ -82,11 +84,11 @@ public void onMessageReactionAdd(MessageReactionAddEvent event) { * @param guildId the guild ID * @return the board text channel */ - private Optional getBoardChannel(JDA jda, long guildId) { + private Optional findQuoteBoardChannel(JDA jda, long guildId) { return jda.getGuildById(guildId) .getTextChannelCache() .stream() - .filter(channel -> boardChannelNamePredicate.test(channel.getName())) + .filter(channel -> isQuoteBoardChannelName.test(channel.getName())) .findAny(); } @@ -95,16 +97,12 @@ private Optional getBoardChannel(JDA jda, long guildId) { * * @return a {@link MessageCreateAction} of the call to make */ - private static MessageCreateAction insertCoolMessage(TextChannel boardChannel, - Message message) { - return message.forwardTo(boardChannel); - } /** * Checks a {@link MessageReaction} to see if the bot has reacted to it. */ - private boolean hasBotReacted(JDA jda, MessageReaction messageReaction) { - if (!coolEmoji.equals(messageReaction.getEmoji())) { + private boolean hasAlreadyForwardedMessage(JDA jda, MessageReaction messageReaction) { + if (!triggerReaction.equals(messageReaction.getEmoji())) { return false; } From a6085db077cafa44885753dade4f9c488526789d Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Sat, 28 Jun 2025 18:44:39 +0300 Subject: [PATCH 5/5] doc(QuoteBoardForwarder.java): improve JavaDoc Since 1ade40902b4 (refactor: code review addressed by Zabuzard, 2025-06-28) primarily contains a generaly vague JavaDoc describing what the `QuoteBoardForwarder.java` class is doing, a more descriptive one replaces it. Signed-off-by: Chris Sdogkos --- .../tjbot/features/basic/QuoteBoardForwarder.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/QuoteBoardForwarder.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/QuoteBoardForwarder.java index 77577a8665..708a79bf72 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/QuoteBoardForwarder.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/QuoteBoardForwarder.java @@ -19,9 +19,17 @@ import java.util.regex.Pattern; /** - * Manager for the cool messages board. It appends highly-voted text messages to a separate channel - * where members of the guild can see a list of all of them. User reacts to a message with a - * configured emoji it then forwards this message to the configured quote board channel + * Listens for reaction-add events and turns popular messages into “quotes”. + *

+ * When someone reacts to a message with the configured emoji, the listener counts how many users + * have used that same emoji. If the total meets or exceeds the minimum threshold and the bot has + * not processed the message before, it copies (forwards) the message to the first text channel + * whose name matches the configured quote-board pattern, then reacts to the original message itself + * to mark it as handled (and to not let people spam react a message and give a way to the bot to + * know that a message has been quoted before). + *

+ * Key points: - Trigger emoji, minimum vote count and quote-board channel pattern are supplied via + * {@code QuoteBoardConfig}. */ public final class QuoteBoardForwarder extends MessageReceiverAdapter {