1759 lines
61 KiB
Markdown
1759 lines
61 KiB
Markdown
# Usage Guide
|
|
|
|
## Running
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```toml
|
|
[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`).
|
|
|
|
```toml
|
|
[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 |
|
|
| `!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](https://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.
|
|
|
|
```toml
|
|
[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.
|
|
|
|
```toml
|
|
[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 |
|
|
|
|
```toml
|
|
[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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```toml
|
|
[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`:
|
|
|
|
```python
|
|
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
|
|
|
|
```bash
|
|
./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
|
|
|
|
```toml
|
|
[webhook]
|
|
enabled = true
|
|
host = "127.0.0.1"
|
|
port = 8080
|
|
secret = "your-shared-secret"
|
|
```
|
|
|
|
### HTTP API
|
|
|
|
Single endpoint: `POST /`
|
|
|
|
**Request body** (JSON):
|
|
|
|
```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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```python
|
|
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
|
|
|
|
### `!searx` -- SearXNG Web Search
|
|
|
|
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
|
|
```
|
|
|
|
### `!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
|
|
|
|
```toml
|
|
[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):
|
|
|
|
```toml
|
|
[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
|
|
|
|
```toml
|
|
[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:
|
|
```bash
|
|
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:
|
|
|
|
```toml
|
|
[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:
|
|
|
|
```json
|
|
{"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
|
|
|
|
```toml
|
|
[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](https://t.me/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:
|
|
|
|
```toml
|
|
[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](https://github.com/azlux/pymumble) for the Mumble
|
|
protocol (connection, SSL, voice encoding). Text commands are bridged
|
|
from pymumble's thread callbacks to asyncio for plugin dispatch.
|
|
|
|
### How It Works
|
|
|
|
pymumble handles the Mumble protocol: TLS connection, ping keepalives,
|
|
channel/user tracking, and Opus voice encoding. The bot registers
|
|
callbacks for text messages and connection events, then bridges them
|
|
to asyncio via `run_coroutine_threadsafe()`. Voice playback feeds raw
|
|
PCM to `sound_output.add_sound()` -- pymumble handles Opus encoding,
|
|
packetization, and timing.
|
|
|
|
### Configuration
|
|
|
|
```toml
|
|
[mumble]
|
|
enabled = true
|
|
proxy = 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:
|
|
|
|
```toml
|
|
[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
|
|
!skip Skip current track
|
|
!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 after playback
|
|
!kept [clear] List kept files 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
|
|
- 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
|
|
- `!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):
|
|
|
|
```toml
|
|
[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
|
|
- Use `!kept` to list preserved files and their sizes
|
|
- Use `!kept clear` to delete all preserved files
|
|
- On cancel/error, files are not deleted (needed for `!resume`)
|
|
|
|
### 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
|
|
|
|
### 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.
|
|
|
|
```toml
|
|
[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):
|
|
|
|
```toml
|
|
[voice]
|
|
whisper_url = "http://192.168.122.1:8080/inference"
|
|
piper_url = "http://192.168.122.1:5000/"
|
|
silence_gap = 1.5
|
|
trigger = ""
|
|
```
|