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 <noreply@anthropic.com>
This commit is contained in:
claude-bot
2026-06-01 17:28:17 +09:00
parent 5f21c27c66
commit 9483d4836f
2 changed files with 101 additions and 1 deletions

View File

@@ -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<string, string> {
const envPath = path.join(import.meta.dir, '..', '.env');
if (!fs.existsSync(envPath)) return {};
const out: Record<string, string> = {};
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<void>((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<string, boolean> = {};
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);
}
}

View File

@@ -1226,7 +1226,9 @@ describe('chunkForDiscord', () => {
const chunks = chunkForDiscord(text, 9); const chunks = chunkForDiscord(text, 9);
for (const c of chunks) { for (const c of chunks) {
// No chunk should start or end mid surrogate pair. // 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); const last = c.charCodeAt(c.length - 1);
expect(last >= 0xd800 && last <= 0xdbff).toBe(false); expect(last >= 0xd800 && last <= 0xdbff).toBe(false);
} }