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.
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).
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").
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>
!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>
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>
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.
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.
_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.
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>
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>
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>
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>
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).
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.
- 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>
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>
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>
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>
- 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
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
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
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>
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>
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>
Add cmd_paste to flaskpaste plugin (create paste, return URL).
Add test suites for encode, hash, defang, cidr, and dns plugins
(83 new test cases, 1093 total).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
canary: generate realistic fake credentials (token/aws/basic) for
planting as canary tripwires. Per-channel state persistence.
tcping: TCP connect latency probe through SOCKS5 proxy with
min/avg/max reporting. Proxy-compatible alternative to traceroute.
archive: save URLs to Wayback Machine via Save Page Now API,
routed through SOCKS5 proxy.
resolve: bulk DNS resolution (up to 10 hosts) via TCP DNS through
SOCKS5 proxy with concurrent asyncio.gather.
83 new tests (1010 total), docs updated.
YouTube's InnerTube /player endpoint now returns LOGIN_REQUIRED for the
WEB client. Switch to ANDROID client context which still returns full
videoDetails. Fixes missing duration in announcements and broken channel
resolution from video URLs.
Extract shared _innertube_player() helper to deduplicate payload
construction between _resolve_via_innertube and _fetch_duration.
Free, keyless API returning open ports, hostnames, CPEs, tags, and
known CVEs for any public IP. All requests routed through SOCKS5.
21 test cases (927 total).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop GeoLite2-ASN.mmdb dependency (required license key) in favor of
iptoasn.com ip2asn-v4.tsv (no auth, public domain). Bisect-based
lookup in pure stdlib, downloaded via SOCKS5 in update-data.sh.
Adds 30 test cases covering load, lookup, and command handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split output when running with --llm: addressed messages from owners
go to stdout, everything else (chatter, logs, plugin loads) goes to
info.log. Adds owner privilege level (superset of admin) for gating
LLM access. Status lines (connect, ping, disconnect, reconnect) and
bot replies also appear on stdout for session awareness.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetches duration via InnerTube player API for new videos at
announcement time. Displayed as compact h:mm:ss before views/likes.
Gracefully omitted for Shorts and unavailable content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Connect to IRC, join a channel, send commands, and print bot
responses. Waits for the bot's WHO cycle before sending.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Alert backends now populate structured `extra` field with engagement
metrics (views, stars, votes, etc.) instead of embedding them in titles.
Subscription plugins show richer announcements: Twitch viewer counts,
YouTube views/likes/dates, RSS published dates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Poll Pastebin archive and GitHub Gists for keyword matches,
announce hits to subscribed IRC channels. Follows rss.py
polling/subscription pattern with state persistence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Bot.long_reply() that sends lines directly when under threshold,
or creates a FlaskPaste paste with preview + link when over. Refactor
abuseipdb, alert history, crtsh, dork, exploitdb, and subdomain
plugins to use long_reply(). Configurable paste_threshold (default: 4).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>