Skip to content
Open
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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<String> isQuoteBoardChannelName;
private final QuoteBoardConfig config;

private final Object reactedMessagesLock = new Object();
private Set<ReactedMessage> reactedMessages = new HashSet<>();

/**
* Constructs a new instance of QuoteBoardForwarder.
*
Expand All @@ -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();
}
Expand All @@ -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<ReactedMessage> messagesToProcess;
synchronized (reactedMessagesLock) {
messagesToProcess = reactedMessages;
reactedMessages = new HashSet<>();
}

Optional<TextChannel> 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<TextChannel> 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<Void> 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<TextChannel> findQuoteBoardChannel(JDA jda, long guildId) {
Guild guild = jda.getGuildById(guildId);

Expand All @@ -139,26 +168,38 @@ private Optional<TextChannel> 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<MessageReaction> reactions) {
return (float) reactions.stream()
private double calculateMessageScore(List<MessageReaction> 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<Void> markMessageAsForwarded(Message message) {
return message.addReaction(messageForwardedEmojiMarker);
}
}
Loading