diff --git a/scripts/diagnose-discord-perms.ts b/scripts/diagnose-discord-perms.ts new file mode 100644 index 0000000..6f81048 --- /dev/null +++ b/scripts/diagnose-discord-perms.ts @@ -0,0 +1,90 @@ +#!/usr/bin/env bun +/** + * One-off diagnostic. Logs in with each configured Discord bot token, walks + * every guild it sees, and prints the bot member's effective permission bits. + * + * Output is meant to verify capability claims like "we have Read Message + * History" / "we have Manage Channels" empirically rather than guessing from + * intents. + */ +import { Client, GatewayIntentBits, PermissionsBitField } from 'discord.js'; +import fs from 'fs'; +import path from 'path'; + +function loadEnv(): Record { + const envPath = path.join(import.meta.dir, '..', '.env'); + if (!fs.existsSync(envPath)) return {}; + const out: Record = {}; + for (const line of fs.readFileSync(envPath, 'utf-8').split('\n')) { + const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/); + if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, ''); + } + return out; +} + +const env = loadEnv(); +const TOKENS: Array<[string, string]> = [ + ['owner', env.DISCORD_OWNER_BOT_TOKEN ?? ''], + ['reviewer', env.DISCORD_REVIEWER_BOT_TOKEN ?? ''], + ['arbiter', env.DISCORD_ARBITER_BOT_TOKEN ?? ''], +].filter(([, t]) => t.length > 0) as Array<[string, string]>; + +// Subset of permissions we actually care about for the capability matrix. +const RELEVANT = [ + 'ViewChannel', + 'SendMessages', + 'EmbedLinks', + 'AttachFiles', + 'ReadMessageHistory', + 'AddReactions', + 'ManageMessages', + 'ManageChannels', + 'ManageThreads', + 'CreatePublicThreads', + 'CreatePrivateThreads', + 'SendMessagesInThreads', + 'MentionEveryone', + 'UseExternalEmojis', +] as const; + +async function diagnose(role: string, token: string) { + const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.DirectMessages, + ], + }); + + await new Promise((resolve, reject) => { + client.once('ready', () => resolve()); + client.once('error', reject); + client.login(token).catch(reject); + }); + + console.log(`\n=== role=${role} bot=${client.user?.tag} (id=${client.user?.id}) ===`); + const guilds = await client.guilds.fetch(); + for (const [gid, partial] of guilds) { + const guild = await partial.fetch(); + const me = await guild.members.fetchMe(); + const perms = me.permissions; + const has: Record = {}; + for (const name of RELEVANT) { + has[name] = perms.has(PermissionsBitField.Flags[name as keyof typeof PermissionsBitField.Flags]); + } + console.log(` guild=${guild.name} (id=${gid})`); + console.log(` admin=${perms.has(PermissionsBitField.Flags.Administrator)}`); + for (const name of RELEVANT) console.log(` ${name}=${has[name]}`); + } + client.destroy(); +} + +for (const [role, token] of TOKENS) { + try { + await diagnose(role, token); + } catch (err) { + console.log(`\n=== role=${role} FAILED ===`); + console.log(err); + } +}