- Use dgr_zone_nextcloud-app-1 throughout (actual container name) - talk:bot:uninstall (not remove — wrong command in previous version) - Added Logs section: occ log:tail + journalctl - Bruteforce reset command now includes full docker exec form Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.7 KiB
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:
{
"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 underhome/{username}/persona/)url— base URL of the Nextcloud instancebot_secret— a shared HMAC secret; you choose this value and use it in bothchannels.jsonand theoccinstall commandtimeout— 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
The Nextcloud container for DgrZone is dgr_zone_nextcloud-app-1. Substitute your own container name if different.
First, list existing bots to check if one is already registered (note the bot ID):
docker exec -it --user www-data dgr_zone_nextcloud-app-1 php /var/www/html/occ talk:bot:list
If re-registering (new URL or new secret), uninstall the old bot first:
docker exec -it --user www-data dgr_zone_nextcloud-app-1 php /var/www/html/occ talk:bot:uninstall <bot-id>
Install the bot:
docker exec -it --user www-data dgr_zone_nextcloud-app-1 php /var/www/html/occ talk:bot:install \
"Inara" \
"<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: open the conversation → three-dot menu → Bots → enable the bot by name.
How It Works
- User sends a message in Talk → Nextcloud POSTs a signed webhook to
/webhook/nextcloud/{username} - Cortex reads the user's
channels.json, verifies the incoming HMAC signature - Sets the persona context, builds the system prompt, runs the LLM in a
BackgroundTask - Returns HTTP 200 immediately (prevents Nextcloud from disabling the bot due to slow response)
- Cortex POSTs the reply to
/ocs/v2.php/apps/spreed/api/v1/bot/{token}/messagewith 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):
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().
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):
# 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:
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
Logs
Two log streams are useful when debugging:
# Nextcloud server logs (bot registration errors, webhook rejections)
docker exec -it --user www-data dgr_zone_nextcloud-app-1 php /var/www/html/occ log:tail
# Cortex service logs (LLM errors, signature failures, timeouts)
journalctl --user -u cortex -f
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 | docker exec -it --user www-data dgr_zone_nextcloud-app-1 php /var/www/html/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 |