diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 15:18:21 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 15:18:21 -0300 |
| commit | 3ca1d2c4370e8e79aba3940d5c8fe115960b75a8 (patch) | |
| tree | aa964675c588b80e4f81182d34130ba2d7d984e1 /internal/app/app.go | |
| download | telegram-3ca1d2c4370e8e79aba3940d5c8fe115960b75a8.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'internal/app/app.go')
| -rw-r--r-- | internal/app/app.go | 96 |
1 files changed, 96 insertions, 0 deletions
diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..0b78baf --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,96 @@ +// Package app wires the configuration, the cep.is client, the Telegram client +// and the bot into a single runnable unit. +// +// It exists so that main stays a thin shell around signal handling, and so the +// test suite can run the whole thing against a fake Telegram and a fake cep.is +// without going through the process environment. +package app + +import ( + "context" + "fmt" + "log/slog" + + cepis "cep.is/go" + + "cep.is/telegrambot/internal/bot" + "cep.is/telegrambot/internal/config" + "cep.is/telegrambot/internal/telegram" +) + +// App is one configured bot process. +type App struct { + poller *telegram.Poller + username string + logger *slog.Logger +} + +// New builds the application and identifies the bot. +// +// The getMe call is not a formality. It fails fast on a bad token, and it is the +// only way to learn the username the operator chose with @BotFather, which the +// help text quotes and which "/cep@username" has to match. +// +// The whole-call timeout of the cep.is client is the shutdown grace on purpose: +// handling one update is what a termination signal waits for, so a lookup must +// not be allowed to outlast that budget. +func New(ctx context.Context, cfg config.Config, logger *slog.Logger) (*App, error) { + if logger == nil { + logger = slog.Default() + } + + lookups, err := cepis.New( + cepis.WithBaseURL(cfg.CepisBaseURL), + cepis.WithTimeout(cfg.RequestTimeout), + cepis.WithMaxAttempts(cfg.MaxAttempts), + cepis.WithBackoff(cfg.BackoffBase, cfg.BackoffMax), + cepis.WithTotalTimeout(cfg.ShutdownGrace), + cepis.WithUserAgent("cepis-telegram/0.1.0 (+https://cep.is)"), + ) + if err != nil { + return nil, fmt.Errorf("app: cep.is client: %w", err) + } + + api, err := telegram.New(cfg.Token, + telegram.WithBaseURL(cfg.TelegramAPIBaseURL), + telegram.WithTimeout(cfg.RequestTimeout), + telegram.WithMaxAttempts(cfg.MaxAttempts), + telegram.WithBackoff(cfg.BackoffBase, cfg.BackoffMax), + ) + if err != nil { + return nil, fmt.Errorf("app: telegram client: %w", err) + } + + me, err := api.GetMe(ctx) + if err != nil { + return nil, fmt.Errorf("app: getMe: %w", err) + } + if me.Username == "" { + return nil, fmt.Errorf("app: getMe returned no username") + } + + handler := bot.New(lookups, api, me.Username, bot.WithLogger(logger)) + poller := telegram.NewPoller(api, handler, + telegram.WithLogger(logger), + telegram.WithPollTimeout(cfg.PollTimeout), + telegram.WithShutdownGrace(cfg.ShutdownGrace), + telegram.WithErrorBackoff(cfg.BackoffBase, cfg.BackoffMax), + ) + + return &App{poller: poller, username: me.Username, logger: logger}, nil +} + +// Username is the bot's own username, as reported by getMe. +func (a *App) Username() string { return a.username } + +// Offset is the identifier of the next update expected. It is meant for tests +// and diagnostics. +func (a *App) Offset() int64 { return a.poller.Offset() } + +// Run polls until ctx is cancelled and then shuts down cleanly. +func (a *App) Run(ctx context.Context) error { + a.logger.Info("bot started", slog.String("username", a.username)) + err := a.poller.Run(ctx) + a.logger.Info("bot stopped", slog.Int64("offset", a.poller.Offset())) + return err +} |