diff --git a/PluralKit.API/Controllers/PKControllerBase.cs b/PluralKit.API/Controllers/PKControllerBase.cs index 18e8593a..e20aeed5 100644 --- a/PluralKit.API/Controllers/PKControllerBase.cs +++ b/PluralKit.API/Controllers/PKControllerBase.cs @@ -18,6 +18,7 @@ public class PKControllerBase: ControllerBase protected readonly ApiConfig _config; protected readonly IDatabase _db; protected readonly ModelRepository _repo; + protected readonly RedisService _redis; protected readonly DispatchService _dispatch; public PKControllerBase(IServiceProvider svc) @@ -25,6 +26,7 @@ public class PKControllerBase: ControllerBase _config = svc.GetRequiredService(); _db = svc.GetRequiredService(); _repo = svc.GetRequiredService(); + _redis = svc.GetRequiredService(); _dispatch = svc.GetRequiredService(); } diff --git a/PluralKit.API/Controllers/PrivateController.cs b/PluralKit.API/Controllers/PrivateController.cs index a4ef7991..561fd23b 100644 --- a/PluralKit.API/Controllers/PrivateController.cs +++ b/PluralKit.API/Controllers/PrivateController.cs @@ -20,12 +20,7 @@ namespace PluralKit.API; [Route("private")] public class PrivateController: PKControllerBase { - private readonly RedisService _redis; - - public PrivateController(IServiceProvider svc) : base(svc) - { - _redis = svc.GetRequiredService(); - } + public PrivateController(IServiceProvider svc) : base(svc) { } [HttpGet("meta")] public async Task> Meta() diff --git a/PluralKit.API/Controllers/v2/DiscordControllerV2.cs b/PluralKit.API/Controllers/v2/DiscordControllerV2.cs index ec624236..340c8698 100644 --- a/PluralKit.API/Controllers/v2/DiscordControllerV2.cs +++ b/PluralKit.API/Controllers/v2/DiscordControllerV2.cs @@ -92,7 +92,9 @@ public class DiscordControllerV2: PKControllerBase [HttpGet("messages/{messageId}")] public async Task> MessageGet(ulong messageId) { - var msg = await _repo.GetFullMessage(messageId); + var messageByOriginal = await _redis.GetOriginalMid(messageId); + + var msg = await _repo.GetFullMessage(messageByOriginal ?? messageId); if (msg == null) throw Errors.MessageNotFound; diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 6398f34f..9cc07d7d 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -40,6 +40,7 @@ public class Context Cache = provider.Resolve(); Database = provider.Resolve(); Repository = provider.Resolve(); + Redis = provider.Resolve(); _metrics = provider.Resolve(); _provider = provider; _commandMessageService = provider.Resolve(); @@ -74,6 +75,7 @@ public class Context internal readonly IDatabase Database; internal readonly ModelRepository Repository; + internal readonly RedisService Redis; public async Task Reply(string text = null, Embed embed = null, AllowedMentions? mentions = null) { diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 1bf1bf92..6cb01e84 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -164,7 +164,7 @@ public class ProxiedMessage ulong? recent = null; if (isReproxy) - recent = await ctx.Repository.GetLastMessage(ctx.Guild.Id, ctx.Channel.Id, ctx.Author.Id); + recent = await ctx.Redis.GetLastMessage(ctx.Author.Id, ctx.Channel.Id); else recent = await FindRecentMessage(ctx, timeout); @@ -210,13 +210,13 @@ public class ProxiedMessage return (msg, member.System); } - private async Task FindRecentMessage(Context ctx, Duration timeout) + private async Task FindRecentMessage(Context ctx, Duration timeout) { - var lastMessage = await ctx.Repository.GetLastMessage(ctx.Guild.Id, ctx.Channel.Id, ctx.Author.Id); + var lastMessage = await ctx.Redis.GetLastMessage(ctx.Author.Id, ctx.Channel.Id); if (lastMessage == null) return null; - var timestamp = DiscordUtils.SnowflakeToInstant(lastMessage.Mid); + var timestamp = DiscordUtils.SnowflakeToInstant(lastMessage.Value); if (SystemClock.Instance.GetCurrentInstant() - timestamp > timeout) return null; diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 52e12b2b..a3f0823b 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -22,6 +22,7 @@ public class ProxyService private static readonly TimeSpan MessageDeletionDelay = TimeSpan.FromMilliseconds(1000); private readonly IDiscordCache _cache; private readonly IDatabase _db; + private readonly RedisService _redis; private readonly DispatchService _dispatch; private readonly LastMessageCacheService _lastMessage; @@ -35,13 +36,14 @@ public class ProxyService private readonly NodaTime.IClock _clock; public ProxyService(LogChannelService logChannel, ILogger logger, WebhookExecutorService webhookExecutor, - DispatchService dispatch, IDatabase db, ProxyMatcher matcher, IMetrics metrics, ModelRepository repo, + DispatchService dispatch, IDatabase db, RedisService redis, ProxyMatcher matcher, IMetrics metrics, ModelRepository repo, NodaTime.IClock clock, IDiscordCache cache, DiscordApiClient rest, LastMessageCacheService lastMessage) { _logChannel = logChannel; _webhookExecutor = webhookExecutor; _dispatch = dispatch; _db = db; + _redis = redis; _matcher = matcher; _metrics = metrics; _repo = repo; @@ -420,6 +422,18 @@ public class ProxyService Task SaveMessageInDatabase() => _repo.AddMessage(sentMessage); + async Task SaveMessageInRedis() + { + // logclean info + await _redis.SetLogCleanup(triggerMessage.Author.Id, triggerMessage.GuildId.Value); + + // last message info (edit/reproxy) + await _redis.SetLastMessage(triggerMessage.Author.Id, triggerMessage.ChannelId, sentMessage.Mid); + + // "by original mid" lookup + await _redis.SetOriginalMid(triggerMessage.Id, proxyMessage.Id); + } + Task LogMessageToChannel() => _logChannel.LogMessage(sentMessage, triggerMessage, proxyMessage).AsTask(); @@ -458,6 +472,7 @@ public class ProxyService await Task.WhenAll( DeleteProxyTriggerMessage(), SaveMessageInDatabase(), + SaveMessageInRedis(), LogMessageToChannel(), SaveLatchAutoproxy(), DispatchWebhook() diff --git a/PluralKit.Bot/Services/LoggerCleanService.cs b/PluralKit.Bot/Services/LoggerCleanService.cs index 304b2a54..c0c5369f 100644 --- a/PluralKit.Bot/Services/LoggerCleanService.cs +++ b/PluralKit.Bot/Services/LoggerCleanService.cs @@ -79,12 +79,12 @@ public class LoggerCleanService private readonly IDiscordCache _cache; private readonly DiscordApiClient _client; - private readonly IDatabase _db; + private readonly RedisService _redis; private readonly ILogger _logger; - public LoggerCleanService(IDatabase db, DiscordApiClient client, IDiscordCache cache, ILogger logger) + public LoggerCleanService(RedisService redis, DiscordApiClient client, IDiscordCache cache, ILogger logger) { - _db = db; + _redis = redis; _client = client; _cache = cache; _logger = logger.ForContext(); @@ -124,20 +124,10 @@ public class LoggerCleanService _logger.Debug("Fuzzy logclean for {BotName} on {MessageId}: {@FuzzyExtractResult}", bot.Name, msg.Id, fuzzy); - var mid = await _db.Execute(conn => - conn.QuerySingleOrDefaultAsync( - "select mid from messages where sender = @User and mid > @ApproxID and guild = @Guild limit 1", - new - { - fuzzy.Value.User, - Guild = msg.GuildId, - ApproxId = DiscordUtils.InstantToSnowflake( - fuzzy.Value.ApproxTimestamp - Duration.FromSeconds(3)) - })); + var exists = await _redis.HasLogCleanup(fuzzy.Value.User, msg.GuildId.Value); // If we didn't find a corresponding message, bail - if (mid == null) - return; + if (!exists) return; // Otherwise, we can *reasonably assume* that this is a logged deletion, so delete the log message. await _client.DeleteMessage(msg.ChannelId, msg.Id); @@ -151,8 +141,7 @@ public class LoggerCleanService _logger.Debug("Pure logclean for {BotName} on {MessageId}: {@FuzzyExtractResult}", bot.Name, msg.Id, extractedId); - var mid = await _db.Execute(conn => conn.QuerySingleOrDefaultAsync( - "select mid from messages where original_mid = @Mid", new { Mid = extractedId.Value })); + var mid = await _redis.GetOriginalMid(extractedId.Value); if (mid == null) return; // If we've gotten this far, we found a logged deletion of a trigger message. Just yeet it! diff --git a/PluralKit.Core/Services/RedisService.cs b/PluralKit.Core/Services/RedisService.cs index 22544514..cd283f56 100644 --- a/PluralKit.Core/Services/RedisService.cs +++ b/PluralKit.Core/Services/RedisService.cs @@ -11,4 +11,41 @@ public class RedisService if (config.RedisAddr != null) Connection = await ConnectionMultiplexer.ConnectAsync(config.RedisAddr); } + + private string LastMessageKey(ulong userId, ulong channelId) => $"user_last_message:{userId}:{channelId}"; + public Task SetLastMessage(ulong userId, ulong channelId, ulong mid) + => Connection.GetDatabase().UlongSetAsync(LastMessageKey(userId, channelId), mid, expiry: TimeSpan.FromMinutes(10)); + public Task GetLastMessage(ulong userId, ulong channelId) + => Connection.GetDatabase().UlongGetAsync(LastMessageKey(userId, channelId)); + + private string LoggerCleanKey(ulong userId, ulong guildId) => $"log_cleanup:{userId}:{guildId}"; + public Task SetLogCleanup(ulong userId, ulong guildId) + => Connection.GetDatabase().StringSetAsync(LoggerCleanKey(userId, guildId), 1, expiry: TimeSpan.FromSeconds(3)); + public Task HasLogCleanup(ulong userId, ulong guildId) + => Connection.GetDatabase().KeyExistsAsync(LoggerCleanKey(userId, guildId)); + + // note: these methods are named weird - they actually get the proxied mid from the original mid + // but anything else would've been more confusing + private string OriginalMidKey(ulong original_mid) => $"original_mid:{original_mid}"; + public Task SetOriginalMid(ulong original_mid, ulong proxied_mid) + => Connection.GetDatabase().UlongSetAsync(OriginalMidKey(original_mid), proxied_mid, expiry: TimeSpan.FromMinutes(30)); + public Task GetOriginalMid(ulong original_mid) + => Connection.GetDatabase().UlongGetAsync(OriginalMidKey(original_mid)); +} + +public static class RedisExt +{ + public static async Task UlongGetAsync(this StackExchange.Redis.IDatabase database, string key) + { + var data = await database.StringGetAsync(key); + if (data == RedisValue.Null) return null; + + if (ulong.TryParse(data, out var value)) + return value; + + return null; + } + + public static Task UlongSetAsync(this StackExchange.Redis.IDatabase database, string key, ulong value, TimeSpan? expiry = null) + => database.StringSetAsync(key, value.ToString(), expiry); } \ No newline at end of file diff --git a/docs/content/api/endpoints.md b/docs/content/api/endpoints.md index 551dfbfb..0f64f111 100644 --- a/docs/content/api/endpoints.md +++ b/docs/content/api/endpoints.md @@ -333,4 +333,8 @@ GET `/messages/{message}` Message can be the ID of a proxied message, or the ID of the message that sent the proxy. +::: warning +Looking up messages by the original message ID only works **up to 30 minutes** after the message was sent. +::: + Returns a [message object](/api/models#message-object).