diff --git a/application/config.json.template b/application/config.json.template index 6ebd7715db..809c6260e1 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -16,5 +16,10 @@ ] } ], - "helpChannelPattern": "([a-zA-Z_]+_)?help(_\\d+)?" + "helpChannelPattern": "([a-zA-Z_]+_)?help(_\\d+)?", + "suggestions": { + "channelPattern": "tj_suggestions", + "upVoteEmoteName": "peepo_yes", + "downVoteEmoteName": "peepo_no" + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java index bc7638a2ac..54fd53349f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -4,6 +4,7 @@ import org.jetbrains.annotations.NotNull; import org.togetherjava.tjbot.commands.basic.PingCommand; import org.togetherjava.tjbot.commands.basic.RoleSelectCommand; +import org.togetherjava.tjbot.commands.basic.SuggestionsUpDownVoter; import org.togetherjava.tjbot.commands.basic.VcActivityCommand; import org.togetherjava.tjbot.commands.free.FreeCommand; import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; @@ -63,6 +64,7 @@ public enum Features { // Message receivers features.add(new TopHelpersMessageListener(database, config)); + features.add(new SuggestionsUpDownVoter(config)); // Event receivers features.add(new RejoinMuteListener(actionsStore, config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java new file mode 100644 index 0000000000..48b05e1bb2 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java @@ -0,0 +1,66 @@ +package org.togetherjava.tjbot.commands.basic; + +import net.dv8tion.jda.api.entities.Emote; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.MessageReceiverAdapter; +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.config.SuggestionsConfig; + +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * Listener that receives all sent messages from suggestion channels and reacts with an up- and + * down-vote on them to indicate to users that they can vote on the suggestion. + */ +public final class SuggestionsUpDownVoter extends MessageReceiverAdapter { + private static final Logger logger = LoggerFactory.getLogger(SuggestionsUpDownVoter.class); + private static final String FALLBACK_UP_VOTE = "👍"; + private static final String FALLBACK_DOWN_VOTE = "👎"; + + private final SuggestionsConfig config; + + /** + * Creates a new listener to receive all message sent in suggestion channels. + * + * @param config the config to use for this + */ + public SuggestionsUpDownVoter(@NotNull Config config) { + super(Pattern.compile(config.getSuggestions().getChannelPattern())); + + this.config = config.getSuggestions(); + } + + @Override + public void onMessageReceived(@NotNull GuildMessageReceivedEvent event) { + if (event.getAuthor().isBot() || event.isWebhookMessage()) { + return; + } + + Guild guild = event.getGuild(); + Message message = event.getMessage(); + + reactWith(config.getUpVoteEmoteName(), FALLBACK_UP_VOTE, guild, message); + reactWith(config.getDownVoteEmoteName(), FALLBACK_DOWN_VOTE, guild, message); + } + + private static void reactWith(@NotNull String emoteName, @NotNull String fallbackUnicodeEmote, + @NotNull Guild guild, @NotNull Message message) { + getEmoteByName(emoteName, guild).map(message::addReaction).orElseGet(() -> { + logger.warn( + "Unable to vote on a suggestion with the configured emote ('{}'), using fallback instead.", + emoteName); + return message.addReaction(fallbackUnicodeEmote); + }).queue(); + } + + private static @NotNull Optional getEmoteByName(@NotNull String name, + @NotNull Guild guild) { + return guild.getEmotesByName(name, false).stream().findAny(); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index c3982adf2e..0dbd97d39d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -15,7 +15,6 @@ * Configuration of the application. Create instances using {@link #load(Path)}. */ public final class Config { - private final String token; private final String databasePath; private final String projectWebsite; @@ -27,6 +26,7 @@ public final class Config { private final String tagManageRolePattern; private final List freeCommand; private final String helpChannelPattern; + private final SuggestionsConfig suggestions; @SuppressWarnings("ConstructorWithTooManyParameters") @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) @@ -40,7 +40,8 @@ private Config(@JsonProperty("token") String token, @JsonProperty("softModerationRolePattern") String softModerationRolePattern, @JsonProperty("tagManageRolePattern") String tagManageRolePattern, @JsonProperty("freeCommand") List freeCommand, - @JsonProperty("helpChannelPattern") String helpChannelPattern) { + @JsonProperty("helpChannelPattern") String helpChannelPattern, + @JsonProperty("suggestions") SuggestionsConfig suggestions) { this.token = token; this.databasePath = databasePath; this.projectWebsite = projectWebsite; @@ -52,6 +53,7 @@ private Config(@JsonProperty("token") String token, this.tagManageRolePattern = tagManageRolePattern; this.freeCommand = Collections.unmodifiableList(freeCommand); this.helpChannelPattern = helpChannelPattern; + this.suggestions = suggestions; } /** @@ -170,4 +172,13 @@ public String getTagManageRolePattern() { public String getHelpChannelPattern() { return helpChannelPattern; } + + /** + * Gets the config for the suggestion system. + * + * @return the suggestion system config + */ + public SuggestionsConfig getSuggestions() { + return suggestions; + } } diff --git a/application/src/main/java/org/togetherjava/tjbot/config/SuggestionsConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/SuggestionsConfig.java new file mode 100644 index 0000000000..01f18beb70 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/config/SuggestionsConfig.java @@ -0,0 +1,53 @@ +package org.togetherjava.tjbot.config; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +/** + * Configuration for the suggestion system, see + * {@link org.togetherjava.tjbot.commands.basic.SuggestionsUpDownVoter}. + */ +@SuppressWarnings("ClassCanBeRecord") +@JsonRootName("suggestions") +public final class SuggestionsConfig { + private final String channelPattern; + private final String upVoteEmoteName; + private final String downVoteEmoteName; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private SuggestionsConfig(@JsonProperty("channelPattern") String channelPattern, + @JsonProperty("upVoteEmoteName") String upVoteEmoteName, + @JsonProperty("downVoteEmoteName") String downVoteEmoteName) { + this.channelPattern = channelPattern; + this.upVoteEmoteName = upVoteEmoteName; + this.downVoteEmoteName = downVoteEmoteName; + } + + /** + * Gets the REGEX pattern used to identify channels that are used for sending suggestions. + * + * @return the channel name pattern + */ + public String getChannelPattern() { + return channelPattern; + } + + /** + * Gets the name of the emote used to up-vote suggestions. + * + * @return the name of the up-vote emote + */ + public String getUpVoteEmoteName() { + return upVoteEmoteName; + } + + /** + * Gets the name of the emote used to down-vote suggestions. + * + * @return the name of the down-vote emote + */ + public String getDownVoteEmoteName() { + return downVoteEmoteName; + } +}