diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d2b1b96..085fcbc 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,14 +3,14 @@ "isRoot": true, "tools": { "csharpier": { - "version": "0.30.6", + "version": "0.30.1", "commands": [ "dotnet-csharpier" ], "rollForward": false }, "husky": { - "version": "0.7.2", + "version": "0.7.1", "commands": [ "husky" ], diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs index 7f2ec0d..2dab68f 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs @@ -43,7 +43,6 @@ public class ChannelCommands( Config config, GuildRepository guildRepository, GuildCache guildCache, - GuildFetchService guildFetchService, ChannelCache channelCache, IMemberCache memberCache, IFeedbackService feedbackService, @@ -69,11 +68,8 @@ public class ChannelCommands( public async Task CheckPermissionsAsync() { var (userId, guildId) = contextInjection.GetUserAndGuild(); - if (!guildCache.TryGet(guildId, out var guild)) - { - return CataloggerError.Result($"Guild {guildId} not in cache"); - } + throw new CataloggerError("Guild not in cache"); var embed = new EmbedBuilder().WithTitle($"Permission check for {guild.Name}"); @@ -82,18 +78,8 @@ public class ChannelCommands( DiscordSnowflake.New(config.Discord.ApplicationId) ); var currentUser = await memberCache.TryGetAsync(guildId, userId); - if (botUser == null || currentUser == null) - { - // If this happens, something has gone wrong when fetching members. Refetch the guild's members. - guildFetchService.EnqueueGuild(guildId); - _logger.Error( - "Either our own user {BotId} or the invoking user {UserId} is not in cache, aborting permission check", - config.Discord.ApplicationId, - userId - ); - return CataloggerError.Result("Bot member or invoking member not found in cache"); - } + throw new CataloggerError("Bot member or invoking member not found in cache"); // We don't want to check categories or threads var guildChannels = channelCache @@ -218,7 +204,7 @@ public class ChannelCommands( { var (userId, guildId) = contextInjection.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildChannels = channelCache.GuildChannels(guildId).ToList(); var guildConfig = await guildRepository.GetAsync(guildId); diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs index 1104c82..9867e7d 100644 --- a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs +++ b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs @@ -50,15 +50,15 @@ public class ChannelCommandsComponents( public async Task OnMenuSelectionAsync(IReadOnlyList values) { if (contextInjection.Context is not IInteractionCommandContext ctx) - return CataloggerError.Result("No context"); + throw new CataloggerError("No context"); if (!ctx.TryGetUserID(out var userId)) - return CataloggerError.Result("No user ID in context"); + throw new CataloggerError("No user ID in context"); if (!ctx.Interaction.Message.TryGet(out var msg)) - return CataloggerError.Result("No message ID in context"); + throw new CataloggerError("No message ID in context"); if (!ctx.TryGetGuildID(out var guildId)) - return CataloggerError.Result("No guild ID in context"); + throw new CataloggerError("No guild ID in context"); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildChannels = channelCache.GuildChannels(guildId).ToList(); var guildConfig = await guildRepository.GetAsync(guildId); @@ -76,7 +76,7 @@ public class ChannelCommandsComponents( var state = values[0]; if (!Enum.TryParse(state, out var logChannelType)) - return CataloggerError.Result($"Invalid config-channels state {state}"); + throw new CataloggerError($"Invalid config-channels state {state}"); var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType); string? channelMention; @@ -147,15 +147,15 @@ public class ChannelCommandsComponents( public async Task OnButtonPressedAsync(string state) { if (contextInjection.Context is not IInteractionCommandContext ctx) - return CataloggerError.Result("No context"); + throw new CataloggerError("No context"); if (!ctx.TryGetUserID(out var userId)) - return CataloggerError.Result("No user ID in context"); + throw new CataloggerError("No user ID in context"); if (!ctx.Interaction.Message.TryGet(out var msg)) - return CataloggerError.Result("No message ID in context"); + throw new CataloggerError("No message ID in context"); if (!ctx.TryGetGuildID(out var guildId)) - return CataloggerError.Result("No guild ID in context"); + throw new CataloggerError("No guild ID in context"); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildChannels = channelCache.GuildChannels(guildId).ToList(); var guildConfig = await guildRepository.GetAsync(guildId); @@ -179,9 +179,9 @@ public class ChannelCommandsComponents( ); case "reset": if (lease.Data.CurrentPage == null) - return CataloggerError.Result("CurrentPage was null in reset button callback"); + throw new CataloggerError("CurrentPage was null in reset button callback"); if (!Enum.TryParse(lease.Data.CurrentPage, out var channelType)) - return CataloggerError.Result( + throw new CataloggerError( $"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'" ); @@ -281,15 +281,15 @@ public class ChannelCommandsComponents( public async Task OnMenuSelectionAsync(IReadOnlyList channels) { if (contextInjection.Context is not IInteractionCommandContext ctx) - return CataloggerError.Result("No context"); + throw new CataloggerError("No context"); if (!ctx.TryGetUserID(out var userId)) - return CataloggerError.Result("No user ID in context"); + throw new CataloggerError("No user ID in context"); if (!ctx.Interaction.Message.TryGet(out var msg)) - return CataloggerError.Result("No message ID in context"); + throw new CataloggerError("No message ID in context"); if (!ctx.TryGetGuildID(out var guildId)) - return CataloggerError.Result("No guild ID in context"); + throw new CataloggerError("No guild ID in context"); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildConfig = await guildRepository.GetAsync(guildId); var channelId = channels[0].ID.ToUlong(); @@ -305,7 +305,7 @@ public class ChannelCommandsComponents( } if (!Enum.TryParse(lease.Data.CurrentPage, out var channelType)) - return CataloggerError.Result( + throw new CataloggerError( $"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'" ); diff --git a/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs b/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs index 7e8987c..5ffc7a9 100644 --- a/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs +++ b/Catalogger.Backend/Bot/Commands/IgnoreEntitiesCommands.cs @@ -98,7 +98,7 @@ public class IgnoreEntitiesCommands : CommandGroup { var (_, guildId) = contextInjection.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildConfig = await guildRepository.GetAsync(guildId); @@ -201,14 +201,14 @@ public class IgnoreEntitiesCommands : CommandGroup { var (userId, guildId) = contextInjection.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildChannels = channelCache.GuildChannels(guildId).ToList(); var guildConfig = await guildRepository.GetAsync(guildId); var member = await memberCache.TryGetAsync(guildId, userId); if (member == null) - return CataloggerError.Result("Executing member not found"); + throw new CataloggerError("Executing member not found"); var ignoredChannels = guildConfig .IgnoredChannels.Select(id => diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs index b61fabc..69b225e 100644 --- a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs +++ b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Channels.cs @@ -110,14 +110,14 @@ public partial class IgnoreMessageCommands : CommandGroup { var (userId, guildId) = contextInjection.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildChannels = channelCache.GuildChannels(guildId).ToList(); var guildConfig = await guildRepository.GetAsync(guildId); var member = await memberCache.TryGetAsync(guildId, userId); if (member == null) - return CataloggerError.Result("Executing member not found"); + throw new CataloggerError("Executing member not found"); var ignoredChannels = guildConfig .Messages.IgnoredChannels.Select(id => diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs index 2cc46b7..cf3cb10 100644 --- a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs +++ b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Roles.cs @@ -90,7 +90,7 @@ public partial class IgnoreMessageCommands { var (_, guildId) = contextInjection.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildConfig = await guildRepository.GetAsync(guildId); diff --git a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs index 15ae280..d89f487 100644 --- a/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs +++ b/Catalogger.Backend/Bot/Commands/IgnoreMessageCommands.Users.cs @@ -94,7 +94,7 @@ public partial class IgnoreMessageCommands { var (userId, guildId) = contextInjection.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild was not cached"); var guildConfig = await guildRepository.GetAsync(guildId); diff --git a/Catalogger.Backend/Bot/Commands/InviteCommands.cs b/Catalogger.Backend/Bot/Commands/InviteCommands.cs index 6ec6991..7ffa031 100644 --- a/Catalogger.Backend/Bot/Commands/InviteCommands.cs +++ b/Catalogger.Backend/Bot/Commands/InviteCommands.cs @@ -59,7 +59,7 @@ public class InviteCommands( var (userId, guildId) = contextInjection.GetUserAndGuild(); var guildInvites = await guildApi.GetGuildInvitesAsync(guildId).GetOrThrow(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var dbInvites = await inviteRepository.GetGuildInvitesAsync(guildId); diff --git a/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs b/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs index dd2ff90..1ebc458 100644 --- a/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs +++ b/Catalogger.Backend/Bot/Commands/KeyRoleCommands.cs @@ -45,7 +45,7 @@ public class KeyRoleCommands( { var (_, guildId) = contextInjection.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild not in cache"); var guildRoles = roleCache.GuildRoles(guildId).ToList(); var guildConfig = await guildRepository.GetAsync(guildId); @@ -85,7 +85,7 @@ public class KeyRoleCommands( var (_, guildId) = contextInjection.GetUserAndGuild(); var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId); if (role == null) - return CataloggerError.Result("Role is not cached"); + throw new CataloggerError("Role is not cached"); var guildConfig = await guildRepository.GetAsync(guildId); if (guildConfig.KeyRoles.Any(id => role.ID.Value == id)) @@ -111,7 +111,7 @@ public class KeyRoleCommands( var (_, guildId) = contextInjection.GetUserAndGuild(); var role = roleCache.GuildRoles(guildId).FirstOrDefault(r => r.ID == roleId); if (role == null) - return CataloggerError.Result("Role is not cached"); + throw new CataloggerError("Role is not cached"); var guildConfig = await guildRepository.GetAsync(guildId); if (guildConfig.KeyRoles.All(id => role.ID != id)) diff --git a/Catalogger.Backend/Bot/Commands/MetaCommands.cs b/Catalogger.Backend/Bot/Commands/MetaCommands.cs index 8b543af..1887507 100644 --- a/Catalogger.Backend/Bot/Commands/MetaCommands.cs +++ b/Catalogger.Backend/Bot/Commands/MetaCommands.cs @@ -218,7 +218,7 @@ public class MetaCommands( await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds); } - // TODO: add more checks around response format + // TODO: add more checks around response format, configurable prometheus endpoint private async Task MessagesRate() { if (!config.Logging.EnableMetrics) @@ -227,8 +227,7 @@ public class MetaCommands( try { var query = HttpUtility.UrlEncode("increase(catalogger_received_messages[5m])"); - var prometheusUrl = config.Logging.PrometheusUrl ?? "http://localhost:9090"; - var resp = await _client.GetAsync($"{prometheusUrl}/api/v1/query?query={query}"); + var resp = await _client.GetAsync($"http://localhost:9090/api/v1/query?query={query}"); resp.EnsureSuccessStatusCode(); var data = await resp.Content.ReadFromJsonAsync(); diff --git a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs index 3864c54..586deb3 100644 --- a/Catalogger.Backend/Bot/Commands/RedirectCommands.cs +++ b/Catalogger.Backend/Bot/Commands/RedirectCommands.cs @@ -141,7 +141,7 @@ public class RedirectCommands( { var (userId, guildId) = contextInjectionService.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild was not cached"); var guildChannels = channelCache.GuildChannels(guildId).ToList(); var guildConfig = await guildRepository.GetAsync(guildId); diff --git a/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs b/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs index c92a886..b47a583 100644 --- a/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs +++ b/Catalogger.Backend/Bot/Commands/WatchlistCommands.cs @@ -83,7 +83,7 @@ public class WatchlistCommands( { var (userId, guildId) = contextInjectionService.GetUserAndGuild(); if (!guildCache.TryGet(guildId, out var guild)) - return CataloggerError.Result("Guild not in cache"); + throw new CataloggerError("Guild was not cached"); var watchlist = await watchlistRepository.GetGuildWatchlistAsync(guildId); if (watchlist.Count == 0) diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs index 0fc9d7f..935734a 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs @@ -17,6 +17,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; +using Microsoft.Extensions.Logging.Configuration; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.Extensions.Embeds; @@ -35,8 +36,6 @@ public class ChannelCreateResponder( { public async Task RespondAsync(IChannelCreate ch, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(ch); - if (!ch.GuildID.IsDefined()) return Result.Success; channelCache.Set(ch); diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs index 06b4727..7e13bcb 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs @@ -35,8 +35,6 @@ public class ChannelDeleteResponder( public async Task RespondAsync(IChannelDelete evt, CancellationToken ct = default) { - using var __ = LogUtils.Enrich(evt); - if (!evt.GuildID.IsDefined()) { _logger.Debug("Deleted channel {ChannelId} is not in a guild", evt.ID); diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs index 5f0ef62..7ed3954 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs @@ -40,8 +40,6 @@ public class ChannelUpdateResponder( public async Task RespondAsync(IChannelUpdate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - try { if (!channelCache.TryGet(evt.ID, out var oldChannel)) diff --git a/Catalogger.Backend/Bot/Responders/CustomInteractionResponder.cs b/Catalogger.Backend/Bot/Responders/CustomInteractionResponder.cs index 1fd3b9e..0281302 100644 --- a/Catalogger.Backend/Bot/Responders/CustomInteractionResponder.cs +++ b/Catalogger.Backend/Bot/Responders/CustomInteractionResponder.cs @@ -13,27 +13,21 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Catalogger.Backend.Extensions; using Microsoft.Extensions.Options; using Remora.Commands.Services; using Remora.Commands.Tokenization; using Remora.Commands.Trees; using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Commands.Responders; using Remora.Discord.Commands.Services; using Remora.Discord.Gateway.Responders; -using Remora.Rest.Core; using Remora.Results; -using Serilog.Context; namespace Catalogger.Backend.Bot.Responders; /// -/// Wrapper for Remora.Discord's default interaction responder, that ignores all events if test mode is enabled, -/// and handles results returned by commands. +/// Wrapper for Remora.Discord's default interaction responder, that ignores all events if test mode is enabled. /// public class CustomInteractionResponder( Config config, @@ -63,66 +57,21 @@ public class CustomInteractionResponder( treeNameResolver ); - public async Task RespondAsync(IInteractionCreate evt, CancellationToken ct = default) + public async Task RespondAsync( + IInteractionCreate gatewayEvent, + CancellationToken ct = default + ) { if (config.Discord.TestMode) { _logger.Information( "Not responding to interaction create event {InteractionId} in {ChannelId} as test mode is enabled", - evt.ID, - evt.Channel.Map(c => c.ID).OrDefault() + gatewayEvent.ID, + gatewayEvent.Channel.Map(c => c.ID).OrDefault() ); return Result.Success; } - using var _ = LogUtils.PushProperties( - ("Event", nameof(IInteractionCreate)), - ("InteractionId", evt.ID), - ("GuildId", evt.GuildID), - ("UserId", evt.User.Map(u => u.ID)), - ("MemberId", evt.Member.Map(m => m.User.Map(u => u.ID).OrDefault())), - ("ChannelId", evt.Channel.Map(c => c.ID)), - ("InteractionType", evt.Type) - ); - - using var __ = LogContext.PushProperty( - "InteractionData", - evt.Data.HasValue ? (object?)evt.Data.Value : null, - true - ); - - var result = await _inner.RespondAsync(evt, ct); - if (result.Error is not CataloggerError cataloggerError) - return result; - - return await interactionAPI.CreateInteractionResponseAsync( - evt.ID, - evt.Token, - new InteractionResponse( - Type: InteractionCallbackType.ChannelMessageWithSource, - Data: new Optional>( - new InteractionMessageCallbackData( - Embeds: new Optional>( - [ - new Embed( - Colour: DiscordUtils.Red, - Title: "Something went wrong", - Description: $""" - Something went wrong while running this command. - > {cataloggerError.Message} - Please try again later. - """ - ), - ] - ) - ) - ) - ), - ct: ct - ); + return await _inner.RespondAsync(gatewayEvent, ct); } } diff --git a/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs index fba79eb..1d88db1 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs @@ -14,7 +14,6 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Extensions; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.Gateway.Responders; @@ -29,8 +28,6 @@ public class AuditLogResponder(AuditLogCache auditLogCache, ILogger logger) public Task RespondAsync(IGuildAuditLogEntryCreate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - if (evt.TargetID == null || evt.UserID == null) return Task.FromResult(Result.Success); diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs index 2feb745..f20d8c2 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs @@ -37,7 +37,6 @@ public class GuildBanAddResponder( public async Task RespondAsync(IGuildBanAdd evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); var guildConfig = await guildRepository.GetAsync(evt.GuildID); // Delay 2 seconds for the audit log diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs index cc50908..99fcce2 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs @@ -37,7 +37,6 @@ public class GuildBanRemoveResponder( public async Task RespondAsync(IGuildBanRemove evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); var guildConfig = await guildRepository.GetAsync(evt.GuildID); // Delay 2 seconds for the audit log diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs index 55bac7a..9d5ac61 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs @@ -44,8 +44,6 @@ public class GuildCreateResponder( public async Task RespondAsync(IGuildCreate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - ulong guildId; string? guildName = null; if (evt.Guild.TryPickT0(out var guild, out var unavailableGuild)) @@ -103,8 +101,6 @@ public class GuildCreateResponder( public async Task RespondAsync(IGuildDelete evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - if (evt.IsUnavailable.OrDefault(false)) { _logger.Debug("Guild {GuildId} became unavailable", evt.ID); diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs index c70bd41..0ccc859 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs @@ -37,8 +37,6 @@ public class GuildEmojisUpdateResponder( public async Task RespondAsync(IGuildEmojisUpdate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - try { if (!emojiCache.TryGet(evt.GuildID, out var oldEmoji)) diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMembersChunkResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMembersChunkResponder.cs index 8a43b46..8a7c786 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMembersChunkResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMembersChunkResponder.cs @@ -14,7 +14,6 @@ // along with this program. If not, see . using Catalogger.Backend.Cache; -using Catalogger.Backend.Extensions; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Gateway.Responders; using Remora.Results; @@ -28,8 +27,6 @@ public class GuildMembersChunkResponder(ILogger logger, IMemberCache memberCache public async Task RespondAsync(IGuildMembersChunk evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - _logger.Debug( "Received chunk {ChunkIndex} / {ChunkCount} for guild {GuildId}", evt.ChunkIndex + 1, diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs index b2992c4..ee43e98 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs @@ -37,8 +37,6 @@ public class GuildUpdateResponder( public async Task RespondAsync(IGuildUpdate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - try { if (!guildCache.TryGet(evt.ID, out var oldGuild)) diff --git a/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs index d50f81d..329d764 100644 --- a/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs @@ -37,7 +37,6 @@ public class InviteCreateResponder( public async Task RespondAsync(IInviteCreate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); var guildId = evt.GuildID.Value; var invitesResult = await guildApi.GetGuildInvitesAsync(guildId, ct); diff --git a/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs index d34b3c8..bb9d855 100644 --- a/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs @@ -38,7 +38,6 @@ public class InviteDeleteResponder( public async Task RespondAsync(IInviteDelete evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); var guildId = evt.GuildID.Value; var dbDeleteCount = await inviteRepository.DeleteInviteAsync(guildId, evt.Code); diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs index 6d8901c..b873efe 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs @@ -48,8 +48,6 @@ public class GuildMemberAddResponder( public async Task RespondAsync(IGuildMemberAdd member, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(member); - await memberCache.SetAsync(member.GuildID, member); await memberCache.SetMemberNamesAsync(member.GuildID, [member]); diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs index a2e61d1..94cf8cc 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs @@ -39,8 +39,6 @@ public class GuildMemberRemoveResponder( public async Task RespondAsync(IGuildMemberRemove evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - try { var embed = new EmbedBuilder() diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs index 649c945..e02b790 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs @@ -48,8 +48,6 @@ public class GuildMemberUpdateResponder( CancellationToken ct = default ) { - using var _ = LogUtils.Enrich(newMember); - try { var oldMember = await memberCache.TryGetAsync(newMember.GuildID, newMember.User.ID); diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs index e54d12b..5098cd2 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs @@ -77,7 +77,7 @@ public class MessageCreateResponder( return Result.Success; } - await messageRepository.SaveMessageAsync(msg, msg.GuildID, ct); + await messageRepository.SaveMessageAsync(msg, ct); return Result.Success; } } @@ -144,19 +144,6 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) await using var messageRepository = scope.ServiceProvider.GetRequiredService(); - if (await messageRepository.IsMessageIgnoredAsync(originalId)) - { - _logger.Debug( - "Proxied message {MessageId} should be ignored as trigger {OriginalId} is already ignored", - msgId, - originalId - ); - - await messageRepository.IgnoreMessageAsync(originalId); - await messageRepository.IgnoreMessageAsync(msgId); - return; - } - _logger.Debug( "Setting proxy data for {MessageId} and ignoring {OriginalId}", msgId, @@ -208,19 +195,6 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) pkMessage.Original ); - if (await messageRepository.IsMessageIgnoredAsync(pkMessage.Original)) - { - _logger.Debug( - "Proxied message {MessageId} should be ignored as trigger {OriginalId} is already ignored", - pkMessage.Id, - pkMessage.Original - ); - - await messageRepository.IgnoreMessageAsync(pkMessage.Original); - await messageRepository.IgnoreMessageAsync(msgId); - return; - } - await messageRepository.SetProxiedMessageDataAsync( msgId, pkMessage.Original, diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs index 55a5a68..315daec 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs @@ -18,6 +18,7 @@ using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; +using NodaTime; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; @@ -26,6 +27,7 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; +using Serilog.Context; namespace Catalogger.Backend.Bot.Responders.Messages; @@ -36,6 +38,7 @@ public class MessageDeleteResponder( WebhookExecutorService webhookExecutor, ChannelCache channelCache, UserCache userCache, + IClock clock, PluralkitApiService pluralkitApi ) : IResponder { @@ -78,8 +81,8 @@ public class MessageDeleteResponder( new Embed( Title: "Message deleted", Description: $"A message not found in the database was deleted in <#{evt.ChannelID}> ({evt.ChannelID}).", - Footer: new EmbedFooter(Text: $"ID: {evt.ID} | Original sent at"), - Timestamp: evt.ID.Timestamp + Footer: new EmbedFooter(Text: $"ID: {evt.ID}"), + Timestamp: clock.GetCurrentInstant().ToDateTimeOffset() ) ); @@ -121,7 +124,7 @@ public class MessageDeleteResponder( .WithTitle("Message deleted") .WithDescription(msg.Content) .WithColour(DiscordUtils.Red) - .WithFooter($"ID: {msg.Id} | Original sent at") + .WithFooter($"ID: {msg.Id}") .WithTimestamp(evt.ID); if (user != null) diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs index 0bf2c28..fa1e75d 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs @@ -20,6 +20,7 @@ using Catalogger.Backend.Services; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Gateway.Events; using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; @@ -39,9 +40,13 @@ public class MessageUpdateResponder( { private readonly ILogger _logger = logger.ForContext(); - public async Task RespondAsync(IMessageUpdate msg, CancellationToken ct = default) + public async Task RespondAsync(IMessageUpdate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(msg); + using var _ = LogUtils.Enrich(evt); + + // Discord only *very* recently changed message update events to have all fields, + // so we convert the event to a MessageCreate to avoid having to unwrap every single field + var msg = ConvertToMessageCreate(evt); if (!msg.GuildID.IsDefined()) { @@ -129,7 +134,7 @@ public class MessageUpdateResponder( if (oldMessage is { System: not null, Member: not null }) { embedBuilder.WithTitle($"Message by {msg.Author.Username} edited"); - embedBuilder.AddField("\u200b", "**PluralKit information**"); + embedBuilder.AddField("\u200b", "**PluralKit information**", false); embedBuilder.AddField("System ID", oldMessage.System, true); embedBuilder.AddField("Member ID", oldMessage.Member, true); } @@ -169,7 +174,7 @@ public class MessageUpdateResponder( ) { if ( - !await messageRepository.SaveMessageAsync(msg, msg.GuildID, ct) + !await messageRepository.SaveMessageAsync(msg, ct) && msg.ApplicationID.Is(DiscordUtils.PkUserId) ) { @@ -191,6 +196,44 @@ public class MessageUpdateResponder( } } + 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) diff --git a/Catalogger.Backend/Bot/Responders/ReadyResponder.cs b/Catalogger.Backend/Bot/Responders/ReadyResponder.cs index 8c43140..192ce72 100644 --- a/Catalogger.Backend/Bot/Responders/ReadyResponder.cs +++ b/Catalogger.Backend/Bot/Responders/ReadyResponder.cs @@ -26,19 +26,19 @@ public class ReadyResponder(ILogger logger, WebhookExecutorService webhookExecut { private readonly ILogger _logger = logger.ForContext(); - public Task RespondAsync(IReady evt, CancellationToken ct = default) + public Task RespondAsync(IReady gatewayEvent, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - - var shardId = evt.Shard.TryGet(out var shard) ? (shard.ShardID, shard.ShardCount) : (0, 1); + var shardId = gatewayEvent.Shard.TryGet(out var shard) + ? (shard.ShardID, shard.ShardCount) + : (0, 1); _logger.Information( "Ready as {User} on shard {ShardId}/{ShardCount}", - evt.User.Tag(), + gatewayEvent.User.Tag(), shardId.Item1, shardId.Item2 ); if (shardId.Item1 == 0) - webhookExecutorService.SetSelfUser(evt.User); + webhookExecutorService.SetSelfUser(gatewayEvent.User); return Task.FromResult(Result.Success); } diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs index 8d079d7..0d09658 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs @@ -35,8 +35,6 @@ public class RoleCreateResponder( public async Task RespondAsync(IGuildRoleCreate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - _logger.Debug("Received new role {RoleId} in guild {GuildId}", evt.Role.ID, evt.GuildID); roleCache.Set(evt.Role, evt.GuildID); diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs index 5f9b648..f76b339 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs @@ -35,8 +35,6 @@ public class RoleDeleteResponder( public async Task RespondAsync(IGuildRoleDelete evt, CancellationToken ct = default) { - using var __ = LogUtils.Enrich(evt); - try { if (!roleCache.TryGet(evt.RoleID, out var role)) diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs index 656bb02..4ed8c2f 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs @@ -37,8 +37,6 @@ public class RoleUpdateResponder( public async Task RespondAsync(IGuildRoleUpdate evt, CancellationToken ct = default) { - using var _ = LogUtils.Enrich(evt); - try { var newRole = evt.Role; diff --git a/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs b/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs index 0e08c1f..dfa9694 100644 --- a/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs +++ b/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs @@ -234,7 +234,6 @@ internal record RedisMember( User.ToRemoraUser(), Nickname, Avatar != null ? new ImageHash(Avatar) : null, - Banner: null, Roles.Select(DiscordSnowflake.New).ToList(), JoinedAt, PremiumSince, diff --git a/Catalogger.Backend/Catalogger.Backend.csproj b/Catalogger.Backend/Catalogger.Backend.csproj index b377418..6d048ab 100644 --- a/Catalogger.Backend/Catalogger.Backend.csproj +++ b/Catalogger.Backend/Catalogger.Backend.csproj @@ -21,18 +21,18 @@ - + - + - + diff --git a/Catalogger.Backend/CataloggerError.cs b/Catalogger.Backend/CataloggerError.cs index 40322a4..31abf6f 100644 --- a/Catalogger.Backend/CataloggerError.cs +++ b/Catalogger.Backend/CataloggerError.cs @@ -13,13 +13,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Remora.Results; -using RemoraResult = Remora.Results.Result; - namespace Catalogger.Backend; -public class CataloggerError(string message) : Exception(message), IResultError -{ - public static RemoraResult Result(string message) => - RemoraResult.FromError(new CataloggerError(message)); -} +public class CataloggerError(string message) : Exception(message) { } diff --git a/Catalogger.Backend/CataloggerMetrics.cs b/Catalogger.Backend/CataloggerMetrics.cs index ce55805..1dc0cf0 100644 --- a/Catalogger.Backend/CataloggerMetrics.cs +++ b/Catalogger.Backend/CataloggerMetrics.cs @@ -29,11 +29,6 @@ public static class CataloggerMetrics public static long MessageRateMinute { get; set; } - public static readonly Gauge DatabaseConnections = Metrics.CreateGauge( - "catalogger_open_database_connections", - "Number of open database connections" - ); - public static readonly Gauge GuildsCached = Metrics.CreateGauge( "catalogger_cache_guilds", "Number of guilds in the cache" diff --git a/Catalogger.Backend/Config.cs b/Catalogger.Backend/Config.cs index 831c439..612b91e 100644 --- a/Catalogger.Backend/Config.cs +++ b/Catalogger.Backend/Config.cs @@ -33,7 +33,6 @@ public class Config public bool EnableMetrics { get; init; } = true; public string? SeqLogUrl { get; init; } - public string? PrometheusUrl { get; init; } } public class DatabaseConfig diff --git a/Catalogger.Backend/Database/DatabaseConnection.cs b/Catalogger.Backend/Database/DatabaseConnection.cs index b2488f9..0c44667 100644 --- a/Catalogger.Backend/Database/DatabaseConnection.cs +++ b/Catalogger.Backend/Database/DatabaseConnection.cs @@ -17,13 +17,17 @@ using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; using Npgsql; -using Serilog; namespace Catalogger.Backend.Database; -public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposable +public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner) + : DbConnection, + IDisposable { + public Guid ConnectionId => id; public NpgsqlConnection Inner => inner; + private readonly ILogger _logger = logger.ForContext(); + private readonly DateTimeOffset _openTime = DateTimeOffset.UtcNow; private bool _hasClosed; @@ -39,6 +43,8 @@ public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposa } DatabasePool.DecrementConnections(); + var openFor = DateTimeOffset.UtcNow - _openTime; + _logger.Verbose("Closing connection {ConnId}, open for {OpenFor}", ConnectionId, openFor); _hasClosed = true; await inner.CloseAsync(); } @@ -46,20 +52,17 @@ public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposa protected override async ValueTask BeginDbTransactionAsync( IsolationLevel isolationLevel, CancellationToken cancellationToken - ) => await inner.BeginTransactionAsync(isolationLevel, cancellationToken); + ) + { + _logger.Verbose("Beginning transaction on connection {ConnId}", ConnectionId); + return await inner.BeginTransactionAsync(isolationLevel, cancellationToken); + } public new void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected override void Dispose(bool disposing) - { - Log.Error("Called Dispose method on DbConnection, should call DisposeAsync!"); - Log.Warning("CloseAsync will be called synchronously."); - CloseAsync().Wait(); + Close(); inner.Dispose(); + GC.SuppressFinalize(this); } public override async ValueTask DisposeAsync() @@ -70,13 +73,13 @@ public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposa } protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => - throw new SyncException(nameof(BeginDbTransaction)); + inner.BeginTransaction(isolationLevel); public override void ChangeDatabase(string databaseName) => inner.ChangeDatabase(databaseName); - public override void Close() => throw new SyncException(nameof(Close)); + public override void Close() => inner.Close(); - public override void Open() => throw new SyncException(nameof(Open)); + public override void Open() => inner.Open(); [AllowNull] public override string ConnectionString @@ -91,6 +94,4 @@ public class DatabaseConnection(NpgsqlConnection inner) : DbConnection, IDisposa public override string ServerVersion => inner.ServerVersion; protected override DbCommand CreateDbCommand() => inner.CreateCommand(); - - public class SyncException(string method) : Exception($"Tried to use sync method {method}"); } diff --git a/Catalogger.Backend/Database/DatabaseMigrator.cs b/Catalogger.Backend/Database/DatabaseMigrator.cs index 183723d..80e68c6 100644 --- a/Catalogger.Backend/Database/DatabaseMigrator.cs +++ b/Catalogger.Backend/Database/DatabaseMigrator.cs @@ -141,7 +141,7 @@ public class DatabaseMigrator(ILogger logger, IClock clock, DatabaseConnection c if (hasMigrationTable) { return await conn.QuerySingleOrDefaultAsync( - "SELECT * FROM migrations ORDER BY applied_at DESC, migration_name DESC LIMIT 1" + "SELECT * FROM migrations ORDER BY applied_at DESC LIMIT 1" ); } @@ -163,7 +163,7 @@ public class DatabaseMigrator(ILogger logger, IClock clock, DatabaseConnection c return await reader.ReadToEndAsync(); } - private static IEnumerable GetMigrationNames() => + public static IEnumerable GetMigrationNames() => typeof(DatabasePool) .Assembly.GetManifestResourceNames() .Where(s => s.StartsWith($"{RootPath}.Migrations")) diff --git a/Catalogger.Backend/Database/DatabasePool.cs b/Catalogger.Backend/Database/DatabasePool.cs index 29a845e..fb266dd 100644 --- a/Catalogger.Backend/Database/DatabasePool.cs +++ b/Catalogger.Backend/Database/DatabasePool.cs @@ -24,13 +24,18 @@ namespace Catalogger.Backend.Database; public class DatabasePool { + private readonly ILogger _rootLogger; + private readonly ILogger _logger; private readonly NpgsqlDataSource _dataSource; private static int _openConnections; public static int OpenConnections => _openConnections; - public DatabasePool(Config config, ILoggerFactory? loggerFactory) + public DatabasePool(Config config, ILogger logger, ILoggerFactory? loggerFactory) { + _rootLogger = logger; + _logger = logger.ForContext(); + var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) { Timeout = config.Database.Timeout ?? 5, @@ -46,14 +51,24 @@ public class DatabasePool public async Task AcquireAsync(CancellationToken ct = default) { - IncrementConnections(); - return new DatabaseConnection(await _dataSource.OpenConnectionAsync(ct)); + return new DatabaseConnection( + LogOpen(), + _rootLogger, + await _dataSource.OpenConnectionAsync(ct) + ); } public DatabaseConnection Acquire() { + return new DatabaseConnection(LogOpen(), _rootLogger, _dataSource.OpenConnection()); + } + + private Guid LogOpen() + { + var connId = Guid.NewGuid(); + _logger.Verbose("Opening database connection {ConnId}", connId); IncrementConnections(); - return new DatabaseConnection(_dataSource.OpenConnection()); + return connId; } public async Task ExecuteAsync( diff --git a/Catalogger.Backend/Database/Repositories/MessageRepository.cs b/Catalogger.Backend/Database/Repositories/MessageRepository.cs index fc45e4f..17925c3 100644 --- a/Catalogger.Backend/Database/Repositories/MessageRepository.cs +++ b/Catalogger.Backend/Database/Repositories/MessageRepository.cs @@ -18,7 +18,6 @@ using Catalogger.Backend.Extensions; using Dapper; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.API.Abstractions.Objects; using Remora.Rest.Core; namespace Catalogger.Backend.Database.Repositories; @@ -64,11 +63,7 @@ public class MessageRepository( /// /// Adds a new message. If the message is already in the database, updates the existing message instead. /// - public async Task SaveMessageAsync( - IMessage msg, - Optional guildId, - CancellationToken ct = default - ) + public async Task SaveMessageAsync(IMessageCreate msg, CancellationToken ct = default) { var content = await Task.Run( () => @@ -112,9 +107,7 @@ public class MessageRepository( Id = msg.ID.Value, UserId = msg.Author.ID.Value, ChannelId = msg.ChannelID.Value, - GuildId = guildId.IsDefined(out var guildIdValue) - ? guildIdValue.Value - : (ulong?)null, + GuildId = msg.GuildID.Map(s => s.Value).OrDefault(), Content = content, Username = username, Metadata = metadata, diff --git a/Catalogger.Backend/Extensions/LogUtils.cs b/Catalogger.Backend/Extensions/LogUtils.cs index 330e5f7..b8499b5 100644 --- a/Catalogger.Backend/Extensions/LogUtils.cs +++ b/Catalogger.Backend/Extensions/LogUtils.cs @@ -1,5 +1,4 @@ using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.API.Abstractions.Objects; using Remora.Rest.Core; using Serilog.Context; @@ -20,13 +19,7 @@ public static class LogUtils ("ChannelId", md.ChannelID), ("MessageId", md.ID) ), - IMessageUpdate mu => PushProperties( - type, - ("GuildId", mu.GuildID), - ("ChannelId", mu.ChannelID), - ("MessageId", mu.ID) - ), - IMessageCreate mc => PushProperties( + IMessageUpdate mc => PushProperties( type, ("GuildId", mc.GuildID), ("ChannelId", mc.ChannelID), @@ -38,93 +31,6 @@ public static class LogUtils ("ChannelId", mdb.ChannelID), ("MessageIds", mdb.IDs) ), - IGuildRoleCreate grc => PushProperties( - type, - ("GuildId", grc.GuildID), - ("RoleId", grc.Role.ID) - ), - IGuildRoleUpdate gru => PushProperties( - type, - ("GuildId", gru.GuildID), - ("RoleId", gru.Role.ID) - ), - IGuildRoleDelete grd => PushProperties( - type, - ("GuildId", grd.GuildID), - ("RoleId", grd.RoleID) - ), - IGuildMemberAdd gma => PushProperties( - type, - ("GuildId", gma.GuildID), - ("UserId", gma.User.Map(u => u.ID)) - ), - IGuildMemberUpdate gmu => PushProperties( - type, - ("GuildId", gmu.GuildID), - ("UserId", gmu.User.ID) - ), - IGuildMemberRemove gmr => PushProperties( - type, - ("GuildId", gmr.GuildID), - ("UserId", gmr.User.ID) - ), - IInviteCreate ic => PushProperties( - type, - ("GuildId", ic.GuildID), - ("ChannelId", ic.ChannelID), - ("InviteCode", ic.Code) - ), - IInviteDelete id => PushProperties( - type, - ("GuildId", id.GuildID), - ("ChannelId", id.ChannelID), - ("Code", id.Code) - ), - IChannelCreate cc => PushProperties( - type, - ("GuildId", cc.GuildID), - ("ChannelId", cc.ID) - ), - IChannelUpdate cu => PushProperties( - type, - ("GuildId", cu.GuildID), - ("ChannelId", cu.ID) - ), - IChannelDelete cd => PushProperties( - type, - ("GuildId", cd.GuildID), - ("ChannelId", cd.ID) - ), - IGuildAuditLogEntryCreate ale => PushProperties( - type, - ("GuildId", ale.GuildID), - ("AuditLogEntryId", ale.ID), - ("ActionType", ale.ActionType) - ), - IGuildBanAdd gba => PushProperties( - type, - ("GuildId", gba.GuildID), - ("UserId", gba.User.ID) - ), - IGuildBanRemove gbr => PushProperties( - type, - ("GuildId", gbr.GuildID), - ("UserId", gbr.User.ID) - ), - IGuildCreate gc => PushProperties( - type, - ("GuildId", gc.Guild.Match(g => g.ID, g => g.ID)) - ), - IGuildDelete gd => PushProperties(type, ("GuildId", gd.ID)), - IGuildEmojisUpdate geu => PushProperties(type, ("GuildId", geu.GuildID)), - IGuildMembersChunk gmc => PushProperties( - type, - ("GuildId", gmc.GuildID), - ("MemberCount", gmc.Members.Count), - ("ChunkIndex", gmc.ChunkIndex), - ("ChunkCount", gmc.ChunkCount) - ), - IGuildUpdate gu => PushProperties(type, ("GuildId", gu.ID)), _ => PushProperties(type), }; } diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs index f9a226c..c1af3ab 100644 --- a/Catalogger.Backend/Extensions/StartupExtensions.cs +++ b/Catalogger.Backend/Extensions/StartupExtensions.cs @@ -51,22 +51,19 @@ public static class StartupExtensions { var logCfg = new LoggerConfiguration() .Enrich.FromLogContext() - .MinimumLevel.Verbose() - // Most Microsoft.* package logs are needlessly verbose, so we restrict them to INFO level and up - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Is(config.Logging.LogEventLevel) // 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.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) - // Let's not put webhook tokens and even *full bot tokens* in the logs, thank you - .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning) // The default theme doesn't support light mode - .WriteTo.Console( - theme: AnsiConsoleTheme.Sixteen, - applyThemeToRedirectedOutput: true, - restrictedToMinimumLevel: config.Logging.LogEventLevel - ); + .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen, applyThemeToRedirectedOutput: true); if (config.Logging.SeqLogUrl != null) { diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index 6fbf6b3..7f948fc 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -66,7 +66,8 @@ builder | GatewayIntents.GuildMessages | GatewayIntents.GuildWebhooks | GatewayIntents.MessageContents - | GatewayIntents.GuildExpressions; + // Actually GUILD_EXPRESSIONS + | GatewayIntents.GuildEmojisAndStickers; // Set a default status for all shards. This is updated to a shard-specific one in StatusUpdateService. g.Presence = new UpdatePresence( @@ -96,7 +97,12 @@ builder .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() .WithCommandGroup() .WithCommandGroup() // End command tree diff --git a/Catalogger.Backend/Services/MetricsCollectionService.cs b/Catalogger.Backend/Services/MetricsCollectionService.cs index afba829..5c003a0 100644 --- a/Catalogger.Backend/Services/MetricsCollectionService.cs +++ b/Catalogger.Backend/Services/MetricsCollectionService.cs @@ -43,7 +43,6 @@ public class MetricsCollectionService( var messageCount = await conn.ExecuteScalarAsync("select count(id) from messages"); - CataloggerMetrics.DatabaseConnections.Set(DatabasePool.OpenConnections); CataloggerMetrics.GuildsCached.Set(guildCache.Size); CataloggerMetrics.ChannelsCached.Set(channelCache.Size); CataloggerMetrics.RolesCached.Set(roleCache.Size); diff --git a/Catalogger.Backend/Services/TimeoutService.cs b/Catalogger.Backend/Services/TimeoutService.cs index 03d1831..6615f43 100644 --- a/Catalogger.Backend/Services/TimeoutService.cs +++ b/Catalogger.Backend/Services/TimeoutService.cs @@ -25,8 +25,7 @@ public class TimeoutService( _logger.Information("Populating timeout service with existing database timeouts"); await using var scope = serviceProvider.CreateAsyncScope(); - await using var timeoutRepository = - scope.ServiceProvider.GetRequiredService(); + var timeoutRepository = scope.ServiceProvider.GetRequiredService(); var timeouts = await timeoutRepository.GetAllAsync(); foreach (var timeout in timeouts) @@ -54,10 +53,8 @@ public class TimeoutService( _logger.Information("Sending timeout log for {TimeoutId}", timeoutId); await using var scope = serviceProvider.CreateAsyncScope(); - await using var guildRepository = - scope.ServiceProvider.GetRequiredService(); - await using var timeoutRepository = - scope.ServiceProvider.GetRequiredService(); + var guildRepository = scope.ServiceProvider.GetRequiredService(); + var timeoutRepository = scope.ServiceProvider.GetRequiredService(); var timeout = await timeoutRepository.RemoveAsync(timeoutId); if (timeout == null) diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index 8ca8cb3..f82ea75 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -43,7 +43,7 @@ public class WebhookExecutorService( private readonly ILogger _logger = logger.ForContext(); private readonly Snowflake _applicationId = DiscordSnowflake.New(config.Discord.ApplicationId); private readonly ConcurrentDictionary> _cache = new(); - private readonly ConcurrentDictionary _locks = new(); + private readonly ConcurrentDictionary _locks = new(); private readonly ConcurrentDictionary _timers = new(); private IUser? _selfUser; @@ -189,7 +189,7 @@ public class WebhookExecutorService( private List TakeFromQueue(ulong channelId) { var queue = _cache.GetOrAdd(channelId, []); - var channelLock = _locks.GetOrAdd(channelId, new Lock()); + var channelLock = _locks.GetOrAdd(channelId, channelId); lock (channelLock) { var totalContentLength = 0; @@ -293,10 +293,10 @@ public class WebhookExecutorService( roleIds != null && logChannelType is LogChannelType.GuildMemberUpdate; if (isMessageLog) - return GetLogChannelForMessageEvent(guild, logChannelType, channelId, userId); + return GetMessageLogChannel(guild, logChannelType, channelId, userId); if (isChannelLog) - return GetLogChannelForChannelEvent(guild, logChannelType, channelId!.Value); + return GetChannelLogChannel(guild, logChannelType, channelId!.Value); if (isRoleLog && guild.IgnoredRoles.Contains(roleId!.Value.Value)) return null; @@ -305,201 +305,20 @@ public class WebhookExecutorService( if (isMemberRoleUpdateLog && roleIds!.All(r => guild.IgnoredRoles.Contains(r.Value))) return null; - // If nothing is ignored, and this isn't a message or channel event, return the default log channel. + // If nothing is ignored, return the correct log channel! return GetDefaultLogChannel(guild, logChannelType); } - private ulong? GetLogChannelForMessageEvent( - Guild guild, - LogChannelType logChannelType, - Snowflake? channelId = null, - ulong? userId = null - ) - { - _logger.Verbose( - "Getting log channel for event {Event}. Channel ID: {ChannelId}, user ID: {UserId}", - logChannelType, - channelId, - userId - ); - - // Check if the user is ignored globally - if (userId != null && guild.Messages.IgnoredUsers.Contains(userId.Value)) - { - _logger.Verbose("User {UserId} is ignored globally", userId); - return null; - } - - // If the user isn't ignored and we didn't get a channel ID, return the default log channel - if (channelId == null) - { - _logger.Verbose( - "No channel ID given so returning default channel for {Event}", - logChannelType - ); - return GetDefaultLogChannel(guild, logChannelType); - } - - if (!channelCache.TryGet(channelId.Value, out var channel)) - { - _logger.Verbose( - "Channel with ID {ChannelId} is not cached, returning default log channel", - channelId - ); - return GetDefaultLogChannel(guild, logChannelType); - } - - if (!GetChannelAndParentId(channel, out var actualChannelId, out var categoryId)) - { - _logger.Verbose( - "Could not get root channel and category ID for channel {ChannelId}, returning default log channel", - channelId - ); - return GetDefaultLogChannel(guild, logChannelType); - } - - // Check if the channel or its category is ignored - if ( - guild.Messages.IgnoredChannels.Contains(actualChannelId.Value) - || categoryId != null && guild.Messages.IgnoredChannels.Contains(categoryId.Value.Value) - ) - { - _logger.Verbose( - "Channel {ChannelId} or its parent {CategoryId} is ignored", - actualChannelId, - categoryId - ); - return null; - } - - if (userId != null) - { - // Check the channel-local and category-local ignored users - var channelIgnoredUsers = - guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault(actualChannelId.Value) - ?? []; - - // Obviously, we can only check for category-level ignored users if we actually got a category ID. - var categoryIgnoredUsers = - ( - categoryId != null - ? guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault( - categoryId.Value.Value - ) - : [] - ) ?? []; - - // Combine the ignored users in the channel and category, then check if the user is in there. - if (channelIgnoredUsers.Concat(categoryIgnoredUsers).Contains(userId.Value)) - { - _logger.Verbose( - "User {UserId} is ignored in {ChannelId} or its category {CategoryId}", - userId, - channelId, - categoryId - ); - return null; - } - } - - // These three events can be redirected to other channels. Redirects can be on a channel or category level. - // The events are only redirected if they're supposed to be logged in the first place (i.e. GetDefaultLogChannel doesn't return 0) - if (GetDefaultLogChannel(guild, logChannelType) == 0) - { - _logger.Verbose( - "No default log channel for event {EventType}, ignoring event", - logChannelType - ); - return null; - } - - if (guild.Channels.Redirects.TryGetValue(actualChannelId.Value, out var channelRedirect)) - { - _logger.Verbose( - "Messages from channel {ChannelId} should be redirected to {RedirectId}", - actualChannelId, - channelRedirect - ); - return channelRedirect; - } - - var categoryRedirect = - categoryId != null - ? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value) - : 0; - - if (categoryRedirect != 0) - { - _logger.Verbose( - "Messages from categoryId {CategoryId} should be redirected to {RedirectId}", - categoryId, - categoryRedirect - ); - return categoryRedirect; - } - - _logger.Verbose( - "No redirects or ignores for event {EventType}, returning default log channel", - logChannelType - ); - return GetDefaultLogChannel(guild, logChannelType); - } - - private ulong? GetLogChannelForChannelEvent( + private ulong? GetChannelLogChannel( Guild guild, LogChannelType logChannelType, Snowflake channelId ) { - _logger.Verbose( - "Getting log channel for event {Event} in guild {GuildId} and channel {ChannelId}", - logChannelType, - guild.Id, - channelId - ); - if (!channelCache.TryGet(channelId, out var channel)) - { - _logger.Verbose( - "Channel with ID {ChannelId} is not cached, returning default log channel", - channelId - ); return GetDefaultLogChannel(guild, logChannelType); - } - if (!GetChannelAndParentId(channel, out channelId, out var categoryId)) - { - _logger.Verbose( - "Could not get root channel and category ID for channel {ChannelId}, returning default log channel", - channelId - ); - return GetDefaultLogChannel(guild, logChannelType); - } - - // Check if the channel or its category is ignored - if ( - guild.IgnoredChannels.Contains(channelId.Value) - || (categoryId != null && guild.IgnoredChannels.Contains(categoryId.Value.Value)) - ) - { - _logger.Verbose( - "Channel {ChannelId} or its parent {CategoryId} is ignored", - channelId, - categoryId - ); - return null; - } - - _logger.Verbose("Returning default log channel for {EventType}", logChannelType); - return GetDefaultLogChannel(guild, logChannelType); - } - - private bool GetChannelAndParentId( - IChannel channel, - out Snowflake channelId, - out Snowflake? categoryId - ) - { + Snowflake? categoryId; if ( channel.Type is ChannelType.AnnouncementThread @@ -510,16 +329,7 @@ public class WebhookExecutorService( // parent_id should always have a value for threads channelId = channel.ParentID.Value!.Value; if (!channelCache.TryGet(channelId, out var parentChannel)) - { - _logger.Verbose( - "Parent channel for thread {ChannelId} is not in cache, returning the default log channel", - channelId - ); - - channelId = Snowflake.CreateTimestampSnowflake(); - categoryId = null; - return false; - } + return GetDefaultLogChannel(guild, logChannelType); categoryId = parentChannel.ParentID.Value; } else @@ -528,7 +338,94 @@ public class WebhookExecutorService( categoryId = channel.ParentID.Value; } - return true; + // Check if the channel or its category is ignored + if ( + guild.IgnoredChannels.Contains(channelId.Value) + || (categoryId != null && guild.IgnoredChannels.Contains(categoryId.Value.Value)) + ) + return null; + + return GetDefaultLogChannel(guild, logChannelType); + } + + private ulong? GetMessageLogChannel( + Guild guild, + LogChannelType logChannelType, + Snowflake? channelId = null, + ulong? userId = null + ) + { + // Check if the user is ignored globally + if (userId != null && guild.Messages.IgnoredUsers.Contains(userId.Value)) + return null; + + // If the user isn't ignored and we didn't get a channel ID, return the default log channel + 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 + ) + { + // parent_id should always have a value for threads + channelId = channel.ParentID.Value!.Value; + if (!channelCache.TryGet(channelId.Value, out var parentChannel)) + return GetDefaultLogChannel(guild, logChannelType); + categoryId = parentChannel.ParentID.Value; + } + else + { + channelId = channel.ID; + categoryId = channel.ParentID.Value; + } + + // Check if the channel or its category is ignored + if ( + guild.Messages.IgnoredChannels.Contains(channelId.Value.Value) + || categoryId != null && guild.Messages.IgnoredChannels.Contains(categoryId.Value.Value) + ) + return null; + + if (userId != null) + { + // Check the channel-local and category-local ignored users + var channelIgnoredUsers = + guild.Messages.IgnoredUsersPerChannel.GetValueOrDefault(channelId.Value.Value) + ?? []; + var categoryIgnoredUsers = + ( + categoryId != null + ? guild.Messages.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. + // The events are only redirected if they're supposed to be logged in the first place. + if (GetDefaultLogChannel(guild, logChannelType) == 0) + return null; + + var categoryRedirect = + categoryId != null + ? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value) + : 0; + + if (guild.Channels.Redirects.TryGetValue(channelId.Value.Value, out var channelRedirect)) + return channelRedirect; + return categoryRedirect != 0 + ? categoryRedirect + : GetDefaultLogChannel(guild, logChannelType); } public static ulong GetDefaultLogChannel(Guild guild, LogChannelType logChannelType) => diff --git a/Catalogger.Backend/config.example.ini b/Catalogger.Backend/config.example.ini index 3f12c79..9b74c55 100644 --- a/Catalogger.Backend/config.example.ini +++ b/Catalogger.Backend/config.example.ini @@ -7,9 +7,6 @@ LogQueries = false SeqLogUrl = http://localhost:5341 # Whether to enable Prometheus metrics. If disabled, Catalogger will update metrics manually every so often. EnableMetrics = false -# The URL for the Prometheus server. Used for message rate if metrics are enabled. -# Defaults to http://localhost:9090, should be changed if Prometheus is on another server. -PrometheusUrl = http://localhost:9090 [Database] Url = Host=localhost;Database=postgres;Username=postgres;Password=postgres diff --git a/Catalogger.GoImporter/Program.cs b/Catalogger.GoImporter/Program.cs index 3746ef4..a34f08a 100644 --- a/Catalogger.GoImporter/Program.cs +++ b/Catalogger.GoImporter/Program.cs @@ -43,7 +43,7 @@ internal class Program return; } - var db = new DatabasePool(config, null); + var db = new DatabasePool(config, Log.Logger, null); DatabasePool.ConfigureDapper(); if (Environment.GetEnvironmentVariable("MIGRATE") == "true") { diff --git a/README.md b/README.md index e33918a..9b9603c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ Command-line tools for this project can be installed with `dotnet tool restore`. - We use [CSharpier][csharpier] for formatting .NET code. It can be called with `dotnet csharpier .`, but is automatically run by Husky pre-commit. +### Nuget + +We currently use Remora's GitHub packages as the releases on nuget.org are missing some key features. +Add these with `dotnet nuget add source --username --password --store-password-in-clear-text --name Remora "https://nuget.pkg.github.com/Remora/index.json"` + +You must generate a personal access token (classic) [here](personal-access-token). Only give it the `read:packages` permission. + ## Deploying Catalogger yourself The bot itself should run on any server with .NET 8 and PostgreSQL 15 or later. diff --git a/catalogger.sln.DotSettings b/catalogger.sln.DotSettings index 67d3f0f..b3d1d34 100644 --- a/catalogger.sln.DotSettings +++ b/catalogger.sln.DotSettings @@ -1,5 +1,3 @@  - UseVar - UseVar True True \ No newline at end of file