diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index fb4b93baae..7383bebb8d 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -5,8 +5,8 @@ import net.dv8tion.jda.api.requests.GatewayIntent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.togetherjava.tjbot.commands.Commands; -import org.togetherjava.tjbot.commands.system.CommandSystem; +import org.togetherjava.tjbot.commands.Features; +import org.togetherjava.tjbot.commands.system.BotCore; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.db.Database; @@ -22,7 +22,7 @@ * New commands can be created by implementing * {@link net.dv8tion.jda.api.events.interaction.SlashCommandEvent} or extending * {@link org.togetherjava.tjbot.commands.SlashCommandAdapter}. They can then be registered in - * {@link Commands}. + * {@link Features}. */ public enum Application { ; @@ -79,7 +79,7 @@ public static void runBot(String token, Path databasePath) { JDA jda = JDABuilder.createDefault(token) .enableIntents(GatewayIntent.GUILD_MEMBERS) .build(); - jda.addEventListener(new CommandSystem(jda, database)); + jda.addEventListener(new BotCore(jda, database)); jda.awaitReady(); logger.info("Bot is ready"); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java b/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java deleted file mode 100644 index 556ea519d2..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Commands.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.togetherjava.tjbot.commands; - -import net.dv8tion.jda.api.JDA; -import org.jetbrains.annotations.NotNull; -import org.togetherjava.tjbot.commands.basic.PingCommand; -import org.togetherjava.tjbot.commands.basic.VcActivityCommand; -import org.togetherjava.tjbot.commands.free.FreeCommand; -import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; -import org.togetherjava.tjbot.commands.moderation.*; -import org.togetherjava.tjbot.commands.moderation.temp.TemporaryModerationRoutine; -import org.togetherjava.tjbot.commands.tags.TagCommand; -import org.togetherjava.tjbot.commands.tags.TagManageCommand; -import org.togetherjava.tjbot.commands.tags.TagSystem; -import org.togetherjava.tjbot.commands.tags.TagsCommand; -import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.routines.ModAuditLogRoutine; - -import java.util.ArrayList; -import java.util.Collection; - -/** - * Utility class that offers all commands that should be registered by the system. New commands have - * to be added here, where {@link org.togetherjava.tjbot.commands.system.CommandSystem} will then - * pick it up from and register it with the system. - *

- * To add a new slash command, extend the commands returned by - * {@link #createSlashCommands(JDA, Database)}. - */ -public enum Commands { - ; - - /** - * Creates all slash commands that should be registered with this application. - *

- * Calling this method multiple times will result in multiple commands being created, which - * generally should be avoided. - * - * @param jda the JDA instance commands will be registered at - * @param database the database of the application, which commands can use to persist data - * @return a collection of all slash commands - */ - public static @NotNull Collection createSlashCommands(@NotNull JDA jda, - @NotNull Database database) { - TagSystem tagSystem = new TagSystem(database); - ModerationActionsStore actionsStore = new ModerationActionsStore(database); - - // TODO This should be moved into some proper command system instead (see GH issue #235 - // which adds support for routines) - new ModAuditLogRoutine(jda, database).start(); - new TemporaryModerationRoutine(jda, actionsStore).start(); - - // TODO This should be moved into some proper command system instead (see GH issue #236 - // which adds support for listeners) - jda.addEventListener(new RejoinMuteListener(actionsStore)); - - // NOTE The command system can add special system relevant commands also by itself, - // hence this list may not necessarily represent the full list of all commands actually - // available. - Collection commands = new ArrayList<>(); - - commands.add(new PingCommand()); - commands.add(new TeXCommand()); - commands.add(new TagCommand(tagSystem)); - commands.add(new TagManageCommand(tagSystem)); - commands.add(new TagsCommand(tagSystem)); - commands.add(new VcActivityCommand()); - commands.add(new WarnCommand(actionsStore)); - commands.add(new KickCommand(actionsStore)); - commands.add(new BanCommand(actionsStore)); - commands.add(new UnbanCommand(actionsStore)); - commands.add(new FreeCommand()); - commands.add(new AuditCommand(actionsStore)); - commands.add(new MuteCommand(actionsStore)); - commands.add(new UnmuteCommand(actionsStore)); - - return commands; - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/EventReceiver.java b/application/src/main/java/org/togetherjava/tjbot/commands/EventReceiver.java new file mode 100644 index 0000000000..2cc6e3fbc5 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/EventReceiver.java @@ -0,0 +1,22 @@ +package org.togetherjava.tjbot.commands; + +import net.dv8tion.jda.api.hooks.EventListener; + +/** + * Receives all incoming Discord events, unfiltered. A list of all available event types can be + * found in {@link net.dv8tion.jda.api.hooks.ListenerAdapter}. + * + * If possible, prefer one of the more concrete features instead, such as {@link SlashCommand} or + * {@link MessageReceiver}. Take care to not accidentally implement both, this and one of the other + * {@link Feature}s, as this might result in events being received multiple times. + *

+ * All event receivers have to implement this interface. A new receiver can then be registered by + * adding it to {@link Features}. + *

+ *

+ * After registration, the system will notify a receiver for any incoming Discord event. + */ +@FunctionalInterface +public interface EventReceiver extends EventListener, Feature { + // Basically a renaming of JDAs EventListener, plus our Feature marker interface +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Feature.java b/application/src/main/java/org/togetherjava/tjbot/commands/Feature.java new file mode 100644 index 0000000000..8b16cca77c --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Feature.java @@ -0,0 +1,11 @@ +package org.togetherjava.tjbot.commands; + +/** + * Interface for features supported by the bots core system. + *

+ * New features are added in {@link org.togetherjava.tjbot.commands.Features} and from there picked + * up by {@link org.togetherjava.tjbot.commands.system.BotCore}. + */ +public interface Feature { + // Marker interface +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java new file mode 100644 index 0000000000..1a05547d91 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -0,0 +1,84 @@ +package org.togetherjava.tjbot.commands; + +import net.dv8tion.jda.api.JDA; +import org.jetbrains.annotations.NotNull; +import org.togetherjava.tjbot.commands.basic.PingCommand; +import org.togetherjava.tjbot.commands.basic.VcActivityCommand; +import org.togetherjava.tjbot.commands.free.FreeCommand; +import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; +import org.togetherjava.tjbot.commands.moderation.*; +import org.togetherjava.tjbot.commands.moderation.temp.TemporaryModerationRoutine; +import org.togetherjava.tjbot.commands.system.BotCore; +import org.togetherjava.tjbot.commands.tags.TagCommand; +import org.togetherjava.tjbot.commands.tags.TagManageCommand; +import org.togetherjava.tjbot.commands.tags.TagSystem; +import org.togetherjava.tjbot.commands.tags.TagsCommand; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.routines.ModAuditLogRoutine; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Utility class that offers all features that should be registered by the system, such as commands. + * New features have to be added here, where {@link BotCore} will then pick it up from and register + * it with the system. + *

+ * To add a new slash command, extend the commands returned by + * {@link #createFeatures(JDA, Database)}. + */ +public enum Features { + ; + + /** + * Creates all features that should be registered with this application. + *

+ * Calling this method multiple times will result in multiple features being created, which + * generally should be avoided. + * + * @param jda the JDA instance commands will be registered at + * @param database the database of the application, which features can use to persist data + * @return a collection of all features + */ + public static @NotNull Collection createFeatures(@NotNull JDA jda, + @NotNull Database database) { + TagSystem tagSystem = new TagSystem(database); + ModerationActionsStore actionsStore = new ModerationActionsStore(database); + + // NOTE The system can add special system relevant commands also by itself, + // hence this list may not necessarily represent the full list of all commands actually + // available. + Collection features = new ArrayList<>(); + + // Routines + // TODO This should be moved into some proper command system instead (see GH issue #235 + // which adds support for routines) + new ModAuditLogRoutine(jda, database).start(); + new TemporaryModerationRoutine(jda, actionsStore).start(); + + // Message receivers + + // Event receivers + features.add(new RejoinMuteListener(actionsStore)); + + // Slash commands + features.add(new PingCommand()); + features.add(new TeXCommand()); + features.add(new TagCommand(tagSystem)); + features.add(new TagManageCommand(tagSystem)); + features.add(new TagsCommand(tagSystem)); + features.add(new VcActivityCommand()); + features.add(new WarnCommand(actionsStore)); + features.add(new KickCommand(actionsStore)); + features.add(new BanCommand(actionsStore)); + features.add(new UnbanCommand(actionsStore)); + features.add(new AuditCommand(actionsStore)); + features.add(new MuteCommand(actionsStore)); + features.add(new UnmuteCommand(actionsStore)); + + // Mixtures + features.add(new FreeCommand()); + + return features; + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiver.java b/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiver.java new file mode 100644 index 0000000000..e43284ee31 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiver.java @@ -0,0 +1,51 @@ +package org.togetherjava.tjbot.commands; + +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Pattern; + +/** + * Receives incoming Discord guild messages from channels matching a given pattern. + *

+ * All message receivers have to implement this interface. For convenience, there is a + * {@link MessageReceiverAdapter} available that implemented most methods already. A new receiver + * can then be registered by adding it to {@link Features}. + *

+ *

+ * After registration, the system will notify a receiver whenever a new message was sent or an + * existing message was updated in any channel matching the {@link #getChannelNamePattern()} the bot + * is added to. + */ +public interface MessageReceiver extends Feature { + /** + * Retrieves the pattern matching the names of channels of which this receiver is interested in + * receiving sent messages from. Called by the core system once during the startup in order to + * register the receiver accordingly. + *

+ * Changes on the pattern returned by this method afterwards will not be picked up. + * + * @return the pattern matching the names of relevant channels + */ + @NotNull + Pattern getChannelNamePattern(); + + /** + * Triggered by the core system whenever a new message was sent and received 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 + * message that was sent and received + */ + void onMessageReceived(@NotNull GuildMessageReceivedEvent event); + + /** + * Triggered by the core system whenever an existing message was edited 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 + * message that was edited + */ + void onMessageUpdated(@NotNull GuildMessageUpdateEvent event); +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiverAdapter.java b/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiverAdapter.java new file mode 100644 index 0000000000..506d2aa6e7 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/MessageReceiverAdapter.java @@ -0,0 +1,47 @@ +package org.togetherjava.tjbot.commands; + +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Pattern; + +/** + * Adapter implementation of a {@link MessageReceiver}. A new receiver can then be registered by + * adding it to {@link Features}. + *

+ * {@link #onMessageReceived(GuildMessageReceivedEvent)} and + * {@link #onMessageUpdated(GuildMessageUpdateEvent)} can be overridden if desired. The default + * implementation is empty, the adapter will not react to such events. + */ +public abstract class MessageReceiverAdapter implements MessageReceiver { + + private final Pattern channelNamePattern; + + /** + * Creates an instance of a message receiver with the given pattern. + * + * @param channelNamePattern the pattern matching names of channels interested in, only messages + * from matching channels will be received + */ + protected MessageReceiverAdapter(@NotNull Pattern channelNamePattern) { + this.channelNamePattern = channelNamePattern; + } + + @Override + public final @NotNull Pattern getChannelNamePattern() { + return channelNamePattern; + } + + @SuppressWarnings("NoopMethodInAbstractClass") + @Override + public void onMessageReceived(@NotNull GuildMessageReceivedEvent event) { + // Adapter does not react by default, subclasses may change this behavior + } + + @SuppressWarnings("NoopMethodInAbstractClass") + @Override + public void onMessageUpdated(@NotNull GuildMessageUpdateEvent event) { + // Adapter does not react by default, subclasses may change this behavior + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java index c8644c1147..a5a01c20e8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java @@ -1,7 +1,6 @@ package org.togetherjava.tjbot.commands; import net.dv8tion.jda.api.entities.Emoji; -import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; import net.dv8tion.jda.api.events.interaction.SelectionMenuEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; @@ -21,7 +20,7 @@ *

* All slash commands have to implement this interface. For convenience, there is a * {@link SlashCommandAdapter} available that implemented most methods already. A new command can - * then be registered by adding it to {@link Commands}. + * then be registered by adding it to {@link Features}. *

*

* Slash commands can either be visible globally in Discord or just to specific guilds. They can @@ -36,7 +35,7 @@ *

* Some example commands are available in {@link org.togetherjava.tjbot.commands.basic}. */ -public interface SlashCommand { +public interface SlashCommand extends Feature { /** * Gets the name of the command. @@ -84,8 +83,8 @@ public interface SlashCommand { *

*

* This method may be called multiple times, implementations must not create new data each time - * but instead configure it once beforehand. The command system will automatically call this - * method to register the command to Discord. + * but instead configure it once beforehand. The core system will automatically call this method + * to register the command to Discord. * * @return the command data of this command */ @@ -93,29 +92,8 @@ public interface SlashCommand { CommandData getData(); /** - * Triggered by the command system after system startup is complete. This can be used for - * initialisation actions that cannot occur during construction. - *

- * This method may be called multi-threaded. There is no guarantee as to the order that commands - * will get called and there is no guarantee which thread they will be called on or even that - * they will be called by the same thread. - *

- * There is also no guarantee that slashCommands will be registered on guilds before this is - * called. Do not use this method to interact with slashCommands. - *

- * Details are available in the given event and the event also enables implementations to - * respond to it. - *

- * This method will be called in a multi-threaded context and the event may not be hold valid - * forever. - * - * @param event the event that triggered this - */ - void onReady(@NotNull ReadyEvent event); - - /** - * Triggered by the command system when a slash command corresponding to this implementation - * (based on {@link #getData()}) has been triggered. + * Triggered by the core system when a slash command corresponding to this implementation (based + * on {@link #getData()}) has been triggered. *

* This method may be called multi-threaded. In particular, there are no guarantees that it will * be executed on the same thread repeatedly or on the same thread that other event methods have @@ -127,7 +105,7 @@ public interface SlashCommand { * Buttons or menus have to be created with a component ID (see * {@link ComponentInteraction#getComponentId()}, * {@link net.dv8tion.jda.api.interactions.components.Button#of(ButtonStyle, String, Emoji)}) in - * a very specific format, otherwise the command system will fail to identify the command that + * a very specific format, otherwise the core system will fail to identify the command that * corresponded to the button or menu click event and is unable to route it back. *

* The component ID has to be a UUID-string (see {@link java.util.UUID}), which is associated to @@ -154,7 +132,7 @@ public interface SlashCommand { void onSlashCommand(@NotNull SlashCommandEvent event); /** - * Triggered by the command system when a button corresponding to this implementation (based on + * Triggered by the core system when a button corresponding to this implementation (based on * {@link #getData()}) has been clicked. *

* This method may be called multi-threaded. In particular, there are no guarantees that it will @@ -174,7 +152,7 @@ public interface SlashCommand { void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args); /** - * Triggered by the command system when a selection menu corresponding to this implementation + * Triggered by the core system when a selection menu corresponding to this implementation * (based on {@link #getData()}) has been clicked. *

* This method may be called multi-threaded. In particular, there are no guarantees that it will @@ -194,10 +172,10 @@ public interface SlashCommand { void onSelectionMenu(@NotNull SelectionMenuEvent event, @NotNull List args); /** - * Triggered by the command system during its setup phase. It will provide the command a - * component id generator through this method, which can be used to generate component ids, as - * used for button or selection menus. See {@link #onSlashCommand(SlashCommandEvent)} for - * details on how to use this. + * Triggered by the core system during its setup phase. It will provide the command a component + * id generator through this method, which can be used to generate component ids, as used for + * button or selection menus. See {@link #onSlashCommand(SlashCommandEvent)} for details on how + * to use this. * * @param generator the provided component id generator */ diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java index 9b461b50fe..f49d82a639 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommandAdapter.java @@ -1,6 +1,5 @@ package org.togetherjava.tjbot.commands; -import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; import net.dv8tion.jda.api.events.interaction.SelectionMenuEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; @@ -17,7 +16,7 @@ /** * Adapter implementation of a {@link SlashCommand}. The minimal setup only requires implementation * of {@link #onSlashCommand(SlashCommandEvent)}. A new command can then be registered by adding it - * to {@link Commands}. + * to {@link Features}. *

* Further, {@link #onButtonClick(ButtonClickEvent, List)} and * {@link #onSelectionMenu(SelectionMenuEvent, List)} can be overridden if desired. The default @@ -54,7 +53,7 @@ * } * *

- * and registration of an instance of that class in {@link Commands}. + * and registration of an instance of that class in {@link Features}. */ public abstract class SlashCommandAdapter implements SlashCommand { private final String name; @@ -106,12 +105,6 @@ public final void acceptComponentIdGenerator(@NotNull ComponentIdGenerator gener componentIdGenerator = generator; } - @SuppressWarnings("NoopMethodInAbstractClass") - @Override - public void onReady(@NotNull ReadyEvent event) { - // Adapter does not react by default, subclasses may change this behavior - } - @SuppressWarnings("NoopMethodInAbstractClass") @Override public void onButtonClick(@NotNull ButtonClickEvent event, @NotNull List args) { diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java index 9b600e79b6..af2619dfc9 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelMonitor.java @@ -269,11 +269,15 @@ public void updateStatusFor(@NotNull Guild guild) { "Guild %s is not configured in the free command system." .formatted(guild.getName())); } + long channelId = guildIdToStatusChannel.get(guild.getIdLong()); TextChannel channel = guild.getTextChannelById(channelId); - if (channel == null) + + if (channel == null) { throw new IllegalStateException("Status channel %d does not exist in guild %s" .formatted(channelId, guild.getName())); + } + return channel; } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java index 3dda6a17c8..41eb9528fe 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/ChannelStatus.java @@ -164,10 +164,12 @@ public synchronized void setFree() { public boolean equals(final Object o) { // TODO should I overload equals with equals(long) so that a Set may be used instead of a // Map - if (this == o) + if (this == o) { return true; - if (o == null || getClass() != o.getClass()) + } + if (o == null || getClass() != o.getClass()) { return false; + } ChannelStatus channelStatus = (ChannelStatus) o; return channelId == channelStatus.channelId; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java index 707b3e24c8..ee289c0402 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/FreeCommand.java @@ -10,11 +10,11 @@ import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; -import net.dv8tion.jda.api.hooks.EventListener; import net.dv8tion.jda.api.requests.RestAction; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.EventReceiver; import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import org.togetherjava.tjbot.config.Config; @@ -54,7 +54,7 @@ * channel may be one of the monitored channels however it is recommended that a different channel * is used. */ -public final class FreeCommand extends SlashCommandAdapter implements EventListener { +public final class FreeCommand extends SlashCommandAdapter implements EventReceiver { private static final Logger logger = LoggerFactory.getLogger(FreeCommand.class); private static final String STATUS_TITLE = "**__CHANNEL STATUS__**\n\n"; @@ -65,7 +65,7 @@ public final class FreeCommand extends SlashCommandAdapter implements EventListe private final ChannelMonitor channelMonitor; private final Map channelIdToMessageIdForStatus; - private boolean isReady; + private volatile boolean isReady; /** @@ -97,11 +97,8 @@ public FreeCommand() { * * @param event the event this method reacts to */ - @Override public void onReady(@NotNull final ReadyEvent event) { final JDA jda = event.getJDA(); - // TODO remove this when onGuildMessageReceived has another access point - jda.addEventListener(this); initChannelsToMonitor(); initStatusMessageChannels(jda); @@ -285,9 +282,14 @@ private void checkBusyStatusAllChannels(@NotNull JDA jda) { * * @param event the generic event that includes the 'onGuildMessageReceived'. */ + @SuppressWarnings("squid:S2583") // False-positive about the if-else-instanceof, sonar thinks + // the second case is unreachable; but it passes without + // pattern-matching. Probably a bug in SonarLint with Java 17. @Override public void onEvent(@NotNull GenericEvent event) { - if (event instanceof GuildMessageReceivedEvent guildEvent) { + if (event instanceof ReadyEvent readyEvent) { + onReady(readyEvent); + } else if (event instanceof GuildMessageReceivedEvent guildEvent) { if (guildEvent.isWebhookMessage() || guildEvent.getAuthor().isBot()) { return; } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java index cae1875cd3..6ef068cafa 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/free/UserStrings.java @@ -23,8 +23,8 @@ enum UserStrings { Command not ready please try again in a minute. """), NOT_MONITORED_ERROR("This channel is not being monitored for free/busy status. If you" - + "believe this channel should be part of the free/busy status system, please discuss it" - + "with a moderator"), + + " believe this channel should be part of the free/busy status system, please" + + " consult a moderator."), NOT_CONFIGURED_ERROR(""" This guild (%s) is not configured to use the '/free' command. Please add entries in the config, restart the bot and try again. diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/RejoinMuteListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/RejoinMuteListener.java index 2fa0ff438f..3fa6440a92 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/RejoinMuteListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/RejoinMuteListener.java @@ -3,15 +3,16 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.IPermissionHolder; import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.GenericEvent; import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.EventReceiver; -import javax.annotation.Nonnull; import java.time.Instant; -import java.util.*; +import java.util.Objects; +import java.util.Optional; /** * Reapplies existing mutes to users who have left and rejoined a guild. @@ -21,7 +22,7 @@ * join events and reapplies the mute role in case the user is supposed to be muted still (according * to the {@link ModerationActionsStore}). */ -public final class RejoinMuteListener extends ListenerAdapter { +public final class RejoinMuteListener implements EventReceiver { private static final Logger logger = LoggerFactory.getLogger(RejoinMuteListener.class); private final ModerationActionsStore actionsStore; @@ -52,7 +53,13 @@ private static boolean isActionEffective(@NotNull ActionRecord action) { } @Override - public void onGuildMemberJoin(@Nonnull GuildMemberJoinEvent event) { + public void onEvent(@NotNull GenericEvent event) { + if (event instanceof GuildMemberJoinEvent joinEvent) { + onGuildMemberJoin(joinEvent); + } + } + + private void onGuildMemberJoin(@NotNull GuildMemberJoinEvent event) { Member member = event.getMember(); if (!shouldMemberBeMuted(member)) { return; diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/package-info.java index 80f8b3a701..17e5e3366c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/package-info.java @@ -2,8 +2,8 @@ * This package contains the command system and most commands of the bot. Commands can also be * created in different modules, if desired. *

- * Commands are registered in {@link org.togetherjava.tjbot.commands.Commands} and then picked up by - * the {@link org.togetherjava.tjbot.commands.system.CommandSystem}. + * Commands are registered in {@link org.togetherjava.tjbot.commands.Features} and then picked up by + * the {@link org.togetherjava.tjbot.commands.system.BotCore}. *

* Custom slash commands can be created by implementing * {@link org.togetherjava.tjbot.commands.SlashCommand} or using the adapter diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/CommandSystem.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java similarity index 82% rename from application/src/main/java/org/togetherjava/tjbot/commands/system/CommandSystem.java rename to application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java index 4be65e20ae..5b32efa476 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/CommandSystem.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java @@ -2,12 +2,15 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.AbstractChannel; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; import net.dv8tion.jda.api.events.interaction.SelectionMenuEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent; import net.dv8tion.jda.api.exceptions.ErrorHandler; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.interactions.commands.Command; @@ -16,8 +19,7 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.togetherjava.tjbot.commands.Commands; -import org.togetherjava.tjbot.commands.SlashCommand; +import org.togetherjava.tjbot.commands.*; import org.togetherjava.tjbot.commands.componentids.ComponentId; import org.togetherjava.tjbot.commands.componentids.ComponentIdParser; import org.togetherjava.tjbot.commands.componentids.ComponentIdStore; @@ -29,40 +31,60 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Function; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; /** - * The command system is the core of command handling in this application. + * The bot core is the core of command handling in this application. *

* It knows and manages all commands, registers them towards Discord and is the entry point of all * events. It forwards events to their corresponding commands and does the heavy lifting on all sort * of event parsing. *

*

- * Commands are made available via {@link Commands}, then the system has to be added to JDA as an + * Commands are made available via {@link Features}, then the system has to be added to JDA as an * event listener, using {@link net.dv8tion.jda.api.JDA#addEventListener(Object...)}. Afterwards, * the system is ready and will correctly forward events to all commands. */ -public final class CommandSystem extends ListenerAdapter implements SlashCommandProvider { - private static final Logger logger = LoggerFactory.getLogger(CommandSystem.class); +public final class BotCore extends ListenerAdapter implements SlashCommandProvider { + private static final Logger logger = LoggerFactory.getLogger(BotCore.class); private static final String RELOAD_COMMAND = "reload"; private static final ExecutorService COMMAND_SERVICE = Executors.newCachedThreadPool(); private final Map nameToSlashCommands; private final ComponentIdParser componentIdParser; private final ComponentIdStore componentIdStore; + private final Map channelNameToMessageReceiver = new HashMap<>(); /** * Creates a new command system which uses the given database to allow commands to persist data. *

- * Commands are fetched from {@link Commands}. + * Commands are fetched from {@link Features}. * * @param jda the JDA instance that this command system will be used with * @param database the database that commands may use to persist data */ @SuppressWarnings("ThisEscapedInObjectConstruction") - public CommandSystem(@NotNull JDA jda, @NotNull Database database) { - nameToSlashCommands = Commands.createSlashCommands(jda, database) - .stream() + public BotCore(@NotNull JDA jda, @NotNull Database database) { + Collection features = Features.createFeatures(jda, database); + + // Message receivers + features.stream() + .filter(MessageReceiver.class::isInstance) + .map(MessageReceiver.class::cast) + .forEach(messageReceiver -> channelNameToMessageReceiver + .put(messageReceiver.getChannelNamePattern(), messageReceiver)); + + // Event receivers + features.stream() + .filter(EventReceiver.class::isInstance) + .map(EventReceiver.class::cast) + .forEach(jda::addEventListener); + + // Slash commands + nameToSlashCommands = features.stream() + .filter(SlashCommand.class::isInstance) + .map(SlashCommand.class::cast) .collect(Collectors.toMap(SlashCommand::getName, Function.identity())); if (nameToSlashCommands.containsKey(RELOAD_COMMAND)) { @@ -72,7 +94,7 @@ public CommandSystem(@NotNull JDA jda, @NotNull Database database) { nameToSlashCommands.put(RELOAD_COMMAND, new ReloadCommand(this)); componentIdStore = new ComponentIdStore(database); - componentIdStore.addComponentIdRemovedListener(CommandSystem::onComponentIdRemoved); + componentIdStore.addComponentIdRemovedListener(BotCore::onComponentIdRemoved); componentIdParser = uuid -> componentIdStore.get(UUID.fromString(uuid)); nameToSlashCommands.values() .forEach(slashCommand -> slashCommand @@ -106,12 +128,30 @@ public void onReady(@NotNull ReadyEvent event) { .forEach(guild -> COMMAND_SERVICE.execute(() -> registerReloadCommand(guild))); // NOTE We do not have to wait for reload to complete for the command system to be ready // itself - logger.debug("Command system is now ready"); + logger.debug("Bot core is now ready"); + } - // Propagate the onReady event to all commands - // NOTE 'registerReloadCommands' will not be finished running, this does not wait for it - nameToSlashCommands.values() - .forEach(command -> COMMAND_SERVICE.execute(() -> command.onReady(event))); + @Override + public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) { + getMessageReceiversSubscribedTo(event.getChannel()) + .forEach(messageReceiver -> messageReceiver.onMessageReceived(event)); + } + + @Override + public void onGuildMessageUpdate(@NotNull GuildMessageUpdateEvent event) { + getMessageReceiversSubscribedTo(event.getChannel()) + .forEach(messageReceiver -> messageReceiver.onMessageUpdated(event)); + } + + private @NotNull Stream getMessageReceiversSubscribedTo( + @NotNull AbstractChannel channel) { + String channelName = channel.getName(); + return channelNameToMessageReceiver.entrySet() + .stream() + .filter(patternAndReceiver -> patternAndReceiver.getKey() + .matcher(channelName) + .matches()) + .map(Map.Entry::getValue); } @Override diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/package-info.java index ad1709f0f5..60670e1341 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/system/package-info.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/package-info.java @@ -1,5 +1,5 @@ /** * This package represents the core of the command system. The entry point is - * {@link org.togetherjava.tjbot.commands.system.CommandSystem}. + * {@link org.togetherjava.tjbot.commands.system.BotCore}. */ package org.togetherjava.tjbot.commands.system; diff --git a/logviewer/src/main/java/org/togetherjava/tjbot/logwatcher/entities/InstantWrapper.java b/logviewer/src/main/java/org/togetherjava/tjbot/logwatcher/entities/InstantWrapper.java index b32b114089..4754ad9b32 100644 --- a/logviewer/src/main/java/org/togetherjava/tjbot/logwatcher/entities/InstantWrapper.java +++ b/logviewer/src/main/java/org/togetherjava/tjbot/logwatcher/entities/InstantWrapper.java @@ -53,8 +53,9 @@ public Instant toInstant() { @Override public boolean equals(Object o) { - if (!(o instanceof InstantWrapper other)) + if (!(o instanceof InstantWrapper other)) { return false; + } return epochSecond == other.epochSecond && nanoOfSecond != other.nanoOfSecond; }