Files

147 lines
4.1 KiB
Go

package bot
import (
"context"
"log/slog"
"strings"
tgbot "github.com/go-telegram/bot"
tgmodels "github.com/go-telegram/bot/models"
"github.com/tiennm99/openai-status-bot/internal/mongostore"
openai "github.com/tiennm99/openai-status-bot/internal/openai"
)
type ReplySender interface {
SendText(ctx context.Context, chatID int64, threadID *int, text string) error
}
type StatusClient interface {
FetchSummary(ctx context.Context) (openai.Summary, error)
FetchIncidents(ctx context.Context) (openai.IncidentsResponse, error)
}
type Store interface {
AddSubscriber(ctx context.Context, sub mongostore.Subscriber) error
GetSubscriber(ctx context.Context, sub mongostore.Subscriber) (mongostore.Subscriber, bool, error)
RemoveSubscriber(ctx context.Context, sub mongostore.Subscriber) error
SaveTelegramOffset(ctx context.Context, offset int64) error
UpdateSubscriberSettings(ctx context.Context, sub mongostore.Subscriber, types, components []string) (bool, error)
UpdateSubscriberTypes(ctx context.Context, sub mongostore.Subscriber, types []string) (bool, error)
}
type App struct {
sender ReplySender
statusClient StatusClient
store Store
logger *slog.Logger
username string
}
type MessageContext struct {
ChatID int64
ThreadID *int
Text string
}
func New(sender ReplySender, statusClient StatusClient, store Store, logger *slog.Logger, username string) *App {
return &App{
sender: sender,
statusClient: statusClient,
store: store,
logger: logger,
username: username,
}
}
func (b *App) RegisterHandlers(telegramBot *tgbot.Bot) {
telegramBot.RegisterHandlerMatchFunc(hasMessage, b.HandleUpdate)
}
func hasMessage(update *tgmodels.Update) bool {
return update != nil && update.Message != nil
}
func (b *App) HandleUpdate(ctx context.Context, _ *tgbot.Bot, update *tgmodels.Update) {
message, ok := messageContextFromUpdate(update)
if !ok {
return
}
b.handleMessage(ctx, message)
if err := b.store.SaveTelegramOffset(ctx, update.ID+1); err != nil {
b.logger.Warn("save telegram offset", "offset", update.ID+1, "error", err)
}
}
func messageContextFromUpdate(update *tgmodels.Update) (MessageContext, bool) {
if update == nil || update.Message == nil {
return MessageContext{}, false
}
message := update.Message
var threadID *int
if message.MessageThreadID != 0 {
id := message.MessageThreadID
threadID = &id
}
return MessageContext{
ChatID: message.Chat.ID,
ThreadID: threadID,
Text: message.Text,
}, true
}
func (b *App) handleMessage(ctx context.Context, message MessageContext) {
if !strings.HasPrefix(strings.TrimSpace(message.Text), "/") {
return
}
command, fields := normalizeCommand(message.Text, b.username)
switch command {
case "":
return
case "/start":
b.subscribe(ctx, message)
case "/stop":
b.unsubscribe(ctx, message)
case "/status":
b.replyStatus(ctx, message, fields)
case "/components":
b.replyComponents(ctx, message)
case "/subscribe":
b.replySubscribe(ctx, message, fields)
case "/history":
b.replyHistory(ctx, message, parseHistoryCount(fields))
case "/uptime":
b.replyUptime(ctx, message)
case "/info":
b.replyInfo(ctx, message)
case "/help":
b.reply(ctx, message, helpText)
default:
b.reply(ctx, message, "Unknown command. Use /help.")
}
}
func (b *App) subscribe(ctx context.Context, message MessageContext) {
sub := mongostore.NewSubscriber(message.ChatID, message.ThreadID)
if err := b.store.AddSubscriber(ctx, sub); err != nil {
b.logger.Error("subscribe", "error", err)
b.reply(ctx, message, "Could not subscribe right now.")
return
}
b.reply(ctx, message, "Subscribed to OpenAI status updates. Use /subscribe to change preferences.")
}
func (b *App) unsubscribe(ctx context.Context, message MessageContext) {
sub := mongostore.NewSubscriber(message.ChatID, message.ThreadID)
if err := b.store.RemoveSubscriber(ctx, sub); err != nil {
b.logger.Error("unsubscribe", "error", err)
b.reply(ctx, message, "Could not unsubscribe right now.")
return
}
b.reply(ctx, message, "Unsubscribed from OpenAI status updates. Use /start to resubscribe.")
}