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"` // 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__TOKEN, BOT__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 }