feat: role delete logging, used invite logging, also some random changes

This commit is contained in:
sam 2024-10-09 22:31:58 +02:00
parent 4f54077c68
commit c906a4d6b6
18 changed files with 386 additions and 76 deletions

View file

@ -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;

View file

@ -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:** <t:{usedInvite.CreatedAt.ToUnixTimeSeconds()}>
""";
if (usedInvite.Inviter.IsDefined(out var inviter))
inviteDescription += $"\n**Created by:** {inviter.Tag()} <@{inviter.ID}>";
builder.AddField("Invite used", inviteDescription);
afterInvite:
List<Embed> embeds = [builder.Build().GetOrThrow()];
@ -145,4 +202,32 @@ public class GuildMemberAddResponder(
return Result.Success;
}
private static IInviteWithMetadata? FindUsedInvite(
List<IInviteWithMetadata> existingInvites,
IReadOnlyList<IInviteWithMetadata> 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
);
}
}

View file

@ -16,8 +16,7 @@ public class GuildMemberRemoveResponder(
DatabaseContext db,
IMemberCache memberCache,
RoleCache roleCache,
WebhookExecutorService webhookExecutor,
AuditLogEnrichedResponderService auditLogEnrichedResponderService
WebhookExecutorService webhookExecutor
) : IResponder<IGuildMemberRemove>
{
private readonly ILogger _logger = logger.ForContext<GuildMemberAddResponder>();
@ -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())