diff --git a/src/main/java/net/neoforged/camelot/commands/moderation/ModerationCommand.java b/src/main/java/net/neoforged/camelot/commands/moderation/ModerationCommand.java index 7850b31..be284f4 100644 --- a/src/main/java/net/neoforged/camelot/commands/moderation/ModerationCommand.java +++ b/src/main/java/net/neoforged/camelot/commands/moderation/ModerationCommand.java @@ -3,6 +3,9 @@ import com.google.common.base.Preconditions; import com.jagrosh.jdautilities.command.SlashCommand; import com.jagrosh.jdautilities.command.SlashCommandEvent; +import it.unimi.dsi.fastutil.longs.LongArraySet; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.longs.LongSets; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; @@ -13,13 +16,13 @@ import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.messages.MessageEditData; +import net.neoforged.camelot.BotMain; import net.neoforged.camelot.Database; +import net.neoforged.camelot.db.schemas.ModLogEntry; import net.neoforged.camelot.db.transactionals.ModLogsDAO; +import net.neoforged.camelot.log.ModerationActionRecorder; import net.neoforged.camelot.util.Utils; import org.jetbrains.annotations.Nullable; -import net.neoforged.camelot.BotMain; -import net.neoforged.camelot.db.schemas.ModLogEntry; -import net.neoforged.camelot.log.ModerationActionRecorder; import javax.annotation.ParametersAreNullableByDefault; import java.util.concurrent.CompletableFuture; @@ -31,6 +34,11 @@ */ public abstract class ModerationCommand extends SlashCommand { + /** + * The list of user having in-progress actions against them. + */ + public static final LongSet IN_PROGRESS = LongSets.synchronize(new LongArraySet()); + protected ModerationCommand() { this.guildOnly = true; } @@ -50,6 +58,18 @@ protected ModerationCommand() { @Nullable protected abstract ModerationAction createEntry(SlashCommandEvent event); + /** + * Checks if the given {@code action} can be executed. + *

+ * If impossible, it is up to the implementor to reply. + * + * @param action the moderation action + * @return a CF returning the result of this action. + */ + protected CompletableFuture canExecute(SlashCommandEvent event, ModerationAction action) { + return CompletableFuture.completedFuture(true); + } + @Override protected final void execute(SlashCommandEvent event) { final ModerationAction action; @@ -63,26 +83,39 @@ protected final void execute(SlashCommandEvent event) { if (action == null) return; final ModLogEntry entry = action.entry; + if (!IN_PROGRESS.add(entry.user())) { + event.reply("User is already being moderated. Please wait...").setEphemeral(true).queue(); + return; + } - entry.setId(Database.main().withExtension(ModLogsDAO.class, dao -> dao.insert(entry))); event.deferReply().queue(); - event.getJDA().retrieveUserById(entry.user()) - .submit() - .thenCompose(usr -> { - if (shouldDMUser) { - return dmUser(entry, usr).submit(); - } - return CompletableFuture.completedFuture(null); - }) - .whenComplete((msg, t) -> { - if (t == null) { - logAndExecute(action, event.getHook(), true); - } else { - logAndExecute(action, event.getHook(), false); - if (t instanceof ErrorResponseException ex && ex.getErrorResponse() != ErrorResponse.CANNOT_SEND_TO_USER) { - BotMain.LOGGER.error("Encountered exception DMing user {}: ", entry.user(), ex); - } + canExecute(event, action) + .thenAccept(pos -> { + if (!pos) { + IN_PROGRESS.remove(entry.user()); + return; } + + entry.setId(Database.main().withExtension(ModLogsDAO.class, dao -> dao.insert(entry))); + event.getJDA().retrieveUserById(entry.user()) + .submit() + .thenCompose(usr -> { + if (shouldDMUser) { + return dmUser(entry, usr).submit(); + } + return CompletableFuture.completedFuture(null); + }) + .whenComplete((_, t) -> { + if (t == null) { + logAndExecute(action, event.getHook(), true); + } else { + logAndExecute(action, event.getHook(), false); + if (t instanceof ErrorResponseException ex && ex.getErrorResponse() != ErrorResponse.CANNOT_SEND_TO_USER) { + BotMain.LOGGER.error("Encountered exception DMing user {}: ", entry.user(), ex); + } + } + IN_PROGRESS.remove(entry.user()); + }); }); } @@ -167,7 +200,10 @@ protected void logAndExecute(ModerationAction action, InteractionHook interac } return handle.flatMap(_ -> edit); }) - .queue(); + .queue(_ -> IN_PROGRESS.remove(action.entry().user()), err -> { + IN_PROGRESS.remove(action.entry().user()); + RestAction.getDefaultFailure().accept(err); + }); } /** diff --git a/src/main/java/net/neoforged/camelot/commands/moderation/UnbanCommand.java b/src/main/java/net/neoforged/camelot/commands/moderation/UnbanCommand.java index 966de60..911463b 100644 --- a/src/main/java/net/neoforged/camelot/commands/moderation/UnbanCommand.java +++ b/src/main/java/net/neoforged/camelot/commands/moderation/UnbanCommand.java @@ -8,10 +8,11 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.requests.RestAction; -import org.jetbrains.annotations.Nullable; import net.neoforged.camelot.db.schemas.ModLogEntry; +import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * The command used to unban a user. @@ -42,6 +43,15 @@ protected ModerationAction createEntry(SlashCommandEvent event) { ); } + @Override + protected CompletableFuture canExecute(SlashCommandEvent event, ModerationAction action) { + return event.getGuild().retrieveBan(UserSnowflake.fromId(action.entry().user())) + .submit() + .thenApply(_ -> true) + .exceptionallyCompose(_ -> event.getHook().editOriginal("User is not banned.") + .submit().thenApply(_ -> false)); + } + @Override @SuppressWarnings("DataFlowIssue") protected RestAction handle(User user, ModerationAction action) {