# simplexcalc simplexcalc is a Go chat bot by [@sneak](https://sneak.berlin) for the SimpleX Chat network: it accepts every contact request and answers arithmetic such as `2 + 2` with the result. Send it `2 + 2` and it replies `4`; send `5 * 5/2` and it replies `12.5`. It understands decimal numbers, `+ - * /`, unary minus and parentheses, and computes exactly, so `0.1 + 0.2` is `0.3`. Anything else gets a short explanation instead of a result. ## Getting Started Build the image and run the bot, with its SimpleX profile on a named volume: ```sh git clone git@git.eeqj.de:clawbot/simplexcalc.git cd simplexcalc make docker docker run -d --name simplexcalc --restart unless-stopped \ -v simplexcalc-data:/var/lib/simplexcalc simplexcalc docker logs simplexcalc 2>&1 | grep '"msg":"ready"' ``` The `ready` log line carries the bot's contact address: `address` is the short link to share, and `full_address` is the same address in the long form that older SimpleX clients need. Open the link in any SimpleX Chat app, or paste it into its "connect via link" screen; the bot accepts at once, greets you, and answers every message you send it. The address is stored on the `simplexcalc-data` volume. It survives restarts, image upgrades and recreating the container, and is lost only with the volume. ### Messaging it from a terminal The image carries the SimpleX Chat command-line client, so a throwaway second client can talk to the bot without installing anything. Replace `ADDRESS` with the bot's address: ```sh docker run --rm -v simplexcalc-tester:/var/lib/simplexcalc simplexcalc \ simplex-chat -d /var/lib/simplexcalc/tester --user-display-name tester \ -e "/c ADDRESS" -t 30 --execute-log all docker run --rm -v simplexcalc-tester:/var/lib/simplexcalc simplexcalc \ simplex-chat -d /var/lib/simplexcalc/tester \ -e "@calc 5 * 5/2" -t 20 --execute-log messages docker volume rm simplexcalc-tester ``` The first command connects and prints the bot's greeting; the second sends `5 * 5/2` and prints the reply, `12.5`. ## Configuration All configuration is environment variables, read once at startup. A `.env` file in the working directory is loaded automatically for development. **A variable that is set to something unparseable aborts startup.** It is never quietly replaced by the default. Defaults apply only to variables that are absent. - `DATA_DIR` — where the SimpleX database lives: the bot's profile, its keys, its address and its contacts. Default `./data`; the image sets `/var/lib/simplexcalc`. - `DEBUG` — `true` or `false`, default `false`. `true` logs every event the chat client sends. ## Entrypoints This repo adheres to the [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) standard. The `script/` entrypoints, each with a thin `make` shim: - `script/bootstrap` (`make bootstrap`) — install build dependencies (git, make, go) idempotently. Linting additionally needs docker; markdown formatting needs docker or a local `prettier`. - `script/setup` (`make setup`) — `bootstrap` plus the git pre-commit hook - `script/test` (`make test`) — `go test` with the race detector and coverage, `-count=1`, 90 s timeout; quiet on success, verbose rerun on failure - `script/lint` (`make lint`) — `golangci-lint` via docker only, against the digest-pinned image, then `script/assert-step-ran` and `script/assert-context-complete` over the build log. Nothing is installed locally and nothing runs on the host. - `script/fmt` (`make fmt`) — format Go with `gofmt` and everything else with `prettier` (writes) - `script/fmt-check` (`make fmt-check`) — the same scope, read-only - `script/check` (`make check`) — `test` + `lint` + `fmt-check`. What the pre-commit hook runs. Never modifies files. - `script/docker` (`make docker`) — build the image, tagged with `script/projectname` - `script/cibuild` (`make cibuild`) — the CI gate: `docker build --progress=plain --no-cache-filter=lint --no-cache-filter=builder .` followed by assertions that the lint step and the test step really ran, and that both stages really received the whole repository. The Gitea workflow runs this on every push. - `script/precommit` — the hook body: `go mod tidy` must not change `go.mod`/`go.sum`, then `check` - `script/install-precommit` (`make hooks`) — installs the hook - `script/projectname` — prints the project name; other scripts call it so they can stay identical across repos - `script/prettier`, `script/assert-step-ran`, `script/assert-context-complete`, `script/repo-source-manifest` — helpers, not entrypoints `make build`, `make run`, `make dev`, `make deps` and `make clean` are the ordinary conveniences. `make run` and `make dev` run the bot on the host, which needs the `simplex-chat` command-line client on `PATH`. ### Why the assertions exist A green `docker build` is not evidence that the checks ran. On an unchanged tree every layer comes from cache and the build exits 0 having executed nothing; BuildKit silently ignores a `--no-cache-filter` naming a stage that no longer exists; and a `.dockerignore` entry can remove a package from the build context, after which the linter genuinely runs, genuinely examines what it was handed, and genuinely reports `0 issues.` over a repository with a violation in it. So the build is not trusted. `script/assert-step-ran` requires the log to show the step executing and the tool's own success line coming out of it. `script/assert-context-complete` requires the file inventory each stage emitted to match the git index — an expectation `.dockerignore` cannot reach. Read the comments in those two scripts before changing either; they document what they still cannot see. ## Rationale SimpleX Chat is an end-to-end encrypted messenger with no user identifiers: people connect through addresses they choose to share. simplexcalc is a calculator anyone can reach that way, and a small, complete example of a SimpleX bot in Go: profile and address set-up, automatic acceptance, and replies, with the chat client in the same container. ## Design - **One process tree, one container.** `simplexcalc run` starts the SimpleX Chat command-line client, `simplex-chat`, as a child process, with its database under `$DATA_DIR/simplex` and its WebSocket API on `127.0.0.1:5225`. The API has no authentication, which is why it is never exposed outside the container. - **The protocol** (`internal/simplex`) is JSON over that WebSocket: a command carries a correlation id, its response carries the same id, and anything without one is an event. Only the fields the bot reads are decoded, so records that grow new fields in a later client release still decode. - **Set-up on every start** (`internal/bot`): read the bot's profile, create its long-term address if it has none, and set the address to accept every contact request and to greet each new contact. The first start creates the profile itself, a bot profile named `calc`. The address is logged in the `ready` line. - **Replies**: for each text message a contact sends in a direct chat, the bot sends back the result, as a reply quoting the message. Group messages, files and the bot's own messages are ignored. - **Arithmetic** (`internal/calc`): the text is parsed as a Go expression with `go/parser`, and only numbers, `+ - * /`, unary signs and parentheses are evaluated; anything else in the syntax tree is refused. `go/constant` computes with exact rationals. Numbers are read as decimal, so `010` is ten. Input over 256 bytes is refused, so a message cannot make the bot do unbounded work. Whole numbers below 1021 are written exactly; other results in the shortest form that reads back as the same double, in exponent notation from 1021 up and below 10-6. A result beyond the range of a double is refused as too large. - **Failure is an exit.** If the chat client exits or the connection to it drops, the bot exits with an error and the container's restart policy starts both again. `SIGTERM` stops the bot, which stops the chat client with `SIGTERM` and kills it if it has not exited within 10 seconds. - **The image**: a Go build stage, then an Ubuntu 22.04 runtime — the release the chat client is built for, which already has every library it links — with the `simplex-chat` v7.0.2 binary downloaded by an `ADD` whose checksum BuildKit verifies. It runs as an unprivileged user and is x86_64 only. ## Operating it **Backup.** Everything durable is the SimpleX database on the volume: `simplex_chat.db` and `simplex_agent.db`. Stop the container before copying them, since a copy taken from under the running client can be inconsistent. The database holds the bot's keys, so a copy lets its holder answer as the bot; keep it as private as the running instance. **Upgrade.** Rebuild the image and recreate the container with the same volume. The chat client migrates its database on start. A newer `simplex-chat` release is a change to the URL and the checksum on the `ADD` line in the `Dockerfile`. ## TODO See `docs/TODO.md` and the repository's issue tracker. ## Author [@sneak](https://sneak.berlin)