Files
bot-pontos/backend/internal/bot/runner.go
2026-10-23 22:31:02 -03:00

77 lines
1.9 KiB
Go

package bot
import (
"context"
"log/slog"
"time"
"botpontos/internal/adapters/telegram"
)
// Runner faz o long polling do Telegram e responde cada mensagem.
type Runner struct {
client *telegram.Client
router *Router
log *slog.Logger
pollTimeout int
}
func NewRunner(client *telegram.Client, router *Router, log *slog.Logger, pollTimeout int) *Runner {
return &Runner{client: client, router: router, log: log, pollTimeout: pollTimeout}
}
// Run roda até o contexto ser cancelado. Erros de rede não derrubam o
// worker: o loop espera alguns segundos e tenta de novo.
func (r *Runner) Run(ctx context.Context) {
var offset int64
backoff := time.Second
r.log.Info("bot do Telegram iniciado")
for ctx.Err() == nil {
updates, err := r.client.GetUpdates(ctx, offset, r.pollTimeout)
if err != nil {
if ctx.Err() != nil {
return
}
r.log.Warn("falha no getUpdates do Telegram", "error", err, "retry_in", backoff.String())
select {
case <-ctx.Done():
return
case <-time.After(backoff):
}
if backoff < 30*time.Second {
backoff *= 2
}
continue
}
backoff = time.Second
for _, update := range updates {
offset = update.UpdateID + 1
msg := update.Message
// Só conversas privadas: o bot trata dados pessoais de jornada.
if msg == nil || msg.Text == "" || (msg.Chat.Type != "" && msg.Chat.Type != "private") {
continue
}
r.handle(ctx, msg)
}
}
}
func (r *Runner) handle(ctx context.Context, msg *telegram.Message) {
defer func() {
if v := recover(); v != nil {
r.log.Error("panic ao tratar mensagem do Telegram", "panic", v)
}
}()
handleCtx, cancel := context.WithTimeout(ctx, 45*time.Second)
defer cancel()
reply := r.router.Handle(handleCtx, msg.Chat.ID, msg.From.Name(), msg.Text)
if reply == "" {
return
}
if _, err := r.client.Send(ctx, msg.Chat.ID, reply); err != nil {
r.log.Warn("falha ao responder no Telegram", "error", err)
}
}