Commit Graph

178 Commits

Author SHA1 Message Date
user
192ea717a7 feat: split CI into gitleaks, lint, and test jobs
Some checks failed
CI / gitleaks (push) Failing after 15s
CI / lint (push) Failing after 17s
CI / test (3.11) (push) Has been skipped
CI / test (3.12) (push) Has been skipped
CI / test (3.13) (push) Has been skipped
- Add gitleaks secret scanning (full history)
- Separate lint (ruff, Python 3.13 only) from test matrix
- Test job gates on lint; gitleaks runs in parallel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:51:53 +01:00
user
7a4aa65882 fix: align cmd_stop else branch with _play_loop finally cleanup
The else branch (no active task) only cleared current, task, and
duck_vol. Now resets all play state fields to match _play_loop's
finally block: done_event, duck_task, progress, and cur_seek.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:49:53 +01:00
user
2cd1d5efb1 fix: race condition in skip/seek/stop losing track state
task.cancel() triggers _play_loop's finally block asynchronously.
When cmd_skip or cmd_seek called _ensure_loop before the finally
block ran, the old task's cleanup would overwrite the new task's
state -- causing !np to report "Nothing playing" while audio
was still streaming.

Now await the cancelled task before restarting the loop, ensuring
the finally block completes first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:45:00 +01:00
user
95981275b5 feat: add OpenRouter LLM chat plugin (!ask, !chat)
Single-shot (!ask) and conversational (!chat) LLM commands backed by
OpenRouter's API. Per-user history (20 msg cap), 5s cooldown, reasoning
model fallback, and model switching via subcommands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:39:11 +01:00
user
66116d2caf docs: update Piper TTS endpoint and document available voices
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:01:57 +01:00
user
eded764f6a fix: update Piper TTS endpoint and request format
Piper is on 192.168.129.9:5100, not :5000. It expects POST with
JSON body {"text": "..."}, not GET with query params. Also update
Whisper default to 192.168.129.9.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 04:56:24 +01:00
user
9783365b1e feat: add extra Mumble bot instances and TTS greeting
Support [[mumble.extra]] config for additional Mumble identities that
inherit connection settings from the main [mumble] section. Extra bots
get their own state DB and do not run the voice trigger by default.

Add TTS greeting on first connect via mumble.greet config option.
Merlin joins as a second identity with his own client certificate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 04:34:10 +01:00
user
165938a801 fix: mumble disconnect loop from stale socket and dead parent thread
Two issues causing ~2min reconnect cycles:

1. pymumble captures threading.current_thread() as parent_thread at
   init. Since we construct it in a run_in_executor thread, the parent
   dies after _connect_sync returns, triggering pymumble's loop exit.
   Fix: point parent_thread at threading.main_thread().

2. pymumble's init_connection() drops the control_socket reference
   without closing it. The lingering TCP connection makes Murmur kick
   the new session with "connected from another device". Fix: patch
   init_connection to close the old socket before reconnecting.
2026-02-22 04:24:23 +01:00
user
221cb1f06b fix: voice trigger not receiving audio from pymumble
pymumble passes a User object, not a dict. The isinstance(user, dict)
check returned False, setting name to None and silently discarding
every voice packet. Use try/except for dict-like access instead.
2026-02-22 04:02:23 +01:00
user
c4908f2a63 docs: document seek command and volume persistence 2026-02-22 03:31:39 +01:00
user
c493583a71 feat: add !seek command and persist volume across restarts
Seek to absolute or relative positions mid-track via !seek. Supports
M:SS and plain seconds with +/- prefixes. Volume is now saved to
bot.state and restored on connect.
2026-02-22 03:31:35 +01:00
user
7c099d8cf0 docs: document voice trigger configuration 2026-02-22 03:24:07 +01:00
user
e127f72660 feat: add always-on voice trigger mode with TTS echo
When [voice] trigger is set in config, the bot continuously listens and
transcribes voice. Speech starting with the trigger word is stripped and
echoed back via TTS. Non-triggered speech is silently discarded unless
!listen is also active.
2026-02-22 03:24:03 +01:00
user
7b9359c152 docs: document voice plugin commands
Add Voice STT/TTS section covering !listen, !say, and optional
[voice] config block with whisper_url, piper_url, silence_gap.
2026-02-22 03:08:10 +01:00
user
9fbf45f67d feat: add voice plugin with STT and TTS
Whisper STT: buffers incoming voice PCM per user, transcribes on
silence gap via local whisper.cpp endpoint, posts results as actions.

