From dc324a307bd603fa1f67de702781c8d0004bc943 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Wed, 3 Dec 2025 13:53:51 -0800 Subject: [PATCH] feat: add script to get discord guild count --- package.json | 8 +- pnpm-lock.yaml | 19 +++-- src/discord/guildCount.ts | 165 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 15 deletions(-) create mode 100644 src/discord/guildCount.ts diff --git a/package.json b/package.json index a20d127..62233f3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "build": "tsc", + "build": "tsc --noEmit", "lint": "eslint src --max-warnings 0", "start": "op run --env-file=prod.env --no-masking -- tsx", "test": "echo \"Error: no test specified\" && exit 0" @@ -14,7 +14,7 @@ "author": "", "license": "ISC", "packageManager": "pnpm@10.15.0", - "devDependencies": { + "dependencies": { "@aws-sdk/client-s3": "3.940.0", "@inquirer/prompts": "7.8.6", "@nhcarrigan/eslint-config": "5.2.0", @@ -22,9 +22,7 @@ "@types/node": "24.3.0", "eslint": "9.34.0", "tsx": "4.20.5", - "typescript": "5.9.2" - }, - "dependencies": { + "typescript": "5.9.2", "@octokit/rest": "22.0.0", "@types/cli-progress": "3.11.6", "cli-progress": "3.12.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba8a3ae..1e9465b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,16 +8,6 @@ importers: .: dependencies: - '@octokit/rest': - specifier: 22.0.0 - version: 22.0.0 - '@types/cli-progress': - specifier: 3.11.6 - version: 3.11.6 - cli-progress: - specifier: 3.12.0 - version: 3.12.0 - devDependencies: '@aws-sdk/client-s3': specifier: 3.940.0 version: 3.940.0 @@ -30,9 +20,18 @@ importers: '@nhcarrigan/typescript-config': specifier: 4.0.0 version: 4.0.0(typescript@5.9.2) + '@octokit/rest': + specifier: 22.0.0 + version: 22.0.0 + '@types/cli-progress': + specifier: 3.11.6 + version: 3.11.6 '@types/node': specifier: 24.3.0 version: 24.3.0 + cli-progress: + specifier: 3.12.0 + version: 3.12.0 eslint: specifier: 9.34.0 version: 9.34.0 diff --git a/src/discord/guildCount.ts b/src/discord/guildCount.ts new file mode 100644 index 0000000..b4122eb --- /dev/null +++ b/src/discord/guildCount.ts @@ -0,0 +1,165 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +/* eslint-disable complexity, max-lines-per-function -- This is a chonky boi script. */ +/** + * DISCORD SERVER STATS ANALYZER + * * Instructions: + * 1. Get your User Token (Open Discord in Browser -> F12 -> Network Tab -> Type a message -> Look for "authorization" in request headers). + * 2. Set it in the .env file as TOKEN. + * 3. Run with: node discord_stats.js. + */ + +interface Guild { + name?: string; + permissions?: string; + features?: Array; + owner?: boolean; + id: string; +} + +interface Stats { + community: Array; + moderating: Array; + owned: Array; + partnered: Array; + total: number; + verified: Array; +} + +// Permission Flags (BigInt) +const permissions = { + administrator: 0x00_00_00_00_00_00_00_08n, + banMembers: 0x00_00_00_00_00_00_00_04n, + kickMembers: 0x00_00_00_00_00_00_00_02n, + manageGuild: 0x00_00_00_00_00_00_00_20n, + manageMessages: 0x00_00_00_00_00_00_20_00n, + moderateMembers: 0x00_00_01_00_00_00_00_00n, +}; + +const printList = (title: string, list: Array, icon: string): void => { + console.log(`${icon} ${title}: ${list.length.toString()}`); + if (list.length > 0) { + for (const name of list) { + console.log(` - ${name}`); + } + } + console.log("\n"); +}; + +const printReport = (stats: Stats): void => { + console.log("\n====== DISCORD SERVER BREAKDOWN ======"); + console.log(`Total Servers Joined: ${stats.total.toString()}\n`); + + printList("Owned Servers", stats.owned, "👑"); + printList("Moderating (Non-Owned)", stats.moderating, "🛡️ "); + printList("Partnered Servers", stats.partnered, "🤝"); + printList("Verified Servers", stats.verified, "✅"); + printList("Community/Public Servers", stats.community, "🌍"); + + console.log(`======================================\n`); +}; + +const checkForPermission = (perms: bigint, permission: bigint): boolean => { + // eslint-disable-next-line no-bitwise -- Since Discord uses bit flags... + return (perms & permission) === permission; +}; + +const analyzeGuilds = (guilds: Array): void => { + // Arrays to store names instead of just counts + const stats: Stats = { + community: [], + moderating: [], + owned: [], + partnered: [], + total: guilds.length, + verified: [], + }; + + for (const guild of guilds) { + const perms = BigInt(guild.permissions ?? 0); + const features = guild.features ?? []; + const { name, owner, id } = guild; + + // 1. Ownership + if (owner === true) { + stats.owned.push(name ?? "Unknown"); + } + + /* + * 2. Moderation + * We consider you a "Moderator" if you have specific mod permissions, + * Even if you don't own the server. + */ + const isModerator + = checkForPermission(perms, permissions.administrator) + || checkForPermission(perms, permissions.manageGuild) + || checkForPermission(perms, permissions.banMembers) + || checkForPermission(perms, permissions.kickMembers) + || checkForPermission(perms, permissions.moderateMembers); + if (isModerator && owner !== true) { + stats.moderating.push(name ?? id); + } + + // 3. Partnered + if (features.includes("PARTNERED")) { + stats.partnered.push(name ?? id); + } + + // 4. Verified + if (features.includes("VERIFIED")) { + stats.verified.push(name ?? id); + } + + /* + * 5. Community / Public + * "COMMUNITY" feature enables public facing screens (Welcome Screen, Rules, etc) + * "DISCOVERABLE" means it appears in Server Discovery + */ + if (features.includes("COMMUNITY") || features.includes("DISCOVERABLE")) { + stats.community.push(name ?? id); + } + } + + printReport(stats); +}; + +/** + * + */ +async function getGuilds(): Promise { + const token = process.env.TOKEN; + + if (token === undefined) { + throw new Error("Missing TOKEN"); + } + console.log("Fetching servers..."); + + // Discord allows max 200 servers per user (with Nitro), so limit=200 catches all. + const response = await fetch( + "https://discord.com/api/v10/users/@me/guilds?limit=200", + { + headers: { + authorization: token, + }, + }, + ); + + if (!response.ok) { + if (response.status === 401) { + console.error("Error: Invalid Token. Please check your TOKEN."); + } else { + console.error(`Error: API returned status ${response.status.toString()}`); + } + return; + } + + const guilds: Array = await response.json(); + analyzeGuilds(guilds); +} + +await getGuilds(); + +export { getGuilds };