- docs/GOOGLE_CHAT_BOT.md: new step-by-step guide covering channels.json,
Google Cloud Console config, JWT audience, and troubleshooting
- docs/NEXTCLOUD_TALK_BOT.md: updated for per-user endpoints
(/webhook/nextcloud/{username}), channels.json config, removed old
server-level .env references, updated Multi-User note
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
5.1 KiB
Markdown
146 lines
5.1 KiB
Markdown
# Nextcloud Talk Bot Integration
|
|
|
|
Cortex connects to Nextcloud Talk as a bot — each Cortex user gets their own webhook endpoint routed to their chosen persona.
|
|
|
|
**Status:** Live and confirmed working (2026-03-20); per-user routing added 2026-03-27
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
- Access to the Nextcloud server (Docker exec or SSH)
|
|
- The Cortex server reachable at a public HTTPS URL
|
|
- The user pre-registered in Cortex (`manage_passwords.py invite`)
|
|
|
|
---
|
|
|
|
## Per-User Setup
|
|
|
|
### 1. Create the user's `channels.json`
|
|
|
|
Create `home/{username}/channels.json` on the Cortex server:
|
|
|
|
```json
|
|
{
|
|
"nextcloud": {
|
|
"persona": "inara",
|
|
"url": "https://cloud.dgrzone.com",
|
|
"bot_secret": "<a secret you choose — must match the occ install command>",
|
|
"timeout": 55
|
|
}
|
|
}
|
|
```
|
|
|
|
- **`persona`** — which persona responds (must exist under `home/{username}/persona/`)
|
|
- **`url`** — base URL of the Nextcloud instance
|
|
- **`bot_secret`** — a shared HMAC secret; you choose this value and use it in both `channels.json` and the `occ` install command
|
|
- **`timeout`** — seconds to wait for the LLM before sending a timeout message (NC Talk is async, so 55s is safe)
|
|
|
|
### 2. Register the bot in Nextcloud
|
|
|
|
Run on the Nextcloud server (inside the Docker container):
|
|
|
|
```bash
|
|
docker exec -it --user www-data <nc-app-container> php /var/www/html/occ talk:bot:install \
|
|
"{persona_name}" \
|
|
"<bot_secret from channels.json>" \
|
|
"https://cortex.dgrzone.com/webhook/nextcloud/{username}" \
|
|
--feature webhook --feature response --feature reaction
|
|
```
|
|
|
|
After installing, enable the bot in each Talk conversation via the conversation settings UI (three-dot menu → Bots).
|
|
|
|
To list installed bots and verify registration:
|
|
|
|
```bash
|
|
docker exec -it --user www-data <nc-app-container> php /var/www/html/occ talk:bot:list
|
|
```
|
|
|
|
To remove a bot (e.g. to re-register with a new secret or URL):
|
|
|
|
```bash
|
|
docker exec -it --user www-data <nc-app-container> php /var/www/html/occ talk:bot:remove <bot-id>
|
|
```
|
|
|
|
---
|
|
|
|
## How It Works
|
|
|
|
1. User sends a message in Talk → Nextcloud POSTs a signed webhook to `/webhook/nextcloud/{username}`
|
|
2. Cortex reads the user's `channels.json`, verifies the incoming HMAC signature
|
|
3. Sets the persona context, builds the system prompt, runs the LLM in a `BackgroundTask`
|
|
4. Returns HTTP 200 immediately (prevents Nextcloud from disabling the bot due to slow response)
|
|
5. Cortex POSTs the reply to `/ocs/v2.php/apps/spreed/api/v1/bot/{token}/message` with its own HMAC signature
|
|
|
|
---
|
|
|
|
## HMAC Signing — Critical Detail
|
|
|
|
**Incoming and outgoing signatures use different message construction.** This is the most common source of 401 errors.
|
|
|
|
### Incoming (Nextcloud → Cortex)
|
|
|
|
Nextcloud signs its outgoing webhook with `HMAC-SHA256(secret, random + raw_body)`:
|
|
|
|
```python
|
|
expected = hmac.new(
|
|
secret.encode(),
|
|
(random_header + body.decode("utf-8")).encode(),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
```
|
|
|
|
### Outgoing (Cortex → Nextcloud)
|
|
|
|
When Cortex posts a reply, Nextcloud verifies the signature against the *parsed message string*, not the raw body. This is because `BotController::sendMessage` passes the parsed `$message` parameter to `checksumVerificationService::validateRequest`, not `$request->getContent()`.
|
|
|
|
```python
|
|
sig = hmac.new(
|
|
secret.encode(),
|
|
(random_str + message).encode("utf-8"), # message text only, NOT json.dumps({"message": ...})
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
```
|
|
|
|
Wrong (causes persistent outgoing 401):
|
|
```python
|
|
# DON'T sign the full JSON body:
|
|
sig = hmac.new(secret.encode(), (random_str + '{"message": "..."}').encode(), hashlib.sha256).hexdigest()
|
|
```
|
|
|
|
---
|
|
|
|
## Nginx
|
|
|
|
The `/webhook/` prefix is already public in `auth_middleware.py`. If Nginx applies basic auth or IP restrictions, add a `location` block before the default auth block:
|
|
|
|
```nginx
|
|
location ^~ /webhook/ {
|
|
proxy_pass http://127.0.0.1:8000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Triggering the Bot
|
|
|
|
- **@mention** — prefix the message with `@{persona_name}`; the prefix is stripped before sending to the LLM
|
|
- **Any message** in a conversation where the bot is enabled — all messages are forwarded, not just @mentions
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause | Fix |
|
|
|---|---|---|
|
|
| 404 on the webhook | `channels.json` missing or no `nextcloud` key | Create/check `home/{username}/channels.json` |
|
|
| Webhook not received | Bot not enabled for conversation | Enable in Talk conversation settings (Bots) |
|
|
| Incoming 401 | `bot_secret` in `channels.json` doesn't match `occ install` secret | Re-register with matching secret |
|
|
| Reply POST returns 401 (first try) | HMAC computed over wrong data | Sign `random + message_text` only (not raw JSON body) |
|
|
| Reply POST returns 401 (persistent) | Brute force protection triggered | `occ security:bruteforce:reset <cortex-IP>` |
|
|
| Bot auto-disabled by Nextcloud | Webhook held open too long | Verify `BackgroundTasks` is used — Cortex returns 200 immediately |
|
|
| Claude falls back to Gemini | Stale/expired auth token | Run `claude auth login`; token is auto-refreshed from `~/.claude/.credentials.json` |
|
|
| No response at all | Nginx blocking the path | Add a `location ^~ /webhook/` block before any auth block |
|