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 HandlerLabel string // raw 'handler:' value (echo|auth|http|…) — used by queue rows for diagnostics RequireAuth bool Async bool Handler Handler } // HandlerType returns the textual handler label for queue rows / logs. func (b Bot) HandlerType() string { return b.HandlerLabel } 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, Auth and Queue 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. `queue` may be nil (no DATABASE_URL set) ; in that case any bot // with `async: true` is a fatal config error. func NewRegistry(cfg *Config, tg *TelegramClient, auth *Auth, queue Queue) (*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) } if b.Async && queue == nil { return nil, fmt.Errorf("bot %s has async: true but DATABASE_URL is unset (no queue available)", 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, HandlerLabel: b.Handler, RequireAuth: requireAuth, Async: b.Async, 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 }