HTTP API: register, list and remove webhooks, kept across restarts (closes #6)
check / check (push) Successful in 1m19s

An external app can register webhooks on a chat (`POST /api/v1/chats/{id}/webhooks` with a URL), list them, and remove one. Registering the same URL again returns the existing registration. Registrations are kept in `$DATA_DIR/webhooks.json`, rewritten whole on each change through a file created 0600, synced and renamed, so a crash leaves the old file or the new one; a file present but unreadable stops startup. Delivery of incoming messages is the next unit.

Disclosures: the JSON body reading is now one helper shared with the send endpoint; gosec G304 is suppressed on reading the webhooks file; the 0600-from-creation claim rests on `os.CreateTemp`'s source, not a system-call trace.

Model: opus-5-5
This commit was merged in pull request #17.
This commit is contained in:
2026-09-29 08:38:13 +02:00
parent f53b666119
commit 397fc95149
11 changed files with 1018 additions and 47 deletions
+101 -9
View File
@@ -73,8 +73,10 @@ development.
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
- `DATA_DIR` — where the bot keeps what it must not lose: the SimpleX
database, which holds the bot's profile, its keys, its address and its
contacts, and `webhooks.json`, which holds the webhooks registered
through the [API](#api). Default `./data`; the image sets
`/var/lib/simplexcalc`.
- `DEBUG` — `true` or `false`, default `false`. `true` logs every event
the chat client sends.
@@ -89,7 +91,8 @@ variables that are absent.
## API
An HTTP API beside the chat client lets another program read the bot's
chats and send messages in them. It speaks JSON on `PORT`.
chats, send messages in them and register webhooks on them. It speaks
JSON on `PORT`.
**Authentication.** Every request carries the credential from
`API_TOKEN_FILE`:
@@ -218,6 +221,88 @@ Nothing is sent, and the answer is:
- `413` if the body is over 64 KiB, or the text is too long for one
SimpleX message, which holds about 15,000 bytes.
### `POST /api/v1/chats/{id}/webhooks`
Registers the `url` in the body as a webhook on the chat `id`, and
answers `201` with the webhook. If the chat already has a webhook with
the same `url`, character for character, the answer is `200` with that
webhook instead.
```sh
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"url":"https://example.com/hook"}' \
http://127.0.0.1:8080/api/v1/chats/3/webhooks
```
```json
{
"id": "5f0c7a1e9b2d4c8e3a6f1b7d2e9c4a80",
"chat_id": 3,
"url": "https://example.com/hook"
}
```
A webhook has:
- `id` — 16 random bytes, written as 32 hexadecimal digits.
- `chat_id` — the chat it is registered on.
- `url` — the URL, as it was registered.
The bot keeps the webhooks in `webhooks.json` in `DATA_DIR`, with mode
0600, and reads that file at startup, so they survive a restart. Without
the file there are none; a file that cannot be read, or that holds
anything but webhooks as the bot writes them, aborts startup, as
configuration that cannot be parsed does. The bot does not post anything
to a webhook yet.
Nothing is registered, and the answer is:
- `400` if the body is not JSON of that shape, or `url` is not an
absolute `http` or `https` URL with a host, at most 2048 bytes long;
- `404` if `GET /api/v1/chats` does not list `id`;
- `413` if the body is over 64 KiB;
- `500` if `webhooks.json` cannot be written.
### `GET /api/v1/chats/{id}/webhooks`
The webhooks registered on the chat `id`, in the order they were
registered; `404` if `GET /api/v1/chats` does not list `id`. A chat's
webhooks stay after its contact deletes the chat (`contact_deleted` is
`true`), until they are removed.
```sh
curl -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:8080/api/v1/chats/3/webhooks
```
```json
{
"webhooks": [
{
"id": "5f0c7a1e9b2d4c8e3a6f1b7d2e9c4a80",
"chat_id": 3,
"url": "https://example.com/hook"
}
]
}
```
### `DELETE /api/v1/chats/{id}/webhooks/{webhook_id}`
Removes the webhook `webhook_id` from the chat `id`, and answers `204`
with no body.
```sh
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:8080/api/v1/chats/3/webhooks/5f0c7a1e9b2d4c8e3a6f1b7d2e9c4a80
```
Nothing is removed, and the answer is:
- `404` if `GET /api/v1/chats` does not list `id`, or the chat has no
webhook `webhook_id`;
- `500` if `webhooks.json` cannot be written.
## Entrypoints
This repo adheres to the
@@ -315,6 +400,12 @@ container.
the one that delivers events, which also delivers the chat client's
answers. When the bot stops, requests in progress get 5 seconds to
finish before the chat client is stopped.
- **Webhooks** (`internal/api`) are read from `$DATA_DIR/webhooks.json`
before the chat client starts. Changes are made one at a time, and
each rewrites the file whole: the bot writes a temporary file beside
it, named `webhooks.json.` and digits, and renames it over the old
one. A crash leaves the old file or the new one, never part of one,
and at worst a stray temporary file, which the bot ignores.
- **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.
@@ -359,12 +450,13 @@ container.
## Operating it
**Backup.** Everything durable is on the volume: the SimpleX database,
`simplex_chat.db` and `simplex_agent.db`, and the API credential,
`api-token`. Stop the container before copying the database, 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, and the credential lets its holder use the API; keep both as
private as the running instance.
`simplex_chat.db` and `simplex_agent.db`; the API credential,
`api-token`; and the webhooks, `webhooks.json`. Stop the container
before copying the database, 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; the credential lets its holder use
the API; and a webhook's URL can hold a secret of the program it points
at. Keep all three 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