Files
Cortex-Inara/docs/NEXTCLOUD_TALK_BOT.md
Scott Idem fe854ee534 Add Nextcloud Talk bot integration (Inara)
- New routers/nextcloud_talk.py: webhook handler verifies incoming HMAC,
  calls LLM via BackgroundTasks, posts reply with correctly computed
  signature (random + message_text, not raw body)
- llm_client.py: read Claude OAuth token live from
  ~/.claude/.credentials.json to avoid stale systemd env tokens;
  strip conflicting ANTHROPIC_API_KEY
- config.py: add nextcloud_url, nextcloud_talk_bot_secret,
  nextcloud_talk_timeout settings
- main.py: register nextcloud_talk router, add logging setup
- docs/NEXTCLOUD_TALK_BOT.md: installation guide + HMAC signing gotcha

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 23:04:26 -04:00

3.3 KiB

Nextcloud Talk Bot Integration

Inara is registered as a bot in Nextcloud Talk, receiving messages via webhook and replying through the bot API.


Installation

Run on the Nextcloud server (inside the Docker container):

docker exec -it --user www-data <nc-app-container> php /var/www/html/occ talk:bot:install \
  "Inara" \
  "<secret from cortex .env NEXTCLOUD_TALK_BOT_SECRET>" \
  "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.


Configuration (cortex/.env)

NEXTCLOUD_TALK_BOT_SECRET=<shared secret — must match occ install command>

NEXTCLOUD_URL defaults to https://cloud.dgrzone.com in config.py.


How It Works

  1. User sends a message in Talk → Nextcloud POSTs signed webhook to /inara-nextcloud-talk-webhook
  2. Cortex verifies the incoming HMAC signature, extracts the message, runs it through the LLM
  3. Cortex POSTs the reply to /ocs/v2.php/apps/spreed/api/v1/bot/{token}/message

HMAC Signing — Critical Detail

The signature covers random + message_text, NOT random + raw_body.

This differs from typical webhook protocols. Nextcloud Talk's BotController::sendMessage passes the parsed $message parameter to ChecksumVerificationService::validateRequest, not the raw request body.

Source: spreed/lib/Controller/BotController.phpgetBotFromHeaders():

$this->checksumVerificationService->validateRequest($random, $checksum, $secret, $message);
// $message is the parsed string, not $request->getContent()

Correct Python:

sig = hmac.new(
    secret.encode(),
    (random_str + message_text).encode("utf-8"),  # message_text only, not full body
    hashlib.sha256,
).hexdigest()

Wrong (causes persistent 401):

# DON'T sign the full JSON body:
sig = hmac.new(secret.encode(), (random_str + '{"message": "..."}').encode(), hashlib.sha256).hexdigest()

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.


Troubleshooting

Symptom Cause Fix
Webhook not received Bot not enabled for conversation Enable in Talk conversation settings
Incoming 401 on webhook Wrong secret in .env Match secret to occ talk:bot:install value
Reply POST returns 401 HMAC computed over wrong data Sign random + message_text only
Reply POST returns 401 (persistent) Brute force protection occ security:bruteforce:reset <cortex-IP>
Claude falls back to Gemini Stale/wrong auth token Token is auto-refreshed from ~/.claude/.credentials.json; run claude auth login if expired
Bot auto-disabled by Nextcloud Webhook held open too long during LLM call Use BackgroundTasks — return 200 immediately