From a56cb8729405c1fd4917ecabeee1585ecd783b34 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 11 Oct 2024 21:29:02 +0200 Subject: [PATCH] feat: member role and key role logging --- .../Responders/Guilds/AuditLogResponder.cs | 6 +- .../GuildMemberUpdateResponder.cs | 126 ++++++++++++++++++ .../Cache/RedisCache/RedisMemberCache.cs | 14 +- 3 files changed, 140 insertions(+), 6 deletions(-) diff --git a/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs index 36a7946..7a5249e 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs @@ -28,6 +28,7 @@ public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger) case AuditLogEvent.MemberBanRemove: auditLogCache.SetUnban(evt.GuildID, evt.TargetID, evt.UserID.Value, evt.Reason); break; + case AuditLogEvent.MemberRoleUpdate: case AuditLogEvent.MemberUpdate: auditLogCache.SetMemberUpdate( evt.GuildID, @@ -38,8 +39,9 @@ public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger) break; default: _logger.Debug( - "Received audit log event {Id} that we don't care about, ignoring", - evt.ID + "Received audit log event {Id} with type {Type} that we don't care about, ignoring", + evt.ID, + evt.ActionType ); break; } diff --git a/Catalogger.Backend/Bot/Responders/MemberUpdate/GuildMemberUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/MemberUpdate/GuildMemberUpdateResponder.cs index 81be74c..62b6fa7 100644 --- a/Catalogger.Backend/Bot/Responders/MemberUpdate/GuildMemberUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/MemberUpdate/GuildMemberUpdateResponder.cs @@ -8,6 +8,7 @@ using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; +using Remora.Rest.Core; using Remora.Results; namespace Catalogger.Backend.Bot.Responders.MemberUpdate; @@ -16,6 +17,7 @@ public class GuildMemberUpdateResponder( ILogger logger, DatabaseContext db, UserCache userCache, + RoleCache roleCache, IMemberCache memberCache, WebhookExecutorService webhookExecutor, AuditLogCache auditLogCache @@ -67,6 +69,14 @@ public class GuildMemberUpdateResponder( { return await HandleTimeoutAsync(newMember, ct); } + + if ( + newMember.Roles.Except(oldMember.Roles).Any() + || oldMember.Roles.Except(newMember.Roles).Any() + ) + { + return await HandleRoleUpdateAsync(newMember, oldMember.Roles, ct); + } } finally { @@ -234,4 +244,120 @@ public class GuildMemberUpdateResponder( ); return Result.Success; } + + private async Task HandleRoleUpdateAsync( + IGuildMemberUpdate member, + IReadOnlyList oldRoles, + CancellationToken ct = default + ) + { + var guildConfig = await db.GetGuildAsync(member.GuildID, ct); + var guildRoles = roleCache.GuildRoles(member.GuildID).ToList(); + + var keyRoleUpdate = new EmbedBuilder() + .WithAuthor(member.User.Tag(), null, member.User.AvatarUrl()) + .WithTitle("Key roles added or removed") + .WithDescription($"<@{member.User.ID}>") + .WithColour(DiscordUtils.Purple) + .WithFooter($"User ID: {member.User.ID}") + .WithCurrentTimestamp(); + + var roleUpdate = new EmbedBuilder() + .WithAuthor(member.User.Tag(), null, member.User.AvatarUrl()) + .WithTitle("Roles added or removed") + .WithDescription($"<@{member.User.ID}>") + .WithColour(DiscordUtils.Purple) + .WithFooter($"User ID: {member.User.ID}") + .WithCurrentTimestamp(); + + var addedRoles = member.Roles.Except(oldRoles).Select(s => s.Value).ToList(); + var removedRoles = oldRoles.Except(member.Roles).Select(s => s.Value).ToList(); + + if (addedRoles.Count != 0) + { + roleUpdate.AddField("Added", string.Join(", ", addedRoles.Select(id => $"<@&{id}>"))); + + // Add all added key roles to the log + if (!addedRoles.Except(guildConfig.KeyRoles).Any()) + { + var value = string.Join( + "\n", + addedRoles + .Where(guildConfig.KeyRoles.Contains) + .Select(id => + { + var role = guildRoles.FirstOrDefault(r => r.ID.Value == id); + return role != null ? $"{role.Name} <@&{role.ID}>" : $"<@&{id}>"; + }) + ); + + keyRoleUpdate.AddField("Added", value); + } + } + + if (removedRoles.Count != 0) + { + roleUpdate.AddField( + "Removed", + string.Join(", ", removedRoles.Select(id => $"<@&{id}>")) + ); + + // Add all removed key roles to the log + if (!removedRoles.Except(guildConfig.KeyRoles).Any()) + { + var value = string.Join( + "\n", + removedRoles + .Where(guildConfig.KeyRoles.Contains) + .Select(id => + { + var role = guildRoles.FirstOrDefault(r => r.ID.Value == id); + return role != null ? $"{role.Name} <@&{role.ID}>" : $"<@&{id}>"; + }) + ); + + keyRoleUpdate.AddField("Added", value); + } + } + + // If there are any fields in the role update embed, we should send it + if (roleUpdate.Fields.Count != 0) + { + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.GuildMemberUpdate, + roleUpdate.Build().GetOrThrow() + ); + } + + // Do the same for the key role update embed, but we also need to fetch the moderator that updated them + if (keyRoleUpdate.Fields.Count != 0) + { + if ( + auditLogCache.TryGetMemberUpdate(member.GuildID, member.User.ID, out var actionData) + ) + { + var moderator = await userCache.GetUserAsync(actionData.ModeratorId); + keyRoleUpdate.AddField( + "Responsible moderator", + moderator == null + ? $"*(unknown user {actionData.ModeratorId}) <@{actionData.ModeratorId}>*" + : $"{moderator.Tag()} <@{moderator.ID}>" + ); + } + else + { + keyRoleUpdate.AddField("Responsible moderator", "*(unknown)*"); + } + + // Finally, send the embed + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.GuildKeyRoleUpdate, + keyRoleUpdate.Build().GetOrThrow() + ); + } + + return Result.Success; + } } diff --git a/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs b/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs index d4aad09..e16c35b 100644 --- a/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs +++ b/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs @@ -75,6 +75,12 @@ public class RedisMemberCache( public async Task UpdateAsync(IGuildMemberUpdate newMember) { + _logger.Debug( + "Updating member {MemberId} in {GuildId}", + newMember.User.ID, + newMember.GuildID + ); + var oldMember = await TryGetInnerAsync(newMember.GuildID, newMember.User.ID); if (oldMember == null) { @@ -109,7 +115,7 @@ public class RedisMemberCache( User = RedisUser.FromIUser(newMember.User), Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname, Avatar = newMember.Avatar.HasValue ? newMember.Avatar.Value?.Value : oldMember.Avatar, - Roles = newMember.Roles.ToArray(), + Roles = newMember.Roles.Select(id => id.Value).ToArray(), JoinedAt = newMember.JoinedAt ?? oldMember.JoinedAt, PremiumSince = newMember.PremiumSince.HasValue ? newMember.PremiumSince.Value @@ -134,7 +140,7 @@ internal record RedisMember( RedisUser User, string? Nickname, string? Avatar, - Snowflake[] Roles, + ulong[] Roles, DateTimeOffset JoinedAt, DateTimeOffset? PremiumSince, GuildMemberFlags Flags, @@ -147,7 +153,7 @@ internal record RedisMember( RedisUser.FromIUser(member.User.Value), member.Nickname.OrDefault(null), member.Avatar.OrDefault(null)?.Value, - member.Roles.ToArray(), + member.Roles.Select(id => id.Value).ToArray(), member.JoinedAt, member.PremiumSince.OrDefault(null), member.Flags, @@ -160,7 +166,7 @@ internal record RedisMember( User.ToRemoraUser(), Nickname, Avatar != null ? new ImageHash(Avatar) : null, - Roles, + Roles.Select(DiscordSnowflake.New).ToList(), JoinedAt, PremiumSince, false,