using System.Text; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using NodaTime.Extensions; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; namespace Catalogger.Backend.Bot.Responders.Messages; public class MessageDeleteBulkResponder( ILogger logger, DatabaseContext db, MessageRepository messageRepository, WebhookExecutorService webhookExecutor, ChannelCache channelCache ) : IResponder { private readonly ILogger _logger = logger.ForContext(); public async Task RespondAsync(IMessageDeleteBulk evt, CancellationToken ct = default) { var guild = await db.GetGuildAsync(evt.GuildID, ct); if (guild.IsMessageIgnored(evt.ChannelID, null)) return Result.Success; var logChannel = webhookExecutor.GetLogChannel( guild, LogChannelType.MessageDeleteBulk, evt.ChannelID ); if (logChannel == null) { return Result.Success; } IChannel? rootChannel = null; channelCache.TryGet(evt.ChannelID, out var channel); if ( channel is { Type: ChannelType.AnnouncementThread or ChannelType.PrivateThread or ChannelType.PublicThread } ) { if (channel.ParentID.TryGet(out var parentId) && parentId != null) channelCache.TryGet(parentId.Value, out rootChannel); } List renderedMessages = []; var notFoundMessages = 0; var ignoredMessages = 0; foreach (var msgId in evt.IDs.Order()) { if (await messageRepository.IsMessageIgnoredAsync(msgId.Value, ct)) { ignoredMessages++; continue; } var msg = await messageRepository.GetMessageAsync(msgId.Value, ct); renderedMessages.Add(RenderMessage(msgId, msg)); if (msg == null) notFoundMessages++; } var output = "Bulk message delete in"; if (channel != null) { output += $" #{channel.Name} ({channel.ID})"; if (rootChannel != null) output += $" (thread in #{rootChannel.Name} ({rootChannel.ID})"; } else { output += $" unknown channel {evt.ChannelID}"; } output += $"\nwith {renderedMessages.Count} messages\n\n"; output += string.Join("\n", renderedMessages); var embed = new EmbedBuilder() .WithTitle("Bulk message delete") .WithDescription( $""" {evt.IDs.Count} messages were deleted in <#{evt.ChannelID}> ({notFoundMessages} messages not found, {ignoredMessages} messages ignored) """ ) .WithColour(DiscordUtils.Red) .WithCurrentTimestamp(); await webhookExecutor.SendLogAsync( logChannel.Value, [embed.Build().GetOrThrow()], [ new FileData( $"bulk-delete-{evt.ChannelID}.txt", new MemoryStream(Encoding.UTF8.GetBytes(output)) ), ] ); return Result.Success; } private string RenderMessage(Snowflake messageId, MessageRepository.Message? message) { var timestamp = messageId.Timestamp.ToOffsetDateTime().ToString(); if (message == null) { return $""" [{timestamp}] Unknown message {messageId} -------------------------------------------- """; } var builder = new StringBuilder(); builder.Append($"[{timestamp}] {message.Username} ({message.UserId})\n"); if (message is { System: not null, Member: not null }) { builder.Append($"PK system: {message.System} | PK member: {message.Member}\n"); } builder.Append("--------------------------------------------\n"); builder.Append(message.Content); builder.Append("\n--------------------------------------------\n"); return builder.ToString(); } }