Skip to content
Open
Show file tree
Hide file tree
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
29 changes: 27 additions & 2 deletions application/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,34 @@
"pollIntervalInMinutes": 10
},
"quoteBoardConfig": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we should ignore some text channels like announcements, and hall-of-fame, github-activity-logs, vps-status, bots and the likes, because they are not intended for such a feature, consider to put a config attribute to list them here

"minimumReactionsToTrigger": 5,
"minimumScoreToTrigger": 5.0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to put it decimal

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@firasrg Just curious: what happens if we want half-point scoring later (like the 0.5 for non-star emojis)?

"channel": "quotes",
"reactionEmoji": "⭐"
"botEmoji": "⭐",
"defaultEmojiScore": 0.5,
"emojiScores": {
"😬": -0.5,
"💔": -0.5,
"😐": -0.5,
"😊": -0.5,
"🖕": -0.5,
"👎": -0.5,
"💩": -0.5,
"🤢": -0.5,
"🤮": -0.5,
"🤬": -0.5,
"😡": -0.5,
"😒": -0.5,
"🤨": -0.5,

"🇷🇺": 0.0,
"🇵🇸": 0.0,
"🇮🇱": 0.0,
"🏳️‍🌈": 0.0,
Comment on lines +217 to +220
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should keep this politically influenced example in the repo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in Discord, it's an example, it's made to prove a point for the person setting the configuration, to give them an idea on how to provide scores for each emoji. It's intentionally there to evoke an emotion, and all of these flags have a flag of 0 to maintain neutrality. No flag is more powerful than the other, at least none of the flags commonly used as reactions to offend in 2026.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My tip: Avoid flag reactions


"⭐": 1.0,

"youtube:1464573182206804010": 0.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I know this is an example, it's better to add a custom emoji ID from the server TJ for more consistency

}
},
"memberCountCategoryPattern": "Info",
"topHelpers": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,44 @@

import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder;

import java.util.Map;
import java.util.Objects;

