S8: main DRB bot optional end to end + owner-flow integration tests (server-26#225)

- Copy: picker, admin Servers/Streams/How-it-works, org Discord card, bot
  /help and the server-limit refusal no longer imply the DRB server bot is
  required; a radio bot alone is enough.
- Admin overview: per-server main_bot_present, radio_bots, linked_via,
  directory_source; Servers tab gets a Bots column.
- discord_web docstring: states the reversed "never calls Discord" rule
  (plan finding 5) and the optional server bot.
- Server bot directory cog: re-push (which drops C2's cached permission
  answers) on overwrite changes, role permission changes and role deletes.
- tests/test_discord_owner_flows.py: one-bot org (BYO bot, owner-set
  preferred, OAuth link, picker, start, reconciler) and full setup (server
  bot + 4 radio bots: placement, shared channel, every refusal explained),
  against a modelled Discord over httpx.MockTransport.
- storage.download_audio removed: no caller on main since #233.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-05 03:24:44 -04:00
co-authored by Claude Opus 5.5
parent f49dc86111
commit 9c124d4c8b
13 changed files with 652 additions and 60 deletions
+12 -8
View File
@@ -2,9 +2,11 @@
// Node page "Discord radio" panel + picker (server-26#213 item 3, #109 item 13).
// Replaces the raw Guild ID / Voice Channel ID modal. The picker lists only
// the Discord servers linked to this node's org and the voice channels the
// DRB bot reported in them; a pick becomes a radio channel pinned to this node
// (desired state — the node reconciles and reports back, shown per row).
// the Discord servers linked to this node's org and their voice channels, as
// read by a radio bot in the server or pushed by the DRB server bot — either
// is enough; the server bot is optional (server-26#225). A pick becomes a
// radio channel pinned to this node (desired state — the node reconciles and
// reports back, shown per row).
// A voice channel can carry several radio streams, each its own bot playing
// its own system (server-26#213 follow-up): picking a channel that already
// plays another system ADDS this node's system beside it; only a stream of
@@ -211,8 +213,9 @@ function DiscordPicker({
<legend className="text-xs text-ink-muted mb-2">Discord server</legend>
{usable.length === 0 ? (
<p className="text-xs text-ink-2 bg-raised border border-line rounded-lg px-3 py-2">
No Discord server is linked to your organization yet. Add the DRB bot to your Discord server and
link it to your organization; it shows up here once the bot reports its channels.
No Discord server is linked to your organization yet. The organization owner connects one under
Settings → Organization → <b className="text-ink">Connect a Discord server</b>: adding a radio bot
to the server links it, and its voice channels show up here.
</p>
) : (
<div className="space-y-1.5">
@@ -259,12 +262,13 @@ function DiscordPicker({
<legend className="text-xs text-ink-muted mb-2">Voice channel</legend>
{!guild.synced ? (
<p className="text-xs text-ink-2 bg-raised border border-line rounded-lg px-3 py-2">
The DRB bot has not reported this server&apos;s channels yet. It does when it starts and every 15
minutes — check back shortly.
DRB hasn&apos;t read this server&apos;s voice channels yet. A radio bot in the server lists them
(or the DRB server bot, if it&apos;s there) — close this and open it again in a moment. If it
stays empty, no radio bot is in this server yet — invite one with the link under the server above.
</p>
) : guild.voice_channels.length === 0 ? (
<p className="text-xs text-ink-2 bg-raised border border-line rounded-lg px-3 py-2">
This server has no voice channels the DRB bot can see.
This server has no voice channels DRB&apos;s bots can see.
</p>
) : (
<div className="space-y-2">