Files
Cortex-Inara/docs/NEXTCLOUD_TALK_BOT.md
Scott Idem 6b725afc3e docs: update NC Talk doc with real container name, commands, and logs section
- 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>
2026-03-29 13:47:43 -04:00

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

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

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

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