From 4f718cde206e9eeddd5dc4b7f3158e751d1b8e91 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 14 Oct 2024 17:09:12 +0200 Subject: [PATCH] feat: emoji update event --- .../Bot/Commands/MetaCommands.cs | 3 +- .../Responders/Guilds/GuildCreateResponder.cs | 4 +- .../Guilds/GuildEmojisUpdateResponder.cs | 128 ++++++++++++++++++ .../Cache/InMemoryCache/EmojiCache.cs | 33 +++++ Catalogger.Backend/CataloggerMetrics.cs | 5 + .../Extensions/StartupExtensions.cs | 1 + .../Services/MetricsCollectionService.cs | 2 + 7 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs create mode 100644 Catalogger.Backend/Cache/InMemoryCache/EmojiCache.cs diff --git a/Catalogger.Backend/Bot/Commands/MetaCommands.cs b/Catalogger.Backend/Bot/Commands/MetaCommands.cs index d08ce9a..cf96522 100644 --- a/Catalogger.Backend/Bot/Commands/MetaCommands.cs +++ b/Catalogger.Backend/Bot/Commands/MetaCommands.cs @@ -46,6 +46,7 @@ public class MetaCommands( ContextInjectionService contextInjection, GuildCache guildCache, ChannelCache channelCache, + EmojiCache emojiCache, IDiscordRestChannelAPI channelApi ) : CommandGroup { @@ -107,7 +108,7 @@ public class MetaCommands( embed.AddField( "Numbers", $"{CataloggerMetrics.MessagesStored.Value:N0} messages " - + $"from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels", + + $"from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels, {emojiCache.Size:N0}", false ); diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs index ddff25c..5abc461 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs @@ -16,13 +16,13 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Models; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; +using Guild = Catalogger.Backend.Database.Models.Guild; namespace Catalogger.Backend.Bot.Responders.Guilds; @@ -33,6 +33,7 @@ public class GuildCreateResponder( GuildCache guildCache, RoleCache roleCache, ChannelCache channelCache, + EmojiCache emojiCache, WebhookExecutorService webhookExecutor, IMemberCache memberCache, GuildFetchService guildFetchService @@ -55,6 +56,7 @@ public class GuildCreateResponder( guildName = guild.Name; guildCache.Set(guild); + emojiCache.Set(guild.ID, guild.Emojis); foreach (var c in guild.Channels) channelCache.Set(c, guild.ID); foreach (var r in guild.Roles) diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs new file mode 100644 index 0000000..b7e2c7e --- /dev/null +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs @@ -0,0 +1,128 @@ +// 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 Catalogger.Backend.Cache.InMemoryCache; +using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Extensions; +using Catalogger.Backend.Services; +using Remora.Discord.API; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Catalogger.Backend.Bot.Responders.Guilds; + +public class GuildEmojisUpdateResponder( + ILogger logger, + DatabaseContext db, + EmojiCache emojiCache, + WebhookExecutorService webhookExecutor +) : IResponder +{ + private readonly ILogger _logger = logger.ForContext(); + + public async Task RespondAsync(IGuildEmojisUpdate evt, CancellationToken ct = default) + { + try + { + if (!emojiCache.TryGet(evt.GuildID, out var oldEmoji)) + { + _logger.Information( + "Previous emoji for {GuildId} were not in cache, ignoring event", + evt.GuildID + ); + return Result.Success; + } + + IEmbed embed; + + // As far as I know, only one emoji can be added or removed at once. + var added = evt.Emojis.FirstOrDefault(e => oldEmoji.All(o => o.ID != e.ID)); + var removed = oldEmoji.FirstOrDefault(o => evt.Emojis.All(e => o.ID != e.ID)); + var updated = evt.Emojis.FirstOrDefault(e => + oldEmoji.Any(o => o.ID == e.ID && o.Name != e.Name) + ); + if (added != null) + { + var url = CDN.GetEmojiUrl(added).GetOrThrow().ToString(); + embed = new EmbedBuilder() + .WithTitle("Emoji created") + .WithDescription($"{FormatEmoji(added)} [{added.Name}]({url})") + .WithThumbnailUrl(url) + .WithFooter($"ID: {added.ID}") + .WithColour(DiscordUtils.Green) + .WithCurrentTimestamp() + .Build() + .GetOrThrow(); + } + else if (removed != null) + { + var url = CDN.GetEmojiUrl(removed).GetOrThrow().ToString(); + embed = new EmbedBuilder() + .WithTitle("Emoji removed") + .WithDescription($"[{removed.Name}]({url})") + .WithThumbnailUrl(url) + .WithFooter($"ID: {removed.ID}") + .WithColour(DiscordUtils.Red) + .WithCurrentTimestamp() + .Build() + .GetOrThrow(); + } + else if (updated != null) + { + var url = CDN.GetEmojiUrl(updated).GetOrThrow().ToString(); + var previous = oldEmoji.First(o => o.ID == updated.ID); + embed = new EmbedBuilder() + .WithTitle("Emoji renamed") + .WithDescription( + $""" + {FormatEmoji(updated)} [{updated.Name}]({url}) + {previous.Name} → {updated.Name} + """ + ) + .WithThumbnailUrl(url) + .WithFooter($"ID: {updated.ID}") + .WithColour(DiscordUtils.Green) + .WithCurrentTimestamp() + .Build() + .GetOrThrow(); + } + else + { + _logger.Warning( + "Received emoji update event for {GuildId} but all emoji were identical, not logging", + evt.GuildID + ); + return Result.Success; + } + + var guildConfig = await db.GetGuildAsync(evt.GuildID, ct); + webhookExecutor.QueueLog(guildConfig, LogChannelType.GuildEmojisUpdate, embed); + return Result.Success; + } + finally + { + emojiCache.Set(evt.GuildID, evt.Emojis); + } + } + + private static string FormatEmoji(IEmoji emoji) => + emoji.IsAnimated.OrDefault(false) + ? $"" + : $"<:{emoji.Name}:{emoji.ID}>"; +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/EmojiCache.cs b/Catalogger.Backend/Cache/InMemoryCache/EmojiCache.cs new file mode 100644 index 0000000..734661c --- /dev/null +++ b/Catalogger.Backend/Cache/InMemoryCache/EmojiCache.cs @@ -0,0 +1,33 @@ +// 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.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Rest.Core; + +namespace Catalogger.Backend.Cache.InMemoryCache; + +public class EmojiCache +{ + private readonly ConcurrentDictionary> _emojis = new(); + + public void Set(Snowflake guildId, IReadOnlyList emoji) => _emojis[guildId] = emoji; + + public bool TryGet(Snowflake guildId, [NotNullWhen(true)] out IReadOnlyList? emoji) => + _emojis.TryGetValue(guildId, out emoji); + + public int Size => _emojis.Select(kv => kv.Value.Count).Sum(); +} diff --git a/Catalogger.Backend/CataloggerMetrics.cs b/Catalogger.Backend/CataloggerMetrics.cs index 0cc7e06..b8726b5 100644 --- a/Catalogger.Backend/CataloggerMetrics.cs +++ b/Catalogger.Backend/CataloggerMetrics.cs @@ -44,6 +44,11 @@ public static class CataloggerMetrics "Number of users in the cache" ); + public static readonly Gauge EmojiCached = Metrics.CreateGauge( + "catalogger_cache_emoji", + "Number of custom emoji in the cache" + ); + public static readonly Gauge MessagesStored = Metrics.CreateGauge( "catalogger_stored_messages", "Number of users in the cache" diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs index 19b81de..1ec5f2a 100644 --- a/Catalogger.Backend/Extensions/StartupExtensions.cs +++ b/Catalogger.Backend/Extensions/StartupExtensions.cs @@ -105,6 +105,7 @@ public static class StartupExtensions .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddScoped() .AddSingleton() diff --git a/Catalogger.Backend/Services/MetricsCollectionService.cs b/Catalogger.Backend/Services/MetricsCollectionService.cs index 6333666..40c9c0c 100644 --- a/Catalogger.Backend/Services/MetricsCollectionService.cs +++ b/Catalogger.Backend/Services/MetricsCollectionService.cs @@ -27,6 +27,7 @@ public class MetricsCollectionService( GuildCache guildCache, ChannelCache channelCache, UserCache userCache, + EmojiCache emojiCache, IServiceProvider services ) { @@ -44,6 +45,7 @@ public class MetricsCollectionService( CataloggerMetrics.GuildsCached.Set(guildCache.Size); CataloggerMetrics.ChannelsCached.Set(channelCache.Size); CataloggerMetrics.UsersCached.Set(userCache.Size); + CataloggerMetrics.EmojiCached.Set(emojiCache.Size); CataloggerMetrics.MessagesStored.Set(messageCount); CataloggerMetrics.MessageRateMinute = messageCount - CataloggerMetrics.MessageRateMinute;