Files
derp/docs/USAGE.md
user 8f1df167b9
Some checks failed
CI / gitleaks (push) Failing after 4s
CI / lint (push) Successful in 24s
CI / test (3.11) (push) Failing after 30s
CI / test (3.13) (push) Failing after 34s
CI / test (3.12) (push) Failing after 36s
CI / build (push) Has been skipped
feat: fade-out on skip/stop/prev, song metadata on keep
- Add fade_step parameter to stream_audio for fast volume ramps
- _fade_and_cancel helper: smooth ~0.8s fade before track switch
- !skip, !stop, !seek now fade out instead of cutting instantly
- !prev command: go back to previous track (10-track history stack)
- !keep fetches title/artist/duration via yt-dlp, stores in bot.state
- !kept displays metadata (title, artist, duration, file size)
- !kept clear also removes stored metadata
- 29 new tests for fade, prev, history, keep metadata

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 06:38:25 +01:00

64 KiB

Usage Guide

Running

# From project directory
derp

# With options
derp --config /path/to/derp.toml --verbose

CLI Flags

Flag Description
-c, --config PATH Config file path
-v, --verbose Debug logging
--cprofile [PATH] Enable cProfile, dump to PATH [derp.prof]
--tracemalloc [N] Enable tracemalloc, capture N frames deep [10]
-V, --version Print version
-h, --help Show help

Configuration

All settings in config/derp.toml.

Single-Server (Legacy)

[server]
host = "irc.libera.chat"    # IRC server hostname
port = 6697                   # Port (6697 = TLS, 6667 = plain)
tls = true                    # Enable TLS encryption
proxy = false                 # Route through SOCKS5 proxy (default: false)
nick = "derp"                 # Bot nickname
user = "derp"                 # Username (ident)
realname = "derp IRC bot"     # Real name field
password = ""                 # Server password (optional)
sasl_user = ""                # SASL PLAIN username (optional)
sasl_pass = ""                # SASL PLAIN password (optional)
ircv3_caps = [                # IRCv3 capabilities to request
    "multi-prefix",
    "away-notify",
    "server-time",
    "cap-notify",
    "account-notify",
]

[bot]
prefix = "!"                  # Command prefix character
channels = ["#test"]          # Channels to join on connect
plugins_dir = "plugins"       # Plugin directory path
rate_limit = 2.0              # Max messages per second (default: 2.0)
rate_burst = 5                # Burst capacity (default: 5)
paste_threshold = 4           # Max lines before overflow to FlaskPaste (default: 4)
admins = []                   # Hostmask patterns (fnmatch), IRCOPs auto-detected
timezone = "UTC"              # Timezone for calendar reminders (IANA tz name)
operators = []                # Hostmask patterns for "oper" tier (fnmatch)
trusted = []                  # Hostmask patterns for "trusted" tier (fnmatch)

[logging]
level = "info"                # Logging level: debug, info, warning, error
format = "text"               # Log format: "text" (default) or "json"

[webhook]
enabled = false               # Enable HTTP webhook listener
host = "127.0.0.1"            # Bind address
port = 8080                   # Bind port
secret = ""                   # HMAC-SHA256 shared secret (empty = no auth)

Multi-Server

Connect to multiple IRC servers from a single config. Plugins are loaded once and shared; state is isolated per server (data/state-<name>.db).

[bot]
prefix = "!"                  # Shared defaults for all servers
plugins_dir = "plugins"
admins = ["*!~root@*.ops.net"]

[servers.libera]
host = "irc.libera.chat"
port = 6697
tls = true
nick = "derp"
channels = ["#test", "#ops"]

[servers.oftc]
host = "irc.oftc.net"
port = 6697
tls = true
nick = "derpbot"
channels = ["#derp"]
admins = ["*!~admin@oftc.host"]  # Override shared admins

[logging]
level = "info"
format = "json"

[webhook]
enabled = true
port = 8080
secret = "shared-secret"

Each [servers.<name>] block may contain both server-level keys (host, port, tls, nick, etc.) and bot-level overrides (prefix, channels, admins, operators, trusted, rate_limit, rate_burst, paste_threshold). Unset keys inherit from the shared [bot] and [server] defaults.

The server name (e.g. libera, oftc) is used for:

  • Log prefixes and !version output
  • State DB path (data/state-libera.db)
  • Plugin runtime state isolation

Existing single-server configs ([server] section) continue to work unchanged. The server name is derived from the hostname automatically.

Built-in Commands

Command Description
!ping Bot responds with "pong"
!help List all available commands
!help <cmd> Show help for a specific command
!help <plugin> Show plugin description and its commands
!version Show bot version
!uptime Show how long the bot has been running
!echo <text> Echo back text (example plugin)
!cert <domain> [...] Lookup CT logs for up to 5 domains
!whoami Show your hostmask and admin status
!load <plugin> Hot-load a plugin (admin)
!reload <plugin> Reload a plugin (admin)
!unload <plugin> Unload a plugin (admin)
!admins Show admin patterns and detected opers (admin)
!plugins List loaded plugins with handler counts
!state <action> <plugin> [key] Inspect plugin state store (admin)
!kick <nick> [reason] Kick user from channel (admin)
!ban <mask> Ban a hostmask in channel (admin)
!unban <mask> Remove a ban from channel (admin)
!topic [text] Set or query channel topic (admin)
!mode <mode> [args] Set channel mode (admin)
!dns <target> [type] DNS lookup (A, AAAA, MX, NS, TXT, CNAME, PTR, SOA)
!tdns <target> [type] [@server] TCP DNS lookup via SOCKS5 proxy
!encode <fmt> <text> Encode text (b64, hex, url, rot13)
!decode <fmt> <text> Decode text (b64, hex, url, rot13)
!hash [algo] <text> Generate hash digests (md5, sha1, sha256, sha512)
!hashid <hash> Identify hash type by format
!defang <ioc> Defang URLs/IPs/domains for safe sharing
!refang <text> Restore defanged IOCs
!revshell <type> <ip> <port> Generate reverse shell one-liner
!cidr <network> Subnet info (range, hosts, mask)
!cidr contains <net> <ip> Check if IP belongs to network
!whois <domain|ip> WHOIS lookup via raw TCP (port 43)
!portcheck <host> [ports] Async TCP port scan (max 20 ports)
!httpcheck <url> HTTP status, redirects, response time
!tlscheck <host> [port] TLS version, cipher, cert details
!blacklist <ip> Check IP against 10 DNSBLs
!rand <mode> [args] Random: password, hex, uuid, bytes, int, coin, dice
!timer <duration> [label] Set countdown timer with notification
!timer list Show active timers
!timer cancel <label> Cancel a running timer
!remind <duration> <text> One-shot reminder after duration
!remind every <duration> <text> Repeating reminder at interval
!remind at <YYYY-MM-DD> [HH:MM] <text> Calendar reminder at specific date/time
!remind yearly <MM-DD> [HH:MM] <text> Yearly recurring reminder
!remind list Show all active reminders
!remind cancel <id> Cancel a reminder by ID
!geoip <ip> GeoIP lookup (city, country, coords, timezone)
!asn <ip> ASN lookup (AS number, organization)
!tor <ip|update> Check IP against Tor exit nodes
!iprep <ip|update> Check IP against Firehol/ET blocklists
!cve <id|search> CVE lookup from local NVD mirror
!opslog <add|list|search|del|clear> Timestamped operational log
!note <set|get|del|list|clear> Per-channel key-value notes
!subdomain <domain> [brute] Subdomain enumeration (crt.sh + DNS)
!headers <url> HTTP header fingerprinting
!exploitdb <search|id|cve|update> Search local Exploit-DB mirror
!payload <type> [variant] Web vuln payload templates
!dork <category|list> [target] Google dork query builder
!wayback <url> [YYYYMMDD] Wayback Machine snapshot lookup
!username <user> Check username across ~25 services
!username <user> <service> Check single service
!username list Show available services by category
!alert <add|del|list|check|info|history> Keyword alert subscriptions across platforms
!searx <query> Search SearXNG and show top results
!ask <question> Single-shot LLM question via OpenRouter
!chat <msg|clear|model|models> Conversational LLM chat with history
!jwt <token> Decode JWT header, claims, and flag issues
!mac <address|random|update> MAC OUI vendor lookup / random MAC
!abuse <ip> [ip2 ...] AbuseIPDB reputation check
!abuse <ip> report <cats> <comment> Report IP to AbuseIPDB (admin)
!vt <hash|ip|domain|url> VirusTotal lookup
!emailcheck <email> [email2 ...] SMTP email verification (admin)
!internetdb <ip> Shodan InternetDB host recon (ports, CVEs, CPEs)
!canary <gen|list|info|del> Canary token generator/tracker
!tcping <host> [port] [count] TCP connect latency probe via SOCKS5
!archive <url> Save URL to Wayback Machine
!resolve <host> [host2 ...] [type] Bulk DNS resolution via TCP/SOCKS5
!shorten <url> Shorten a URL via FlaskPaste
!paste <text> Create a paste via FlaskPaste
!pastemoni <add|del|list|check> Paste site keyword monitoring
!cron <add|del|list> Scheduled command execution (admin)
!webhook Show webhook listener status (admin)

