diff --git a/Catalogger.Backend/Bot/DiscordUtils.cs b/Catalogger.Backend/Bot/DiscordUtils.cs index c0c531a..4fd0c60 100644 --- a/Catalogger.Backend/Bot/DiscordUtils.cs +++ b/Catalogger.Backend/Bot/DiscordUtils.cs @@ -1,9 +1,13 @@ using System.Drawing; +using Remora.Discord.API; +using Remora.Rest.Core; namespace Catalogger.Backend.Bot; public static class DiscordUtils { + public static readonly Snowflake PkUserId = DiscordSnowflake.New(466378653216014359); + public static readonly Color Red = Color.FromArgb(231, 76, 60); public static readonly Color Purple = Color.FromArgb(155, 89, 182); } \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Responders/MessageCreateResponder.cs b/Catalogger.Backend/Bot/Responders/MessageCreateResponder.cs index 5d94fb2..c608fb8 100644 --- a/Catalogger.Backend/Bot/Responders/MessageCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/MessageCreateResponder.cs @@ -26,7 +26,6 @@ public class MessageCreateResponder( : IResponder { private readonly ILogger _logger = logger.ForContext(); - private static readonly Snowflake PkUserId = DiscordSnowflake.New(466378653216014359); public async Task RespondAsync(IMessageCreate msg, CancellationToken ct = default) { @@ -50,11 +49,11 @@ public class MessageCreateResponder( return Result.Success; } - if (msg.Author.ID == PkUserId) + if (msg.Author.ID == DiscordUtils.PkUserId) _ = pkMessageHandler.HandlePkMessageAsync(msg); - if (msg.ApplicationID.IsDefined(out var appId) && appId == PkUserId) + if (msg.ApplicationID.Is(DiscordUtils.PkUserId)) _ = pkMessageHandler.HandleProxiedMessageAsync(msg.ID.Value); - else if (msg.ApplicationID.HasValue && appId == config.Discord.ApplicationId) + else if (msg.ApplicationID.HasValue && msg.ApplicationID.Is(config.Discord.ApplicationId)) { db.IgnoredMessages.Add(new IgnoredMessage(msg.ID.Value)); await db.SaveChangesAsync(ct); diff --git a/Catalogger.Backend/Bot/Responders/MessageDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/MessageDeleteResponder.cs index a389c85..bc8ebb0 100644 --- a/Catalogger.Backend/Bot/Responders/MessageDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/MessageDeleteResponder.cs @@ -4,6 +4,7 @@ using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; +using Microsoft.VisualBasic; using NodaTime; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; @@ -11,6 +12,7 @@ using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; +using Remora.Rest.Core; using Remora.Results; namespace Catalogger.Backend.Bot.Responders; @@ -22,15 +24,18 @@ public class MessageDeleteResponder( WebhookExecutorService webhookExecutor, ChannelCacheService channelCache, UserCacheService userCache, - IClock clock) : IResponder + IClock clock, + PluralkitApiService pluralkitApi) : IResponder { private readonly ILogger _logger = logger.ForContext(); + private static bool MaybePkProxyTrigger(Snowflake id) => id.Timestamp > DateTimeOffset.Now - 1.Minutes(); + public async Task RespondAsync(IMessageDelete ev, CancellationToken ct = default) { if (!ev.GuildID.IsDefined()) return Result.Success; - if (ev.ID.Timestamp < DateTimeOffset.Now - 1.Minutes()) + if (MaybePkProxyTrigger(ev.ID)) { _logger.Debug( "Deleted message {MessageId} is less than 1 minute old, delaying 5 seconds to give PK time to catch up", @@ -59,6 +64,20 @@ public class MessageDeleteResponder( return Result.Success; } + // Check if the message is an edit trigger message. + // If it is, the API will return a valid message for its ID, but the ID won't match either `Id` or `Original`. + // (We also won't have any system/member information stored for it) + if (msg is { System: null, Member: null } && MaybePkProxyTrigger(ev.ID) && false) + { + // TODO: remove the "false" if/when the API is updated to actually return this :neofox_woozy: + var pkMsg = await pluralkitApi.GetPluralKitMessageAsync(ev.ID.Value, ct); + if (pkMsg != null && pkMsg.Id != ev.ID.Value && pkMsg.Original != ev.ID.Value) + { + _logger.Debug("Deleted message {MessageId} is a `pk;edit` message, ignoring", ev.ID); + return Result.Success; + } + } + logChannel = webhookExecutor.GetLogChannel(guild, LogChannelType.MessageDelete, ev.ChannelID, msg.UserId); if (logChannel == null) return Result.Success; diff --git a/Catalogger.Backend/Bot/Responders/MessageUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/MessageUpdateResponder.cs index 9e99e88..9d309e1 100644 --- a/Catalogger.Backend/Bot/Responders/MessageUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/MessageUpdateResponder.cs @@ -20,7 +20,8 @@ public class MessageUpdateResponder( ChannelCacheService channelCache, UserCacheService userCache, MessageRepository messageRepository, - WebhookExecutorService webhookExecutor) : IResponder + WebhookExecutorService webhookExecutor, + PluralkitApiService pluralkitApi) : IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -76,8 +77,7 @@ public class MessageUpdateResponder( .WithFooter($"ID: {msg.ID}") .WithTimestamp(msg.ID.Timestamp); - var fields = Enumerable.Range(0, msg.Content.Length / 1000) - .Select(i => msg.Content.Substring(i * 1000, 1000)) + var fields = ChunksUpTo(msg.Content, 1000) .Select((s, i) => new EmbedField($"New content{(i != 0 ? " (cont.)" : "")}", s, false)) .ToList(); @@ -110,7 +110,16 @@ public class MessageUpdateResponder( } finally { - await messageRepository.UpdateMessageAsync(msg, ct); + if (!await messageRepository.UpdateMessageAsync(msg, ct) && msg.ApplicationID.Is(DiscordUtils.PkUserId)) + { + _logger.Debug( + "Message {MessageId} wasn't stored yet and was proxied by PluralKit, fetching proxy information from its API", + msg.ID); + var pkMsg = await pluralkitApi.GetPluralKitMessageAsync(msg.ID.Value, ct); + if (pkMsg != null) + await messageRepository.SetProxiedMessageDataAsync(msg.ID.Value, pkMsg.Original, pkMsg.Sender, + pkMsg.System?.Id, pkMsg.Member?.Id); + } } } @@ -122,4 +131,10 @@ public class MessageUpdateResponder( evt.WebhookID, evt.Type.GetOrThrow(), evt.Activity, evt.Application, evt.ApplicationID, evt.MessageReference, evt.Flags, evt.ReferencedMessage, evt.Interaction, evt.Thread, evt.Components, evt.StickerItems, evt.Position, evt.Resolved, evt.InteractionMetadata, evt.Poll); + + private static IEnumerable ChunksUpTo(string str, int maxChunkSize) + { + for (var i = 0; i < str.Length; i += maxChunkSize) + yield return str.Substring(i, Math.Min(maxChunkSize, str.Length - i)); + } } \ No newline at end of file diff --git a/Catalogger.Backend/Database/Queries/MessageRepository.cs b/Catalogger.Backend/Database/Queries/MessageRepository.cs index 1d94a0c..b6a6a25 100644 --- a/Catalogger.Backend/Database/Queries/MessageRepository.cs +++ b/Catalogger.Backend/Database/Queries/MessageRepository.cs @@ -36,7 +36,12 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe await db.SaveChangesAsync(ct); } - public async Task UpdateMessageAsync(IMessageCreate msg, CancellationToken ct = default) + /// + /// Updates an edited message. + /// + /// true if the message was already stored and got updated, + /// false if the message wasn't stored and was newly inserted. + public async Task UpdateMessageAsync(IMessageCreate msg, CancellationToken ct = default) { _logger.Debug("Updating message {MessageId}", msg.ID); @@ -44,13 +49,16 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe var (isStored, _) = await HasProxyInfoAsync(msg.ID.Value); if (!isStored) { + _logger.Debug("Edited message {MessageId} is not stored yet, storing it", msg.ID); await SaveMessageAsync(msg, ct); + await tx.CommitAsync(ct); + return false; } else { var metadata = new Metadata(IsWebhook: msg.WebhookID.HasValue, msg.Attachments.Select(a => new Attachment(a.Filename, a.Size, a.ContentType.Value))); - + var dbMsg = await db.Messages.FindAsync(msg.ID.Value); if (dbMsg == null) throw new CataloggerError("Message was null despite HasProxyInfoAsync returning true"); @@ -62,9 +70,9 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe db.Update(dbMsg); await db.SaveChangesAsync(ct); + await tx.CommitAsync(ct); + return true; } - - await tx.CommitAsync(ct); } public async Task GetMessageAsync(ulong id, CancellationToken ct = default) diff --git a/Catalogger.Backend/Extensions/DiscordExtensions.cs b/Catalogger.Backend/Extensions/DiscordExtensions.cs index 4e96c1d..723e484 100644 --- a/Catalogger.Backend/Extensions/DiscordExtensions.cs +++ b/Catalogger.Backend/Extensions/DiscordExtensions.cs @@ -26,7 +26,7 @@ public static class DiscordExtensions var avatarIndex = user.Discriminator == 0 ? (int)((user.ID.Value >> 22) % 6) : user.Discriminator % 5; return $"https://cdn.discordapp.com/embed/avatars/{avatarIndex}.png?size={size}"; } - + public static string? IconUrl(this IGuild guild, int size = 256) { if (guild.Icon == null) return null; @@ -44,6 +44,9 @@ public static class DiscordExtensions return snowflake.Value.Value; } + public static bool Is(this Optional s1, Snowflake s2) => s1.IsDefined(out var value) && value == s2; + public static bool Is(this Optional s1, ulong s2) => s1.IsDefined(out var value) && value == s2; + public static T GetOrThrow(this Result result) { if (result.Error != null) throw new DiscordRestException(result.Error.Message);