# Nextcloud Talk Bot Integration Inara is registered as a bot in Nextcloud Talk, receiving messages via webhook and replying through the bot API. **Status:** Live and confirmed working (2026-03-20) --- ## Installation Run on the Nextcloud server (inside the Docker container): ```bash docker exec -it --user www-data php /var/www/html/occ talk:bot:install \ "Inara" \ "" \ "https://cortex.dgrzone.com/inara-nextcloud-talk-webhook" \ --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 php /var/www/html/occ talk:bot:list ``` To uninstall (if re-registering with a new secret): ```bash docker exec -it --user www-data php /var/www/html/occ talk:bot:remove ``` --- ## Configuration **`cortex/.env`:** ``` NEXTCLOUD_URL=https://cloud.dgrzone.com NEXTCLOUD_TALK_BOT_SECRET= ``` `NEXTCLOUD_URL` defaults to `https://cloud.dgrzone.com` in `config.py`. **Nginx:** The `/inara-nextcloud-talk-webhook` endpoint must be reachable by Nextcloud without basic auth. Add a location block before the default `auth_basic` block: ```nginx location = /inara-nextcloud-talk-webhook { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } ``` (The `/channels/` prefix is already bypassed for Google Chat — consider moving the webhook path to `/channels/nextcloud` in a future cleanup to unify the nginx config.) --- ## How It Works 1. User sends a message in Talk → Nextcloud POSTs a signed webhook to `/inara-nextcloud-talk-webhook` 2. Cortex verifies the incoming HMAC signature, extracts the message text, runs it through the LLM 3. Cortex POSTs the reply to `/ocs/v2.php/apps/spreed/api/v1/bot/{token}/message` with its own HMAC signature 4. The webhook handler returns HTTP 200 immediately; the LLM call happens in a `BackgroundTask` (prevents Nextcloud from disabling the bot due to slow response) --- ## 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 # _verify_signature in nextcloud_talk.py 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 # _send_reply in nextcloud_talk.py 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() ``` --- ## Multi-User Note NC Talk currently uses the **default user and persona** (`settings.default_tier`, `load_context()`). All Talk conversations go to Inara regardless of who is messaging. Per-conversation persona routing (e.g., Holly gets Tina) is a future enhancement — would require mapping Nextcloud user IDs or conversation tokens to Cortex users. --- ## Claude CLI Auth in systemd The `CLAUDE_CODE_OAUTH_TOKEN` in `.env` goes stale after each `claude auth login` (tokens rotate). Cortex reads the token live from `~/.claude/.credentials.json` on every Claude call (`llm_client._fresh_claude_token()`), so no manual `.env` update is needed after re-authentication. Also: never set `ANTHROPIC_API_KEY` to an OAuth token value (`sk-ant-oat01-...`) — the Claude CLI treats it as a direct API key and fails. Only real API keys (`sk-ant-api03-...`) belong in `ANTHROPIC_API_KEY`. --- ## Triggering the Bot - **@mention** — prefix the message with `@inara` (or whatever `AGENT_NAME` is set to); 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 | |---|---|---| | Webhook not received | Bot not enabled for conversation | Enable in Talk conversation settings (Bots) | | Incoming 401 | Wrong secret in `.env` | Match secret to `occ talk:bot:install` value | | 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 ` | | Bot auto-disabled by Nextcloud | Webhook held open too long | Verify `BackgroundTasks` is used — return 200 immediately | | Claude falls back to Gemini | Stale/wrong auth token | Token is auto-refreshed from `~/.claude/.credentials.json`; run `claude auth login` if expired | | No response at all | Nginx blocking the path with basic auth | Add a `location =` block before the auth block (see Nginx section above) |