HTTP API: register, list and remove webhooks, kept across restarts (closes #6)
check / check (push) Successful in 1m2s
check / check (push) Successful in 1m2s
POST, GET and DELETE under /api/v1/chats/{id}/webhooks, for the chats
that GET /api/v1/chats lists. The webhooks are kept in
$DATA_DIR/webhooks.json, mode 0600, which each change replaces whole
through a temporary file in the same directory and a rename. bot.Run
reads the file before it starts the chat client: absent means none, and
a file that cannot be read aborts startup. Reading a JSON request body
moved into decodeBody, which the messages endpoint now shares. Nothing
is posted to a webhook yet.
Model: opus-5-5
This commit is contained in:
+47
-6
@@ -1,5 +1,6 @@
|
||||
// Package api is the bot's HTTP API, through which another program
|
||||
// reads the bot's chats and sends messages in them.
|
||||
// reads the bot's chats, sends messages in them and registers webhooks
|
||||
// on them.
|
||||
//
|
||||
// Every request must carry the credential, as "Authorization: Bearer
|
||||
// {credential}"; with no credential configured, every request is
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -75,6 +78,10 @@ type Params struct {
|
||||
// Token is the credential every request must carry. Empty refuses
|
||||
// every request.
|
||||
Token string
|
||||
|
||||
// Webhooks holds the webhooks registered on the chats; ReadWebhooks
|
||||
// makes it.
|
||||
Webhooks *Webhooks
|
||||
}
|
||||
|
||||
// New returns the API's server. The caller starts it with
|
||||
@@ -84,7 +91,13 @@ func New(p Params) *http.Server {
|
||||
p.Log.Warn("API_TOKEN_FILE is not set, so the API refuses every request")
|
||||
}
|
||||
|
||||
h := &handlers{log: p.Log, client: p.Client, userID: p.UserID, token: p.Token}
|
||||
h := &handlers{
|
||||
log: p.Log,
|
||||
client: p.Client,
|
||||
userID: p.UserID,
|
||||
token: p.Token,
|
||||
webhooks: p.Webhooks,
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(securityHeaders, h.authenticate,
|
||||
@@ -99,6 +112,9 @@ func New(p Params) *http.Server {
|
||||
r.Get("/chats", h.handleChats())
|
||||
r.Get("/chats/{id}/messages", h.handleMessages())
|
||||
r.Post("/chats/{id}/messages", h.handleSend())
|
||||
r.Get("/chats/{id}/webhooks", h.handleWebhooks())
|
||||
r.Post("/chats/{id}/webhooks", h.handleRegister())
|
||||
r.Delete("/chats/{id}/webhooks/{webhook_id}", h.handleRemove())
|
||||
})
|
||||
|
||||
return &http.Server{
|
||||
@@ -119,10 +135,11 @@ func New(p Params) *http.Server {
|
||||
|
||||
// handlers holds what the handlers share.
|
||||
type handlers struct {
|
||||
log *slog.Logger
|
||||
client ChatClient
|
||||
userID int64
|
||||
token string
|
||||
log *slog.Logger
|
||||
client ChatClient
|
||||
userID int64
|
||||
token string
|
||||
webhooks *Webhooks
|
||||
}
|
||||
|
||||
// authenticate lets a request through only if it carries the
|
||||
@@ -165,6 +182,30 @@ func (h *handlers) respondError(w http.ResponseWriter, status int, sentence stri
|
||||
}{sentence})
|
||||
}
|
||||
|
||||
// decodeBody decodes the request's JSON body into v. If the body is too
|
||||
// large, or is not JSON of v's shape, it answers the request itself, 413
|
||||
// or 400 naming example as the shape wanted, and returns false.
|
||||
func (h *handlers) decodeBody(
|
||||
w http.ResponseWriter, r *http.Request, v any, example string,
|
||||
) bool {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
h.respondError(w, http.StatusRequestEntityTooLarge, "the body is too large")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil || json.Unmarshal(body, v) != nil {
|
||||
h.respondError(w, http.StatusBadRequest, "the body must be JSON such as "+example)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// securityHeaders go on every response. The API returns JSON to
|
||||
// programs, so a browser may not frame, sniff, cache or refer from it,
|
||||
// nor give it the camera, microphone or location, and must reach it
|
||||
|
||||
Reference in New Issue
Block a user