discord_radio: desired-state streams, logout on release, no mention controls (#25) #32

Merged
logan merged 1 commits from feat/discord-system into main 2026-10-04 18:22:30 -04:00
Owner

Fixes node-26#25. Companion server PR: logan/server-26 feat/discord-system (server-26#203/#204). Merge both together. This edge ignores legacy discord_join/discord_leave commands, so on an old C2 it would not stream at all.

What changed (drb-edge-node)

  • discord_radio.py is rewritten as RadioManager. It reconciles C2's retained nodes/{id}/discord/desired and publishes nodes/{id}/discord/actual = {applied_seq, sessions, streams} after every change and every ~60s.
  • Item 1: one gateway session per leased token, and a token no stream needs is logged out with client.close(), not just disconnected from voice. C2 frees the token only after this node reports it gone.
  • Item 2: the watchdog is a separate reconcile loop, so recovering a dead session cannot cancel it. Presence survives rejoins.
  • Item 3: no message-content intent and no @bot leave/join mention handling. The intents are guilds + voice_states only.
  • Item 4: POST /api/discord/join now returns 409 with an explanation (it was a TypeError 500). POST /api/discord/leave pauses locally until C2 sends a new list. Added GET /api/discord.
  • Item 5: join/leave results go back to C2 per stream (state, error, code such as not_in_guild).
  • One voice connection per (token, guild), so a node can serve several guilds and channels (cap enforced by C2).
  • Token strings are fetched over HTTPS from C2 (node API key, lease holder only) and kept in memory only. They never travel over MQTT.
  • MQTT-loss fail-safe: every bot is logged out after DISCORD_MQTT_LOSS_STOP_SECONDS (300), which is below C2's 600s lease grace.
  • Audio path is unchanged: each voice connection plays FFmpeg -f pulse on settings.pulse_source, never Icecast. Nothing touches call_recorder.py.

Tests

  • pytest: 299 passed, 9 skipped, 1 warning. That includes 12 new tests in tests/test_discord_radio.py, run against a faked Discord client: reconcile, fan-out, logout on release, stale seq, watchdog recovery, not-in-guild/login-failure reporting, the MQTT-loss fail-safe, local pause, and no message-content intent.
  • flake8 drb-edge-node/app --max-line-length=120: clean. The DEFERRED.md row about pre-existing flake8 failures in discord_radio.py is now stale.

Not done

  • drb-audio-review has NOT been run. It is required before merge for discord_radio.py changes, and this agent could not spawn it. Please run it.
  • Not exercised against a real Discord gateway or a real PulseAudio sink. All of this is unit-level.

🤖 Generated with Claude Code

Fixes node-26#25. Companion server PR: logan/server-26 `feat/discord-system` (server-26#203/#204). **Merge both together.** This edge ignores legacy `discord_join`/`discord_leave` commands, so on an old C2 it would not stream at all. ## What changed (`drb-edge-node`) - `discord_radio.py` is rewritten as `RadioManager`. It reconciles C2's retained `nodes/{id}/discord/desired` and publishes `nodes/{id}/discord/actual` = `{applied_seq, sessions, streams}` after every change and every ~60s. - **Item 1:** one gateway session per leased token, and a token no stream needs is logged out with `client.close()`, not just disconnected from voice. C2 frees the token only after this node reports it gone. - **Item 2:** the watchdog is a separate reconcile loop, so recovering a dead session cannot cancel it. Presence survives rejoins. - **Item 3:** no message-content intent and no `@bot leave/join` mention handling. The intents are `guilds` + `voice_states` only. - **Item 4:** `POST /api/discord/join` now returns 409 with an explanation (it was a TypeError 500). `POST /api/discord/leave` pauses locally until C2 sends a new list. Added `GET /api/discord`. - **Item 5:** join/leave results go back to C2 per stream (state, error, code such as `not_in_guild`). - One voice connection per (token, guild), so a node can serve several guilds and channels (cap enforced by C2). - Token strings are fetched over HTTPS from C2 (node API key, lease holder only) and kept in memory only. They never travel over MQTT. - MQTT-loss fail-safe: every bot is logged out after `DISCORD_MQTT_LOSS_STOP_SECONDS` (300), which is below C2's 600s lease grace. - Audio path is unchanged: each voice connection plays FFmpeg `-f pulse` on `settings.pulse_source`, never Icecast. Nothing touches `call_recorder.py`. ## Tests - `pytest`: `299 passed, 9 skipped, 1 warning`. That includes 12 new tests in `tests/test_discord_radio.py`, run against a faked Discord client: reconcile, fan-out, logout on release, stale seq, watchdog recovery, not-in-guild/login-failure reporting, the MQTT-loss fail-safe, local pause, and no message-content intent. - `flake8 drb-edge-node/app --max-line-length=120`: clean. The DEFERRED.md row about pre-existing flake8 failures in `discord_radio.py` is now stale. ## Not done - **`drb-audio-review` has NOT been run.** It is required before merge for `discord_radio.py` changes, and this agent could not spawn it. Please run it. - Not exercised against a real Discord gateway or a real PulseAudio sink. All of this is unit-level. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
logan added 1 commit 2026-10-04 18:10:35 -04:00
discord_radio: desired-state streams, logout on release, no mention controls (node-26#25)
CI / lint (push) Successful in 8s
CI / test-op25 (push) Successful in 18s
CI / test (push) Successful in 58s
CI / lint (pull_request) Successful in 7s
CI / test-op25 (pull_request) Successful in 13s
CI / test (pull_request) Successful in 44s
419d7c4d8c
- Rewritten as RadioManager reconciling C2's retained
  nodes/{id}/discord/desired (token ids) and publishing
  nodes/{id}/discord/actual {applied_seq, sessions, streams}.
- One gateway session per leased token, one voice connection per
  (token, guild); a token no stream needs is logged OUT (client.close()),
  which is what lets C2 free it safely (item 1).
- Watchdog is a separate reconcile loop; recovering a dead session can no
  longer cancel it (item 2). Presence kept across rejoins.
- No message-content intent and no @bot leave/join mention controls (item 3).
- Local dashboard: /api/discord/join returns 409 with an explanation instead
  of a TypeError 500; /api/discord/leave pauses locally; GET /api/discord
  shows state (item 4). Join/leave results are reported to C2 (item 5).
- Token strings fetched over HTTPS from C2 (lease holder only), memory only.
- MQTT-loss fail-safe: logs every bot out after
  DISCORD_MQTT_LOSS_STOP_SECONDS (300, below C2's 600s lease grace).
- Legacy discord_join/discord_leave MQTT commands are ignored.
- Audio path unchanged: FFmpeg -f pulse on settings.pulse_source per
  connection, never Icecast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
logan merged commit 8c0bf752f0 into main 2026-10-04 18:22:30 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/node-26#32