Skip to content

Fix bug with /audit unable to retrieve users #527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 28, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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) {
Expand All @@ -56,63 +55,6 @@ public AuditCommand(@NotNull ModerationActionsStore actionsStore) {
this.actionsStore = Objects.requireNonNull(actionsStore);
}

private static @NotNull EmbedBuilder createSummaryEmbed(@NotNull User user,
@NotNull Collection<ActionRecord> 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<ActionRecord> 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<ModerationAction, Long> actionTypeToCount = actions.stream()
.collect(Collectors.groupingBy(ActionRecord::actionType, Collectors.counting()));

String typeCountSummary = actionTypeToCount.entrySet()
.stream()
.filter(typeAndCount -> typeAndCount.getValue() > 0)
.sorted(Map.Entry.<ModerationAction, Long>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<Instant, String> 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 =
Expand All @@ -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")
Expand All @@ -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<Message> auditUser(long guildId, long targetId, long callerId,
int pageNumber, @NotNull JDA jda) {
List<ActionRecord> actions = actionsStore.getActionsByTargetAscending(guildId, targetId);
List<List<ActionRecord>> 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<List<ActionRecord>> groupActionsByPages(
@NotNull List<ActionRecord> actions) {
List<List<ActionRecord>> groupedActions = new ArrayList<>();
Expand All @@ -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<ActionRecord> actions = actionsStore.getActionsByTargetAscending(guildId, targetId);
List<List<ActionRecord>> 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<ActionRecord> 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<ActionRecord> 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<ModerationAction, Long> actionTypeToCount = actions.stream()
.collect(Collectors.groupingBy(ActionRecord::actionType, Collectors.counting()));

String typeCountSummary = actionTypeToCount.entrySet()
.stream()
.filter(typeAndCount -> typeAndCount.getValue() > 0)
.sorted(Map.Entry.<ModerationAction, Long>comparingByValue().reversed())
.map(typeAndCount -> "- **%s**: %d".formatted(typeAndCount.getKey(),
typeAndCount.getValue()))
.collect(Collectors.joining("\n"));

return shortSummary + "\n" + typeCountSummary;
}

private @NotNull RestAction<EmbedBuilder> attachEmbedFields(@NotNull EmbedBuilder auditEmbed,
@NotNull List<? extends List<ActionRecord>> groupedActions, int pageNumber,
int totalPages, @NotNull JDA jda) {
if (groupedActions.isEmpty()) {
return new MessageBuilder(audit.build()).build();
return new CompletedRestAction<>(jda, auditEmbed);
}

List<RestAction<MessageEmbed.Field>> 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<MessageEmbed.Field> 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();
}

Expand All @@ -213,11 +244,11 @@ private boolean handleChecks(@NotNull Member bot, @NotNull Member author,

@Override
public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List<String> 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();

Expand All @@ -231,7 +262,8 @@ public void onButtonClick(@NotNull ButtonInteractionEvent event, @NotNull List<S
long targetId = Long.parseLong(args.get(1));
int pageToDisplay = currentPage + turnPageBy;

event.editMessage(auditUser(guildId, targetId, interactorId, pageToDisplay, event.getJDA()))
auditUser(guildId, targetId, buttonUserId, pageToDisplay, event.getJDA())
.flatMap(event::editMessage)
.queue();
}
}