Commit Graph

97 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
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
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
user
9abf8dce64 feat: add !paste command and unit tests for 5 core plugins
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>
2026-02-21 16:54:18 +01:00
user
e3bb793574 feat: add canary, tcping, archive, resolve plugins
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.
2026-02-20 19:38:10 +01:00
user
7c40a6b7f1 fix: switch youtube innertube to ANDROID client (WEB blocked)
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.
2026-02-20 19:38:01 +01:00
user
3de3f054df feat: add internetdb plugin (Shodan InternetDB host recon)
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>
2026-02-20 17:41:51 +01:00
user
442fea703c feat: replace MaxMind ASN with iptoasn.com TSV backend
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>
2026-02-19 20:43:00 +01:00
user
0c18ba8e3a feat: append source domain fragment to alert short URLs
Short URLs now include the original source domain as a URL fragment,
e.g. https://paste.mymx.me/s/foo#github.com, so the destination is
visible before clicking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:33:13 +01:00
user
8ce6922cc3 feat: add video duration to YouTube announcements
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>
2026-02-19 18:28:40 +01:00
user
1fe7da9ed8 feat: metadata enrichment for alerts and subscription plugins
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>
2026-02-19 10:00:17 +01:00
user
c3b19feb0f feat: add paste site keyword monitor plugin
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>
2026-02-18 09:01:46 +01:00
user
1836fa50af feat: paste overflow via FlaskPaste for long replies
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>
2026-02-17 22:07:31 +01:00
user
8cabe0f8e8 feat: add URL title preview plugin
Event-driven plugin that auto-fetches page titles for URLs posted in
channel messages. HEAD-then-GET via SOCKS5 pool, og:title priority,
cooldown dedup, !-suppression, binary/host filtering. 52 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 21:57:00 +01:00
user
94f563d55a feat: connection pooling via urllib3 + batch OG fetching
Replace per-request SOCKS5+TLS handshakes with urllib3 SOCKSProxyManager
connection pool (20 pools, 4 conns/host). Batch _fetch_og calls via
ThreadPoolExecutor to parallelize OG tag enrichment in alert polling.
Cache flaskpaste SSL context at module level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 20:52:22 +01:00
user
694c775782 fix: remove title truncation from alert backend builders
Mastodon, Bluesky, GitHub, GitLab, npm, PyPI, and arXiv backends
no longer truncate content/descriptions in titles. Full text is
shown on the PRIVMSG line; only !alert history keeps truncation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:44:48 +01:00
user
9672e325c2 fix: show full alert titles, split metadata into ACTION line
ACTION carries the tag/date/URL, PRIVMSG carries the uncropped title.
Removes _truncate on alert output for better readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:22:23 +01:00
user
76301ac8f2 perf: concurrent fetches for multi-instance alert backends
Add _fetch_many() helper using ThreadPoolExecutor to query instances
in parallel. Refactors PeerTube, Mastodon, Lemmy, and SearXNG from
sequential to concurrent fetches. Also adds retries parameter to
derp.http.urlopen; multi-instance backends use retries=1 since
instance redundancy already provides resilience.

Worst-case wall time per backend drops from N*timeout to 1*timeout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:02:57 +01:00
user
da908a45e4 fix: track alert backend errors independently
Per-backend error counts with exponential backoff: after 5 consecutive
failures a backend is skipped every 2^(n-5) cycles (capped at 32).
Working backends are no longer penalized by one flaky backend doubling
the entire poll interval.