Command Shorthand

Commands can be abbreviated to any unambiguous prefix:

!h           -> !help (unique match)
!pi          -> !ping (unique match)
!p           -> error: ambiguous (ping, plugins)

Exact matches always take priority over prefix matches.

!cert -- Certificate Transparency Lookup

Query crt.sh CT logs to enumerate SSL certificates for domains. Reports totals (expired/valid) and flags domains still serving expired certs.

!cert example.com
!cert example.com badsite.com another.org

Output format:

example.com -- 127 certs (23 expired, 104 valid)
badsite.com -- 45 certs (8 expired, 37 valid) | live cert EXPIRED
broken.test -- error: timeout
  • Max 5 domains per invocation
  • crt.sh can be slow; the bot confirms receipt before querying
  • Live cert check runs only when expired CT entries exist

Per-Channel Plugin Control

Restrict which plugins are active in specific channels. Channels without a [channels."<name>"] section run all plugins. Channels with a plugins list only run those plugins. The core plugin is always active (exempt from filtering). Private messages are always unrestricted.

[channels."#public"]
plugins = ["core", "dns", "cidr", "encode"]

[channels."#ops"]
plugins = ["core", "revshell", "payload", "exploitdb", "opslog"]

# #unrestricted -- no section, runs everything

When a command is denied by channel config, it is silently ignored (no error message). Event handlers from denied plugins are also skipped.

Structured Logging (JSON)

Set format = "json" in [logging] to emit one JSON object per log line (JSONL), suitable for log aggregation tools.

[logging]
level = "info"
format = "json"

Each line contains:

Field Description
ts Timestamp (YYYY-MM-DDTHH:MM:SS)
level Log level (debug, info, warning, error)
logger Logger name (derp.bot, derp.plugin, etc.)
msg Log message text
exc Exception traceback (only present on errors)

Default format is "text" (human-readable, same as before).

Permission Tiers (ACL)

The bot uses a 4-tier permission model. Each command has a required tier; users must meet or exceed it.

user < trusted < oper < admin
Tier Granted by
user Everyone (default)
trusted [bot] trusted hostmask patterns
oper [bot] operators hostmask patterns
admin [bot] admins hostmask patterns or IRC operator status
[bot]
admins = ["*!~root@*.ops.net"]
operators = ["*!~staff@trusted.host"]
trusted = ["*!~user@known.host"]

All lists are empty by default -- only IRC operators get admin access unless patterns are configured. Patterns use fnmatch-style matching.

Oper Detection

IRC operators are detected via the * flag in WHO replies. Detection happens at two points:

  • On connect -- WHO #channel for each configured channel
  • On user JOIN -- debounced WHO #channel (2-second window)

The debounce prevents flooding the server during netsplit recovery: many rapid JOINs produce a single WHO per channel. Note that the first command after joining may not yet have oper status -- the debounced WHO fires after a 2-second delay. Users who QUIT are removed from the oper set automatically.

Command Description
!whoami Show your hostmask and permission tier
!admins Show configured tiers and detected opers (admin)

Admin-restricted commands: !load, !reload, !unload, !admins, !state, !kick, !ban, !unban, !topic, !mode, !webhook.

Writing Tiered Commands

# admin=True still works (maps to tier="admin")
@command("dangerous", help="Admin-only action", admin=True)
async def cmd_dangerous(bot, message):
    ...

# Explicit tier for finer control
@command("moderate", help="Trusted-only action", tier="trusted")
async def cmd_moderate(bot, message):
    ...

IRCv3 Capability Negotiation

The bot negotiates IRCv3 capabilities using CAP LS 302 during registration. This enables richer features on servers that support them.

Default Capabilities

[server]
ircv3_caps = ["multi-prefix", "away-notify", "server-time",
              "cap-notify", "account-notify"]
Capability Purpose
multi-prefix Better IRCOP/voice detection
away-notify Receive AWAY status changes
server-time Accurate message timestamps
cap-notify Dynamic capability updates
account-notify Account login/logout notices
sasl Auto-added when SASL credentials configured

The bot only requests caps the server advertises. SASL is automatically included when sasl_user and sasl_pass are configured.

Message Tags

IRCv3 message tags (@key=value;...) are parsed automatically and available on the message object as message.tags (a dict[str, str]). Values are unescaped per the IRCv3 spec.

Channel Management

Channel management commands require admin privileges and must be used in a channel (not DM).

!kick <nick> [reason]     Kick a user from the channel
!ban <mask>               Set +b on a hostmask (e.g. *!*@bad.host)
!unban <mask>             Remove +b from a hostmask
!topic [text]             Set topic (empty = query current topic)
!mode <mode> [args]       Set raw MODE on the channel

The bot must have channel operator status for these commands to take effect.

Invite Auto-Join

When an admin or IRC operator sends an INVITE, the bot automatically joins the target channel and persists it for auto-rejoin on reconnect. Non-admin invites are silently ignored. If the bot is kicked from a persisted channel, it is removed from the auto-rejoin list. Channels already in the config [bot] channels are skipped (they rejoin via the normal path).

Plugin State Persistence

Plugins can persist key-value data across restarts via bot.state:

bot.state.set("myplugin", "last_run", "2026-02-15")
value = bot.state.get("myplugin", "last_run")
bot.state.delete("myplugin", "last_run")
keys = bot.state.keys("myplugin")
bot.state.clear("myplugin")

Data is stored in data/state-<name>.db (SQLite, one per server). Each plugin gets its own namespace so keys never collide.

Inspection Commands (admin)

!state list <plugin>      List keys for a plugin
!state get <plugin> <key> Get a value
!state del <plugin> <key> Delete a key
!state clear <plugin>     Clear all state for a plugin

Local Databases (Wave 3)

Several plugins rely on local data files in the data/ directory. Use the update script or in-bot commands to populate them.

Data Update Script

./scripts/update-data.sh                        # Update all feeds
MAXMIND_LICENSE_KEY=xxx ./scripts/update-data.sh # Include GeoLite2

