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

Each message a contact sends in a direct chat is posted to each webhook registered on that chat: `POST`, `Content-Type: application/json`, body `{"chat_id":N,"message":{...}}` with the same record the messages endpoint returns. The event handler only queues the message; four goroutines post it, one attempt each with a 10-second timeout and no retries, and with 100 already waiting a message is dropped and logged, so a slow webhook never delays the bot's replies. Posting stops last, after the chat client.

Disclosures: redirects are not followed; a logged failure can name the webhook's host but never its path, query or credentials; posts still in flight at shutdown are abandoned and logged.

Model: opus-5-5
This commit was merged in pull request #19.
This commit is contained in:
2026-09-29 10:23:24 +02:00
parent 0b9121a806
commit fa2aa09769
8 changed files with 816 additions and 54 deletions
+20 -9
View File
@@ -56,10 +56,10 @@ var errExited = errors.New("simplex-chat exited")
// Run reads the webhooks kept in cfg.DataDir, starts the chat client with
// its database there, serving its API on localhost at chatPort, connects
// to it, sets up the bot's address, then answers messages and serves the
// bot's API until ctx is cancelled — which is a clean stop and returns
// nil — or until the chat client, the connection to it or the API's
// listener fails, which returns the error.
// to it, sets up the bot's address, then answers messages, posts them to
// the webhooks and serves the bot's API until ctx is cancelled — which is
// a clean stop and returns nil — or until the chat client, the connection
// to it or the API's listener fails, which returns the error.
func Run(
ctx context.Context, log *slog.Logger, cfg *config.Config, chatPort int,
) error {
@@ -75,6 +75,12 @@ func Run(
return err
}
// Posts the messages the event handler hands it to the webhooks. Its
// stop, deferred first, runs last, once the chat client has exited
// and can send no more messages; cancelling ctx does not reach it.
deliveries := api.StartDeliveries(context.WithoutCancel(ctx), log, webhooks)
defer deliveries.Stop()
// Cancelling this stops the chat client; the deferred wait makes
// Run return only once it has exited, whatever path Run takes.
// Cancelling ctx does not reach it, so that the API, stopped first,
@@ -94,7 +100,7 @@ func Run(
<-cli.Done()
}()
client, err := connect(ctx, log, cli, chatPort)
client, err := connect(ctx, log, cli, chatPort, handle(log, deliveries))
if err != nil {
return err
}
@@ -155,9 +161,10 @@ func stopAPI(ctx context.Context, log *slog.Logger, srv *http.Server) {
}
// connect waits for the chat client to open its API on chatPort and
// connects to it.
// connects to it, passing its events to onEvent.
func connect(
ctx context.Context, log *slog.Logger, cli *simplex.CLI, chatPort int,
onEvent simplex.EventHandler,
) (*simplex.Client, error) {
ctx, cancel := context.WithTimeout(ctx, connectTimeout)
defer cancel()
@@ -165,7 +172,7 @@ func connect(
url := "ws://127.0.0.1:" + strconv.Itoa(chatPort)
for {
client, err := simplex.Dial(ctx, url, log, handle(log))
client, err := simplex.Dial(ctx, url, log, onEvent)
if err == nil {
return client, nil
}
@@ -228,8 +235,9 @@ func setUp(
return user, nil
}
// handle answers each text message a contact sends.
func handle(log *slog.Logger) simplex.EventHandler {
// handle answers each text message a contact sends, and hands every
// message a contact sends to deliveries, for the webhooks of its chat.
func handle(log *slog.Logger, deliveries *api.Deliveries) simplex.EventHandler {
return func(c *simplex.Client, ev simplex.Event) {
switch ev.Type {
case simplex.TypeNewChatItems:
@@ -243,6 +251,9 @@ func handle(log *slog.Logger) simplex.EventHandler {
}
for _, item := range r.ChatItems {
// Never waits, so no webhook holds up the replies.
deliveries.Add(item)
msg, ok := item.Message()
if !ok {
continue