Migrates last_error (string) to last_errors (dict per backend).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:51:42 +01:00
user
f2199f2bec perf: seed alert seen IDs in background on add
Reply immediately with empty seen, run a silent _poll_once in a
background task to populate seen IDs, then start the poller.
Eliminates the 30-120s blocking wait from 27 sequential backend queries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:45:25 +01:00
user
3c505dd825 fix: persist short URLs in alert history, regenerate on expiry
Store shortened URLs in the results DB at poll time alongside the
original URL. History output uses the stored short URL directly,
only regenerating (and persisting) when no short URL exists yet.
Original URL always preserved for re-shortening if needed.
2026-02-16 23:24:26 +01:00
user
c92fdbfc30 refactor: remove !paste command, keep as internal helper
Paste creation is only used internally by the bot for multi-line
output. The create_paste() helper remains importable by other plugins.
2026-02-16 23:17:53 +01:00
user
ffa75670e2 fix: use mTLS client cert to bypass PoW on flaskpaste
When secrets/flaskpaste/derp.crt and derp.key are present, load them
into the SSL context for mutual TLS auth and skip the PoW challenge
entirely. Fall back to PoW only when no client cert is available.
2026-02-16 23:13:09 +01:00
user
3cdc00c285 feat: add flaskpaste plugin with paste/shorten commands
- PoW-authenticated paste creation and URL shortening via FlaskPaste
- !paste <text> creates a paste, !shorten <url> shortens a URL
- Module-level shorten_url/create_paste helpers for cross-plugin use
- Alert plugin auto-shortens URLs in announcements and history output
- Custom TLS CA cert support via secrets/flaskpaste/derp.crt
- No SOCKS proxy -- direct urllib.request to FlaskPaste instance
2026-02-16 23:10:59 +01:00
user
35acc744ac fix: use DNS-over-HTTPS with provider rotation for emailcheck
Tor exit nodes poison plain DNS on port 53 and MITM some HTTPS
connections. Replace raw TCP DNS with DoH (Google, Cloudflare, Quad9)
and retry up to 5 times across providers to find a clean exit node.
MX results are now sorted by priority.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:40:43 +01:00
user
e8d803abe6 fix: account for server prefix in IRC line splitting
The 512-byte IRC limit includes the :nick!user@host prefix the server
prepends when relaying. Reserve 64 bytes for it and prefer splitting at
space boundaries instead of mid-word. Also strip the command prefix and
"Commands:" label from help output to keep the listing compact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:02:52 +01:00
user
eb37fef730 feat: add jwt, mac, abuseipdb, virustotal, and emailcheck plugins
v2.0.0 sprint 1 -- five standalone plugins requiring no core changes:

- jwt: decode JWT header/payload, flag alg=none/expired/nbf issues
- mac: IEEE OUI vendor lookup, random MAC generation, OUI download
- abuseipdb: IP reputation check + abuse reporting (admin) via API
- virustotal: hash/IP/domain/URL lookup via VT APIv3, 4/min rate limit
- emailcheck: SMTP RCPT TO verification via MX + SOCKS proxy (admin)

Also adds update_oui() to update-data.sh and documents all five
plugins in USAGE.md and CHEATSHEET.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 21:04:43 +01:00
user
8e2b94fef0 feat: add 11 alert backends and fix PyPI/DEV.to search
Add Wikipedia, Stack Exchange, GitLab, npm, PyPI, Docker Hub,
arXiv, Lobsters, DEV.to, Medium, and Hugging Face backends to
the alert plugin (16 -> 27 total). Fix PyPI backend to use RSS
updates feed (web search now requires JS challenge). Fix DEV.to
to use public articles API (feed_content endpoint returns empty).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 20:07:01 +01:00
user
34d5dd6f8d fix: resolve YouTube channel ID via InnerTube for video URLs
Video URLs (watch, shorts, embed, youtu.be) now resolve the channel
ID through the InnerTube player API -- a small JSON POST instead of
fetching the full 1MB watch page. Much more resilient to transient
proxy failures. Page scraping remains as fallback for handle URLs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 18:39:32 +01:00
user
daa3370433 feat: add short IDs to alert results with !alert info command
Each alert result gets a deterministic 8-char base36 ID derived from
backend:item_id. IDs appear in announcements and history, and can be
looked up with !alert info <id> for full details. Existing rows are
backfilled on startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:20:56 +01:00