A SimpleX Chat bot that answers arithmetic (closes #1)
check / check (push) Successful in 54s

Remove the template's HTTP service, database and fx wiring. Add exact
arithmetic on go/parser and go/constant, a client that runs simplex-chat
as a child process and drives its WebSocket API, and the bot, which keeps
an auto-accepting address and replies to each message. The image adds the
checksum-pinned simplex-chat v7.0.2 on Ubuntu 22.04.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-26 22:01:51 +00:00
parent f8ce8cef83
commit 16649e0f2f
66 changed files with 2119 additions and 5059 deletions
+195
View File
@@ -3,3 +3,198 @@
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
10<sup>21</sup> are written exactly; other results in the shortest
form that reads back as the same double, in exponent notation from
10<sup>21</sup> up and below 10<sup>-6</sup>. 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)