Piper TTS: !say fetches WAV from local Piper endpoint and plays via
stream_audio(). 37 tests cover buffering, flush logic, transcription,
WAV encoding, commands, and lifecycle.
2026-02-22 03:08:02 +01:00
user
039f060b50 feat: add sound listener hook to MumbleBot
Allow plugins to register callbacks for incoming voice PCM via
bot._sound_listeners. Empty list by default = zero overhead.
2026-02-22 03:07:55 +01:00
user
df20c154ca feat: download audio before playback, add !keep and !kept commands
Audio is now downloaded to data/music/ before playback begins,
eliminating CDN hiccups mid-stream. Falls back to streaming on
download failure. Files are deleted after playback unless marked
with !keep. stream_audio detects local files and uses a direct
ffmpeg pipeline (no yt-dlp).
2026-02-22 02:52:51 +01:00
user
ab924444de fix: survive mumble disconnects without restarting audio stream
Guard stream_audio with _is_audio_ready() so that PCM frames are
dropped (not crashed on) when pymumble recreates SoundOutput with
encoder=None during reconnect. The ffmpeg pipeline stays alive,
position tracking remains accurate, and audio feeding resumes once
the codec is negotiated. Listeners hear brief silence instead of
a 30+ second restart with URL re-resolution.

Also adds chat messages to _auto_resume so users see what the bot
intends ("Resuming 'X' at M:SS in a moment" / "...aborted").
2026-02-22 02:41:44 +01:00
user
ec55c2aef1 feat: auto-resume music on reconnect, sorcerer tier, cert auth
Auto-resume: save playback position on stream errors and cancellation,
restore automatically after reconnect or container restart once the
channel is silent. Plugin lifecycle hook (on_connected) ensures the
reconnect watcher starts without waiting for user commands.

Sorcerer tier: new permission level between oper and admin. Configured
via [mumble] sorcerers list in derp.toml.

Mumble cert auth: pass certfile/keyfile to pymumble for client
certificate authentication.

Fixes: stream_audio now re-raises CancelledError and Exception so
_play_loop detects failures correctly. Subprocess cleanup uses 3s
timeout. Graceful shutdown cancels background tasks before stopping
pymumble. Safe getattr for _opers in core plugin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 02:14:43 +01:00
user
f899241d73 feat: support relative volume adjustment (+N/-N)
!volume +10 increases by 10, !volume -5 decreases by 5.
Out-of-range results (below 0 or above 100) are rejected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:18:43 +01:00
user
f189cbd290 feat: add !resume to continue playback from last interruption
Tracks playback position via frame counting in stream_audio().
On stop/skip, saves URL + elapsed time to bot.state (SQLite).
!resume reloads the track and seeks to the saved position via
ffmpeg -ss. State persists across bot restarts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:15:39 +01:00
user
9d58a5d073 fix: slow volume ramp to 1s for smoother transitions
Reduce _max_step from 0.1 to 0.02 per frame, extending the full
0-to-1 volume ramp from ~200ms to ~1 second.
2026-02-21 23:56:04 +01:00
user
e4e1e219f0 feat: add YouTube search to !play and fix NA URL fallback
Non-URL input (e.g. !play classical music) searches YouTube for 10
results and picks one randomly. Also fixes --flat-playlist returning
"NA" as the URL for single videos by falling back to the original
input URL.
2026-02-21 23:52:01 +01:00
user
6b7d733650 feat: smooth volume ramping over 200ms in audio streaming
Some checks failed
CI / test (3.11) (push) Failing after 22s
CI / test (3.12) (push) Failing after 22s
CI / test (3.13) (push) Failing after 22s
Volume changes now ramp linearly per-sample via _scale_pcm_ramp instead
of jumping abruptly. Each frame steps _cur_vol toward target by at most
0.1, giving ~200ms for a full 0-to-1 sweep. Fast path unchanged when
volume is stable.
2026-02-21 23:32:22 +01:00
user
c5c61e63cc feat: expand YouTube playlists into individual queue tracks
_resolve_title replaced with _resolve_tracks using --flat-playlist to
enumerate playlist entries. cmd_play enqueues each track individually,
with truncation when the queue is nearly full. Single-video behavior
unchanged.
2026-02-21 23:32:16 +01:00
user
67b2dc827d fix: make !volume apply immediately during playback
stream_audio now accepts a callable for volume, re-read on each PCM
frame instead of capturing a static float at track start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:20:17 +01:00
user
eae36aa1f9 docs: update Mumble docs for pymumble transport
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:16:56 +01:00
user
d884d2bb55 refactor: switch Mumble voice to pymumble transport
asyncio's SSL memory-BIO transport silently drops voice packets even
though text works fine. pymumble uses blocking ssl.SSLSocket.send()
which reliably delivers voice data.

