Catalogger.NET/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberUpdateResponder.cs
2024-10-12 23:28:15 +02:00

352 lines
12 KiB
C#

using Catalogger.Backend.Cache;
using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Database;
using Catalogger.Backend.Database.Queries;
using Catalogger.Backend.Extensions;
using Catalogger.Backend.Services;
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.Guilds;
public class GuildMemberUpdateResponder(
ILogger logger,
DatabaseContext db,
UserCache userCache,
RoleCache roleCache,
IMemberCache memberCache,
WebhookExecutorService webhookExecutor,
AuditLogCache auditLogCache
) : IResponder<IGuildMemberUpdate>
{
private readonly ILogger _logger = logger.ForContext<GuildMemberUpdateResponder>();
public async Task<Result> RespondAsync(
IGuildMemberUpdate newMember,
CancellationToken ct = default
)
{
try
{
var oldMember = await memberCache.TryGetAsync(newMember.GuildID, newMember.User.ID);
if (oldMember == null)
{
_logger.Information(
"Received member update event for {MemberId} in {GuildId} but they weren't cached, ignoring",
newMember.User.ID,
newMember.GuildID
);
return Result.Success;
}
var oldUser = oldMember.User.GetOrThrow();
if (
!Equals(oldMember.Avatar.OrDefault(), newMember.Avatar.OrDefault())
|| !Equals(oldUser.Avatar, newMember.User.Avatar)
)
{
return await HandleAvatarUpdateAsync(newMember, oldMember, ct);
}
if (
newMember.Nickname.OrDefault() != oldMember.Nickname.OrDefault()
|| newMember.User.Tag() != oldUser.Tag()
|| newMember.User.GlobalName.OrDefault() != oldUser.GlobalName.OrDefault()
)
{
return await HandleNameUpdateAsync(newMember, oldMember, ct);
}
if (
newMember.CommunicationDisabledUntil.OrDefault()
!= oldMember.CommunicationDisabledUntil.OrDefault()
)
{
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
{
await memberCache.UpdateAsync(newMember);
userCache.UpdateUser(newMember.User);
}
return Result.Success;
}
private async Task<Result> HandleAvatarUpdateAsync(
IGuildMemberUpdate newMember,
IGuildMember oldMember,
CancellationToken ct = default
)
{
IEmbed embed;
if (!Equals(oldMember.Avatar.OrDefault(), newMember.Avatar.OrDefault()))
{
var builder = new EmbedBuilder()
.WithAuthor(newMember.User.Tag(), null, newMember.User.AvatarUrl())
.WithColour(DiscordUtils.Green)
.WithFooter($"User ID: {newMember.User.ID}")
.WithCurrentTimestamp();
if (newMember.Avatar.IsDefined())
{
builder = builder
.WithTitle("Changed server avatar")
.WithThumbnailUrl(newMember.AvatarUrl(1024)!);
}
else
{
builder = builder.WithTitle("Removed server avatar");
}
embed = builder.Build().GetOrThrow();
}
else
{
embed = new EmbedBuilder()
.WithAuthor(newMember.User.Tag(), null, newMember.User.AvatarUrl())
.WithTitle("Changed avatar")
.WithThumbnailUrl(newMember.User.AvatarUrl(1024))
.WithColour(DiscordUtils.Green)
.WithFooter($"User ID: {newMember.User.ID}")
.WithCurrentTimestamp()
.Build()
.GetOrThrow();
}
var guildConfig = await db.GetGuildAsync(newMember.GuildID, ct);
webhookExecutor.QueueLog(guildConfig, LogChannelType.GuildMemberAvatarUpdate, embed);
return Result.Success;
}
private async Task<Result> HandleNameUpdateAsync(
IGuildMemberUpdate newMember,
IGuildMember oldMember,
CancellationToken ct = default
)
{
var oldUser = oldMember.User.GetOrThrow();
var builder = new EmbedBuilder()
.WithAuthor(newMember.User.Tag(), null, newMember.User.AvatarUrl())
.WithColour(DiscordUtils.Green)
.WithFooter($"User ID: {newMember.User.ID}")
.WithCurrentTimestamp();
if (newMember.Nickname.OrDefault() != oldMember.Nickname.OrDefault())
{
builder.AddField(
"Changed nickname",
$"""
**Before:** {oldMember.Nickname.OrDefault("*(none)*")}
**After:** {newMember.Nickname.OrDefault("*(none)*")}
"""
);
}
if (newMember.User.GlobalName.OrDefault() != oldUser.GlobalName.OrDefault())
{
builder.AddField(
"Changed display name",
$"""
**Before:** {oldUser.GlobalName.OrDefault("*(none)*")}
**After:** {newMember.User.GlobalName.OrDefault("*(none)*")}
"""
);
}
if (newMember.User.Tag() != oldUser.Tag())
{
builder.AddField(
"Changed username",
$"""
**Before:** {oldUser.Tag()}
**After:** {newMember.User.Tag()}
"""
);
}
var guildConfig = await db.GetGuildAsync(newMember.GuildID, ct);
webhookExecutor.QueueLog(
guildConfig,
LogChannelType.GuildMemberNickUpdate,
builder.Build().GetOrThrow()
);
return Result.Success;
}
private async Task<Result> HandleTimeoutAsync(
IGuildMemberUpdate member,
CancellationToken ct = default
)
{
// Delay 2 seconds to make sure the timeout audit log got cached
await Task.Delay(2000, ct);
var timeoutUntil = member.CommunicationDisabledUntil.OrDefault();
var embed = new EmbedBuilder()
.WithAuthor(member.User.Tag(), null, member.User.AvatarUrl())
.WithTitle(
timeoutUntil != null ? "Member timed out" : "Member removed from timeout early"
)
.WithDescription($"<@{member.User.ID}>")
.WithColour(DiscordUtils.Red)
.WithFooter($"User ID: {member.User.ID}")
.WithCurrentTimestamp();
if (timeoutUntil != null)
{
embed.AddField(
"Until",
$"<t:{timeoutUntil.Value.ToUnixTimeSeconds()}>\nin {timeoutUntil.Value.AddSeconds(5).Prettify()}"
);
}
if (auditLogCache.TryGetMemberUpdate(member.GuildID, member.User.ID, out var actionData))
{
var moderator = await userCache.TryFormatModeratorAsync(actionData);
embed.AddField("Responsible moderator", moderator);
embed.AddField("Reason", actionData.Reason ?? "No reason given");
}
else
{
embed.AddField("Responsible moderator", "*(unknown)*");
embed.AddField("Reason", "*(unknown)*");
}
var guildConfig = await db.GetGuildAsync(member.GuildID, ct);
webhookExecutor.QueueLog(
guildConfig,
LogChannelType.GuildMemberTimeout,
embed.Build().GetOrThrow()
);
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.TryFormatModeratorAsync(actionData);
keyRoleUpdate.AddField("Responsible moderator", moderator);
}
else
{
keyRoleUpdate.AddField("Responsible moderator", "*(unknown)*");
}
// Finally, send the embed
webhookExecutor.QueueLog(
guildConfig,
LogChannelType.GuildKeyRoleUpdate,
keyRoleUpdate.Build().GetOrThrow()
);
}
return Result.Success;
}
}