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:
user
2026-02-21 19:52:39 +01:00
parent 014b609686
commit 4a165e8b28
7 changed files with 221 additions and 2 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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
View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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`.