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
@@ -269,6 +269,11 @@ async def admin_overview(_: dict = Depends(require_admin_token)):
|
||||
Radio bots come from ``GET /tokens``; no token string is returned here.
|
||||
Discord user ids (``created_by_discord_id``) and the linking admin's uid
|
||||
are deliberately left out.
|
||||
|
||||
Per server, ``main_bot_present`` (the DRB server bot — optional since
|
||||
server-26#225: True/False as it last reported, None = never heard from it
|
||||
there) and ``radio_bots`` (names of radio bots Discord last said are in
|
||||
it). A server with radio bots and no server bot is a normal setup.
|
||||
"""
|
||||
links = {str(g.get("guild_id")): g for g in await fstore.collection_list(streams.GUILDS)
|
||||
if g.get("guild_id")}
|
||||
@@ -284,6 +289,11 @@ async def admin_overview(_: dict = Depends(require_admin_token)):
|
||||
continue # removed from DRB (DELETE /discord/guilds/{id}); re-listed if it gets a channel again
|
||||
guilds[gid] = {"guild_id": gid, "guild_name": link.get("guild_name"), "linked": bool(link.get("org_id")),
|
||||
"org_id": link.get("org_id"), "linked_at": link.get("linked_at"),
|
||||
"linked_via": link.get("linked_via"),
|
||||
# The DRB server bot is optional (server-26#225 S8): True/False
|
||||
# = it said it is / is not in the server, None = never heard.
|
||||
"main_bot_present": link.get("bot_present"),
|
||||
"directory_source": link.get("directory_source"),
|
||||
# Explicitly unlinked servers get no fallback org (mirrors
|
||||
# streams.resolve_guild_org once server-26#218 lands).
|
||||
"_no_fallback": bool(link.get("unlinked_at"))}
|
||||
@@ -291,7 +301,8 @@ async def admin_overview(_: dict = Depends(require_admin_token)):
|
||||
for s in all_streams:
|
||||
gid = str(s.get("guild_id") or leases.guild_of(s.get("stream_id") or ""))
|
||||
g = guilds.setdefault(gid, {"guild_id": gid, "guild_name": None, "linked": False,
|
||||
"org_id": None, "linked_at": None})
|
||||
"org_id": None, "linked_at": None, "linked_via": None,
|
||||
"main_bot_present": None, "directory_source": None})
|
||||
if s.get("guild_name") and not g.get("guild_name"):
|
||||
g["guild_name"] = s["guild_name"]
|
||||
node = nodes.get(s.get("node_id")) or {}
|
||||
@@ -305,6 +316,11 @@ async def admin_overview(_: dict = Depends(require_admin_token)):
|
||||
g["acting_org_id"] = acting
|
||||
g["acting_org_name"] = (orgs.get(acting) or {}).get("name") if acting else None
|
||||
g["live_streams"] = sum(1 for r in rows if r.get("guild_id") == g["guild_id"] and r.get("desired") == "on")
|
||||
# Radio bots Discord last said are in the server (names only). Any one
|
||||
# of them is enough to list channels, check permissions and stream.
|
||||
g["radio_bots"] = sorted(str(t.get("name") or tid) for tid, t in tokens.items()
|
||||
if t.get("valid") is not False and isinstance(t.get("guild_ids"), list)
|
||||
and g["guild_id"] in t["guild_ids"])
|
||||
|
||||
# Streams sharing a voice channel sit together, oldest first.
|
||||
rows.sort(key=lambda r: (0 if r.get("desired") == "on" else 1, r.get("guild_name") or r.get("guild_id") or "",
|
||||
|
||||
@@ -21,9 +21,20 @@ Where the directory comes from (server-26#225 S5 — the main bot is optional):
|
||||
stored list is missing (inline, inside the permission-check time budget)
|
||||
or older than ``discord_directory_refresh_seconds`` (in the background).
|
||||
|
||||
So C2 does call Discord when a signed-in user opens the picker — with a
|
||||
radio bot's token, for a server already linked to the caller's org, through
|
||||
the rate-limited, cached client. No token string ever reaches the browser.
|
||||
This reverses the old rule "C2 never calls Discord on the browser's behalf"
|
||||
on purpose (server-26#225 plan finding 5): C2 does call Discord when a
|
||||
signed-in user opens the picker, presses Start or Re-check — with a radio
|
||||
bot's token, only for a server already linked to the caller's org, through
|
||||
discord_rest's rate-limited, cached, breaker-guarded client (refresh=1 at
|
||||
most once per 10 s per user and server). No token string ever reaches the
|
||||
browser, and REST answers never create or re-link a ``discord_guilds`` doc.
|
||||
|
||||
The DRB server bot is optional end to end (server-26#225 S8). One radio bot
|
||||
in the server is enough to link it (per-bot OAuth, routers/discord_oauth.py),
|
||||
list its voice channels (here), check permissions (routers/discord_checks.py)
|
||||
and stream (streams.start_stream). The server bot adds its slash commands
|
||||
(/join, /leave, /status, /drb link) and a gateway-fed channel list; every
|
||||
path it uses keeps working.
|
||||
|
||||
Which servers an org sees (``_org_guilds``):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user