From 93891567dad85e5c6a2620db95e7712c3f32119f Mon Sep 17 00:00:00 2001 From: christolis Date: Tue, 29 Oct 2024 21:48:36 +0200 Subject: [PATCH] feat(cool-messages): add primary logic --- .../togetherjava/tjbot/features/Features.java | 2 + .../basic/CoolMessagesBoardManager.java | 141 ++++++++++++++++++ 2 files changed, 143 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 893adbc00f..e8cbfd84ef 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; @@ -150,6 +151,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..86a1a42e18 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/CoolMessagesBoardManager.java @@ -0,0 +1,141 @@ +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 Emoji coolEmoji; + private final Predicate boardChannelNamePredicate; + private final CoolMessagesBoardConfig config; + + 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()); + } +}