The script is cron-friendly (exit 0/1, quiet unless NO_COLOR is unset).

In-Bot Updates

!tor update          # Download Tor exit node list
!iprep update        # Download Firehol/ET blocklist feeds
!cve update          # Download NVD CVE feed (slow, paginated)

Data Directory Layout

data/
  GeoLite2-City.mmdb    # MaxMind GeoIP (requires license key)
  GeoLite2-ASN.mmdb     # MaxMind ASN (requires license key)
  tor-exit-nodes.txt    # Tor exit node IPs
  iprep/                # Firehol/ET blocklist feeds
    firehol_level1.netset
    firehol_level2.netset
    et_compromised.ipset
    ...
  nvd/                  # NVD CVE JSON files
    nvd_0000.json
    ...

GeoLite2 databases require a free MaxMind license key. Set MAXMIND_LICENSE_KEY when running the update script.

Plugin Management

Plugins can be loaded, unloaded, and reloaded at runtime without restarting the bot.

!load crtsh          # Hot-load a new plugin from plugins/
!reload crtsh        # Reload a changed plugin
!unload crtsh        # Remove a plugin and all its handlers
!plugins             # List loaded plugins with handler counts

The core plugin cannot be unloaded (prevents losing !load/!reload), but it can be reloaded.

Webhook Listener

Receive HTTP POST requests from external services (CI, monitoring, GitHub, etc.) and relay messages to IRC channels.

Configuration

[webhook]
enabled = true
host = "127.0.0.1"
port = 8080
secret = "your-shared-secret"

HTTP API

Single endpoint: POST /

Request body (JSON):

{"channel": "#ops", "text": "Deploy v2.3.1 complete"}

Optional "action": true sends as a /me action.

Authentication: HMAC-SHA256 via X-Signature: sha256=<hex> header. If secret is empty, no authentication is required.

Response codes:

Status When
200 OK Message sent
400 Bad Request Invalid JSON, missing/invalid channel, empty text
401 Unauthorized Bad or missing HMAC signature
405 Method Not Allowed Non-POST request
413 Payload Too Large Body > 64 KB

Usage Example

SECRET="your-shared-secret"
BODY='{"channel":"#ops","text":"Deploy v2.3.1 complete"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST http://127.0.0.1:8080/ \
  -H "Content-Type: application/json" \
  -H "X-Signature: sha256=$SIG" \
  -d "$BODY"

Status Command

!webhook    Show listener address, request count, uptime (admin)

The server starts on IRC connect (event 001) and runs for the lifetime of the bot. If the server is already running (e.g. after reconnect), it is not restarted.

Writing Plugins

Create a .py file in the plugins/ directory:

from derp.plugin import command, event

@command("hello", help="Greet the user")
async def cmd_hello(bot, message):
    """Handler receives bot instance and parsed Message."""
    await bot.reply(message, f"Hello, {message.nick}!")

@event("JOIN")
async def on_join(bot, message):
    """Event handlers fire on IRC events (JOIN, PART, QUIT, etc.)."""
    if message.nick != bot.nick:
        await bot.send(message.target, f"Welcome, {message.nick}")

Plugin API

The bot object provides:

Method Description
bot.send(target, text) Send message to channel or nick
bot.reply(msg, text) Reply to source (channel or PM)
bot.action(target, text) Send /me action
bot.join(channel) Join a channel
bot.part(channel [, reason]) Leave a channel
bot.quit([reason]) Disconnect from server
bot.kick(channel, nick [, reason]) Kick a user
bot.mode(target, mode_str, *args) Set a mode
bot.set_topic(channel, topic) Set channel topic
bot.state Plugin state store (get/set/delete/keys/clear)

The message object provides:

Attribute Description
message.nick Sender's nickname
message.prefix Full nick!user@host prefix
message.command IRC command (PRIVMSG, JOIN, etc.)
message.target First param (channel or nick)
message.text Trailing text content
message.is_channel Whether target is a channel
message.params All message parameters
message.tags IRCv3 message tags (dict)

Message Truncation

Messages are automatically split at UTF-8 safe boundaries to comply with the IRC 512-byte line limit (RFC 2812). The overhead of PRIVMSG <target> : and \r\n is accounted for, so plugins can send arbitrarily long text without worrying about protocol limits.

!remind -- Reminders

Set one-shot, repeating, or calendar-based reminders. Duration-based reminders are in-memory and lost on restart. Calendar reminders (at, yearly) are persisted via bot.state and restored on reconnect.

!remind 5m check the oven               One-shot after 5 minutes
!remind 2d12h renew cert                 One-shot after 2 days 12 hours
!remind every 1h drink water             Repeat every hour
!remind at 2027-06-15 deploy release     Fire at specific date (noon default)
!remind at 2027-06-15 14:30 deploy       Fire at specific date + time
!remind yearly 02-14 valentines          Every year on Feb 14 at noon
!remind yearly 12-25 09:00 merry xmas    Every year on Dec 25 at 09:00
!remind list                             Show all active reminders
!remind cancel abc123                    Cancel by ID
  • Default time when omitted: 12:00 (noon) in configured timezone
  • Timezone: bot.timezone in config, default UTC
  • Leap day (02-29): fires on Feb 28 in non-leap years
  • Past dates for at are rejected
  • Calendar reminders survive restarts; duration-based do not

Reconnect Backoff

On connection loss, the bot reconnects with exponential backoff and jitter:

  • Initial delay: 5 seconds
  • Growth: doubles each attempt (5s, 10s, 20s, 40s, ...)
  • Cap: 300 seconds (5 minutes)
  • Jitter: +/- 25% to avoid thundering herd
  • Resets to 5s after a successful connection

!dork -- Google Dork Query Builder

Generate Google dork queries for a target domain. Template-based, no HTTP requests -- just outputs the query string for manual use.

!dork list                    List all dork categories
!dork admin example.com       Admin/login panel dorks
!dork files example.com       Exposed document dorks

Categories: admin, backup, cloud, config, creds, dirs, errors, exposed, files, login.

!wayback -- Wayback Machine Lookup

Check the Wayback Machine for archived snapshots of a URL.

!wayback example.com                Check latest snapshot
!wayback example.com/page 20240101  Check snapshot near a date

Auto-prepends https:// if no scheme is provided. Uses the Wayback Machine availability API.

!username -- Username Enumeration

Check username availability across ~25 services using HTTP probes and public JSON APIs. Supports GitHub, GitLab, Reddit, Docker Hub, Keybase, Dev.to, Twitch, Steam, and more.

!username list                     List services by category
!username john                     Full scan (~25 services)
!username john github              Check single service

Output format:

Full scan:
  Checking "john" across 25 services...
  john -- 8 found, 14 not found, 3 errors
  Found: GitHub, GitLab, Reddit, Twitch, Steam, PyPI, Docker Hub, Medium

Single service:
  GitHub: john -> found | https://github.com/john
  GitHub: john -> not found

List:
  Dev: GitHub, GitLab, Codeberg, ...
  Social: Reddit, Twitter/X, ...
  Media: Twitch, Spotify, ...
  Other: Steam, Pastebin, ...
  • Username must match [a-zA-Z0-9._-]{1,39}
  • Full scan sends acknowledgment before probing
  • 8 parallel workers, 20s overall timeout
  • Three check methods: HTTP status, JSON API, body search

!rss -- RSS/Atom Feed Subscriptions

Subscribe RSS or Atom feeds to IRC channels with automatic polling and new-item announcements.

