From df8af75dd4f407e1061b5b4554151a1d5891faf5 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 20 Aug 2024 18:18:17 +0200 Subject: [PATCH] finish ChannelUpdate responder --- .../Bot/Commands/ChannelCommands.cs | 1 - .../Bot/Commands/ChannelCommandsComponents.cs | 5 +- .../Bot/Commands/MetaCommands.cs | 4 - .../Channels/ChannelCreateResponder.cs | 5 +- .../Channels/ChannelUpdateResponder.cs | 122 ++++++++++++++++-- .../Bot/Responders/MessageCreateResponder.cs | 1 - .../Bot/Responders/MessageDeleteResponder.cs | 2 - .../Bot/Responders/MessageUpdateResponder.cs | 37 ++++-- .../Cache/InMemoryCache/UserCache.cs | 27 ++-- Catalogger.Backend/Catalogger.Backend.csproj | 1 + Catalogger.Backend/Database/QueryUtils.cs | 2 - .../Database/Redis/RedisService.cs | 4 - .../Services/MetricsCollectionService.cs | 1 - .../Services/WebhookExecutorService.cs | 6 +- 14 files changed, 155 insertions(+), 63 deletions(-) diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs index 9875bca..312bcfb 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs index c9d1c3d..dc342f7 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs @@ -1,4 +1,3 @@ -using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; @@ -152,8 +151,8 @@ public class ChannelCommandsComponents( var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType); string? channelMention; - if (channelId is null or 0) channelMention = null; - else if (guildChannels.All(c => c.ID != channelId.Value)) channelMention = $"unknown channel {channelId}"; + if (channelId is 0) channelMention = null; + else if (guildChannels.All(c => c.ID != channelId)) channelMention = $"unknown channel {channelId}"; else channelMention = $"<#{channelId}>"; List embeds = diff --git a/Catalogger.Backend/Bot/Commands/MetaCommands.cs b/Catalogger.Backend/Bot/Commands/MetaCommands.cs index 905dd52..8f3260c 100644 --- a/Catalogger.Backend/Bot/Commands/MetaCommands.cs +++ b/Catalogger.Backend/Bot/Commands/MetaCommands.cs @@ -1,19 +1,15 @@ using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; -using System.Text.Json; using App.Metrics; using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; using Catalogger.Backend.Extensions; using Humanizer; -using Microsoft.EntityFrameworkCore; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Feedback.Services; diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs index e341ac8..2c3231f 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs @@ -1,4 +1,3 @@ -using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; @@ -16,6 +15,7 @@ public class ChannelCreateResponder( DatabaseContext db, RoleCache roleCache, ChannelCache channelCache, + UserCache userCache, WebhookExecutorService webhookExecutor) : IResponder { public async Task RespondAsync(IChannelCreate ch, CancellationToken ct = default) @@ -57,13 +57,14 @@ public class ChannelCreateResponder( } else { + var user = await userCache.GetUserAsync(overwrite.ID); var embedFieldValue = ""; if (overwrite.Allow.GetPermissions().Count != 0) embedFieldValue += $"\u2705 {overwrite.Allow.ToPrettyString()}"; if (overwrite.Deny.GetPermissions().Count != 0) embedFieldValue += $"\n\n\u274c {overwrite.Deny.ToPrettyString()}"; - builder.AddField($"Override for user {overwrite.ID}", embedFieldValue.Trim()); + builder.AddField($"Override for {user?.Tag() ?? $"user {overwrite.ID}"}", embedFieldValue.Trim()); } } diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs index 1e4ba48..ddb291e 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs @@ -1,9 +1,9 @@ -using System.Diagnostics; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; +using Humanizer; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.Extensions.Embeds; @@ -39,18 +39,21 @@ public class ChannelUpdateResponder( var builder = new EmbedBuilder() .WithTitle(evt.Type switch { - ChannelType.GuildVoice => "Voice channel created", - ChannelType.GuildCategory => "Category channel created", - ChannelType.GuildAnnouncement or ChannelType.GuildText => "Text channel created", - _ => "Channel created" + ChannelType.GuildVoice => "Voice channel edited", + ChannelType.GuildCategory => "Category channel edited", + ChannelType.GuildAnnouncement or ChannelType.GuildText => "Text channel edited", + _ => "Channel edited" }) .WithColour(DiscordUtils.Blue) .WithFooter($"ID: {evt.ID} | Name: {evt.Name}") .WithCurrentTimestamp(); if (oldChannel.ParentID != evt.ParentID) - builder.AddField("Category", - CategoryUpdate(oldChannel.ParentID.OrDefault(), evt.ParentID.OrDefault())); + { + var categoryUpdate = CategoryUpdate(oldChannel.ParentID.OrDefault(), evt.ParentID.OrDefault()); + if (!string.IsNullOrWhiteSpace(categoryUpdate)) + builder.AddField("Category", categoryUpdate); + } if (oldChannel.Name != evt.Name) builder.AddField("Name", $"**Before:** {oldChannel.Name}\n**After:** {evt.Name}"); @@ -69,12 +72,32 @@ public class ChannelUpdateResponder( var oldOverrides = oldChannel.PermissionOverwrites.OrDefault() ?? []; var newOverrides = evt.PermissionOverwrites.OrDefault() ?? []; - var addedOverrides = newOverrides.Where(o => oldOverrides.All(o2 => o.ID != o2.ID)); + var addedOverrides = newOverrides.Where(o => oldOverrides.All(o2 => o.ID != o2.ID)).ToList(); var removedOverrides = oldOverrides.Where(o => newOverrides.All(o2 => o.ID != o2.ID)).ToList(); // Overrides filtered to ones that exist in both lists, but have different allow or deny values var editedOverrides = newOverrides.Where(o => oldOverrides.Any(o2 => o.ID == o2.ID && (o.Allow.Value != o2.Allow.Value || o.Deny.Value != o2.Deny.Value))); + if (addedOverrides.Count != 0) + { + var addedOverrideNames = new List(); + foreach (var o in addedOverrides) + { + if (o.Type is PermissionOverwriteType.Member) + { + var user = await userCache.GetUserAsync(o.ID); + addedOverrideNames.Add(user != null ? $"<@{user.ID}>" : $"user {o.ID}"); + } + else + { + addedOverrideNames.Add(roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}"); + break; + } + + builder.AddField("Added overrides", string.Join(", ", addedOverrideNames)); + } + } + if (removedOverrides.Count != 0) { var removedOverrideNames = new List(); @@ -94,18 +117,40 @@ public class ChannelUpdateResponder( builder.AddField("Removed overrides", string.Join(", ", removedOverrideNames)); } - - + foreach (var overwrite in editedOverrides) + { + var perms = string.Join("\n", + PermissionUpdate(oldOverrides.First(o => o.ID == overwrite.ID), overwrite)); + if (string.IsNullOrWhiteSpace(perms)) continue; + + builder.AddField(await OverwriteName(overwrite), perms.Trim()); + } + + foreach (var overwrite in addedOverrides) + { + var embedFieldValue = ""; + if (overwrite.Allow.GetPermissions().Count != 0) + embedFieldValue += $"\u2705 {overwrite.Allow.ToPrettyString()}"; + if (overwrite.Deny.GetPermissions().Count != 0) + embedFieldValue += $"\n\n\u274c {overwrite.Deny.ToPrettyString()}"; + if (string.IsNullOrWhiteSpace(embedFieldValue)) continue; + builder.AddField(await OverwriteName(overwrite), embedFieldValue.Trim()); + } + + // Sometimes we get channel update events for channels that didn't actually have anything loggable change. + // If that happens, there will be no embed fields, so just check for that + if (builder.Fields.Count == 0) return Result.Success; await webhookExecutor.QueueLogAsync(guildConfig, LogChannelType.ChannelUpdate, builder.Build().GetOrThrow()); + + return Result.Success; } finally + { channelCache.Set(evt); } - - throw new NotImplementedException(); } private string CategoryUpdate(Snowflake? oldCategory, Snowflake? newCategory) @@ -117,4 +162,57 @@ public class ChannelUpdateResponder( value += $"\n**After:** {newChannel.Name}"; return value.Trim(); } + + private async Task OverwriteName(IPermissionOverwrite overwrite) + { + switch (overwrite.Type) + { + case PermissionOverwriteType.Role: + return roleCache.TryGet(overwrite.ID, out var role) + ? $"Override for {role.Name}" + : $"Override for role {overwrite.ID}"; + case PermissionOverwriteType.Member: + var user = await userCache.GetUserAsync(overwrite.ID); + return user != null ? $"Override for {user.Tag()}" : $"Override for user {overwrite.ID}"; + default: + throw new ArgumentOutOfRangeException(nameof(overwrite), overwrite.Type, + "Invalid PermissionOverwriteType"); + } + } + + private static IEnumerable PermissionUpdate(IPermissionOverwrite oldOverwrite, + IPermissionOverwrite newOverwrite) + { + foreach (var perm in Enum.GetValues()) + { + if (newOverwrite.Allow.HasPermission(perm) && !oldOverwrite.Allow.HasPermission(perm) && + !oldOverwrite.Deny.HasPermission(perm)) + { + yield return $"\u2b1c \u279c \u2705 {perm.Humanize(LetterCasing.Title)}"; + } + else if (newOverwrite.Deny.HasPermission(perm) && !oldOverwrite.Allow.HasPermission(perm) && + !oldOverwrite.Deny.HasPermission(perm)) + { + yield return $"\u2b1c \u279c \u274c {perm.Humanize(LetterCasing.Title)}"; + } + else if (newOverwrite.Allow.HasPermission(perm) && oldOverwrite.Deny.HasPermission(perm)) + { + yield return $"\u274c \u279c \u2705 {perm.Humanize(LetterCasing.Title)}"; + } + else if (newOverwrite.Deny.HasPermission(perm) && oldOverwrite.Allow.HasPermission(perm)) + { + yield return $"\u2705 \u279c \u274c {perm.Humanize(LetterCasing.Title)}"; + } + else if (!newOverwrite.Allow.HasPermission(perm) && !newOverwrite.Deny.HasPermission(perm) && + oldOverwrite.Allow.HasPermission(perm)) + { + yield return $"\u2705 \u279c \u2b1c {perm.Humanize(LetterCasing.Title)}"; + } + else if (!newOverwrite.Allow.HasPermission(perm) && !newOverwrite.Deny.HasPermission(perm) && + oldOverwrite.Allow.HasPermission(perm)) + { + yield return $"\u274c \u279c \u2b1c {perm.Humanize(LetterCasing.Title)}"; + } + } + } } \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Responders/MessageCreateResponder.cs b/Catalogger.Backend/Bot/Responders/MessageCreateResponder.cs index 4ba968f..d71b009 100644 --- a/Catalogger.Backend/Bot/Responders/MessageCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/MessageCreateResponder.cs @@ -1,6 +1,5 @@ using System.Text.RegularExpressions; using App.Metrics; -using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Models; diff --git a/Catalogger.Backend/Bot/Responders/MessageDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/MessageDeleteResponder.cs index 0d93efd..12619c2 100644 --- a/Catalogger.Backend/Bot/Responders/MessageDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/MessageDeleteResponder.cs @@ -1,11 +1,9 @@ -using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; 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; diff --git a/Catalogger.Backend/Bot/Responders/MessageUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/MessageUpdateResponder.cs index f969b05..5032507 100644 --- a/Catalogger.Backend/Bot/Responders/MessageUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/MessageUpdateResponder.cs @@ -1,4 +1,3 @@ -using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; @@ -39,7 +38,6 @@ public class MessageUpdateResponder( return Result.Success; } - _logger.Debug("Guild is {GuildId}", msg.GuildID.Value); var guildConfig = await db.GetGuildAsync(msg.GuildID.Value, ct); if (await messageRepository.IsMessageIgnoredAsync(msg.ID.Value, ct)) @@ -48,12 +46,12 @@ public class MessageUpdateResponder( return Result.Success; } - var logChannel = webhookExecutor.GetLogChannel(guildConfig, LogChannelType.MessageUpdate, msg.ChannelID, - msg.Author.ID.Value); - if (logChannel == null) return Result.Success; - try { + var logChannel = webhookExecutor.GetLogChannel(guildConfig, LogChannelType.MessageUpdate, msg.ChannelID, + msg.Author.ID.Value); + if (logChannel == null) return Result.Success; + var oldMessage = await messageRepository.GetMessageAsync(msg.ID.Value, ct); if (oldMessage == null) { @@ -112,15 +110,26 @@ public class MessageUpdateResponder( } finally { - if (!await messageRepository.UpdateMessageAsync(msg, ct) && msg.ApplicationID.Is(DiscordUtils.PkUserId)) + // Messages should be *saved* if any of the message events are enabled for this channel, but should only + // be *logged* if the MessageUpdate event is enabled, so we check if we should save here. + // You also can't return early in `finally` blocks, so this has to be nested :( + if (webhookExecutor.GetLogChannel(guildConfig, LogChannelType.MessageUpdate, msg.ChannelID, + msg.Author.ID.Value) != null || webhookExecutor.GetLogChannel(guildConfig, + LogChannelType.MessageDelete, msg.ChannelID, + msg.Author.ID.Value) != null || webhookExecutor.GetLogChannel(guildConfig, + LogChannelType.MessageDeleteBulk, msg.ChannelID, + msg.Author.ID.Value) != null) { - _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); + 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); + } } } } diff --git a/Catalogger.Backend/Cache/InMemoryCache/UserCache.cs b/Catalogger.Backend/Cache/InMemoryCache/UserCache.cs index 24c6f77..d12151e 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/UserCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/UserCache.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using Catalogger.Backend.Extensions; +using LazyCache; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Rest.Core; @@ -7,20 +8,18 @@ namespace Catalogger.Backend.Cache.InMemoryCache; public class UserCache(IDiscordRestUserAPI userApi) { - private readonly ConcurrentDictionary _cache = new(); + private readonly IAppCache _cache = new CachingService(); + private int _cacheSize = 0; - public int Size => _cache.Count; + public int Size => _cacheSize; - public async Task GetUserAsync(Snowflake userId) - { - if (_cache.TryGetValue(userId, out var user)) return user; + public async Task GetUserAsync(Snowflake userId) => await _cache.GetOrAddAsync(userId.ToString(), + async () => + { + var user = await userApi.GetUserAsync(userId).GetOrThrow(); + Interlocked.Increment(ref _cacheSize); + return user; + }); - var res = await userApi.GetUserAsync(userId); - if (!res.IsSuccess) return null; - - _cache[userId] = res.Entity; - return res.Entity; - } - - public void UpdateUser(IUser user) => _cache[user.ID] = user; + public void UpdateUser(IUser user) => _cache.Add(user.ID.ToString(), user); } \ No newline at end of file diff --git a/Catalogger.Backend/Catalogger.Backend.csproj b/Catalogger.Backend/Catalogger.Backend.csproj index d0bd5c2..c4a3668 100644 --- a/Catalogger.Backend/Catalogger.Backend.csproj +++ b/Catalogger.Backend/Catalogger.Backend.csproj @@ -10,6 +10,7 @@ + diff --git a/Catalogger.Backend/Database/QueryUtils.cs b/Catalogger.Backend/Database/QueryUtils.cs index 5defa3e..1969d41 100644 --- a/Catalogger.Backend/Database/QueryUtils.cs +++ b/Catalogger.Backend/Database/QueryUtils.cs @@ -1,3 +1 @@ -using NodaTime; - namespace Catalogger.Backend.Database; \ No newline at end of file diff --git a/Catalogger.Backend/Database/Redis/RedisService.cs b/Catalogger.Backend/Database/Redis/RedisService.cs index 099c1c8..058ec73 100644 --- a/Catalogger.Backend/Database/Redis/RedisService.cs +++ b/Catalogger.Backend/Database/Redis/RedisService.cs @@ -1,8 +1,4 @@ using System.Text.Json; -using Remora.Discord.API; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Objects; -using Remora.Rest.Json; using StackExchange.Redis; namespace Catalogger.Backend.Database.Redis; diff --git a/Catalogger.Backend/Services/MetricsCollectionService.cs b/Catalogger.Backend/Services/MetricsCollectionService.cs index 2d6da0d..2c48800 100644 --- a/Catalogger.Backend/Services/MetricsCollectionService.cs +++ b/Catalogger.Backend/Services/MetricsCollectionService.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using App.Metrics; -using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Humanizer; diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index 56e3b3e..f278e77 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -153,9 +153,9 @@ public class WebhookExecutorService( if (logChannelType is LogChannelType.MessageUpdate or LogChannelType.MessageDelete or LogChannelType.MessageDeleteBulk) { - if (GetDefaultLogChannel(guild, logChannelType) == null) return null; + if (GetDefaultLogChannel(guild, logChannelType) == 0) return null; - ulong categoryRedirect = categoryId != null + var categoryRedirect = categoryId != null ? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value) : 0; @@ -168,7 +168,7 @@ public class WebhookExecutorService( return GetDefaultLogChannel(guild, logChannelType); } - public static ulong? GetDefaultLogChannel(Guild guild, LogChannelType channelType) => channelType switch + public static ulong GetDefaultLogChannel(Guild guild, LogChannelType channelType) => channelType switch { LogChannelType.GuildUpdate => guild.Channels.GuildUpdate, LogChannelType.GuildEmojisUpdate => guild.Channels.GuildEmojisUpdate,