From 05d5679f0cce7f06d6ab3f4c4481cfce043de105 Mon Sep 17 00:00:00 2001 From: Zabuzard Date: Tue, 21 Jul 2026 17:36:57 +0200 Subject: [PATCH] Redesigned QuoteBoardForwarded slightly to fix race condition --- .../features/basic/QuoteBoardForwarder.java | 141 +++++++++++------- 1 file changed, 91 insertions(+), 50 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 a6cee17024..2de390e5f6 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 @@ -4,9 +4,10 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageReaction; +import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.emoji.Emoji; -import net.dv8tion.jda.api.entities.emoji.EmojiUnion; import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent; import net.dv8tion.jda.api.requests.RestAction; import org.slf4j.Logger; @@ -15,9 +16,13 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.QuoteBoardConfig; import org.togetherjava.tjbot.features.MessageReceiverAdapter; +import org.togetherjava.tjbot.features.Routine; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -34,13 +39,20 @@ * Key points: - Trigger emoji, minimum vote count and quote-board channel pattern are supplied via * {@code QuoteBoardConfig}. */ -public final class QuoteBoardForwarder extends MessageReceiverAdapter { +public final class QuoteBoardForwarder extends MessageReceiverAdapter implements Routine { private static final Logger logger = LoggerFactory.getLogger(QuoteBoardForwarder.class); - private final Emoji botEmoji; + + private record ReactedMessage(long guildId, long channelId, long messageId) { + } + + private final Emoji messageForwardedEmojiMarker; private final Predicate isQuoteBoardChannelName; private final QuoteBoardConfig config; + private final Object reactedMessagesLock = new Object(); + private Set reactedMessages = new HashSet<>(); + /** * Constructs a new instance of QuoteBoardForwarder. * @@ -49,7 +61,7 @@ public final class QuoteBoardForwarder extends MessageReceiverAdapter { */ public QuoteBoardForwarder(Config config) { this.config = config.getQuoteBoardConfig(); - this.botEmoji = Emoji.fromUnicode(this.config.botEmoji()); + this.messageForwardedEmojiMarker = Emoji.fromUnicode(this.config.botEmoji()); this.isQuoteBoardChannelName = Pattern.compile(this.config.channel()).asMatchPredicate(); } @@ -64,59 +76,76 @@ public void onMessageReactionAdd(MessageReactionAddEvent event) { return; } - final long guildId = event.getGuild().getIdLong(); + if (event.getChannelType() != ChannelType.TEXT) { + logger.debug("Skipping reaction as only text-channels are supported"); + return; + } + + synchronized (reactedMessagesLock) { + reactedMessages.add(new ReactedMessage(event.getGuild().getIdLong(), + event.getChannel().getIdLong(), event.getMessageIdLong())); + } + } + + @Override + public Schedule createSchedule() { + return new Schedule(ScheduleMode.FIXED_DELAY, 1, 1, TimeUnit.MINUTES); + } + + @Override + public void runRoutine(JDA jda) { + Set messagesToProcess; + synchronized (reactedMessagesLock) { + messagesToProcess = reactedMessages; + reactedMessages = new HashSet<>(); + } - Optional boardChannelOptional = findQuoteBoardChannel(event.getJDA(), guildId); + messagesToProcess.forEach(message -> { + try { + processMessage(jda, message); + } catch (Exception e) { + logger.warn( + "Failed to process message ({}) for potentially forwarding it to the quote-board.", + message, e); + } + }); + } + private void processMessage(JDA jda, ReactedMessage reactedMessage) { + Optional boardChannelOptional = + findQuoteBoardChannel(jda, reactedMessage.guildId); if (boardChannelOptional.isEmpty()) { logger.warn( "Could not find board channel with pattern '{}' in server with ID '{}'. Skipping reaction handling...", - this.config.channel(), guildId); + config.channel(), reactedMessage.guildId); return; } TextChannel boardChannel = boardChannelOptional.orElseThrow(); - - if (boardChannel.getId().equals(event.getChannel().getId())) { - logger.debug("Someone tried to react with the react emoji to the quotes channel."); + if (boardChannel.getIdLong() == reactedMessage.channelId) { + logger.debug( + "Someone tried to react with the react emoji to the quotes channel, ignoring."); return; } - event.retrieveMessage().queue(message -> { - if (hasAlreadyForwardedMessage(message)) { - logger.debug("Message has already been forwarded by the bot. Skipping."); - return; - } - - float emojiScore = calculateMessageScore(message.getReactions()); - - if (emojiScore < config.minimumScoreToTrigger()) { - return; - } - - logger.debug("Attempting to forward message to quote board channel: {}", - boardChannel.getName()); - - markAsProcessed(message).flatMap(_ -> message.forwardTo(boardChannel)) - .queue(_ -> logger.debug("Message forwarded to quote board channel: {}", - boardChannel.getName()), - e -> logger.warn( - "Unknown error while attempting to retrieve and forward message for quote-board, message is ignored.", - e)); - }); + jda.getGuildById(reactedMessage.guildId) + .getTextChannelById(reactedMessage.channelId) + .retrieveMessageById(reactedMessage.messageId) + .queue(message -> { + if (hasAlreadyForwardedMessage(message)) { + logger.debug("Message has already been forwarded by the bot. Skipping."); + return; + } + + double emojiScore = calculateMessageScore(message.getReactions()); + if (emojiScore < config.minimumScoreToTrigger()) { + return; + } + + forwardMessage(message, boardChannel); + }); } - private RestAction markAsProcessed(Message message) { - return message.addReaction(botEmoji); - } - - /** - * 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 findQuoteBoardChannel(JDA jda, long guildId) { Guild guild = jda.getGuildById(guildId); @@ -139,26 +168,38 @@ private Optional findQuoteBoardChannel(JDA jda, long guildId) { return matchingChannels.stream().findFirst(); } - /** - * Checks whether the bot has already reacted to the given message with its marker emoji. - */ private boolean hasAlreadyForwardedMessage(Message message) { return message.getReactions() .stream() - .filter(reaction -> botEmoji.equals(reaction.getEmoji())) + .filter(reaction -> messageForwardedEmojiMarker.equals(reaction.getEmoji())) .anyMatch(MessageReaction::isSelf); } - private float calculateMessageScore(List reactions) { - return (float) reactions.stream() + private double calculateMessageScore(List reactions) { + return reactions.stream() .mapToDouble(reaction -> reaction.getCount() * getEmojiScore(reaction.getEmoji())) .sum(); } - private float getEmojiScore(EmojiUnion emoji) { + private float getEmojiScore(Emoji emoji) { float defaultScore = config.defaultEmojiScore(); String reactionCode = emoji.getAsReactionCode(); return config.emojiScores().getOrDefault(reactionCode, defaultScore); } + + private void forwardMessage(Message message, MessageChannel boardChannel) { + logger.debug("Attempting to forward message to quote board channel: {}", + boardChannel.getName()); + + markMessageAsForwarded(message).flatMap(_ -> message.forwardTo(boardChannel)) + .queue(_ -> logger.debug("Message forwarded to quote board channel: {}", boardChannel + .getName()), e -> logger.warn( + "Unknown error while attempting to retrieve and forward message for quote-board, message is ignored.", + e)); + } + + private RestAction markMessageAsForwarded(Message message) { + return message.addReaction(messageForwardedEmojiMarker); + } }