- Rewrite MumbleBot to use pymumble for connection, SSL, ping, and
  voice encoding/sending
- Bridge pymumble thread callbacks to asyncio via
  run_coroutine_threadsafe for text dispatch
- Voice via sound_output.add_sound(pcm) -- pymumble handles Opus
  encoding, packetization, and timing
- Remove custom protobuf codec, voice varint, and opus ctypes wrapper
- Add container patches for pymumble ssl.wrap_socket (Python 3.13) and
  opuslib find_library (musl/Alpine)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:15:42 +01:00
user
d756e7c020 fix: add opus, ffmpeg, yt-dlp to container image
Music playback requires system libraries that were missing from the
Alpine-based container.
2026-02-21 21:47:49 +01:00
user
7206b27fb0 docs: add music playback documentation
USAGE.md music section under Mumble, CHEATSHEET.md music commands,
TASKS.md sprint update for v2.3.0.
2026-02-21 21:42:33 +01:00
user
47b13c3f1f feat: add Mumble music playback with Opus streaming
ctypes libopus encoder (src/derp/opus.py), voice varint/packet builder
and stream_audio method on MumbleBot (src/derp/mumble.py), music plugin
with play/stop/skip/queue/np/volume commands (plugins/music.py).
Audio pipeline: yt-dlp|ffmpeg subprocess -> PCM -> Opus -> UDPTunnel.
67 new tests (1561 total).
2026-02-21 21:42:28 +01:00
user
b074356ec6 fix: always pass server_hostname for Mumble TLS on pre-connected socket
asyncio.open_connection(sock=..., ssl=...) requires server_hostname
even when check_hostname is disabled. Pass self._host unconditionally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:21:33 +01:00
user
9d4cb09069 feat: make SOCKS5 proxy configurable per adapter
Add `proxy` config option to server (IRC), teams, telegram, and mumble
sections. IRC defaults to false (preserving current direct-connect
behavior); all others default to true. The `derp.http` module now
accepts `proxy=True/False` on urlopen, create_connection,
open_connection, and build_opener -- when false, uses stdlib directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:19:22 +01:00
user
ca46042c41 docs: update docs for Mumble integration
Add Mumble sections to USAGE.md, CHEATSHEET.md, API.md, README.md.
Mark Mumble done in ROADMAP.md and TODO.md. Update TASKS.md sprint.
2026-02-21 21:02:46 +01:00
user
37c858f4d7 feat: add Mumble bot adapter with minimal protobuf codec
TCP/TLS connection over SOCKS5 proxy to Mumble servers for text chat.
Minimal varint/field protobuf encoder/decoder (no external dep) handles
Version, Authenticate, Ping, ServerSync, ChannelState, UserState, and
TextMessage message types. MumbleBot exposes the same duck-typed plugin
API as Bot/TeamsBot/TelegramBot. 93 new tests (1470 total).
2026-02-21 21:02:41 +01:00
user
0d92e6ed31 docs: update docs for Telegram integration 2026-02-21 20:06:29 +01:00
user
3bcba8b0a9 feat: add Telegram bot support via long-polling
TelegramBot adapter with getUpdates long-polling, all HTTP through
SOCKS5 proxy. Duck-typed TelegramMessage compatible with IRC Message.
Message splitting at 4096 chars, @botusername suffix stripping,
permission tiers via user IDs. 75 test cases.
2026-02-21 20:06:25 +01:00
user
4a304f2498 fix: route Teams send() through SOCKS5 proxy
Teams send() used urllib.request.urlopen directly, bypassing the SOCKS5
proxy. Replace with derp.http.urlopen to match all other outbound HTTP.
2026-02-21 20:06:20 +01:00
user
4a165e8b28 docs: update docs for Teams integration
- USAGE.md: Teams Integration section (config, setup, compat matrix)
- CHEATSHEET.md: Teams config snippet
- API.md: TeamsBot and TeamsMessage reference
- README.md: Teams in features list
- ROADMAP.md: v2.1.0 milestone
- TODO.md/TASKS.md: Teams items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:52:39 +01:00
user
014b609686 feat: add Microsoft Teams support via outgoing webhooks
TeamsBot adapter exposes the same plugin API as IRC Bot so ~90% of
plugins work without modification.  Uses raw asyncio HTTP server
(no MS SDK dependency) with HMAC-SHA256 signature validation.

