vojo/apps/ai-bot
2026-06-01 02:40:26 +03:00
..
prompts update vojo grok system promt 2026-05-31 16:32:29 +03:00
.dockerignore feat(ai-bot): add the Vojo AI Matrix appservice (xAI Grok backend) with push transactions, mention/DM replies, self-generated registration and spend limiter 2026-05-31 15:36:00 +03:00
.env.example feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
.gitignore feat(ai-bot): add the Vojo AI Matrix appservice (xAI Grok backend) with push transactions, mention/DM replies, self-generated registration and spend limiter 2026-05-31 15:36:00 +03:00
appservice.go fix(ai-bot): ack transactions instantly with async per-room processing to stop slow-call freezes, and signal system states via emoji reactions 2026-06-01 01:30:30 +03:00
appservice_test.go feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
bot.go feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
bot_test.go feat(ai-bot): add the Vojo AI Matrix appservice (xAI Grok backend) with push transactions, mention/DM replies, self-generated registration and spend limiter 2026-05-31 15:36:00 +03:00
concurrency_test.go fix(ai-bot): ack transactions instantly with async per-room processing to stop slow-call freezes, and signal system states via emoji reactions 2026-06-01 01:30:30 +03:00
config.go feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
context.go feat(ai-bot): add the Vojo AI Matrix appservice (xAI Grok backend) with push transactions, mention/DM replies, self-generated registration and spend limiter 2026-05-31 15:36:00 +03:00
Dockerfile feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
events.go feat(ai-bot): add the Vojo AI Matrix appservice (xAI Grok backend) with push transactions, mention/DM replies, self-generated registration and spend limiter 2026-05-31 15:36:00 +03:00
go.mod feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
go.sum feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
logging.go replace the hand-rolled markdown renderer with goldmark/bluemonday and harden the ai-bot against quota abuse and third-party leaks 2026-05-31 20:39:10 +03:00
main.go replace the hand-rolled markdown renderer with goldmark/bluemonday and harden the ai-bot against quota abuse and third-party leaks 2026-05-31 20:39:10 +03:00
markdown.go replace the hand-rolled markdown renderer with goldmark/bluemonday and harden the ai-bot against quota abuse and third-party leaks 2026-05-31 20:39:10 +03:00
markdown_test.go replace the hand-rolled markdown renderer with goldmark/bluemonday and harden the ai-bot against quota abuse and third-party leaks 2026-05-31 20:39:10 +03:00
matrix.go replace the hand-rolled markdown renderer with goldmark/bluemonday and harden the ai-bot against quota abuse and third-party leaks 2026-05-31 20:39:10 +03:00
mentions.go feat(ai-bot): add the Vojo AI Matrix appservice (xAI Grok backend) with push transactions, mention/DM replies, self-generated registration and spend limiter 2026-05-31 15:36:00 +03:00
messages.go fix(ai-bot): ack transactions instantly with async per-room processing to stop slow-call freezes, and signal system states via emoji reactions 2026-06-01 01:30:30 +03:00
README.md feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
registration.go feat(ai-bot): add the Vojo AI Matrix appservice (xAI Grok backend) with push transactions, mention/DM replies, self-generated registration and spend limiter 2026-05-31 15:36:00 +03:00
registration_test.go feat(ai-bot): add the Vojo AI Matrix appservice (xAI Grok backend) with push transactions, mention/DM replies, self-generated registration and spend limiter 2026-05-31 15:36:00 +03:00
store.go feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
store_test.go feat(ai-bot): move the operational store off SQLite onto a dedicated Postgres database (vojo_ai) via pgx 2026-06-01 02:40:26 +03:00
util.go fix(ai-bot): ack transactions instantly with async per-room processing to stop slow-call freezes, and signal system states via emoji reactions 2026-06-01 01:30:30 +03:00
xai.go fix(ai-bot): ack transactions instantly with async per-room processing to stop slow-call freezes, and signal system states via emoji reactions 2026-06-01 01:30:30 +03:00

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        # Postgres (vojo_ai): spend ledger, txn/event 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, AI_BOT_DATABASE_URL. 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).

Database

The bot keeps its operational state — appservice transaction + event dedup, the daily spend ledger, and the encrypted-room warned set — in a dedicated Postgres database vojo_ai on the shared server, mirroring the per-service bridge databases (each bridge owns its own role + DB). It stores no message content: the room timeline is canonical in Synapse, and the bot's xAI context window is the in-memory buffer in bot.go. The schema is created/migrated on startup (a schema_version table + idempotent CREATE TABLE IF NOT EXISTS), so a fresh vojo_ai needs no manual DDL — just the role + database:

-- once, as the Postgres superuser (e.g. `docker exec vojo-postgres-1 psql -U synapse -d postgres`):
CREATE ROLE vojo_ai LOGIN PASSWORD '<32-char secret>';   -- least privilege; NOT a superuser
CREATE DATABASE vojo_ai OWNER vojo_ai;

Point the bot at it with AI_BOT_DATABASE_URL (libpq/pgx DSN). Inside the docker network the host is the postgres service; sslmode=disable matches Synapse and the bridges on the internal network:

AI_BOT_DATABASE_URL=postgres://vojo_ai:<secret>@postgres:5432/vojo_ai?sslmode=disable

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, postgres]                # needs both up before it starts
  env_file: ./ai-bot/ai-bot.env                  # config incl. AI_BOT_DATABASE_URL (chmod 600 — embeds the DB password)
  environment:
    REGISTRATION_PATH: /data/registration.yaml   # tokens (generated; shared with Synapse)
    STATE_DIR: /data/state                        # runtime dir (the operational store is now in Postgres)
    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.

The store-backed tests (appservice transaction handling + the dedup/limiter/warned store in store_test.go, including the concurrent per-user-cap guarantee and restart-durability) need a throwaway Postgres via AI_BOT_TEST_DATABASE_URL; they skip when it is unset, so go test ./... stays green without one. To run them:

docker run -d --name pg -e POSTGRES_PASSWORD=p -p 5432:5432 postgres:16
# … create role+db vojo_ai, then:
AI_BOT_TEST_DATABASE_URL=postgres://vojo_ai:…@localhost:5432/vojo_ai?sslmode=disable go test ./...

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