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:
EJClaw
2026-08-22 12:06:03 +09:00
parent 7eb590b729
commit 441ab4831f
4 changed files with 163 additions and 3 deletions

View File

@@ -29,6 +29,36 @@ class BotControl:
self._state: dict[str, Any] = {"connected": False, "ts": 0.0}
self._commands: deque[dict[str, Any]] = deque()
self._cmd_id = 0
# Per-guild listen filter. Empty whitelist => listen to everyone;
# blacklist always excludes. Users and roles both supported.
self._lists: dict[str, dict[str, Any]] = {}
# -- whitelist / blacklist (per guild) ------------------------------- #
@staticmethod
def _empty_lists() -> dict[str, Any]:
return {"whitelistUsers": [], "blacklistUsers": [],
"whitelistRoles": [], "blacklistRoles": []}
def get_lists(self, guild_id: str) -> dict[str, Any]:
with self._lock:
return dict(self._lists.get(guild_id) or self._empty_lists())
def all_lists(self) -> dict[str, Any]:
with self._lock:
return {g: dict(v) for g, v in self._lists.items()}
def set_lists(self, guild_id: str, lists: dict[str, Any]) -> dict[str, Any]:
clean = self._empty_lists()
for key in clean:
items = lists.get(key) or []
# Normalise to [{id, name}] and drop anything without an id.
clean[key] = [
{"id": str(it["id"]), "name": str(it.get("name", it["id"]))}
for it in items if isinstance(it, dict) and it.get("id")
]
with self._lock:
self._lists[guild_id] = clean
return clean
# -- bot -> dashboard (state push) ----------------------------------- #
def report(self, state: dict[str, Any]) -> None: