Webhooks: post each new incoming message (closes #7)
check / check (push) Successful in 1m45s

Each message a contact sends in a direct chat is posted to each webhook
on that chat, as JSON holding `chat_id` and the message record of the
messages endpoint. The event handler only queues it: `api.Deliveries`
serves a queue of 100 with four goroutines, reads the chat's webhooks
there, and makes one POST per webhook, with a 10-second timeout, no
retry and no redirect followed. A full queue or a failed post is logged
by ids, never by URL or text. `bot.Run` stops it last, once the chat
client has exited. Tests cover the payload and its routing, a full
queue, failures, and a webhook that never answers; the README documents
the payload and what delivery promises.

Model: opus-5-5
This commit is contained in:
2026-09-29 07:19:56 +00:00
parent 397fc95149
commit 7170576a8e
8 changed files with 816 additions and 54 deletions
+56 -4
View File
@@ -226,7 +226,9 @@ Nothing is sent, and the answer is:
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.
webhook instead. Each new message the contact sends in the chat is then
posted to the webhook, as
[Messages posted to webhooks](#messages-posted-to-webhooks) describes.
```sh
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
@@ -252,8 +254,7 @@ 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.
configuration that cannot be parsed does.
Nothing is registered, and the answer is:
@@ -303,6 +304,51 @@ Nothing is removed, and the answer is:
webhook `webhook_id`;
- `500` if `webhooks.json` cannot be written.
### Messages posted to webhooks
Each new message a contact sends in a chat is posted to each of the
chat's webhooks: a `POST` to its `url`, with
`Content-Type: application/json`, whose body is the chat's `id` as
`chat_id` and the message as `GET /api/v1/chats/{id}/messages` shows it:
```json
{
"chat_id": 3,
"message": {
"id": 9,
"direction": "received",
"type": "text",
"text": "2 + 2",
"time": "2026-09-29T03:14:34Z"
}
}
```
Every kind of message is posted, not only arithmetic, so `type` can be
`image`, `file` or another kind, with the caption as `text`. Messages
the bot sends, its replies and those sent through the API, are not
posted.
A message is posted to each webhook at most once: there are no retries.
A webhook misses it if the bot cannot reach it, if it does not answer
within 10 seconds, or if it answers with a status other than 2xx; a
redirect is not followed, and counts as such a status. So that no
webhook holds up the bot's replies, messages wait in a queue, and the
bot posts four at a time; a message that arrives while 100 wait is not
posted. Posting goes on while the bot stops; once its chat client has
exited, posts in progress are abandoned, and messages still waiting are
not posted.
The bot logs two warnings, which never hold a webhook's URL, as it can
carry a secret, nor a message's text:
- `dropping a message for the webhooks: the queue is full`, with
`chat_id` and `message_id`, for a message that found the queue full;
- `posting a message to a webhook`, with `webhook_id`, `message_id` and
either the `status` the webhook answered with or the `error` that
ended the post, for each post that got no 2xx answer, abandoned posts
included. The error can name the webhook's host.
## Entrypoints
This repo adheres to the
@@ -405,7 +451,13 @@ container.
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.
and at worst a stray temporary file, which the bot ignores. Messages
are posted to the webhooks by four goroutines serving a queue. The
goroutine that delivers the chat client's events, and also its
answers, only puts each message in the queue, never waiting: not for a
webhook, and not for the list of webhooks, which waits while the file
is written. Posting goes on while the bot stops, until the chat client
has exited.
- **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.