UNPKG

invitron

Version:

A powerful Discord.js invite tracker with persistent storage, member analytics, vanity URL support, and comprehensive join monitoring system for Discord bots.

286 lines (285 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InviteTracker = void 0; const discord_js_1 = require("discord.js"); const Storage_1 = require("./Storage"); class InviteTracker { constructor(client, options = {}) { this.cache = {}; this.counts = {}; this.client = client; this.options = options; // Initialize storage if enabled if (options.storage?.enabled) { this.storage = new Storage_1.JSONStorage(options.storage.path); this.counts = this.storage.load() || {}; } // Ensure required intents are enabled if (!client.options.intents?.has(discord_js_1.GatewayIntentBits.GuildInvites) || !client.options.intents?.has(discord_js_1.GatewayIntentBits.GuildMembers)) { throw new Error("InvitesTracker Error: Missing intents (GuildInvites, GuildMembers)!"); } // Preload invites from all guilds on client ready if (options.fetchGuilds) { this.client.once("ready", async () => { for (const [, guild] of this.client.guilds.cache) { await this.loadGuildInvites(guild); } }); } // Track invite creation events this.client.on("inviteCreate", (invite) => { this.handleInviteCreate(invite); if (options.debug) console.log(`[DEBUG] Invite created: ${invite.code}`); }); // Track invite deletion events this.client.on("inviteDelete", (invite) => { this.handleInviteDelete(invite); if (options.debug) console.log(`[DEBUG] Invite deleted: ${invite.code}`); }); // Track member leave events this.client.on("guildMemberRemove", (member) => { this.handleMemberLeave(member); }); } /** Persist current invite counts to storage */ persist() { if (this.storage) { this.storage.save(this.counts); } } /** Load all current invites for a guild into cache */ async loadGuildInvites(guild) { try { const invites = await guild.invites.fetch(); this.cache[guild.id] = {}; invites.forEach((inv) => { this.cache[guild.id][inv.code] = inv.uses ?? 0; }); } catch { this.cache[guild.id] = {}; } } /** Handle new invite creation and update cache */ handleInviteCreate(invite) { if (!invite.guild) return; this.cache[invite.guild.id] = this.cache[invite.guild.id] ?? {}; this.cache[invite.guild.id][invite.code] = invite.uses ?? 0; } /** Handle invite deletion and update cache */ handleInviteDelete(invite) { if (!invite.guild) return; if (this.cache[invite.guild.id]) delete this.cache[invite.guild.id][invite.code]; } /** * Handle member joining a guild * Updates inviter stats based on which invite was used */ async handleMemberJoin(member, options) { if (this.options.ignoreBots && member.user.bot) { if (this.options.debug) { console.log(`[DEBUG] Ignored bot join: ${member.user.tag}`); } return null; } try { const newInvites = await member.guild.invites.fetch(); const oldInvites = this.cache[member.guild.id] ?? {}; let usedInvite = null; // Determine which invite was used for (const [code, inv] of newInvites) { const before = oldInvites[code] ?? 0; const after = inv.uses ?? 0; if (after > before) { usedInvite = inv; break; } } // Update cache with current invite uses this.cache[member.guild.id] = { ...this.cache[member.guild.id] }; newInvites.forEach((inv) => { this.cache[member.guild.id][inv.code] = inv.uses ?? 0; }); // Process normal invite if (usedInvite) { const inviter = usedInvite.inviter ?? null; if (inviter) { const guildId = member.guild.id; const inviterId = inviter.id; const code = usedInvite.code; if (!this.counts[guildId]) this.counts[guildId] = {}; if (!this.counts[guildId][inviterId]) { this.counts[guildId][inviterId] = { total: 0, codes: {} }; } if (!this.counts[guildId][inviterId].codes[code]) { this.counts[guildId][inviterId].codes[code] = { uses: 0, members: [] }; } this.counts[guildId][inviterId].total++; this.counts[guildId][inviterId].codes[code].uses++; this.counts[guildId][inviterId].codes[code].members.push(member.id); this.persist(); } return { guildId: member.guild.id, code: usedInvite.code, url: usedInvite.url, uses: usedInvite.uses ?? null, maxUses: usedInvite.maxUses ?? null, maxAge: usedInvite.maxAge ?? null, createdTimestamp: usedInvite.createdTimestamp ?? null, inviter, totalInvites: inviter ? this.getUserInvites(member.guild.id, inviter.id) : null, type: "normal", }; } // Handle vanity URL if (options.fetchVanity && member.guild.vanityURLCode) { return { guildId: member.guild.id, code: member.guild.vanityURLCode, url: `https://discord.gg/${member.guild.vanityURLCode}`, uses: null, maxUses: null, maxAge: null, createdTimestamp: null, inviter: null, totalInvites: null, type: "vanity", }; } // Handle audit logs as fallback if (options.fetchAuditLogs) { try { const logs = await member.guild.fetchAuditLogs({ limit: 1, type: 28 }); const entry = logs.entries.first(); if (entry && entry.target?.id === member.id) { return { guildId: member.guild.id, code: "audit-log", url: null, uses: null, maxUses: null, maxAge: null, createdTimestamp: entry.createdTimestamp, inviter: entry.executor ?? null, totalInvites: entry.executor ? this.getUserInvites(member.guild.id, entry.executor.id) : null, type: "audit", }; } } catch { // Ignore errors } } // Unknown join type return { guildId: member.guild.id, code: "unknown", url: null, uses: null, maxUses: null, maxAge: null, createdTimestamp: null, inviter: null, totalInvites: null, type: "unknown", }; } catch { return null; } } /** * Handle member leaving a guild * Deducts invites if configured to do so */ handleMemberLeave(member) { if (this.options.ignoreBots && member.user?.bot) return; if (this.options.deductOnLeave) { const guildId = member.guild.id; const guildData = this.counts[guildId]; if (!guildData) return; for (const [inviterId, inviterData] of Object.entries(guildData)) { for (const [code, codeData] of Object.entries(inviterData.codes)) { const index = codeData.members.indexOf(member.id); if (index !== -1) { // Remove member from invite record codeData.members.splice(index, 1); // Deduct usage count if (codeData.uses > 0) codeData.uses--; // Deduct from total invites if (inviterData.total > 0) inviterData.total--; if (this.options.debug) { console.log(`[DEBUG] Member ${member.id} left, deducted invite from inviter ${inviterId}, code ${code}`); } // Save and exit after deduction this.persist(); return; } } } } this.persist(); } /** Clean invalid members from guild invite counts */ async cleanGuild(guildId) { const guild = this.client.guilds.cache.get(guildId); if (!guild) return; const members = await guild.members.fetch(); const validIds = new Set(members.map((m) => m.id)); if (this.counts[guildId]) { for (const [inviterId, inviterData] of Object.entries(this.counts[guildId])) { for (const [code, codeData] of Object.entries(inviterData.codes)) { codeData.members = codeData.members.filter((m) => validIds.has(m)); codeData.uses = codeData.members.length; } inviterData.total = Object.values(inviterData.codes).reduce((acc, c) => acc + c.uses, 0); } } this.persist(); } /** Get total invites for a user in a guild */ getUserInvites(guildId, userId) { return this.counts[guildId]?.[userId]?.total ?? 0; } /** Reset all invite data for a guild */ resetGuild(guildId) { this.counts[guildId] = {}; this.persist(); } /** Get the top inviter in a guild */ getTopInviter(guildId) { const guildInvites = this.counts[guildId]; if (!guildInvites) return null; const [userId, data] = Object.entries(guildInvites).sort((a, b) => b[1].total - a[1].total)[0] || []; return userId ? { userId, invites: data.total } : null; } /** Get the leaderboard for a guild, optionally limited */ getLeaderboard(guildId, limit = 10) { const guildInvites = this.counts[guildId]; if (!guildInvites) return []; return Object.entries(guildInvites) .sort((a, b) => b[1].total - a[1].total) .slice(0, limit) .map(([userId, data]) => ({ userId, invites: data.total })); } } exports.InviteTracker = InviteTracker;