feat(bot): list all server members for whitelist search (gated intent)

To show every server user (not just cached speakers) in the whitelist/blacklist
popup, the bot needs the privileged Server Members Intent. Declaring it while the
Developer Portal toggle is off breaks login, so it's gated behind
WSAI_MEMBERS_INTENT=1: when set, the bot adds GuildMembers intent, fetches each
guild's full member list on ready, and reports it (cap 200→2000). Default off =
unchanged behavior, safe to deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-22 22:20:30 +09:00
parent d1d4bd1514
commit 2ea7d04289

View File

@@ -161,9 +161,14 @@ if (process.argv.includes('--invite')) {
if (!TOKEN) { console.error('no DISCORD_BOT_TOKEN in env or ../.env'); process.exit(2); }
// ---------- client ----------
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
});
// Listing ALL server members needs the privileged "Server Members Intent".
// Gate it behind an env flag: enabling the intent in code while the Developer
// Portal toggle is OFF makes login fail ("disallowed intents"). So this stays
// off until the user turns the portal intent on and sets WSAI_MEMBERS_INTENT=1.
const MEMBERS_INTENT = (process.env.WSAI_MEMBERS_INTENT || env.WSAI_MEMBERS_INTENT) === '1';
const intents = [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates];
if (MEMBERS_INTENT) intents.push(GatewayIntentBits.GuildMembers);
const client = new Client({ intents });
const perUser = new Map(); // userId -> { opusPackets, pcmFrames }
@@ -271,7 +276,7 @@ function buildState() {
roles: [...g.roles.cache.values()]
.filter((r) => r.id !== g.id && r.name !== '@everyone')
.map((r) => ({ id: r.id, name: r.name })),
members: [...g.members.cache.values()].slice(0, 200).map((m) => ({
members: [...g.members.cache.values()].slice(0, 2000).map((m) => ({
id: m.id, name: m.displayName, bot: m.user?.bot === true,
roleIds: [...m.roles.cache.keys()],
})),
@@ -342,6 +347,16 @@ client.once('clientReady', async () => {
log('no default channel — waiting for the dashboard to select a server/voice channel…');
}
// With the members intent on, pull the full member list of each guild so the
// whitelist/blacklist popup can search every user (not just cached speakers).
if (MEMBERS_INTENT) {
for (const g of client.guilds.cache.values()) {
g.members.fetch()
.then((m) => log(`fetched ${m.size} members of "${g.name}"`))
.catch((e) => log(`member fetch failed ("${g.name}"): ${e.message}`));
}
}
// Report state + poll commands forever. This is what powers the dashboard's
// bot info, server/voice-channel pickers, participant list, and join/leave.
reportLoop();