From 0d87778b78b53368f34452b11b267a75037e2215 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 1 Jun 2026 10:30:58 +0900 Subject: [PATCH] chore(scripts): add Discord bot permission diagnostic One-off Bun script that logs each configured Discord bot in (owner / reviewer / arbiter), walks every guild it sees, and prints the bot member's effective permission bits for the capabilities we plan to expose (ReadMessageHistory, ManageMessages, ManageChannels, AddReactions, thread perms, etc.). Used to verify "current permissions support feature X" empirically instead of guessing from intents. --- scripts/diagnose-discord-perms.ts | 90 +++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 scripts/diagnose-discord-perms.ts 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); + } +}