From db01f879bd021a3a371f4de88a4bb81a034248c1 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 2 Sep 2024 15:59:16 +0200 Subject: [PATCH] fix: remove unnecessary async methods, fix PluralkitApiService --- .../Channels/ChannelCreateResponder.cs | 2 +- .../Channels/ChannelDeleteResponder.cs | 2 +- .../Channels/ChannelUpdateResponder.cs | 3 +- .../Responders/Guilds/GuildCreateResponder.cs | 12 +- .../Guilds/GuildMemberAddResponder.cs | 6 +- .../Messages/MessageDeleteResponder.cs | 4 +- .../Messages/MessageUpdateResponder.cs | 2 +- Catalogger.Backend/Catalogger.Backend.csproj | 1 + .../Services/PluralkitApiService.cs | 12 +- .../Services/WebhookExecutorService.cs | 125 ++++++++++-------- 10 files changed, 96 insertions(+), 73 deletions(-) diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs index 2c3231f..92f8527 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs @@ -69,7 +69,7 @@ public class ChannelCreateResponder( } var guildConfig = await db.GetGuildAsync(ch.GuildID.Value, ct); - await webhookExecutor.QueueLogAsync(guildConfig, LogChannelType.ChannelCreate, builder.Build().GetOrThrow()); + webhookExecutor.QueueLog(guildConfig, LogChannelType.ChannelCreate, builder.Build().GetOrThrow()); return Result.Success; } } \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs index ecced61..0414a82 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs @@ -49,7 +49,7 @@ public class ChannelDeleteResponder( if (channel.Topic.IsDefined(out var topic)) embed.AddField("Description", topic); - await webhookExecutor.QueueLogAsync(guildConfig, LogChannelType.ChannelDelete, embed.Build().GetOrThrow()); + webhookExecutor.QueueLog(guildConfig, LogChannelType.ChannelDelete, embed.Build().GetOrThrow()); return Result.Success; } } \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs index ddb291e..b2aa977 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs @@ -141,8 +141,7 @@ public class ChannelUpdateResponder( // 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()); + webhookExecutor.QueueLog(guildConfig, LogChannelType.ChannelUpdate, builder.Build().GetOrThrow()); return Result.Success; } diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs index a0dd422..2139c0f 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs @@ -61,7 +61,7 @@ public class GuildCreateResponder( _logger.Information("Joined new guild {GuildName} / {GuildId}", guildName, guildId); if (config.Discord.GuildLogId != null && evt.Guild.IsT0) - await webhookExecutor.QueueLogAsync(config.Discord.GuildLogId.Value, new EmbedBuilder() + webhookExecutor.QueueLog(config.Discord.GuildLogId.Value, new EmbedBuilder() .WithTitle("Joined new guild") .WithDescription($"Joined new guild **{guild.Name}**") .WithFooter($"ID: {guild.ID}") @@ -75,24 +75,24 @@ public class GuildCreateResponder( return Result.Success; } - public async Task RespondAsync(IGuildDelete evt, CancellationToken ct = default) + public Task RespondAsync(IGuildDelete evt, CancellationToken ct = default) { if (evt.IsUnavailable.OrDefault(false)) { _logger.Debug("Guild {GuildId} became unavailable", evt.ID); - return Result.Success; + return Task.FromResult(Result.Success); } if (!guildCache.TryGet(evt.ID, out var guild)) { _logger.Information("Left uncached guild {GuildId}", evt.ID); - return Result.Success; + return Task.FromResult(Result.Success); } _logger.Information("Left guild {GuildName} / {GuildId}", guild.Name, guild.ID); if (config.Discord.GuildLogId != null) - await webhookExecutor.QueueLogAsync(config.Discord.GuildLogId.Value, new EmbedBuilder() + webhookExecutor.QueueLog(config.Discord.GuildLogId.Value, new EmbedBuilder() .WithTitle("Left guild") .WithDescription($"Left guild **{guild.Name}**") .WithFooter($"ID: {guild.ID}") @@ -103,6 +103,6 @@ public class GuildCreateResponder( .Build() .GetOrThrow()); - return Result.Success; + return Task.FromResult(Result.Success); } } \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs index 604e1b7..9e9be32 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs @@ -7,6 +7,7 @@ using Catalogger.Backend.Services; using Humanizer; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; @@ -110,8 +111,9 @@ public class GuildMemberAddResponder( } if (embeds.Count > 1) - await webhookExecutor.SendLogWithAttachmentsAsync(guildConfig.Channels.GuildMemberAdd, embeds, []); - else await webhookExecutor.QueueLogAsync(guildConfig.Channels.GuildMemberAdd, embeds[0]); + await webhookExecutor.SendLogAsync(guildConfig.Channels.GuildMemberAdd, + embeds.Cast().ToList(), []); + else webhookExecutor.QueueLog(guildConfig.Channels.GuildMemberAdd, embeds[0]); return Result.Success; } diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs index 36b4bae..fb0d34a 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs @@ -53,7 +53,7 @@ public class MessageDeleteResponder( if (msg == null) { if (logChannel == null) return Result.Success; - await webhookExecutor.QueueLogAsync(logChannel.Value, new Embed( + webhookExecutor.QueueLog(logChannel.Value, new Embed( Title: "Message deleted", Description: $"A message not found in the database was deleted in <#{ev.ChannelID}> ({ev.ChannelID}).", Footer: new EmbedFooter(Text: $"ID: {ev.ID}"), @@ -121,7 +121,7 @@ public class MessageDeleteResponder( if (!string.IsNullOrWhiteSpace(attachmentInfo)) builder.AddField("Attachments", attachmentInfo, false); } - await webhookExecutor.QueueLogAsync(logChannel.Value, builder.Build().GetOrThrow()); + webhookExecutor.QueueLog(logChannel.Value, builder.Build().GetOrThrow()); return Result.Success; } } \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs index ebafe82..1510e25 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs @@ -105,7 +105,7 @@ public class MessageUpdateResponder( embedBuilder.AddField("Link", $"https://discord.com/channels/{msg.GuildID}/{msg.ChannelID}/{msg.ID}"); - await webhookExecutor.QueueLogAsync(logChannel.Value, embedBuilder.Build().GetOrThrow()); + webhookExecutor.QueueLog(logChannel.Value, embedBuilder.Build().GetOrThrow()); return Result.Success; } finally diff --git a/Catalogger.Backend/Catalogger.Backend.csproj b/Catalogger.Backend/Catalogger.Backend.csproj index 17b47f2..cfde964 100644 --- a/Catalogger.Backend/Catalogger.Backend.csproj +++ b/Catalogger.Backend/Catalogger.Backend.csproj @@ -19,6 +19,7 @@ + diff --git a/Catalogger.Backend/Services/PluralkitApiService.cs b/Catalogger.Backend/Services/PluralkitApiService.cs index 4569856..8b92fda 100644 --- a/Catalogger.Backend/Services/PluralkitApiService.cs +++ b/Catalogger.Backend/Services/PluralkitApiService.cs @@ -3,6 +3,8 @@ using System.Text.Json; using System.Threading.RateLimiting; using Humanizer; using NodaTime; +using NodaTime.Serialization.SystemTextJson; +using NodaTime.Text; using Polly; namespace Catalogger.Backend.Services; @@ -46,8 +48,14 @@ public class PluralkitApiService(ILogger logger) throw new CataloggerError("Non-200 status code from PluralKit API"); } - return await resp.Content.ReadFromJsonAsync(new JsonSerializerOptions - { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }, ct) ?? + var jsonOptions = new JsonSerializerOptions + { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower } + .ConfigureForNodaTime(new NodaJsonSettings + { + InstantConverter = new NodaPatternConverter(InstantPattern.ExtendedIso) + }); + + return await resp.Content.ReadFromJsonAsync(jsonOptions, ct) ?? throw new CataloggerError("JSON response from PluralKit API was null"); } diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index 0929c61..556b7ab 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -11,7 +11,6 @@ using Guild = Catalogger.Backend.Database.Models.Guild; namespace Catalogger.Backend.Services; -// TODO: this entire class is a mess, clean it up public class WebhookExecutorService( Config config, ILogger logger, @@ -28,14 +27,79 @@ public class WebhookExecutorService( public void SetSelfUser(IUser user) => _selfUser = user; - public async Task QueueLogAsync(Guild guild, LogChannelType logChannelType, IEmbed embed) + /// + /// Queues a log embed for the given log channel type. + /// If the log channel is already known, use the ulong overload of this method instead. + /// If the log channel depends on the source channel and source user, also use the ulong overload. + /// + public void QueueLog(Guild guildConfig, LogChannelType logChannelType, IEmbed embed) { - var logChannel = GetLogChannel(guild, logChannelType, channelId: null, userId: null); + var logChannel = GetLogChannel(guildConfig, logChannelType, channelId: null, userId: null); if (logChannel == null) return; - await QueueLogAsync(logChannel.Value, embed); + QueueLog(logChannel.Value, embed); } + /// + /// Queues a log embed for the given channel ID. + /// + public void QueueLog(ulong channelId, IEmbed embed) + { + if (channelId == 0) return; + + var queue = _cache.GetOrAdd(channelId, []); + queue.Enqueue(embed); + _cache[channelId] = queue; + + SetTimer(channelId, queue); + } + + /// + /// Sends multiple embeds and/or files to a channel, bypassing the embed queue. + /// + /// The channel ID to send the content to. + /// The embeds to send. Must be under 6000 characters in length total, this is not checked by this method. + /// The files to send. + public async Task SendLogAsync(ulong channelId, List embeds, IEnumerable files) + { + if (channelId == 0) return; + + var attachments = files + .Select>(f => f) + .ToList(); + + _logger.Debug("Sending {EmbedCount} embeds/{FileCount} files to channel {ChannelId}", embeds.Count, + attachments.Count, channelId); + + var webhook = await webhookCache.GetOrFetchWebhookAsync(channelId, id => FetchWebhookAsync(id)); + await webhookApi.ExecuteWebhookAsync(DiscordSnowflake.New(webhook.Id), webhook.Token, shouldWait: false, + embeds: embeds, attachments: attachments, username: _selfUser!.Username, + avatarUrl: _selfUser.AvatarUrl()); + } + + /// + /// Sets a 3 second timer for the given channel. + /// + private void SetTimer(ulong channelId, ConcurrentQueue queue) + { + if (_timers.TryGetValue(channelId, out var existingTimer)) existingTimer.Dispose(); + _timers[channelId] = new Timer(_ => + { + _logger.Debug("Sending 5 queued embeds"); + + var __ = SendLogAsync(channelId, TakeFromQueue(channelId).ToList(), []); + if (!queue.IsEmpty) + { + if (_timers.TryGetValue(channelId, out var timer)) timer.Dispose(); + SetTimer(channelId, queue); + } + }, null, 3000, Timeout.Infinite); + } + + /// + /// Takes 5 embeds from the queue for the given channel. + /// Note that this locks the queue to prevent duplicate embeds from being sent. + /// private List TakeFromQueue(ulong channelId) { var queue = _cache.GetOrAdd(channelId, []); @@ -53,58 +117,7 @@ public class WebhookExecutorService( } } - public async Task QueueLogAsync(ulong channelId, IEmbed embed) - { - if (channelId == 0) return; - - var queue = _cache.GetOrAdd(channelId, []); - queue.Enqueue(embed); - _cache[channelId] = queue; - - await SetTimer(channelId, queue); - } - - private async Task SetTimer(ulong channelId, ConcurrentQueue queue) - { - if (_timers.TryGetValue(channelId, out var existingTimer)) await existingTimer.DisposeAsync(); - _timers[channelId] = new Timer(_ => - { - _logger.Debug("Sending 5 queued embeds"); - - var __ = SendLogsAsync(channelId, TakeFromQueue(channelId)); - if (!queue.IsEmpty) - { - if (_timers.TryGetValue(channelId, out var timer)) timer.Dispose(); - var ___ = SetTimer(channelId, queue); - } - }, null, 3000, Timeout.Infinite); - } - - private async Task SendLogsAsync(ulong channelId, List embeds) - { - _logger.Debug("Sending {Count} embeds to channel {ChannelId}", embeds.Count, channelId); - if (embeds.Count == 0) return; - - var webhook = await webhookCache.GetOrFetchWebhookAsync(channelId, id => FetchWebhookAsync(id)); - - await webhookApi.ExecuteWebhookAsync(DiscordSnowflake.New(webhook.Id), webhook.Token, shouldWait: false, - embeds: embeds, username: _selfUser!.Username, avatarUrl: _selfUser.AvatarUrl()); - } - - public async Task SendLogWithAttachmentsAsync(ulong channelId, List embeds, IEnumerable files) - { - if (channelId == 0) return; - - var attachments = files - .Select>(f => f) - .ToList(); - - var webhook = await webhookCache.GetOrFetchWebhookAsync(channelId, id => FetchWebhookAsync(id)); - await webhookApi.ExecuteWebhookAsync(DiscordSnowflake.New(webhook.Id), webhook.Token, shouldWait: false, - embeds: embeds, attachments: attachments, username: _selfUser!.Username, - avatarUrl: _selfUser.AvatarUrl()); - } - + // TODO: make it so this method can only have one request per channel in flight simultaneously private async Task FetchWebhookAsync(Snowflake channelId, CancellationToken ct = default) { var channelWebhooks =