vojo/apps/ai-bot/README.md
2026-05-31 20:46:47 +03:00

7.3 KiB

ai-bot

A plaintext Matrix bot user (@ai:vojo.chat, display name Vojo AI) that answers xAI Grok completions in its rooms: @-mentions in group rooms and every message in a 1:1. It runs as a Synapse application service — Synapse pushes event transactions to the bot's HTTP endpoint; the bot speaks the Matrix CS-API back over plain HTTP (no Olm/Megolm — Vojo rooms are unencrypted by default) and calls the xAI OpenAI-compatible Chat Completions API.

Authentication is the appservice as_token/hs_token (from the registration) — non-expiring, so there is no token rotation and no stored password.

It is a separate server-side service, deployed next to Synapse. It lives in this repo (alongside apps/widget-*) but ships nothing to the web client.

Branding: user-facing name is Vojo AI with a generic icon. "Grok" appears only as the factual attribution ("powered by Grok, xAI") and as the real model id — never as the product name or logo (xAI Brand Guidelines).

Design source of truth: docs/plans/grok_bot.md. Privacy/152-ФЗ pre-launch gating lives there (§6) and is not closed by this code.

Layout

apps/ai-bot/
├── main.go         # entrypoint, lifecycle, `check-config` subcommand
├── config.go       # env parsing + validation + redacted summary
├── bot.go          # event handling, classification, limiter wiring
├── appservice.go   # HTTP transaction-push server (hs_token auth, txn idempotency)
├── matrix.go       # CS-API client as the appservice user (as_token + ?user_id=)
├── registration.go # generate + read registration.yaml (tokens, mautrix idiom)
├── events.go       # Matrix event types + decoders
├── mentions.go     # m.mentions + pill/reply fallbacks (F29/F30)
├── context.go      # xAI message-window assembly (trigger + bot replies)
├── xai.go          # chat/completions client + retry (F6)
├── store.go        # SQLite: spend ledger, txn dedup, encrypted-warned set
├── messages.go     # bot-authored RU notices
├── markdown.go     # markdown → org.matrix.custom.html for the reply's formatted_body
├── util.go         # bounded dedup set
├── prompts/system_ru.txt
├── Dockerfile      # CGO-free static build → distroless, EXPOSE 8009
└── .env.example

Configuration

All via environment (see .env.example). Required: HOMESERVER_URL, BOT_MXID, AS_TOKEN, HS_TOKEN, XAI_API_KEY, ALLOWED_SERVERS. AS_ADDR (default :8009) is the transaction-push listen address — it must match the url port in the registration. The model is env-configurable (XAI_MODEL, default grok-4.20-0309-non-reasoning; grok-4.3 is an alternative — **re-verify the id

  • price on docs.x.ai before deploy**).

The hard USD ceiling is priced from the API-returned token usage times the configured XAI_PRICE_*_PER_M fallbacks, so a price change only needs those constants updated — it can't silently blow the cap.

One-time setup (appservice registration)

Like the mautrix bridges (e.g. telegram), the bot generates its own registration (random as_token/hs_token) and reads its tokens back from that same file — the single source of truth shared with Synapse, no hand-copying.

  1. Generate it (writes REGISTRATION_PATH, default /data/registration.yaml):
    docker compose run --rm ai-bot generate-registration
    
  2. Bind-mount that same file into the Synapse container (e.g. as /data/ai-registration.yaml) and add it to homeserver.yaml:
    app_service_config_files:
      - /data/ai-registration.yaml
    
  3. Restart Synapse (it caches AS configs at startup). Synapse auto-creates @ai:vojo.chat from sender_localpart — no register_new_matrix_user.

The bot reads REGISTRATION_PATH for its tokens (no env AS_TOKEN/HS_TOKEN needed) and sets its own display name (BOT_DISPLAY_NAME, default "Vojo AI") on startup. The bot writes/reads /data, so that dir must be owned by the image's runtime uid (distroless nonroot = 65532): sudo chown -R 65532:65532 ~/vojo/ai-bot.

Run

go run . check-config   # local config smoke test (no homeserver contact)
go run .                # real run (needs env + a reachable homeserver)

Image & secrets model

The image is config-less (a .dockerignore keeps .env, state/ and VCS out of the build context; the Dockerfile copies only the binary + prompts/). Build locally and ship like the mautrix bridges (VS Code task Deploy AI bot = docker build -t ai-bot:customdocker save | ssh docker load), then run on the server with config + secrets supplied at runtime.

Config and secrets are separated: non-secret config in ai-bot.env (env_file); the appservice tokens live in the generated registration.yaml (read via REGISTRATION_PATH); the only remaining standalone secret is the xAI key (XAI_API_KEY_FILE).

Compose stanza (add to ~/vojo/docker-compose.yml; the service key ai-bot must match the registration url host http://ai-bot:8009):

ai-bot:
  image: ai-bot:custom
  container_name: vojo-ai-bot
  restart: unless-stopped
  depends_on: [synapse]
  env_file: ./ai-bot/ai-bot.env                  # NON-SECRET config (chmod 600)
  environment:
    REGISTRATION_PATH: /data/registration.yaml   # tokens (generated; shared with Synapse)
    STATE_DIR: /data/state                        # SQLite: spend ledger + txn dedup
    XAI_API_KEY_FILE: /data/secrets/xai_api_key   # the one standalone secret
  volumes:
    - ./ai-bot:/data                              # owned by uid 65532 (see setup)

Also bind-mount the same registration into Synapse and restart it:

synapse:
  volumes:
    - ./ai-bot/registration.yaml:/data/ai-registration.yaml:ro

HOMESERVER_URL must use the Synapse service name (http://synapse:8008), not localhost. Synapse and the bot must share a docker network (same compose project does this) so Synapse can push to http://ai-bot:8009.

Verification status

Compile-level + unit-tested locally:

  • go vet clean, gofmt clean, static CGO-free build.
  • go test — appservice transaction handling (hs_token auth → 403 on bad token, txnId idempotency / no re-dispatch, legacy ?access_token=, user query 200/404); mention detection (m.mentions, empty-{} F29, no-body-fallback F30, pill, reply-to-bot); DM classification (invited+joined==2, F3: 2 joined + 1 invited is not a 1:1); group-vs-DM context minimisation (groups never leak third-party content); USD pricing; markdown → HTML rendering (escaping, safe-URL allowlist, false-positive guards, oversize/adversarial fallbacks).
  • check-config reads env + loads the system prompt.

Deferred to a live homeserver + xAI key + a loaded registration (runtime ✔):

  • Synapse pushes transactions → bot replies (authenticated as @ai:vojo.chat in logs);
  • invite from :vojo.chat → join, foreign-server invite → leave (F11);
  • @-mention / 1:1 message → m.notice reply with reply (and thread, F27) relation, carrying a formatted_body (org.matrix.custom.html) when the answer has markdown;
  • encrypted room → exactly one notice, not repeated after restart (F5);
  • per-user cap → silent drop; global USD ceiling → one notice/room/day;
  • a retried transaction (lost 200) is processed at most once (txn dedup).