1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
}
|