docs: update docs for ACL tiers and webhook
- USAGE.md: permission tiers section, webhook config/API/example - CHEATSHEET.md: ACL tiers and webhook quick-ref sections - ROADMAP.md: mark webhook and ACL items done - TODO.md: mark webhook and ACL items done - TASKS.md: new sprint for ACL + webhook work
This commit is contained in:
@@ -114,8 +114,8 @@
|
|||||||
- [ ] Stable plugin API (versioned, breaking change policy)
|
- [ ] Stable plugin API (versioned, breaking change policy)
|
||||||
- [x] Paste overflow (auto-paste long output to FlaskPaste, return link)
|
- [x] Paste overflow (auto-paste long output to FlaskPaste, return link)
|
||||||
- [x] URL shortener integration (shorten URLs in subscription announcements)
|
- [x] URL shortener integration (shorten URLs in subscription announcements)
|
||||||
- [ ] Webhook listener (HTTP endpoint for push events to channels)
|
- [x] Webhook listener (HTTP endpoint for push events to channels)
|
||||||
- [ ] Granular ACLs (per-command permission tiers: trusted, operator, admin)
|
- [x] Granular ACLs (per-command permission tiers: trusted, operator, admin)
|
||||||
- [x] `paste` command (manual paste to FlaskPaste)
|
- [x] `paste` command (manual paste to FlaskPaste)
|
||||||
- [x] `shorten` command (manual URL shortening)
|
- [x] `shorten` command (manual URL shortening)
|
||||||
- [x] `emailcheck` plugin (SMTP VRFY/RCPT TO)
|
- [x] `emailcheck` plugin (SMTP VRFY/RCPT TO)
|
||||||
|
|||||||
14
TASKS.md
14
TASKS.md
@@ -1,6 +1,18 @@
|
|||||||
# derp - Tasks
|
# 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 |
|
| Pri | Status | Task |
|
||||||
|-----|--------|------|
|
|-----|--------|------|
|
||||||
|
|||||||
4
TODO.md
4
TODO.md
@@ -6,8 +6,8 @@
|
|||||||
- [ ] Stable plugin API (versioned, breaking change policy)
|
- [ ] Stable plugin API (versioned, breaking change policy)
|
||||||
- [x] Paste overflow (auto-paste long output to FlaskPaste)
|
- [x] Paste overflow (auto-paste long output to FlaskPaste)
|
||||||
- [x] URL shortener integration (shorten URLs in subscription announcements)
|
- [x] URL shortener integration (shorten URLs in subscription announcements)
|
||||||
- [ ] Webhook listener (HTTP endpoint for push events to channels)
|
- [x] Webhook listener (HTTP endpoint for push events to channels)
|
||||||
- [ ] Granular ACLs (per-command: trusted, operator, admin)
|
- [x] Granular ACLs (per-command: trusted, operator, admin)
|
||||||
|
|
||||||
## LLM Bridge
|
## LLM Bridge
|
||||||
|
|
||||||
|
|||||||
@@ -75,21 +75,27 @@ code changes -- restart the container or use `!reload` for plugins.
|
|||||||
!h # Shorthand (any unambiguous prefix works)
|
!h # Shorthand (any unambiguous prefix works)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Admin
|
## Permission Tiers
|
||||||
|
|
||||||
```
|
```
|
||||||
!whoami # Show your hostmask + admin status
|
user < trusted < oper < admin
|
||||||
!admins # Show admin patterns + detected opers (admin)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# config/derp.toml
|
# config/derp.toml
|
||||||
[bot]
|
[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)
|
## 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.
|
Intervals: `5m`, `1h30m`, `2d`, `90s`, or raw seconds. Min 1m, max 7d.
|
||||||
Max 20 jobs/channel. Persists across restarts. Channel only.
|
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
|
## Plugin Template
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
114
docs/USAGE.md
114
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)
|
paste_threshold = 4 # Max lines before overflow to FlaskPaste (default: 4)
|
||||||
admins = [] # Hostmask patterns (fnmatch), IRCOPs auto-detected
|
admins = [] # Hostmask patterns (fnmatch), IRCOPs auto-detected
|
||||||
timezone = "UTC" # Timezone for calendar reminders (IANA tz name)
|
timezone = "UTC" # Timezone for calendar reminders (IANA tz name)
|
||||||
|
operators = [] # Hostmask patterns for "oper" tier (fnmatch)
|
||||||
|
trusted = [] # Hostmask patterns for "trusted" tier (fnmatch)
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
level = "info" # Logging level: debug, info, warning, error
|
level = "info" # Logging level: debug, info, warning, error
|
||||||
format = "text" # Log format: "text" (default) or "json"
|
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
|
## Built-in Commands
|
||||||
@@ -142,6 +150,7 @@ format = "text" # Log format: "text" (default) or "json"
|
|||||||
| `!paste <text>` | Create a paste via FlaskPaste |
|
| `!paste <text>` | Create a paste via FlaskPaste |
|
||||||
| `!pastemoni <add\|del\|list\|check>` | Paste site keyword monitoring |
|
| `!pastemoni <add\|del\|list\|check>` | Paste site keyword monitoring |
|
||||||
| `!cron <add\|del\|list>` | Scheduled command execution (admin) |
|
| `!cron <add\|del\|list>` | Scheduled command execution (admin) |
|
||||||
|
| `!webhook` | Show webhook listener status (admin) |
|
||||||
|
|
||||||
### Command Shorthand
|
### Command Shorthand
|
||||||
|
|
||||||
@@ -221,24 +230,31 @@ Each line contains:
|
|||||||
|
|
||||||
Default format is `"text"` (human-readable, same as before).
|
Default format is `"text"` (human-readable, same as before).
|
||||||
|
|
||||||
## Admin System
|
## Permission Tiers (ACL)
|
||||||
|
|
||||||
Commands marked as `admin` require elevated permissions. Admin access is
|
The bot uses a 4-tier permission model. Each command has a required tier;
|
||||||
granted via:
|
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
|
```toml
|
||||||
[bot]
|
[bot]
|
||||||
admins = [
|
admins = ["*!~root@*.ops.net"]
|
||||||
"*!~user@trusted.host",
|
operators = ["*!~staff@trusted.host"]
|
||||||
"ops!*@*.ops.net",
|
trusted = ["*!~user@known.host"]
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Empty by default -- only IRC operators get admin access unless patterns
|
All lists are empty by default -- only IRC operators get admin access
|
||||||
are configured.
|
unless patterns are configured. Patterns use fnmatch-style matching.
|
||||||
|
|
||||||
### Oper Detection
|
### Oper Detection
|
||||||
|
|
||||||
@@ -256,18 +272,24 @@ set automatically.
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `!whoami` | Show your hostmask and admin status |
|
| `!whoami` | Show your hostmask and permission tier |
|
||||||
| `!admins` | Show configured patterns and detected opers (admin) |
|
| `!admins` | Show configured tiers and detected opers (admin) |
|
||||||
|
|
||||||
Admin-restricted commands: `!load`, `!reload`, `!unload`, `!admins`, `!state`,
|
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
|
```python
|
||||||
|
# admin=True still works (maps to tier="admin")
|
||||||
@command("dangerous", help="Admin-only action", admin=True)
|
@command("dangerous", help="Admin-only action", admin=True)
|
||||||
async def cmd_dangerous(bot, message):
|
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
|
## IRCv3 Capability Negotiation
|
||||||
@@ -405,6 +427,68 @@ restarting the bot.
|
|||||||
The `core` plugin cannot be unloaded (prevents losing `!load`/`!reload`),
|
The `core` plugin cannot be unloaded (prevents losing `!load`/`!reload`),
|
||||||
but it can be reloaded.
|
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
|
## Writing Plugins
|
||||||
|
|
||||||
Create a `.py` file in the `plugins/` directory:
|
Create a `.py` file in the `plugins/` directory:
|
||||||
|
|||||||
Reference in New Issue
Block a user