From 98c90a74143c0c75b2dda9a55b67b64c74f739f6 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sun, 19 Feb 2023 00:05:37 +0800 Subject: [PATCH 1/5] Switch ModerationEvents to new audit log event This lessens our dependence on Utils#getAuditLog, which is prone to timing issues (whether the audit log is updated or the gateway event is sent first). --- .../thelistener/events/ModerationEvents.java | 275 +++++++++--------- 1 file changed, 132 insertions(+), 143 deletions(-) diff --git a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/ModerationEvents.java b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/ModerationEvents.java index 0243ccfd..d6d333c8 100644 --- a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/ModerationEvents.java +++ b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/ModerationEvents.java @@ -24,32 +24,31 @@ import com.mcmoddev.mmdbot.core.event.moderation.WarningEvent; import com.mcmoddev.mmdbot.core.util.webhook.WebhookManager; import com.mcmoddev.mmdbot.thelistener.util.LoggingType; -import com.mcmoddev.mmdbot.thelistener.util.Utils; import io.github.matyrobbrt.eventdispatcher.SubscribeEvent; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.audit.ActionType; +import net.dv8tion.jda.api.audit.AuditLogChange; +import net.dv8tion.jda.api.audit.AuditLogEntry; import net.dv8tion.jda.api.audit.AuditLogKey; -import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildMessageChannel; -import net.dv8tion.jda.api.events.guild.GuildBanEvent; -import net.dv8tion.jda.api.events.guild.GuildUnbanEvent; -import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; -import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent; -import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent; +import net.dv8tion.jda.api.events.guild.GuildAuditLogEntryCreateEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.utils.MarkdownSanitizer; import net.dv8tion.jda.api.utils.TimeFormat; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.awt.*; +import java.awt.Color; import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Optional; +import java.time.OffsetDateTime; import static com.mcmoddev.mmdbot.thelistener.TheListener.getInstance; +import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; public final class ModerationEvents extends ListenerAdapter { @@ -66,135 +65,145 @@ private ModerationEvents() { } @Override - public void onGuildBan(@NotNull final GuildBanEvent event) { - Utils.getAuditLog(event.getGuild(), event.getUser().getIdLong(), log -> log - .limit(5) - .type(ActionType.BAN), log -> { - final var embed = new EmbedBuilder(); - final var bannedUser = event.getUser(); - final var bannedBy = Optional.ofNullable(log.getUser()); - embed.setColor(Color.RED); - embed.setTitle("User Banned."); - embed.addField("**User:**", bannedUser.getAsTag(), true); - if (log.getReason() != null) { - embed.addField("**Ban reason:**", log.getReason(), false); - } else { - embed.addField("**Ban reason:**", "Reason for ban was not provided or could not be found " - + "please contact a member of staff for more information about this ban ban.", false); + public void onGuildAuditLogEntryCreate(@NotNull final GuildAuditLogEntryCreateEvent event) { + final AuditLogEntry entry = event.getEntry(); + final ActionType type = entry.getType(); + + switch (type) { + case BAN -> this.onBan(entry); + case UNBAN -> this.onUnban(entry); + case MEMBER_UPDATE -> { + final @Nullable AuditLogChange nicknameChange = entry.getChangeByKey(AuditLogKey.MEMBER_NICK); + if (nicknameChange != null) this.onNicknameUpdate(entry, nicknameChange); + + final @Nullable AuditLogChange timeoutChange = entry.getChangeByKey(AuditLogKey.MEMBER_TIME_OUT); + if (timeoutChange != null) this.onTimeoutUpdate(entry, timeoutChange); } - embed.setFooter("User ID: " + bannedUser.getId(), bannedUser.getEffectiveAvatarUrl()); - embed.setTimestamp(Instant.now()); - log(event.getGuild().getIdLong(), event.getJDA(), embed.build(), bannedBy); - }); + case KICK -> this.onKick(entry); + } } - @Override - public void onGuildUnban(@NotNull final GuildUnbanEvent event) { - Utils.getAuditLog(event.getGuild(), event.getUser().getIdLong(), log -> log - .limit(5) - .type(ActionType.UNBAN), log -> { - final var embed = new EmbedBuilder(); - final var unBannedUser = event.getUser(); - final var bannedBy = Optional.ofNullable(log.getUser()); - embed.setColor(Color.GREEN); - embed.setTitle("User Un-banned."); - embed.addField("**User:**", unBannedUser.getAsTag(), true); - embed.setFooter("User ID: " + unBannedUser.getId(), unBannedUser.getEffectiveAvatarUrl()); - embed.setTimestamp(Instant.now()); - log(event.getGuild().getIdLong(), event.getJDA(), embed.build(), bannedBy); - }); + public void onBan(final AuditLogEntry entry) { + final User bannedUser = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); + final User bannedBy = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); + final var reason = requireNonNullElse(entry.getReason(), + "_Reason for ban was not provided or could not be found; " + + "please contact a member of staff for more information about this ban._"); + + final var embed = new EmbedBuilder(); + embed.setColor(Color.RED); + embed.setTitle("User Banned."); + embed.addField("**User:**", bannedUser.getAsTag(), true); + embed.addField("**Ban reason:**", reason, false); + embed.setFooter("User ID: " + bannedUser.getId(), bannedUser.getEffectiveAvatarUrl()); + embed.setTimestamp(Instant.now()); + + log(entry.getGuild().getIdLong(), entry.getJDA(), embed.build(), bannedBy); } - @Override - public void onGuildMemberUpdateNickname(@NotNull final GuildMemberUpdateNicknameEvent event) { + public void onUnban(final AuditLogEntry entry) { + final User unBannedUser = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); + final User bannedBy = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); + + final var embed = new EmbedBuilder(); + embed.setColor(Color.GREEN); + embed.setTitle("User Un-banned."); + embed.addField("**User:**", unBannedUser.getAsTag(), true); + embed.setFooter("User ID: " + unBannedUser.getId(), unBannedUser.getEffectiveAvatarUrl()); + embed.setTimestamp(Instant.now()); + + log(entry.getGuild().getIdLong(), entry.getJDA(), embed.build(), bannedBy); + } + + public void onNicknameUpdate(final AuditLogEntry entry, final AuditLogChange nicknameChange) { + final User target = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); + final User editor = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); + final var embed = new EmbedBuilder(); - final var targetUser = event.getUser(); embed.setColor(Color.YELLOW); embed.setTitle("Nickname Changed"); - embed.addField("User:", targetUser.getAsTag(), true); - embed.addField("Old Nickname:", event.getOldNickname() == null - ? "*None*" : event.getOldNickname(), true); - embed.addField("New Nickname:", event.getNewNickname() == null - ? "*None*" : event.getNewNickname(), true); - embed.setFooter("User ID: " + event.getUser().getId(), event.getUser().getEffectiveAvatarUrl()); + embed.addField("User:", target.getAsTag(), true); + embed.addField("Old Nickname:", wrapNicknameValue(nicknameChange.getOldValue()), true); + embed.addField("New Nickname:", wrapNicknameValue(nicknameChange.getNewValue()), true); + embed.setFooter("User ID: " + target.getId(), target.getEffectiveAvatarUrl()); embed.setTimestamp(Instant.now()); - logWithWebhook(event.getGuild().getIdLong(), event.getJDA(), embed.build(), event.getUser()); + log(entry.getGuild().getIdLong(), entry.getJDA(), embed.build(), editor); } - @Override - public void onGuildMemberRemove(@NotNull final GuildMemberRemoveEvent event) { - Utils.getAuditLog(event.getGuild(), event.getUser().getIdLong(), log -> log - .type(ActionType.KICK) - .limit(5), log -> { - if (log.getTimeCreated().toInstant().isBefore(Instant.now().minus(2, ChronoUnit.MINUTES))) { - return; - } + private static String wrapNicknameValue(@Nullable String nicknameValue) { + if (nicknameValue == null) return "*None*"; + return MarkdownSanitizer.escape(nicknameValue); + } - final var embed = new EmbedBuilder(); - final var kicker = Optional.ofNullable(log.getUser()); - final var kickedUser = event.getUser(); - - if (kicker.isPresent() && kicker.get().isBot()) { - var botKickMessage = kickedUser.getAsTag() + " was kicked! Kick Reason: " + log.getReason(); - log(event.getGuild().getIdLong(), event.getJDA(), botKickMessage, kicker); - } else { - embed.setColor(RUBY); - embed.setTitle("User Kicked"); - embed.addField("**Name:**", kickedUser.getAsTag(), true); - embed.addField("**Kick reason:**", log.getReason() != null ? log.getReason() : - ("Reason for kick was not provided or could not be found, please contact " - + "a member of staff for more information about this kick."), false); - embed.setFooter("User ID: " + kickedUser.getId(), kickedUser.getAvatarUrl()); - embed.setTimestamp(Instant.now()); - - log(event.getGuild().getIdLong(), event.getJDA(), embed.build(), kicker); - } - }); + public void onKick(final AuditLogEntry entry) { + final User kickedUser = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); + final User kicker = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); + + final var embed = new EmbedBuilder(); + if (kicker.isBot()) { + var botKickMessage = kickedUser.getAsTag() + " was kicked! Kick Reason: " + entry.getReason(); + log(entry.getGuild().getIdLong(), entry.getJDA(), botKickMessage, kicker); + } else { + final var reason = requireNonNullElse(entry.getReason(), "Reason for kick was not provided or could not be found, please contact " + + "a member of staff for more information about this kick."); + + embed.setColor(RUBY); + embed.setTitle("User Kicked"); + embed.addField("**Name:**", kickedUser.getAsTag(), true); + embed.addField("**Kick reason:**", reason, false); + embed.setFooter("User ID: " + kickedUser.getId(), kickedUser.getAvatarUrl()); + embed.setTimestamp(Instant.now()); + + log(entry.getGuild().getIdLong(), entry.getJDA(), embed.build(), kicker); + } } - @Override - public void onGuildMemberUpdateTimeOut(@NotNull final GuildMemberUpdateTimeOutEvent event) { - if (event.getOldTimeOutEnd() == null && event.getNewTimeOutEnd() != null) { + public void onTimeoutUpdate(final AuditLogEntry entry, final AuditLogChange timeoutChange) { + final OffsetDateTime oldTimeoutEnd = parseDateTime(timeoutChange.getOldValue()); + final OffsetDateTime newTimeoutEnd = parseDateTime(timeoutChange.getNewValue()); + final User user = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); + final User moderator = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); + + if (oldTimeoutEnd == null && newTimeoutEnd != null) { // Somebody was timed out! - Utils.getAuditLog(event.getGuild(), event.getUser().getIdLong(), log -> log.type(ActionType.MEMBER_UPDATE) - .limit(5), log -> { - if (log.getChangeByKey(AuditLogKey.MEMBER_TIME_OUT) == null) return; - final var embed = new EmbedBuilder(); - final var moderator = Optional.ofNullable(log.getUser()); - final var user = event.getUser(); - - embed.setColor(LIGHT_SEA_GREEN); - embed.setTitle("User Timed Out"); - embed.addField("**User:**", user.getAsTag(), true); - embed.addField("**Timeout End:**", TimeFormat.RELATIVE.format(event.getNewTimeOutEnd()), - true); - embed.addField("**Reason:**", log.getReason() != null ? log.getReason() : - "Reason for timeout was not provided or could not be found, please ask a member of staff for " - + "information about this timeout.", false); - embed.setFooter("User ID: " + user.getId(), user.getEffectiveAvatarUrl()); - embed.setTimestamp(Instant.now()); - log(event.getGuild().getIdLong(), event.getJDA(), embed.build(), moderator); - }); - } else if (event.getOldTimeOutEnd() != null && event.getNewTimeOutEnd() == null) { + + final String reason = requireNonNullElse(entry.getReason(), + "Reason for timeout was not provided or could not be found; " + + "please ask a member of staff for information about this timeout."); + + final var embed = new EmbedBuilder(); + + embed.setColor(LIGHT_SEA_GREEN); + embed.setTitle("User Timed Out"); + embed.addField("**User:**", user.getAsTag(), true); + embed.addField("**Timeout End:**", TimeFormat.RELATIVE.format(newTimeoutEnd), + true); + embed.addField("**Reason:**", reason, false); + embed.setFooter("User ID: " + user.getId(), user.getEffectiveAvatarUrl()); + embed.setTimestamp(Instant.now()); + log(entry.getGuild().getIdLong(), entry.getJDA(), embed.build(), moderator); + + } else if (oldTimeoutEnd != null && newTimeoutEnd == null) { // Somebody's timeout was removed - Utils.getAuditLog(event.getGuild(), event.getUser().getIdLong(), log -> log.type(ActionType.MEMBER_UPDATE) - .limit(5), log -> { - if (log.getChangeByKey(AuditLogKey.MEMBER_TIME_OUT) == null) return; - final var embed = new EmbedBuilder(); - final var moderator = Optional.ofNullable(log.getUser()); - final var user = event.getUser(); - embed.setColor(Color.CYAN); - embed.setTitle("User Timeout Removed"); - embed.addField("**User:**", user.getAsTag(), true); - embed.addField("**Old Timeout End:**", TimeFormat.RELATIVE.format(event.getOldTimeOutEnd()), - true); - embed.setFooter("User ID: " + user.getId(), user.getEffectiveAvatarUrl()); - embed.setTimestamp(Instant.now()); - log(event.getGuild().getIdLong(), event.getJDA(), embed.build(), moderator); - }); + final var embed = new EmbedBuilder(); + + embed.setColor(Color.CYAN); + embed.setTitle("User Timeout Removed"); + embed.addField("**User:**", user.getAsTag(), true); + embed.addField("**Old Timeout End:**", TimeFormat.RELATIVE.format(oldTimeoutEnd), + true); + embed.setFooter("User ID: " + user.getId(), user.getEffectiveAvatarUrl()); + embed.setTimestamp(Instant.now()); + log(entry.getGuild().getIdLong(), entry.getJDA(), embed.build(), moderator); + } } + private static @Nullable OffsetDateTime parseDateTime(@Nullable String dateTimeString) { + if (dateTimeString == null) return null; + return OffsetDateTime.parse(dateTimeString); + } + @SubscribeEvent public void onWarnAdd(final WarningEvent.Add event) { if (getInstance() == null) { @@ -211,7 +220,7 @@ public void onWarnAdd(final WarningEvent.Add event) { .addField("Warning ID:", doc.warnId(), false) .setFooter("User ID: " + user.getId(), user.getEffectiveAvatarUrl()) .setTimestamp(Instant.now()); - logWithWebhook(event.getGuildId(), jda, embed.build(), moderator); + log(event.getGuildId(), jda, embed.build(), moderator); return null; }).queue(); } @@ -255,15 +264,6 @@ public void onWarnClearAll(final WarningEvent.ClearAllWarns event) { }); } - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private void log(long guildId, JDA jda, MessageEmbed embed, Optional owner) { - owner.ifPresentOrElse(user -> logWithWebhook(guildId, jda, embed, user), () -> log(guildId, jda, embed)); - } - - private void log(long guildId, JDA jda, String message, Optional owner) { - owner.ifPresentOrElse(user -> logWithWebhook(guildId, jda, message, user), () -> log(guildId, jda, message)); - } - private void log(long guildId, JDA jda, MessageEmbed embed) { final var loggingChannels = LoggingType.MODERATION_EVENTS.getChannels(guildId); loggingChannels @@ -275,18 +275,7 @@ private void log(long guildId, JDA jda, MessageEmbed embed) { }); } - private void log(long guildId, JDA jda, String message) { - final var loggingChannels = LoggingType.MODERATION_EVENTS.getChannels(guildId); - loggingChannels - .forEach(id -> { - final var ch = id.resolve(idL -> jda.getChannelById(MessageChannel.class, idL)); - if (ch != null) { - ch.sendMessage(message).queue(); - } - }); - } - - private void logWithWebhook(long guildId, JDA jda, MessageEmbed embed, User author) { + private void log(long guildId, JDA jda, MessageEmbed embed, User author) { final var loggingChannels = LoggingType.MODERATION_EVENTS.getChannels(guildId); loggingChannels .forEach(id -> { @@ -301,7 +290,7 @@ private void logWithWebhook(long guildId, JDA jda, MessageEmbed embed, User auth }); } - private void logWithWebhook(long guildId, JDA jda, String message, User author) { + private void log(long guildId, JDA jda, String message, User author) { final var loggingChannels = LoggingType.MODERATION_EVENTS.getChannels(guildId); loggingChannels .forEach(id -> { From a7516c50b4200dae2fc4d96e1449c4015923801b Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sun, 19 Feb 2023 14:19:45 +0800 Subject: [PATCH 2/5] Switch RoleEvents to new audit log event --- .../mmdbot/thelistener/events/RoleEvents.java | 93 +++++++++++++------ 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java index 610b8a7c..d8ad3f9e 100644 --- a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java +++ b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java @@ -23,19 +23,22 @@ import com.mcmoddev.mmdbot.core.util.config.SnowflakeValue; import com.mcmoddev.mmdbot.thelistener.TheListener; import com.mcmoddev.mmdbot.thelistener.util.LoggingType; -import com.mcmoddev.mmdbot.thelistener.util.Utils; import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.audit.ActionType; +import net.dv8tion.jda.api.audit.AuditLogChange; import net.dv8tion.jda.api.audit.AuditLogEntry; +import net.dv8tion.jda.api.audit.AuditLogKey; import net.dv8tion.jda.api.entities.IMentionable; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.entities.Role; -import net.dv8tion.jda.api.events.guild.member.GenericGuildMemberEvent; -import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent; -import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.events.guild.GuildAuditLogEntryCreateEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.awt.Color; import java.util.ArrayList; @@ -43,43 +46,80 @@ import java.util.function.BiConsumer; import java.util.stream.Collectors; -import static com.mcmoddev.mmdbot.thelistener.util.Utils.mentionable; +import static java.util.Objects.requireNonNull; public final class RoleEvents extends ListenerAdapter { + private static final Logger LOGGER = LoggerFactory.getLogger(RoleEvents.class); @Override - public void onGuildMemberRoleAdd(@NotNull final GuildMemberRoleAddEvent event) { - processEvent(event, "Added", event.getRoles(), List::removeAll); // Just in case if the member has already been updated with its new role list + public void onGuildAuditLogEntryCreate(@NotNull final GuildAuditLogEntryCreateEvent event) { + final AuditLogEntry entry = event.getEntry(); + if (entry.getType() != ActionType.MEMBER_ROLE_UPDATE) return; + + final @Nullable Member targetMember = event.getGuild().getMemberById(entry.getTargetIdLong()); + if (targetMember == null) { + LOGGER.warn("Could not find target member with ID {} for log entry {}", entry.getTargetId(), entry); + return; + } + + final List addedRoles = parseRoles(entry, AuditLogKey.MEMBER_ROLES_ADD); + if (!addedRoles.isEmpty()) { + processEvent(entry, targetMember, "Added", addedRoles, List::removeAll); + } + + final List removedRoles = parseRoles(entry, AuditLogKey.MEMBER_ROLES_REMOVE); + if (!removedRoles.isEmpty()) { + processEvent(entry, targetMember, "Removed", removedRoles, List::addAll); + } } - @Override - public void onGuildMemberRoleRemove(@NotNull final GuildMemberRoleRemoveEvent event) { - processEvent(event, "Removed", event.getRoles(), List::addAll); // Just in case if the member has already been updated with its new role list + private static List parseRoles(final AuditLogEntry entry, AuditLogKey logKey) { + final @Nullable AuditLogChange change = entry.getChangeByKey(logKey); + + if (change == null) return List.of(); + + // https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-exceptions + // The added/removed roles are marked in the new_value property of the audit log change + + final List roleIds = requireNonNull(change.getNewValue()); + final List roles = new ArrayList<>(roleIds.size()); + final JDA jda = entry.getJDA(); + + for (String roleId : roleIds) { + final @Nullable Role roleById = jda.getRoleById(roleId); + if (roleById == null) { + LOGGER.warn("Could not find role with ID {} while parsing change key {} for log entry {}", roleId, logKey, entry); + continue; + } + roles.add(roleById); + } + + return roles; } - private static void processEvent(GenericGuildMemberEvent event, String changeType, List modifiedRoles, + private static void processEvent(AuditLogEntry entry, Member member, String changeType, List modifiedRoles, BiConsumer, List> roleListsConsumer) { // The role list consumer allows the caller to update the previous roles list with the modified roles list, // to guard against the case where the target's roles list has been updated before we receive the event // We could also check JDA's impl to see if the event always fires before the target's roles list is updated and // get rid of this, but :shrug: + final var guild = member.getGuild(); final var roleIds = modifiedRoles.stream().map(Role::getIdLong).toList(); - final var noLoggingRoles = TheListener.getInstance().getConfigForGuild(event.getGuild().getIdLong()).getNoLoggingRoles(); + final var noLoggingRoles = TheListener.getInstance().getConfigForGuild(guild.getIdLong()).getNoLoggingRoles(); if (noLoggingRoles.stream().map(SnowflakeValue::asLong).anyMatch(roleIds::contains)) { return; } - final List previousRoles = new ArrayList<>(event.getMember().getRoles()); + final List previousRoles = new ArrayList<>(member.getRoles()); roleListsConsumer.accept(previousRoles, modifiedRoles); - final var target = event.getMember(); - Utils.getAuditLog(event.getGuild(), target.getIdLong(), log -> log.type(ActionType.MEMBER_ROLE_UPDATE).limit(5), - entry -> buildAndSendMessage(event, entry, target, changeType, previousRoles, modifiedRoles)); + buildAndSendMessage(entry, member, changeType, previousRoles, modifiedRoles); } - private static void buildAndSendMessage(GenericGuildMemberEvent event, AuditLogEntry entry, Member target, String changeType, + private static void buildAndSendMessage(AuditLogEntry entry, Member target, String changeType, List previousRoles, List modifiedRoles) { + final var jda = target.getJDA(); final var embed = new EmbedBuilder(); embed.setTitle("User Role(s) " + changeType) @@ -88,21 +128,16 @@ private static void buildAndSendMessage(GenericGuildMemberEvent event, AuditLogE .setFooter("User ID: " + target.getUser().getId(), target.getEffectiveAvatarUrl()) .setTimestamp(entry.getTimeCreated()); - final var targetId = entry.getTargetIdLong(); - - if (targetId != target.getIdLong()) { - TheListener.LOGGER.warn("Inconsistency between target of retrieved audit log entry and actual role event target: " + - "retrieved is {}, but target is {}", targetId, target); - } else if (entry.getUser() != null) { - final var editor = entry.getUser(); + final @Nullable var editor = jda.getUserById(entry.getUserIdLong()); + if (editor == null) { embed.addField("Editor:", editor.getAsTag(), true); } - embed.addField("Previous Role(s):", mentionsOrEmpty(previousRoles), true); - embed.addField(changeType + " Role(s):", mentionsOrEmpty(modifiedRoles), true); + embed.addField("Previous Role(s):", mentionsOrEmpty(previousRoles), true) + .addField(changeType + " Role(s):", mentionsOrEmpty(modifiedRoles), true); - LoggingType.ROLE_EVENTS.getChannels(event.getGuild().getIdLong()).forEach(id -> { - final var ch = id.resolve(idL -> event.getJDA().getChannelById(MessageChannel.class, idL)); + LoggingType.ROLE_EVENTS.getChannels(target.getGuild().getIdLong()).forEach(id -> { + final var ch = id.resolve(idL -> jda.getChannelById(MessageChannel.class, idL)); if (ch != null) { ch.sendMessageEmbeds(embed.build()).queue(); } From 141a985be165c9cc6cda56e3de40c614e46370ee Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sun, 19 Feb 2023 14:20:42 +0800 Subject: [PATCH 3/5] Yeet and delete Utils#getAuditLog --- .../mmdbot/thelistener/util/Utils.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/listener/java/com/mcmoddev/mmdbot/thelistener/util/Utils.java b/src/listener/java/com/mcmoddev/mmdbot/thelistener/util/Utils.java index f6b3474d..9227fba5 100644 --- a/src/listener/java/com/mcmoddev/mmdbot/thelistener/util/Utils.java +++ b/src/listener/java/com/mcmoddev/mmdbot/thelistener/util/Utils.java @@ -20,13 +20,7 @@ */ package com.mcmoddev.mmdbot.thelistener.util; -import net.dv8tion.jda.api.audit.AuditLogEntry; -import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.IMentionable; -import net.dv8tion.jda.api.requests.restaction.pagination.AuditLogPaginationAction; - -import java.util.function.Consumer; -import java.util.function.UnaryOperator; public final class Utils { @@ -38,17 +32,4 @@ public static String mentionable(final IMentionable mention) { return mention.getAsMention() + " (" + mention.getId() + ")"; } - public static void getAuditLog(final Guild guild, final long targetId, UnaryOperator modifier, Consumer consumer) { - getAuditLog(guild, targetId, modifier, consumer, () -> { - }); - } - - public static void getAuditLog(final Guild guild, final long targetId, UnaryOperator modifier, Consumer consumer, Runnable orElse) { - modifier.apply(guild.retrieveAuditLogs()) - .queue(logs -> logs.stream() - .filter(entry -> entry.getTargetIdLong() == targetId) - .findFirst() - .ifPresentOrElse(consumer, orElse)); - } - } From f5cc47686dec7d46ffb355b0713da893399139cf Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Fri, 24 Feb 2023 22:36:54 +0800 Subject: [PATCH 4/5] Switch to JDA#retrieveUserById and #retrieveMemberById --- .../thelistener/events/ModerationEvents.java | 71 ++++++++++++------- .../mmdbot/thelistener/events/RoleEvents.java | 66 +++++++++-------- 2 files changed, 84 insertions(+), 53 deletions(-) diff --git a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/ModerationEvents.java b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/ModerationEvents.java index d6d333c8..4594fb69 100644 --- a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/ModerationEvents.java +++ b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/ModerationEvents.java @@ -21,6 +21,7 @@ package com.mcmoddev.mmdbot.thelistener.events; import club.minnced.discord.webhook.send.AllowedMentions; +import com.google.errorprone.annotations.CheckReturnValue; import com.mcmoddev.mmdbot.core.event.moderation.WarningEvent; import com.mcmoddev.mmdbot.core.util.webhook.WebhookManager; import com.mcmoddev.mmdbot.thelistener.util.LoggingType; @@ -37,22 +38,26 @@ import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildMessageChannel; import net.dv8tion.jda.api.events.guild.GuildAuditLogEntryCreateEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.requests.ErrorResponse; +import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.utils.MarkdownSanitizer; import net.dv8tion.jda.api.utils.TimeFormat; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.awt.Color; import java.time.Instant; import java.time.OffsetDateTime; import static com.mcmoddev.mmdbot.thelistener.TheListener.getInstance; -import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNullElse; public final class ModerationEvents extends ListenerAdapter { public static final ModerationEvents INSTANCE = new ModerationEvents(); + private static final Logger LOGGER = LoggerFactory.getLogger(ModerationEvents.class); private static final String WEBHOOK_NAME = "ModerationLogs"; private static final WebhookManager WEBHOOKS = WebhookManager.of(e @@ -70,22 +75,51 @@ public void onGuildAuditLogEntryCreate(@NotNull final GuildAuditLogEntryCreateEv final ActionType type = entry.getType(); switch (type) { - case BAN -> this.onBan(entry); - case UNBAN -> this.onUnban(entry); + case BAN -> retrieveUsers(entry, this::onBan).queue(); + case UNBAN -> retrieveUsers(entry, this::onUnban).queue(); case MEMBER_UPDATE -> { final @Nullable AuditLogChange nicknameChange = entry.getChangeByKey(AuditLogKey.MEMBER_NICK); - if (nicknameChange != null) this.onNicknameUpdate(entry, nicknameChange); + if (nicknameChange != null) retrieveUsers(entry, + (target, actor, auditEntry) -> this.onNicknameUpdate(target, actor, auditEntry, nicknameChange)).queue(); final @Nullable AuditLogChange timeoutChange = entry.getChangeByKey(AuditLogKey.MEMBER_TIME_OUT); - if (timeoutChange != null) this.onTimeoutUpdate(entry, timeoutChange); + if (timeoutChange != null) retrieveUsers(entry, + (target, actor, auditEntry) -> this.onTimeoutUpdate(target, actor, auditEntry, timeoutChange)).queue(); } - case KICK -> this.onKick(entry); + case KICK -> retrieveUsers(entry, this::onKick).queue(); } } - public void onBan(final AuditLogEntry entry) { - final User bannedUser = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); - final User bannedBy = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); + @FunctionalInterface + private interface AuditLogEntryHandler { + void handle(final User target, final User actor, final AuditLogEntry entry); + } + + @CheckReturnValue + private RestAction retrieveUsers(final AuditLogEntry entry, final AuditLogEntryHandler handler) { + final String targetId = entry.getTargetId(); + final String actorId = entry.getUserId(); + + final RestAction<@Nullable User> targetRestAction = entry.getJDA().retrieveUserById(targetId) + .onErrorMap(ErrorResponse.UNKNOWN_USER::test, e -> { + LOGGER.error("Could not retrieve target user for ID {}", targetId); + return null; + }); + final RestAction<@Nullable User> actorRestAction = entry.getJDA().retrieveUserById(actorId) + .onErrorMap(ErrorResponse.UNKNOWN_USER::test, e -> { + LOGGER.error("Could not retrieve actor user for ID {}", actorId); + return null; + }); + + return targetRestAction.and(actorRestAction, (targetUser, actorUser) -> { + if (targetUser != null && actorUser != null) { + handler.handle(targetUser, actorUser, entry); + } + return null; + }); + } + + public void onBan(final User bannedUser, final User bannedBy, final AuditLogEntry entry) { final var reason = requireNonNullElse(entry.getReason(), "_Reason for ban was not provided or could not be found; " + "please contact a member of staff for more information about this ban._"); @@ -101,10 +135,7 @@ public void onBan(final AuditLogEntry entry) { log(entry.getGuild().getIdLong(), entry.getJDA(), embed.build(), bannedBy); } - public void onUnban(final AuditLogEntry entry) { - final User unBannedUser = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); - final User bannedBy = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); - + public void onUnban(final User unBannedUser, final User bannedBy, final AuditLogEntry entry) { final var embed = new EmbedBuilder(); embed.setColor(Color.GREEN); embed.setTitle("User Un-banned."); @@ -115,10 +146,7 @@ public void onUnban(final AuditLogEntry entry) { log(entry.getGuild().getIdLong(), entry.getJDA(), embed.build(), bannedBy); } - public void onNicknameUpdate(final AuditLogEntry entry, final AuditLogChange nicknameChange) { - final User target = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); - final User editor = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); - + public void onNicknameUpdate(final User target, final User editor, final AuditLogEntry entry, final AuditLogChange nicknameChange) { final var embed = new EmbedBuilder(); embed.setColor(Color.YELLOW); embed.setTitle("Nickname Changed"); @@ -135,10 +163,7 @@ private static String wrapNicknameValue(@Nullable String nicknameValue) { return MarkdownSanitizer.escape(nicknameValue); } - public void onKick(final AuditLogEntry entry) { - final User kickedUser = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); - final User kicker = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); - + public void onKick(final User kickedUser, final User kicker, final AuditLogEntry entry) { final var embed = new EmbedBuilder(); if (kicker.isBot()) { var botKickMessage = kickedUser.getAsTag() + " was kicked! Kick Reason: " + entry.getReason(); @@ -158,11 +183,9 @@ public void onKick(final AuditLogEntry entry) { } } - public void onTimeoutUpdate(final AuditLogEntry entry, final AuditLogChange timeoutChange) { + public void onTimeoutUpdate(final User user, final User moderator, final AuditLogEntry entry, final AuditLogChange timeoutChange) { final OffsetDateTime oldTimeoutEnd = parseDateTime(timeoutChange.getOldValue()); final OffsetDateTime newTimeoutEnd = parseDateTime(timeoutChange.getNewValue()); - final User user = requireNonNull(entry.getJDA().getUserById(entry.getTargetId())); - final User moderator = requireNonNull(entry.getJDA().getUserById(entry.getUserIdLong())); if (oldTimeoutEnd == null && newTimeoutEnd != null) { // Somebody was timed out! diff --git a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java index d8ad3f9e..871dfa68 100644 --- a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java +++ b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java @@ -34,7 +34,9 @@ import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.events.guild.GuildAuditLogEntryCreateEvent; +import net.dv8tion.jda.api.exceptions.ErrorHandler; import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.requests.ErrorResponse; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -56,21 +58,21 @@ public void onGuildAuditLogEntryCreate(@NotNull final GuildAuditLogEntryCreateEv final AuditLogEntry entry = event.getEntry(); if (entry.getType() != ActionType.MEMBER_ROLE_UPDATE) return; - final @Nullable Member targetMember = event.getGuild().getMemberById(entry.getTargetIdLong()); - if (targetMember == null) { - LOGGER.warn("Could not find target member with ID {} for log entry {}", entry.getTargetId(), entry); - return; - } - - final List addedRoles = parseRoles(entry, AuditLogKey.MEMBER_ROLES_ADD); - if (!addedRoles.isEmpty()) { - processEvent(entry, targetMember, "Added", addedRoles, List::removeAll); - } - - final List removedRoles = parseRoles(entry, AuditLogKey.MEMBER_ROLES_REMOVE); - if (!removedRoles.isEmpty()) { - processEvent(entry, targetMember, "Removed", removedRoles, List::addAll); - } + event.getGuild().retrieveMemberById(entry.getTargetId()) + .queue(targetMember -> { + final List addedRoles = parseRoles(entry, AuditLogKey.MEMBER_ROLES_ADD); + if (!addedRoles.isEmpty()) { + processEvent(entry, targetMember, "Added", addedRoles, List::removeAll); + } + + final List removedRoles = parseRoles(entry, AuditLogKey.MEMBER_ROLES_REMOVE); + if (!removedRoles.isEmpty()) { + processEvent(entry, targetMember, "Removed", removedRoles, List::addAll); + } + }, new ErrorHandler() + .handle(List.of(ErrorResponse.UNKNOWN_MEMBER, ErrorResponse.UNKNOWN_USER), + e -> LOGGER.warn("Could not find target member with ID {} for log entry {}", entry.getTargetId(), entry)) + ); } private static List parseRoles(final AuditLogEntry entry, AuditLogKey logKey) { @@ -128,20 +130,26 @@ private static void buildAndSendMessage(AuditLogEntry entry, Member target, Stri .setFooter("User ID: " + target.getUser().getId(), target.getEffectiveAvatarUrl()) .setTimestamp(entry.getTimeCreated()); - final @Nullable var editor = jda.getUserById(entry.getUserIdLong()); - if (editor == null) { - embed.addField("Editor:", editor.getAsTag(), true); - } - - embed.addField("Previous Role(s):", mentionsOrEmpty(previousRoles), true) - .addField(changeType + " Role(s):", mentionsOrEmpty(modifiedRoles), true); - - LoggingType.ROLE_EVENTS.getChannels(target.getGuild().getIdLong()).forEach(id -> { - final var ch = id.resolve(idL -> jda.getChannelById(MessageChannel.class, idL)); - if (ch != null) { - ch.sendMessageEmbeds(embed.build()).queue(); - } - }); + jda.retrieveUserById(entry.getUserIdLong()) + .onErrorMap(ErrorResponse.UNKNOWN_USER::test, e -> { + LOGGER.warn("Could not retrieve editor user with ID {} for log entry {}", entry.getUserId(), entry); + return null; + }) + .queue(editor -> { + if (editor == null) { + embed.addField("Editor:", editor.getAsTag(), true); + } + + embed.addField("Previous Role(s):", mentionsOrEmpty(previousRoles), true) + .addField(changeType + " Role(s):", mentionsOrEmpty(modifiedRoles), true); + + LoggingType.ROLE_EVENTS.getChannels(target.getGuild().getIdLong()).forEach(id -> { + final var ch = id.resolve(idL -> jda.getChannelById(MessageChannel.class, idL)); + if (ch != null) { + ch.sendMessageEmbeds(embed.build()).queue(); + } + }); + }); } public static String mentionsOrEmpty(List list) { From 196cd6962b9511c89f2407411b7be38035e6c4cc Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Fri, 24 Feb 2023 22:37:37 +0800 Subject: [PATCH 5/5] Fix nullability check in RoleEvents --- .../java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java index 871dfa68..69eede6e 100644 --- a/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java +++ b/src/listener/java/com/mcmoddev/mmdbot/thelistener/events/RoleEvents.java @@ -136,7 +136,7 @@ private static void buildAndSendMessage(AuditLogEntry entry, Member target, Stri return null; }) .queue(editor -> { - if (editor == null) { + if (editor != null) { embed.addField("Editor:", editor.getAsTag(), true); }