feat(bot+dashboard): whitelist/blacklist listen filter (users + roles)
- Per-guild listen filter stored in the control plane (BotControl) with GET/POST /api/bot/lists; the filter also rides along in the bot report response so the bot always has the latest config. - 화이트리스트/블랙리스트 popup: search the guild's members OR roles (type selector), add/remove to white/black, save. Whitelist = listen to only those (empty = everyone); blacklist = exclude. Reuses the shared 뒤로가기 modal. - Bot reports guild roles + known members for the search UI, and filters incoming audio via a pure, unit-tested isAllowed() (dave/filter.mjs): blacklist always excludes; a non-empty whitelist restricts; else everyone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
22
dave/bot.mjs
22
dave/bot.mjs
@@ -36,6 +36,7 @@ import {
|
||||
NoSubscriberBehavior,
|
||||
} from '@discordjs/voice';
|
||||
import prism from 'prism-media';
|
||||
import { isAllowed } from './filter.mjs';
|
||||
|
||||
// ---------- config ----------
|
||||
function loadEnvFile() {
|
||||
@@ -164,6 +165,7 @@ const perUser = new Map(); // userId -> { opusPackets, pcmFrames }
|
||||
let currentGuildId = null, currentChannelId = null, currentChannelName = null;
|
||||
const speakingSet = new Set(); // userIds currently speaking (for participant list)
|
||||
const activeSubs = new Set(); // userIds with an in-flight receive subscription
|
||||
let listsByGuild = {}; // guildId -> {whitelistUsers, blacklistUsers, whitelistRoles, blacklistRoles}
|
||||
|
||||
// Attach the bot's audio player (so it can speak) to a fresh connection.
|
||||
function setupPlayer(connection) {
|
||||
@@ -178,6 +180,16 @@ function setupReceiver(connection) {
|
||||
receiver.speaking.on('start', (userId) => {
|
||||
speakingSet.add(userId);
|
||||
if (userId === client.user.id || activeSubs.has(userId)) return;
|
||||
// Whitelist/blacklist: skip speakers we are configured not to listen to.
|
||||
const lists = listsByGuild[currentGuildId];
|
||||
if (lists) {
|
||||
const member = client.guilds.cache.get(currentGuildId)?.members.cache.get(userId);
|
||||
const roleIds = member ? [...member.roles.cache.keys()] : [];
|
||||
if (!isAllowed(userId, roleIds, lists)) {
|
||||
logThrottled(`skip:${userId}`, `skip user=${userId} (listen filter)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
activeSubs.add(userId);
|
||||
if (!perUser.has(userId)) perUser.set(userId, { opusPackets: 0, pcmFrames: 0 });
|
||||
const opusStream = receiver.subscribe(userId, {
|
||||
@@ -248,6 +260,15 @@ function buildState() {
|
||||
voiceChannels: [...g.channels.cache.values()]
|
||||
.filter((c) => c.isVoiceBased())
|
||||
.map((c) => ({ id: c.id, name: c.name })),
|
||||
// Roles (minus @everyone) and known members, so the dashboard's
|
||||
// whitelist/blacklist popup can search by user OR role.
|
||||
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) => ({
|
||||
id: m.id, name: m.displayName, bot: m.user?.bot === true,
|
||||
roleIds: [...m.roles.cache.keys()],
|
||||
})),
|
||||
}));
|
||||
let members = [];
|
||||
if (currentGuildId && currentChannelId) {
|
||||
@@ -281,6 +302,7 @@ async function reportLoop() {
|
||||
});
|
||||
j = await r.json();
|
||||
} catch { return; } // dashboard down: keep running, retry next tick
|
||||
if (j?.lists) listsByGuild = j.lists; // latest whitelist/blacklist config
|
||||
for (const cmd of (j?.commands || [])) {
|
||||
try { await handleCommand(cmd); } catch (e) { log(`command ${cmd?.type} failed: ${e.message}`); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user