Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions application/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,10 @@
"fallbackChannelPattern": "java-news-and-changes",
"pollIntervalInMinutes": 10
},
"coolMessagesConfig": {
Comment thread
christolis marked this conversation as resolved.
Outdated
"minimumReactions": 2,
Comment thread
christolis marked this conversation as resolved.
Outdated
"boardChannelPattern": "quotes",
Comment thread
christolis marked this conversation as resolved.
Outdated
"reactionEmoji": "⭐"
Comment thread
christolis marked this conversation as resolved.
},
"memberCountCategoryPattern": "Info"
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public final class Config {
private final RSSFeedsConfig rssFeedsConfig;
private final String selectRolesChannelPattern;
private final String memberCountCategoryPattern;
private final QuoteBoardConfig coolMessagesConfig;
Comment thread
christolis marked this conversation as resolved.
Outdated

@SuppressWarnings("ConstructorWithTooManyParameters")
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
Expand Down Expand Up @@ -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) QuoteBoardConfig coolMessagesConfig) {
Comment thread
christolis marked this conversation as resolved.
Outdated
this.token = Objects.requireNonNull(token);
this.githubApiKey = Objects.requireNonNull(githubApiKey);
this.databasePath = Objects.requireNonNull(databasePath);
Expand Down Expand Up @@ -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);
Comment thread
christolis marked this conversation as resolved.
Outdated
}

/**
Expand Down Expand Up @@ -428,6 +432,15 @@ public String getSelectRolesChannelPattern() {
return selectRolesChannelPattern;
}

/**
* The configuration of the cool messages config.
*
* @return configuration of cool messages config
*/
public QuoteBoardConfig getCoolMessagesConfig() {
return coolMessagesConfig;
}
Comment thread
christolis marked this conversation as resolved.

/**
* Gets the pattern matching the category that is used to display the total member count.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 ``QuoteBoardForwarder``}.
Comment thread
christolis marked this conversation as resolved.
Outdated
*/
@JsonRootName("coolMessagesConfig")
Comment thread
christolis marked this conversation as resolved.
Outdated
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 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 QuoteBoardConfig {
Comment thread
christolis marked this conversation as resolved.
Objects.requireNonNull(boardChannelPattern);
Comment thread
christolis marked this conversation as resolved.
Outdated
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine;
import org.togetherjava.tjbot.features.basic.PingCommand;
import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder;
import org.togetherjava.tjbot.features.basic.RoleSelectCommand;
import org.togetherjava.tjbot.features.basic.SlashCommandEducator;
import org.togetherjava.tjbot.features.basic.SuggestionsUpDownVoter;
Expand Down Expand Up @@ -154,6 +155,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(new CodeMessageManualDetection(codeMessageHandler));
features.add(new SlashCommandEducator());
features.add(new PinnedNotificationRemover(config));
features.add(new QuoteBoardForwarder(config));

// Event receivers
features.add(new RejoinModerationRoleListener(actionsStore, config));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.togetherjava.tjbot.features.basic;

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageReaction;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.togetherjava.tjbot.config.Config;
import org.togetherjava.tjbot.config.QuoteBoardConfig;
import org.togetherjava.tjbot.features.MessageReceiverAdapter;

import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;

/**
* Listens for reaction-add events and turns popular messages into “quotes”.
Comment thread
christolis marked this conversation as resolved.
Outdated
* <p>
* 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).
* <p>
* Key points: - Trigger emoji, minimum vote count and quote-board channel pattern are supplied via
* {@code QuoteBoardConfig}.
*/
public final class QuoteBoardForwarder extends MessageReceiverAdapter {

private static final Logger logger = LoggerFactory.getLogger(QuoteBoardForwarder.class);
private final Emoji triggerReaction;
private final Predicate<String> isQuoteBoardChannelName;
Comment thread
christolis marked this conversation as resolved.
private final QuoteBoardConfig config;

/**
* 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 QuoteBoardForwarder(Config config) {
this.config = config.getCoolMessagesConfig();
this.triggerReaction = Emoji.fromUnicode(this.config.reactionEmoji());

isQuoteBoardChannelName =
Comment thread
christolis marked this conversation as resolved.
Outdated
Pattern.compile(this.config.boardChannelPattern()).asMatchPredicate();
}

@Override
public void onMessageReactionAdd(MessageReactionAddEvent event) {
final MessageReaction messageReaction = event.getReaction();
boolean isCoolEmoji = messageReaction.getEmoji().equals(triggerReaction);
Comment thread
christolis marked this conversation as resolved.
Outdated
long guildId = event.getGuild().getIdLong();
Comment thread
christolis marked this conversation as resolved.
Outdated

if (hasAlreadyForwardedMessage(event.getJDA(), messageReaction)) {
return;
}

final int reactionsCount = (int) messageReaction.retrieveUsers().stream().count();
if (isCoolEmoji && reactionsCount >= config.minimumReactions()) {
Comment thread
christolis marked this conversation as resolved.
Outdated
Comment thread
christolis marked this conversation as resolved.
Outdated
Optional<TextChannel> 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;
}

event.retrieveMessage()
.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<Void> markAsProcessed(Message message) {
return message.addReaction(triggerReaction);
}

/**
* 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<TextChannel> findQuoteBoardChannel(JDA jda, long guildId) {
Comment thread
christolis marked this conversation as resolved.
return jda.getGuildById(guildId)
Comment thread
christolis marked this conversation as resolved.
Outdated
.getTextChannelCache()
.stream()
.filter(channel -> isQuoteBoardChannelName.test(channel.getName()))
.findAny();
Comment thread
christolis marked this conversation as resolved.
Outdated
}

/**
* Inserts a message to the specified text channel
*
* @return a {@link MessageCreateAction} of the call to make
*/

/**
* Checks a {@link MessageReaction} to see if the bot has reacted to it.
*/
private boolean hasAlreadyForwardedMessage(JDA jda, MessageReaction messageReaction) {
if (!triggerReaction.equals(messageReaction.getEmoji())) {
return false;
}

return messageReaction.retrieveUsers()
.parallelStream()
.anyMatch(user -> jda.getSelfUser().getIdLong() == user.getIdLong());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<MessageReceiver> getMessageReceiversSubscribedTo(Channel channel) {
String channelName = channel.getName();
return channelNameToMessageReceiver.entrySet()
Expand Down
Loading