!rss add <url> [name]       Subscribe a feed to this channel (admin)
!rss del <name>             Unsubscribe a feed (admin)
!rss list                   List feeds in this channel
!rss check <name>           Force-poll a feed now
  • add and del require admin privileges
  • All subcommands must be used in a channel (not PM)
  • If name is omitted on add, it is derived from the URL hostname
  • Names must be lowercase alphanumeric + hyphens, 1-20 characters
  • Maximum 20 feeds per channel

Polling and announcements:

  • Feeds are polled every 10 minutes by default
  • On add, existing items are recorded without announcing (prevents flood)
  • New items are announced as [name] title | YYYY-MM-DD -- link
  • Published date is included when available (RSS pubDate, Atom published)
  • Maximum 5 items announced per poll; excess shown as ... and N more
  • Titles are truncated to 80 characters
  • Supports HTTP conditional requests (ETag, If-Modified-Since)
  • 5 consecutive errors doubles the poll interval (max 1 hour)
  • Feed subscriptions persist across bot restarts via bot.state

!yt -- YouTube Channel Subscriptions

Follow YouTube channels and announce new videos in IRC channels. Accepts any YouTube URL (video, channel, handle, shorts, embed) and resolves it to the channel's Atom feed.

!yt follow <url> [name]    Follow a YouTube channel (admin)
!yt unfollow <name>        Unfollow a channel (admin)
!yt list                   List followed channels
!yt check <name>           Force-poll a channel now
  • follow and unfollow require admin privileges
  • All subcommands must be used in a channel (not PM)
  • If name is omitted on follow, it is derived from the channel title
  • Names must be lowercase alphanumeric + hyphens, 1-20 characters
  • Maximum 20 channels per IRC channel

URL resolution:

  • /channel/UCXXX URLs extract the channel ID directly
  • All other YouTube URLs (handles, videos, shorts) fetch the page to resolve
  • Constructs the YouTube Atom feed URL from the resolved channel ID

Polling and announcements:

  • Channels are polled every 10 minutes by default
  • On follow, existing videos are recorded without announcing (prevents flood)
  • New videos are announced as [name] Video Title | 18:25 1.5Mv 45klk 2026-01-15 -- URL
  • Metadata suffix includes duration, views, likes, and published date when available
  • Duration fetched via InnerTube API per new video (only at announcement time)
  • Maximum 5 videos announced per poll; excess shown as ... and N more
  • Titles are truncated to 80 characters
  • Supports HTTP conditional requests (ETag, If-Modified-Since)
  • 5 consecutive errors doubles the poll interval (max 1 hour)
  • Subscriptions persist across bot restarts via bot.state

!twitch -- Twitch Livestream Notifications

Follow Twitch streamers and get notified when they go live. Uses Twitch's public GQL endpoint (no API credentials required).

!twitch follow <username> [name]   Follow a streamer (admin)
!twitch unfollow <name>            Unfollow a streamer (admin)
!twitch list                       List followed streamers
!twitch check <name>               Check status now
  • follow and unfollow require admin privileges
  • All subcommands must be used in a channel (not PM)
  • If name is omitted on follow, it defaults to the Twitch login (lowercase)
  • Names must be lowercase alphanumeric + hyphens, 1-20 characters
  • Twitch usernames must match [a-zA-Z0-9_]{1,25}
  • Maximum 20 streamers per IRC channel

Polling and announcements:

  • Streamers are polled every 2 minutes by default
  • On follow, the current stream state is recorded without announcing
  • Announcements fire on state transitions: offline to live, or new stream ID
  • Format: [name] is live: Stream Title (Game) | 50k viewers -- https://twitch.tv/login
  • Game is omitted if not set; viewer count shown when available
  • Titles are truncated to 80 characters
  • 5 consecutive errors doubles the poll interval (max 1 hour)
  • Subscriptions persist across bot restarts via bot.state
  • list shows live/error status with viewer count: name (live, 50k)
  • check forces an immediate poll and reports current status

Search the local SearXNG instance and display top results.

!searx <query...>     Search SearXNG and show top results
  • Open to all users, channel only (no PM)
  • Query is everything after !searx
  • Shows top 3 results as Title -- URL
  • Titles truncated to 80 characters
  • Query limited to 200 characters

Output format:

Title One -- https://example.com/page1
Title Two -- https://example.com/page2
Title Three -- https://example.com/page3

!ask / !chat -- LLM Chat (OpenRouter)

Chat with large language models via OpenRouter's API. !ask is stateless (single question), !chat maintains per-user conversation history.

!ask <question>              Single-shot question (no history)
!chat <message>              Chat with conversation history
!chat clear                  Clear your history
!chat model                  Show current model
!chat model <name>           Switch model
!chat models                 List suggested free models

Output format:

<alice> !ask what is DNS
<derp> DNS (Domain Name System) translates domain names to IP addresses...

<alice> !chat explain TCP
<derp> TCP is a connection-oriented transport protocol...
<alice> !chat how does the handshake work
<derp> The TCP three-way handshake: SYN, SYN-ACK, ACK...
  • Open to all users, works in channels and PMs
  • Per-user cooldown: 5 seconds between requests
  • Conversation history capped at 20 messages per user (ephemeral, not persisted across restarts)
  • Responses truncated to 400 characters; multi-line replies use paste overflow
  • Default model: openrouter/auto (auto-routes to best available free model)
  • Reasoning models (DeepSeek R1) are handled transparently -- falls back to the reasoning field when content is empty
  • Rate limit errors (HTTP 429) produce a clear user-facing message

Configuration:

[openrouter]
api_key = ""                  # or set OPENROUTER_API_KEY env var
model = "openrouter/auto"     # default model
system_prompt = "You are a helpful IRC bot assistant. Keep responses concise and under 200 words."

API key: set OPENROUTER_API_KEY env var (preferred) or api_key under [openrouter] in config. The env var takes precedence.

!alert -- Keyword Alert Subscriptions

Search keywords across 27 platforms and announce new results. Unlike !rss/!yt/!twitch which follow specific channels/feeds, !alert searches keywords across all supported platforms simultaneously.

!alert add <name> <keyword...>   Add keyword alert (admin)
!alert del <name>                Remove alert (admin)
!alert list                      List alerts
!alert check <name>              Force-poll now
!alert info <id>                 Show full details for a result
!alert history <name> [n]        Show recent results (default 5, max 20)
  • add and del require admin privileges
  • All subcommands must be used in a channel (not PM)
  • Name is required as the first argument after add; everything after is the keyword
  • Names must be lowercase alphanumeric + hyphens, 1-20 characters
  • Keywords: 1-100 characters, free-form text
  • Maximum 20 alerts per IRC channel

Platforms searched:

  • YouTube (yt) -- InnerTube search API (no auth required)
  • Twitch (tw) -- Public GQL endpoint: live streams and VODs (no auth required)
  • SearXNG (sx) -- Local SearXNG instance, searches general/news/videos/social media categories filtered to last 24h (no auth required)
  • Reddit (rd) -- JSON search API, sorted by new, past week (no auth required)
  • Mastodon (ft) -- Public hashtag timeline across 4 instances (no auth required)
  • DuckDuckGo (dg) -- HTML lite search endpoint via SOCKS5 proxy (no auth required)
  • Google News (gn) -- Public RSS feed via SOCKS5 proxy (no auth required)
  • Kick (kk) -- Public search API: channels and livestreams (no auth required)
  • Dailymotion (dm) -- Public video API, sorted by recent (no auth required)
  • PeerTube (pt) -- Federated video search across 4 instances (no auth required)
  • Bluesky (bs) -- Public post search API via SOCKS5 proxy (no auth required)
  • Lemmy (ly) -- Federated post search across 4 instances (no auth required)
  • Odysee (od) -- LBRY JSON-RPC claim search: video, audio, documents (no auth required)
  • Archive.org (ia) -- Internet Archive advanced search, sorted by date (no auth required)
  • Hacker News (hn) -- Algolia search API, sorted by date (no auth required)
  • GitHub (gh) -- Repository search API, sorted by recently updated (no auth required)
  • Wikipedia (wp) -- MediaWiki search API, English Wikipedia (no auth required)
  • Stack Exchange (se) -- Stack Overflow search API, sorted by activity (no auth required)
  • GitLab (gl) -- Public project search API, sorted by last activity (no auth required)
  • npm (nm) -- npm registry search API (no auth required)
  • PyPI (pp) -- Recent package updates RSS feed, keyword-filtered (no auth required)
  • Docker Hub (dh) -- Public repository search API (no auth required)
  • arXiv (ax) -- Atom search API for academic papers (no auth required)
  • Lobsters (lb) -- Community link aggregator search (no auth required)
  • DEV.to (dv) -- Forem articles API, tag-based search (no auth required)
  • Medium (md) -- Tag-based RSS feed (no auth required)
  • Hugging Face (hf) -- Model search API, sorted by downloads (no auth required)