- TeamsMessage dataclass duck-typed with IRC Message
- Permission tiers via AAD object IDs (exact match)
- Reply buffer collected and returned as HTTP JSON response
- Incoming webhook support for proactive send()
- IRC-only methods (join/part/kick/mode) as no-ops
- 74 new tests (1302 total)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:52:33 +01:00
user
c8879f6089 feat: add stable plugin API reference and bump to v2.0.0
Document the full public plugin surface (decorators, bot methods, IRC
primitives, state store, HTTP/DNS helpers) with semver stability
guarantees and breaking-change policy. Bump version from 0.1.0 to 2.0.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:22:47 +01:00
user
144193e3bb docs: update docs for multi-server support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:04:23 +01:00
user
073659607e feat: add multi-server support
Connect to multiple IRC servers concurrently from a single config file.
Plugins are loaded once and shared; per-server state is isolated via
separate SQLite databases and per-bot runtime state (bot._pstate).

- Add build_server_configs() for [servers.*] config layout
- Bot.__init__ gains name parameter, _pstate dict for plugin isolation
- cli.py runs multiple bots via asyncio.gather
- 9 stateful plugins migrated from module-level dicts to _ps(bot) pattern
- Backward compatible: legacy [server] config works unchanged

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:04:20 +01:00
user
e9528bd879 docs: update docs for ACL tiers and webhook
All checks were successful
CI / test (3.11) (push) Successful in 1m37s
CI / test (3.12) (push) Successful in 1m35s
CI / test (3.13) (push) Successful in 1m20s
- USAGE.md: permission tiers section, webhook config/API/example
- CHEATSHEET.md: ACL tiers and webhook quick-ref sections
- ROADMAP.md: mark webhook and ACL items done
- TODO.md: mark webhook and ACL items done
- TASKS.md: new sprint for ACL + webhook work
2026-02-21 17:59:22 +01:00
user
c483beb555 feat: add webhook listener for push events to channels
HTTP POST endpoint for external services (CI, monitoring, GitHub).
HMAC-SHA256 auth, JSON body, single POST endpoint at /.

- asyncio.start_server with raw HTTP parsing (zero deps)
- Body validation: channel prefix, non-empty text, 64KB cap
- !webhook admin command shows address, request count, uptime
- Module-level server guard prevents duplicates on reconnect
- 22 test cases in test_webhook.py
2026-02-21 17:59:14 +01:00
user
2514aa777d feat: add granular ACL tiers (trusted/oper/admin)
4-tier permission model: user < trusted < oper < admin.
Commands specify a required tier via tier= parameter.
Backward compatible: admin=True maps to tier="admin".

- TIERS constant and Handler.tier field in plugin.py
- _get_tier() method in bot.py with pattern matching
- _is_admin() preserved as thin wrapper
- operators/trusted config lists in config.py
- whoami shows tier, admins shows all configured tiers
- 32 test cases in test_acl.py
2026-02-21 17:59:05 +01:00
user
5bc59730c4 docs: update docs for cron, shortener, CI
Add !cron section to USAGE.md and CHEATSHEET.md.
Mark cron, URL shortener, CI complete in ROADMAP.md and TODO.md.
New sprint in TASKS.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 17:35:16 +01:00
user
6ef3fee72c feat: add Gitea Actions CI pipeline
Matrix build: Python 3.11, 3.12, 3.13.
Runs ruff lint and pytest on push/PR to master.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 17:35:12 +01:00
user
7b14efb30f feat: add cron plugin for scheduled commands
Admin-only plugin for interval-based command execution.
Supports add/del/list, 1m-7d intervals, 20 jobs/channel.
Persists via bot.state, restores on reconnect.
Includes test_cron.py (~38 cases).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 17:35:08 +01:00
user
aebe1589d2 feat: add URL shortening to subscription announcements
Bot.shorten_url() method delegates to flaskpaste plugin when loaded.
RSS, YouTube, and pastemoni announcements auto-shorten links.
Includes test_flaskpaste.py (9 cases) and FakeBot updates in 3 test files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 17:35:03 +01:00