using System.Text.RegularExpressions; using Catalogger.Backend.Cache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Models; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; namespace Catalogger.Backend.Bot.Responders; public class MessageCreateResponder( ILogger logger, Config config, DatabaseContext db, MessageRepository messageRepository, UserCacheService userCache, PkMessageHandler pkMessageHandler) : IResponder { private readonly ILogger _logger = logger.ForContext(); private static readonly Snowflake PkUserId = DiscordSnowflake.New(466378653216014359); public async Task RespondAsync(IMessageCreate msg, CancellationToken ct = default) { userCache.UpdateUser(msg.Author); if (!msg.GuildID.IsDefined()) { _logger.Debug("Received message create event for message {MessageId} despite it not being in a guild", msg.ID); return Result.Success; } var guild = await db.GetGuildAsync(msg.GuildID, ct); // The guild needs to have enabled at least one of the message logging events, // and the channel must not be ignored, to store the message. if (guild.IsMessageIgnored(msg.ChannelID, msg.Author.ID)) { db.IgnoredMessages.Add(new IgnoredMessage(msg.ID.ToUlong())); await db.SaveChangesAsync(ct); return Result.Success; } if (msg.Author.ID == PkUserId) _ = pkMessageHandler.HandlePkMessageAsync(msg); if (msg.ApplicationID.IsDefined(out var appId) && appId == PkUserId) _ = pkMessageHandler.HandleProxiedMessageAsync(msg.ID.Value); else if (msg.ApplicationID.HasValue && appId == config.Discord.ApplicationId) { db.IgnoredMessages.Add(new IgnoredMessage(msg.ID.Value)); await db.SaveChangesAsync(ct); return Result.Success; } await messageRepository.SaveMessageAsync(msg, ct); return Result.Success; } } public partial class PkMessageHandler(ILogger logger, IServiceProvider services) { private readonly ILogger _logger = logger.ForContext(); [GeneratedRegex( @"^System ID: (\w{5,6}) \| Member ID: (\w{5,6}) \| Sender: .+ \((\d+)\) \| Message ID: (\d+) \| Original Message ID: (\d+)$")] private static partial Regex FooterRegex(); [GeneratedRegex(@"^https:\/\/discord.com\/channels\/\d+\/(\d+)\/\d+$")] private static partial Regex LinkRegex(); public async Task HandlePkMessageAsync(IMessageCreate msg) { _logger.Debug("Received PluralKit message"); await Task.Delay(500.Milliseconds()); _logger.Debug("Starting handling PluralKit message"); // Check if the content matches a Discord link--if not, it's not a log message (we already check if this is a PluralKit message earlier) if (!LinkRegex().IsMatch(msg.Content)) { _logger.Debug("PluralKit message is not a log message because content is not a link"); return; } // The first (only, I think always?) embed's footer must match the expected format var firstEmbed = msg.Embeds.FirstOrDefault(); if (firstEmbed == null || !firstEmbed.Footer.TryGet(out var footer) || !FooterRegex().IsMatch(footer.Text)) { _logger.Debug( "PK message is not a log message because there is no first embed or its footer doesn't match the regex"); return; } var match = FooterRegex().Match(footer.Text); if (!ulong.TryParse(match.Groups[3].Value, out var authorId)) { _logger.Debug("Author ID in PluralKit log {LogMessageId} was not a valid snowflake", msg.ID); return; } if (!ulong.TryParse(match.Groups[4].Value, out var msgId)) { _logger.Debug("Message ID in PluralKit log {LogMessageId} was not a valid snowflake", msg.ID); return; } if (!ulong.TryParse(match.Groups[5].Value, out var originalId)) { _logger.Debug("Original ID in PluralKit log {LogMessageId} was not a valid snowflake", msg.ID); return; } await using var scope = services.CreateAsyncScope(); await using var db = scope.ServiceProvider.GetRequiredService(); var messageRepository = scope.ServiceProvider.GetRequiredService(); await messageRepository.SetProxiedMessageDataAsync(msgId, originalId, authorId, systemId: match.Groups[1].Value, memberId: match.Groups[2].Value); db.IgnoredMessages.Add(new IgnoredMessage(originalId)); await db.SaveChangesAsync(); } public async Task HandleProxiedMessageAsync(ulong msgId) { await Task.Delay(3.Seconds()); await using var scope = services.CreateAsyncScope(); await using var db = scope.ServiceProvider.GetRequiredService(); var messageRepository = scope.ServiceProvider.GetRequiredService(); var pluralkitApi = scope.ServiceProvider.GetRequiredService(); var (isStored, hasProxyInfo) = await messageRepository.HasProxyInfoAsync(msgId); if (!isStored) { _logger.Debug("Message with ID {MessageId} is not stored in the database", msgId); return; } if (hasProxyInfo) return; var pkMessage = await pluralkitApi.GetPluralKitMessageAsync(msgId); if (pkMessage == null) { _logger.Debug("Message with ID {MessageId} was proxied by PluralKit, but API returned 404", msgId); return; } await messageRepository.SetProxiedMessageDataAsync(msgId, pkMessage.Original, pkMessage.Sender, pkMessage.System?.Id, pkMessage.Member?.Id); db.IgnoredMessages.Add(new IgnoredMessage(pkMessage.Original)); await db.SaveChangesAsync(); } }