From c40b4e8e44a18c783b4b13d17b332a5051ba5ea2 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 3 Nov 2021 16:13:07 +0100 Subject: [PATCH] Added more response infrastructure and implemented a purge command. --- .../javabot2/command/ResponseException.java | 28 ++++ .../javabot2/command/Responses.java | 148 ++++++++++++++++++ .../javabot2/command/SlashCommandHandler.java | 4 + .../command/SlashCommandListener.java | 23 ++- .../command/{ => data}/CommandConfig.java | 2 +- .../command/{ => data}/CommandDataLoader.java | 2 +- .../{ => data}/CommandPrivilegeConfig.java | 2 +- .../command/{ => data}/OptionConfig.java | 6 +- .../command/{ => data}/SubCommandConfig.java | 2 +- .../{ => data}/SubCommandGroupConfig.java | 2 +- .../systems/moderation/PurgeCommand.java | 29 +++- src/main/resources/commands/moderation.yaml | 14 +- 12 files changed, 235 insertions(+), 27 deletions(-) create mode 100644 src/main/java/net/javadiscord/javabot2/command/ResponseException.java create mode 100644 src/main/java/net/javadiscord/javabot2/command/Responses.java rename src/main/java/net/javadiscord/javabot2/command/{ => data}/CommandConfig.java (97%) rename src/main/java/net/javadiscord/javabot2/command/{ => data}/CommandDataLoader.java (94%) rename src/main/java/net/javadiscord/javabot2/command/{ => data}/CommandPrivilegeConfig.java (97%) rename src/main/java/net/javadiscord/javabot2/command/{ => data}/OptionConfig.java (83%) rename src/main/java/net/javadiscord/javabot2/command/{ => data}/SubCommandConfig.java (95%) rename src/main/java/net/javadiscord/javabot2/command/{ => data}/SubCommandGroupConfig.java (95%) diff --git a/src/main/java/net/javadiscord/javabot2/command/ResponseException.java b/src/main/java/net/javadiscord/javabot2/command/ResponseException.java new file mode 100644 index 0000000..1c527b9 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/ResponseException.java @@ -0,0 +1,28 @@ +package net.javadiscord.javabot2.command; + +import lombok.Getter; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +import java.util.function.Supplier; + +/** + * An exception which can be thrown so that the bot can respond with a well + * formatted error or warning response, while still allowing you to take + * advantage of throwing an exception up the call stack. + */ +public class ResponseException extends Exception { + @Getter + private final InteractionImmediateResponseBuilder responseBuilder; + + public ResponseException(InteractionImmediateResponseBuilder responseBuilder) { + this.responseBuilder = responseBuilder; + } + + public static Supplier warning(String message) { + return () -> new ResponseException(Responses.deferredWarningBuilder().message(message).build()); + } + + public static Supplier error(String message) { + return () -> new ResponseException(Responses.deferredErrorBuilder().message(message).build()); + } +} diff --git a/src/main/java/net/javadiscord/javabot2/command/Responses.java b/src/main/java/net/javadiscord/javabot2/command/Responses.java new file mode 100644 index 0000000..54fb978 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/Responses.java @@ -0,0 +1,148 @@ +package net.javadiscord.javabot2.command; + +import org.javacord.api.entity.message.MessageFlag; +import org.javacord.api.entity.message.embed.EmbedBuilder; +import org.javacord.api.event.interaction.SlashCommandCreateEvent; +import org.javacord.api.interaction.InteractionBase; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; +import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater; + +import java.awt.*; +import java.util.concurrent.CompletableFuture; + +/** + * Provides methods for standardized replies to interaction events. + */ +public class Responses { + public static InteractionImmediateResponseBuilder success(InteractionBase interaction, String title, String message) { + return reply(interaction, title, message, Color.GREEN, true); + } + + public static ResponseBuilder successBuilder(InteractionBase interaction) { + return new ResponseBuilder(interaction, Color.GREEN).title("Success"); + } + + public static ResponseBuilder successBuilder(SlashCommandCreateEvent event) { + return successBuilder(event.getSlashCommandInteraction()); + } + + public static InteractionImmediateResponseBuilder info(InteractionBase interaction, String title, String message) { + return reply(interaction, title, message, Color.BLUE, true); + } + + public static ResponseBuilder infoBuilder(InteractionBase interaction) { + return new ResponseBuilder(interaction, Color.BLUE).title("Info"); + } + + public static ResponseBuilder infoBuilder(SlashCommandCreateEvent event) { + return infoBuilder(event.getSlashCommandInteraction()); + } + + public static InteractionImmediateResponseBuilder warning(InteractionBase interaction, String title, String message) { + return reply(interaction, title, message, Color.ORANGE, true); + } + + public static InteractionImmediateResponseBuilder warning(InteractionBase interaction, String message) { + return warning(interaction, "Warning", message); + } + + public static ResponseBuilder warningBuilder(InteractionBase interaction) { + return new ResponseBuilder(interaction, Color.ORANGE).title("Warning"); + } + + public static ResponseBuilder warningBuilder(SlashCommandCreateEvent event) { + return warningBuilder(event.getSlashCommandInteraction()); + } + + public static ResponseBuilder deferredWarningBuilder() { + return new ResponseBuilder(Color.ORANGE).title("Warning"); + } + + public static InteractionImmediateResponseBuilder error(InteractionBase interaction, String message) { + return reply(interaction, "An Error Occurred", message, Color.RED, true); + } + + public static ResponseBuilder errorBuilder(InteractionBase interaction) { + return new ResponseBuilder(interaction, Color.RED).title("An Error Occurred"); + } + + public static ResponseBuilder errorBuilder(SlashCommandCreateEvent event) { + return errorBuilder(event.getSlashCommandInteraction()); + } + + public static ResponseBuilder deferredErrorBuilder() { + return new ResponseBuilder(Color.RED).title("An Error Occurred"); + } + + private static InteractionImmediateResponseBuilder reply( + InteractionBase interaction, + String title, + String message, + Color color, + boolean ephemeral + ) { + var responder = interaction.createImmediateResponder() + .addEmbed(new EmbedBuilder() + .setTitle(title) + .setColor(color) + .setTimestampToNow() + .setDescription(message)); + if (ephemeral) { + responder.setFlags(MessageFlag.EPHEMERAL); + } + return responder; + } + + /** + * A builder that's used to construct a response using a fluent interface. + */ + public static class ResponseBuilder { + private InteractionBase interaction; + private String title = null; + private String message = null; + private final Color color; + private boolean ephemeral = true; + + private ResponseBuilder(InteractionBase interaction, Color color) { + this.interaction = interaction; + this.color = color; + } + + private ResponseBuilder(Color color) { + this(null, color); + } + + public ResponseBuilder title(String title) { + this.title = title; + return this; + } + + public ResponseBuilder message(String message) { + this.message = message; + return this; + } + + public ResponseBuilder makePublic() { + this.ephemeral = false; + return this; + } + + public InteractionImmediateResponseBuilder build() { + return reply(interaction, title, message, color, ephemeral); + } + + public InteractionImmediateResponseBuilder build(InteractionBase interaction) { + this.interaction = interaction; + return build(); + } + + public CompletableFuture respond(InteractionBase interaction) { + this.interaction = interaction; + return build().respond(); + } + + public CompletableFuture respond() { + return this.build().respond(); + } + } +} diff --git a/src/main/java/net/javadiscord/javabot2/command/SlashCommandHandler.java b/src/main/java/net/javadiscord/javabot2/command/SlashCommandHandler.java index 527e2a3..b1e2b1a 100644 --- a/src/main/java/net/javadiscord/javabot2/command/SlashCommandHandler.java +++ b/src/main/java/net/javadiscord/javabot2/command/SlashCommandHandler.java @@ -3,6 +3,10 @@ import org.javacord.api.interaction.SlashCommandInteraction; import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; +/** + * An interface that should be implemented by any class that is defined as a + * handler in any command configuration file. + */ public interface SlashCommandHandler { InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) throws Exception; } diff --git a/src/main/java/net/javadiscord/javabot2/command/SlashCommandListener.java b/src/main/java/net/javadiscord/javabot2/command/SlashCommandListener.java index af813b8..131eade 100644 --- a/src/main/java/net/javadiscord/javabot2/command/SlashCommandListener.java +++ b/src/main/java/net/javadiscord/javabot2/command/SlashCommandListener.java @@ -1,8 +1,9 @@ package net.javadiscord.javabot2.command; import lombok.extern.slf4j.Slf4j; +import net.javadiscord.javabot2.command.data.CommandConfig; +import net.javadiscord.javabot2.command.data.CommandDataLoader; import org.javacord.api.DiscordApi; -import org.javacord.api.entity.message.MessageFlag; import org.javacord.api.event.interaction.SlashCommandCreateEvent; import org.javacord.api.interaction.ServerSlashCommandPermissionsBuilder; import org.javacord.api.interaction.SlashCommandBuilder; @@ -21,6 +22,13 @@ public final class SlashCommandListener implements SlashCommandCreateListener { private final Map commandHandlers = new HashMap<>(); + /** + * Constructs a new slash command listener using the given Discord api, and + * loads commands from configuration YAML files according to the list of + * resources. + * @param api The Discord api to use. + * @param resources The list of classpath resources to load commands from. + */ public SlashCommandListener(DiscordApi api, String... resources) { registerSlashCommands(api, resources) .thenAcceptAsync(commandHandlers::putAll); @@ -32,17 +40,18 @@ public void onSlashCommandCreate(SlashCommandCreateEvent event) { if (handler != null) { try { handler.handle(event.getSlashCommandInteraction()).respond(); + } catch (ResponseException e) { + e.getResponseBuilder().respond(); } catch (Exception e) { log.error("An error occurred while handling a slash command.", e); - event.getSlashCommandInteraction().createImmediateResponder() - .setFlags(MessageFlag.EPHEMERAL) - .append("An error occurred.") + Responses.errorBuilder(event) + .message("An error occurred while executing the command.") .respond(); } } else { - event.getSlashCommandInteraction().createImmediateResponder() - .setFlags(MessageFlag.EPHEMERAL) - .append("There is no associated handler for this command. Please contact an administrator if this error persists.") + Responses.warningBuilder(event) + .title("No Handler") + .message("There is no associated handler for this command. Please contact an administrator if this error persists.") .respond(); } } diff --git a/src/main/java/net/javadiscord/javabot2/command/CommandConfig.java b/src/main/java/net/javadiscord/javabot2/command/data/CommandConfig.java similarity index 97% rename from src/main/java/net/javadiscord/javabot2/command/CommandConfig.java rename to src/main/java/net/javadiscord/javabot2/command/data/CommandConfig.java index c094010..8f97e17 100644 --- a/src/main/java/net/javadiscord/javabot2/command/CommandConfig.java +++ b/src/main/java/net/javadiscord/javabot2/command/data/CommandConfig.java @@ -1,4 +1,4 @@ -package net.javadiscord.javabot2.command; +package net.javadiscord.javabot2.command.data; import lombok.Data; import org.javacord.api.interaction.SlashCommandBuilder; diff --git a/src/main/java/net/javadiscord/javabot2/command/CommandDataLoader.java b/src/main/java/net/javadiscord/javabot2/command/data/CommandDataLoader.java similarity index 94% rename from src/main/java/net/javadiscord/javabot2/command/CommandDataLoader.java rename to src/main/java/net/javadiscord/javabot2/command/data/CommandDataLoader.java index 57781db..eb0e7e6 100644 --- a/src/main/java/net/javadiscord/javabot2/command/CommandDataLoader.java +++ b/src/main/java/net/javadiscord/javabot2/command/data/CommandDataLoader.java @@ -1,4 +1,4 @@ -package net.javadiscord.javabot2.command; +package net.javadiscord.javabot2.command.data; import org.yaml.snakeyaml.Yaml; diff --git a/src/main/java/net/javadiscord/javabot2/command/CommandPrivilegeConfig.java b/src/main/java/net/javadiscord/javabot2/command/data/CommandPrivilegeConfig.java similarity index 97% rename from src/main/java/net/javadiscord/javabot2/command/CommandPrivilegeConfig.java rename to src/main/java/net/javadiscord/javabot2/command/data/CommandPrivilegeConfig.java index 85877f1..a22882f 100644 --- a/src/main/java/net/javadiscord/javabot2/command/CommandPrivilegeConfig.java +++ b/src/main/java/net/javadiscord/javabot2/command/data/CommandPrivilegeConfig.java @@ -1,4 +1,4 @@ -package net.javadiscord.javabot2.command; +package net.javadiscord.javabot2.command.data; import lombok.Data; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/net/javadiscord/javabot2/command/OptionConfig.java b/src/main/java/net/javadiscord/javabot2/command/data/OptionConfig.java similarity index 83% rename from src/main/java/net/javadiscord/javabot2/command/OptionConfig.java rename to src/main/java/net/javadiscord/javabot2/command/data/OptionConfig.java index d56d4b8..fbba5f4 100644 --- a/src/main/java/net/javadiscord/javabot2/command/OptionConfig.java +++ b/src/main/java/net/javadiscord/javabot2/command/data/OptionConfig.java @@ -1,7 +1,8 @@ -package net.javadiscord.javabot2.command; +package net.javadiscord.javabot2.command.data; import lombok.Data; import org.javacord.api.interaction.SlashCommandOptionBuilder; +import org.javacord.api.interaction.SlashCommandOptionChoiceBuilder; import org.javacord.api.interaction.SlashCommandOptionType; /** @@ -16,12 +17,11 @@ public class OptionConfig { private boolean required; public SlashCommandOptionBuilder toData() { - var builder = new SlashCommandOptionBuilder() + return new SlashCommandOptionBuilder() .setType(SlashCommandOptionType.valueOf(this.type.toUpperCase())) .setName(this.name) .setDescription(this.description) .setRequired(this.required); - return builder; } @Override diff --git a/src/main/java/net/javadiscord/javabot2/command/SubCommandConfig.java b/src/main/java/net/javadiscord/javabot2/command/data/SubCommandConfig.java similarity index 95% rename from src/main/java/net/javadiscord/javabot2/command/SubCommandConfig.java rename to src/main/java/net/javadiscord/javabot2/command/data/SubCommandConfig.java index a886919..45cb197 100644 --- a/src/main/java/net/javadiscord/javabot2/command/SubCommandConfig.java +++ b/src/main/java/net/javadiscord/javabot2/command/data/SubCommandConfig.java @@ -1,4 +1,4 @@ -package net.javadiscord.javabot2.command; +package net.javadiscord.javabot2.command.data; import lombok.Data; import org.javacord.api.interaction.SlashCommandOptionBuilder; diff --git a/src/main/java/net/javadiscord/javabot2/command/SubCommandGroupConfig.java b/src/main/java/net/javadiscord/javabot2/command/data/SubCommandGroupConfig.java similarity index 95% rename from src/main/java/net/javadiscord/javabot2/command/SubCommandGroupConfig.java rename to src/main/java/net/javadiscord/javabot2/command/data/SubCommandGroupConfig.java index 98b15fb..6c581f0 100644 --- a/src/main/java/net/javadiscord/javabot2/command/SubCommandGroupConfig.java +++ b/src/main/java/net/javadiscord/javabot2/command/data/SubCommandGroupConfig.java @@ -1,4 +1,4 @@ -package net.javadiscord.javabot2.command; +package net.javadiscord.javabot2.command.data; import lombok.Data; import org.javacord.api.interaction.SlashCommandOptionBuilder; diff --git a/src/main/java/net/javadiscord/javabot2/systems/moderation/PurgeCommand.java b/src/main/java/net/javadiscord/javabot2/systems/moderation/PurgeCommand.java index 38e8b51..3c3c989 100644 --- a/src/main/java/net/javadiscord/javabot2/systems/moderation/PurgeCommand.java +++ b/src/main/java/net/javadiscord/javabot2/systems/moderation/PurgeCommand.java @@ -1,13 +1,36 @@ package net.javadiscord.javabot2.systems.moderation; +import lombok.extern.slf4j.Slf4j; +import net.javadiscord.javabot2.Bot; +import net.javadiscord.javabot2.command.ResponseException; +import net.javadiscord.javabot2.command.Responses; import net.javadiscord.javabot2.command.SlashCommandHandler; +import org.javacord.api.entity.message.Message; +import org.javacord.api.entity.user.User; import org.javacord.api.interaction.SlashCommandInteraction; import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; +@Slf4j public class PurgeCommand implements SlashCommandHandler { @Override - public InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) { - return interaction.createImmediateResponder() - .append("Not yet implemented"); + public InteractionImmediateResponseBuilder handle(SlashCommandInteraction interaction) throws Exception { + var until = interaction.getOptionStringValueByName("until") + .orElseThrow(ResponseException.warning("Missing required parameter.")); + var userOption = interaction.getOptionUserValueByName("user"); + var channel = interaction.getChannel() + .orElseThrow(ResponseException.warning("This command can only be used in text channels.")); + channel.getMessageById(until).thenAcceptAsync(message -> Bot.asyncPool.submit(() -> purge(message, userOption.orElse(null)))); + return Responses.info(interaction, "Purge Started", "Messages will be deleted!"); + } + + private void purge(Message until, User user) { + log.info("Purging all messages in {} until {}.", until.getServerTextChannel().orElseThrow().getName(), until.getId()); + until.getMessagesAfterAsStream() + .filter(message -> (user == null || message.getAuthor().getId() == user.getId())) + .forEach(message -> message.delete().join()); + if (user == null || until.getAuthor().getId() == user.getId()) { + until.delete().join(); + } + log.info("Purge completed."); } } diff --git a/src/main/resources/commands/moderation.yaml b/src/main/resources/commands/moderation.yaml index b4d498a..1920227 100644 --- a/src/main/resources/commands/moderation.yaml +++ b/src/main/resources/commands/moderation.yaml @@ -1,20 +1,16 @@ - name: purge description: Deletes messages from a channel. - handler: net.javadiscord.javabot2.systems.moderation.PruneCommand + handler: net.javadiscord.javabot2.systems.moderation.PurgeCommand enabledByDefault: false privileges: - type: ROLE id: moderation.staffRoleId options: - - name: amount - description: Number of messages to remove. If left blank, all messages will be removed. - type: INTEGER - required: false + - name: until + description: All messages from now up to (and including) the given message id will be removed. + type: STRING + required: true - name: user description: The user whose messages to remove. If left blank, messages from any user are removed. type: USER required: false - - name: archive - description: If true, save removed messages in an archive. - type: BOOLEAN - required: false