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.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; namespace Catalogger.Backend.Bot.Responders.Members; public class GuildMemberRemoveResponder( ILogger logger, DatabaseContext db, IMemberCache memberCache, RoleCache roleCache, UserCache userCache, AuditLogCache auditLogCache, WebhookExecutorService webhookExecutor ) : IResponder { private readonly ILogger _logger = logger.ForContext(); public async Task RespondAsync(IGuildMemberRemove evt, CancellationToken ct = default) { try { var embed = new EmbedBuilder() .WithTitle("Member left") .WithAuthor(evt.User.Tag(), iconUrl: evt.User.AvatarUrl()) .WithColour(DiscordUtils.Orange) .WithDescription($"<@{evt.User.ID}>") .WithFooter($"ID: {evt.User.ID}") .WithCurrentTimestamp(); var guildConfig = await db.GetGuildAsync(evt.GuildID, ct); var member = await memberCache.TryGetAsync(evt.GuildID, evt.User.ID); if (member == null) { _logger.Information( "Guild member {UserId} in {GuildId} left but wasn't in the cache, sending limited embed", evt.User.ID, evt.GuildID ); webhookExecutor.QueueLog( guildConfig, LogChannelType.GuildMemberRemove, embed.Build().GetOrThrow() ); return Result.Success; } embed.Description += $"\njoined \n({member.JoinedAt.Prettify()} ago)"; // get the member's roles, sort them, and turn them into mentions var guildRoles = roleCache.GuildRoles(evt.GuildID); var roles = guildRoles.Sorted(member.Roles).ToList(); var roleMentions = ""; foreach (var (idx, role) in roles.Select((r, i) => (i, r))) { if (roleMentions.Length > 900) { roleMentions += $"\n(too many roles to list, showing {idx}/{roles.Count})"; break; } roleMentions += $"<@&{role.ID}>"; if (idx != roles.Count - 1) roleMentions += ", "; } embed.AddField("Roles", roleMentions, inline: false); webhookExecutor.QueueLog( guildConfig, LogChannelType.GuildMemberRemove, embed.Build().GetOrThrow() ); // Check for a kick audit log event. We don't get a separate "kick" event so we have to check this manually await Task.Delay(2000, ct); if (!auditLogCache.TryGetKick(evt.GuildID, evt.User.ID, out var actionData)) { return Result.Success; } var kick = new EmbedBuilder() .WithTitle("User kicked") .WithAuthor(evt.User.Tag(), iconUrl: evt.User.AvatarUrl()) .WithColour(DiscordUtils.Red) .WithCurrentTimestamp() .WithDescription($"<@{evt.User.ID}>"); kick.AddField( "Responsible moderator", await userCache.TryFormatUserAsync(actionData.ModeratorId) ); kick.AddField("Reason", actionData.Reason ?? "No reason given"); webhookExecutor.QueueLog( guildConfig, LogChannelType.GuildMemberKick, kick.Build().GetOrThrow() ); return Result.Success; } finally { await memberCache.RemoveAsync(evt.GuildID, evt.User.ID); } } }