From 9483d4836ff0711135e19322208d88fbcb66b38f Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 1 Jun 2026 17:28:17 +0900 Subject: [PATCH] chore(scripts): add Discord bot permission diagnostic One-off script that logs in with each configured Discord bot token and prints the bot member's effective permission bits per guild, so capability claims can be verified empirically instead of inferred from intents. Co-Authored-By: Claude Opus 4 --- scripts/diagnose-discord-perms.ts | 98 +++++++++++++++++++++++++++++++ src/channels/discord.test.ts | 4 +- 2 files changed, 101 insertions(+), 1 deletion(-) 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..0a867c2 --- /dev/null +++ b/scripts/diagnose-discord-perms.ts @@ -0,0 +1,98 @@ +#!/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); + } +} diff --git a/src/channels/discord.test.ts b/src/channels/discord.test.ts index 438a9aa..2463ca7 100644 --- a/src/channels/discord.test.ts +++ b/src/channels/discord.test.ts @@ -1226,7 +1226,9 @@ describe('chunkForDiscord', () => { const chunks = chunkForDiscord(text, 9); for (const c of chunks) { // No chunk should start or end mid surrogate pair. - expect(c.charCodeAt(0) >= 0xdc00 && c.charCodeAt(0) <= 0xdfff).toBe(false); + expect(c.charCodeAt(0) >= 0xdc00 && c.charCodeAt(0) <= 0xdfff).toBe( + false, + ); const last = c.charCodeAt(c.length - 1); expect(last >= 0xd800 && last <= 0xdbff).toBe(false); }