feat: member role and key role logging

This commit is contained in:
sam 2024-10-11 21:29:02 +02:00
parent d445b5ba44
commit a56cb87294
3 changed files with 140 additions and 6 deletions

View file

@ -28,6 +28,7 @@ public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger)
case AuditLogEvent.MemberBanRemove: case AuditLogEvent.MemberBanRemove:
auditLogCache.SetUnban(evt.GuildID, evt.TargetID, evt.UserID.Value, evt.Reason); auditLogCache.SetUnban(evt.GuildID, evt.TargetID, evt.UserID.Value, evt.Reason);
break; break;
case AuditLogEvent.MemberRoleUpdate:
case AuditLogEvent.MemberUpdate: case AuditLogEvent.MemberUpdate:
auditLogCache.SetMemberUpdate( auditLogCache.SetMemberUpdate(
evt.GuildID, evt.GuildID,
@ -38,8 +39,9 @@ public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger)
break; break;
default: default:
_logger.Debug( _logger.Debug(
"Received audit log event {Id} that we don't care about, ignoring", "Received audit log event {Id} with type {Type} that we don't care about, ignoring",
evt.ID evt.ID,
evt.ActionType
); );
break; break;
} }

View file

@ -8,6 +8,7 @@ using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway.Responders; using Remora.Discord.Gateway.Responders;
using Remora.Rest.Core;
using Remora.Results; using Remora.Results;
namespace Catalogger.Backend.Bot.Responders.MemberUpdate; namespace Catalogger.Backend.Bot.Responders.MemberUpdate;
@ -16,6 +17,7 @@ public class GuildMemberUpdateResponder(
ILogger logger, ILogger logger,
DatabaseContext db, DatabaseContext db,
UserCache userCache, UserCache userCache,
RoleCache roleCache,
IMemberCache memberCache, IMemberCache memberCache,
WebhookExecutorService webhookExecutor, WebhookExecutorService webhookExecutor,
AuditLogCache auditLogCache AuditLogCache auditLogCache
@ -67,6 +69,14 @@ public class GuildMemberUpdateResponder(
{ {
return await HandleTimeoutAsync(newMember, ct); 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 finally
{ {
@ -234,4 +244,120 @@ public class GuildMemberUpdateResponder(
); );
return Result.Success; return Result.Success;
} }
private async Task<Result> HandleRoleUpdateAsync(
IGuildMemberUpdate member,
IReadOnlyList<Snowflake> 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;
}
} }

View file

@ -75,6 +75,12 @@ public class RedisMemberCache(
public async Task UpdateAsync(IGuildMemberUpdate newMember) 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); var oldMember = await TryGetInnerAsync(newMember.GuildID, newMember.User.ID);
if (oldMember == null) if (oldMember == null)
{ {
@ -109,7 +115,7 @@ public class RedisMemberCache(
User = RedisUser.FromIUser(newMember.User), User = RedisUser.FromIUser(newMember.User),
Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname, Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname,
Avatar = newMember.Avatar.HasValue ? newMember.Avatar.Value?.Value : oldMember.Avatar, 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, JoinedAt = newMember.JoinedAt ?? oldMember.JoinedAt,
PremiumSince = newMember.PremiumSince.HasValue PremiumSince = newMember.PremiumSince.HasValue
? newMember.PremiumSince.Value ? newMember.PremiumSince.Value
@ -134,7 +140,7 @@ internal record RedisMember(
RedisUser User, RedisUser User,
string? Nickname, string? Nickname,
string? Avatar, string? Avatar,
Snowflake[] Roles, ulong[] Roles,
DateTimeOffset JoinedAt, DateTimeOffset JoinedAt,
DateTimeOffset? PremiumSince, DateTimeOffset? PremiumSince,
GuildMemberFlags Flags, GuildMemberFlags Flags,
@ -147,7 +153,7 @@ internal record RedisMember(
RedisUser.FromIUser(member.User.Value), RedisUser.FromIUser(member.User.Value),
member.Nickname.OrDefault(null), member.Nickname.OrDefault(null),
member.Avatar.OrDefault(null)?.Value, member.Avatar.OrDefault(null)?.Value,
member.Roles.ToArray(), member.Roles.Select(id => id.Value).ToArray(),
member.JoinedAt, member.JoinedAt,
member.PremiumSince.OrDefault(null), member.PremiumSince.OrDefault(null),
member.Flags, member.Flags,
@ -160,7 +166,7 @@ internal record RedisMember(
User.ToRemoraUser(), User.ToRemoraUser(),
Nickname, Nickname,
Avatar != null ? new ImageHash(Avatar) : null, Avatar != null ? new ImageHash(Avatar) : null,
Roles, Roles.Select(DiscordSnowflake.New).ToList(),
JoinedAt, JoinedAt,
PremiumSince, PremiumSince,
false, false,