summaryrefslogtreecommitdiffstats
path: root/internal/config/config.go
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 15:18:21 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 15:18:21 -0300
commit3ca1d2c4370e8e79aba3940d5c8fe115960b75a8 (patch)
treeaa964675c588b80e4f81182d34130ba2d7d984e1 /internal/config/config.go
downloadtelegram-3ca1d2c4370e8e79aba3940d5c8fe115960b75a8.tar.gz
Initial importv0.1.0
Diffstat (limited to 'internal/config/config.go')
-rw-r--r--internal/config/config.go153
1 files changed, 153 insertions, 0 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..b173dbf
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,153 @@
+// Package config loads the bot's settings from the process environment.
+//
+// The environment is the only source: there is no configuration file and no
+// command-line flag. That keeps the token out of the repository and out of the
+// image, and it is what the container platform hands the process anyway.
+package config
+
+import (
+ "errors"
+ "fmt"
+ "log/slog"
+ "os"
+ "strings"
+ "time"
+)
+
+// The environment variables read by [Load].
+const (
+ // EnvToken holds the token issued by @BotFather. Required; [Load] fails
+ // when it is missing or blank.
+ EnvToken = "TELEGRAM_BOT_TOKEN"
+
+ // EnvCepisBaseURL points the lookups at another instance of the API,
+ // typically a local one during development.
+ EnvCepisBaseURL = "CEPIS_BASE_URL"
+
+ // EnvTelegramAPIBaseURL points the bot at another Bot API host. It
+ // exists for local Bot API servers and for the test suite, which stands
+ // up a fake Telegram in-process.
+ EnvTelegramAPIBaseURL = "TELEGRAM_API_BASE_URL"
+
+ // EnvLogLevel selects the minimum level written to stdout: debug, info,
+ // warn or error.
+ EnvLogLevel = "LOG_LEVEL"
+)
+
+// ErrMissingToken is returned by [Load] when [EnvToken] is absent or blank.
+var ErrMissingToken = errors.New("config: " + EnvToken + " is not set")
+
+// Config is the fully resolved configuration of one bot process.
+//
+// Only Token, CepisBaseURL, TelegramAPIBaseURL and LogLevel come from the
+// environment. The remaining fields are tuning that has no reason to differ
+// between deployments; they are exported so the test suite can shorten the
+// waits it would otherwise sit through.
+type Config struct {
+ // Token is the @BotFather token. It is never logged: [Config.LogValue]
+ // redacts it, and the Telegram client scrubs it out of error messages.
+ Token string
+
+ // CepisBaseURL is where address lookups go.
+ CepisBaseURL string
+
+ // TelegramAPIBaseURL is the Bot API host, without a trailing slash and
+ // without the /bot<token> prefix.
+ TelegramAPIBaseURL string
+
+ // PollTimeout is the long-polling window asked of getUpdates. Telegram
+ // holds the request open for this long when there is nothing to send,
+ // which is what makes polling cheap.
+ PollTimeout time.Duration
+
+ // RequestTimeout caps a single non-polling HTTP request.
+ RequestTimeout time.Duration
+
+ // MaxAttempts is how many times a transient failure is retried, the
+ // first attempt included.
+ MaxAttempts int
+
+ // BackoffBase and BackoffMax bound the jittered wait between attempts.
+ BackoffBase time.Duration
+ BackoffMax time.Duration
+
+ // ShutdownGrace is how long an in-flight update may take to finish
+ // after a termination signal, and how long the final offset
+ // confirmation may take.
+ ShutdownGrace time.Duration
+
+ // LogLevel is the minimum level written to stdout.
+ LogLevel slog.Level
+}
+
+// Defaults returns the configuration with everything filled in except Token,
+// which has no safe default.
+func Defaults() Config {
+ return Config{
+ CepisBaseURL: "https://cep.is",
+ TelegramAPIBaseURL: "https://api.telegram.org",
+ PollTimeout: 30 * time.Second,
+ RequestTimeout: 10 * time.Second,
+ MaxAttempts: 3,
+ BackoffBase: 500 * time.Millisecond,
+ BackoffMax: 8 * time.Second,
+ ShutdownGrace: 10 * time.Second,
+ LogLevel: slog.LevelInfo,
+ }
+}
+
+// Load reads the environment on top of [Defaults].
+//
+// It fails with [ErrMissingToken] when the token is absent, before any network
+// is touched, so a misconfigured deployment dies at start-up rather than
+// looking healthy while answering nobody.
+func Load() (Config, error) {
+ cfg := Defaults()
+
+ cfg.Token = strings.TrimSpace(os.Getenv(EnvToken))
+ if cfg.Token == "" {
+ return Config{}, ErrMissingToken
+ }
+
+ if value := strings.TrimSpace(os.Getenv(EnvCepisBaseURL)); value != "" {
+ cfg.CepisBaseURL = strings.TrimRight(value, "/")
+ }
+ if value := strings.TrimSpace(os.Getenv(EnvTelegramAPIBaseURL)); value != "" {
+ cfg.TelegramAPIBaseURL = strings.TrimRight(value, "/")
+ }
+ if value := strings.TrimSpace(os.Getenv(EnvLogLevel)); value != "" {
+ level, err := parseLevel(value)
+ if err != nil {
+ return Config{}, err
+ }
+ cfg.LogLevel = level
+ }
+ return cfg, nil
+}
+
+// LogValue implements [log/slog.LogValuer] so that logging the whole
+// configuration cannot leak the token, however carelessly it is done.
+func (c Config) LogValue() slog.Value {
+ return slog.GroupValue(
+ slog.String("cepis_base_url", c.CepisBaseURL),
+ slog.String("telegram_api_base_url", c.TelegramAPIBaseURL),
+ slog.Duration("poll_timeout", c.PollTimeout),
+ slog.Int("max_attempts", c.MaxAttempts),
+ slog.String("log_level", c.LogLevel.String()),
+ slog.Bool("token_set", c.Token != ""),
+ )
+}
+
+func parseLevel(value string) (slog.Level, error) {
+ switch strings.ToLower(value) {
+ case "debug":
+ return slog.LevelDebug, nil
+ case "info":
+ return slog.LevelInfo, nil
+ case "warn", "warning":
+ return slog.LevelWarn, nil
+ case "error":
+ return slog.LevelError, nil
+ }
+ return 0, fmt.Errorf("config: %s must be debug, info, warn or error, got %q", EnvLogLevel, value)
+}