diff --git a/ROADMAP.md b/ROADMAP.md index 709ce78..90388ef 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -114,8 +114,8 @@ - [ ] Stable plugin API (versioned, breaking change policy) - [x] Paste overflow (auto-paste long output to FlaskPaste, return link) - [x] URL shortener integration (shorten URLs in subscription announcements) -- [ ] Webhook listener (HTTP endpoint for push events to channels) -- [ ] Granular ACLs (per-command permission tiers: trusted, operator, admin) +- [x] Webhook listener (HTTP endpoint for push events to channels) +- [x] Granular ACLs (per-command permission tiers: trusted, operator, admin) - [x] `paste` command (manual paste to FlaskPaste) - [x] `shorten` command (manual URL shortening) - [x] `emailcheck` plugin (SMTP VRFY/RCPT TO) diff --git a/TASKS.md b/TASKS.md index ad67f0c..916572e 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1,6 +1,18 @@ # derp - Tasks -## Current Sprint -- v2.0.0 Tier 2 (2026-02-21) +## Current Sprint -- v2.0.0 ACL + Webhook (2026-02-21) + +| Pri | Status | Task | +|-----|--------|------| +| P0 | [x] | Granular ACL tiers in `src/derp/plugin.py` (TIERS, Handler.tier, decorator) | +| P0 | [x] | ACL dispatch in `src/derp/bot.py` (_get_tier, _operators, _trusted) | +| P0 | [x] | Config defaults: operators, trusted, webhook section | +| P0 | [x] | `plugins/core.py` -- whoami/admins tier display | +| P0 | [x] | `plugins/webhook.py` -- HTTP webhook listener (HMAC, JSON, POST) | +| P1 | [x] | Tests: `test_acl.py` (32 cases), `test_webhook.py` (22 cases) | +| P2 | [x] | Documentation update (USAGE.md, CHEATSHEET.md, ROADMAP.md, TODO.md) | + +## Previous Sprint -- v2.0.0 Tier 2 (2026-02-21) | Pri | Status | Task | |-----|--------|------| diff --git a/TODO.md b/TODO.md index c6da21c..5d5d8da 100644 --- a/TODO.md +++ b/TODO.md @@ -6,8 +6,8 @@ - [ ] Stable plugin API (versioned, breaking change policy) - [x] Paste overflow (auto-paste long output to FlaskPaste) - [x] URL shortener integration (shorten URLs in subscription announcements) -- [ ] Webhook listener (HTTP endpoint for push events to channels) -- [ ] Granular ACLs (per-command: trusted, operator, admin) +- [x] Webhook listener (HTTP endpoint for push events to channels) +- [x] Granular ACLs (per-command: trusted, operator, admin) ## LLM Bridge diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 7bf7c8a..e1b0549 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -75,21 +75,27 @@ code changes -- restart the container or use `!reload` for plugins. !h # Shorthand (any unambiguous prefix works) ``` -## Admin +## Permission Tiers ``` -!whoami # Show your hostmask + admin status -!admins # Show admin patterns + detected opers (admin) +user < trusted < oper < admin ``` ```toml # config/derp.toml [bot] -admins = ["*!~user@trusted.host", "ops!*@*.ops.net"] +admins = ["*!~root@*.ops.net"] # admin tier +operators = ["*!~staff@trusted.host"] # oper tier +trusted = ["*!~user@known.host"] # trusted tier ``` -IRC operators are auto-detected via WHO on connect and on user JOIN -(debounced 2s to handle netsplit floods). Hostmask patterns use fnmatch. +``` +!whoami # Show your hostmask + permission tier +!admins # Show configured tiers + detected opers (admin) +``` + +IRC operators are auto-detected via WHO (admin tier). Hostmask patterns +use fnmatch. `admin=True` on commands still works (maps to tier="admin"). ## Channel Management (admin) @@ -449,6 +455,33 @@ Shows top 3 results as `Title -- URL`. Channel only. Max query length: 200 chars Intervals: `5m`, `1h30m`, `2d`, `90s`, or raw seconds. Min 1m, max 7d. Max 20 jobs/channel. Persists across restarts. Channel only. +## Webhook (admin) + +```toml +# config/derp.toml +[webhook] +enabled = true +host = "127.0.0.1" +port = 8080 +secret = "your-shared-secret" +``` + +```bash +# Send message to IRC channel via webhook +SECRET="your-shared-secret" +BODY='{"channel":"#ops","text":"Deploy done"}' +SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') +curl -X POST http://127.0.0.1:8080/ \ + -H "X-Signature: sha256=$SIG" -d "$BODY" +``` + +``` +!webhook # Show listener status (admin) +``` + +POST JSON: `{"channel":"#chan","text":"msg"}`. Optional `"action":true`. +Auth: HMAC-SHA256 via `X-Signature` header. Starts on IRC connect. + ## Plugin Template ```python diff --git a/docs/USAGE.md b/docs/USAGE.md index 085a528..725e72f 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -53,10 +53,18 @@ 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) ``` ## Built-in Commands @@ -142,6 +150,7 @@ format = "text" # Log format: "text" (default) or "json" | `!paste ` | Create a paste via FlaskPaste | | `!pastemoni ` | Paste site keyword monitoring | | `!cron ` | Scheduled command execution (admin) | +| `!webhook` | Show webhook listener status (admin) | ### Command Shorthand @@ -221,24 +230,31 @@ Each line contains: Default format is `"text"` (human-readable, same as before). -## Admin System +## Permission Tiers (ACL) -Commands marked as `admin` require elevated permissions. Admin access is -granted via: +The bot uses a 4-tier permission model. Each command has a required tier; +users must meet or exceed it. -1. **IRC operator status** -- detected automatically via `WHO` -2. **Hostmask patterns** -- configured in `[bot] admins`, fnmatch-style +``` +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 = [ - "*!~user@trusted.host", - "ops!*@*.ops.net", -] +admins = ["*!~root@*.ops.net"] +operators = ["*!~staff@trusted.host"] +trusted = ["*!~user@known.host"] ``` -Empty by default -- only IRC operators get admin access unless patterns -are configured. +All lists are empty by default -- only IRC operators get admin access +unless patterns are configured. Patterns use fnmatch-style matching. ### Oper Detection @@ -256,18 +272,24 @@ set automatically. | Command | Description | |---------|-------------| -| `!whoami` | Show your hostmask and admin status | -| `!admins` | Show configured patterns and detected opers (admin) | +| `!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`. +`!kick`, `!ban`, `!unban`, `!topic`, `!mode`, `!webhook`. -### Writing Admin Commands +### 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 @@ -405,6 +427,68 @@ restarting the bot. 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=` 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: