feat(bot+dashboard): bot info, server/voice-channel picker, participants, speaker
Adds a dashboard<->bot control plane (bot pushes state + polls commands, keeping the bot's single outbound-HTTP direction): - New bot_control.BotControl + endpoints: GET /api/bot/state, /api/bot/commands; POST /api/bot/report, /api/bot/select. - Dashboard header bar: bot identity/connection, server dropdown (top "없음"), voice-channel dropdown (top "없음"), and live participant list. - Turns record who spoke (Turn.speaker, via X-User-Name on the voice-turn POST). - dave/bot.mjs: reports identity/guilds/voice-channels/members, polls join/leave commands and joins dynamically, and sends the speaker's display name. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
61
wsai/bot_control.py
Normal file
61
wsai/bot_control.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Control plane between the dashboard (Python) and the Discord bot (Node).
|
||||
|
||||
The bot only ever makes *outbound* HTTP (it already POSTs voice turns), so we
|
||||
keep that single direction:
|
||||
|
||||
* the bot PUSHES its live state here (identity, joinable guilds + voice
|
||||
channels, current channel, members, whitelist/blacklist) via
|
||||
``POST /api/bot/report`` → :meth:`report`;
|
||||
* the bot POLLS ``GET /api/bot/commands`` → :meth:`drain` for pending commands
|
||||
(join a channel, leave) that the dashboard UI enqueued via :meth:`enqueue`.
|
||||
|
||||
Thread-safe: the HTTP handler threads read/write from several threads.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
# The bot is considered offline if it hasn't reported within this window.
|
||||
STALE_AFTER_S = 8.0
|
||||
|
||||
|
||||
class BotControl:
|
||||
def __init__(self) -> None:
|
||||
self._lock = threading.Lock()
|
||||
self._state: dict[str, Any] = {"connected": False, "ts": 0.0}
|
||||
self._commands: deque[dict[str, Any]] = deque()
|
||||
self._cmd_id = 0
|
||||
|
||||
# -- bot -> dashboard (state push) ----------------------------------- #
|
||||
def report(self, state: dict[str, Any]) -> None:
|
||||
s = dict(state)
|
||||
s["ts"] = time.time()
|
||||
s["connected"] = True
|
||||
with self._lock:
|
||||
self._state = s
|
||||
|
||||
def state(self) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
s = dict(self._state)
|
||||
# A report older than STALE_AFTER_S means the bot stopped polling/pushing.
|
||||
if s.get("connected") and (time.time() - s.get("ts", 0.0)) > STALE_AFTER_S:
|
||||
s["connected"] = False
|
||||
return s
|
||||
|
||||
# -- dashboard -> bot (command queue) -------------------------------- #
|
||||
def enqueue(self, cmd: dict[str, Any]) -> int:
|
||||
with self._lock:
|
||||
self._cmd_id += 1
|
||||
cmd = {**cmd, "id": self._cmd_id}
|
||||
self._commands.append(cmd)
|
||||
return self._cmd_id
|
||||
|
||||
def drain(self) -> list[dict[str, Any]]:
|
||||
with self._lock:
|
||||
cmds = list(self._commands)
|
||||
self._commands.clear()
|
||||
return cmds
|
||||
Reference in New Issue
Block a user