diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs index 408ab05..a0cf287 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs @@ -16,6 +16,8 @@ public class GuildMemberRemoveResponder( DatabaseContext db, IMemberCache memberCache, RoleCache roleCache, + UserCache userCache, + AuditLogCache auditLogCache, WebhookExecutorService webhookExecutor ) : IResponder { @@ -79,6 +81,33 @@ public class GuildMemberRemoveResponder( LogChannelType.GuildMemberRemove, embed.Build().GetOrThrow() ); + + // Check for a kick audit log event. We don't get a separate "kick" event so we have to check this manually + await Task.Delay(2000, ct); + if (!auditLogCache.TryGetKick(evt.GuildID, evt.User.ID, out var actionData)) + { + return Result.Success; + } + + var kick = new EmbedBuilder() + .WithTitle("User kicked") + .WithAuthor(evt.User.Tag(), iconUrl: evt.User.AvatarUrl()) + .WithColour(DiscordUtils.Red) + .WithCurrentTimestamp() + .WithDescription($"<@{evt.User.ID}>"); + + kick.AddField( + "Responsible moderator", + await userCache.TryFormatModeratorAsync(actionData) + ); + kick.AddField("Reason", actionData.Reason ?? "No reason given"); + + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.GuildMemberKick, + kick.Build().GetOrThrow() + ); + return Result.Success; } finally diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberUpdateResponder.cs index 198c062..e349372 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberUpdateResponder.cs @@ -220,14 +220,8 @@ public class GuildMemberUpdateResponder( if (auditLogCache.TryGetMemberUpdate(member.GuildID, member.User.ID, out var actionData)) { - var moderator = await userCache.GetUserAsync(actionData.ModeratorId); - embed.AddField( - "Responsible moderator", - moderator == null - ? $"*(unknown user {actionData.ModeratorId}) <@{actionData.ModeratorId}>*" - : $"{moderator.Tag()} <@{moderator.ID}>" - ); - + var moderator = await userCache.TryFormatModeratorAsync(actionData); + embed.AddField("Responsible moderator", moderator); embed.AddField("Reason", actionData.Reason ?? "No reason given"); } else @@ -337,13 +331,8 @@ public class GuildMemberUpdateResponder( auditLogCache.TryGetMemberUpdate(member.GuildID, member.User.ID, out var actionData) ) { - var moderator = await userCache.GetUserAsync(actionData.ModeratorId); - keyRoleUpdate.AddField( - "Responsible moderator", - moderator == null - ? $"*(unknown user {actionData.ModeratorId}) <@{actionData.ModeratorId}>*" - : $"{moderator.Tag()} <@{moderator.ID}>" - ); + var moderator = await userCache.TryFormatModeratorAsync(actionData); + keyRoleUpdate.AddField("Responsible moderator", moderator); } else { diff --git a/Catalogger.Backend/Catalogger.Backend.csproj b/Catalogger.Backend/Catalogger.Backend.csproj index 941d136..baf29b4 100644 --- a/Catalogger.Backend/Catalogger.Backend.csproj +++ b/Catalogger.Backend/Catalogger.Backend.csproj @@ -6,48 +6,34 @@ enable - - - - - - - - - - - - - - - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/Catalogger.Backend/Extensions/DiscordExtensions.cs b/Catalogger.Backend/Extensions/DiscordExtensions.cs index 6249716..9487a88 100644 --- a/Catalogger.Backend/Extensions/DiscordExtensions.cs +++ b/Catalogger.Backend/Extensions/DiscordExtensions.cs @@ -1,4 +1,5 @@ using System.Drawing; +using Catalogger.Backend.Cache.InMemoryCache; using Humanizer; using OneOf; using Remora.Discord.API.Abstractions.Gateway.Events; @@ -8,6 +9,7 @@ using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Extensions; using Remora.Discord.Commands.Services; +using Remora.Discord.Extensions.Embeds; using Remora.Rest.Core; using Remora.Results; @@ -117,26 +119,6 @@ public static class DiscordExtensions 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, - interaction.Token, - new InteractionResponse( - InteractionCallbackType.UpdateMessage, - new Optional< - OneOf< - IInteractionMessageCallbackData, - IInteractionAutocompleteCallbackData, - IInteractionModalCallbackData - > - >(data) - ) - ); - public static string ToPrettyString(this IDiscordPermissionSet permissionSet) => string.Join( ", ", @@ -172,5 +154,16 @@ public static class DiscordExtensions return filterByIds != null ? sorted.Where(r => filterByIds.Contains(r.ID)) : sorted; } + public static async Task TryFormatModeratorAsync( + this UserCache userCache, + AuditLogCache.ActionData actionData + ) + { + var moderator = await userCache.GetUserAsync(actionData.ModeratorId); + return moderator != null + ? $"{moderator.Tag()} <@{moderator.ID}>" + : $"*(unknown user {actionData.ModeratorId}) <@{actionData.ModeratorId}>*"; + } + public class DiscordRestException(string message) : Exception(message); } diff --git a/Catalogger.Backend/Extensions/DiscordRestExtensions.cs b/Catalogger.Backend/Extensions/DiscordRestExtensions.cs new file mode 100644 index 0000000..a0a068c --- /dev/null +++ b/Catalogger.Backend/Extensions/DiscordRestExtensions.cs @@ -0,0 +1,31 @@ +using OneOf; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; +using Remora.Results; + +namespace Catalogger.Backend.Extensions; + +public static class DiscordRestExtensions +{ + 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< + OneOf< + IInteractionMessageCallbackData, + IInteractionAutocompleteCallbackData, + IInteractionModalCallbackData + > + >(data) + ) + ); +} diff --git a/Catalogger.Backend/Services/PluralkitApiService.cs b/Catalogger.Backend/Services/PluralkitApiService.cs index 5bc7702..e304764 100644 --- a/Catalogger.Backend/Services/PluralkitApiService.cs +++ b/Catalogger.Backend/Services/PluralkitApiService.cs @@ -15,6 +15,15 @@ public class PluralkitApiService(ILogger logger) private const string ApiBaseUrl = "https://api.pluralkit.me/v2"; private readonly HttpClient _client = new(); private readonly ILogger _logger = logger.ForContext(); + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + }.ConfigureForNodaTime( + new NodaJsonSettings + { + InstantConverter = new NodaPatternConverter(InstantPattern.ExtendedIso), + } + ); private readonly ResiliencePipeline _pipeline = new ResiliencePipelineBuilder() .AddRateLimiter( @@ -59,17 +68,7 @@ public class PluralkitApiService(ILogger logger) throw new CataloggerError("Non-200 status code from PluralKit API"); } - var jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - }.ConfigureForNodaTime( - new NodaJsonSettings - { - InstantConverter = new NodaPatternConverter(InstantPattern.ExtendedIso), - } - ); - - return await resp.Content.ReadFromJsonAsync(jsonOptions, ct) + return await resp.Content.ReadFromJsonAsync(_jsonOptions, ct) ?? throw new CataloggerError("JSON response from PluralKit API was null"); } diff --git a/README.md b/README.md index 545267f..81e44e2 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. + ## License Copyright (C) 2021-present sam (https://starshines.gay) @@ -30,3 +37,4 @@ along with this program. If not, see . [old-repo]: https://github.com/starshine-sys/catalogger/tree/main [husky]: https://github.com/alirezanet/Husky.Net [csharpier]: https://csharpier.com/ +[personal-access-token]: https://github.com/settings/tokens \ No newline at end of file