// Copyright (C) 2021-present sam (starshines.gay) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . using System.Text; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database.Dapper.Repositories; 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, GuildRepository guildRepository, DapperMessageRepository messageRepository, WebhookExecutorService webhookExecutor, ChannelCache channelCache ) : IResponder { private readonly ILogger _logger = logger.ForContext(); public async Task RespondAsync(IMessageDeleteBulk evt, CancellationToken ct = default) { var guild = await guildRepository.GetAsync(evt.GuildID); 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)) { 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, DapperMessageRepository.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(); } }