// Best-effort invite-usage attribution (Phase 6b). Discord doesn't tell you // which invite a member used, so the standard approach is to keep a cache of // each invite's use-count and, on guildMemberAdd, re-fetch and find the one // whose count went up. Requires the GuildInvites intent + Manage Guild (the bot // already creates/deletes invites, so it has the permission). All calls are // best-effort: any failure just yields a null attribution and the join is still // recorded. Vanity-URL and bot-added joins are inherently unattributable. const createLogger = require('../utils/logger') const log = createLogger('invites') // guildId -> Map const cache = new Map() async function snapshot(guild) { const map = new Map() const invites = await guild.invites.fetch() for (const inv of invites.values()) map.set(inv.code, inv.uses || 0) return map } // Populate the cache for a guild (call once the client is ready). async function prime(client, guildId) { try { const guild = client.guilds.cache.get(guildId) || (await client.guilds.fetch(guildId)) cache.set(guildId, await snapshot(guild)) log.info('invite cache primed', { guildId, count: cache.get(guildId).size }) } catch (err) { log.warn('invite cache prime failed (missing Manage Guild / GuildInvites?)', { message: err.message }) } } function onInviteCreate(invite) { if (!invite.guild) return const g = cache.get(invite.guild.id) || new Map() g.set(invite.code, invite.uses || 0) cache.set(invite.guild.id, g) } function onInviteDelete(invite) { if (!invite.guild) return const g = cache.get(invite.guild.id) if (g) g.delete(invite.code) } // Diff current invite uses against the cached snapshot to find which invite the // joining member used, then refresh the cache. Returns { code, inviterId, // inviterTag } with nulls when it can't be determined. async function attribute(member) { const empty = { code: null, inviterId: null, inviterTag: null } try { const guild = member.guild const before = cache.get(guild.id) || new Map() const current = await guild.invites.fetch() let found = empty for (const inv of current.values()) { const prev = before.get(inv.code) || 0 if ((inv.uses || 0) > prev && found === empty) { found = { code: inv.code, inviterId: inv.inviter?.id || null, inviterTag: inv.inviter?.tag || null } } } const next = new Map() for (const inv of current.values()) next.set(inv.code, inv.uses || 0) cache.set(guild.id, next) return found } catch (err) { log.warn('invite attribution failed', { message: err.message }) return empty } } module.exports = { prime, onInviteCreate, onInviteDelete, attribute }