summaryrefslogtreecommitdiffstats
path: root/internal/app/app.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/app/app.go
downloadtelegram-507b9f5009ff8720e0ea9a43c2f6bf888fd53ddb.tar.gz
Initial importv0.1.0
Diffstat (limited to 'internal/app/app.go')
-rw-r--r--internal/app/app.go96
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
+}