From 2ea7d042899baefd51526eb2548a32772d89b81d Mon Sep 17 00:00:00 2001 From: EJClaw Date: Sat, 22 Aug 2026 22:20:30 +0900 Subject: [PATCH] feat(bot): list all server members for whitelist search (gated intent) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- dave/bot.mjs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/dave/bot.mjs b/dave/bot.mjs index a0fad90..e8e710c 100644 --- a/dave/bot.mjs +++ b/dave/bot.mjs @@ -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();