diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java index 0ff92a1a0b..2774271f5f 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java @@ -11,18 +11,17 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.TimeUtil; - +import net.dv8tion.jda.internal.requests.CompletedRestAction; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; - import org.togetherjava.tjbot.commands.SlashCommandAdapter; import org.togetherjava.tjbot.commands.SlashCommandVisibility; import java.time.Instant; import java.time.ZoneOffset; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; /** @@ -36,14 +35,14 @@ public final class AuditCommand extends SlashCommandAdapter { private static final String TARGET_OPTION = "user"; private static final String COMMAND_NAME = "audit"; private static final String ACTION_VERB = "audit"; - private static final int MAX_PAGE_LENGTH = 25; + private static final int MAX_PAGE_LENGTH = 10; private static final String PREVIOUS_BUTTON_LABEL = "⬅"; private static final String NEXT_BUTTON_LABEL = "➡"; private final ModerationActionsStore actionsStore; /** * Constructs an instance. - * + * * @param actionsStore used to store actions issued by this command */ public AuditCommand(@NotNull ModerationActionsStore actionsStore) { @@ -56,63 +55,6 @@ public AuditCommand(@NotNull ModerationActionsStore actionsStore) { this.actionsStore = Objects.requireNonNull(actionsStore); } - private static @NotNull EmbedBuilder createSummaryEmbed(@NotNull User user, - @NotNull Collection actions) { - return new EmbedBuilder().setTitle("Audit log of **%s**".formatted(user.getAsTag())) - .setAuthor(user.getName(), null, user.getAvatarUrl()) - .setDescription(createSummaryMessageDescription(actions)) - .setColor(ModerationUtils.AMBIENT_COLOR); - } - - private static @NotNull String createSummaryMessageDescription( - @NotNull Collection actions) { - int actionAmount = actions.size(); - - String shortSummary = "There are **%s actions** against the user." - .formatted(actionAmount == 0 ? "no" : actionAmount); - - if (actionAmount == 0) { - return shortSummary; - } - - // Summary of all actions with their count, like "- Warn: 5", descending - Map actionTypeToCount = actions.stream() - .collect(Collectors.groupingBy(ActionRecord::actionType, Collectors.counting())); - - String typeCountSummary = actionTypeToCount.entrySet() - .stream() - .filter(typeAndCount -> typeAndCount.getValue() > 0) - .sorted(Map.Entry.comparingByValue().reversed()) - .map(typeAndCount -> "- **%s**: %d".formatted(typeAndCount.getKey(), - typeAndCount.getValue())) - .collect(Collectors.joining("\n")); - - return shortSummary + "\n" + typeCountSummary; - } - - private static @NotNull MessageEmbed.Field actionToField(@NotNull ActionRecord action, - @NotNull JDA jda) { - Function formatTime = instant -> { - if (instant == null) { - return ""; - } - return TimeUtil.getDateTimeString(instant.atOffset(ZoneOffset.UTC)); - }; - - User author = jda.getUserById(action.authorId()); - - Instant expiresAt = action.actionExpiresAt(); - String expiresAtFormatted = expiresAt == null ? "" - : "\nTemporary action, expires at: " + formatTime.apply(expiresAt); - - return new MessageEmbed.Field( - action.actionType().name() + " by " - + (author == null ? "(unknown user)" : author.getAsTag()), - action.reason() + "\nIssued at: " + formatTime.apply(action.issuedAt()) - + expiresAtFormatted, - false); - } - @Override public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { OptionMapping targetOption = @@ -127,10 +69,8 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) { return; } - event - .reply(auditUser(guild.getIdLong(), target.getIdLong(), event.getMember().getIdLong(), - 1, event.getJDA())) - .queue(); + auditUser(guild.getIdLong(), target.getIdLong(), event.getMember().getIdLong(), -1, + event.getJDA()).flatMap(event::reply).queue(); } @SuppressWarnings("BooleanMethodNameMustStartWithQuestion") @@ -143,6 +83,32 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, return ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, target, event); } + /** + * @param pageNumber page number to display when actions are divided into pages and each page + * can contain {@link AuditCommand#MAX_PAGE_LENGTH} actions, {@code -1} encodes the last + * page + */ + private @NotNull RestAction auditUser(long guildId, long targetId, long callerId, + int pageNumber, @NotNull JDA jda) { + List actions = actionsStore.getActionsByTargetAscending(guildId, targetId); + List> groupedActions = groupActionsByPages(actions); + int totalPages = groupedActions.size(); + + int pageNumberInLimits; + if (pageNumber == -1) { + pageNumberInLimits = totalPages; + } else { + pageNumberInLimits = clamp(1, pageNumber, totalPages); + } + + return jda.retrieveUserById(targetId) + .map(user -> createSummaryEmbed(user, actions)) + .flatMap(auditEmbed -> attachEmbedFields(auditEmbed, groupedActions, pageNumberInLimits, + totalPages, jda)) + .map(auditEmbed -> attachPageTurnButtons(auditEmbed, pageNumberInLimits, totalPages, + guildId, targetId, callerId)); + } + private @NotNull List> groupActionsByPages( @NotNull List actions) { List> groupedActions = new ArrayList<>(); @@ -157,47 +123,112 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, return groupedActions; } - /** - * @param pageNumber page number to display when actions are divided into pages and each page - * can contain {@link AuditCommand#MAX_PAGE_LENGTH} actions - */ - private @NotNull Message auditUser(long guildId, long targetId, long callerId, int pageNumber, - @NotNull JDA jda) { - List actions = actionsStore.getActionsByTargetAscending(guildId, targetId); - List> groupedActions = groupActionsByPages(actions); - int totalPages = groupedActions.size(); + private static int clamp(int minInclusive, int value, int maxInclusive) { + return Math.min(Math.max(minInclusive, value), maxInclusive); + } + + private static @NotNull EmbedBuilder createSummaryEmbed(@NotNull User user, + @NotNull Collection actions) { + return new EmbedBuilder().setTitle("Audit log of **%s**".formatted(user.getAsTag())) + .setAuthor(user.getName(), null, user.getAvatarUrl()) + .setDescription(createSummaryMessageDescription(actions)) + .setColor(ModerationUtils.AMBIENT_COLOR); + } - // Handles the case of too low page number and too high page number - pageNumber = Math.max(1, pageNumber); - pageNumber = Math.min(totalPages, pageNumber); + private static @NotNull String createSummaryMessageDescription( + @NotNull Collection actions) { + int actionAmount = actions.size(); - EmbedBuilder audit = createSummaryEmbed(jda.retrieveUserById(targetId).complete(), actions); + String shortSummary = "There are **%s actions** against the user." + .formatted(actionAmount == 0 ? "no" : actionAmount); + + if (actionAmount == 0) { + return shortSummary; + } + // Summary of all actions with their count, like "- Warn: 5", descending + Map actionTypeToCount = actions.stream() + .collect(Collectors.groupingBy(ActionRecord::actionType, Collectors.counting())); + + String typeCountSummary = actionTypeToCount.entrySet() + .stream() + .filter(typeAndCount -> typeAndCount.getValue() > 0) + .sorted(Map.Entry.comparingByValue().reversed()) + .map(typeAndCount -> "- **%s**: %d".formatted(typeAndCount.getKey(), + typeAndCount.getValue())) + .collect(Collectors.joining("\n")); + + return shortSummary + "\n" + typeCountSummary; + } + + private @NotNull RestAction attachEmbedFields(@NotNull EmbedBuilder auditEmbed, + @NotNull List> groupedActions, int pageNumber, + int totalPages, @NotNull JDA jda) { if (groupedActions.isEmpty()) { - return new MessageBuilder(audit.build()).build(); + return new CompletedRestAction<>(jda, auditEmbed); } + List> embedFieldTasks = new ArrayList<>(); groupedActions.get(pageNumber - 1) - .forEach(action -> audit.addField(actionToField(action, jda))); + .forEach(action -> embedFieldTasks.add(actionToField(action, jda))); + + return RestAction.allOf(embedFieldTasks).map(embedFields -> { + embedFields.forEach(auditEmbed::addField); + + auditEmbed.setFooter("Page: " + pageNumber + "/" + totalPages); + return auditEmbed; + }); + } + + private static @NotNull RestAction actionToField( + @NotNull ActionRecord action, @NotNull JDA jda) { + return jda.retrieveUserById(action.authorId()) + .map(author -> author == null ? "(unknown user)" : author.getAsTag()) + .map(authorText -> { + String expiresAtFormatted = action.actionExpiresAt() == null ? "" + : "\nTemporary action, expires at: " + formatTime(action.actionExpiresAt()); + + String fieldName = "%s by %s".formatted(action.actionType().name(), authorText); + String fieldDescription = """ + %s + Issued at: %s%s + """.formatted(action.reason(), formatTime(action.issuedAt()), + expiresAtFormatted); + + return new MessageEmbed.Field(fieldName, fieldDescription, false); + }); + } + + private static @NotNull String formatTime(@NotNull Instant when) { + return TimeUtil.getDateTimeString(when.atOffset(ZoneOffset.UTC)); + } + + private @NotNull Message attachPageTurnButtons(@NotNull EmbedBuilder auditEmbed, int pageNumber, + int totalPages, long guildId, long targetId, long callerId) { + var messageBuilder = new MessageBuilder(auditEmbed.build()); + + if (totalPages <= 1) { + return messageBuilder.build(); + } + ActionRow pageTurnButtons = + createPageTurnButtons(guildId, targetId, callerId, pageNumber, totalPages); - return new MessageBuilder(audit.setFooter("Page: " + pageNumber + "/" + totalPages).build()) - .setActionRows(makeActionRow(guildId, targetId, callerId, pageNumber, totalPages)) - .build(); + return messageBuilder.setActionRows(pageTurnButtons).build(); } - private @NotNull ActionRow makeActionRow(long guildId, long targetId, long callerId, + private @NotNull ActionRow createPageTurnButtons(long guildId, long targetId, long callerId, int pageNumber, int totalPages) { int previousButtonTurnPageBy = -1; Button previousButton = createPageTurnButton(PREVIOUS_BUTTON_LABEL, guildId, targetId, callerId, pageNumber, previousButtonTurnPageBy); - if (pageNumber == 1) { + if (pageNumber <= 1) { previousButton = previousButton.asDisabled(); } int nextButtonTurnPageBy = 1; Button nextButton = createPageTurnButton(NEXT_BUTTON_LABEL, guildId, targetId, callerId, pageNumber, nextButtonTurnPageBy); - if (pageNumber == totalPages) { + if (pageNumber >= totalPages) { nextButton = nextButton.asDisabled(); } @@ -213,11 +244,11 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author, @Override public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List args) { - long callerId = Long.parseLong(args.get(2)); - long interactorId = event.getMember().getIdLong(); + long commandUserId = Long.parseLong(args.get(2)); + long buttonUserId = event.getMember().getIdLong(); - if (callerId != interactorId) { - event.reply("Only the user who triggered the command can use these buttons.") + if (commandUserId != buttonUserId) { + event.reply("Only the user who triggered the command can turn pages.") .setEphemeral(true) .queue(); @@ -231,7 +262,8 @@ public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List