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>
62 lines
2.1 KiB
Python
62 lines
2.1 KiB
Python
"""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
|