Files
store-scraper-bot/internal/bot/bot.go
T
tiennm99 e695d9b223 feat: port logic from Java original to align with current Java behavior
- Switch Telegram parse mode to HTML to match Java client.
- Rename command identifiers to Java's: delgroup/delapple/delgoogle/
  checkappscore/rawappleapp/rawgoogleapp.
- Move admin singleton to "common" collection at _id="admin".
- Store group _id as string-of-int64 to match Java AbstractModel schema.
- Add `class` discriminator field on persisted models.
- Expand AppleAppResponse (42 fields) and GoogleAppResponse (63 fields,
  with nested Category/Feature) to match Java records.
- Add AppleAppRequest (track id or bundle id) / GoogleAppRequest types.
- Implement Raw* commands as Telegram document attachments instead of
  truncated text.
- Group-authorization gate (admin.HasGroup(chatId)) on non-admin commands.
- Cache TTL via millis timestamp; round score to 1 decimal; weekend silent
  send in scheduler.
- Match Java table renderer: " │ " separators, "─┼─" rows every 5 lines.
- Prefer MONGODB_CONNECTION_STRING env (Java parity); auto-extract DB
  name from URI with fallback to "store-scraper-bot".

Note: This is an AI-assisted port (not human-verified). Behavior parity
with the Java implementation has not been tested end-to-end; treat as a
starting point.
2026-04-25 20:26:25 +07:00

128 lines
4.5 KiB
Go

package bot
import (
"fmt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/miti99/store-scraper-bot-go/internal/api/apple"
"github.com/miti99/store-scraper-bot-go/internal/api/google"
"github.com/miti99/store-scraper-bot-go/internal/bot/command"
"github.com/miti99/store-scraper-bot-go/internal/config"
"github.com/miti99/store-scraper-bot-go/internal/repository"
"go.uber.org/zap"
)
// parseMode mirrors Java StoreScrapeBotTelegramClient: HTML for all messages.
const parseMode = "HTML"
type Bot struct {
api *tgbotapi.BotAPI
cfg *config.Config
commands map[string]command.Command
logger *zap.Logger
}
func NewBot(
cfg *config.Config,
adminRepo *repository.AdminRepository,
groupRepo *repository.GroupRepository,
appleScraper *apple.AppleScraper,
googleScraper *google.GoogleScraper,
) (*Bot, error) {
api, err := tgbotapi.NewBotAPI(cfg.TelegramBotToken)
if err != nil {
return nil, fmt.Errorf("failed to create telegram bot: %w", err)
}
api.Debug = cfg.Env == config.Development
cfg.Logger.Info("Authorized on account", zap.String("username", api.Self.UserName))
b := &Bot{api: api, cfg: cfg, commands: map[string]command.Command{}, logger: cfg.Logger}
// Java command identifiers (StoreScrapeBot constructor) — keep these strings
// matching exactly so existing users' muscle memory still works.
b.commands["info"] = command.NewInfoCommand(cfg)
b.commands["addgroup"] = command.NewAddGroupCommand(cfg, adminRepo, groupRepo)
b.commands["delgroup"] = command.NewDeleteGroupCommand(cfg, adminRepo, groupRepo)
b.commands["listgroup"] = command.NewListGroupCommand(cfg, adminRepo)
b.commands["addapple"] = command.NewAddAppleAppCommand(cfg, adminRepo, groupRepo, appleScraper)
b.commands["delapple"] = command.NewDeleteAppleAppCommand(cfg, adminRepo, groupRepo)
b.commands["addgoogle"] = command.NewAddGoogleAppCommand(cfg, adminRepo, groupRepo, googleScraper)
b.commands["delgoogle"] = command.NewDeleteGoogleAppCommand(cfg, adminRepo, groupRepo)
b.commands["listapp"] = command.NewListAppCommand(cfg, adminRepo, groupRepo)
b.commands["checkapp"] = command.NewCheckAppCommand(cfg, adminRepo, groupRepo, appleScraper, googleScraper)
b.commands["checkappscore"] = command.NewCheckAppScoresCommand(cfg, adminRepo, groupRepo, appleScraper, googleScraper)
b.commands["rawappleapp"] = command.NewRawAppleAppCommand(cfg, appleScraper)
b.commands["rawgoogleapp"] = command.NewRawGoogleAppCommand(cfg, googleScraper)
return b, nil
}
func (b *Bot) Start() {
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := b.api.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil || !update.Message.IsCommand() {
continue
}
go b.handleCommand(update.Message)
}
}
func (b *Bot) handleCommand(message *tgbotapi.Message) {
defer func() {
if r := recover(); r != nil {
b.logger.Error("panic in command", zap.Any("panic", r))
_ = b.SendMessage(message.Chat.ID, "Internal server error")
}
}()
name := message.Command()
cmd, ok := b.commands[name]
if !ok {
b.logger.Debug("Unknown command", zap.String("command", name))
return
}
b.logger.Info("Executing command",
zap.String("command", name),
zap.Int64("userId", message.From.ID),
zap.Int64("chatId", message.Chat.ID))
cmd.Execute(message, b)
}
// SendMessage sends an HTML-parsed message (Java parity).
func (b *Bot) SendMessage(chatID int64, html string) error {
msg := tgbotapi.NewMessage(chatID, html)
msg.ParseMode = parseMode
msg.DisableWebPagePreview = true
_, err := b.api.Send(msg)
if err != nil {
b.logger.Warn("send message failed", zap.Int64("chatId", chatID), zap.Error(err))
}
return err
}
// SendMessageSilent sends an HTML message with notifications muted (weekend behavior).
func (b *Bot) SendMessageSilent(chatID int64, html string) error {
msg := tgbotapi.NewMessage(chatID, html)
msg.ParseMode = parseMode
msg.DisableWebPagePreview = true
msg.DisableNotification = true
_, err := b.api.Send(msg)
if err != nil {
b.logger.Warn("send silent message failed", zap.Int64("chatId", chatID), zap.Error(err))
}
return err
}
// SendDocument sends body as a file attachment with the given filename
// (used by /rawappleapp and /rawgoogleapp).
func (b *Bot) SendDocument(chatID int64, filename, body string) error {
file := tgbotapi.FileBytes{Name: filename, Bytes: []byte(body)}
doc := tgbotapi.NewDocument(chatID, file)
_, err := b.api.Send(doc)
if err != nil {
b.logger.Warn("send document failed", zap.Int64("chatId", chatID), zap.Error(err))
}
return err
}