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;