Backend metadata (shown as | extra suffix on titles):

Tag Metrics Example
tw viewers / views 500 viewers, 1k views
rd score, comments +127 42c
ft reblogs, favourites 3rb 12fav
bs likes, reposts 5lk 2rp
ly score, comments +15 3c
kk viewers 500 viewers
dm views 1.2k views
pt views, likes 120v 5lk
hn points, comments 127pt 42c
gh stars, forks 42* 5fk
gl stars, forks 42* 5fk
se score, answers, views +5 3a 1.2kv
dh stars, pulls 42* 1.2M pulls
hf downloads, likes 500dl 12lk
dv reactions, comments +15 3c

Polling and announcements:

  • Alerts are polled every 5 minutes by default
  • On add, the bot replies immediately; existing results are seeded in the background to avoid flooding
  • New results announced as two lines:
    • ACTION: * derp [name/<tag>/<id>] date - URL
    • PRIVMSG: title | extra (title with compact engagement metrics when available)
  • Tags: yt, tw, sx, rd, ft, dg, gn, kk, dm, pt, bs, ly, od, ia, hn, gh, wp, se, gl, nm, pp, dh, ax, lb, dv, md, hf -- <id> is a short deterministic ID for use with !alert info
  • Each platform maintains its own seen list (capped at 200 per platform)
  • Per-backend error tracking with exponential backoff (5+ errors skips that backend with increasing cooldown; other backends unaffected)
  • Multi-instance backends (PeerTube, Mastodon, Lemmy, SearXNG) fetch concurrently for faster polling
  • Subscriptions persist across bot restarts via bot.state
  • Matched results are stored in data/alert_history.db (SQLite)
  • list shows per-backend error counts next to each alert
  • check forces an immediate poll across all platforms
  • history queries stored results (titles truncated), most recent first

!jwt -- JWT Decoder

Decode JSON Web Token header and payload, flag common issues.

!jwt eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.sig

Output format:

Header: alg=RS256 typ=JWT | sig=43 bytes
sub=user123
WARN: expired (2026-03-01 12:00 UTC)

Issues detected:

  • alg=none (unsigned token)
  • Expired tokens (exp in the past)
  • Not-yet-valid tokens (nbf in the future)

No external dependencies -- pure base64/JSON decoding.

!mac -- MAC Address Lookup

OUI vendor lookup from IEEE database, random MAC generation.

!mac AA:BB:CC:DD:EE:FF    Vendor lookup
!mac AABB.CCDD.EEFF       Cisco-style format also accepted
!mac random                Generate random locally-administered MAC
!mac update                Download IEEE OUI database

Output format:

AA:BB:CC:DD:EE:FF -- Cisco Systems, Inc (OUI: AA:BB:CC)
Random MAC: 02:4A:F7:3C:91:E2 (locally administered)
  • Accepts any common MAC format (colon, dash, dot, no separator)
  • Random MACs have the locally-administered bit set and multicast bit cleared
  • OUI database stored at data/oui.txt, also downloadable via scripts/update-data.sh

!abuse -- AbuseIPDB

Check IP reputation or report abuse via the AbuseIPDB API.

!abuse 8.8.8.8                              Check single IP
!abuse 8.8.8.8 1.1.1.1                      Check multiple (max 5)
!abuse 8.8.8.8 report 14,22 Brute force     Report IP (admin)

Output format:

8.8.8.8 -- Abuse: 0% (0 reports) | ISP: Google LLC | Usage: Data Center | Country: US
  • API key: set ABUSEIPDB_API_KEY env var or api_key under [abuseipdb] in config
  • Private/loopback IPs are rejected
  • Reporting requires admin privileges
  • Categories are comma-separated numbers per AbuseIPDB docs

!vt -- VirusTotal

Query VirusTotal API v3 for file hashes, IPs, domains, or URLs.

!vt 44d88612fea8a8f36de82e12...    File hash (MD5/SHA1/SHA256)
!vt 8.8.8.8                        IP address
!vt example.com                    Domain
!vt https://example.com/page       URL

Output format:

44d88612fea8a8... -- 62/72 detected | trojan, malware | first seen: 2024-01-15
8.8.8.8 -- 0/94 | AS15169 GOOGLE | Country: US | Reputation: 0
example.com -- 0/94 | Registrar: Example Inc | Reputation: 0
  • API key: set VIRUSTOTAL_API_KEY env var or api_key under [virustotal] in config
  • Auto-detects input type from format (hash length, URL scheme, IP, domain)
  • Rate limited to 4 requests per minute (VT free tier)
  • URL IDs are base64url-encoded per VT API spec

!emailcheck -- SMTP Email Verification (admin)

Verify email deliverability via MX resolution and raw SMTP RCPT TO conversation through the SOCKS5 proxy.

!emailcheck user@example.com                    Single check
!emailcheck user@example.com user2@test.org     Batch (max 5)

Output format:

user@example.com -- SMTP 250 OK (mx: mail.example.com)
bad@example.com -- SMTP 550 User unknown (mx: mail.example.com)
  • Admin only (prevents enumeration abuse)
  • Resolves MX records via Tor DNS, falls back to A record
  • Raw SMTP via SOCKS5 proxy: EHLO, MAIL FROM:<>, RCPT TO, QUIT
  • 15-second timeout per connection
  • Max 5 emails per invocation

!shorten -- Shorten URL

Shorten a URL via FlaskPaste's URL shortener.

!shorten https://very-long-url.example.com/path/to/resource

Output format:

https://paste.mymx.me/s/AbCdEfGh
  • URL must start with http:// or https://
  • mTLS client cert skips PoW; falls back to PoW challenge if no cert
  • Also used internally by !alert to shorten announcement URLs

!paste -- Create Paste

Create a text paste via FlaskPaste.

!paste some text or data to paste

Output format:

https://paste.mymx.me/AbCdEfGh
  • Pastes arbitrary text content
  • mTLS client cert skips PoW; falls back to PoW challenge if no cert

!pastemoni -- Paste Site Keyword Monitor

Monitor public paste sites for keywords (data leaks, credential dumps, brand mentions). Polls Pastebin's archive and GitHub's public Gists API on a schedule, checks new pastes for keyword matches, and announces hits to the subscribed IRC channel.

!pastemoni add <name> <keyword>     Add monitor (admin)
!pastemoni del <name>               Remove monitor (admin)
!pastemoni list                     List monitors
!pastemoni check <name>             Force-poll now
  • add and del require admin privileges
  • All subcommands must be used in a channel (not PM)
  • Names must be lowercase alphanumeric + hyphens, 1-20 characters
  • Maximum 20 monitors per channel

