docs: update docs for Teams integration
- USAGE.md: Teams Integration section (config, setup, compat matrix) - CHEATSHEET.md: Teams config snippet - API.md: TeamsBot and TeamsMessage reference - README.md: Teams in features list - ROADMAP.md: v2.1.0 milestone - TODO.md/TASKS.md: Teams items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,9 +24,10 @@ make down # Stop
|
||||
## Features
|
||||
|
||||
- Async IRC over plain TCP or TLS (SASL PLAIN auth, IRCv3 CAP negotiation)
|
||||
- Microsoft Teams support via outgoing webhooks (no SDK dependency)
|
||||
- Plugin system with `@command` and `@event` decorators
|
||||
- Hot-reload: load, unload, reload plugins at runtime
|
||||
- Admin permission system (hostmask patterns + IRCOP detection)
|
||||
- Admin permission system (hostmask patterns + IRCOP detection + AAD IDs)
|
||||
- Command shorthand: `!h` resolves to `!help` (unambiguous prefix matching)
|
||||
- TOML configuration with sensible defaults
|
||||
- Rate limiting, CTCP responses, auto reconnect
|
||||
|
||||
14
ROADMAP.md
14
ROADMAP.md
@@ -128,3 +128,17 @@
|
||||
- [x] `cron` plugin (scheduled bot commands on a timer)
|
||||
- [x] Plugin command unit tests (encode, hash, dns, cidr, defang)
|
||||
- [x] CI pipeline (Gitea Actions, Python 3.11-3.13, ruff + pytest)
|
||||
|
||||
## v2.1.0 -- Teams Integration
|
||||
|
||||
- [x] Microsoft Teams adapter via outgoing webhooks (no SDK)
|
||||
- [x] `TeamsBot` class with same plugin API as IRC `Bot`
|
||||
- [x] `TeamsMessage` dataclass duck-typed with IRC `Message`
|
||||
- [x] HMAC-SHA256 webhook signature validation
|
||||
- [x] Permission tiers via AAD object IDs
|
||||
- [x] IRC-only methods as no-ops (join, part, kick, mode, set_topic)
|
||||
- [x] Incoming webhook support for `send()` (proactive messages)
|
||||
- [x] Paste overflow via FlaskPaste (same as IRC)
|
||||
- [ ] Adaptive Cards for richer formatting
|
||||
- [ ] Graph API integration for DMs and richer channel access
|
||||
- [ ] Teams event handlers (member join/leave)
|
||||
|
||||
15
TASKS.md
15
TASKS.md
@@ -1,6 +1,19 @@
|
||||
# derp - Tasks
|
||||
|
||||
## Current Sprint -- v2.0.0 Stable API (2026-02-21)
|
||||
## Current Sprint -- v2.1.0 Teams Integration (2026-02-21)
|
||||
|
||||
| Pri | Status | Task |
|
||||
|-----|--------|------|
|
||||
| P0 | [x] | `src/derp/teams.py` -- TeamsBot, TeamsMessage, HTTP handler |
|
||||
| P0 | [x] | `src/derp/config.py` -- `[teams]` defaults |
|
||||
| P0 | [x] | `src/derp/cli.py` -- conditionally start TeamsBot alongside IRC bots |
|
||||
| P0 | [x] | HMAC-SHA256 signature validation (base64 key, `Authorization: HMAC` header) |
|
||||
| P0 | [x] | Permission tiers via AAD object IDs (exact match, not fnmatch) |
|
||||
| P0 | [x] | IRC no-ops: join, part, kick, mode, set_topic (debug log) |
|
||||
| P1 | [x] | Tests: `test_teams.py` (74 cases, 1302 total) |
|
||||
| P2 | [x] | Documentation update (USAGE.md, CHEATSHEET.md, API.md, README.md, ROADMAP.md) |
|
||||
|
||||
## Previous Sprint -- v2.0.0 Stable API (2026-02-21)
|
||||
|
||||
| Pri | Status | Task |
|
||||
|-----|--------|------|
|
||||
|
||||
10
TODO.md
10
TODO.md
@@ -82,6 +82,16 @@ is preserved in git history for reference.
|
||||
- [x] `shorten` -- manual URL shortening
|
||||
- [x] `cron` -- scheduled bot commands on a timer
|
||||
|
||||
## Teams
|
||||
|
||||
- [x] Microsoft Teams adapter via outgoing webhooks
|
||||
- [x] TeamsBot + TeamsMessage (duck-typed with IRC Message)
|
||||
- [x] HMAC-SHA256 webhook validation
|
||||
- [x] Permission tiers via AAD object IDs
|
||||
- [ ] Adaptive Cards for richer formatting
|
||||
- [ ] Graph API integration for DMs
|
||||
- [ ] Teams event handlers (member join/leave)
|
||||
|
||||
## Testing
|
||||
|
||||
- [x] Plugin command unit tests (encode, hash, dns, cidr, defang)
|
||||
|
||||
60
docs/API.md
60
docs/API.md
@@ -228,6 +228,66 @@ Wire-format encode/decode for raw DNS queries and responses.
|
||||
|
||||
---
|
||||
|
||||
## `derp.teams` -- Teams Adapter
|
||||
|
||||
Alternative bot adapter for Microsoft Teams via outgoing/incoming webhooks.
|
||||
Exposes the same plugin API as `derp.bot.Bot` so protocol-agnostic plugins
|
||||
work without modification.
|
||||
|
||||
### `TeamsMessage` dataclass
|
||||
|
||||
Duck-typed compatible with IRC `Message`:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `raw` | `dict` | Original Activity JSON |
|
||||
| `nick` | `str \| None` | Sender display name |
|
||||
| `prefix` | `str \| None` | Sender AAD object ID (for ACL) |
|
||||
| `text` | `str \| None` | Message body (stripped of @mention) |
|
||||
| `target` | `str \| None` | Conversation/channel ID |
|
||||
| `is_channel` | `bool` | Always `True` (outgoing webhooks) |
|
||||
| `command` | `str` | Always `"PRIVMSG"` (compat shim) |
|
||||
| `params` | `list[str]` | `[target, text]` |
|
||||
| `tags` | `dict[str, str]` | Empty dict (no IRCv3 tags) |
|
||||
| `_replies` | `list[str]` | Reply buffer (unstable) |
|
||||
|
||||
### `TeamsBot`
|
||||
|
||||
Same stable attributes and methods as `Bot`:
|
||||
|
||||
| Attribute | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `name` | `str` | Always `"teams"` |
|
||||
| `config` | `dict` | Merged TOML configuration |
|
||||
| `nick` | `str` | Bot display name (`teams.bot_name`) |
|
||||
| `prefix` | `str` | Command prefix (from `[bot]`) |
|
||||
| `state` | `StateStore` | Persistent key-value storage |
|
||||
| `registry` | `PluginRegistry` | Shared command and event registry |
|
||||
|
||||
**Sending messages** -- same signatures, different transport:
|
||||
|
||||
| Method | Behaviour |
|
||||
|--------|-----------|
|
||||
| `send(target, text)` | POST to incoming webhook URL |
|
||||
| `reply(msg, text)` | Append to `msg._replies` (HTTP response) |
|
||||
| `long_reply(msg, lines, *, label="")` | Paste overflow, appends to replies |
|
||||
| `action(target, text)` | Italic text via incoming webhook |
|
||||
| `shorten_url(url)` | Same FlaskPaste integration |
|
||||
|
||||
**IRC no-ops** (debug log, no error):
|
||||
|
||||
`join`, `part`, `kick`, `mode`, `set_topic`
|
||||
|
||||
**Plugin management** -- delegates to shared registry:
|
||||
|
||||
`load_plugins`, `load_plugin`, `reload_plugin`, `unload_plugin`
|
||||
|
||||
**Permission tiers** -- same model, exact AAD object ID matching:
|
||||
|
||||
`_get_tier(msg)`, `_is_admin(msg)`
|
||||
|
||||
---
|
||||
|
||||
## Handler Signatures
|
||||
|
||||
All command and event handlers are async functions receiving `bot` and
|
||||
|
||||
@@ -482,6 +482,28 @@ curl -X POST http://127.0.0.1:8080/ \
|
||||
POST JSON: `{"channel":"#chan","text":"msg"}`. Optional `"action":true`.
|
||||
Auth: HMAC-SHA256 via `X-Signature` header. Starts on IRC connect.
|
||||
|
||||
## Teams Integration
|
||||
|
||||
```toml
|
||||
# config/derp.toml
|
||||
[teams]
|
||||
enabled = true
|
||||
bot_name = "derp"
|
||||
bind = "127.0.0.1"
|
||||
port = 8081
|
||||
webhook_secret = "base64-secret-from-teams"
|
||||
incoming_webhook_url = "" # optional, for proactive msgs
|
||||
admins = ["aad-object-id-uuid"] # AAD object IDs
|
||||
operators = []
|
||||
trusted = []
|
||||
```
|
||||
|
||||
Expose via Cloudflare Tunnel: `cloudflared tunnel --url http://127.0.0.1:8081`
|
||||
|
||||
Teams endpoint: `POST /api/messages`. HMAC-SHA256 auth via `Authorization: HMAC <sig>`.
|
||||
Replies returned as JSON in HTTP response. IRC-only commands (kick, ban, topic) are no-ops.
|
||||
~90% of plugins work without modification.
|
||||
|
||||
## Plugin Template
|
||||
|
||||
```python
|
||||
|
||||
@@ -1301,3 +1301,102 @@ 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
|
||||
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`.
|
||||
|
||||
Reference in New Issue
Block a user