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