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
JavaScript
"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;