Backends:

  • Pastebin (pb) -- Scrapes pastebin.com/archive for recent pastes, fetches raw content, case-insensitive keyword match against title + content
  • GitHub Gists (gh) -- Queries api.github.com/gists/public, matches keyword against description and filenames

Polling and announcements:

  • Monitors are polled every 5 minutes by default
  • On add, existing items are seeded in the background (no flood)
  • New matches announced as [tag] Title -- snippet -- URL
  • Maximum 5 items announced per backend per poll; excess shown as ... and N more
  • Titles truncated to 60 characters, snippets to 80 characters
  • 5 consecutive all-backend failures doubles the poll interval (max 1 hour)
  • Subscriptions persist across bot restarts via bot.state
  • list shows keyword and per-backend error counts
  • check forces an immediate poll across all backends

!internetdb -- Shodan InternetDB

Look up host information from Shodan's free InternetDB API. Returns open ports, reverse hostnames, CPE software fingerprints, tags, and known CVEs. No API key required.

!internetdb 8.8.8.8

Output format:

8.8.8.8 -- dns.google | Ports: 53, 443 | CPEs: cpe:/a:isc:bind | Tags: cloud
  • Single IP per query (IPv4 or IPv6)
  • Private/loopback addresses are rejected
  • Hostnames truncated to first 5; CVEs truncated to first 10 (with +N more)
  • CPEs truncated to first 8
  • All requests routed through SOCKS5 proxy
  • Returns "no data available" for IPs not in the InternetDB index

!canary -- Canary Token Generator

Generate realistic-looking credentials for planting as canary tokens (tripwires for detecting unauthorized access). Tokens are persisted per-channel.

!canary gen db-cred                Generate default token (40-char hex)
!canary gen aws staging-key        AWS-style keypair
!canary gen basic svc-login        Username:password pair
!canary list                       List canaries in channel
!canary info db-cred               Show full token details
!canary del db-cred                Delete a canary (admin)

Token types:

Type Format Example
token 40-char hex (API key / SHA1) a3f8b2c1d4e5...
aws AKIA access key + base64 secret AKIA7X9M2PVL5N...
basic user:pass pair svcadmin:xK9mP2vL5nR8wQ3z
  • gen and del require admin privileges
  • All subcommands must be used in a channel (not PM)
  • Labels: 1-32 chars, alphanumeric + hyphens + underscores
  • Maximum 50 canaries per channel
  • Persisted via bot.state (survives restarts)

!tcping -- TCP Connect Latency Probe

Measure TCP connect latency to a host:port through the SOCKS5 proxy. Sequential probes with min/avg/max summary.

!tcping example.com                 Port 443, 3 probes
!tcping example.com 22              Port 22, 3 probes
!tcping example.com 80 5            Port 80, 5 probes

Output format:

tcping example.com:443 -- 3 probes  1: 45ms  2: 43ms  3: 47ms  min/avg/max: 43/45/47 ms
  • Default port: 443, default count: 3
  • Max count: 10, timeout: 10s per probe
  • Private/reserved addresses rejected
  • Routed through SOCKS5 proxy

!archive -- Wayback Machine Save

Save a URL to the Wayback Machine via the Save Page Now API.

!archive https://example.com/page

Output format:

Archiving https://example.com/page...
Archived: https://web.archive.org/web/20260220.../https://example.com/page
  • URL must start with http:// or https://
  • Timeout: 30s (archiving can be slow)
  • Handles 429 rate limit, 523 origin unreachable
  • Sends acknowledgment before archiving
  • Routed through SOCKS5 proxy

!resolve -- Bulk DNS Resolution

Resolve multiple hosts via TCP DNS through the SOCKS5 proxy. Concurrent resolution with compact output.

!resolve example.com github.com       A records (default)
!resolve example.com AAAA             Specific record type
!resolve 1.2.3.4 8.8.8.8             Auto PTR for IPs

Output format:

example.com -> 93.184.216.34
github.com -> 140.82.121.3
badhost.invalid -> NXDOMAIN
  • Max 10 hosts per invocation
  • Default type: A (auto-detect IP -> PTR)
  • DNS server: 1.1.1.1 (Cloudflare)
  • Concurrent via asyncio.gather()
  • Valid types: A, NS, CNAME, SOA, PTR, MX, TXT, AAAA

!cron -- Scheduled Command Execution

Schedule bot commands to repeat on a timer. Admins only.

!cron add <interval> <#channel> <command...>   Schedule a command
!cron del <id>                                 Remove a job
!cron list                                     List jobs in channel

Examples:

!cron add 1h #ops !rss check news      Poll RSS feed every hour
!cron add 2d #alerts !tor update       Update Tor list every 2 days
!cron del abc123                       Remove job by ID
!cron list                             Show jobs in current channel

Output format:

Cron #a1b2c3: !rss check news every 1h in #ops
#a1b2c3 every 1h: !rss check news
  • add and del require admin privileges
  • add and list must be used in a channel (not PM)
  • Interval formats: 5m, 1h30m, 2d, 90s, or raw seconds
  • Minimum interval: 1 minute
  • Maximum interval: 7 days
  • Maximum 20 jobs per channel
  • Jobs persist across bot restarts via bot.state
  • Dispatched commands run with the original creator's identity
  • The scheduled command goes through normal command routing and permissions

FlaskPaste Configuration

[flaskpaste]
url = "https://paste.mymx.me"    # or set FLASKPASTE_URL env var

Auth: place client cert/key at secrets/flaskpaste/derp.crt and derp.key for mTLS (bypasses PoW). Without them, PoW challenges are solved per request.

URL Title Preview (urltitle)

Automatic URL title preview for channel messages. When a user posts a URL, the bot fetches the page title and description and displays a one-line preview. No commands -- event-driven only.

<alice> check out https://example.com/article
<derp> ↳ Article Title -- Description of the article...

Behavior:

  • Automatically previews HTTP(S) URLs posted in channel messages
  • Skips private messages, bot's own messages, and command messages (!prefix)
  • URLs prefixed with ! are suppressed: !https://example.com produces no preview
  • HEAD-then-GET fetch strategy (checks Content-Type before downloading body)
  • Skips non-HTML content types (images, PDFs, JSON, etc.)
  • Skips binary file extensions (.png, .jpg, .pdf, .zip, etc.)
  • Skips FlaskPaste URLs and configured ignore hosts
  • Dedup: same URL only previewed once per cooldown window (5 min default)
  • Max 3 URLs previewed per message (configurable)
  • Title from og:title takes priority over <title> tag
  • Description from og:description takes priority over <meta name="description">
  • Title truncated at 200 chars, description at 150 chars

Output format:

↳ Page Title -- Description truncated to 150 chars...
↳ Page Title

Configuration (optional):

[urltitle]
cooldown = 300        # seconds before same URL previewed again
timeout = 10          # HTTP fetch timeout
max_urls = 3          # max URLs to preview per message
ignore_hosts = []     # additional hostnames to skip

Teams Integration

Connect derp to Microsoft Teams via outgoing webhooks. The bot runs an HTTP server that receives messages from Teams and replies inline. No Microsoft SDK required -- raw asyncio HTTP, same pattern as the webhook plugin.

How It Works

  1. Outgoing webhook (Teams -> bot): Teams POSTs an Activity JSON to the bot's HTTP endpoint when a user @mentions the bot. The bot dispatches the command through the shared plugin registry and returns the reply as the HTTP response body.

  2. Incoming webhook (bot -> Teams, optional): For proactive messages (alerts, subscriptions), the bot POSTs to a Teams incoming webhook URL.

