Discord radio channels: wire leases + desired-state streams (server-26#203, #204)
c2-core
- discord_streams/discord_leases wired in: POST /discord/streams,
POST /discord/guilds/{id}/stop, GET /discord/guilds/{id}/overview,
GET /discord/streams/{id}, PUT /discord/guilds/{id}/link (admin),
GET /discord/node/{node}/tokens/{id} (node key, lease holder only).
- Retained nodes/{id}/discord/desired (token ids, never token strings),
nodes/+/discord/actual subscribed and handled; per-node publish lock keeps
the retained list the newest; failed publishes retried by the reconciler.
- Tokens freed only after the node acks logout (or offline past
discord_lease_offline_release_seconds) - sweeper and C2 startup no longer
release leases.
- /nodes/{id}/command discord_join/leave map onto streams; CommandPayload
keeps preferred_token_id. /tokens: admin-only list, validate on add against
Discord, refresh, flush via transactional force release.
- Guild -> org via discord_guilds, unlinked guilds fall back to
DISCORD_UNLINKED_GUILD_ORG_ID (default "founding").
- tests/fake_firestore.py: optimistic-transaction fake; 27 lease/race tests.
discord bot
- /join, /leave, /status, autocomplete scoped to the calling guild; /leave
never touches another server; Manage Channels default permission;
/join edits its reply with the node's real result; /help copy fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0a724a064f
commit
a31a8df17c
@@ -14,7 +14,7 @@ from app.internal.auth import (
|
||||
require_service_or_firebase_token,
|
||||
resolve_caller_org_id,
|
||||
)
|
||||
from app.routers.tokens import assign_token, release_token
|
||||
from app.internal import discord_streams
|
||||
|
||||
router = APIRouter(prefix="/nodes", tags=["nodes"])
|
||||
|
||||
@@ -138,27 +138,28 @@ async def send_command(
|
||||
|
||||
payload = cmd.model_dump(exclude_none=True)
|
||||
|
||||
# Discord join/leave are desired state now (server-26#203/#204, node-26#25):
|
||||
# they never go to the node as commands, and never carry a token string.
|
||||
if cmd.action == "discord_join":
|
||||
# Resolve system doc once — used for preferred token and presence name.
|
||||
system_doc = None
|
||||
system_id = node.get("assigned_system_id")
|
||||
if system_id:
|
||||
system_doc = await fstore.doc_get_cached("systems", system_id)
|
||||
if not cmd.guild_id or not cmd.channel_id:
|
||||
raise HTTPException(400, "guild_id and channel_id are required.")
|
||||
system_id = discord_streams._effective_system(node)
|
||||
if not system_id:
|
||||
raise HTTPException(409, "This node is not listening to a system yet.")
|
||||
try:
|
||||
doc = await discord_streams.start_stream(
|
||||
org_id=node.get("org_id"), guild_id=cmd.guild_id, channel_id=cmd.channel_id,
|
||||
system_id=system_id, node_id=node_id, preferred_token_id=cmd.preferred_token_id,
|
||||
mode="adhoc",
|
||||
)
|
||||
except discord_streams.StreamError as e:
|
||||
raise HTTPException(e.status, e.message)
|
||||
return {"ok": True, "stream_id": doc["stream_id"], "token_id": doc.get("token_id"),
|
||||
"delivered": doc.get("delivered")}
|
||||
|
||||
# Explicit preferred_token_id in the request beats the system-level preference.
|
||||
preferred = payload.pop("preferred_token_id", None) or (system_doc or {}).get("preferred_token_id")
|
||||
token = await assign_token(node_id, preferred_token_id=preferred)
|
||||
if not token:
|
||||
raise HTTPException(503, "No Discord bot tokens available in the pool.")
|
||||
payload["token"] = token
|
||||
|
||||
# Pass system name so the bot can set its Discord presence on join.
|
||||
system_name = (system_doc or {}).get("name")
|
||||
if system_name:
|
||||
payload["system_name"] = system_name
|
||||
|
||||
elif cmd.action == "discord_leave":
|
||||
await release_token(node_id)
|
||||
if cmd.action == "discord_leave":
|
||||
stopped = await discord_streams.stop_node(node_id, "stopped_from_web")
|
||||
return {"ok": True, "stopped": stopped}
|
||||
|
||||
if not mqtt_handler.send_command(node_id, payload):
|
||||
raise HTTPException(503, "MQTT broker unavailable — command not delivered.")
|
||||
|
||||
Reference in New Issue
Block a user