All checks were successful
Docker Build / build-and-push-image (push) Successful in 53s
The http handler POSTs the Telegram Update JSON to a configurable
internal URL and expects a JSON {text} reply, which it sends back via
sendMessage. Sync : the webhook ack waits for the upstream answer
(timeout default 5s, capped at 30s — Telegram itself closes around 60s).
For slow / unreliable backends use the Phase 3 async handlers once the
queue is in place.
YAML config :
bots:
webappbot:
handler: http
http:
url: http://webapp.webapp.svc.cluster.local:8080/telegram/update
timeout: 5s
Refs ~/.claude/plans/pour-les-notifications-on-inherited-seal.md § Phase 2.
116 lines
3.0 KiB
Go
116 lines
3.0 KiB
Go
// Phase 2 — sync HTTP forwarder. Voir
|
|
// ~/.claude/plans/pour-les-notifications-on-inherited-seal.md § Phase 2.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// HTTPHandler forwards an incoming Telegram Update to a configured internal
|
|
// service URL (HTTP POST, JSON body = the Update verbatim) and expects a
|
|
// JSON response of the shape {"text": "<reply>"}. The reply is then sent
|
|
// to the user via Bot API sendMessage.
|
|
//
|
|
// Sync : the handler blocks the webhook ack until the upstream answers or
|
|
// the timeout fires. Keep `timeout` small (≤ 5 s) since Telegram itself
|
|
// may close the webhook around 60 s. For slow / unreliable backends use
|
|
// the Phase 3 async handlers (`shell`, `script`, `ollama`) once the queue
|
|
// is in place.
|
|
type HTTPHandler struct {
|
|
tg *TelegramClient
|
|
cli *http.Client
|
|
url string
|
|
timeout time.Duration
|
|
}
|
|
|
|
const (
|
|
defaultHTTPTimeout = 5 * time.Second
|
|
maxHTTPTimeout = 30 * time.Second
|
|
)
|
|
|
|
func NewHTTPHandler(tg *TelegramClient, cfg HTTPConfig) (*HTTPHandler, error) {
|
|
url := strings.TrimSpace(cfg.URL)
|
|
if url == "" {
|
|
return nil, fmt.Errorf("http handler: url is required")
|
|
}
|
|
timeout := cfg.Timeout
|
|
if timeout <= 0 {
|
|
timeout = defaultHTTPTimeout
|
|
}
|
|
if timeout > maxHTTPTimeout {
|
|
// Cap to avoid Telegram-side webhook timeouts.
|
|
timeout = maxHTTPTimeout
|
|
}
|
|
return &HTTPHandler{
|
|
tg: tg,
|
|
cli: &http.Client{Timeout: timeout},
|
|
url: url,
|
|
timeout: timeout,
|
|
}, nil
|
|
}
|
|
|
|
type httpHandlerReply struct {
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
func (h *HTTPHandler) Handle(ctx context.Context, update Update, bot Bot) error {
|
|
chatID, hasChat := update.ChatID()
|
|
if !hasChat {
|
|
// Nothing to reply to — still forward so the upstream sees the event.
|
|
}
|
|
|
|
body, err := json.Marshal(update)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal update: %w", err)
|
|
}
|
|
|
|
reqCtx, cancel := context.WithTimeout(ctx, h.timeout)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, h.url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("build request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Bot-Slug", bot.Slug)
|
|
|
|
resp, err := h.cli.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("upstream call: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
|
|
|
if resp.StatusCode/100 != 2 {
|
|
return fmt.Errorf("upstream non-2xx (%d): %s", resp.StatusCode, truncate(string(respBody), 200))
|
|
}
|
|
|
|
// Empty body is acceptable : the upstream chose not to reply.
|
|
if len(bytes.TrimSpace(respBody)) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var reply httpHandlerReply
|
|
if err := json.Unmarshal(respBody, &reply); err != nil {
|
|
return fmt.Errorf("decode upstream reply: %w (body=%q)", err, truncate(string(respBody), 200))
|
|
}
|
|
if reply.Text == "" || !hasChat {
|
|
return nil
|
|
}
|
|
return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: reply.Text})
|
|
}
|
|
|
|
func truncate(s string, n int) string {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
return s[:n] + "…"
|
|
}
|