Configuration

[teams]
enabled = true
proxy = true                        # Route outbound HTTP through SOCKS5
bot_name = "derp"                   # outgoing webhook display name
bind = "127.0.0.1"                  # HTTP listen address
port = 8081                         # HTTP listen port
webhook_secret = ""                 # HMAC-SHA256 secret from Teams
incoming_webhook_url = ""           # for proactive messages (optional)
admins = []                         # AAD object IDs (UUID format)
operators = []                      # AAD object IDs
trusted = []                        # AAD object IDs

Teams Setup

  1. Create an outgoing webhook in a Teams channel:

    • Channel settings -> Connectors -> Outgoing Webhook
    • Set the callback URL to your bot's endpoint (e.g. https://derp.example.com/api/messages)
    • Copy the HMAC secret and set webhook_secret in config
  2. Expose the bot via Cloudflare Tunnel or reverse proxy:

    cloudflared tunnel --url http://127.0.0.1:8081
    
  3. Configure permissions using AAD object IDs from the Activity JSON. The AAD object ID is sent in from.aadObjectId on every message. Use !whoami to discover your ID.

Permission Tiers

Same 4-tier model as IRC, but matches exact AAD object IDs instead of fnmatch hostmask patterns:

[teams]
admins = ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"]
operators = ["yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"]
trusted = ["zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"]

Plugin Compatibility

~90% of plugins work on Teams without modification -- any plugin that uses only bot.send(), bot.reply(), bot.state, message.text, .nick, and .target.

Feature IRC Teams
bot.reply() Sends PRIVMSG Appends to HTTP response
bot.send() Sends PRIVMSG POSTs to incoming webhook
bot.action() CTCP ACTION Italic text via incoming webhook
bot.long_reply() Paste overflow Paste overflow (same logic)
bot.state Per-server SQLite Per-server SQLite
bot.join/part/kick/mode IRC commands No-op (logged at debug)
Event handlers (JOIN, etc.) Fired on IRC events Not triggered
Hostmask ACL fnmatch patterns Exact AAD object IDs
Passive monitoring All channel messages @mention only

HMAC Verification

Teams outgoing webhooks sign requests with HMAC-SHA256. The secret is base64-encoded when you create the webhook. The Authorization header format is HMAC <base64(hmac-sha256(b64decode(secret), body))>.

If webhook_secret is empty, no authentication is performed (useful for development but not recommended for production).

Endpoint

Single endpoint: POST /api/messages

The bot returns a JSON response:

{"type": "message", "text": "reply text here"}

Multiple reply lines are joined with \n.

Telegram Integration

Connect derp to Telegram via long-polling (getUpdates). All outbound HTTP is routed through the SOCKS5 proxy. No public endpoint required, no Telegram SDK dependency.

How It Works

The bot calls getUpdates in a loop with a long-poll timeout (default 30s). When a message arrives with the configured prefix, it is dispatched through the shared plugin registry. Replies are sent immediately via sendMessage.

Configuration

[telegram]
enabled = true
proxy = true                        # Route HTTP through SOCKS5
bot_token = "123456:ABC-DEF..."    # from @BotFather
poll_timeout = 30                   # long-poll timeout in seconds
admins = [123456789]                # Telegram user IDs (numeric)
operators = []                      # Telegram user IDs
trusted = []                        # Telegram user IDs

Telegram Setup

  1. Create a bot via @BotFather:

    • /newbot and follow the prompts
    • Copy the bot token and set bot_token in config
  2. Add the bot to a group or send it a DM

  3. Configure permissions using Telegram user IDs. Use !whoami to discover your numeric user ID.

Permission Tiers

Same 4-tier model as IRC, but matches exact Telegram user IDs (numeric strings) instead of fnmatch hostmask patterns:

[telegram]
admins = [123456789]
operators = [987654321]
trusted = [111222333]

Plugin Compatibility

Same compatibility as Teams -- ~90% of plugins work without modification.

Feature IRC Telegram
bot.reply() Sends PRIVMSG sendMessage API call
bot.send() Sends PRIVMSG sendMessage API call
bot.action() CTCP ACTION Italic Markdown text
bot.long_reply() Paste overflow Paste overflow (same logic)
bot.state Per-server SQLite Per-server SQLite
bot.join/part/kick/mode IRC commands No-op (logged at debug)
Event handlers (JOIN, etc.) Fired on IRC events Not triggered
Hostmask ACL fnmatch patterns Exact user IDs
Message limit 512 bytes (IRC) 4096 chars (Telegram)

Group Commands

In groups, Telegram appends @botusername to commands. The bot strips this automatically: !help@mybot becomes !help.

Transport

All HTTP traffic (API calls, long-polling) routes through the SOCKS5 proxy at 127.0.0.1:1080 via derp.http.urlopen when proxy = true (default). Set proxy = false to connect directly.

Mumble Integration

Connect derp to a Mumble server with text chat and voice playback. Uses 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

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

[mumble]
enabled = true
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)
admins = ["admin_user"]             # Mumble usernames
operators = []                      # Mumble usernames
trusted = []                        # Mumble usernames

Mumble Setup

  1. Ensure a Mumble server (Murmur/Mumble-server) is running

  2. Configure the bot with the server hostname, port, and credentials

  3. Configure permissions using Mumble registered usernames. Use !whoami to discover your username as the bot sees it.

Permission Tiers

Same 4-tier model as IRC, but matches exact Mumble usernames instead of fnmatch hostmask patterns:

[mumble]
admins = ["admin_user"]
operators = ["oper_user"]
trusted = ["trusted_user"]

Plugin Compatibility

Same compatibility as Teams/Telegram -- ~90% of plugins work without modification.

Feature IRC Mumble
bot.reply() Sends PRIVMSG TextMessage to channel
bot.send() Sends PRIVMSG TextMessage to channel
bot.action() CTCP ACTION Italic HTML text (<i>...</i>)
bot.long_reply() Paste overflow Paste overflow (same logic)
bot.state Per-server SQLite Per-server SQLite
bot.join/part/kick/mode IRC commands No-op (logged at debug)
Event handlers (JOIN, etc.) Fired on IRC events Not triggered
Hostmask ACL fnmatch patterns Exact usernames

Text Encoding

Mumble uses HTML for text messages. On receive, the bot strips tags and unescapes entities. On send, text is HTML-escaped. Action messages use <i> tags for italic formatting.

Music Playback

Stream audio from YouTube, SoundCloud, and other yt-dlp-supported sites 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 (container image includes these):

  • yt-dlp -- audio stream extraction
  • ffmpeg -- decode to 48kHz mono s16le PCM
  • libopus -- Opus codec (used by pymumble/opuslib)
!play <url|playlist> Play audio or add to queue (playlists expanded)
!play <query>        Search YouTube, play a random result
!stop                Stop playback, clear queue (fade-out)
!skip                Skip current track (fade-out)
!prev                Go back to the previous track (fade-out)
!seek <offset>       Seek to position (1:30, 90, +30, -30)
!resume              Resume last stopped/skipped track from saved position
!queue               Show queue
!queue <url>         Add to queue (alias for !play)
!np                  Now playing
!volume [0-100]      Get/set volume (persisted across restarts)
!keep                Keep current track's audio file (with metadata)
!kept [clear]        List kept files with metadata, or clear all
!testtone            Play 3-second 440Hz test tone
  • Queue holds up to 50 tracks
  • Non-URL input is treated as a YouTube search; 10 results are fetched and one is picked randomly
  • Playlists are expanded into individual tracks; excess tracks are truncated at the queue limit
  • !skip, !stop, !prev, and !seek fade out smoothly (~0.8s) before switching tracks; volume changes ramp smoothly over ~1s (no abrupt jumps)
  • Default volume: 50%; persisted via bot.state across restarts
  • Titles resolved via yt-dlp --flat-playlist before playback
  • Audio is downloaded before playback (data/music/); files are deleted after playback unless !keep is used. Falls back to streaming on download failure.
  • Audio pipeline: ffmpeg subprocess for local files, yt-dlp | ffmpeg for streaming fallback, 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
  • !prev returns to the last-played track; up to 10 tracks are kept in a per-session history stack (populated on skip and natural track completion)
  • !resume continues from where playback was interrupted (!stop/!skip); position is persisted via bot.state and survives bot restarts