/**
* Configuration for the quote board feature, see {@link QuoteBoardForwarder}.
*/
@JsonRootName("quoteBoardConfig")
public record QuoteBoardConfig(
@JsonProperty(value = "minimumReactionsToTrigger", required = true) int minimumReactions,
@JsonProperty(required = true) String channel,
@JsonProperty(value = "reactionEmoji", required = true) String reactionEmoji) {
@JsonProperty(value = "minimumScoreToTrigger", required = true) float minimumScoreToTrigger,
@JsonProperty(value = "channel", required = true) String channel,
@JsonProperty(value = "botEmoji", required = true) String botEmoji,
@JsonProperty(value = "defaultEmojiScore", required = true) float defaultEmojiScore,
@JsonProperty(value = "emojiScores", required = true) Map<String, Float> emojiScores) {

/**
* Creates a QuoteBoardConfig.
*
* @param minimumReactions the minimum amount of reactions
* @param minimumScoreToTrigger the minimum amount of reaction score for a message to be quoted
* @param channel the pattern for the board channel
* @param reactionEmoji the emoji with which users should react to
* @param botEmoji the emoji with which the bot will mark quoted messages
* @param defaultEmojiScore the default score of an emoji if it's not in the emojiScores map
* @param emojiScores a map of each emoji's custom score
*/
public QuoteBoardConfig {
if (minimumReactions <= 0) {
throw new IllegalArgumentException("minimumReactions must be greater than zero");
if (minimumScoreToTrigger <= 0) {
throw new IllegalArgumentException("minimumScoreToTrigger must be greater than zero");
}
Objects.requireNonNull(channel);
if (channel.isBlank()) {
throw new IllegalArgumentException("channel must not be empty or blank");
}
Objects.requireNonNull(reactionEmoji);
if (reactionEmoji.isBlank()) {
Objects.requireNonNull(botEmoji);
if (botEmoji.isBlank()) {
throw new IllegalArgumentException("reactionEmoji must not be empty or blank");
}
Objects.requireNonNull(emojiScores);
LogManager.getLogger(QuoteBoardConfig.class)
.debug("Quote-Board configs loaded: minimumReactions={}, channel='{}', reactionEmoji='{}'",
minimumReactions, channel, reactionEmoji);
minimumScoreToTrigger, channel, botEmoji);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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.entities.emoji.EmojiUnion;
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
import net.dv8tion.jda.api.requests.RestAction;
import org.slf4j.Logger;
Expand Down Expand Up @@ -36,7 +37,7 @@
public final class QuoteBoardForwarder extends MessageReceiverAdapter {

private static final Logger logger = LoggerFactory.getLogger(QuoteBoardForwarder.class);
private final Emoji triggerReaction;
private final Emoji botEmoji;
private final Predicate<String> isQuoteBoardChannelName;
private final QuoteBoardConfig config;

Expand All @@ -48,7 +49,7 @@ public final class QuoteBoardForwarder extends MessageReceiverAdapter {
*/
public QuoteBoardForwarder(Config config) {
this.config = config.getQuoteBoardConfig();
this.triggerReaction = Emoji.fromUnicode(this.config.reactionEmoji());
this.botEmoji = Emoji.fromUnicode(this.config.botEmoji());

this.isQuoteBoardChannelName = Pattern.compile(this.config.channel()).asMatchPredicate();
}
Expand All @@ -60,24 +61,11 @@ public void onMessageReactionAdd(MessageReactionAddEvent event) {

final MessageReaction messageReaction = event.getReaction();

if (!messageReaction.getEmoji().equals(triggerReaction)) {
logger.debug("Reaction emoji '{}' does not match trigger emoji '{}'. Ignoring.",
messageReaction.getEmoji(), triggerReaction);
return;
}

if (hasAlreadyForwardedMessage(event.getJDA(), messageReaction)) {
logger.debug("Message has already been forwarded by the bot. Skipping.");
return;
}

long reactionCount = messageReaction.retrieveUsers().stream().count();
if (reactionCount < config.minimumReactions()) {
logger.debug("Reaction count {} is less than minimum required {}. Skipping.",
reactionCount, config.minimumReactions());
return;
}

final long guildId = event.getGuild().getIdLong();

Optional<TextChannel> boardChannelOptional = findQuoteBoardChannel(event.getJDA(), guildId);
Expand All @@ -96,21 +84,27 @@ public void onMessageReactionAdd(MessageReactionAddEvent event) {
return;
}

logger.debug("Forwarding message to quote board channel: {}", boardChannel.getName());
event.retrieveMessage().queue(message -> {
float emojiScore = calculateMessageScore(message.getReactions());

event.retrieveMessage()
.queue(message -> markAsProcessed(message).flatMap(v -> message.forwardTo(boardChannel))
.queue(_ -> logger.debug("Message forwarded to quote board channel: {}",
boardChannel.getName())),
if (emojiScore < config.minimumScoreToTrigger()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a log.debug can help here

return;
}

e -> logger.warn(
"Unknown error while attempting to retrieve and forward message for quote-board, message is ignored.",
e));
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));
});
}

private RestAction<Void> markAsProcessed(Message message) {
return message.addReaction(triggerReaction);
return message.addReaction(botEmoji);
}

/**
Expand Down Expand Up @@ -146,12 +140,25 @@ private Optional<TextChannel> findQuoteBoardChannel(JDA jda, long guildId) {
* 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())) {
if (!botEmoji.equals(messageReaction.getEmoji())) {
return false;
}

return messageReaction.retrieveUsers()
.parallelStream()
.anyMatch(user -> jda.getSelfUser().getIdLong() == user.getIdLong());
}

private float calculateMessageScore(List<MessageReaction> reactions) {
return (float) reactions.stream()
.mapToDouble(reaction -> reaction.getCount() * getEmojiScore(reaction.getEmoji()))
.sum();
}

private float getEmojiScore(EmojiUnion emoji) {
float defaultScore = config.defaultEmojiScore();
String reactionCode = emoji.getAsReactionCode();

return config.emojiScores().getOrDefault(reactionCode, defaultScore);
}
}
Loading