From 4f54077c6838b1792c9287f5e11295ceba218574 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 9 Oct 2024 17:35:11 +0200 Subject: [PATCH] chore: format with csharpier --- .../Bot/Commands/ChannelCommands.cs | 512 +++++++++++++----- .../Bot/Commands/ChannelCommandsComponents.cs | 194 ++++--- .../Bot/Commands/KeyRoleCommands.cs | 60 +- .../Bot/Commands/MetaCommands.cs | 63 ++- Catalogger.Backend/Bot/DiscordUtils.cs | 2 +- .../Channels/ChannelCreateResponder.cs | 51 +- .../Channels/ChannelDeleteResponder.cs | 16 +- .../Channels/ChannelUpdateResponder.cs | 130 +++-- .../Responders/Guilds/AuditLogResponder.cs | 9 +- .../Responders/Guilds/GuildCreateResponder.cs | 69 ++- .../Guilds/GuildMemberAddResponder.cs | 110 ++-- .../Guilds/GuildMemberRemoveResponder.cs | 24 +- .../Guilds/GuildMembersChunkResponder.cs | 18 +- .../Messages/MessageCreateResponder.cs | 66 ++- .../Messages/MessageDeleteResponder.cs | 96 +++- .../Messages/MessageUpdateResponder.cs | 150 +++-- .../Bot/Responders/ReadyResponder.cs | 17 +- .../Responders/Roles/RoleCreateResponder.cs | 17 +- .../Responders/Roles/RoleUpdateResponder.cs | 52 +- .../Bot/ShardedDiscordService.cs | 5 +- .../Bot/ShardedGatewayClient.cs | 61 ++- Catalogger.Backend/Cache/IInviteCache.cs | 2 +- Catalogger.Backend/Cache/IMemberCache.cs | 2 +- Catalogger.Backend/Cache/IWebhookCache.cs | 16 +- .../Cache/InMemoryCache/AuditLogCache.cs | 28 +- .../Cache/InMemoryCache/ChannelCache.cs | 34 +- .../Cache/InMemoryCache/GuildCache.cs | 10 +- .../InMemoryCache/InMemoryInviteCache.cs | 9 +- .../InMemoryCache/InMemoryMemberCache.cs | 9 +- .../InMemoryCache/InMemoryWebhookCache.cs | 2 +- .../Cache/InMemoryCache/RoleCache.cs | 31 +- .../Cache/InMemoryCache/UserCache.cs | 19 +- .../Cache/RedisCache/RedisInviteCache.cs | 33 +- .../Cache/RedisCache/RedisMemberCache.cs | 101 +++- .../Cache/RedisCache/RedisWebhookCache.cs | 2 +- Catalogger.Backend/CataloggerError.cs | 4 +- Catalogger.Backend/CataloggerMetrics.cs | 44 +- Catalogger.Backend/Config.cs | 2 +- .../Database/DatabaseContext.cs | 53 +- .../Database/EncryptionService.cs | 2 +- .../Database/IEncryptionService.cs | 2 +- .../Migrations/20240803132306_Init.cs | 52 +- Catalogger.Backend/Database/Models/Guild.cs | 22 +- Catalogger.Backend/Database/Models/Invite.cs | 2 +- Catalogger.Backend/Database/Models/Message.cs | 15 +- .../Database/Models/Watchlist.cs | 2 +- .../Database/Queries/MessageRepository.cs | 95 +++- .../Database/Queries/QueryExtensions.cs | 35 +- .../Database/Redis/RedisService.cs | 24 +- .../Extensions/DiscordExtensions.cs | 75 ++- .../Extensions/StartupExtensions.cs | 103 ++-- .../Extensions/TimeExtensions.cs | 8 +- Catalogger.Backend/GlobalUsing.cs | 2 +- Catalogger.Backend/Program.cs | 44 +- .../AuditLogEnrichedResponderService.cs | 19 +- .../Services/GuildFetchService.cs | 11 +- .../Services/MetricsCollectionService.cs | 10 +- .../Services/PluralkitApiService.cs | 63 ++- .../Services/WebhookExecutorService.cs | 233 +++++--- 59 files changed, 2000 insertions(+), 942 deletions(-) diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs index 6889fd6..e4b2312 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs @@ -30,7 +30,8 @@ public class ChannelCommands( ChannelCache channelCache, IFeedbackService feedbackService, ContextInjectionService contextInjection, - InMemoryDataService dataService) : CommandGroup + InMemoryDataService dataService +) : CommandGroup { private readonly ILogger _logger = logger.ForContext(); @@ -40,22 +41,30 @@ public class ChannelCommands( public async Task ConfigureChannelsAsync() { var (userId, guildId) = contextInjection.GetUserAndGuild(); - if (!guildCache.TryGet(guildId, out var guild)) throw new CataloggerError("Guild not in cache"); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild not in cache"); var guildChannels = channelCache.GuildChannels(guildId).ToList(); var guildConfig = await db.GetGuildAsync(guildId); var (embeds, components) = BuildRootMenu(guildChannels, guild, guildConfig); - var msg = await feedbackService.SendContextualAsync(embeds: embeds, - options: new FeedbackMessageOptions(MessageComponents: components)).GetOrThrow(); + var msg = await feedbackService + .SendContextualAsync( + embeds: embeds, + options: new FeedbackMessageOptions(MessageComponents: components) + ) + .GetOrThrow(); dataService.TryAddData(msg.ID, new ChannelCommandData(userId, CurrentPage: null)); return Result.Success; } - public static (List, List) BuildRootMenu(List guildChannels, IGuild guild, - DbGuild guildConfig) + public static (List, List) BuildRootMenu( + List guildChannels, + IGuild guild, + DbGuild guildConfig + ) { List embeds = [ @@ -65,158 +74,369 @@ public class ChannelCommands( Colour: DiscordUtils.Purple, Fields: new[] { - new EmbedField("Server changes", PrettyChannelString(guildConfig.Channels.GuildUpdate), true), - new EmbedField("Emoji changes", PrettyChannelString(guildConfig.Channels.GuildEmojisUpdate), true), - new EmbedField("New roles", PrettyChannelString(guildConfig.Channels.GuildRoleCreate), true), - new EmbedField("Edited roles", PrettyChannelString(guildConfig.Channels.GuildRoleUpdate), true), - new EmbedField("Deleted roles", PrettyChannelString(guildConfig.Channels.GuildRoleDelete), true), - - new EmbedField("New channels", PrettyChannelString(guildConfig.Channels.ChannelCreate), true), - new EmbedField("Edited channels", PrettyChannelString(guildConfig.Channels.ChannelUpdate), true), - new EmbedField("Deleted channels", PrettyChannelString(guildConfig.Channels.ChannelDelete), true), - new EmbedField("Members joining", PrettyChannelString(guildConfig.Channels.GuildMemberAdd), true), - new EmbedField("Members leaving", PrettyChannelString(guildConfig.Channels.GuildMemberRemove), - true), - - new EmbedField("Member role changes", PrettyChannelString(guildConfig.Channels.GuildMemberUpdate), - true), - new EmbedField("Key role changes", PrettyChannelString(guildConfig.Channels.GuildKeyRoleUpdate), - true), - new EmbedField("Member name changes", + new EmbedField( + "Server changes", + PrettyChannelString(guildConfig.Channels.GuildUpdate), + true + ), + new EmbedField( + "Emoji changes", + PrettyChannelString(guildConfig.Channels.GuildEmojisUpdate), + true + ), + new EmbedField( + "New roles", + PrettyChannelString(guildConfig.Channels.GuildRoleCreate), + true + ), + new EmbedField( + "Edited roles", + PrettyChannelString(guildConfig.Channels.GuildRoleUpdate), + true + ), + new EmbedField( + "Deleted roles", + PrettyChannelString(guildConfig.Channels.GuildRoleDelete), + true + ), + new EmbedField( + "New channels", + PrettyChannelString(guildConfig.Channels.ChannelCreate), + true + ), + new EmbedField( + "Edited channels", + PrettyChannelString(guildConfig.Channels.ChannelUpdate), + true + ), + new EmbedField( + "Deleted channels", + PrettyChannelString(guildConfig.Channels.ChannelDelete), + true + ), + new EmbedField( + "Members joining", + PrettyChannelString(guildConfig.Channels.GuildMemberAdd), + true + ), + new EmbedField( + "Members leaving", + PrettyChannelString(guildConfig.Channels.GuildMemberRemove), + true + ), + new EmbedField( + "Member role changes", + PrettyChannelString(guildConfig.Channels.GuildMemberUpdate), + true + ), + new EmbedField( + "Key role changes", + PrettyChannelString(guildConfig.Channels.GuildKeyRoleUpdate), + true + ), + new EmbedField( + "Member name changes", PrettyChannelString(guildConfig.Channels.GuildMemberNickUpdate), - true), - new EmbedField("Member avatar changes", - PrettyChannelString(guildConfig.Channels.GuildMemberAvatarUpdate), true), - new EmbedField("Kicks", PrettyChannelString(guildConfig.Channels.GuildMemberKick), true), - - new EmbedField("Bans", PrettyChannelString(guildConfig.Channels.GuildBanAdd), true), - new EmbedField("Unbans", PrettyChannelString(guildConfig.Channels.GuildBanRemove), true), - new EmbedField("New invites", PrettyChannelString(guildConfig.Channels.InviteCreate), true), - new EmbedField("Deleted invites", PrettyChannelString(guildConfig.Channels.InviteDelete), true), - new EmbedField("Edited messages", PrettyChannelString(guildConfig.Channels.MessageUpdate), true), - - new EmbedField("Deleted messages", PrettyChannelString(guildConfig.Channels.MessageDelete), true), - new EmbedField("Bulk deleted messages", PrettyChannelString(guildConfig.Channels.MessageDeleteBulk), - true), - }) + true + ), + new EmbedField( + "Member avatar changes", + PrettyChannelString(guildConfig.Channels.GuildMemberAvatarUpdate), + true + ), + new EmbedField( + "Kicks", + PrettyChannelString(guildConfig.Channels.GuildMemberKick), + true + ), + new EmbedField( + "Bans", + PrettyChannelString(guildConfig.Channels.GuildBanAdd), + true + ), + new EmbedField( + "Unbans", + PrettyChannelString(guildConfig.Channels.GuildBanRemove), + true + ), + new EmbedField( + "New invites", + PrettyChannelString(guildConfig.Channels.InviteCreate), + true + ), + new EmbedField( + "Deleted invites", + PrettyChannelString(guildConfig.Channels.InviteDelete), + true + ), + new EmbedField( + "Edited messages", + PrettyChannelString(guildConfig.Channels.MessageUpdate), + true + ), + new EmbedField( + "Deleted messages", + PrettyChannelString(guildConfig.Channels.MessageDelete), + true + ), + new EmbedField( + "Bulk deleted messages", + PrettyChannelString(guildConfig.Channels.MessageDeleteBulk), + true + ), + } + ), ]; List components = [ - new ActionRowComponent([ - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Server changes", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildUpdate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Emoji changes", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildEmojisUpdate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "New roles", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildRoleCreate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Edited roles", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildRoleUpdate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Deleted roles", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildRoleDelete))), - ]), - new ActionRowComponent([ - new ButtonComponent(ButtonComponentStyle.Primary, Label: "New channels", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.ChannelCreate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Edited channels", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.ChannelUpdate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Deleted channels", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.ChannelDelete))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Members joining", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildMemberAdd))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Members leaving", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildMemberRemove))), - ]), - new ActionRowComponent([ - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Member role changes", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildMemberUpdate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Key role changes", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildKeyRoleUpdate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Member name changes", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildMemberNickUpdate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Members avatar changes", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildMemberAvatarUpdate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Kicks", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildMemberKick))), - ]), - new ActionRowComponent([ - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Bans", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildBanAdd))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Unbans", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.GuildBanRemove))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "New invites", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.InviteCreate))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Deleted invites", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.InviteDelete))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Edited messages", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.MessageUpdate))), - ]), - new ActionRowComponent([ - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Deleted messages", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.MessageDelete))), - new ButtonComponent(ButtonComponentStyle.Primary, Label: "Bulk deleted messages", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", - nameof(LogChannelType.MessageDeleteBulk))), - new ButtonComponent(ButtonComponentStyle.Secondary, Label: "Close", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "close")), - ]), + new ActionRowComponent( + [ + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Server changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildUpdate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Emoji changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildEmojisUpdate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "New roles", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildRoleCreate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Edited roles", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildRoleUpdate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Deleted roles", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildRoleDelete) + ) + ), + ] + ), + new ActionRowComponent( + [ + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "New channels", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.ChannelCreate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Edited channels", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.ChannelUpdate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Deleted channels", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.ChannelDelete) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Members joining", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildMemberAdd) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Members leaving", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildMemberRemove) + ) + ), + ] + ), + new ActionRowComponent( + [ + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Member role changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildMemberUpdate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Key role changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildKeyRoleUpdate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Member name changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildMemberNickUpdate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Members avatar changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildMemberAvatarUpdate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Kicks", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildMemberKick) + ) + ), + ] + ), + new ActionRowComponent( + [ + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Bans", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildBanAdd) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Unbans", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.GuildBanRemove) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "New invites", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.InviteCreate) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Deleted invites", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.InviteDelete) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Edited messages", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.MessageUpdate) + ) + ), + ] + ), + new ActionRowComponent( + [ + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Deleted messages", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.MessageDelete) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Primary, + Label: "Bulk deleted messages", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + nameof(LogChannelType.MessageDeleteBulk) + ) + ), + new ButtonComponent( + ButtonComponentStyle.Secondary, + Label: "Close", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + "close" + ) + ), + ] + ), ]; return (embeds, components); string PrettyChannelString(ulong id) { - if (id == 0) return "Not set"; - if (guildChannels.All(c => c.ID != id)) return $"unknown channel {id}"; + if (id == 0) + return "Not set"; + if (guildChannels.All(c => c.ID != id)) + return $"unknown channel {id}"; return $"<#{id}>"; } } - public static string PrettyLogTypeName(LogChannelType type) => type switch - { - LogChannelType.GuildUpdate => "Server changes", - LogChannelType.GuildEmojisUpdate => "Emoji changes", - LogChannelType.GuildRoleCreate => "New roles", - LogChannelType.GuildRoleUpdate => "Edited roles", - LogChannelType.GuildRoleDelete => "Deleted roles", - LogChannelType.ChannelCreate => "New channels", - LogChannelType.ChannelUpdate => "Edited channels", - LogChannelType.ChannelDelete => "Deleted channels", - LogChannelType.GuildMemberAdd => "Members joining", - LogChannelType.GuildMemberUpdate => "Members leaving", - LogChannelType.GuildKeyRoleUpdate => "Key role changes", - LogChannelType.GuildMemberNickUpdate => "Member name changes", - LogChannelType.GuildMemberAvatarUpdate => "Member avatar changes", - LogChannelType.GuildMemberRemove => "Members leaving", - LogChannelType.GuildMemberKick => "Kicks", - LogChannelType.GuildBanAdd => "Bans", - LogChannelType.GuildBanRemove => "Unbans", - LogChannelType.InviteCreate => "New invites", - LogChannelType.InviteDelete => "Deleted invites", - LogChannelType.MessageUpdate => "Edited messages", - LogChannelType.MessageDelete => "Deleted messages", - LogChannelType.MessageDeleteBulk => "Bulk deleted messages", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Invalid LogChannelType value") - }; -} \ No newline at end of file + public static string PrettyLogTypeName(LogChannelType type) => + type switch + { + LogChannelType.GuildUpdate => "Server changes", + LogChannelType.GuildEmojisUpdate => "Emoji changes", + LogChannelType.GuildRoleCreate => "New roles", + LogChannelType.GuildRoleUpdate => "Edited roles", + LogChannelType.GuildRoleDelete => "Deleted roles", + LogChannelType.ChannelCreate => "New channels", + LogChannelType.ChannelUpdate => "Edited channels", + LogChannelType.ChannelDelete => "Deleted channels", + LogChannelType.GuildMemberAdd => "Members joining", + LogChannelType.GuildMemberUpdate => "Members leaving", + LogChannelType.GuildKeyRoleUpdate => "Key role changes", + LogChannelType.GuildMemberNickUpdate => "Member name changes", + LogChannelType.GuildMemberAvatarUpdate => "Member avatar changes", + LogChannelType.GuildMemberRemove => "Members leaving", + LogChannelType.GuildMemberKick => "Kicks", + LogChannelType.GuildBanAdd => "Bans", + LogChannelType.GuildBanRemove => "Unbans", + LogChannelType.InviteCreate => "New invites", + LogChannelType.InviteDelete => "Deleted invites", + LogChannelType.MessageUpdate => "Edited messages", + LogChannelType.MessageDelete => "Deleted messages", + LogChannelType.MessageDeleteBulk => "Bulk deleted messages", + _ => throw new ArgumentOutOfRangeException( + nameof(type), + type, + "Invalid LogChannelType value" + ), + }; +} diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs index dc342f7..40b6574 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs @@ -27,7 +27,8 @@ public class ChannelCommandsComponents( ContextInjectionService contextInjection, IFeedbackService feedbackService, IDiscordRestInteractionAPI interactionApi, - InMemoryDataService dataService) : InteractionGroup + InMemoryDataService dataService +) : InteractionGroup { private readonly ILogger _logger = logger.ForContext(); @@ -35,11 +36,16 @@ public class ChannelCommandsComponents( [SuppressInteractionResponse(true)] public async Task OnButtonPressedAsync(string state) { - if (contextInjection.Context is not IInteractionCommandContext ctx) throw new CataloggerError("No context"); - if (!ctx.TryGetUserID(out var userId)) throw new CataloggerError("No user ID in context"); - if (!ctx.Interaction.Message.TryGet(out var msg)) throw new CataloggerError("No message ID in context"); - if (!ctx.TryGetGuildID(out var guildId)) throw new CataloggerError("No guild ID in context"); - if (!guildCache.TryGet(guildId, out var guild)) throw new CataloggerError("Guild not in cache"); + if (contextInjection.Context is not IInteractionCommandContext ctx) + throw new CataloggerError("No context"); + if (!ctx.TryGetUserID(out var userId)) + throw new CataloggerError("No user ID in context"); + if (!ctx.Interaction.Message.TryGet(out var msg)) + throw new CataloggerError("No message ID in context"); + if (!ctx.TryGetGuildID(out var guildId)) + throw new CataloggerError("No guild ID in context"); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild not in cache"); var guildChannels = channelCache.GuildChannels(guildId).ToList(); var guildConfig = await db.GetGuildAsync(guildId); @@ -47,20 +53,27 @@ public class ChannelCommandsComponents( await using var lease = result.GetOrThrow(); if (lease.Data.UserId != userId) { - return (Result)await feedbackService.SendContextualAsync("This is not your configuration menu.", - options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + return (Result) + await feedbackService.SendContextualAsync( + "This is not your configuration menu.", + options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral) + ); } switch (state) { case "close": - return await interactionApi.UpdateMessageAsync(ctx.Interaction, - new InteractionMessageCallbackData(Components: Array.Empty())); + return await interactionApi.UpdateMessageAsync( + ctx.Interaction, + new InteractionMessageCallbackData(Components: Array.Empty()) + ); case "reset": if (lease.Data.CurrentPage == null) throw new CataloggerError("CurrentPage was null in reset button callback"); if (!Enum.TryParse(lease.Data.CurrentPage, out var channelType)) - throw new CataloggerError($"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'"); + throw new CataloggerError( + $"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'" + ); // TODO: figure out some way to make this less verbose? switch (channelType) @@ -140,8 +153,10 @@ public class ChannelCommandsComponents( goto case "return"; case "return": var (e, c) = ChannelCommands.BuildRootMenu(guildChannels, guild, guildConfig); - await interactionApi.UpdateMessageAsync(ctx.Interaction, - new InteractionMessageCallbackData(Embeds: e, Components: c)); + await interactionApi.UpdateMessageAsync( + ctx.Interaction, + new InteractionMessageCallbackData(Embeds: e, Components: c) + ); lease.Data = new ChannelCommandData(userId, CurrentPage: null); return Result.Success; } @@ -151,9 +166,12 @@ public class ChannelCommandsComponents( var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType); string? channelMention; - if (channelId is 0) channelMention = null; - else if (guildChannels.All(c => c.ID != channelId)) channelMention = $"unknown channel {channelId}"; - else channelMention = $"<#{channelId}>"; + if (channelId is 0) + channelMention = null; + else if (guildChannels.All(c => c.ID != channelId)) + channelMention = $"unknown channel {channelId}"; + else + channelMention = $"<#{channelId}>"; List embeds = [ @@ -161,43 +179,69 @@ public class ChannelCommandsComponents( Title: ChannelCommands.PrettyLogTypeName(logChannelType), Description: channelMention == null ? "This event is not currently logged.\nTo start logging it somewhere, select a channel below." - : $"This event is currently set to log to {channelMention}." + - "\nTo change where it is logged, select a channel below." + - "\nTo disable logging this event entirely, select \"Stop logging\" below.", - Colour: DiscordUtils.Purple) + : $"This event is currently set to log to {channelMention}." + + "\nTo change where it is logged, select a channel below." + + "\nTo disable logging this event entirely, select \"Stop logging\" below.", + Colour: DiscordUtils.Purple + ), ]; List components = [ - new ActionRowComponent(new[] - { - new ChannelSelectComponent(CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"), - ChannelTypes: new[] { ChannelType.GuildText }) - }), - new ActionRowComponent(new[] - { - new ButtonComponent(ButtonComponentStyle.Danger, Label: "Stop logging", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "reset"), - IsDisabled: channelMention == null), - new ButtonComponent(ButtonComponentStyle.Secondary, Label: "Return to menu", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "return")) - }) + new ActionRowComponent( + new[] + { + new ChannelSelectComponent( + CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"), + ChannelTypes: new[] { ChannelType.GuildText } + ), + } + ), + new ActionRowComponent( + new[] + { + new ButtonComponent( + ButtonComponentStyle.Danger, + Label: "Stop logging", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + "reset" + ), + IsDisabled: channelMention == null + ), + new ButtonComponent( + ButtonComponentStyle.Secondary, + Label: "Return to menu", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + "return" + ) + ), + } + ), ]; lease.Data = new ChannelCommandData(userId, CurrentPage: state); - return await interactionApi.UpdateMessageAsync(ctx.Interaction, - new InteractionMessageCallbackData(Embeds: embeds, Components: components)); + return await interactionApi.UpdateMessageAsync( + ctx.Interaction, + new InteractionMessageCallbackData(Embeds: embeds, Components: components) + ); } [SelectMenu("config-channels")] [SuppressInteractionResponse(true)] public async Task OnMenuSelectionAsync(IReadOnlyList channels) { - if (contextInjection.Context is not IInteractionCommandContext ctx) throw new CataloggerError("No context"); - if (!ctx.TryGetUserID(out var userId)) throw new CataloggerError("No user ID in context"); - if (!ctx.Interaction.Message.TryGet(out var msg)) throw new CataloggerError("No message ID in context"); - if (!ctx.TryGetGuildID(out var guildId)) throw new CataloggerError("No guild ID in context"); - if (!guildCache.TryGet(guildId, out var guild)) throw new CataloggerError("Guild not in cache"); + if (contextInjection.Context is not IInteractionCommandContext ctx) + throw new CataloggerError("No context"); + if (!ctx.TryGetUserID(out var userId)) + throw new CataloggerError("No user ID in context"); + if (!ctx.Interaction.Message.TryGet(out var msg)) + throw new CataloggerError("No message ID in context"); + if (!ctx.TryGetGuildID(out var guildId)) + throw new CataloggerError("No guild ID in context"); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild not in cache"); var guildConfig = await db.GetGuildAsync(guildId); var channelId = channels[0].ID.ToUlong(); @@ -205,12 +249,17 @@ public class ChannelCommandsComponents( await using var lease = result.GetOrThrow(); if (lease.Data.UserId != userId) { - return (Result)await feedbackService.SendContextualAsync("This is not your configuration menu.", - options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + return (Result) + await feedbackService.SendContextualAsync( + "This is not your configuration menu.", + options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral) + ); } if (!Enum.TryParse(lease.Data.CurrentPage, out var channelType)) - throw new CataloggerError($"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'"); + throw new CataloggerError( + $"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'" + ); switch (channelType) { @@ -291,32 +340,53 @@ public class ChannelCommandsComponents( [ new Embed( Title: ChannelCommands.PrettyLogTypeName(channelType), - Description: $"This event is currently set to log to <#{channelId}>." + - "\nTo change where it is logged, select a channel below." + - "\nTo disable logging this event entirely, select \"Stop logging\" below.", - Colour: DiscordUtils.Purple) + Description: $"This event is currently set to log to <#{channelId}>." + + "\nTo change where it is logged, select a channel below." + + "\nTo disable logging this event entirely, select \"Stop logging\" below.", + Colour: DiscordUtils.Purple + ), ]; List components = [ - new ActionRowComponent(new[] - { - new ChannelSelectComponent(CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"), - ChannelTypes: new[] { ChannelType.GuildText }) - }), - new ActionRowComponent(new[] - { - new ButtonComponent(ButtonComponentStyle.Danger, Label: "Stop logging", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "reset")), - new ButtonComponent(ButtonComponentStyle.Secondary, Label: "Return to menu", - CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "return")) - }) + new ActionRowComponent( + new[] + { + new ChannelSelectComponent( + CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"), + ChannelTypes: new[] { ChannelType.GuildText } + ), + } + ), + new ActionRowComponent( + new[] + { + new ButtonComponent( + ButtonComponentStyle.Danger, + Label: "Stop logging", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + "reset" + ) + ), + new ButtonComponent( + ButtonComponentStyle.Secondary, + Label: "Return to menu", + CustomID: CustomIDHelpers.CreateButtonIDWithState( + "config-channels", + "return" + ) + ), + } + ), ]; lease.Data = lease.Data with { UserId = userId }; - return await interactionApi.UpdateMessageAsync(ctx.Interaction, - new InteractionMessageCallbackData(Embeds: embeds, Components: components)); + return await interactionApi.UpdateMessageAsync( + ctx.Interaction, + new InteractionMessageCallbackData(Embeds: embeds, Components: components) + ); } } -public record ChannelCommandData(Snowflake UserId, string? CurrentPage); \ No newline at end of file +public record ChannelCommandData(Snowflake UserId, string? CurrentPage); diff --git a/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs b/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs index c032920..be456cf 100644 --- a/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs +++ b/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs @@ -22,41 +22,52 @@ public class KeyRoleCommands( ContextInjectionService contextInjection, IFeedbackService feedbackService, GuildCache guildCache, - RoleCache roleCache) : CommandGroup + RoleCache roleCache +) : CommandGroup { [Command("list")] [Description("List this server's key roles.")] public async Task ListKeyRolesAsync() { var (_, guildId) = contextInjection.GetUserAndGuild(); - if (!guildCache.TryGet(guildId, out var guild)) throw new CataloggerError("Guild not in cache"); + if (!guildCache.TryGet(guildId, out var guild)) + throw new CataloggerError("Guild not in cache"); var guildRoles = roleCache.GuildRoles(guildId).ToList(); var guildConfig = await db.GetGuildAsync(guildId); if (guildConfig.KeyRoles.Count == 0) return await feedbackService.SendContextualAsync( - "There are no key roles to list. Add some with `/key-roles add`."); + "There are no key roles to list. Add some with `/key-roles add`." + ); - var description = string.Join("\n", guildConfig.KeyRoles.Select(id => - { - var role = guildRoles.FirstOrDefault(r => r.ID.Value == id); - return role != null ? $"- {role.Name} <@&{role.ID}>" : $"- unknown role {id}"; - })); + var description = string.Join( + "\n", + guildConfig.KeyRoles.Select(id => + { + var role = guildRoles.FirstOrDefault(r => r.ID.Value == id); + return role != null ? $"- {role.Name} <@&{role.ID}>" : $"- unknown role {id}"; + }) + ); - return await feedbackService.SendContextualEmbedAsync(new Embed( - Title: $"Key roles for {guild.Name}", - Description: description, - Colour: DiscordUtils.Purple)); + return await feedbackService.SendContextualEmbedAsync( + new Embed( + Title: $"Key roles for {guild.Name}", + Description: description, + Colour: DiscordUtils.Purple + ) + ); } [Command("add")] [Description("Add a new key role.")] public async Task AddKeyRoleAsync( - [Description("The role to add.")] [DiscordTypeHint(TypeHint.Role)] Snowflake roleId) + [Description("The role to add.")] [DiscordTypeHint(TypeHint.Role)] Snowflake roleId + ) { var (_, guildId) = contextInjection.GetUserAndGuild(); var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId); - if (role == null) throw new CataloggerError("Role is not cached"); + if (role == null) + throw new CataloggerError("Role is not cached"); var guildConfig = await db.GetGuildAsync(guildId); if (guildConfig.KeyRoles.Any(id => role.ID == id)) @@ -66,27 +77,34 @@ public class KeyRoleCommands( db.Update(guildConfig); await db.SaveChangesAsync(); - return await feedbackService.SendContextualAsync($"Added {role.Name} to this server's key roles!"); + return await feedbackService.SendContextualAsync( + $"Added {role.Name} to this server's key roles!" + ); } [Command("remove")] [Description("Remove a key role.")] public async Task RemoveKeyRoleAsync( - [Description("The role to remove.")] [DiscordTypeHint(TypeHint.Role)] - Snowflake roleId) + [Description("The role to remove.")] [DiscordTypeHint(TypeHint.Role)] Snowflake roleId + ) { var (_, guildId) = contextInjection.GetUserAndGuild(); var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId); - if (role == null) throw new CataloggerError("Role is not cached"); + if (role == null) + throw new CataloggerError("Role is not cached"); var guildConfig = await db.GetGuildAsync(guildId); if (guildConfig.KeyRoles.All(id => role.ID != id)) - return await feedbackService.SendContextualAsync($"{role.Name} is already not a key role."); + return await feedbackService.SendContextualAsync( + $"{role.Name} is already not a key role." + ); guildConfig.KeyRoles.Remove(role.ID.Value); db.Update(guildConfig); await db.SaveChangesAsync(); - return await feedbackService.SendContextualAsync($"Removed {role.Name} from this server's key roles!"); + return await feedbackService.SendContextualAsync( + $"Removed {role.Name} from this server's key roles!" + ); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Commands/MetaCommands.cs b/Catalogger.Backend/Bot/Commands/MetaCommands.cs index 466d10c..a9a2208 100644 --- a/Catalogger.Backend/Bot/Commands/MetaCommands.cs +++ b/Catalogger.Backend/Bot/Commands/MetaCommands.cs @@ -32,7 +32,8 @@ public class MetaCommands( ContextInjectionService contextInjection, GuildCache guildCache, ChannelCache channelCache, - IDiscordRestChannelAPI channelApi) : CommandGroup + IDiscordRestChannelAPI channelApi +) : CommandGroup { private readonly ILogger _logger = logger.ForContext(); private readonly HttpClient _client = new(); @@ -41,12 +42,14 @@ public class MetaCommands( [Description("Ping pong! See the bot's latency")] public async Task PingAsync() { - var shardId = contextInjection.Context?.TryGetGuildID(out var guildId) == true - ? client.ShardIdFor(guildId.Value) - : 0; + var shardId = + contextInjection.Context?.TryGetGuildID(out var guildId) == true + ? client.ShardIdFor(guildId.Value) + : 0; - var averageLatency = client.Shards.Values.Select(x => x.Latency.TotalMilliseconds).Sum() / - client.Shards.Count; + var averageLatency = + client.Shards.Values.Select(x => x.Latency.TotalMilliseconds).Sum() + / client.Shards.Count; var t1 = clock.GetCurrentInstant(); var msg = await feedbackService.SendContextualAsync("...").GetOrThrow(); @@ -57,42 +60,54 @@ public class MetaCommands( var embed = new EmbedBuilder() .WithColour(DiscordUtils.Purple) - .WithFooter($"{RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}") + .WithFooter( + $"{RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}" + ) .WithCurrentTimestamp(); - embed.AddField("Ping", - $"Gateway: {client.Shards[shardId].Latency.TotalMilliseconds:N0}ms (average: {averageLatency:N0}ms)\n" + - $"API: {elapsed.TotalMilliseconds:N0}ms", - inline: true); + embed.AddField( + "Ping", + $"Gateway: {client.Shards[shardId].Latency.TotalMilliseconds:N0}ms (average: {averageLatency:N0}ms)\n" + + $"API: {elapsed.TotalMilliseconds:N0}ms", + inline: true + ); embed.AddField("Memory usage", memoryUsage.Bytes().Humanize(), inline: true); var messageRate = await MessagesRate(); - embed.AddField("Messages received", + embed.AddField( + "Messages received", messageRate != null ? $"{messageRate / 5:F1}/m\n({CataloggerMetrics.MessagesReceived.Value:N0} since last restart)" : $"{CataloggerMetrics.MessagesReceived.Value:N0} since last restart", - true); + true + ); embed.AddField("Shard", $"{shardId + 1} of {client.Shards.Count}", true); - embed.AddField("Uptime", - $"{(CataloggerMetrics.Startup - clock.GetCurrentInstant()).Prettify(TimeUnit.Second)}\n" + - $"since ", - true); + embed.AddField( + "Uptime", + $"{(CataloggerMetrics.Startup - clock.GetCurrentInstant()).Prettify(TimeUnit.Second)}\n" + + $"since ", + true + ); - embed.AddField("Numbers", - $"{CataloggerMetrics.MessagesStored.Value:N0} messages " + - $"from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels", - false); + embed.AddField( + "Numbers", + $"{CataloggerMetrics.MessagesStored.Value:N0} messages " + + $"from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels", + false + ); IEmbed[] embeds = [embed.Build().GetOrThrow()]; - return (Result)await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds); + return (Result) + await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds); } // TODO: add more checks around response format, configurable prometheus endpoint private async Task MessagesRate() { - if (!config.Logging.EnableMetrics) return null; + if (!config.Logging.EnableMetrics) + return null; try { @@ -118,4 +133,4 @@ public class MetaCommands( private record PrometheusResult(object[] value); // ReSharper restore InconsistentNaming, ClassNeverInstantiated.Local -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/DiscordUtils.cs b/Catalogger.Backend/Bot/DiscordUtils.cs index 6509fa1..9110944 100644 --- a/Catalogger.Backend/Bot/DiscordUtils.cs +++ b/Catalogger.Backend/Bot/DiscordUtils.cs @@ -14,4 +14,4 @@ public static class DiscordUtils public static readonly Color Green = Color.FromArgb(46, 204, 113); public static readonly Color Blue = Color.FromArgb(52, 152, 219); public static readonly Color Orange = Color.FromArgb(230, 126, 34); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs index 92f8527..856b43d 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs @@ -16,37 +16,47 @@ public class ChannelCreateResponder( RoleCache roleCache, ChannelCache channelCache, UserCache userCache, - WebhookExecutorService webhookExecutor) : IResponder + WebhookExecutorService webhookExecutor +) : IResponder { public async Task RespondAsync(IChannelCreate ch, CancellationToken ct = default) { - if (!ch.GuildID.IsDefined()) return Result.Success; + if (!ch.GuildID.IsDefined()) + return Result.Success; channelCache.Set(ch); var builder = new EmbedBuilder() - .WithTitle(ch.Type switch - { - ChannelType.GuildVoice => "Voice channel created", - ChannelType.GuildCategory => "Category channel created", - ChannelType.GuildAnnouncement or ChannelType.GuildText => "Text channel created", - _ => "Channel created" - }) + .WithTitle( + ch.Type switch + { + ChannelType.GuildVoice => "Voice channel created", + ChannelType.GuildCategory => "Category channel created", + ChannelType.GuildAnnouncement or ChannelType.GuildText => + "Text channel created", + _ => "Channel created", + } + ) .WithColour(DiscordUtils.Green) .WithFooter($"ID: {ch.ID}"); if (ch.ParentID.IsDefined(out var parentId)) { - builder.WithDescription(channelCache.TryGet(parentId.Value, out var parentChannel) - ? $"**Name:** {ch.Name}\n**Category:** {parentChannel.Name}" - : $"**Name:** {ch.Name}"); + builder.WithDescription( + channelCache.TryGet(parentId.Value, out var parentChannel) + ? $"**Name:** {ch.Name}\n**Category:** {parentChannel.Name}" + : $"**Name:** {ch.Name}" + ); } - else builder.WithDescription($"**Name:** {ch.Name}"); + else + builder.WithDescription($"**Name:** {ch.Name}"); foreach (var overwrite in ch.PermissionOverwrites.OrDefault() ?? []) { if (overwrite.Type == PermissionOverwriteType.Role) { - var roleName = roleCache.TryGet(overwrite.ID, out var role) ? role.Name : $"role {overwrite.ID}"; + var roleName = roleCache.TryGet(overwrite.ID, out var role) + ? role.Name + : $"role {overwrite.ID}"; var embedFieldValue = ""; if (overwrite.Allow.GetPermissions().Count != 0) embedFieldValue += $"\u2705 {overwrite.Allow.ToPrettyString()}"; @@ -64,12 +74,19 @@ public class ChannelCreateResponder( if (overwrite.Deny.GetPermissions().Count != 0) embedFieldValue += $"\n\n\u274c {overwrite.Deny.ToPrettyString()}"; - builder.AddField($"Override for {user?.Tag() ?? $"user {overwrite.ID}"}", embedFieldValue.Trim()); + builder.AddField( + $"Override for {user?.Tag() ?? $"user {overwrite.ID}"}", + embedFieldValue.Trim() + ); } } var guildConfig = await db.GetGuildAsync(ch.GuildID.Value, ct); - webhookExecutor.QueueLog(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 0414a82..e092fad 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs @@ -14,7 +14,8 @@ public class ChannelDeleteResponder( ILogger logger, DatabaseContext db, ChannelCache channelCache, - WebhookExecutorService webhookExecutor) : IResponder + WebhookExecutorService webhookExecutor +) : IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -41,7 +42,10 @@ public class ChannelDeleteResponder( .WithCurrentTimestamp() .WithDescription($"**Name:** {channel.Name.Value}"); - if (channel.ParentID.IsDefined(out var parentId) && channelCache.TryGet(parentId.Value, out var category)) + if ( + channel.ParentID.IsDefined(out var parentId) + && channelCache.TryGet(parentId.Value, out var category) + ) embed.Description += $"\n**Category:** {category.Name}"; else embed.Description += "\n**Category:** (none)"; @@ -49,7 +53,11 @@ public class ChannelDeleteResponder( if (channel.Topic.IsDefined(out var topic)) embed.AddField("Description", topic); - webhookExecutor.QueueLog(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 b2aa977..d3448d1 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs @@ -19,8 +19,8 @@ public class ChannelUpdateResponder( ChannelCache channelCache, RoleCache roleCache, UserCache userCache, - WebhookExecutorService webhookExecutor) - : IResponder + WebhookExecutorService webhookExecutor +) : IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -37,20 +37,26 @@ public class ChannelUpdateResponder( var guildConfig = await db.GetGuildAsync(evt.GuildID.Value, ct); var builder = new EmbedBuilder() - .WithTitle(evt.Type switch - { - ChannelType.GuildVoice => "Voice channel edited", - ChannelType.GuildCategory => "Category channel edited", - ChannelType.GuildAnnouncement or ChannelType.GuildText => "Text channel edited", - _ => "Channel edited" - }) + .WithTitle( + evt.Type switch + { + 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) { - var categoryUpdate = CategoryUpdate(oldChannel.ParentID.OrDefault(), evt.ParentID.OrDefault()); + var categoryUpdate = CategoryUpdate( + oldChannel.ParentID.OrDefault(), + evt.ParentID.OrDefault() + ); if (!string.IsNullOrWhiteSpace(categoryUpdate)) builder.AddField("Category", categoryUpdate); } @@ -64,7 +70,8 @@ public class ChannelUpdateResponder( var newTopic = evt.Topic.OrDefault() ?? "(none)"; var topicField = $"**Before:** {oldTopic}\n\n**After:** {newTopic}"; - if (topicField.Length > 1000) topicField = topicField[..1000] + "…"; + if (topicField.Length > 1000) + topicField = topicField[..1000] + "…"; builder.AddField("Description", topicField); } @@ -72,11 +79,19 @@ 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)).ToList(); - var removedOverrides = oldOverrides.Where(o => newOverrides.All(o2 => o.ID != o2.ID)).ToList(); + 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))); + 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) { @@ -90,7 +105,9 @@ public class ChannelUpdateResponder( } else { - addedOverrideNames.Add(roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}"); + addedOverrideNames.Add( + roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}" + ); break; } @@ -110,7 +127,9 @@ public class ChannelUpdateResponder( } else { - removedOverrideNames.Add(roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}"); + removedOverrideNames.Add( + roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}" + ); break; } } @@ -120,9 +139,12 @@ public class ChannelUpdateResponder( foreach (var overwrite in editedOverrides) { - var perms = string.Join("\n", - PermissionUpdate(oldOverrides.First(o => o.ID == overwrite.ID), overwrite)); - if (string.IsNullOrWhiteSpace(perms)) continue; + 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()); } @@ -134,19 +156,24 @@ public class ChannelUpdateResponder( embedFieldValue += $"\u2705 {overwrite.Allow.ToPrettyString()}"; if (overwrite.Deny.GetPermissions().Count != 0) embedFieldValue += $"\n\n\u274c {overwrite.Deny.ToPrettyString()}"; - if (string.IsNullOrWhiteSpace(embedFieldValue)) continue; + 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; - webhookExecutor.QueueLog(guildConfig, LogChannelType.ChannelUpdate, builder.Build().GetOrThrow()); + if (builder.Fields.Count == 0) + return Result.Success; + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.ChannelUpdate, + builder.Build().GetOrThrow() + ); return Result.Success; } finally - { channelCache.Set(evt); } @@ -172,46 +199,69 @@ public class ChannelUpdateResponder( : $"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}"; + return user != null + ? $"Override for {user.Tag()}" + : $"Override for user {overwrite.ID}"; default: - throw new ArgumentOutOfRangeException(nameof(overwrite), overwrite.Type, - "Invalid PermissionOverwriteType"); + throw new ArgumentOutOfRangeException( + nameof(overwrite), + overwrite.Type, + "Invalid PermissionOverwriteType" + ); } } - private static IEnumerable PermissionUpdate(IPermissionOverwrite oldOverwrite, - IPermissionOverwrite newOverwrite) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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/Guilds/AuditLogResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs index 71898b9..ebb7a2d 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs @@ -6,15 +6,16 @@ using Remora.Results; namespace Catalogger.Backend.Bot.Responders.Guilds; -public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger) : IResponder +public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger) + : IResponder { private readonly ILogger _logger = logger.ForContext(); - + public Task RespondAsync(IGuildAuditLogEntryCreate evt, CancellationToken ct = default) { _logger.Debug("type: {ActionType}", evt.ActionType); _logger.Debug("{Id}, {Reason}", evt.ID, evt.Reason); - + throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs index 2139c0f..8e124bd 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs @@ -20,8 +20,8 @@ public class GuildCreateResponder( ChannelCache channelCache, WebhookExecutorService webhookExecutor, IMemberCache memberCache, - GuildFetchService guildFetchService) - : IResponder, IResponder + GuildFetchService guildFetchService +) : IResponder, IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -31,46 +31,56 @@ public class GuildCreateResponder( string? guildName = null; if (evt.Guild.TryPickT0(out var guild, out var unavailableGuild)) { - _logger.Verbose("Received guild create for available guild {GuildName} / {GuildId})", guild.Name, guild.ID); + _logger.Verbose( + "Received guild create for available guild {GuildName} / {GuildId})", + guild.Name, + guild.ID + ); guildId = guild.ID.ToUlong(); guildName = guild.Name; guildCache.Set(guild); - foreach (var c in guild.Channels) channelCache.Set(c, guild.ID); - foreach (var r in guild.Roles) roleCache.Set(r, guild.ID); + foreach (var c in guild.Channels) + channelCache.Set(c, guild.ID); + foreach (var r in guild.Roles) + roleCache.Set(r, guild.ID); if (!await memberCache.IsGuildCachedAsync(guild.ID)) guildFetchService.EnqueueGuild(guild.ID); } else { - _logger.Verbose("Received guild create for unavailable guild {GuildId}", unavailableGuild.ID); + _logger.Verbose( + "Received guild create for unavailable guild {GuildId}", + unavailableGuild.ID + ); guildId = unavailableGuild.ID.ToUlong(); } var tx = await db.Database.BeginTransactionAsync(ct); - if (await db.Guilds.FindAsync([guildId], ct) != null) return Result.Success; + if (await db.Guilds.FindAsync([guildId], ct) != null) + return Result.Success; - db.Add(new Guild - { - Id = guildId - }); + db.Add(new Guild { Id = guildId }); await db.SaveChangesAsync(ct); await tx.CommitAsync(ct); _logger.Information("Joined new guild {GuildName} / {GuildId}", guildName, guildId); if (config.Discord.GuildLogId != null && evt.Guild.IsT0) - webhookExecutor.QueueLog(config.Discord.GuildLogId.Value, new EmbedBuilder() - .WithTitle("Joined new guild") - .WithDescription($"Joined new guild **{guild.Name}**") - .WithFooter($"ID: {guild.ID}") - .WithCurrentTimestamp() + webhookExecutor.QueueLog( + config.Discord.GuildLogId.Value, + new EmbedBuilder() + .WithTitle("Joined new guild") + .WithDescription($"Joined new guild **{guild.Name}**") + .WithFooter($"ID: {guild.ID}") + .WithCurrentTimestamp() #pragma warning disable CS8604 // Possible null reference argument. - .WithThumbnailUrl(guild.IconUrl()) + .WithThumbnailUrl(guild.IconUrl()) #pragma warning restore CS8604 // Possible null reference argument. - .Build() - .GetOrThrow()); + .Build() + .GetOrThrow() + ); return Result.Success; } @@ -92,17 +102,20 @@ public class GuildCreateResponder( _logger.Information("Left guild {GuildName} / {GuildId}", guild.Name, guild.ID); if (config.Discord.GuildLogId != null) - webhookExecutor.QueueLog(config.Discord.GuildLogId.Value, new EmbedBuilder() - .WithTitle("Left guild") - .WithDescription($"Left guild **{guild.Name}**") - .WithFooter($"ID: {guild.ID}") - .WithCurrentTimestamp() + webhookExecutor.QueueLog( + config.Discord.GuildLogId.Value, + new EmbedBuilder() + .WithTitle("Left guild") + .WithDescription($"Left guild **{guild.Name}**") + .WithFooter($"ID: {guild.ID}") + .WithCurrentTimestamp() #pragma warning disable CS8604 // Possible null reference argument. - .WithThumbnailUrl(guild.IconUrl()) + .WithThumbnailUrl(guild.IconUrl()) #pragma warning restore CS8604 // Possible null reference argument. - .Build() - .GetOrThrow()); + .Build() + .GetOrThrow() + ); 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 9e9be32..2902bf3 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs @@ -23,7 +23,8 @@ public class GuildMemberAddResponder( UserCache userCache, WebhookExecutorService webhookExecutor, IDiscordRestGuildAPI guildApi, - PluralkitApiService pluralkitApi) : IResponder + PluralkitApiService pluralkitApi +) : IResponder { private readonly ILogger _logger = logger.ForContext(); private static readonly TimeSpan NewAccountThreshold = 7.Days(); @@ -45,7 +46,8 @@ public class GuildMemberAddResponder( var guildConfig = await db.GetGuildAsync(member.GuildID, ct); var guildRes = await guildApi.GetGuildAsync(member.GuildID, withCounts: true, ct); if (guildRes.IsSuccess) - builder.Description += $"\n{guildRes.Entity.ApproximateMemberCount.Value.Ordinalize()} to join"; + builder.Description += + $"\n{guildRes.Entity.ApproximateMemberCount.Value.Ordinalize()} to join"; builder.Description += $"\ncreated {user.ID.Timestamp.Prettify()} ago\n"; @@ -53,15 +55,19 @@ public class GuildMemberAddResponder( var pkSystem = await pluralkitApi.GetPluralKitSystemAsync(user.ID.Value, ct); if (pkSystem != null) { - var createdAt = pkSystem.Created != null - ? $"{pkSystem.Created.Value.Prettify()} ago ()" - : "*(unknown)*"; - builder.AddField("PluralKit system", $""" - **ID:** {pkSystem.Id} (`{pkSystem.Uuid}`) - **Name:** {pkSystem.Name ?? "*(none)*"} - **Tag:** {pkSystem.Tag ?? "*(none)*"} - **Created:** {createdAt} - """); + var createdAt = + pkSystem.Created != null + ? $"{pkSystem.Created.Value.Prettify()} ago ()" + : "*(unknown)*"; + builder.AddField( + "PluralKit system", + $""" + **ID:** {pkSystem.Id} (`{pkSystem.Uuid}`) + **Name:** {pkSystem.Name ?? "*(none)*"} + **Tag:** {pkSystem.Tag ?? "*(none)*"} + **Created:** {createdAt} + """ + ); } // TODO: find used invite @@ -70,51 +76,73 @@ public class GuildMemberAddResponder( if (user.ID.Timestamp > DateTimeOffset.Now - NewAccountThreshold) { - embeds.Add(new EmbedBuilder() - .WithTitle("New account") - .WithColour(DiscordUtils.Orange) - .WithDescription($"\u26a0\ufe0f Created {user.ID.Timestamp.Prettify()} ago") - .Build() - .GetOrThrow()); + embeds.Add( + new EmbedBuilder() + .WithTitle("New account") + .WithColour(DiscordUtils.Orange) + .WithDescription($"\u26a0\ufe0f Created {user.ID.Timestamp.Prettify()} ago") + .Build() + .GetOrThrow() + ); } var watchlist = await db.GetWatchlistEntryAsync(member.GuildID, user.ID, ct); if (watchlist != null) { - var moderator = await userCache.GetUserAsync(DiscordSnowflake.New(watchlist.ModeratorId)); - var mod = moderator != null ? $"{moderator.Tag()} (<@{moderator.ID}>)" : $"<@{watchlist.ModeratorId}>"; + var moderator = await userCache.GetUserAsync( + DiscordSnowflake.New(watchlist.ModeratorId) + ); + var mod = + moderator != null + ? $"{moderator.Tag()} (<@{moderator.ID}>)" + : $"<@{watchlist.ModeratorId}>"; - embeds.Add(new EmbedBuilder() - .WithTitle("⚠️ User on watchlist") - .WithColour(DiscordUtils.Red) - .WithDescription($"**{user.Tag()}** is on this server's watch list.\n\n{watchlist.Reason}") - .WithFooter($"ID: {user.ID} | Added") - .WithTimestamp(watchlist.AddedAt.ToDateTimeOffset()) - .AddField("Moderator", mod).GetOrThrow() - .Build() - .GetOrThrow()); + embeds.Add( + new EmbedBuilder() + .WithTitle("⚠️ User on watchlist") + .WithColour(DiscordUtils.Red) + .WithDescription( + $"**{user.Tag()}** is on this server's watch list.\n\n{watchlist.Reason}" + ) + .WithFooter($"ID: {user.ID} | Added") + .WithTimestamp(watchlist.AddedAt.ToDateTimeOffset()) + .AddField("Moderator", mod) + .GetOrThrow() + .Build() + .GetOrThrow() + ); } if (pkSystem != null) { - if (guildConfig.BannedSystems.Contains(pkSystem.Id) || - guildConfig.BannedSystems.Contains(pkSystem.Uuid.ToString())) + if ( + guildConfig.BannedSystems.Contains(pkSystem.Id) + || guildConfig.BannedSystems.Contains(pkSystem.Uuid.ToString()) + ) { - embeds.Add(new EmbedBuilder().WithTitle("Banned system") - .WithDescription( - "\u26a0\ufe0f The system associated with this account has been banned from the server.") - .WithColour(DiscordUtils.Red) - .WithFooter($"ID: {pkSystem.Id}") - .Build() - .GetOrThrow()); + embeds.Add( + new EmbedBuilder() + .WithTitle("Banned system") + .WithDescription( + "\u26a0\ufe0f The system associated with this account has been banned from the server." + ) + .WithColour(DiscordUtils.Red) + .WithFooter($"ID: {pkSystem.Id}") + .Build() + .GetOrThrow() + ); } } if (embeds.Count > 1) - await webhookExecutor.SendLogAsync(guildConfig.Channels.GuildMemberAdd, - embeds.Cast().ToList(), []); - else webhookExecutor.QueueLog(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; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs index dab6702..16d9b4f 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs @@ -17,7 +17,8 @@ public class GuildMemberRemoveResponder( IMemberCache memberCache, RoleCache roleCache, WebhookExecutorService webhookExecutor, - AuditLogEnrichedResponderService auditLogEnrichedResponderService) : IResponder + AuditLogEnrichedResponderService auditLogEnrichedResponderService +) : IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -43,8 +44,14 @@ public class GuildMemberRemoveResponder( { _logger.Information( "Guild member {UserId} in {GuildId} left but wasn't in the cache, sending limited embed", - evt.User.ID, evt.GuildID); - webhookExecutor.QueueLog(guildConfig, LogChannelType.GuildMemberRemove, embed.Build().GetOrThrow()); + evt.User.ID, + evt.GuildID + ); + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.GuildMemberRemove, + embed.Build().GetOrThrow() + ); return Result.Success; } @@ -65,12 +72,17 @@ public class GuildMemberRemoveResponder( } roleMentions += $"<@&{role.ID}>"; - if (idx != roles.Count - 1) roleMentions += ", "; + if (idx != roles.Count - 1) + roleMentions += ", "; } embed.AddField("Roles", roleMentions, inline: false); - webhookExecutor.QueueLog(guildConfig, LogChannelType.GuildMemberRemove, embed.Build().GetOrThrow()); + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.GuildMemberRemove, + embed.Build().GetOrThrow() + ); return Result.Success; } finally @@ -78,4 +90,4 @@ public class GuildMemberRemoveResponder( await memberCache.RemoveAsync(evt.GuildID, evt.User.ID); } } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMembersChunkResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMembersChunkResponder.cs index be85fa8..f411ecc 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMembersChunkResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMembersChunkResponder.cs @@ -5,23 +5,31 @@ using Remora.Results; namespace Catalogger.Backend.Bot.Responders.Guilds; -public class GuildMembersChunkResponder(ILogger logger, IMemberCache memberCache) : IResponder +public class GuildMembersChunkResponder(ILogger logger, IMemberCache memberCache) + : IResponder { private readonly ILogger _logger = logger.ForContext(); public async Task RespondAsync(IGuildMembersChunk evt, CancellationToken ct = default) { - _logger.Debug("Received chunk {ChunkIndex} / {ChunkCount} for guild {GuildId}", evt.ChunkIndex + 1, - evt.ChunkCount, evt.GuildID); + _logger.Debug( + "Received chunk {ChunkIndex} / {ChunkCount} for guild {GuildId}", + evt.ChunkIndex + 1, + evt.ChunkCount, + evt.GuildID + ); await memberCache.SetManyAsync(evt.GuildID, evt.Members); if (evt.ChunkIndex == evt.ChunkCount - 1) { - _logger.Debug("Final chunk for guild {GuildId} received, marking as cached", evt.GuildID); + _logger.Debug( + "Final chunk for guild {GuildId} received, marking as cached", + evt.GuildID + ); await memberCache.MarkAsCachedAsync(evt.GuildID); } return Result.Success; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs index d33caf8..c28c759 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs @@ -18,8 +18,8 @@ public class MessageCreateResponder( DatabaseContext db, MessageRepository messageRepository, UserCache userCache, - PkMessageHandler pkMessageHandler) - : IResponder + PkMessageHandler pkMessageHandler +) : IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -30,8 +30,10 @@ public class MessageCreateResponder( if (!msg.GuildID.IsDefined()) { - _logger.Debug("Received message create event for message {MessageId} despite it not being in a guild", - msg.ID); + _logger.Debug( + "Received message create event for message {MessageId} despite it not being in a guild", + msg.ID + ); return Result.Success; } @@ -66,7 +68,8 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) private readonly ILogger _logger = logger.ForContext(); [GeneratedRegex( - @"^System ID: (\w{5,6}) \| Member ID: (\w{5,6}) \| Sender: .+ \((\d+)\) \| Message ID: (\d+) \| Original Message ID: (\d+)$")] + @"^System ID: (\w{5,6}) \| Member ID: (\w{5,6}) \| Sender: .+ \((\d+)\) \| Message ID: (\d+) \| Original Message ID: (\d+)$" + )] private static partial Regex FooterRegex(); [GeneratedRegex(@"^https:\/\/discord.com\/channels\/\d+\/(\d+)\/\d+$")] @@ -89,11 +92,15 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) // The first (only, I think always?) embed's footer must match the expected format var firstEmbed = msg.Embeds.FirstOrDefault(); - if (firstEmbed == null || !firstEmbed.Footer.TryGet(out var footer) || - !FooterRegex().IsMatch(footer.Text)) + if ( + firstEmbed == null + || !firstEmbed.Footer.TryGet(out var footer) + || !FooterRegex().IsMatch(footer.Text) + ) { _logger.Debug( - "PK message is not a log message because there is no first embed or its footer doesn't match the regex"); + "PK message is not a log message because there is no first embed or its footer doesn't match the regex" + ); return; } @@ -101,19 +108,28 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) if (!ulong.TryParse(match.Groups[3].Value, out var authorId)) { - _logger.Debug("Author ID in PluralKit log {LogMessageId} was not a valid snowflake", msg.ID); + _logger.Debug( + "Author ID in PluralKit log {LogMessageId} was not a valid snowflake", + msg.ID + ); return; } if (!ulong.TryParse(match.Groups[4].Value, out var msgId)) { - _logger.Debug("Message ID in PluralKit log {LogMessageId} was not a valid snowflake", msg.ID); + _logger.Debug( + "Message ID in PluralKit log {LogMessageId} was not a valid snowflake", + msg.ID + ); return; } if (!ulong.TryParse(match.Groups[5].Value, out var originalId)) { - _logger.Debug("Original ID in PluralKit log {LogMessageId} was not a valid snowflake", msg.ID); + _logger.Debug( + "Original ID in PluralKit log {LogMessageId} was not a valid snowflake", + msg.ID + ); return; } @@ -121,8 +137,13 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) await using var db = scope.ServiceProvider.GetRequiredService(); var messageRepository = scope.ServiceProvider.GetRequiredService(); - await messageRepository.SetProxiedMessageDataAsync(msgId, originalId, authorId, - systemId: match.Groups[1].Value, memberId: match.Groups[2].Value); + await messageRepository.SetProxiedMessageDataAsync( + msgId, + originalId, + authorId, + systemId: match.Groups[1].Value, + memberId: match.Groups[2].Value + ); db.IgnoredMessages.Add(new IgnoredMessage(originalId)); await db.SaveChangesAsync(); @@ -144,19 +165,28 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) return; } - if (hasProxyInfo) return; + if (hasProxyInfo) + return; var pkMessage = await pluralkitApi.GetPluralKitMessageAsync(msgId); if (pkMessage == null) { - _logger.Debug("Message with ID {MessageId} was proxied by PluralKit, but API returned 404", msgId); + _logger.Debug( + "Message with ID {MessageId} was proxied by PluralKit, but API returned 404", + msgId + ); return; } - await messageRepository.SetProxiedMessageDataAsync(msgId, pkMessage.Original, pkMessage.Sender, - pkMessage.System?.Id, pkMessage.Member?.Id); + await messageRepository.SetProxiedMessageDataAsync( + msgId, + pkMessage.Original, + pkMessage.Sender, + pkMessage.System?.Id, + pkMessage.Member?.Id + ); db.IgnoredMessages.Add(new IgnoredMessage(pkMessage.Original)); await db.SaveChangesAsync(); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs index fb0d34a..1a280c5 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs @@ -24,41 +24,55 @@ public class MessageDeleteResponder( ChannelCache channelCache, UserCache userCache, IClock clock, - PluralkitApiService pluralkitApi) : IResponder + PluralkitApiService pluralkitApi +) : IResponder { private readonly ILogger _logger = logger.ForContext(); - private static bool MaybePkProxyTrigger(Snowflake id) => id.Timestamp > DateTimeOffset.Now - 1.Minutes(); + private static bool MaybePkProxyTrigger(Snowflake id) => + id.Timestamp > DateTimeOffset.Now - 1.Minutes(); public async Task RespondAsync(IMessageDelete ev, CancellationToken ct = default) { - if (!ev.GuildID.IsDefined()) return Result.Success; + if (!ev.GuildID.IsDefined()) + return Result.Success; if (MaybePkProxyTrigger(ev.ID)) { _logger.Debug( "Deleted message {MessageId} is less than 1 minute old, delaying 5 seconds to give PK time to catch up", - ev.ID); + ev.ID + ); await Task.Delay(5.Seconds(), ct); } - if (await messageRepository.IsMessageIgnoredAsync(ev.ID.Value, ct)) return Result.Success; + if (await messageRepository.IsMessageIgnoredAsync(ev.ID.Value, ct)) + return Result.Success; var guild = await db.GetGuildAsync(ev.GuildID, ct); - if (guild.IsMessageIgnored(ev.ChannelID, ev.ID)) return Result.Success; + if (guild.IsMessageIgnored(ev.ChannelID, ev.ID)) + return Result.Success; - var logChannel = webhookExecutor.GetLogChannel(guild, LogChannelType.MessageDelete, ev.ChannelID); + var logChannel = webhookExecutor.GetLogChannel( + guild, + LogChannelType.MessageDelete, + ev.ChannelID + ); var msg = await messageRepository.GetMessageAsync(ev.ID.Value, ct); // Sometimes a message that *should* be logged isn't stored in the database, notify the user of that if (msg == null) { - if (logChannel == null) return Result.Success; - 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}"), - Timestamp: clock.GetCurrentInstant().ToDateTimeOffset() - )); + if (logChannel == null) + return Result.Success; + 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}"), + Timestamp: clock.GetCurrentInstant().ToDateTimeOffset() + ) + ); return Result.Success; } @@ -71,13 +85,22 @@ public class MessageDeleteResponder( var pkMsg = await pluralkitApi.GetPluralKitMessageAsync(ev.ID.Value, ct); if (pkMsg != null && pkMsg.Id != ev.ID.Value && pkMsg.Original != ev.ID.Value) { - _logger.Debug("Deleted message {MessageId} is a `pk;edit` message, ignoring", ev.ID); + _logger.Debug( + "Deleted message {MessageId} is a `pk;edit` message, ignoring", + ev.ID + ); return Result.Success; } } - logChannel = webhookExecutor.GetLogChannel(guild, LogChannelType.MessageDelete, ev.ChannelID, msg.UserId); - if (logChannel == null) return Result.Success; + logChannel = webhookExecutor.GetLogChannel( + guild, + LogChannelType.MessageDelete, + ev.ChannelID, + msg.UserId + ); + if (logChannel == null) + return Result.Success; var user = await userCache.GetUserAsync(DiscordSnowflake.New(msg.UserId)); var builder = new EmbedBuilder() @@ -89,23 +112,34 @@ public class MessageDeleteResponder( if (user != null) builder.WithAuthor(user.Tag(), url: null, iconUrl: user.AvatarUrl()); - if (msg.Member != null) builder.WithTitle($"Message by {msg.Username} deleted"); + if (msg.Member != null) + builder.WithTitle($"Message by {msg.Username} deleted"); string channelMention; if (!channelCache.TryGet(ev.ChannelID, out var channel)) channelMention = $"<#{msg.ChannelId}>"; - else if (channel.Type is ChannelType.AnnouncementThread or ChannelType.PrivateThread - or ChannelType.PublicThread) + else if ( + channel.Type + is ChannelType.AnnouncementThread + or ChannelType.PrivateThread + or ChannelType.PublicThread + ) channelMention = $"<#{channel.ParentID.Value}>\nID: {channel.ParentID.Value}\n\nThread: {channel.Name} (<#{channel.ID}>)"; - else channelMention = $"<#{channel.ID}>\nID: {channel.ID}"; + else + channelMention = $"<#{channel.ID}>\nID: {channel.ID}"; - var userMention = user != null - ? $"<@{user.ID}>\n{user.Tag()}\nID: {user.ID}" - : $"<@{msg.UserId}>\nID: {msg.UserId}"; + var userMention = + user != null + ? $"<@{user.ID}>\n{user.Tag()}\nID: {user.ID}" + : $"<@{msg.UserId}>\nID: {msg.UserId}"; builder.AddField("Channel", channelMention, true); - builder.AddField(msg.System != null ? "Linked Discord account" : "Sender", userMention, true); + builder.AddField( + msg.System != null ? "Linked Discord account" : "Sender", + userMention, + true + ); if (msg is { System: not null, Member: not null }) { builder.AddField("\u200b", "**PluralKit information**", false); @@ -115,13 +149,17 @@ public class MessageDeleteResponder( if (msg.Metadata != null) { - var attachmentInfo = string.Join("\n", + var attachmentInfo = string.Join( + "\n", msg.Metadata.Attachments.Select(a => - $"{a.Filename} ({a.ContentType}, {a.Size.Bytes().Humanize()})")); - if (!string.IsNullOrWhiteSpace(attachmentInfo)) builder.AddField("Attachments", attachmentInfo, false); + $"{a.Filename} ({a.ContentType}, {a.Size.Bytes().Humanize()})" + ) + ); + if (!string.IsNullOrWhiteSpace(attachmentInfo)) + builder.AddField("Attachments", attachmentInfo, false); } 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 1510e25..6742d92 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs @@ -21,7 +21,8 @@ public class MessageUpdateResponder( UserCache userCache, MessageRepository messageRepository, WebhookExecutorService webhookExecutor, - PluralkitApiService pluralkitApi) : IResponder + PluralkitApiService pluralkitApi +) : IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -33,8 +34,10 @@ public class MessageUpdateResponder( if (!msg.GuildID.IsDefined()) { - _logger.Debug("Received message create event for message {MessageId} despite it not being in a guild", - msg.ID); + _logger.Debug( + "Received message create event for message {MessageId} despite it not being in a guild", + msg.ID + ); return Result.Success; } @@ -48,25 +51,39 @@ public class MessageUpdateResponder( try { - var logChannel = webhookExecutor.GetLogChannel(guildConfig, LogChannelType.MessageUpdate, msg.ChannelID, - msg.Author.ID.Value); - if (logChannel == null) return Result.Success; + 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) { - logger.Debug("Message {Id} was edited and should be logged but is not in the database", msg.ID); + logger.Debug( + "Message {Id} was edited and should be logged but is not in the database", + msg.ID + ); return Result.Success; } - if (oldMessage.Content == msg.Content || - (oldMessage.Content == "None" && string.IsNullOrEmpty(msg.Content))) return Result.Success; + if ( + oldMessage.Content == msg.Content + || (oldMessage.Content == "None" && string.IsNullOrEmpty(msg.Content)) + ) + return Result.Success; var user = msg.Author; if (msg.Author.ID != oldMessage.UserId) { - var systemAccount = await userCache.GetUserAsync(DiscordSnowflake.New(oldMessage.UserId)); - if (systemAccount != null) user = systemAccount; + var systemAccount = await userCache.GetUserAsync( + DiscordSnowflake.New(oldMessage.UserId) + ); + if (systemAccount != null) + user = systemAccount; } var embedBuilder = new EmbedBuilder() @@ -78,19 +95,25 @@ public class MessageUpdateResponder( .WithTimestamp(msg.ID.Timestamp); var fields = ChunksUpTo(msg.Content, 1000) - .Select((s, i) => - new EmbedField($"New content{(i != 0 ? " (cont.)" : "")}", s, false)) + .Select( + (s, i) => new EmbedField($"New content{(i != 0 ? " (cont.)" : "")}", s, false) + ) .ToList(); embedBuilder.SetFields(fields); string channelMention; if (!channelCache.TryGet(msg.ChannelID, out var channel)) channelMention = $"<#{msg.ChannelID}>"; - else if (channel.Type is ChannelType.AnnouncementThread or ChannelType.PrivateThread - or ChannelType.PublicThread) + else if ( + channel.Type + is ChannelType.AnnouncementThread + or ChannelType.PrivateThread + or ChannelType.PublicThread + ) channelMention = $"<#{channel.ParentID.Value}>\nID: {channel.ParentID.Value}\n\nThread: {channel.Name} (<#{channel.ID}>)"; - else channelMention = $"<#{channel.ID}>\nID: {channel.ID}"; + else + channelMention = $"<#{channel.ID}>\nID: {channel.ID}"; embedBuilder.AddField("Channel", channelMention, true); embedBuilder.AddField("Sender", $"<@{user.ID}>\n{user.Tag()}\nID: {user.ID}", true); @@ -103,7 +126,10 @@ public class MessageUpdateResponder( embedBuilder.AddField("Member ID", oldMessage.Member, true); } - embedBuilder.AddField("Link", $"https://discord.com/channels/{msg.GuildID}/{msg.ChannelID}/{msg.ID}"); + embedBuilder.AddField( + "Link", + $"https://discord.com/channels/{msg.GuildID}/{msg.ChannelID}/{msg.ID}" + ); webhookExecutor.QueueLog(logChannel.Value, embedBuilder.Build().GetOrThrow()); return Result.Success; @@ -113,39 +139,91 @@ public class MessageUpdateResponder( // 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) + 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 + ) { - if (!await messageRepository.UpdateMessageAsync(msg, ct) && msg.ApplicationID.Is(DiscordUtils.PkUserId)) + 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); + 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); + await messageRepository.SetProxiedMessageDataAsync( + msg.ID.Value, + pkMsg.Original, + pkMsg.Sender, + pkMsg.System?.Id, + pkMsg.Member?.Id + ); } } } } - private static MessageCreate ConvertToMessageCreate(IMessageUpdate evt) => new(evt.GuildID, evt.Member, - evt.Mentions.GetOrThrow(), evt.ID.GetOrThrow(), evt.ChannelID.GetOrThrow(), evt.Author.GetOrThrow(), - evt.Content.GetOrThrow(), evt.Timestamp.GetOrThrow(), evt.EditedTimestamp.GetOrThrow(), IsTTS: false, - evt.MentionsEveryone.GetOrThrow(), evt.MentionedRoles.GetOrThrow(), evt.MentionedChannels, - evt.Attachments.GetOrThrow(), evt.Embeds.GetOrThrow(), evt.Reactions, evt.Nonce, evt.IsPinned.GetOrThrow(), - evt.WebhookID, evt.Type.GetOrThrow(), evt.Activity, evt.Application, evt.ApplicationID, evt.MessageReference, - evt.Flags, evt.ReferencedMessage, evt.Interaction, evt.Thread, evt.Components, evt.StickerItems, evt.Position, - evt.Resolved, evt.InteractionMetadata, evt.Poll); + private static MessageCreate ConvertToMessageCreate(IMessageUpdate evt) => + new( + evt.GuildID, + evt.Member, + evt.Mentions.GetOrThrow(), + evt.ID.GetOrThrow(), + evt.ChannelID.GetOrThrow(), + evt.Author.GetOrThrow(), + evt.Content.GetOrThrow(), + evt.Timestamp.GetOrThrow(), + evt.EditedTimestamp.GetOrThrow(), + IsTTS: false, + evt.MentionsEveryone.GetOrThrow(), + evt.MentionedRoles.GetOrThrow(), + evt.MentionedChannels, + evt.Attachments.GetOrThrow(), + evt.Embeds.GetOrThrow(), + evt.Reactions, + evt.Nonce, + evt.IsPinned.GetOrThrow(), + evt.WebhookID, + evt.Type.GetOrThrow(), + evt.Activity, + evt.Application, + evt.ApplicationID, + evt.MessageReference, + evt.Flags, + evt.ReferencedMessage, + evt.Interaction, + evt.Thread, + evt.Components, + evt.StickerItems, + evt.Position, + evt.Resolved, + evt.InteractionMetadata, + evt.Poll + ); private static IEnumerable ChunksUpTo(string str, int maxChunkSize) { for (var i = 0; i < str.Length; i += maxChunkSize) yield return str.Substring(i, Math.Min(maxChunkSize, str.Length - i)); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/ReadyResponder.cs b/Catalogger.Backend/Bot/Responders/ReadyResponder.cs index 1e98e07..6cce8bf 100644 --- a/Catalogger.Backend/Bot/Responders/ReadyResponder.cs +++ b/Catalogger.Backend/Bot/Responders/ReadyResponder.cs @@ -13,11 +13,18 @@ public class ReadyResponder(ILogger logger, WebhookExecutorService webhookExecut public Task RespondAsync(IReady gatewayEvent, CancellationToken ct = default) { - var shardId = gatewayEvent.Shard.TryGet(out var shard) ? (shard.ShardID, shard.ShardCount) : (0, 1); - _logger.Information("Ready as {User} on shard {ShardId} / {ShardCount}", gatewayEvent.User.Tag(), shardId.Item1, - shardId.Item2); - if (shardId.Item1 == 0) webhookExecutorService.SetSelfUser(gatewayEvent.User); + var shardId = gatewayEvent.Shard.TryGet(out var shard) + ? (shard.ShardID, shard.ShardCount) + : (0, 1); + _logger.Information( + "Ready as {User} on shard {ShardId} / {ShardCount}", + gatewayEvent.User.Tag(), + shardId.Item1, + shardId.Item2 + ); + if (shardId.Item1 == 0) + webhookExecutorService.SetSelfUser(gatewayEvent.User); return Task.FromResult(Result.Success); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs index 71d1dc1..df3e8e2 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs @@ -14,7 +14,8 @@ public class RoleCreateResponder( ILogger logger, DatabaseContext db, RoleCache roleCache, - WebhookExecutorService webhookExecutor) : IResponder + WebhookExecutorService webhookExecutor +) : IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -28,16 +29,22 @@ public class RoleCreateResponder( var embed = new EmbedBuilder() .WithTitle("Role created") .WithColour(DiscordUtils.Green) - .WithDescription($"**Name:** {evt.Role.Name}\n**Colour:** {evt.Role.Colour.ToPrettyString()}" + - $"\n**Mentionable:** {evt.Role.IsMentionable}\n**Shown separately:** {evt.Role.IsHoisted}"); + .WithDescription( + $"**Name:** {evt.Role.Name}\n**Colour:** {evt.Role.Colour.ToPrettyString()}" + + $"\n**Mentionable:** {evt.Role.IsMentionable}\n**Shown separately:** {evt.Role.IsHoisted}" + ); if (!evt.Role.Permissions.Value.IsZero) { embed.AddField("Permissions", evt.Role.Permissions.ToPrettyString(), inline: false); } - webhookExecutor.QueueLog(guildConfig, LogChannelType.GuildRoleCreate, embed.Build().GetOrThrow()); + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.GuildRoleCreate, + embed.Build().GetOrThrow() + ); return Result.Success; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs index 9ac8d0e..8caf53a 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs @@ -12,22 +12,27 @@ using Remora.Results; namespace Catalogger.Backend.Bot.Responders.Roles; -public class RoleUpdateResponder(ILogger logger, +public class RoleUpdateResponder( + ILogger logger, DatabaseContext db, RoleCache roleCache, - WebhookExecutorService webhookExecutor) : IResponder + WebhookExecutorService webhookExecutor +) : IResponder { private readonly ILogger _logger = logger.ForContext(); - + public async Task RespondAsync(IGuildRoleUpdate evt, CancellationToken ct = default) { try { var newRole = evt.Role; - + if (!roleCache.TryGet(evt.Role.ID, out var oldRole)) { - _logger.Information("Received role update event for {RoleId} but it wasn't cached, ignoring", evt.Role.ID); + _logger.Information( + "Received role update event for {RoleId} but it wasn't cached, ignoring", + evt.Role.ID + ); return Result.Success; } @@ -42,21 +47,31 @@ public class RoleUpdateResponder(ILogger logger, embed.AddField("Name", $"**Before:** {oldRole.Name}\n**After:** {newRole.Name}"); } - if (newRole.IsHoisted != oldRole.IsHoisted || newRole.IsMentionable != oldRole.IsMentionable) + if ( + newRole.IsHoisted != oldRole.IsHoisted + || newRole.IsMentionable != oldRole.IsMentionable + ) { embed.AddField( - "\u200b", $"**Mentionable:** {newRole.IsMentionable}\n**Shown separately:** {newRole.IsHoisted}"); + "\u200b", + $"**Mentionable:** {newRole.IsMentionable}\n**Shown separately:** {newRole.IsHoisted}" + ); } if (newRole.Colour != oldRole.Colour) { - embed.AddField("Colour", - $"**Before:** {oldRole.Colour.ToPrettyString()}\n**After:** {newRole.Colour.ToPrettyString()}"); + embed.AddField( + "Colour", + $"**Before:** {oldRole.Colour.ToPrettyString()}\n**After:** {newRole.Colour.ToPrettyString()}" + ); } if (newRole.Permissions.Value != oldRole.Permissions.Value) { - var diff = string.Join("\n", PermissionUpdate(oldRole.Permissions, newRole.Permissions)); + var diff = string.Join( + "\n", + PermissionUpdate(oldRole.Permissions, newRole.Permissions) + ); embed.AddField("Permissions", $"```diff\n{diff}\n```"); } @@ -67,9 +82,13 @@ public class RoleUpdateResponder(ILogger logger, _logger.Debug("We don't care about update of role {RoleId}, ignoring", evt.Role.ID); return Result.Success; } - + var guildConfig = await db.GetGuildAsync(evt.GuildID, ct); - webhookExecutor.QueueLog(guildConfig, LogChannelType.GuildRoleUpdate, embed.Build().GetOrThrow()); + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.GuildRoleUpdate, + embed.Build().GetOrThrow() + ); } finally { @@ -78,8 +97,11 @@ public class RoleUpdateResponder(ILogger logger, return Result.Success; } - - private static IEnumerable PermissionUpdate(IDiscordPermissionSet oldValue, IDiscordPermissionSet newValue) + + private static IEnumerable PermissionUpdate( + IDiscordPermissionSet oldValue, + IDiscordPermissionSet newValue + ) { foreach (var perm in Enum.GetValues()) { @@ -93,4 +115,4 @@ public class RoleUpdateResponder(ILogger logger, } } } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/ShardedDiscordService.cs b/Catalogger.Backend/Bot/ShardedDiscordService.cs index c961e81..a725ed7 100644 --- a/Catalogger.Backend/Bot/ShardedDiscordService.cs +++ b/Catalogger.Backend/Bot/ShardedDiscordService.cs @@ -2,7 +2,8 @@ using Remora.Discord.Gateway.Results; namespace Catalogger.Backend.Bot; -public class ShardedDiscordService(ShardedGatewayClient client, IHostApplicationLifetime lifetime) : BackgroundService +public class ShardedDiscordService(ShardedGatewayClient client, IHostApplicationLifetime lifetime) + : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { @@ -10,4 +11,4 @@ public class ShardedDiscordService(ShardedGatewayClient client, IHostApplication if (result.Error is GatewayError { IsCritical: true }) lifetime.StopApplication(); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Bot/ShardedGatewayClient.cs b/Catalogger.Backend/Bot/ShardedGatewayClient.cs index 56e9207..ec8a28b 100644 --- a/Catalogger.Backend/Bot/ShardedGatewayClient.cs +++ b/Catalogger.Backend/Bot/ShardedGatewayClient.cs @@ -16,18 +16,22 @@ public class ShardedGatewayClient( IDiscordRestGatewayAPI gatewayApi, IServiceProvider services, IOptions gatewayClientOptions, - Config config) - : IDisposable + Config config +) : IDisposable { private int _shardCount = config.Discord.ShardCount ?? 0; private readonly ILogger _logger = logger.ForContext(); private readonly ConcurrentDictionary _gatewayClients = new(); - private static readonly FieldInfo Field = - typeof(DiscordGatewayClient).GetField("_connectionStatus", BindingFlags.Instance | BindingFlags.NonPublic)!; + private static readonly FieldInfo Field = typeof(DiscordGatewayClient).GetField( + "_connectionStatus", + BindingFlags.Instance | BindingFlags.NonPublic + )!; - private static readonly Func GetConnectionStatus = - client => (GatewayConnectionStatus)Field.GetValue(client)!; + private static readonly Func< + DiscordGatewayClient, + GatewayConnectionStatus + > GetConnectionStatus = client => (GatewayConnectionStatus)Field.GetValue(client)!; public IReadOnlyDictionary Shards => _gatewayClients; @@ -45,19 +49,26 @@ public class ShardedGatewayClient( if (_shardCount < discordShardCount && _shardCount != 0) _logger.Warning( "Discord recommends {DiscordShardCount} for this bot, but only {ConfigShardCount} shards are requested. This may cause issues later", - discordShardCount, _shardCount); + discordShardCount, + _shardCount + ); - if (_shardCount == 0) _shardCount = discordShardCount; + if (_shardCount == 0) + _shardCount = discordShardCount; } - var clients = Enumerable.Range(0, _shardCount).Select(s => - { - var client = - ActivatorUtilities.CreateInstance(services, - CloneOptions(gatewayClientOptions.Value, s)); - _gatewayClients[s] = client; - return client; - }).ToArray(); + var clients = Enumerable + .Range(0, _shardCount) + .Select(s => + { + var client = ActivatorUtilities.CreateInstance( + services, + CloneOptions(gatewayClientOptions.Value, s) + ); + _gatewayClients[s] = client; + return client; + }) + .ToArray(); var tasks = new List>(); @@ -69,7 +80,10 @@ public class ShardedGatewayClient( var res = client.RunAsync(ct); tasks.Add(res); - while (GetConnectionStatus(client) is not GatewayConnectionStatus.Connected && !res.IsCompleted) + while ( + GetConnectionStatus(client) is not GatewayConnectionStatus.Connected + && !res.IsCompleted + ) { await Task.Delay(100, ct); } @@ -92,7 +106,9 @@ public class ShardedGatewayClient( public DiscordGatewayClient ClientFor(ulong guildId) => _gatewayClients.TryGetValue(ShardIdFor(guildId), out var client) ? client - : throw new CataloggerError("Shard was null, has ShardedGatewayClient.RunAsync been called?"); + : throw new CataloggerError( + "Shard was null, has ShardedGatewayClient.RunAsync been called?" + ); public void Dispose() { @@ -100,7 +116,10 @@ public class ShardedGatewayClient( client.Dispose(); } - private IOptions CloneOptions(DiscordGatewayClientOptions options, int shardId) + private IOptions CloneOptions( + DiscordGatewayClientOptions options, + int shardId + ) { var ret = new DiscordGatewayClientOptions { @@ -112,9 +131,9 @@ public class ShardedGatewayClient( LargeThreshold = options.LargeThreshold, CommandBurstRate = options.CommandBurstRate, HeartbeatSafetyMargin = options.HeartbeatSafetyMargin, - MinimumSafetyMargin = options.MinimumSafetyMargin + MinimumSafetyMargin = options.MinimumSafetyMargin, }; return Options.Create(ret); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/IInviteCache.cs b/Catalogger.Backend/Cache/IInviteCache.cs index 8ee4912..49a4131 100644 --- a/Catalogger.Backend/Cache/IInviteCache.cs +++ b/Catalogger.Backend/Cache/IInviteCache.cs @@ -7,4 +7,4 @@ public interface IInviteCache { public Task> TryGetAsync(Snowflake guildId); public Task SetAsync(Snowflake guildId, IEnumerable invites); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/IMemberCache.cs b/Catalogger.Backend/Cache/IMemberCache.cs index 72fa463..e7a218a 100644 --- a/Catalogger.Backend/Cache/IMemberCache.cs +++ b/Catalogger.Backend/Cache/IMemberCache.cs @@ -12,4 +12,4 @@ public interface IMemberCache public Task IsGuildCachedAsync(Snowflake guildId); public Task MarkAsCachedAsync(Snowflake guildId); public Task MarkAsUncachedAsync(Snowflake guildId); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/IWebhookCache.cs b/Catalogger.Backend/Cache/IWebhookCache.cs index 6d855f3..1191b73 100644 --- a/Catalogger.Backend/Cache/IWebhookCache.cs +++ b/Catalogger.Backend/Cache/IWebhookCache.cs @@ -10,13 +10,21 @@ public interface IWebhookCache Task GetWebhookAsync(ulong channelId); Task SetWebhookAsync(ulong channelId, Webhook webhook); - public async Task GetOrFetchWebhookAsync(ulong channelId, Func> fetch) + public async Task GetOrFetchWebhookAsync( + ulong channelId, + Func> fetch + ) { var webhook = await GetWebhookAsync(channelId); - if (webhook != null) return webhook.Value; + if (webhook != null) + return webhook.Value; var discordWebhook = await fetch(DiscordSnowflake.New(channelId)); - webhook = new Webhook { Id = discordWebhook.ID.ToUlong(), Token = discordWebhook.Token.Value }; + webhook = new Webhook + { + Id = discordWebhook.ID.ToUlong(), + Token = discordWebhook.Token.Value, + }; await SetWebhookAsync(channelId, webhook.Value); return webhook.Value; } @@ -26,4 +34,4 @@ public struct Webhook { public required ulong Id { get; init; } public required string Token { get; init; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/AuditLogCache.cs b/Catalogger.Backend/Cache/InMemoryCache/AuditLogCache.cs index be6786e..18f72ca 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/AuditLogCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/AuditLogCache.cs @@ -6,10 +6,21 @@ namespace Catalogger.Backend.Cache.InMemoryCache; public class AuditLogCache { - private readonly ConcurrentDictionary<(Snowflake GuildId, Snowflake TargetId), ActionData> _kicks = new(); - private readonly ConcurrentDictionary<(Snowflake GuildId, Snowflake TargetId), ActionData> _bans = new(); + private readonly ConcurrentDictionary< + (Snowflake GuildId, Snowflake TargetId), + ActionData + > _kicks = new(); + private readonly ConcurrentDictionary< + (Snowflake GuildId, Snowflake TargetId), + ActionData + > _bans = new(); - public void SetKick(Snowflake guildId, string targetId, Snowflake moderatorId, Optional reason) + public void SetKick( + Snowflake guildId, + string targetId, + Snowflake moderatorId, + Optional reason + ) { if (!DiscordSnowflake.TryParse(targetId, out var targetUser)) throw new CataloggerError("Target ID was not a valid snowflake"); @@ -20,16 +31,21 @@ public class AuditLogCache public bool TryGetKick(Snowflake guildId, Snowflake targetId, out ActionData data) => _kicks.TryGetValue((guildId, targetId), out data); - public void SetBan(Snowflake guildId, string targetId, Snowflake moderatorId, Optional reason) + public void SetBan( + Snowflake guildId, + string targetId, + Snowflake moderatorId, + Optional reason + ) { if (!DiscordSnowflake.TryParse(targetId, out var targetUser)) throw new CataloggerError("Target ID was not a valid snowflake"); _bans[(guildId, targetUser.Value)] = new ActionData(moderatorId, reason.OrDefault()); } - + public bool TryGetBan(Snowflake guildId, Snowflake targetId, out ActionData data) => _bans.TryGetValue((guildId, targetId), out data); public record struct ActionData(Snowflake ModeratorId, string? Reason); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/ChannelCache.cs b/Catalogger.Backend/Cache/InMemoryCache/ChannelCache.cs index dfc20de..56303e8 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/ChannelCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/ChannelCache.cs @@ -17,18 +17,21 @@ public class ChannelCache _channels[channel.ID] = channel; if (guildId == null) { - if (!channel.GuildID.TryGet(out var snowflake)) return; + if (!channel.GuildID.TryGet(out var snowflake)) + return; guildId = snowflake; } // Add to set of guild channels - _guildChannels.AddOrUpdate(guildId.Value, + _guildChannels.AddOrUpdate( + guildId.Value, _ => [channel.ID], (_, l) => { l.Add(channel.ID); return l; - }); + } + ); } public bool TryGet(Snowflake id, [NotNullWhen(true)] out IChannel? channel) => @@ -37,13 +40,18 @@ public class ChannelCache public void Remove(Snowflake? guildId, Snowflake id, out IChannel? channel) { _channels.Remove(id, out channel); - if (guildId == null) return; + if (guildId == null) + return; // Remove from set of guild channels - _guildChannels.AddOrUpdate(guildId.Value, _ => [], (_, s) => - { - s.Remove(id); - return s; - }); + _guildChannels.AddOrUpdate( + guildId.Value, + _ => [], + (_, s) => + { + s.Remove(id); + return s; + } + ); } /// @@ -54,6 +62,8 @@ public class ChannelCache public IEnumerable GuildChannels(Snowflake guildId) => !_guildChannels.TryGetValue(guildId, out var channelIds) ? [] - : channelIds.Select(id => _channels.GetValueOrDefault(id)) - .Where(c => c != null).Select(c => c!); -} \ No newline at end of file + : channelIds + .Select(id => _channels.GetValueOrDefault(id)) + .Where(c => c != null) + .Select(c => c!); +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/GuildCache.cs b/Catalogger.Backend/Cache/InMemoryCache/GuildCache.cs index 17d4e60..03ab8af 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/GuildCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/GuildCache.cs @@ -12,6 +12,10 @@ public class GuildCache public int Size => _guilds.Count; public void Set(IGuild guild) => _guilds[guild.ID] = guild; - public bool Remove(Snowflake id, [NotNullWhen(true)] out IGuild? guild) => _guilds.Remove(id, out guild); - public bool TryGet(Snowflake id, [NotNullWhen(true)] out IGuild? guild) => _guilds.TryGetValue(id, out guild); -} \ No newline at end of file + + public bool Remove(Snowflake id, [NotNullWhen(true)] out IGuild? guild) => + _guilds.Remove(id, out guild); + + public bool TryGet(Snowflake id, [NotNullWhen(true)] out IGuild? guild) => + _guilds.TryGetValue(id, out guild); +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/InMemoryInviteCache.cs b/Catalogger.Backend/Cache/InMemoryCache/InMemoryInviteCache.cs index 603c499..66c39f7 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/InMemoryInviteCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/InMemoryInviteCache.cs @@ -8,13 +8,14 @@ public class InMemoryInviteCache : IInviteCache { private readonly ConcurrentDictionary> _invites = new(); - public Task> TryGetAsync(Snowflake guildId) => _invites.TryGetValue(guildId, out var invites) - ? Task.FromResult(invites) - : Task.FromResult>([]); + public Task> TryGetAsync(Snowflake guildId) => + _invites.TryGetValue(guildId, out var invites) + ? Task.FromResult(invites) + : Task.FromResult>([]); public Task SetAsync(Snowflake guildId, IEnumerable invites) { _invites[guildId] = invites; return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/InMemoryMemberCache.cs b/Catalogger.Backend/Cache/InMemoryCache/InMemoryMemberCache.cs index 4eae1d0..d67d20b 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/InMemoryMemberCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/InMemoryMemberCache.cs @@ -19,7 +19,9 @@ public class InMemoryMemberCache : IMemberCache public Task SetAsync(Snowflake guildId, IGuildMember member) { if (!member.User.IsDefined()) - throw new CataloggerError("Member with undefined User passed to InMemoryMemberCache.SetAsync"); + throw new CataloggerError( + "Member with undefined User passed to InMemoryMemberCache.SetAsync" + ); _members[(guildId, member.User.Value.ID)] = member; return Task.CompletedTask; } @@ -36,7 +38,8 @@ public class InMemoryMemberCache : IMemberCache return Task.CompletedTask; } - public Task IsGuildCachedAsync(Snowflake guildId) => Task.FromResult(_guilds.ContainsKey(guildId)); + public Task IsGuildCachedAsync(Snowflake guildId) => + Task.FromResult(_guilds.ContainsKey(guildId)); public Task MarkAsCachedAsync(Snowflake guildId) { @@ -49,4 +52,4 @@ public class InMemoryMemberCache : IMemberCache _guilds.Remove(guildId, out _); return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/InMemoryWebhookCache.cs b/Catalogger.Backend/Cache/InMemoryCache/InMemoryWebhookCache.cs index d88d595..6a7d31a 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/InMemoryWebhookCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/InMemoryWebhookCache.cs @@ -18,4 +18,4 @@ public class InMemoryWebhookCache : IWebhookCache _cache[channelId] = webhook; return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/RoleCache.cs b/Catalogger.Backend/Cache/InMemoryCache/RoleCache.cs index de8732e..87943c2 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/RoleCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/RoleCache.cs @@ -16,13 +16,15 @@ public class RoleCache { _roles[role.ID] = role; // Add to set of guild channels - _guildRoles.AddOrUpdate(guildId, + _guildRoles.AddOrUpdate( + guildId, _ => [role.ID], (_, l) => { l.Add(role.ID); return l; - }); + } + ); } public bool TryGet(Snowflake id, [NotNullWhen(true)] out IRole? role) => @@ -32,16 +34,21 @@ public class RoleCache { _roles.Remove(id, out role); // Remove from set of guild channels - _guildRoles.AddOrUpdate(guildId, _ => [], (_, s) => - { - s.Remove(id); - return s; - }); + _guildRoles.AddOrUpdate( + guildId, + _ => [], + (_, s) => + { + s.Remove(id); + return s; + } + ); } public void RemoveGuild(Snowflake guildId) { - if (!_guildRoles.TryGetValue(guildId, out var roleIds)) return; + if (!_guildRoles.TryGetValue(guildId, out var roleIds)) + return; foreach (var id in roleIds) { _roles.Remove(id, out _); @@ -58,6 +65,8 @@ public class RoleCache public IEnumerable GuildRoles(Snowflake guildId) => !_guildRoles.TryGetValue(guildId, out var roleIds) ? [] - : roleIds.Select(id => _roles.GetValueOrDefault(id)) - .Where(r => r != null).Select(r => r!); -} \ No newline at end of file + : roleIds + .Select(id => _roles.GetValueOrDefault(id)) + .Where(r => r != null) + .Select(r => r!); +} diff --git a/Catalogger.Backend/Cache/InMemoryCache/UserCache.cs b/Catalogger.Backend/Cache/InMemoryCache/UserCache.cs index d12151e..8865c6a 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/UserCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/UserCache.cs @@ -13,13 +13,16 @@ public class UserCache(IDiscordRestUserAPI userApi) public int Size => _cacheSize; - 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; - }); + 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; + } + ); public void UpdateUser(IUser user) => _cache.Add(user.ID.ToString(), user); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/RedisCache/RedisInviteCache.cs b/Catalogger.Backend/Cache/RedisCache/RedisInviteCache.cs index 38c4814..95f71b6 100644 --- a/Catalogger.Backend/Cache/RedisCache/RedisInviteCache.cs +++ b/Catalogger.Backend/Cache/RedisCache/RedisInviteCache.cs @@ -10,7 +10,8 @@ public class RedisInviteCache(RedisService redisService) : IInviteCache { public async Task> TryGetAsync(Snowflake guildId) { - var redisInvites = await redisService.GetAsync>(InvitesKey(guildId)) ?? []; + var redisInvites = + await redisService.GetAsync>(InvitesKey(guildId)) ?? []; return redisInvites.Select(r => r.ToRemoraInvite()); } @@ -25,15 +26,26 @@ internal record RedisInvite( RedisPartialGuild? Guild, RedisPartialChannel? Channel, RedisUser? Inviter, - DateTimeOffset? ExpiresAt) + DateTimeOffset? ExpiresAt +) { - public static RedisInvite FromIInvite(IInvite invite) => new(invite.Code, - invite.Guild.Map(RedisPartialGuild.FromIPartialGuild).OrDefault(), - invite.Channel != null ? RedisPartialChannel.FromIPartialChannel(invite.Channel) : null, - invite.Inviter.Map(RedisUser.FromIUser).OrDefault(), invite.ExpiresAt.OrDefault()); + public static RedisInvite FromIInvite(IInvite invite) => + new( + invite.Code, + invite.Guild.Map(RedisPartialGuild.FromIPartialGuild).OrDefault(), + invite.Channel != null ? RedisPartialChannel.FromIPartialChannel(invite.Channel) : null, + invite.Inviter.Map(RedisUser.FromIUser).OrDefault(), + invite.ExpiresAt.OrDefault() + ); - public Invite ToRemoraInvite() => new(Code, Guild?.ToRemoraPartialGuild() ?? new Optional(), - Channel?.ToRemoraPartialChannel(), Inviter?.ToRemoraUser() ?? new Optional(), ExpiresAt: ExpiresAt); + public Invite ToRemoraInvite() => + new( + Code, + Guild?.ToRemoraPartialGuild() ?? new Optional(), + Channel?.ToRemoraPartialChannel(), + Inviter?.ToRemoraUser() ?? new Optional(), + ExpiresAt: ExpiresAt + ); } internal record RedisPartialGuild(ulong Id, string? Name) @@ -41,7 +53,8 @@ internal record RedisPartialGuild(ulong Id, string? Name) public static RedisPartialGuild FromIPartialGuild(IPartialGuild guild) => new(guild.ID.Value.Value, guild.Name.OrDefault(null)); - public PartialGuild ToRemoraPartialGuild() => new(DiscordSnowflake.New(Id), Name ?? new Optional()); + public PartialGuild ToRemoraPartialGuild() => + new(DiscordSnowflake.New(Id), Name ?? new Optional()); } internal record RedisPartialChannel(ulong Id, string? Name) @@ -50,4 +63,4 @@ internal record RedisPartialChannel(ulong Id, string? Name) new(channel.ID.Value.Value, channel.Name.OrDefault(null)); public PartialChannel ToRemoraPartialChannel() => new(DiscordSnowflake.New(Id), Name: Name); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs b/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs index 3856225..7fa45ff 100644 --- a/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs +++ b/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs @@ -10,29 +10,45 @@ public class RedisMemberCache(RedisService redisService) : IMemberCache { public async Task TryGetAsync(Snowflake guildId, Snowflake userId) { - var redisMember = await redisService.GetHashAsync(GuildMembersKey(guildId), userId.ToString()); + var redisMember = await redisService.GetHashAsync( + GuildMembersKey(guildId), + userId.ToString() + ); return redisMember?.ToRemoraMember(); } public async Task SetAsync(Snowflake guildId, IGuildMember member) { if (!member.User.IsDefined()) - throw new CataloggerError("Member with undefined User passed to RedisMemberCache.SetAsync"); - await redisService.SetHashAsync(GuildMembersKey(guildId), member.User.Value.ID.ToString(), - RedisMember.FromIGuildMember(member)); + throw new CataloggerError( + "Member with undefined User passed to RedisMemberCache.SetAsync" + ); + await redisService.SetHashAsync( + GuildMembersKey(guildId), + member.User.Value.ID.ToString(), + RedisMember.FromIGuildMember(member) + ); } public async Task SetManyAsync(Snowflake guildId, IReadOnlyList members) { if (members.Any(m => !m.User.IsDefined())) - throw new CataloggerError("Member with undefined User passed to RedisMemberCache.SetAsync"); + throw new CataloggerError( + "Member with undefined User passed to RedisMemberCache.SetAsync" + ); var redisMembers = members.Select(RedisMember.FromIGuildMember).ToList(); - await redisService.SetHashAsync(GuildMembersKey(guildId), redisMembers, m => m.User.Id.ToString()); + await redisService.SetHashAsync( + GuildMembersKey(guildId), + redisMembers, + m => m.User.Id.ToString() + ); } public async Task RemoveAsync(Snowflake guildId, Snowflake userId) => - await redisService.GetDatabase().HashDeleteAsync(GuildMembersKey(guildId), userId.ToString()); + await redisService + .GetDatabase() + .HashDeleteAsync(GuildMembersKey(guildId), userId.ToString()); public async Task IsGuildCachedAsync(Snowflake guildId) => await redisService.GetDatabase().SetContainsAsync(GuildCacheKey, guildId.ToString()); @@ -44,6 +60,7 @@ public class RedisMemberCache(RedisService redisService) : IMemberCache await redisService.GetDatabase().SetRemoveAsync(GuildCacheKey, guildId.ToString()); private const string GuildCacheKey = "cached-guilds"; + private static string GuildMembersKey(Snowflake guildId) => $"guild-members:{guildId}"; } @@ -56,16 +73,37 @@ internal record RedisMember( DateTimeOffset? PremiumSince, GuildMemberFlags Flags, bool? IsPending, - DateTimeOffset? CommunicationDisabledUntil) + DateTimeOffset? CommunicationDisabledUntil +) { - public static RedisMember FromIGuildMember(IGuildMember member) => new( - RedisUser.FromIUser(member.User.Value), member.Nickname.OrDefault(null), member.Avatar.OrDefault(null)?.Value, - member.Roles.ToArray(), member.JoinedAt, member.PremiumSince.OrDefault(null), member.Flags, - member.IsPending.OrDefault(null), member.CommunicationDisabledUntil.OrDefault(null)); + public static RedisMember FromIGuildMember(IGuildMember member) => + new( + RedisUser.FromIUser(member.User.Value), + member.Nickname.OrDefault(null), + member.Avatar.OrDefault(null)?.Value, + member.Roles.ToArray(), + member.JoinedAt, + member.PremiumSince.OrDefault(null), + member.Flags, + member.IsPending.OrDefault(null), + member.CommunicationDisabledUntil.OrDefault(null) + ); - public GuildMember ToRemoraMember() => new(User.ToRemoraUser(), Nickname, - Avatar != null ? new ImageHash(Avatar) : null, Roles, JoinedAt, PremiumSince, false, false, Flags, - IsPending, default, CommunicationDisabledUntil); + public GuildMember ToRemoraMember() => + new( + User.ToRemoraUser(), + Nickname, + Avatar != null ? new ImageHash(Avatar) : null, + Roles, + JoinedAt, + PremiumSince, + false, + false, + Flags, + IsPending, + default, + CommunicationDisabledUntil + ); } internal record RedisUser( @@ -76,13 +114,30 @@ internal record RedisUser( string? Avatar, bool IsBot, bool IsSystem, - string? Banner) + string? Banner +) { - public static RedisUser FromIUser(IUser user) => new(user.ID.Value, user.Username, user.Discriminator, - user.GlobalName.OrDefault(null), user.Avatar?.Value, user.IsBot.OrDefault(false), - user.IsSystem.OrDefault(false), user.Banner.OrDefault(null)?.Value); + public static RedisUser FromIUser(IUser user) => + new( + user.ID.Value, + user.Username, + user.Discriminator, + user.GlobalName.OrDefault(null), + user.Avatar?.Value, + user.IsBot.OrDefault(false), + user.IsSystem.OrDefault(false), + user.Banner.OrDefault(null)?.Value + ); - public User ToRemoraUser() => new(DiscordSnowflake.New(Id), Username, Discriminator, GlobalName, - Avatar != null ? new ImageHash(Avatar) : null, IsBot, IsSystem, - Banner: Banner != null ? new ImageHash(Banner) : null); -} \ No newline at end of file + public User ToRemoraUser() => + new( + DiscordSnowflake.New(Id), + Username, + Discriminator, + GlobalName, + Avatar != null ? new ImageHash(Avatar) : null, + IsBot, + IsSystem, + Banner: Banner != null ? new ImageHash(Banner) : null + ); +} diff --git a/Catalogger.Backend/Cache/RedisCache/RedisWebhookCache.cs b/Catalogger.Backend/Cache/RedisCache/RedisWebhookCache.cs index b11d9db..4c18c84 100644 --- a/Catalogger.Backend/Cache/RedisCache/RedisWebhookCache.cs +++ b/Catalogger.Backend/Cache/RedisCache/RedisWebhookCache.cs @@ -12,4 +12,4 @@ public class RedisWebhookCache(RedisService redisService) : IWebhookCache await redisService.SetAsync(WebhookKey(channelId), webhook, 24.Hours()); private static string WebhookKey(ulong channelId) => $"webhook:{channelId}"; -} \ No newline at end of file +} diff --git a/Catalogger.Backend/CataloggerError.cs b/Catalogger.Backend/CataloggerError.cs index f4c021d..f2b6a9d 100644 --- a/Catalogger.Backend/CataloggerError.cs +++ b/Catalogger.Backend/CataloggerError.cs @@ -1,5 +1,3 @@ namespace Catalogger.Backend; -public class CataloggerError(string message) : Exception(message) -{ -} \ No newline at end of file +public class CataloggerError(string message) : Exception(message) { } diff --git a/Catalogger.Backend/CataloggerMetrics.cs b/Catalogger.Backend/CataloggerMetrics.cs index 3c8edcc..b4128d9 100644 --- a/Catalogger.Backend/CataloggerMetrics.cs +++ b/Catalogger.Backend/CataloggerMetrics.cs @@ -7,25 +7,37 @@ public static class CataloggerMetrics { public static Instant Startup { get; set; } - public static readonly Gauge MessagesReceived = - Metrics.CreateGauge("catalogger_received_messages", "Number of messages Catalogger has received"); + public static readonly Gauge MessagesReceived = Metrics.CreateGauge( + "catalogger_received_messages", + "Number of messages Catalogger has received" + ); public static long MessageRateMinute { get; set; } - public static readonly Gauge GuildsCached = - Metrics.CreateGauge("catalogger_cache_guilds", "Number of guilds in the cache"); + public static readonly Gauge GuildsCached = Metrics.CreateGauge( + "catalogger_cache_guilds", + "Number of guilds in the cache" + ); - public static readonly Gauge ChannelsCached = - Metrics.CreateGauge("catalogger_cache_channels", "Number of channels in the cache"); + public static readonly Gauge ChannelsCached = Metrics.CreateGauge( + "catalogger_cache_channels", + "Number of channels in the cache" + ); - public static readonly Gauge UsersCached = - Metrics.CreateGauge("catalogger_cache_users", "Number of users in the cache"); + public static readonly Gauge UsersCached = Metrics.CreateGauge( + "catalogger_cache_users", + "Number of users in the cache" + ); - public static readonly Gauge MessagesStored = - Metrics.CreateGauge("catalogger_stored_messages", "Number of users in the cache"); + public static readonly Gauge MessagesStored = Metrics.CreateGauge( + "catalogger_stored_messages", + "Number of users in the cache" + ); - public static readonly Summary MetricsCollectionTime = - Metrics.CreateSummary("catalogger_time_metrics", "Time it took to collect metrics"); + public static readonly Summary MetricsCollectionTime = Metrics.CreateSummary( + "catalogger_time_metrics", + "Time it took to collect metrics" + ); public static Gauge ProcessPhysicalMemory => Metrics.CreateGauge("catalogger_process_physical_memory", "Process physical memory"); @@ -36,7 +48,9 @@ public static class CataloggerMetrics public static Gauge ProcessPrivateMemory => Metrics.CreateGauge("catalogger_process_private_memory", "Process private memory"); - public static Gauge ProcessThreads => Metrics.CreateGauge("catalogger_process_threads", "Process thread count"); + public static Gauge ProcessThreads => + Metrics.CreateGauge("catalogger_process_threads", "Process thread count"); - public static Gauge ProcessHandles => Metrics.CreateGauge("catalogger_process_handles", "Process handle count"); -} \ No newline at end of file + public static Gauge ProcessHandles => + Metrics.CreateGauge("catalogger_process_handles", "Process handle count"); +} diff --git a/Catalogger.Backend/Config.cs b/Catalogger.Backend/Config.cs index cc65037..af27715 100644 --- a/Catalogger.Backend/Config.cs +++ b/Catalogger.Backend/Config.cs @@ -44,4 +44,4 @@ public class Config public string BaseUrl { get; init; } = null!; public string Address => $"http://{Host}:{Port}"; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Database/DatabaseContext.cs b/Catalogger.Backend/Database/DatabaseContext.cs index 3126782..d5cdfd9 100644 --- a/Catalogger.Backend/Database/DatabaseContext.cs +++ b/Catalogger.Backend/Database/DatabaseContext.cs @@ -30,18 +30,17 @@ public class DatabaseContext : DbContext }.ConnectionString; var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); - dataSourceBuilder - .EnableDynamicJson() - .UseNodaTime(); + dataSourceBuilder.EnableDynamicJson().UseNodaTime(); _dataSource = dataSourceBuilder.Build(); _loggerFactory = loggerFactory; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => + optionsBuilder .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning) - .Ignore(CoreEventId.SaveChangesFailed)) + .Ignore(CoreEventId.SaveChangesFailed) + ) .UseNpgsql(_dataSource, o => o.UseNodaTime()) .UseSnakeCaseNamingConvention() .UseLoggerFactory(_loggerFactory) @@ -53,14 +52,17 @@ public class DatabaseContext : DbContext configurationBuilder.Properties>().HaveConversion(); } - private static readonly ValueComparer> UlongListValueComparer = new( - (c1, c2) => c1 != null && c2 != null && c1.SequenceEqual(c2), - c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())) - ); + private static readonly ValueComparer> UlongListValueComparer = + new( + (c1, c2) => c1 != null && c2 != null && c1.SequenceEqual(c2), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())) + ); protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity().Property(g => g.KeyRoles) + modelBuilder + .Entity() + .Property(g => g.KeyRoles) .Metadata.SetValueComparer(UlongListValueComparer); modelBuilder.Entity().HasKey(i => i.Code); @@ -76,22 +78,25 @@ public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory() ?? new(); + var config = + new ConfigurationBuilder() + .AddConfiguration() + .Build() + // Get the configuration as our config class + .Get() ?? new(); return new DatabaseContext(config, null); } } -public class UlongValueConverter() : ValueConverter( - convertToProviderExpression: x => (long)x, - convertFromProviderExpression: x => (ulong)x -); +public class UlongValueConverter() + : ValueConverter( + convertToProviderExpression: x => (long)x, + convertFromProviderExpression: x => (ulong)x + ); -public class UlongArrayValueConverter() : ValueConverter, List>( - convertToProviderExpression: x => x.Select(i => (long)i).ToList(), - convertFromProviderExpression: x => x.Select(i => (ulong)i).ToList() -); \ No newline at end of file +public class UlongArrayValueConverter() + : ValueConverter, List>( + convertToProviderExpression: x => x.Select(i => (long)i).ToList(), + convertFromProviderExpression: x => x.Select(i => (ulong)i).ToList() + ); diff --git a/Catalogger.Backend/Database/EncryptionService.cs b/Catalogger.Backend/Database/EncryptionService.cs index 7cfd908..3b0475a 100644 --- a/Catalogger.Backend/Database/EncryptionService.cs +++ b/Catalogger.Backend/Database/EncryptionService.cs @@ -33,4 +33,4 @@ public class EncryptionService(Config config) : IEncryptionService return Encoding.UTF8.GetString(plaintext); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Database/IEncryptionService.cs b/Catalogger.Backend/Database/IEncryptionService.cs index df13477..0fba95e 100644 --- a/Catalogger.Backend/Database/IEncryptionService.cs +++ b/Catalogger.Backend/Database/IEncryptionService.cs @@ -4,4 +4,4 @@ public interface IEncryptionService { public byte[] Encrypt(string data); public string Decrypt(byte[] input); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Database/Migrations/20240803132306_Init.cs b/Catalogger.Backend/Database/Migrations/20240803132306_Init.cs index 7867acd..51c7218 100644 --- a/Catalogger.Backend/Database/Migrations/20240803132306_Init.cs +++ b/Catalogger.Backend/Database/Migrations/20240803132306_Init.cs @@ -20,23 +20,22 @@ namespace Catalogger.Backend.Database.Migrations id = table.Column(type: "bigint", nullable: false), channels = table.Column(type: "jsonb", nullable: false), banned_systems = table.Column>(type: "text[]", nullable: false), - key_roles = table.Column>(type: "bigint[]", nullable: false) + key_roles = table.Column>(type: "bigint[]", nullable: false), }, constraints: table => { table.PrimaryKey("pk_guilds", x => x.id); - }); + } + ); migrationBuilder.CreateTable( name: "ignored_messages", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - }, + columns: table => new { id = table.Column(type: "bigint", nullable: false) }, constraints: table => { table.PrimaryKey("pk_ignored_messages", x => x.id); - }); + } + ); migrationBuilder.CreateTable( name: "invites", @@ -44,12 +43,13 @@ namespace Catalogger.Backend.Database.Migrations { code = table.Column(type: "text", nullable: false), guild_id = table.Column(type: "bigint", nullable: false), - name = table.Column(type: "text", nullable: false) + name = table.Column(type: "text", nullable: false), }, constraints: table => { table.PrimaryKey("pk_invites", x => x.code); - }); + } + ); migrationBuilder.CreateTable( name: "messages", @@ -65,12 +65,13 @@ namespace Catalogger.Backend.Database.Migrations username = table.Column(type: "bytea", nullable: false), content = table.Column(type: "bytea", nullable: false), metadata = table.Column(type: "bytea", nullable: true), - attachment_size = table.Column(type: "integer", nullable: false) + attachment_size = table.Column(type: "integer", nullable: false), }, constraints: table => { table.PrimaryKey("pk_messages", x => x.id); - }); + } + ); migrationBuilder.CreateTable( name: "watchlists", @@ -78,38 +79,39 @@ namespace Catalogger.Backend.Database.Migrations { guild_id = table.Column(type: "bigint", nullable: false), user_id = table.Column(type: "bigint", nullable: false), - added_at = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), + added_at = table.Column( + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()" + ), moderator_id = table.Column(type: "bigint", nullable: false), - reason = table.Column(type: "text", nullable: false) + reason = table.Column(type: "text", nullable: false), }, constraints: table => { table.PrimaryKey("pk_watchlists", x => new { x.guild_id, x.user_id }); - }); + } + ); migrationBuilder.CreateIndex( name: "ix_invites_guild_id", table: "invites", - column: "guild_id"); + column: "guild_id" + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "guilds"); + migrationBuilder.DropTable(name: "guilds"); - migrationBuilder.DropTable( - name: "ignored_messages"); + migrationBuilder.DropTable(name: "ignored_messages"); - migrationBuilder.DropTable( - name: "invites"); + migrationBuilder.DropTable(name: "invites"); - migrationBuilder.DropTable( - name: "messages"); + migrationBuilder.DropTable(name: "messages"); - migrationBuilder.DropTable( - name: "watchlists"); + migrationBuilder.DropTable(name: "watchlists"); } } } diff --git a/Catalogger.Backend/Database/Models/Guild.cs b/Catalogger.Backend/Database/Models/Guild.cs index 5696a4b..4792cc2 100644 --- a/Catalogger.Backend/Database/Models/Guild.cs +++ b/Catalogger.Backend/Database/Models/Guild.cs @@ -9,18 +9,26 @@ public class Guild [DatabaseGenerated(DatabaseGeneratedOption.None)] public required ulong Id { get; init; } - [Column(TypeName = "jsonb")] public ChannelConfig Channels { get; init; } = new(); + [Column(TypeName = "jsonb")] + public ChannelConfig Channels { get; init; } = new(); public List BannedSystems { get; init; } = []; public List KeyRoles { get; init; } = []; public bool IsMessageIgnored(Snowflake channelId, Snowflake userId) { - if (Channels is { MessageDelete: 0, MessageUpdate: 0, MessageDeleteBulk: 0 } || - Channels.IgnoredChannels.Contains(channelId.ToUlong()) || - Channels.IgnoredUsers.Contains(userId.ToUlong())) return true; + if ( + Channels is { MessageDelete: 0, MessageUpdate: 0, MessageDeleteBulk: 0 } + || Channels.IgnoredChannels.Contains(channelId.ToUlong()) + || Channels.IgnoredUsers.Contains(userId.ToUlong()) + ) + return true; - if (Channels.IgnoredUsersPerChannel.TryGetValue(channelId.ToUlong(), - out var thisChannelIgnoredUsers)) + if ( + Channels.IgnoredUsersPerChannel.TryGetValue( + channelId.ToUlong(), + out var thisChannelIgnoredUsers + ) + ) return thisChannelIgnoredUsers.Contains(userId.ToUlong()); return false; @@ -56,4 +64,4 @@ public class Guild public ulong MessageDelete { get; set; } public ulong MessageDeleteBulk { get; set; } } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Database/Models/Invite.cs b/Catalogger.Backend/Database/Models/Invite.cs index 9f76b2c..ba3c49a 100644 --- a/Catalogger.Backend/Database/Models/Invite.cs +++ b/Catalogger.Backend/Database/Models/Invite.cs @@ -5,4 +5,4 @@ public class Invite public required ulong GuildId { get; init; } public required string Code { get; init; } public required string Name { get; set; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Database/Models/Message.cs b/Catalogger.Backend/Database/Models/Message.cs index 40b55e8..7e3e8b7 100644 --- a/Catalogger.Backend/Database/Models/Message.cs +++ b/Catalogger.Backend/Database/Models/Message.cs @@ -15,13 +15,16 @@ public class Message public string? Member { get; set; } public string? System { get; set; } - [Column("username")] public byte[] EncryptedUsername { get; set; } = []; - [Column("content")] public byte[] EncryptedContent { get; set; } = []; - [Column("metadata")] public byte[]? EncryptedMetadata { get; set; } + [Column("username")] + public byte[] EncryptedUsername { get; set; } = []; + + [Column("content")] + public byte[] EncryptedContent { get; set; } = []; + + [Column("metadata")] + public byte[]? EncryptedMetadata { get; set; } public int AttachmentSize { get; set; } = 0; } -public record IgnoredMessage( - [property: DatabaseGenerated(DatabaseGeneratedOption.None)] - ulong Id); \ No newline at end of file +public record IgnoredMessage([property: DatabaseGenerated(DatabaseGeneratedOption.None)] ulong Id); diff --git a/Catalogger.Backend/Database/Models/Watchlist.cs b/Catalogger.Backend/Database/Models/Watchlist.cs index 4f94adc..c81012d 100644 --- a/Catalogger.Backend/Database/Models/Watchlist.cs +++ b/Catalogger.Backend/Database/Models/Watchlist.cs @@ -10,4 +10,4 @@ public class Watchlist public required ulong ModeratorId { get; set; } public required string Reason { get; set; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Database/Queries/MessageRepository.cs b/Catalogger.Backend/Database/Queries/MessageRepository.cs index b6a6a25..9234660 100644 --- a/Catalogger.Backend/Database/Queries/MessageRepository.cs +++ b/Catalogger.Backend/Database/Queries/MessageRepository.cs @@ -6,7 +6,11 @@ using DbMessage = Catalogger.Backend.Database.Models.Message; namespace Catalogger.Backend.Database.Queries; -public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionService encryptionService) +public class MessageRepository( + ILogger logger, + DatabaseContext db, + IEncryptionService encryptionService +) { private readonly ILogger _logger = logger.ForContext(); @@ -14,8 +18,10 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe { _logger.Debug("Saving message {MessageId}", msg.ID); - var metadata = new Metadata(IsWebhook: msg.WebhookID.HasValue, - msg.Attachments.Select(a => new Attachment(a.Filename, a.Size, a.ContentType.Value))); + var metadata = new Metadata( + IsWebhook: msg.WebhookID.HasValue, + msg.Attachments.Select(a => new Attachment(a.Filename, a.Size, a.ContentType.Value)) + ); var dbMessage = new DbMessage { @@ -24,12 +30,22 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe ChannelId = msg.ChannelID.ToUlong(), GuildId = msg.GuildID.ToUlong(), - EncryptedContent = - await Task.Run( - () => encryptionService.Encrypt(string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content), ct), - EncryptedUsername = await Task.Run(() => encryptionService.Encrypt(msg.Author.Tag()), ct), - EncryptedMetadata = await Task.Run(() => encryptionService.Encrypt(JsonSerializer.Serialize(metadata)), ct), - AttachmentSize = msg.Attachments.Select(a => a.Size).Sum() + EncryptedContent = await Task.Run( + () => + encryptionService.Encrypt( + string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content + ), + ct + ), + EncryptedUsername = await Task.Run( + () => encryptionService.Encrypt(msg.Author.Tag()), + ct + ), + EncryptedMetadata = await Task.Run( + () => encryptionService.Encrypt(JsonSerializer.Serialize(metadata)), + ct + ), + AttachmentSize = msg.Attachments.Select(a => a.Size).Sum(), }; db.Add(dbMessage); @@ -56,17 +72,32 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe } else { - var metadata = new Metadata(IsWebhook: msg.WebhookID.HasValue, - msg.Attachments.Select(a => new Attachment(a.Filename, a.Size, a.ContentType.Value))); + var metadata = new Metadata( + IsWebhook: msg.WebhookID.HasValue, + msg.Attachments.Select(a => new Attachment(a.Filename, a.Size, a.ContentType.Value)) + ); var dbMsg = await db.Messages.FindAsync(msg.ID.Value); - if (dbMsg == null) throw new CataloggerError("Message was null despite HasProxyInfoAsync returning true"); + if (dbMsg == null) + throw new CataloggerError( + "Message was null despite HasProxyInfoAsync returning true" + ); dbMsg.EncryptedContent = await Task.Run( - () => encryptionService.Encrypt(string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content), ct); - dbMsg.EncryptedUsername = await Task.Run(() => encryptionService.Encrypt(msg.Author.Tag()), ct); - dbMsg.EncryptedMetadata = - await Task.Run(() => encryptionService.Encrypt(JsonSerializer.Serialize(metadata)), ct); + () => + encryptionService.Encrypt( + string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content + ), + ct + ); + dbMsg.EncryptedUsername = await Task.Run( + () => encryptionService.Encrypt(msg.Author.Tag()), + ct + ); + dbMsg.EncryptedMetadata = await Task.Run( + () => encryptionService.Encrypt(JsonSerializer.Serialize(metadata)), + ct + ); db.Update(dbMsg); await db.SaveChangesAsync(ct); @@ -80,17 +111,26 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe _logger.Debug("Retrieving message {MessageId}", id); var dbMsg = await db.Messages.FindAsync(id); - if (dbMsg == null) return null; + if (dbMsg == null) + return null; - return new Message(dbMsg.Id, dbMsg.OriginalId, dbMsg.UserId, dbMsg.ChannelId, dbMsg.GuildId, dbMsg.Member, + return new Message( + dbMsg.Id, + dbMsg.OriginalId, + dbMsg.UserId, + dbMsg.ChannelId, + dbMsg.GuildId, + dbMsg.Member, dbMsg.System, Username: await Task.Run(() => encryptionService.Decrypt(dbMsg.EncryptedUsername), ct), Content: await Task.Run(() => encryptionService.Decrypt(dbMsg.EncryptedContent), ct), Metadata: dbMsg.EncryptedMetadata != null ? JsonSerializer.Deserialize( - await Task.Run(() => encryptionService.Decrypt(dbMsg.EncryptedMetadata), ct)) + await Task.Run(() => encryptionService.Decrypt(dbMsg.EncryptedMetadata), ct) + ) : null, - dbMsg.AttachmentSize); + dbMsg.AttachmentSize + ); } /// @@ -101,12 +141,19 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe { _logger.Debug("Checking if message {MessageId} has proxy information", id); - var msg = await db.Messages.Select(m => new { m.Id, m.OriginalId }).FirstOrDefaultAsync(m => m.Id == id); + var msg = await db + .Messages.Select(m => new { m.Id, m.OriginalId }) + .FirstOrDefaultAsync(m => m.Id == id); return (msg != null, msg?.OriginalId != null); } - public async Task SetProxiedMessageDataAsync(ulong id, ulong originalId, ulong authorId, string? systemId, - string? memberId) + public async Task SetProxiedMessageDataAsync( + ulong id, + ulong originalId, + ulong authorId, + string? systemId, + string? memberId + ) { _logger.Debug("Setting proxy information for message {MessageId}", id); @@ -151,4 +198,4 @@ public class MessageRepository(ILogger logger, DatabaseContext db, IEncryptionSe public record Metadata(bool IsWebhook, IEnumerable Attachments); public record Attachment(string Filename, int Size, string ContentType); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Database/Queries/QueryExtensions.cs b/Catalogger.Backend/Database/Queries/QueryExtensions.cs index a59764d..7fdc539 100644 --- a/Catalogger.Backend/Database/Queries/QueryExtensions.cs +++ b/Catalogger.Backend/Database/Queries/QueryExtensions.cs @@ -6,21 +6,34 @@ namespace Catalogger.Backend.Database.Queries; public static class QueryExtensions { - public static async ValueTask GetGuildAsync(this DatabaseContext db, Snowflake id, - CancellationToken ct = default) => await db.GetGuildAsync(id.ToUlong(), ct); + public static async ValueTask GetGuildAsync( + this DatabaseContext db, + Snowflake id, + CancellationToken ct = default + ) => await db.GetGuildAsync(id.ToUlong(), ct); - public static async ValueTask GetGuildAsync(this DatabaseContext db, Optional id, - CancellationToken ct = default) => await db.GetGuildAsync(id.ToUlong(), ct); + public static async ValueTask GetGuildAsync( + this DatabaseContext db, + Optional id, + CancellationToken ct = default + ) => await db.GetGuildAsync(id.ToUlong(), ct); - public static async ValueTask GetGuildAsync(this DatabaseContext db, ulong id, - CancellationToken ct = default) + public static async ValueTask GetGuildAsync( + this DatabaseContext db, + ulong id, + CancellationToken ct = default + ) { var guild = await db.Guilds.FindAsync([id], ct); - if (guild == null) throw new CataloggerError("Guild not found, was not initialized during guild create"); + if (guild == null) + throw new CataloggerError("Guild not found, was not initialized during guild create"); return guild; } - public static async Task GetWatchlistEntryAsync(this DatabaseContext db, Snowflake guildId, - Snowflake userId, CancellationToken ct = default) => - await db.Watchlists.FindAsync([guildId.Value, userId.Value], ct); -} \ No newline at end of file + public static async Task GetWatchlistEntryAsync( + this DatabaseContext db, + Snowflake guildId, + Snowflake userId, + CancellationToken ct = default + ) => await db.Watchlists.FindAsync([guildId.Value, userId.Value], ct); +} diff --git a/Catalogger.Backend/Database/Redis/RedisService.cs b/Catalogger.Backend/Database/Redis/RedisService.cs index 058ec73..d44182f 100644 --- a/Catalogger.Backend/Database/Redis/RedisService.cs +++ b/Catalogger.Backend/Database/Redis/RedisService.cs @@ -5,12 +5,12 @@ namespace Catalogger.Backend.Database.Redis; public class RedisService(Config config) { - private readonly ConnectionMultiplexer _multiplexer = ConnectionMultiplexer.Connect(config.Database.Redis!); + private readonly ConnectionMultiplexer _multiplexer = ConnectionMultiplexer.Connect( + config.Database.Redis! + ); - private readonly JsonSerializerOptions _options = new() - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower - }; + private readonly JsonSerializerOptions _options = + new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; public IDatabase GetDatabase(int db = -1) => _multiplexer.GetDatabase(db); @@ -32,10 +32,18 @@ public class RedisService(Config config) await GetDatabase().HashSetAsync(hashKey, fieldKey, json); } - public async Task SetHashAsync(string hashKey, IEnumerable values, Func keySelector) + public async Task SetHashAsync( + string hashKey, + IEnumerable values, + Func keySelector + ) { var hashEntries = values - .Select(v => new { Key = keySelector(v), Value = JsonSerializer.Serialize(v, _options) }) + .Select(v => new + { + Key = keySelector(v), + Value = JsonSerializer.Serialize(v, _options), + }) .Select(v => new HashEntry(v.Key, v.Value)); await GetDatabase().HashSetAsync(hashKey, hashEntries.ToArray()); } @@ -45,4 +53,4 @@ public class RedisService(Config config) var value = await GetDatabase().HashGetAsync(hashKey, fieldKey); return value.IsNull ? default : JsonSerializer.Deserialize(value!, _options); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Extensions/DiscordExtensions.cs b/Catalogger.Backend/Extensions/DiscordExtensions.cs index 5be95c5..6779e52 100644 --- a/Catalogger.Backend/Extensions/DiscordExtensions.cs +++ b/Catalogger.Backend/Extensions/DiscordExtensions.cs @@ -17,7 +17,9 @@ public static class DiscordExtensions public static string Tag(this IPartialUser user) { var discriminator = user.Discriminator.OrDefault(); - return discriminator == 0 ? user.Username.Value : $"{user.Username.Value}#{discriminator:0000}"; + return discriminator == 0 + ? user.Username.Value + : $"{user.Username.Value}#{discriminator:0000}"; } public static string AvatarUrl(this IUser user, int size = 256) @@ -28,13 +30,15 @@ public static class DiscordExtensions return $"https://cdn.discordapp.com/avatars/{user.ID}/{user.Avatar.Value}{ext}?size={size}"; } - var avatarIndex = user.Discriminator == 0 ? (int)((user.ID.Value >> 22) % 6) : user.Discriminator % 5; + var avatarIndex = + user.Discriminator == 0 ? (int)((user.ID.Value >> 22) % 6) : user.Discriminator % 5; return $"https://cdn.discordapp.com/embed/avatars/{avatarIndex}.png?size={size}"; } public static string? IconUrl(this IGuild guild, int size = 256) { - if (guild.Icon == null) return null; + if (guild.Icon == null) + return null; var ext = guild.Icon.HasGif ? ".gif" : ".webp"; @@ -45,7 +49,8 @@ public static class DiscordExtensions public static ulong ToUlong(this Optional snowflake) { - if (!snowflake.IsDefined()) throw new Exception("ToUlong called on an undefined Snowflake"); + if (!snowflake.IsDefined()) + throw new Exception("ToUlong called on an undefined Snowflake"); return snowflake.Value.Value; } @@ -58,40 +63,64 @@ public static class DiscordExtensions return $"#{r}{g}{b}"; } - public static bool Is(this Optional s1, Snowflake s2) => s1.IsDefined(out var value) && value == s2; - public static bool Is(this Optional s1, ulong s2) => s1.IsDefined(out var value) && value == s2; + public static bool Is(this Optional s1, Snowflake s2) => + s1.IsDefined(out var value) && value == s2; + + public static bool Is(this Optional s1, ulong s2) => + s1.IsDefined(out var value) && value == s2; public static T GetOrThrow(this Result result) { - if (result.Error != null) throw new DiscordRestException(result.Error.Message); + if (result.Error != null) + throw new DiscordRestException(result.Error.Message); return result.Entity; } public static T GetOrThrow(this Optional optional) => optional.OrThrow(() => new CataloggerError("Optional was unset")); - public static async Task GetOrThrow(this Task> result) => (await result).GetOrThrow(); + public static async Task GetOrThrow(this Task> result) => + (await result).GetOrThrow(); - public static async Task UpdateMessageAsync(this IDiscordRestInteractionAPI interactionApi, - IInteraction interaction, InteractionMessageCallbackData data) => - await interactionApi.CreateInteractionResponseAsync(interaction.ID, + public static async Task UpdateMessageAsync( + this IDiscordRestInteractionAPI interactionApi, + IInteraction interaction, + InteractionMessageCallbackData data + ) => + await interactionApi.CreateInteractionResponseAsync( + interaction.ID, interaction.Token, - new InteractionResponse(InteractionCallbackType.UpdateMessage, - new Optional>(data))); + new InteractionResponse( + InteractionCallbackType.UpdateMessage, + new Optional< + OneOf< + IInteractionMessageCallbackData, + IInteractionAutocompleteCallbackData, + IInteractionModalCallbackData + > + >(data) + ) + ); public static string ToPrettyString(this IDiscordPermissionSet permissionSet) => - string.Join(", ", permissionSet.GetPermissions().Select(p => p.Humanize(LetterCasing.Title))); + string.Join( + ", ", + permissionSet.GetPermissions().Select(p => p.Humanize(LetterCasing.Title)) + ); - public static (Snowflake, Snowflake) GetUserAndGuild(this ContextInjectionService contextInjectionService) + public static (Snowflake, Snowflake) GetUserAndGuild( + this ContextInjectionService contextInjectionService + ) { if (contextInjectionService.Context is not IInteractionCommandContext ctx) throw new CataloggerError("No context"); - if (!ctx.TryGetUserID(out var userId)) throw new CataloggerError("No user ID in context"); - if (!ctx.TryGetGuildID(out var guildId)) throw new CataloggerError("No guild ID in context"); + if (!ctx.TryGetUserID(out var userId)) + throw new CataloggerError("No user ID in context"); + if (!ctx.TryGetGuildID(out var guildId)) + throw new CataloggerError("No guild ID in context"); return (userId, guildId); } - + /// /// Sorts a list of roles by their position in the Discord interface. /// @@ -99,12 +128,14 @@ public static class DiscordExtensions /// An optional list of role IDs to return, from a member object or similar. /// If null, the entire list is returned. /// - public static IEnumerable Sorted(this IEnumerable roles, - IEnumerable? filterByIds = null) + public static IEnumerable Sorted( + this IEnumerable roles, + IEnumerable? filterByIds = null + ) { var sorted = roles.OrderByDescending(r => r.Position); return filterByIds != null ? sorted.Where(r => filterByIds.Contains(r.ID)) : sorted; } public class DiscordRestException(string message) : Exception(message); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs index 3c1b772..cc69d6a 100644 --- a/Catalogger.Backend/Extensions/StartupExtensions.cs +++ b/Catalogger.Backend/Extensions/StartupExtensions.cs @@ -27,7 +27,10 @@ public static class StartupExtensions /// /// Adds Serilog to this service collection. This method also initializes Serilog, so it should be called as early as possible, before any log calls. /// - public static WebApplicationBuilder AddSerilog(this WebApplicationBuilder builder, Config config) + public static WebApplicationBuilder AddSerilog( + this WebApplicationBuilder builder, + Config config + ) { var logCfg = new LoggerConfiguration() .Enrich.FromLogContext() @@ -35,8 +38,10 @@ public static class StartupExtensions // ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead. // Serilog doesn't disable the built-in logs, so we do it here. .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", - config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Fatal) + .MinimumLevel.Override( + "Microsoft.EntityFrameworkCore.Database.Command", + config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Fatal + ) .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) @@ -69,32 +74,43 @@ public static class StartupExtensions .AddEnvironmentVariables(); } - public static IServiceCollection AddCustomServices(this IServiceCollection services) => services - .AddSingleton(SystemClock.Instance) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddScoped() - .AddSingleton() - .AddSingleton() - .AddScoped() - .AddSingleton() - .AddSingleton() - .AddSingleton(InMemoryDataService.Instance) - .AddSingleton() - .AddHostedService(serviceProvider => serviceProvider.GetRequiredService()); + public static IServiceCollection AddCustomServices(this IServiceCollection services) => + services + .AddSingleton(SystemClock.Instance) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddScoped() + .AddSingleton() + .AddSingleton() + .AddScoped() + .AddSingleton() + .AddSingleton() + .AddSingleton(InMemoryDataService.Instance) + .AddSingleton() + .AddHostedService(serviceProvider => + serviceProvider.GetRequiredService() + ); - public static IHostBuilder AddShardedDiscordService(this IHostBuilder builder, - Func tokenFactory) => - builder.ConfigureServices((_, services) => services - .AddDiscordGateway(tokenFactory) - .AddSingleton() - .AddHostedService()); + public static IHostBuilder AddShardedDiscordService( + this IHostBuilder builder, + Func tokenFactory + ) => + builder.ConfigureServices( + (_, services) => + services + .AddDiscordGateway(tokenFactory) + .AddSingleton() + .AddHostedService() + ); - public static IServiceCollection MaybeAddRedisCaches(this IServiceCollection services, Config config) + public static IServiceCollection MaybeAddRedisCaches( + this IServiceCollection services, + Config config + ) { if (config.Database.Redis == null) { @@ -104,7 +120,8 @@ public static class StartupExtensions .AddSingleton(); } - return services.AddSingleton() + return services + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); @@ -116,7 +133,9 @@ public static class StartupExtensions var logger = scope.ServiceProvider.GetRequiredService().ForContext(); logger.Information("Starting Catalogger.NET"); - CataloggerMetrics.Startup = scope.ServiceProvider.GetRequiredService().GetCurrentInstant(); + CataloggerMetrics.Startup = scope + .ServiceProvider.GetRequiredService() + .GetCurrentInstant(); await using (var db = scope.ServiceProvider.GetRequiredService()) { @@ -126,7 +145,8 @@ public static class StartupExtensions logger.Information("Applying {Count} database migrations", migrationCount); await db.Database.MigrateAsync(); } - else logger.Information("There are no pending migrations"); + else + logger.Information("There are no pending migrations"); } var config = scope.ServiceProvider.GetRequiredService(); @@ -135,20 +155,28 @@ public static class StartupExtensions if (config.Discord.ApplicationId == 0) { logger.Warning( - "Application ID not set in config. Fetching and setting it now, but for future restarts, please add it to config.ini as Discord.ApplicationId."); + "Application ID not set in config. Fetching and setting it now, but for future restarts, please add it to config.ini as Discord.ApplicationId." + ); var restApi = scope.ServiceProvider.GetRequiredService(); var application = await restApi.GetCurrentApplicationAsync().GetOrThrow(); config.Discord.ApplicationId = application.ID.ToUlong(); - logger.Information("Current application ID is {ApplicationId}", config.Discord.ApplicationId); + logger.Information( + "Current application ID is {ApplicationId}", + config.Discord.ApplicationId + ); } if (config.Discord.SyncCommands) { if (config.Discord.CommandsGuildId != null) { - logger.Information("Syncing application commands with guild {GuildId}", config.Discord.CommandsGuildId); + logger.Information( + "Syncing application commands with guild {GuildId}", + config.Discord.CommandsGuildId + ); await slashService.UpdateSlashCommandsAsync( - guildID: DiscordSnowflake.New(config.Discord.CommandsGuildId.Value)); + guildID: DiscordSnowflake.New(config.Discord.CommandsGuildId.Value) + ); } else { @@ -156,6 +184,9 @@ public static class StartupExtensions await slashService.UpdateSlashCommandsAsync(); } } - else logger.Information("Not syncing slash commands, Discord.SyncCommands is false or unset"); + else + logger.Information( + "Not syncing slash commands, Discord.SyncCommands is false or unset" + ); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Extensions/TimeExtensions.cs b/Catalogger.Backend/Extensions/TimeExtensions.cs index 5721b88..fd5bc35 100644 --- a/Catalogger.Backend/Extensions/TimeExtensions.cs +++ b/Catalogger.Backend/Extensions/TimeExtensions.cs @@ -12,9 +12,11 @@ public static class TimeExtensions public static string Prettify(this Duration duration, TimeUnit minUnit = TimeUnit.Minute) => duration.ToTimeSpan().Prettify(minUnit); - public static string Prettify(this DateTimeOffset datetime, TimeUnit minUnit = TimeUnit.Minute) => - (datetime - DateTimeOffset.Now).Prettify(minUnit); + public static string Prettify( + this DateTimeOffset datetime, + TimeUnit minUnit = TimeUnit.Minute + ) => (datetime - DateTimeOffset.Now).Prettify(minUnit); public static string Prettify(this Instant instant, TimeUnit minUnit = TimeUnit.Minute) => (instant - SystemClock.Instance.GetCurrentInstant()).Prettify(minUnit); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/GlobalUsing.cs b/Catalogger.Backend/GlobalUsing.cs index bcd04ee..dd1fe48 100644 --- a/Catalogger.Backend/GlobalUsing.cs +++ b/Catalogger.Backend/GlobalUsing.cs @@ -1 +1 @@ -global using ILogger = Serilog.ILogger; \ No newline at end of file +global using ILogger = Serilog.ILogger; diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index 91a9fbd..c69d272 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -18,27 +18,30 @@ var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration(); builder.AddSerilog(config); -builder.Services - .AddControllers() - .AddNewtonsoftJson(o => o.SerializerSettings.ContractResolver = - new DefaultContractResolver +builder + .Services.AddControllers() + .AddNewtonsoftJson(o => + o.SerializerSettings.ContractResolver = new DefaultContractResolver { - NamingStrategy = new SnakeCaseNamingStrategy() - }); + NamingStrategy = new SnakeCaseNamingStrategy(), + } + ); -builder.Host - .AddShardedDiscordService(_ => config.Discord.Token) +builder + .Host.AddShardedDiscordService(_ => config.Discord.Token) .ConfigureServices(s => s.AddRespondersFromAssembly(typeof(Program).Assembly) .Configure(g => - g.Intents = GatewayIntents.Guilds | - GatewayIntents.GuildBans | - GatewayIntents.GuildInvites | - GatewayIntents.GuildMembers | - GatewayIntents.GuildMessages | - GatewayIntents.GuildWebhooks | - GatewayIntents.MessageContents | - GatewayIntents.GuildEmojisAndStickers) + g.Intents = + GatewayIntents.Guilds + | GatewayIntents.GuildBans + | GatewayIntents.GuildInvites + | GatewayIntents.GuildMembers + | GatewayIntents.GuildMessages + | GatewayIntents.GuildWebhooks + | GatewayIntents.MessageContents + | GatewayIntents.GuildEmojisAndStickers + ) .AddDiscordCommands(enableSlash: true, useDefaultCommandResponder: false) .AddCommandTree() // Start command tree @@ -59,8 +62,8 @@ builder.Services.AddMetricServer(o => o.Port = (ushort)config.Logging.MetricsPor if (!config.Logging.EnableMetrics) builder.Services.AddHostedService(); -builder.Services - .AddDbContext() +builder + .Services.AddDbContext() .MaybeAddRedisCaches(config) .AddCustomServices() .AddEndpointsApiExplorer() @@ -83,7 +86,8 @@ app.Urls.Add(config.Web.Address); // Make sure metrics are updated whenever Prometheus scrapes them Metrics.DefaultRegistry.AddBeforeCollectCallback(async ct => - await app.Services.GetRequiredService().CollectMetricsAsync(ct)); + await app.Services.GetRequiredService().CollectMetricsAsync(ct) +); app.Run(); -Log.CloseAndFlush(); \ No newline at end of file +Log.CloseAndFlush(); diff --git a/Catalogger.Backend/Services/AuditLogEnrichedResponderService.cs b/Catalogger.Backend/Services/AuditLogEnrichedResponderService.cs index 780ad9e..1d90af2 100644 --- a/Catalogger.Backend/Services/AuditLogEnrichedResponderService.cs +++ b/Catalogger.Backend/Services/AuditLogEnrichedResponderService.cs @@ -19,19 +19,28 @@ public class AuditLogEnrichedResponderService(AuditLogCache auditLogCache, ILogg if (auditLogCache.TryGetBan(evt.GuildID, evt.User.ID, out var banData)) return await HandleBanAsync(evt, banData); - _logger.Debug("Guild member remove event for guild {GuildId}/user {UserId} didn't match an audit log entry", - evt.GuildID, evt.User.ID); + _logger.Debug( + "Guild member remove event for guild {GuildId}/user {UserId} didn't match an audit log entry", + evt.GuildID, + evt.User.ID + ); return Result.Success; } - private async Task HandleKickAsync(IGuildMemberRemove evt, AuditLogCache.ActionData kickData) + private async Task HandleKickAsync( + IGuildMemberRemove evt, + AuditLogCache.ActionData kickData + ) { return Result.Success; } - private async Task HandleBanAsync(IGuildMemberRemove evt, AuditLogCache.ActionData banData) + private async Task HandleBanAsync( + IGuildMemberRemove evt, + AuditLogCache.ActionData banData + ) { return Result.Success; } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Services/GuildFetchService.cs b/Catalogger.Backend/Services/GuildFetchService.cs index ee3c084..f92074d 100644 --- a/Catalogger.Backend/Services/GuildFetchService.cs +++ b/Catalogger.Backend/Services/GuildFetchService.cs @@ -13,7 +13,8 @@ public class GuildFetchService( ILogger logger, ShardedGatewayClient client, IDiscordRestGuildAPI guildApi, - IInviteCache inviteCache) : BackgroundService + IInviteCache inviteCache +) : BackgroundService { private readonly ILogger _logger = logger.ForContext(); private readonly ConcurrentQueue _guilds = new(); @@ -23,7 +24,8 @@ public class GuildFetchService( using var timer = new PeriodicTimer(500.Milliseconds()); while (await timer.WaitForNextTickAsync(stoppingToken)) { - if (!_guilds.TryPeek(out var guildId)) continue; + if (!_guilds.TryPeek(out var guildId)) + continue; _logger.Debug("Fetching members and invites for guild {GuildId}", guildId); client.ClientFor(guildId).SubmitCommand(new RequestGuildMembers(guildId, "", 0)); @@ -43,6 +45,7 @@ public class GuildFetchService( public void EnqueueGuild(Snowflake guildId) { - if (!_guilds.Contains(guildId)) _guilds.Enqueue(guildId); + if (!_guilds.Contains(guildId)) + _guilds.Enqueue(guildId); } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Services/MetricsCollectionService.cs b/Catalogger.Backend/Services/MetricsCollectionService.cs index fb42016..072f411 100644 --- a/Catalogger.Backend/Services/MetricsCollectionService.cs +++ b/Catalogger.Backend/Services/MetricsCollectionService.cs @@ -12,7 +12,8 @@ public class MetricsCollectionService( GuildCache guildCache, ChannelCache channelCache, UserCache userCache, - IServiceProvider services) + IServiceProvider services +) { private readonly ILogger _logger = logger.ForContext(); @@ -42,7 +43,10 @@ public class MetricsCollectionService( } } -public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectionService innerService) : BackgroundService +public class BackgroundMetricsCollectionService( + ILogger logger, + MetricsCollectionService innerService +) : BackgroundService { private readonly ILogger _logger = logger.ForContext(); @@ -57,4 +61,4 @@ public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectio await innerService.CollectMetricsAsync(ct); } } -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Services/PluralkitApiService.cs b/Catalogger.Backend/Services/PluralkitApiService.cs index 8b92fda..5bc7702 100644 --- a/Catalogger.Backend/Services/PluralkitApiService.cs +++ b/Catalogger.Backend/Services/PluralkitApiService.cs @@ -17,17 +17,25 @@ public class PluralkitApiService(ILogger logger) private readonly ILogger _logger = logger.ForContext(); private readonly ResiliencePipeline _pipeline = new ResiliencePipelineBuilder() - .AddRateLimiter(new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions() - { - Window = 1.Seconds(), - PermitLimit = 2, - QueueLimit = 64, - })) + .AddRateLimiter( + new FixedWindowRateLimiter( + new FixedWindowRateLimiterOptions() + { + Window = 1.Seconds(), + PermitLimit = 2, + QueueLimit = 64, + } + ) + ) .AddTimeout(20.Seconds()) .Build(); - private async Task DoRequestAsync(string path, bool allowNotFound = false, - CancellationToken ct = default) where T : class + private async Task DoRequestAsync( + string path, + bool allowNotFound = false, + CancellationToken ct = default + ) + where T : class { var req = new HttpRequestMessage(HttpMethod.Get, $"{ApiBaseUrl}{path}"); req.Headers.Add("User-Agent", UserAgent); @@ -43,27 +51,37 @@ public class PluralkitApiService(ILogger logger) if (!resp.IsSuccessStatusCode) { - _logger.Error("Received non-200 status code {StatusCode} from PluralKit API path {Path}", resp.StatusCode, - req); + _logger.Error( + "Received non-200 status code {StatusCode} from PluralKit API path {Path}", + resp.StatusCode, + req + ); throw new CataloggerError("Non-200 status code from PluralKit API"); } var jsonOptions = new JsonSerializerOptions - { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower } - .ConfigureForNodaTime(new NodaJsonSettings + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }.ConfigureForNodaTime( + new NodaJsonSettings { - InstantConverter = new NodaPatternConverter(InstantPattern.ExtendedIso) - }); + InstantConverter = new NodaPatternConverter(InstantPattern.ExtendedIso), + } + ); - return await resp.Content.ReadFromJsonAsync(jsonOptions, ct) ?? - throw new CataloggerError("JSON response from PluralKit API was null"); + return await resp.Content.ReadFromJsonAsync(jsonOptions, ct) + ?? throw new CataloggerError("JSON response from PluralKit API was null"); } - public async Task GetPluralKitMessageAsync(ulong id, CancellationToken ct = default) => - await DoRequestAsync($"/messages/{id}", allowNotFound: true, ct); + public async Task GetPluralKitMessageAsync( + ulong id, + CancellationToken ct = default + ) => await DoRequestAsync($"/messages/{id}", allowNotFound: true, ct); - public async Task GetPluralKitSystemAsync(ulong id, CancellationToken ct = default) => - await DoRequestAsync($"/systems/{id}", allowNotFound: true, ct); + public async Task GetPluralKitSystemAsync( + ulong id, + CancellationToken ct = default + ) => await DoRequestAsync($"/systems/{id}", allowNotFound: true, ct); public record PkMessage( ulong Id, @@ -72,9 +90,10 @@ public class PluralkitApiService(ILogger logger) ulong Channel, ulong Guild, PkSystem? System, - PkMember? Member); + PkMember? Member + ); public record PkSystem(string Id, Guid Uuid, string? Name, string? Tag, Instant? Created); public record PkMember(string Id, Guid Uuid, string Name, string? DisplayName); -} \ No newline at end of file +} diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index 556b7ab..567f863 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -16,7 +16,8 @@ public class WebhookExecutorService( ILogger logger, IWebhookCache webhookCache, ChannelCache channelCache, - IDiscordRestWebhookAPI webhookApi) + IDiscordRestWebhookAPI webhookApi +) { private readonly ILogger _logger = logger.ForContext(); private readonly Snowflake _applicationId = DiscordSnowflake.New(config.Discord.ApplicationId); @@ -35,7 +36,8 @@ public class WebhookExecutorService( public void QueueLog(Guild guildConfig, LogChannelType logChannelType, IEmbed embed) { var logChannel = GetLogChannel(guildConfig, logChannelType, channelId: null, userId: null); - if (logChannel == null) return; + if (logChannel == null) + return; QueueLog(logChannel.Value, embed); } @@ -45,7 +47,8 @@ public class WebhookExecutorService( /// public void QueueLog(ulong channelId, IEmbed embed) { - if (channelId == 0) return; + if (channelId == 0) + return; var queue = _cache.GetOrAdd(channelId, []); queue.Enqueue(embed); @@ -60,40 +63,65 @@ public class WebhookExecutorService( /// 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) + public async Task SendLogAsync( + ulong channelId, + List embeds, + IEnumerable files + ) { - if (channelId == 0) return; + 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); + _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()); + 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. + /// 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 existingTimer)) + existingTimer.Dispose(); + _timers[channelId] = new Timer( + _ => { - if (_timers.TryGetValue(channelId, out var timer)) timer.Dispose(); - SetTimer(channelId, queue); - } - }, null, 3000, Timeout.Infinite); + _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 + ); } /// @@ -109,7 +137,8 @@ public class WebhookExecutorService( var embeds = new List(); for (var i = 0; i < 5; i++) { - if (!queue.TryDequeue(out var embed)) break; + if (!queue.TryDequeue(out var embed)) + break; embeds.Add(embed); } @@ -118,25 +147,48 @@ public class WebhookExecutorService( } // 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) + private async Task FetchWebhookAsync( + Snowflake channelId, + CancellationToken ct = default + ) { - var channelWebhooks = - await webhookApi.GetChannelWebhooksAsync(channelId, ct).GetOrThrow(); - var webhook = channelWebhooks.FirstOrDefault(w => w.ApplicationID == _applicationId && w.Token.IsDefined()); - if (webhook != null) return webhook; + var channelWebhooks = await webhookApi.GetChannelWebhooksAsync(channelId, ct).GetOrThrow(); + var webhook = channelWebhooks.FirstOrDefault(w => + w.ApplicationID == _applicationId && w.Token.IsDefined() + ); + if (webhook != null) + return webhook; - return await webhookApi.CreateWebhookAsync(channelId, "Catalogger", default, reason: "Creating logging webhook", - ct: ct).GetOrThrow(); + return await webhookApi + .CreateWebhookAsync( + channelId, + "Catalogger", + default, + reason: "Creating logging webhook", + ct: ct + ) + .GetOrThrow(); } - public ulong? GetLogChannel(Guild guild, LogChannelType logChannelType, Snowflake? channelId = null, - ulong? userId = null) + public ulong? GetLogChannel( + Guild guild, + LogChannelType logChannelType, + Snowflake? channelId = null, + ulong? userId = null + ) { - if (channelId == null) return GetDefaultLogChannel(guild, logChannelType); - if (!channelCache.TryGet(channelId.Value, out var channel)) return null; + if (channelId == null) + return GetDefaultLogChannel(guild, logChannelType); + if (!channelCache.TryGet(channelId.Value, out var channel)) + return null; Snowflake? categoryId; - if (channel.Type is ChannelType.AnnouncementThread or ChannelType.PrivateThread or ChannelType.PublicThread) + if ( + channel.Type + is ChannelType.AnnouncementThread + or ChannelType.PrivateThread + or ChannelType.PublicThread + ) { // parent_id should always have a value for threads channelId = channel.ParentID.Value!.Value; @@ -151,67 +203,88 @@ public class WebhookExecutorService( } // Check if the channel, or its category, or the user is ignored - if (guild.Channels.IgnoredChannels.Contains(channelId.Value.Value) || - categoryId != null && guild.Channels.IgnoredChannels.Contains(categoryId.Value.Value)) return null; + if ( + guild.Channels.IgnoredChannels.Contains(channelId.Value.Value) + || categoryId != null && guild.Channels.IgnoredChannels.Contains(categoryId.Value.Value) + ) + return null; if (userId != null) { - if (guild.Channels.IgnoredUsers.Contains(userId.Value)) return null; + if (guild.Channels.IgnoredUsers.Contains(userId.Value)) + return null; // Check the channel-local and category-local ignored users var channelIgnoredUsers = - guild.Channels.IgnoredUsersPerChannel.GetValueOrDefault(channelId.Value.Value) ?? []; - var categoryIgnoredUsers = (categoryId != null - ? guild.Channels.IgnoredUsersPerChannel.GetValueOrDefault(categoryId.Value.Value) - : []) ?? []; - if (channelIgnoredUsers.Concat(categoryIgnoredUsers).Contains(userId.Value)) return null; + guild.Channels.IgnoredUsersPerChannel.GetValueOrDefault(channelId.Value.Value) + ?? []; + var categoryIgnoredUsers = + ( + categoryId != null + ? guild.Channels.IgnoredUsersPerChannel.GetValueOrDefault( + categoryId.Value.Value + ) + : [] + ) ?? []; + if (channelIgnoredUsers.Concat(categoryIgnoredUsers).Contains(userId.Value)) + return null; } // These three events can be redirected to other channels. Redirects can be on a channel or category level. // Obviously, the events are only redirected if they're supposed to be logged in the first place. - if (logChannelType is LogChannelType.MessageUpdate or LogChannelType.MessageDelete - or LogChannelType.MessageDeleteBulk) + if ( + logChannelType + is LogChannelType.MessageUpdate + or LogChannelType.MessageDelete + or LogChannelType.MessageDeleteBulk + ) { - if (GetDefaultLogChannel(guild, logChannelType) == 0) return null; + if (GetDefaultLogChannel(guild, logChannelType) == 0) + return null; - var categoryRedirect = categoryId != null - ? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value) - : 0; + var categoryRedirect = + categoryId != null + ? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value) + : 0; - if (guild.Channels.Redirects.TryGetValue(channelId.Value.Value, out var channelRedirect)) + if ( + guild.Channels.Redirects.TryGetValue(channelId.Value.Value, out var channelRedirect) + ) return channelRedirect; - if (categoryRedirect != 0) return categoryRedirect; + if (categoryRedirect != 0) + return categoryRedirect; return GetDefaultLogChannel(guild, logChannelType); } return GetDefaultLogChannel(guild, logChannelType); } - public static ulong GetDefaultLogChannel(Guild guild, LogChannelType channelType) => channelType switch - { - LogChannelType.GuildUpdate => guild.Channels.GuildUpdate, - LogChannelType.GuildEmojisUpdate => guild.Channels.GuildEmojisUpdate, - LogChannelType.GuildRoleCreate => guild.Channels.GuildRoleCreate, - LogChannelType.GuildRoleUpdate => guild.Channels.GuildRoleUpdate, - LogChannelType.GuildRoleDelete => guild.Channels.GuildRoleDelete, - LogChannelType.ChannelCreate => guild.Channels.ChannelCreate, - LogChannelType.ChannelUpdate => guild.Channels.ChannelUpdate, - LogChannelType.ChannelDelete => guild.Channels.ChannelDelete, - LogChannelType.GuildMemberAdd => guild.Channels.GuildMemberAdd, - LogChannelType.GuildMemberUpdate => guild.Channels.GuildMemberUpdate, - LogChannelType.GuildKeyRoleUpdate => guild.Channels.GuildKeyRoleUpdate, - LogChannelType.GuildMemberNickUpdate => guild.Channels.GuildMemberNickUpdate, - LogChannelType.GuildMemberAvatarUpdate => guild.Channels.GuildMemberAvatarUpdate, - LogChannelType.GuildMemberRemove => guild.Channels.GuildMemberRemove, - LogChannelType.GuildMemberKick => guild.Channels.GuildMemberKick, - LogChannelType.GuildBanAdd => guild.Channels.GuildBanAdd, - LogChannelType.GuildBanRemove => guild.Channels.GuildBanRemove, - LogChannelType.InviteCreate => guild.Channels.InviteCreate, - LogChannelType.InviteDelete => guild.Channels.InviteDelete, - LogChannelType.MessageUpdate => guild.Channels.MessageUpdate, - LogChannelType.MessageDelete => guild.Channels.MessageDelete, - LogChannelType.MessageDeleteBulk => guild.Channels.MessageDeleteBulk, - _ => throw new ArgumentOutOfRangeException(nameof(channelType)) - }; + public static ulong GetDefaultLogChannel(Guild guild, LogChannelType channelType) => + channelType switch + { + LogChannelType.GuildUpdate => guild.Channels.GuildUpdate, + LogChannelType.GuildEmojisUpdate => guild.Channels.GuildEmojisUpdate, + LogChannelType.GuildRoleCreate => guild.Channels.GuildRoleCreate, + LogChannelType.GuildRoleUpdate => guild.Channels.GuildRoleUpdate, + LogChannelType.GuildRoleDelete => guild.Channels.GuildRoleDelete, + LogChannelType.ChannelCreate => guild.Channels.ChannelCreate, + LogChannelType.ChannelUpdate => guild.Channels.ChannelUpdate, + LogChannelType.ChannelDelete => guild.Channels.ChannelDelete, + LogChannelType.GuildMemberAdd => guild.Channels.GuildMemberAdd, + LogChannelType.GuildMemberUpdate => guild.Channels.GuildMemberUpdate, + LogChannelType.GuildKeyRoleUpdate => guild.Channels.GuildKeyRoleUpdate, + LogChannelType.GuildMemberNickUpdate => guild.Channels.GuildMemberNickUpdate, + LogChannelType.GuildMemberAvatarUpdate => guild.Channels.GuildMemberAvatarUpdate, + LogChannelType.GuildMemberRemove => guild.Channels.GuildMemberRemove, + LogChannelType.GuildMemberKick => guild.Channels.GuildMemberKick, + LogChannelType.GuildBanAdd => guild.Channels.GuildBanAdd, + LogChannelType.GuildBanRemove => guild.Channels.GuildBanRemove, + LogChannelType.InviteCreate => guild.Channels.InviteCreate, + LogChannelType.InviteDelete => guild.Channels.InviteDelete, + LogChannelType.MessageUpdate => guild.Channels.MessageUpdate, + LogChannelType.MessageDelete => guild.Channels.MessageDelete, + LogChannelType.MessageDeleteBulk => guild.Channels.MessageDeleteBulk, + _ => throw new ArgumentOutOfRangeException(nameof(channelType)), + }; } public enum LogChannelType @@ -237,5 +310,5 @@ public enum LogChannelType InviteDelete, MessageUpdate, MessageDelete, - MessageDeleteBulk -} \ No newline at end of file + MessageDeleteBulk, +}