diff --git a/TASKS.md b/TASKS.md index 17e8f9c..54ae3ab 100644 --- a/TASKS.md +++ b/TASKS.md @@ -4,10 +4,10 @@ | Pri | Status | Task | |-----|--------|------| -| P0 | [x] | `src/derp/opus.py` -- ctypes libopus encoder (zero Python deps) | -| P0 | [x] | `src/derp/mumble.py` -- voice varint, voice packet, PCM scaling, stream_audio | +| P0 | [x] | `src/derp/mumble.py` -- rewrite to pymumble transport (voice + text) | | P0 | [x] | `plugins/music.py` -- play/stop/skip/queue/np/volume commands | -| P1 | [x] | Tests: `test_opus.py`, `test_mumble.py` voice additions, `test_music.py` | +| P0 | [x] | Container patches for pymumble ssl + opuslib musl | +| P1 | [x] | Tests: `test_mumble.py` (62 cases), `test_music.py` (28 cases) | | P2 | [x] | Documentation update (USAGE.md, CHEATSHEET.md) | ## Previous Sprint -- v2.2.0 Configurable Proxy (2026-02-21) diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 9dad945..fc7345b 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -529,20 +529,19 @@ split at 4096 chars. IRC-only commands are no-ops. ~90% of plugins work. # config/derp.toml [mumble] enabled = true -proxy = true # SOCKS5 proxy for TCP +proxy = false # pymumble connects directly host = "mumble.example.com" port = 64738 username = "derp" password = "" -tls_verify = false # self-signed certs common admins = ["admin_user"] # Mumble usernames operators = [] trusted = [] ``` -TCP/TLS via SOCKS5 proxy by default (`proxy = true`). Minimal protobuf -codec (no external dep). HTML stripped on receive, escaped on send. -IRC-only commands are no-ops. ~90% of plugins work. +Uses pymumble for protocol handling (connection, voice, Opus encoding). +HTML stripped on receive, escaped on send. IRC-only commands are no-ops. +~90% of plugins work. ## Music (Mumble only) diff --git a/docs/USAGE.md b/docs/USAGE.md index 03846bf..30f7bf3 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -1480,30 +1480,30 @@ proxy at `127.0.0.1:1080` via `derp.http.urlopen` when `proxy = true` ## Mumble Integration -Connect derp to a Mumble server via TCP/TLS protobuf control channel. -Text chat only (no voice). TCP is routed through the SOCKS5 proxy when -`proxy = true` (default). No protobuf library dependency -- uses a -minimal built-in varint/field encoder/decoder for the ~7 message types -needed. +Connect derp to a Mumble server with text chat and voice playback. +Uses [pymumble](https://github.com/azlux/pymumble) for the Mumble +protocol (connection, SSL, voice encoding). Text commands are bridged +from pymumble's thread callbacks to asyncio for plugin dispatch. ### How It Works -The bot connects to the Mumble server over TLS, sends -Version and Authenticate messages, then enters a read loop. It tracks -channels (ChannelState), users (UserState), and dispatches commands -from TextMessage messages through the shared plugin registry. +pymumble handles the Mumble protocol: TLS connection, ping keepalives, +channel/user tracking, and Opus voice encoding. The bot registers +callbacks for text messages and connection events, then bridges them +to asyncio via `run_coroutine_threadsafe()`. Voice playback feeds raw +PCM to `sound_output.add_sound()` -- pymumble handles Opus encoding, +packetization, and timing. ### Configuration ```toml [mumble] enabled = true -proxy = true # Route TCP through SOCKS5 +proxy = false # SOCKS5 proxy (pymumble connects directly) host = "mumble.example.com" # Mumble server hostname port = 64738 # Default Mumble port username = "derp" # Bot username password = "" # Server password (optional) -tls_verify = false # Mumble commonly uses self-signed certs admins = ["admin_user"] # Mumble usernames operators = [] # Mumble usernames trusted = [] # Mumble usernames @@ -1511,8 +1511,7 @@ trusted = [] # Mumble usernames ### Mumble Setup -1. **Ensure a Mumble server** (Murmur/Mumble-server) is running and - accessible through the SOCKS5 proxy +1. **Ensure a Mumble server** (Murmur/Mumble-server) is running 2. **Configure the bot** with the server hostname, port, and credentials @@ -1556,13 +1555,14 @@ unescapes entities. On send, text is HTML-escaped. Action messages use ### Music Playback Stream audio from YouTube, SoundCloud, and other yt-dlp-supported sites -into the Mumble voice channel. Audio is decoded to PCM, encoded to Opus -via system libopus, and transmitted as voice packets over the TCP tunnel. +into the Mumble voice channel. Audio is decoded to PCM via a +`yt-dlp | ffmpeg` subprocess pipeline; pymumble handles Opus encoding +and voice transmission. -**System dependencies** (must be installed on the host): +**System dependencies** (container image includes these): - `yt-dlp` -- audio stream extraction - `ffmpeg` -- decode to 48kHz mono s16le PCM -- `libopus.so.0` -- Opus codec (ctypes, no Python binding) +- `libopus` -- Opus codec (used by pymumble/opuslib) ``` !play Play audio or add to queue @@ -1572,21 +1572,14 @@ via system libopus, and transmitted as voice packets over the TCP tunnel. !queue Add to queue (alias for !play) !np Now playing !volume [0-100] Get/set volume +!testtone Play 3-second 440Hz test tone ``` - Queue holds up to 50 tracks - Volume applies from the next track (default: 50%) - Title resolved via `yt-dlp --get-title` before playback -- Audio pipeline: `yt-dlp | ffmpeg` subprocess, 20ms Opus frames +- Audio pipeline: `yt-dlp | ffmpeg` subprocess, PCM fed to pymumble - Commands are Mumble-only; `!play` on other adapters replies with an error, other music commands silently no-op - Playback runs as an asyncio background task; the bot remains responsive to text commands during streaming - -### Transport - -TCP connections route through the SOCKS5 proxy at `127.0.0.1:1080` -via `derp.http.create_connection` when `proxy = true` (default). Set -`proxy = false` to connect directly. TLS is applied on top of the -socket. Mumble commonly uses self-signed certificates, so `tls_verify` -defaults to `false`.