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.
117 lines
2.9 KiB
Go
117 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
)
|
|
|
|
type Handler interface {
|
|
Handle(ctx context.Context, update Update, bot Bot) error
|
|
}
|
|
|
|
type Bot struct {
|
|
Slug string
|
|
Token string
|
|
Secret string
|
|
RequireAuth bool
|
|
Handler Handler
|
|
}
|
|
|
|
type Registry struct {
|
|
bots map[string]Bot
|
|
}
|
|
|
|
// NewRegistry builds the bot routing map from the parsed YAML config + the
|
|
// per-bot secrets (already merged into BotConfig from env). It receives the
|
|
// shared TelegramClient and Auth so per-handler instances can use them.
|
|
// `auth` may be nil (no AUTH_SECRET set) ; in that case any bot configured
|
|
// with `handler: auth` or `requireAuth: true` is a fatal config error.
|
|
func NewRegistry(cfg *Config, tg *TelegramClient, auth *Auth) (*Registry, error) {
|
|
if len(cfg.Bots) == 0 {
|
|
return nil, fmt.Errorf("no bots configured")
|
|
}
|
|
|
|
bots := make(map[string]Bot, len(cfg.Bots))
|
|
|
|
for slug, b := range cfg.Bots {
|
|
token := b.Token
|
|
secret := b.Secret
|
|
if token == "" {
|
|
return nil, fmt.Errorf("bot %s: token missing", slug)
|
|
}
|
|
if secret == "" {
|
|
return nil, fmt.Errorf("bot %s: secret missing", slug)
|
|
}
|
|
|
|
// Default requireAuth = true (secure by default). Explicit
|
|
// `requireAuth: false` is required to expose a bot publicly.
|
|
requireAuth := true
|
|
if b.RequireAuth != nil {
|
|
requireAuth = *b.RequireAuth
|
|
}
|
|
|
|
// Chicken-and-egg : the auth handler itself can't be gated, otherwise
|
|
// no one could ever authenticate. Force off and warn loudly.
|
|
if b.Handler == "auth" {
|
|
if requireAuth {
|
|
log.Printf("bot %s: handler=auth implies requireAuth=false (otherwise unreachable) — overriding", slug)
|
|
}
|
|
requireAuth = false
|
|
}
|
|
|
|
if requireAuth && auth == nil {
|
|
return nil, fmt.Errorf("bot %s has requireAuth: true (default) but AUTH_SECRET is unset; either set AUTH_SECRET or add `requireAuth: false` to the bot config", slug)
|
|
}
|
|
|
|
var h Handler
|
|
switch b.Handler {
|
|
case "echo":
|
|
h = &EchoHandler{tg: tg}
|
|
case "auth":
|
|
if auth == nil {
|
|
return nil, fmt.Errorf("bot %s uses handler=auth but AUTH_SECRET is unset", slug)
|
|
}
|
|
h = &AuthHandler{tg: tg, auth: auth}
|
|
case "http":
|
|
if b.HTTP == nil {
|
|
return nil, fmt.Errorf("bot %s uses handler=http but no `http:` config block was provided", slug)
|
|
}
|
|
hh, err := NewHTTPHandler(tg, *b.HTTP)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bot %s: %w", slug, err)
|
|
}
|
|
h = hh
|
|
case "":
|
|
return nil, fmt.Errorf("bot %s: handler missing", slug)
|
|
default:
|
|
return nil, fmt.Errorf("bot %s: unknown handler %q", slug, b.Handler)
|
|
}
|
|
|
|
bots[slug] = Bot{
|
|
Slug: slug,
|
|
Token: token,
|
|
Secret: secret,
|
|
RequireAuth: requireAuth,
|
|
Handler: h,
|
|
}
|
|
}
|
|
|
|
return &Registry{bots: bots}, nil
|
|
}
|
|
|
|
func (r *Registry) Get(slug string) (Bot, bool) {
|
|
b, ok := r.bots[slug]
|
|
return b, ok
|
|
}
|
|
|
|
func (r *Registry) Count() int { return len(r.bots) }
|
|
|
|
func (r *Registry) Slugs() []string {
|
|
out := make([]string, 0, len(r.bots))
|
|
for s := range r.bots {
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|