Auto-Resume on Reconnect

If the bot disconnects while music is playing (network hiccup, server restart), it saves the current track and position. On reconnect, it automatically resumes playback -- but only after the channel is silent (using the same silence threshold as voice ducking, default 15s).

  • Resume state is saved on both explicit stop/skip and on stream errors (disconnect)
  • Works across container restarts (cold boot) and network reconnections
  • The bot waits up to 60s for silence; if the channel stays active, it aborts and the saved state remains for manual !resume
  • Chat messages announce resume intentions and abort reasons
  • The reconnect watcher starts via the on_connected plugin lifecycle hook

Seeking

Fast-forward or rewind within the currently playing track.

!seek 1:30       Seek to 1 minute 30 seconds
!seek 90         Seek to 90 seconds
!seek +30        Jump forward 30 seconds
!seek -30        Jump backward 30 seconds
!seek +1:00      Jump forward 1 minute
  • Absolute offsets (1:30, 90) seek to that position from the start
  • Relative offsets (+30, -1:00) jump from the current position
  • Negative seeks are clamped to the start of the track
  • Seeking restarts the audio pipeline at the new position

Disconnect-Resilient Streaming

During brief network disconnects (~5-15s), the audio stream stays alive. The ffmpeg pipeline keeps running; PCM frames are read at real-time pace but dropped while pymumble reconnects. Once the connection re-establishes and the codec is negotiated, audio feeding resumes automatically. The listener hears a brief silence instead of a 30+ second restart with URL re-resolution.

  • The _is_audio_ready() guard checks: mumble connected, sound_output exists, Opus encoder initialized
  • Frames are counted even during disconnect, so position tracking remains accurate
  • State transitions (connected/disconnected) are logged for diagnostics

Voice Ducking

When other users speak in the Mumble channel, the music volume automatically ducks (lowers) to a configurable floor. After a configurable silence period, volume gradually restores to the user-set level in small steps.

!duck                  Show ducking status and settings
!duck on               Enable voice ducking
!duck off              Disable voice ducking
!duck floor <0-100>    Set floor volume % (default: 1)
!duck silence <sec>    Set silence timeout in seconds (default: 15)
!duck restore <sec>    Set restore ramp duration in seconds (default: 30)

Behavior:

  • Enabled by default; voice is detected via pymumble's sound callback
  • When someone speaks, volume drops immediately to the floor value
  • After silence seconds of no voice, volume restores via a single smooth linear ramp over restore seconds (default 30s)
  • The per-frame volume ramp in stream_audio further smooths the transition, eliminating audible steps
  • Ducking resets when playback stops, skips, or the queue empties

Configuration (optional):

[music]
duck_enabled = true       # Enable voice ducking (default: true)
duck_floor = 1            # Floor volume % during ducking (default: 1)
duck_silence = 15         # Seconds of silence before restoring (default: 15)
duck_restore = 30         # Seconds for smooth volume restore (default: 30)

Download-First Playback

Audio is downloaded to data/music/ before playback begins. This eliminates CDN hiccups mid-stream and enables instant seeking. Files are identified by a hash of the URL so the same URL reuses the same file (natural dedup).

  • If download fails, playback falls back to streaming (yt-dlp | ffmpeg)
  • After a track finishes, the local file is automatically deleted
  • Use !keep during playback to preserve the file; metadata (title, artist, duration) is fetched via yt-dlp and stored in bot.state
  • Use !kept to list preserved files with metadata (title, artist, duration, file size)
  • Use !kept clear to delete all preserved files and their metadata
  • On cancel/error, files are not deleted (needed for !resume)

Extra Mumble Bots

Run additional bot identities on the same Mumble server. Each extra bot inherits the main [mumble] connection settings and overrides only what differs (username, certificates, greeting). Extra bots share the plugin registry but get their own state DB and do not run the voice trigger by default (prevents double-processing).

[[mumble.extra]]
username = "merlin"
certfile = "secrets/mumble/merlin.crt"
keyfile = "secrets/mumble/merlin.key"
greet = "The sorcerer has arrived."
  • username, certfile, keyfile -- identity overrides
  • greet -- TTS message spoken on first connect (optional)
  • All other [mumble] keys (host, port, password, admins, etc.) are inherited
  • Voice trigger is disabled unless the extra entry includes a voice key

Voice STT/TTS

Transcribe voice from Mumble users via Whisper STT and speak text aloud via Piper TTS. Requires local Whisper and Piper services.

!listen [on|off]     Toggle voice-to-text transcription (admin)
!listen              Show current listen status
!say <text>          Speak text aloud via TTS (max 500 chars)

STT behavior:

  • When enabled, the bot buffers incoming voice PCM per user
  • After a configurable silence gap (default 1.5s), the buffer is transcribed via Whisper and posted as an action message
  • Utterances shorter than 0.5s are discarded (noise filter)
  • Utterances are capped at 30s to bound memory and latency
  • Transcription results are posted as: * derp heard Alice say: hello
  • The listener survives reconnects when !listen is on

TTS behavior:

  • !say fetches WAV from Piper and plays it via stream_audio()
  • Piper outputs 22050Hz WAV; ffmpeg resamples to 48kHz automatically
  • TTS shares the audio output with music playback
  • Text is limited to 500 characters
  • Set greet in [mumble] or [[mumble.extra]] for automatic TTS on first connect

Always-On Trigger Mode

Set a trigger word to enable always-on voice listening. The bot continuously transcribes voice and watches for the trigger word. When detected, the text after the trigger is spoken back via TTS. No !listen command needed.

[voice]
trigger = "claude"

Behavior:

  • Listener starts automatically on connect (no !listen on required)
  • All speech is transcribed and checked for the trigger word
  • Trigger match is case-insensitive: "Claude", "CLAUDE", "claude" all work
  • On match, the trigger word is stripped and the remainder is sent to TTS
  • Non-triggered speech is silently discarded (unless !listen is also on)
  • When both trigger and !listen are active, triggered speech goes to TTS and all other speech is posted as the usual "heard X say: ..."
  • !listen status shows trigger configuration when set

Configuration (optional):

[voice]
whisper_url = "http://192.168.129.9:8080/inference"
piper_url = "http://192.168.129.9:5100/"
silence_gap = 1.5
trigger = ""

Piper TTS accepts POST with JSON body {"text": "..."} and returns 22050 Hz 16-bit PCM mono WAV. Default voice: en_US-lessac-medium.

Available voices:

Region Voices
en_US lessac-medium/high, amy-medium, ryan-medium/high, joe-medium, john-medium, kristin-medium, danny-low, ljspeech-high
en_GB alba-medium, cori-medium/high, jenny_dioco-medium, northern_english_male-medium, southern_english_female-low
fr_FR siwis-low/medium, gilles-low, tom-medium, mls-medium, mls_1840-low, upmc-medium

To switch the active voice, set piper_tts_voice (e.g. fr_FR-siwis-medium) and redeploy the TTS service.