Some checks failed
Docker Build / build-and-push-image (push) Has been cancelled
Adds the async dispatch infrastructure : - Postgres pool + embedded migration (CREATE TABLE/INDEX IF NOT EXISTS gateway_jobs). Auto-applied at boot. lib/pq driver (matches webapp convention). - queue.go : Enqueue (idempotent on UNIQUE(bot_slug, update_id) — handles Telegram redelivery), Pop with FOR UPDATE SKIP LOCKED, MarkDone, MarkFailed with exponential backoff (30s → 2m → 10m → 1h → dead at 5). - worker.go : goroutine that drains the queue, dispatches via the same Handler interface as sync, schedules retries on failure, notifies the user once when a job goes to dead. - BotConfig gains `async: bool`. Registry refuses bots with async=true if DATABASE_URL is unset (queue=nil). - Server : when bot.Async, the webhook ack is immediate ; the update payload is enqueued for the worker. When DATABASE_URL is unset (current default), queue/worker stay disabled and only sync handlers (echo, http, auth) work — no breaking change to the running cluster. Refs ~/.claude/plans/pour-les-notifications-on-inherited-seal.md § Phase 2.
69 lines
2.3 KiB
Go
69 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Bots map[string]BotConfig `yaml:"bots"`
|
|
}
|
|
|
|
type BotConfig struct {
|
|
Handler string `yaml:"handler"`
|
|
// RequireAuth is *bool so "absent" is distinct from "false". When unset
|
|
// (nil) the registry treats it as true — secure by default. Explicit
|
|
// `requireAuth: false` is required to expose a bot publicly.
|
|
// Forced to false for handler=auth (chicken-and-egg).
|
|
RequireAuth *bool `yaml:"requireAuth,omitempty"`
|
|
// Async, when true, dispatches incoming Updates through the durable
|
|
// Postgres queue + worker (Phase 2b) instead of running inline. The
|
|
// webhook ack returns 200 immediately ; the worker may retry the job
|
|
// with backoff if the handler fails. Required for handlers that may
|
|
// outlive the Telegram webhook timeout (shell, script, ollama).
|
|
// Requires DATABASE_URL to be set, otherwise registry refuses to start.
|
|
Async bool `yaml:"async,omitempty"`
|
|
// HTTP is the per-bot config for the `http` handler. Required when
|
|
// handler=http. See handler_http.go.
|
|
HTTP *HTTPConfig `yaml:"http,omitempty"`
|
|
|
|
Token string `yaml:"-"`
|
|
Secret string `yaml:"-"`
|
|
}
|
|
|
|
// HTTPConfig drives the `http` handler : the gateway POSTs the Telegram
|
|
// Update JSON to URL, awaits a JSON `{text}` response within Timeout,
|
|
// then sends Text back to the user via sendMessage.
|
|
type HTTPConfig struct {
|
|
URL string `yaml:"url"`
|
|
Timeout time.Duration `yaml:"timeout,omitempty"`
|
|
}
|
|
|
|
// LoadConfig reads the YAML routing config and merges per-bot secrets pulled
|
|
// from the process environment. Per-bot env keys are derived from the bot
|
|
// slug uppercased: BOT_<UPPER_SLUG>_TOKEN, BOT_<UPPER_SLUG>_SECRET.
|
|
func LoadConfig(path string) (*Config, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read config %s: %w", path, err)
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(raw, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parse yaml: %w", err)
|
|
}
|
|
if len(cfg.Bots) == 0 {
|
|
return nil, fmt.Errorf("no bots in %s", path)
|
|
}
|
|
for slug, b := range cfg.Bots {
|
|
envSlug := strings.ToUpper(strings.ReplaceAll(slug, "-", "_"))
|
|
b.Token = os.Getenv("BOT_" + envSlug + "_TOKEN")
|
|
b.Secret = os.Getenv("BOT_" + envSlug + "_SECRET")
|
|
cfg.Bots[slug] = b
|
|
}
|
|
return &cfg, nil
|
|
}
|