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:
co-authored by
Claude Opus 5.5
parent
f49dc86111
commit
9c124d4c8b
@@ -8,8 +8,12 @@ import { DIM, Field, INPUT, Panel, Status, Table, TD } from "./ui";
|
||||
|
||||
// Linked Discord servers = discord_guilds docs, plus any server that has had a
|
||||
// radio channel without being linked (it acts for DISCORD_UNLINKED_GUILD_ORG_ID).
|
||||
// Linking is PUT /discord/guilds/{id}/link (platform admin). The owner-side
|
||||
// "/drb link code:" flow is server-26#213 item 1 and is not built here.
|
||||
// Linking here is PUT /discord/guilds/{id}/link (platform admin, support).
|
||||
// Org owners link their own from Settings → Organization: "Connect a Discord
|
||||
// server" (a radio bot's OAuth invite, no DRB server bot needed) or a
|
||||
// /drb link code (needs the server bot) — server-26#213 item 1, #225.
|
||||
// The Bots column says which bots are in each server: the DRB server bot is
|
||||
// optional (server-26#225 S8), one radio bot is enough to stream.
|
||||
// Remove is DELETE /discord/guilds/{id}: its radio channels are stopped then
|
||||
// deleted, and the server is unlinked with no fallback org.
|
||||
|
||||
@@ -85,6 +89,30 @@ function LinkForm({
|
||||
);
|
||||
}
|
||||
|
||||
// Which bots are in the server. The DRB server bot is optional (server-26#225):
|
||||
// "not there" is only a problem when no radio bot is either.
|
||||
function BotsCell({ guild }: { guild: DiscordGuild }) {
|
||||
const radio = guild.radio_bots ?? [];
|
||||
const main = guild.main_bot_present;
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{radio.length > 0 ? (
|
||||
<span title={radio.join(", ")}>
|
||||
{radio.length} radio bot{radio.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
) : (
|
||||
<span className={main ? DIM : "text-sev-moderate"}>no radio bot</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={DIM}>
|
||||
DRB server bot: {main === true ? "present" : main === false ? "not there" : "never seen"}
|
||||
{guild.directory_source === "radio_bot" && " · channels via radio bot"}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type RemoveResult = { guild: string; streams: number; bots: { name: string; left: boolean; error: string | null }[] };
|
||||
|
||||
function RemoveForm({
|
||||
@@ -131,7 +159,7 @@ function RemoveForm({
|
||||
</span>
|
||||
</label>
|
||||
<p className="text-ink-muted">
|
||||
The DRB bot itself stays in the Discord server until someone kicks it there.
|
||||
The DRB server bot, if it is in the Discord server, stays until someone kicks it there.
|
||||
</p>
|
||||
{error && <p className="text-sev-major">{error}</p>}
|
||||
<div className="flex gap-2">
|
||||
@@ -171,8 +199,9 @@ export function ServersPanel({
|
||||
fallback ? (
|
||||
<>
|
||||
<b className="text-ink">Unlinked servers act for {fallbackName}</b> (DISCORD_UNLINKED_GUILD_ORG_ID). Any
|
||||
server the DRB bot is invited to can use that org's systems until it is linked here. Set the variable
|
||||
to an empty string once every real server is linked.
|
||||
server the DRB server bot is invited to can use that org's systems until it is linked here (a radio
|
||||
bot alone never puts an unlinked server here). Set the variable to an empty string once every real server
|
||||
is linked.
|
||||
</>
|
||||
) : (
|
||||
<>Unlinked servers can't use radio commands until they are linked here.</>
|
||||
@@ -189,14 +218,16 @@ export function ServersPanel({
|
||||
{removed.bots.map((b) => `${b.name} ${b.left ? "left" : `did not leave (${b.error ?? "unknown error"})`}`).join("; ")}.
|
||||
</>
|
||||
)}{" "}
|
||||
The DRB bot is still in that Discord server until someone kicks it.
|
||||
The DRB server bot, if it was there, stays in that Discord server until someone kicks it.
|
||||
<button className="ml-3 text-accent hover:underline" onClick={() => setRemoved(null)}>Dismiss</button>
|
||||
</div>
|
||||
)}
|
||||
{overview.guilds.length === 0 ? (
|
||||
<p className="px-4 py-6 text-sm text-ink-muted">No Discord servers yet. Link one, or run a radio command in a server the DRB bot is in.</p>
|
||||
<p className="px-4 py-6 text-sm text-ink-muted">
|
||||
No Discord servers yet. Link one here, or an organization owner connects one under Settings → Organization.
|
||||
</p>
|
||||
) : (
|
||||
<Table head={["Server", "Acts for", "Radio channels", "Linked", "Status", ""]}>
|
||||
<Table head={["Server", "Acts for", "Bots", "Radio channels", "Linked", "Status", ""]}>
|
||||
{overview.guilds.flatMap((g) => {
|
||||
const row = (
|
||||
<tr key={g.guild_id}>
|
||||
@@ -208,6 +239,9 @@ export function ServersPanel({
|
||||
{g.acting_org_name || g.acting_org_id || <span className={DIM}>nobody</span>}
|
||||
{!g.linked && g.acting_org_id && <div className={DIM}>fallback, not linked</div>}
|
||||
</td>
|
||||
<td className={TD}>
|
||||
<BotsCell guild={g} />
|
||||
</td>
|
||||
<td className={TD}>
|
||||
{g.live_streams} / {max}
|
||||
</td>
|
||||
@@ -251,7 +285,7 @@ export function ServersPanel({
|
||||
return [
|
||||
row,
|
||||
<tr key={`${g.guild_id}-remove`}>
|
||||
<td colSpan={6} className="p-0 border-b border-line">
|
||||
<td colSpan={7} className="p-0 border-b border-line">
|
||||
<RemoveForm
|
||||
guild={g}
|
||||
onCancel={() => setRemoving(null)}
|
||||
@@ -269,7 +303,7 @@ export function ServersPanel({
|
||||
return [
|
||||
row,
|
||||
<tr key={`${g.guild_id}-edit`}>
|
||||
<td colSpan={6} className="p-0 border-b border-line">
|
||||
<td colSpan={7} className="p-0 border-b border-line">
|
||||
<LinkForm overview={overview} guild={g} onDone={done} onCancel={() => setEditing(null)} />
|
||||
</td>
|
||||
</tr>,
|
||||
|
||||
@@ -348,7 +348,8 @@ export function StreamsPanel({
|
||||
)}
|
||||
{overview.streams.length === 0 ? (
|
||||
<p className="px-4 py-6 text-sm text-ink-muted">
|
||||
No radio channels yet. Start one here, or run <span className="font-mono">/join</span> in a linked Discord server.
|
||||
No radio channels yet. Start one here or from a node's Discord radio panel, or run{" "}
|
||||
<span className="font-mono">/join</span> where the DRB server bot is in a linked Discord server.
|
||||
</p>
|
||||
) : (
|
||||
<Table head={["Server · channel", "Source", "Talkgroups", "Mode", "Desired → actual", "Bot", ""]}>
|
||||
|
||||
Reference in New Issue
Block a user