diff --git a/.idea/.idea.catalogger/.idea/CSharpierPlugin.xml b/.idea/.idea.catalogger/.idea/CSharpierPlugin.xml new file mode 100644 index 0000000..5e24061 --- /dev/null +++ b/.idea/.idea.catalogger/.idea/CSharpierPlugin.xml @@ -0,0 +1,7 @@ + + + + + \ 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 ebb7a2d..0241b48 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/AuditLogResponder.cs @@ -1,5 +1,4 @@ using Catalogger.Backend.Cache.InMemoryCache; -using Newtonsoft.Json; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Gateway.Responders; using Remora.Results; diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs index 2902bf3..4e272a2 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberAddResponder.cs @@ -5,6 +5,7 @@ using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; +using Microsoft.EntityFrameworkCore; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; @@ -20,6 +21,7 @@ public class GuildMemberAddResponder( ILogger logger, DatabaseContext db, IMemberCache memberCache, + IInviteCache inviteCache, UserCache userCache, WebhookExecutorService webhookExecutor, IDiscordRestGuildAPI guildApi, @@ -70,7 +72,62 @@ public class GuildMemberAddResponder( ); } - // TODO: find used invite + // Bots don't use invites, so the entire following block is useless + if (user.IsBot.OrDefault()) + { + goto afterInvite; + } + + var existingInvites = (await inviteCache.TryGetAsync(member.GuildID)).ToList(); + var newInvitesRes = await guildApi.GetGuildInvitesAsync(member.GuildID, ct); + if (!newInvitesRes.IsSuccess) + { + _logger.Error( + "Could not fetch invites for guild {GuildId}: {Error}", + member.GuildID, + newInvitesRes.Error + ); + + goto afterInvite; + } + + // Update the invite cache immediately--we've already fetched a copy of the invites, after all + await inviteCache.SetAsync(member.GuildID, newInvitesRes.Entity); + + // If we can't find a used invite, and the guild has a vanity link, that invite was used. + // Otherwise, we give up + var usedInvite = FindUsedInvite(existingInvites, newInvitesRes.Entity); + if (usedInvite == null) + { + builder.AddField( + "Invite used", + guildRes is { IsSuccess: true, Entity.VanityUrlCode: not null } + ? $"Vanity invite (`{guildRes.Entity.VanityUrlCode}`)" + : "*Could not determine invite, sorry.*" + ); + + goto afterInvite; + } + + var inviteName = + await db + .Invites.Where(i => i.Code == usedInvite.Code && i.GuildId == member.GuildID.Value) + .Select(i => i.Name) + .FirstOrDefaultAsync(ct) ?? "*(unnamed)*"; + + var inviteDescription = $""" + **Code:** {usedInvite.Code} + **Name:** {inviteName} + **Uses:** {usedInvite.Uses} + **Created at:** + """; + + if (usedInvite.Inviter.IsDefined(out var inviter)) + inviteDescription += $"\n**Created by:** {inviter.Tag()} <@{inviter.ID}>"; + + builder.AddField("Invite used", inviteDescription); + + afterInvite: List embeds = [builder.Build().GetOrThrow()]; @@ -145,4 +202,32 @@ public class GuildMemberAddResponder( return Result.Success; } + + private static IInviteWithMetadata? FindUsedInvite( + List existingInvites, + IReadOnlyList newInvites + ) + { + // First, we check all invites in *both* lists, and look for one that has more uses now than before. + // If one matches that, it's probably the used invite. + var usedInvite = existingInvites.FirstOrDefault(e => + newInvites.Any(n => e.Code == n.Code && e.Uses < n.Uses) + ); + if (usedInvite != null) + return usedInvite; + + // Then we check all new invites (i.e. ones that don't exist in the old list, but do in the new one) + // and check for one that has one use. + usedInvite = newInvites.FirstOrDefault(n => + existingInvites.All(e => e.Code != n.Code) && n.Uses == 1 + ); + if (usedInvite != null) + return usedInvite; + + // Finally, we check invites that exist in the old list but not the new one, and were one use away from expiry. + // If one matches, we can safely say it was the used invite. + return existingInvites.FirstOrDefault(e => + newInvites.All(n => n.Code != e.Code) && e.MaxUses == e.Uses - 1 + ); + } } diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs index 16d9b4f..408ab05 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildMemberRemoveResponder.cs @@ -16,8 +16,7 @@ public class GuildMemberRemoveResponder( DatabaseContext db, IMemberCache memberCache, RoleCache roleCache, - WebhookExecutorService webhookExecutor, - AuditLogEnrichedResponderService auditLogEnrichedResponderService + WebhookExecutorService webhookExecutor ) : IResponder { private readonly ILogger _logger = logger.ForContext(); @@ -26,9 +25,6 @@ public class GuildMemberRemoveResponder( { try { - // spin events that Discord doesn't send us all the data for off to another responder - _ = auditLogEnrichedResponderService.RespondAsync(evt); - var embed = new EmbedBuilder() .WithTitle("Member left") .WithAuthor(evt.User.Tag(), iconUrl: evt.User.AvatarUrl()) diff --git a/Catalogger.Backend/Bot/Responders/MemberUpdate/GuildMemberUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/MemberUpdate/GuildMemberUpdateResponder.cs new file mode 100644 index 0000000..4d5e056 --- /dev/null +++ b/Catalogger.Backend/Bot/Responders/MemberUpdate/GuildMemberUpdateResponder.cs @@ -0,0 +1,38 @@ +using Catalogger.Backend.Cache; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Catalogger.Backend.Bot.Responders.MemberUpdate; + +public class GuildMemberUpdateResponder(ILogger logger, IMemberCache memberCache) + : IResponder +{ + private readonly ILogger _logger = logger.ForContext(); + + public async Task RespondAsync( + IGuildMemberUpdate newMember, + CancellationToken ct = default + ) + { + try + { + var oldMember = await memberCache.TryGetAsync(newMember.GuildID, newMember.User.ID); + if (oldMember == null) + { + _logger.Information( + "Received member update event for {MemberId} in {GuildId} but they weren't cached, ignoring", + newMember.User.ID, + newMember.GuildID + ); + return Result.Success; + } + } + finally + { + await memberCache.UpdateAsync(newMember); + } + + return Result.Success; + } +} diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs new file mode 100644 index 0000000..37a7120 --- /dev/null +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs @@ -0,0 +1,71 @@ +using Catalogger.Backend.Cache.InMemoryCache; +using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Extensions; +using Catalogger.Backend.Services; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Catalogger.Backend.Bot.Responders.Roles; + +public class RoleDeleteResponder( + ILogger logger, + DatabaseContext db, + RoleCache roleCache, + WebhookExecutorService webhookExecutor +) : IResponder +{ + private readonly ILogger _logger = logger.ForContext(); + + public async Task RespondAsync(IGuildRoleDelete evt, CancellationToken ct = default) + { + try + { + if (!roleCache.TryGet(evt.RoleID, out var role)) + { + _logger.Information( + "Received role delete event for {RoleId} but it wasn't cached, ignoring", + evt.RoleID + ); + return Result.Success; + } + + var guildConfig = await db.GetGuildAsync(evt.GuildID, ct); + + var embed = new EmbedBuilder() + .WithTitle($"Role \"{role.Name}\" deleted") + .WithColour(DiscordUtils.Red) + .WithFooter($"ID: {role.ID}") + .WithCurrentTimestamp() + .WithDescription( + $""" + **Name:** {role.Name} + **Colour:** {role.Colour.ToPrettyString()} + **Mentionable:** {role.IsMentionable} + **Shown separately:** {role.IsHoisted} + **Position:** {role.Position} + Created ({role.ID.Timestamp.Prettify()} ago) + """ + ); + + if (!role.Permissions.Value.IsZero) + { + embed.AddField("Permissions", role.Permissions.ToPrettyString()); + } + + webhookExecutor.QueueLog( + guildConfig, + LogChannelType.GuildRoleDelete, + embed.Build().GetOrThrow() + ); + } + finally + { + roleCache.Remove(evt.RoleID, evt.GuildID, out _); + } + + return Result.Success; + } +} diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs index 8caf53a..182245b 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs @@ -78,10 +78,7 @@ public class RoleUpdateResponder( // All updates are shown in fields. If there are no fields, there were no updates we care about // (we don't care about position, for example, because it's not actually useful) if (embed.Fields.Count == 0) - { - _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( diff --git a/Catalogger.Backend/Cache/IInviteCache.cs b/Catalogger.Backend/Cache/IInviteCache.cs index 49a4131..487732b 100644 --- a/Catalogger.Backend/Cache/IInviteCache.cs +++ b/Catalogger.Backend/Cache/IInviteCache.cs @@ -5,6 +5,6 @@ namespace Catalogger.Backend.Cache; public interface IInviteCache { - public Task> TryGetAsync(Snowflake guildId); - public Task SetAsync(Snowflake guildId, IEnumerable invites); + public Task> TryGetAsync(Snowflake guildId); + public Task SetAsync(Snowflake guildId, IEnumerable invites); } diff --git a/Catalogger.Backend/Cache/IMemberCache.cs b/Catalogger.Backend/Cache/IMemberCache.cs index e7a218a..9244d13 100644 --- a/Catalogger.Backend/Cache/IMemberCache.cs +++ b/Catalogger.Backend/Cache/IMemberCache.cs @@ -1,3 +1,4 @@ +using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Rest.Core; @@ -12,4 +13,5 @@ public interface IMemberCache public Task IsGuildCachedAsync(Snowflake guildId); public Task MarkAsCachedAsync(Snowflake guildId); public Task MarkAsUncachedAsync(Snowflake guildId); + public Task UpdateAsync(IGuildMemberUpdate newMember); } diff --git a/Catalogger.Backend/Cache/InMemoryCache/InMemoryInviteCache.cs b/Catalogger.Backend/Cache/InMemoryCache/InMemoryInviteCache.cs index 66c39f7..ef97319 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/InMemoryInviteCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/InMemoryInviteCache.cs @@ -6,14 +6,15 @@ namespace Catalogger.Backend.Cache.InMemoryCache; public class InMemoryInviteCache : IInviteCache { - private readonly ConcurrentDictionary> _invites = new(); + private readonly ConcurrentDictionary> _invites = + new(); - public Task> TryGetAsync(Snowflake guildId) => + public Task> TryGetAsync(Snowflake guildId) => _invites.TryGetValue(guildId, out var invites) ? Task.FromResult(invites) - : Task.FromResult>([]); + : Task.FromResult>([]); - public Task SetAsync(Snowflake guildId, IEnumerable invites) + public Task SetAsync(Snowflake guildId, IEnumerable invites) { _invites[guildId] = invites; return Task.CompletedTask; diff --git a/Catalogger.Backend/Cache/InMemoryCache/InMemoryMemberCache.cs b/Catalogger.Backend/Cache/InMemoryCache/InMemoryMemberCache.cs index d67d20b..69d6a22 100644 --- a/Catalogger.Backend/Cache/InMemoryCache/InMemoryMemberCache.cs +++ b/Catalogger.Backend/Cache/InMemoryCache/InMemoryMemberCache.cs @@ -1,12 +1,19 @@ using System.Collections.Concurrent; +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.Rest.Core; namespace Catalogger.Backend.Cache.InMemoryCache; -public class InMemoryMemberCache : IMemberCache +public class InMemoryMemberCache(IDiscordRestGuildAPI guildApi, ILogger logger) : IMemberCache { - private readonly ConcurrentDictionary<(Snowflake, Snowflake), IGuildMember> _members = new(); + private readonly ILogger _logger = logger.ForContext(); + private readonly ConcurrentDictionary< + (Snowflake GuildId, Snowflake UserId), + IGuildMember + > _members = new(); private readonly ConcurrentDictionary _guilds = new(); #pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. @@ -52,4 +59,56 @@ public class InMemoryMemberCache : IMemberCache _guilds.Remove(guildId, out _); return Task.CompletedTask; } + + public async Task UpdateAsync(IGuildMemberUpdate newMember) + { + if (!_members.TryGetValue((newMember.GuildID, newMember.User.ID), out var oldMember)) + { + _logger.Warning( + "Received member update event for {MemberId} in {GuildId}, but member wasn't found in cache. Fetching them and storing.", + newMember.User.ID, + newMember.GuildID + ); + + var memberResult = await guildApi.GetGuildMemberAsync( + newMember.GuildID, + newMember.User.ID + ); + if (!memberResult.IsSuccess) + { + _logger.Error( + "Could not get uncached member {MemberId} in {GuildId} via REST: {Error}", + newMember.User.ID, + newMember.GuildID, + memberResult.Error + ); + + return; + } + + _members[(newMember.GuildID, newMember.User.ID)] = memberResult.Entity; + return; + } + + // Unwrapping the interface because fuck it, we ball + var member = (GuildMember)oldMember with + { + User = new Optional(newMember.User), + Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname, + Avatar = newMember.Avatar.HasValue ? newMember.Avatar : oldMember.Avatar, + Roles = newMember.Roles.ToArray(), + JoinedAt = newMember.JoinedAt ?? oldMember.JoinedAt, + PremiumSince = newMember.PremiumSince.HasValue + ? newMember.PremiumSince.Value + : oldMember.PremiumSince, + IsPending = newMember.IsPending.HasValue + ? newMember.IsPending.Value + : oldMember.IsPending, + CommunicationDisabledUntil = newMember.CommunicationDisabledUntil.HasValue + ? newMember.CommunicationDisabledUntil.Value + : oldMember.CommunicationDisabledUntil, + }; + + _members[(newMember.GuildID, newMember.User.ID)] = member; + } } diff --git a/Catalogger.Backend/Cache/RedisCache/RedisInviteCache.cs b/Catalogger.Backend/Cache/RedisCache/RedisInviteCache.cs index 95f71b6..f950c0b 100644 --- a/Catalogger.Backend/Cache/RedisCache/RedisInviteCache.cs +++ b/Catalogger.Backend/Cache/RedisCache/RedisInviteCache.cs @@ -8,14 +8,14 @@ namespace Catalogger.Backend.Cache.RedisCache; public class RedisInviteCache(RedisService redisService) : IInviteCache { - public async Task> TryGetAsync(Snowflake guildId) + public async Task> TryGetAsync(Snowflake guildId) { var redisInvites = await redisService.GetAsync>(InvitesKey(guildId)) ?? []; return redisInvites.Select(r => r.ToRemoraInvite()); } - public async Task SetAsync(Snowflake guildId, IEnumerable invites) => + public async Task SetAsync(Snowflake guildId, IEnumerable invites) => await redisService.SetAsync(InvitesKey(guildId), invites.Select(RedisInvite.FromIInvite)); private static string InvitesKey(Snowflake guildId) => $"guild-invites:{guildId}"; @@ -25,24 +25,39 @@ internal record RedisInvite( string Code, RedisPartialGuild? Guild, RedisPartialChannel? Channel, + int Uses, + int MaxUses, + TimeSpan MaxAge, + bool IsTemporary, + DateTimeOffset CreatedAt, RedisUser? Inviter, DateTimeOffset? ExpiresAt ) { - public static RedisInvite FromIInvite(IInvite invite) => + public static RedisInvite FromIInvite(IInviteWithMetadata invite) => new( invite.Code, invite.Guild.Map(RedisPartialGuild.FromIPartialGuild).OrDefault(), invite.Channel != null ? RedisPartialChannel.FromIPartialChannel(invite.Channel) : null, + invite.Uses, + invite.MaxUses, + invite.MaxAge, + invite.IsTemporary, + invite.CreatedAt, invite.Inviter.Map(RedisUser.FromIUser).OrDefault(), invite.ExpiresAt.OrDefault() ); - public Invite ToRemoraInvite() => + public InviteWithMetadata ToRemoraInvite() => new( Code, Guild?.ToRemoraPartialGuild() ?? new Optional(), Channel?.ToRemoraPartialChannel(), + Uses, + MaxUses, + MaxAge, + IsTemporary, + CreatedAt, Inviter?.ToRemoraUser() ?? new Optional(), ExpiresAt: ExpiresAt ); diff --git a/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs b/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs index 7fa45ff..d4aad09 100644 --- a/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs +++ b/Catalogger.Backend/Cache/RedisCache/RedisMemberCache.cs @@ -1,32 +1,46 @@ using Catalogger.Backend.Database.Redis; using Remora.Discord.API; +using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; using Remora.Rest.Core; namespace Catalogger.Backend.Cache.RedisCache; -public class RedisMemberCache(RedisService redisService) : IMemberCache +public class RedisMemberCache( + RedisService redisService, + IDiscordRestGuildAPI guildApi, + ILogger logger +) : IMemberCache { + private readonly ILogger _logger = logger.ForContext(); + public async Task TryGetAsync(Snowflake guildId, Snowflake userId) { - var redisMember = await redisService.GetHashAsync( - GuildMembersKey(guildId), - userId.ToString() - ); + var redisMember = await TryGetInnerAsync(guildId, userId); return redisMember?.ToRemoraMember(); } + private async Task TryGetInnerAsync(Snowflake guildId, Snowflake userId) => + await redisService.GetHashAsync(GuildMembersKey(guildId), userId.ToString()); + public async Task SetAsync(Snowflake guildId, IGuildMember member) { if (!member.User.IsDefined()) throw new CataloggerError( "Member with undefined User passed to RedisMemberCache.SetAsync" ); + + await SetInnerAsync(guildId, RedisMember.FromIGuildMember(member)); + } + + private async Task SetInnerAsync(Snowflake guildId, RedisMember member) + { await redisService.SetHashAsync( GuildMembersKey(guildId), - member.User.Value.ID.ToString(), - RedisMember.FromIGuildMember(member) + member.User.Id.ToString(), + member ); } @@ -59,6 +73,58 @@ public class RedisMemberCache(RedisService redisService) : IMemberCache public async Task MarkAsUncachedAsync(Snowflake guildId) => await redisService.GetDatabase().SetRemoveAsync(GuildCacheKey, guildId.ToString()); + public async Task UpdateAsync(IGuildMemberUpdate newMember) + { + var oldMember = await TryGetInnerAsync(newMember.GuildID, newMember.User.ID); + if (oldMember == null) + { + _logger.Warning( + "Received member update event for {MemberId} in {GuildId}, but member wasn't found in cache. Fetching them and storing.", + newMember.User.ID, + newMember.GuildID + ); + + var memberResult = await guildApi.GetGuildMemberAsync( + newMember.GuildID, + newMember.User.ID + ); + if (!memberResult.IsSuccess) + { + _logger.Error( + "Could not get uncached member {MemberId} in {GuildId} via REST: {Error}", + newMember.User.ID, + newMember.GuildID, + memberResult.Error + ); + + return; + } + + await SetAsync(newMember.GuildID, memberResult.Entity); + return; + } + + var member = oldMember with + { + User = RedisUser.FromIUser(newMember.User), + Nickname = newMember.Nickname.HasValue ? newMember.Nickname.Value : oldMember.Nickname, + Avatar = newMember.Avatar.HasValue ? newMember.Avatar.Value?.Value : oldMember.Avatar, + Roles = newMember.Roles.ToArray(), + JoinedAt = newMember.JoinedAt ?? oldMember.JoinedAt, + PremiumSince = newMember.PremiumSince.HasValue + ? newMember.PremiumSince.Value + : oldMember.PremiumSince, + IsPending = newMember.IsPending.HasValue + ? newMember.IsPending.Value + : oldMember.IsPending, + CommunicationDisabledUntil = newMember.CommunicationDisabledUntil.HasValue + ? newMember.CommunicationDisabledUntil.Value + : oldMember.CommunicationDisabledUntil, + }; + + await SetInnerAsync(newMember.GuildID, member); + } + private const string GuildCacheKey = "cached-guilds"; private static string GuildMembersKey(Snowflake guildId) => $"guild-members:{guildId}"; diff --git a/Catalogger.Backend/Catalogger.Backend.csproj b/Catalogger.Backend/Catalogger.Backend.csproj index 221283c..941d136 100644 --- a/Catalogger.Backend/Catalogger.Backend.csproj +++ b/Catalogger.Backend/Catalogger.Backend.csproj @@ -6,6 +6,20 @@ enable + + + + + + + + + + + + + + diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs index cc69d6a..1d4b822 100644 --- a/Catalogger.Backend/Extensions/StartupExtensions.cs +++ b/Catalogger.Backend/Extensions/StartupExtensions.cs @@ -85,7 +85,6 @@ public static class StartupExtensions .AddSingleton() .AddScoped() .AddSingleton() - .AddSingleton() .AddScoped() .AddSingleton() .AddSingleton() diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index c69d272..1f950a2 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json.Serialization; using Prometheus; using Remora.Commands.Extensions; using Remora.Discord.API.Abstractions.Gateway.Commands; +using Remora.Discord.Caching.Extensions; using Remora.Discord.Commands.Extensions; using Remora.Discord.Extensions.Extensions; using Remora.Discord.Gateway; @@ -42,6 +43,7 @@ builder | GatewayIntents.MessageContents | GatewayIntents.GuildEmojisAndStickers ) + .AddDiscordCaching() .AddDiscordCommands(enableSlash: true, useDefaultCommandResponder: false) .AddCommandTree() // Start command tree diff --git a/Catalogger.Backend/Services/AuditLogEnrichedResponderService.cs b/Catalogger.Backend/Services/AuditLogEnrichedResponderService.cs deleted file mode 100644 index 1d90af2..0000000 --- a/Catalogger.Backend/Services/AuditLogEnrichedResponderService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Catalogger.Backend.Cache.InMemoryCache; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Results; - -namespace Catalogger.Backend.Services; - -public class AuditLogEnrichedResponderService(AuditLogCache auditLogCache, ILogger logger) -{ - private readonly ILogger _logger = logger.ForContext(); - - public async Task RespondAsync(IGuildMemberRemove evt) - { - // give a second or so for the audit log to catch up - await Task.Delay(1000); - - if (auditLogCache.TryGetKick(evt.GuildID, evt.User.ID, out var kickData)) - return await HandleKickAsync(evt, kickData); - - 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 - ); - - return Result.Success; - } - - private async Task HandleKickAsync( - IGuildMemberRemove evt, - AuditLogCache.ActionData kickData - ) - { - return Result.Success; - } - - private async Task HandleBanAsync( - IGuildMemberRemove evt, - AuditLogCache.ActionData banData - ) - { - return Result.Success; - } -} diff --git a/global.json b/global.json new file mode 100644 index 0000000..cae40ce --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "msbuild-sdks": { + "Remora.Sdk": "3.1.2" + } +} \ No newline at end of file