mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-10-14 02:16:19 +00:00
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.
This commit is contained in:
1 parent
cfc8d67641
commit
e695d9b223
34 files changed
+1132
-1149
No files matched your search
@@ -2,69 +2,63 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
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/apple/request"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
)
|
||||
|
||||
// /addapple <id|appId> [country=vn] — Java AddAppleAppCommand.
|
||||
type AddAppleAppCommand struct {
|
||||
BaseCommand
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
appleScraper *apple.AppleScraper
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
scraper *apple.AppleScraper
|
||||
}
|
||||
|
||||
func NewAddAppleAppCommand(
|
||||
cfg *config.Config,
|
||||
adminRepo *repository.AdminRepository,
|
||||
groupRepo *repository.GroupRepository,
|
||||
appleScraper *apple.AppleScraper,
|
||||
) *AddAppleAppCommand {
|
||||
return &AddAppleAppCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
appleScraper: appleScraper,
|
||||
}
|
||||
func NewAddAppleAppCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository, scraper *apple.AppleScraper) *AddAppleAppCommand {
|
||||
return &AddAppleAppCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo, scraper: scraper}
|
||||
}
|
||||
|
||||
func (c *AddAppleAppCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *AddAppleAppCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !authorizeGroup(msg.Chat.ID, c.adminRepo, sender) {
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
hasGroup, err := c.adminRepo.HasGroup(groupID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to check group: %v", err)
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
if !hasGroup {
|
||||
return "This group is not registered. Please use /addgroup first."
|
||||
}
|
||||
|
||||
args := strings.Fields(message.CommandArguments())
|
||||
if len(args) == 0 {
|
||||
return "Usage: /addapple <appId> [country]\nExample: /addapple com.example.app vn"
|
||||
}
|
||||
|
||||
appID := args[0]
|
||||
country := "vn"
|
||||
if len(args) > 1 {
|
||||
if len(args) == 2 {
|
||||
country = args[1]
|
||||
}
|
||||
|
||||
// Verify app exists
|
||||
app, err := c.appleScraper.GetApp(appID, country)
|
||||
// Java: try parsing arg[0] as Long (trackId); else treat as bundleId.
|
||||
var req request.AppleAppRequest
|
||||
if trackID, err := strconv.ParseInt(args[0], 10, 64); err == nil {
|
||||
req = request.ByTrackID(trackID, country)
|
||||
} else {
|
||||
req = request.ByBundleID(args[0], country)
|
||||
}
|
||||
|
||||
resp, err := c.scraper.FetchAndCache(req)
|
||||
if err != nil || resp == nil || resp.AppID == "" {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Error when request app info")
|
||||
return
|
||||
}
|
||||
|
||||
added, err := c.groupRepo.AddAppleApp(msg.Chat.ID, resp.AppID, country)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to fetch app from store: %v", err)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.groupRepo.AddAppleApp(groupID, appID, country); err != nil {
|
||||
return fmt.Sprintf("Failed to add app: %v", err)
|
||||
if !added {
|
||||
_ = sender.SendMessage(msg.Chat.ID, fmt.Sprintf("Apple app <code>%s</code> is already added", resp.AppID))
|
||||
return
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Apple app added successfully:\n*%s*\nApp ID: %s\nCountry: %s\nScore: %.1f", app.Title, appID, country, app.Score)
|
||||
_ = sender.SendMessage(msg.Chat.ID, fmt.Sprintf("Apple app <code>%s</code>, country <b>%s</b> added successfully", resp.AppID, country))
|
||||
}
|
||||
@@ -2,69 +2,53 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/api/google"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/api/google/request"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
)
|
||||
|
||||
// /addgoogle <appId> [country=vn] — Java AddGoogleAppCommand.
|
||||
type AddGoogleAppCommand struct {
|
||||
BaseCommand
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
googleScraper *google.GoogleScraper
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
scraper *google.GoogleScraper
|
||||
}
|
||||
|
||||
func NewAddGoogleAppCommand(
|
||||
cfg *config.Config,
|
||||
adminRepo *repository.AdminRepository,
|
||||
groupRepo *repository.GroupRepository,
|
||||
googleScraper *google.GoogleScraper,
|
||||
) *AddGoogleAppCommand {
|
||||
return &AddGoogleAppCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
googleScraper: googleScraper,
|
||||
}
|
||||
func NewAddGoogleAppCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository, scraper *google.GoogleScraper) *AddGoogleAppCommand {
|
||||
return &AddGoogleAppCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo, scraper: scraper}
|
||||
}
|
||||
|
||||
func (c *AddGoogleAppCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *AddGoogleAppCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !authorizeGroup(msg.Chat.ID, c.adminRepo, sender) {
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
hasGroup, err := c.adminRepo.HasGroup(groupID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to check group: %v", err)
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
if !hasGroup {
|
||||
return "This group is not registered. Please use /addgroup first."
|
||||
}
|
||||
|
||||
args := strings.Fields(message.CommandArguments())
|
||||
if len(args) == 0 {
|
||||
return "Usage: /addgoogle <appId> [country]\nExample: /addgoogle com.example.app vn"
|
||||
}
|
||||
|
||||
appID := args[0]
|
||||
country := "vn"
|
||||
if len(args) > 1 {
|
||||
if len(args) == 2 {
|
||||
country = args[1]
|
||||
}
|
||||
|
||||
// Verify app exists
|
||||
app, err := c.googleScraper.GetApp(appID, country)
|
||||
resp, err := c.scraper.FetchAndCache(request.New(appID, country))
|
||||
if err != nil || resp == nil {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Error when request app info")
|
||||
return
|
||||
}
|
||||
added, err := c.groupRepo.AddGoogleApp(msg.Chat.ID, appID, country)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to fetch app from store: %v", err)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.groupRepo.AddGoogleApp(groupID, appID, country); err != nil {
|
||||
return fmt.Sprintf("Failed to add app: %v", err)
|
||||
if !added {
|
||||
_ = sender.SendMessage(msg.Chat.ID, fmt.Sprintf("Google app <code>%s</code> is already added", appID))
|
||||
return
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Google app added successfully:\n*%s*\nApp ID: %s\nCountry: %s\nScore: %.1f", app.Title, appID, country, app.Score)
|
||||
_ = sender.SendMessage(msg.Chat.ID, fmt.Sprintf("Google app <code>%s</code>, country <b>%s</b> added successfully", appID, country))
|
||||
}
|
||||
@@ -1,36 +1,55 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
)
|
||||
|
||||
// /addgroup [groupId] — Java AddGroupCommand. Admin-only.
|
||||
type AddGroupCommand struct {
|
||||
BaseCommand
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
}
|
||||
|
||||
func NewAddGroupCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository) *AddGroupCommand {
|
||||
return &AddGroupCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
}
|
||||
return &AddGroupCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo}
|
||||
}
|
||||
|
||||
func (c *AddGroupCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *AddGroupCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !requireAdminUser(msg.From.ID, msg.Chat.ID, c.cfg, sender) {
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
if err := c.adminRepo.AddGroup(groupID); err != nil {
|
||||
return fmt.Sprintf("Failed to add group: %v", err)
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) > 1 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Group %d has been added successfully.", groupID)
|
||||
groupID := msg.Chat.ID
|
||||
if len(args) == 1 {
|
||||
parsed, err := strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
groupID = parsed
|
||||
}
|
||||
added, err := c.adminRepo.AddGroup(groupID)
|
||||
if err != nil {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
if !added {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Group is already added")
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = c.groupRepo.Init(ctx, groupID)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Group added successfully")
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package command
|
||||
|
||||
import "strings"
|
||||
|
||||
// splitArgs mirrors Java BotCommand argument parsing: split on whitespace,
|
||||
// drop empty tokens.
|
||||
func splitArgs(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Fields(s)
|
||||
return parts
|
||||
}
|
||||
@@ -13,153 +13,108 @@ import (
|
||||
"github.com/miti99/store-scraper-bot-go/internal/model"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/util"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// /checkapp — Java CheckAppCommand. Reports update status per app, per store.
|
||||
type CheckAppCommand struct {
|
||||
BaseCommand
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
appleScraper *apple.AppleScraper
|
||||
googleScraper *google.GoogleScraper
|
||||
}
|
||||
|
||||
func NewCheckAppCommand(
|
||||
cfg *config.Config,
|
||||
adminRepo *repository.AdminRepository,
|
||||
groupRepo *repository.GroupRepository,
|
||||
appleScraper *apple.AppleScraper,
|
||||
googleScraper *google.GoogleScraper,
|
||||
) *CheckAppCommand {
|
||||
return &CheckAppCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
appleScraper: appleScraper,
|
||||
googleScraper: googleScraper,
|
||||
}
|
||||
func NewCheckAppCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository, a *apple.AppleScraper, g *google.GoogleScraper) *CheckAppCommand {
|
||||
return &CheckAppCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo, appleScraper: a, googleScraper: g}
|
||||
}
|
||||
|
||||
func (c *CheckAppCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *CheckAppCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !authorizeGroup(msg.Chat.ID, c.adminRepo, sender) {
|
||||
return
|
||||
}
|
||||
if len(splitArgs(msg.CommandArguments())) != 0 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
hasGroup, err := c.adminRepo.HasGroup(groupID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to check group: %v", err)
|
||||
}
|
||||
if !hasGroup {
|
||||
return "This group is not registered."
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
group, err := c.groupRepo.Get(ctx, groupID)
|
||||
group, err := c.groupRepo.Get(ctx, msg.Chat.ID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to get group: %v", err)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
threshold := c.cfg.NumDaysWarningNotUpdated
|
||||
|
||||
if len(group.AppleApps) == 0 && len(group.GoogleApps) == 0 {
|
||||
return "No apps in this group."
|
||||
}
|
||||
|
||||
nonUpdatedApps := make([]model.NonUpdatedApp, 0)
|
||||
now := time.Now().In(c.cfg.VietnamLocation)
|
||||
|
||||
// Check Apple apps
|
||||
for _, appInfo := range group.AppleApps {
|
||||
app, err := c.appleScraper.GetApp(appInfo.AppID, appInfo.Country)
|
||||
if err != nil {
|
||||
c.cfg.Logger.Error("Failed to fetch Apple app",
|
||||
zap.String("appId", appInfo.AppID),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
updatedTime, err := time.Parse(time.RFC3339, app.Updated)
|
||||
if err != nil {
|
||||
c.cfg.Logger.Error("Failed to parse update time",
|
||||
zap.String("appId", appInfo.AppID),
|
||||
zap.String("updated", app.Updated),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
daysSinceUpdate := int(now.Sub(updatedTime).Hours() / 24)
|
||||
if daysSinceUpdate > c.cfg.NumDaysWarningNotUpdated {
|
||||
nonUpdatedApps = append(nonUpdatedApps, model.NonUpdatedApp{
|
||||
AppID: appInfo.AppID,
|
||||
Title: app.Title,
|
||||
Days: daysSinceUpdate,
|
||||
Updated: app.Updated[:10], // Just the date part
|
||||
Score: app.Score,
|
||||
Reviews: app.Reviews,
|
||||
Ratings: app.Ratings,
|
||||
IsApple: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check Google apps
|
||||
for _, appInfo := range group.GoogleApps {
|
||||
app, err := c.googleScraper.GetApp(appInfo.AppID, appInfo.Country)
|
||||
if err != nil {
|
||||
c.cfg.Logger.Error("Failed to fetch Google app",
|
||||
zap.String("appId", appInfo.AppID),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
updatedTime := time.UnixMilli(app.Updated)
|
||||
daysSinceUpdate := int(now.Sub(updatedTime).Hours() / 24)
|
||||
|
||||
if daysSinceUpdate > c.cfg.NumDaysWarningNotUpdated {
|
||||
nonUpdatedApps = append(nonUpdatedApps, model.NonUpdatedApp{
|
||||
AppID: appInfo.AppID,
|
||||
Title: app.Title,
|
||||
Days: daysSinceUpdate,
|
||||
Updated: updatedTime.Format("2006-01-02"),
|
||||
Score: app.Score,
|
||||
Reviews: app.Reviews,
|
||||
Ratings: app.Ratings,
|
||||
IsApple: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(nonUpdatedApps) == 0 {
|
||||
return fmt.Sprintf("All apps are up to date (checked within %d days).", c.cfg.NumDaysWarningNotUpdated)
|
||||
}
|
||||
|
||||
// Build table
|
||||
var rows [][]string
|
||||
for _, app := range nonUpdatedApps {
|
||||
store := "Google"
|
||||
if app.IsApple {
|
||||
store = "Apple"
|
||||
}
|
||||
|
||||
rows = append(rows, []string{
|
||||
util.TruncateString(app.Title, 30),
|
||||
store,
|
||||
fmt.Sprintf("%d", app.Days),
|
||||
app.Updated,
|
||||
fmt.Sprintf("%.1f", app.Score),
|
||||
fmt.Sprintf("%v", app.Reviews),
|
||||
util.FormatNumber(app.Ratings),
|
||||
})
|
||||
}
|
||||
|
||||
headers := []string{"App", "Store", "Days", "Updated", "Score", "Reviews", "Ratings"}
|
||||
table := util.BuildTable(headers, rows)
|
||||
headers := []string{"AppId", "Updated", "Days", "OK"}
|
||||
appleRows := c.appleRows(group.AppleApps, now, threshold)
|
||||
googleRows := c.googleRows(group.GoogleApps, now, threshold)
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("*Non-Updated Apps Report*\nGroup: %d\nApps not updated in >%d days: *%d*\n\n",
|
||||
groupID, c.cfg.NumDaysWarningNotUpdated, len(nonUpdatedApps)))
|
||||
sb.WriteString(table)
|
||||
sb.WriteString("<b>Apple Apps</b>\n")
|
||||
if len(appleRows) == 0 {
|
||||
sb.WriteString("<i>(none)</i>\n")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("<pre>%s</pre>\n", util.BuildTable(headers, appleRows)))
|
||||
}
|
||||
sb.WriteString("\n<b>Google Apps</b>\n")
|
||||
if len(googleRows) == 0 {
|
||||
sb.WriteString("<i>(none)</i>\n")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("<pre>%s</pre>\n", util.BuildTable(headers, googleRows)))
|
||||
}
|
||||
_ = sender.SendMessage(msg.Chat.ID, sb.String())
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
func (c *CheckAppCommand) appleRows(apps []model.AppInfo, now time.Time, threshold int) [][]string {
|
||||
rows := make([][]string, 0, len(apps))
|
||||
for _, a := range apps {
|
||||
resp, err := c.appleScraper.GetApp(a.AppID, a.Country)
|
||||
if err != nil || resp == nil {
|
||||
rows = append(rows, []string{a.AppID, "?", "?", okMark(false)})
|
||||
continue
|
||||
}
|
||||
updated, days, ok := evalAppleUpdated(resp.Updated, now, threshold)
|
||||
rows = append(rows, []string{a.AppID, updated, fmt.Sprintf("%d", days), okMark(ok)})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (c *CheckAppCommand) googleRows(apps []model.AppInfo, now time.Time, threshold int) [][]string {
|
||||
rows := make([][]string, 0, len(apps))
|
||||
for _, a := range apps {
|
||||
resp, err := c.googleScraper.GetApp(a.AppID, a.Country)
|
||||
if err != nil || resp == nil {
|
||||
rows = append(rows, []string{a.AppID, "?", "?", okMark(false)})
|
||||
continue
|
||||
}
|
||||
updated, days, ok := evalGoogleUpdated(resp.Updated, now, threshold)
|
||||
rows = append(rows, []string{a.AppID, updated, fmt.Sprintf("%d", days), okMark(ok)})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// evalAppleUpdated parses Apple's ISO 8601 timestamp and returns (yyyy-MM-dd,
|
||||
// days since update, OK).
|
||||
func evalAppleUpdated(updated string, now time.Time, threshold int) (string, int, bool) {
|
||||
t, err := time.Parse(time.RFC3339, updated)
|
||||
if err != nil {
|
||||
return updated, 0, false
|
||||
}
|
||||
days := int(now.Sub(t).Hours() / 24)
|
||||
return t.Format("2006-01-02"), days, days <= threshold
|
||||
}
|
||||
|
||||
func evalGoogleUpdated(millis int64, now time.Time, threshold int) (string, int, bool) {
|
||||
t := time.UnixMilli(millis)
|
||||
days := int(now.Sub(t).Hours() / 24)
|
||||
return t.Format("2006-01-02"), days, days <= threshold
|
||||
}
|
||||
|
||||
func okMark(ok bool) string {
|
||||
if ok {
|
||||
return "✅"
|
||||
}
|
||||
return "❌"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -10,114 +11,90 @@ import (
|
||||
"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/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/model"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/util"
|
||||
)
|
||||
|
||||
// /checkappscore — Java CheckAppScoreCommand. Reports score + ratings count.
|
||||
// Score is rounded to 1 decimal (Java Precision.round(score, 1) parity).
|
||||
type CheckAppScoresCommand struct {
|
||||
BaseCommand
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
appleScraper *apple.AppleScraper
|
||||
googleScraper *google.GoogleScraper
|
||||
}
|
||||
|
||||
func NewCheckAppScoresCommand(
|
||||
cfg *config.Config,
|
||||
adminRepo *repository.AdminRepository,
|
||||
groupRepo *repository.GroupRepository,
|
||||
appleScraper *apple.AppleScraper,
|
||||
googleScraper *google.GoogleScraper,
|
||||
) *CheckAppScoresCommand {
|
||||
return &CheckAppScoresCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
appleScraper: appleScraper,
|
||||
googleScraper: googleScraper,
|
||||
}
|
||||
func NewCheckAppScoresCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository, a *apple.AppleScraper, g *google.GoogleScraper) *CheckAppScoresCommand {
|
||||
return &CheckAppScoresCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo, appleScraper: a, googleScraper: g}
|
||||
}
|
||||
|
||||
func (c *CheckAppScoresCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *CheckAppScoresCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !authorizeGroup(msg.Chat.ID, c.adminRepo, sender) {
|
||||
return
|
||||
}
|
||||
if len(splitArgs(msg.CommandArguments())) != 0 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
hasGroup, err := c.adminRepo.HasGroup(groupID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to check group: %v", err)
|
||||
}
|
||||
if !hasGroup {
|
||||
return "This group is not registered."
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
group, err := c.groupRepo.Get(ctx, groupID)
|
||||
group, err := c.groupRepo.Get(ctx, msg.Chat.ID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to get group: %v", err)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
if len(group.AppleApps) == 0 && len(group.GoogleApps) == 0 {
|
||||
return "No apps in this group."
|
||||
}
|
||||
|
||||
var rows [][]string
|
||||
|
||||
// Check Apple apps
|
||||
for _, appInfo := range group.AppleApps {
|
||||
app, err := c.appleScraper.GetApp(appInfo.AppID, appInfo.Country)
|
||||
if err != nil {
|
||||
rows = append(rows, []string{
|
||||
util.TruncateString(appInfo.AppID, 30),
|
||||
"Apple",
|
||||
"Error",
|
||||
"0",
|
||||
"0",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
rows = append(rows, []string{
|
||||
util.TruncateString(app.Title, 30),
|
||||
"Apple",
|
||||
fmt.Sprintf("%.1f", app.Score),
|
||||
fmt.Sprintf("%d", app.Reviews),
|
||||
util.FormatNumber(app.Ratings),
|
||||
})
|
||||
}
|
||||
|
||||
// Check Google apps
|
||||
for _, appInfo := range group.GoogleApps {
|
||||
app, err := c.googleScraper.GetApp(appInfo.AppID, appInfo.Country)
|
||||
if err != nil {
|
||||
rows = append(rows, []string{
|
||||
util.TruncateString(appInfo.AppID, 30),
|
||||
"Google",
|
||||
"Error",
|
||||
"0",
|
||||
"0",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
rows = append(rows, []string{
|
||||
util.TruncateString(app.Title, 30),
|
||||
"Google",
|
||||
fmt.Sprintf("%.1f", app.Score),
|
||||
fmt.Sprintf("%d", app.Reviews),
|
||||
util.FormatNumber(app.Ratings),
|
||||
})
|
||||
}
|
||||
|
||||
headers := []string{"App", "Store", "Score", "Reviews", "Ratings"}
|
||||
table := util.BuildTable(headers, rows)
|
||||
headers := []string{"AppId", "Score", "Ratings"}
|
||||
appleRows := c.appleScoreRows(group.AppleApps)
|
||||
googleRows := c.googleScoreRows(group.GoogleApps)
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("*App Scores Report*\nGroup: %d\n\n", groupID))
|
||||
sb.WriteString(table)
|
||||
sb.WriteString("<b>Apple Apps</b>\n")
|
||||
if len(appleRows) == 0 {
|
||||
sb.WriteString("<i>(none)</i>\n")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("<pre>%s</pre>\n", util.BuildTable(headers, appleRows)))
|
||||
}
|
||||
sb.WriteString("\n<b>Google Apps</b>\n")
|
||||
if len(googleRows) == 0 {
|
||||
sb.WriteString("<i>(none)</i>\n")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("<pre>%s</pre>\n", util.BuildTable(headers, googleRows)))
|
||||
}
|
||||
_ = sender.SendMessage(msg.Chat.ID, sb.String())
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
func (c *CheckAppScoresCommand) appleScoreRows(apps []model.AppInfo) [][]string {
|
||||
rows := make([][]string, 0, len(apps))
|
||||
for _, a := range apps {
|
||||
resp, err := c.appleScraper.GetApp(a.AppID, a.Country)
|
||||
if err != nil || resp == nil {
|
||||
rows = append(rows, []string{a.AppID, "?", "?"})
|
||||
continue
|
||||
}
|
||||
rows = append(rows, []string{a.AppID, formatScore(resp.Score), fmt.Sprintf("%d", resp.Ratings)})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (c *CheckAppScoresCommand) googleScoreRows(apps []model.AppInfo) [][]string {
|
||||
rows := make([][]string, 0, len(apps))
|
||||
for _, a := range apps {
|
||||
resp, err := c.googleScraper.GetApp(a.AppID, a.Country)
|
||||
if err != nil || resp == nil {
|
||||
rows = append(rows, []string{a.AppID, "?", "?"})
|
||||
continue
|
||||
}
|
||||
rows = append(rows, []string{a.AppID, formatScore(resp.Score), fmt.Sprintf("%d", resp.Ratings)})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// formatScore rounds to 1 decimal place (Java Precision.round(score, 1)).
|
||||
func formatScore(score float64) string {
|
||||
rounded := math.Round(score*10) / 10
|
||||
return fmt.Sprintf("%.1f", rounded)
|
||||
}
|
||||
@@ -3,22 +3,36 @@ package command
|
||||
import (
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
)
|
||||
|
||||
// Sender is what bot.Bot exposes to commands. HTML parse mode (Java parity).
|
||||
type Sender interface {
|
||||
SendMessage(chatID int64, html string) error
|
||||
SendMessageSilent(chatID int64, html string) error
|
||||
SendDocument(chatID int64, filename, body string) error
|
||||
}
|
||||
|
||||
// Command is the unit registered on the bot dispatcher.
|
||||
type Command interface {
|
||||
Execute(message *tgbotapi.Message) string
|
||||
Execute(msg *tgbotapi.Message, sender Sender)
|
||||
}
|
||||
|
||||
type BaseCommand struct {
|
||||
cfg *config.Config
|
||||
// authorizeGroup verifies the chat is in the admin's authorized group list.
|
||||
// Mirrors Java's per-command "Group is not allowed to use bot" gate.
|
||||
func authorizeGroup(chatID int64, adminRepo *repository.AdminRepository, sender Sender) bool {
|
||||
ok, err := adminRepo.HasGroup(chatID)
|
||||
if err != nil || !ok {
|
||||
_ = sender.SendMessage(chatID, "Group is not allowed to use bot")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *BaseCommand) isAdmin(userID int64) bool {
|
||||
return c.cfg.IsAdmin(userID)
|
||||
}
|
||||
|
||||
func (c *BaseCommand) requireAdmin(message *tgbotapi.Message) bool {
|
||||
if !c.isAdmin(message.From.ID) {
|
||||
// requireAdminUser checks the user is in Environment.ADMIN_IDS.
|
||||
func requireAdminUser(userID, chatID int64, cfg *config.Config, sender Sender) bool {
|
||||
if !cfg.IsAdmin(userID) {
|
||||
_ = sender.SendMessage(chatID, "You are not authorized to use this command")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -1,52 +1,39 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
)
|
||||
|
||||
// /delapple <appId> — Java DeleteAppleAppCommand.
|
||||
type DeleteAppleAppCommand struct {
|
||||
BaseCommand
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
}
|
||||
|
||||
func NewDeleteAppleAppCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository) *DeleteAppleAppCommand {
|
||||
return &DeleteAppleAppCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
}
|
||||
return &DeleteAppleAppCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo}
|
||||
}
|
||||
|
||||
func (c *DeleteAppleAppCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *DeleteAppleAppCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !authorizeGroup(msg.Chat.ID, c.adminRepo, sender) {
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
hasGroup, err := c.adminRepo.HasGroup(groupID)
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) != 1 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
removed, err := c.groupRepo.RemoveAppleApp(msg.Chat.ID, args[0])
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to check group: %v", err)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
if !hasGroup {
|
||||
return "This group is not registered."
|
||||
if !removed {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Apple app is not added")
|
||||
return
|
||||
}
|
||||
|
||||
args := strings.Fields(message.CommandArguments())
|
||||
if len(args) == 0 {
|
||||
return "Usage: /deleteapple <appId>\nExample: /deleteapple com.example.app"
|
||||
}
|
||||
|
||||
appID := args[0]
|
||||
|
||||
if err := c.groupRepo.RemoveAppleApp(groupID, appID); err != nil {
|
||||
return fmt.Sprintf("Failed to remove app: %v", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Apple app %s has been removed successfully.", appID)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Apple app deleted successfully")
|
||||
}
|
||||
@@ -1,52 +1,39 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
)
|
||||
|
||||
// /delgoogle <appId> — Java DeleteGoogleAppCommand.
|
||||
type DeleteGoogleAppCommand struct {
|
||||
BaseCommand
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
}
|
||||
|
||||
func NewDeleteGoogleAppCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository) *DeleteGoogleAppCommand {
|
||||
return &DeleteGoogleAppCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
}
|
||||
return &DeleteGoogleAppCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo}
|
||||
}
|
||||
|
||||
func (c *DeleteGoogleAppCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *DeleteGoogleAppCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !authorizeGroup(msg.Chat.ID, c.adminRepo, sender) {
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
hasGroup, err := c.adminRepo.HasGroup(groupID)
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) != 1 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
removed, err := c.groupRepo.RemoveGoogleApp(msg.Chat.ID, args[0])
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to check group: %v", err)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
if !hasGroup {
|
||||
return "This group is not registered."
|
||||
if !removed {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Google app is not added")
|
||||
return
|
||||
}
|
||||
|
||||
args := strings.Fields(message.CommandArguments())
|
||||
if len(args) == 0 {
|
||||
return "Usage: /deletegoogle <appId>\nExample: /deletegoogle com.example.app"
|
||||
}
|
||||
|
||||
appID := args[0]
|
||||
|
||||
if err := c.groupRepo.RemoveGoogleApp(groupID, appID); err != nil {
|
||||
return fmt.Sprintf("Failed to remove app: %v", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Google app %s has been removed successfully.", appID)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Google app deleted successfully")
|
||||
}
|
||||
@@ -1,46 +1,50 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
)
|
||||
|
||||
// /delgroup [groupId] — Java DeleteGroupCommand. Admin-only.
|
||||
type DeleteGroupCommand struct {
|
||||
BaseCommand
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
}
|
||||
|
||||
func NewDeleteGroupCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository) *DeleteGroupCommand {
|
||||
return &DeleteGroupCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
}
|
||||
return &DeleteGroupCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo}
|
||||
}
|
||||
|
||||
func (c *DeleteGroupCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *DeleteGroupCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !requireAdminUser(msg.From.ID, msg.Chat.ID, c.cfg, sender) {
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
|
||||
if err := c.adminRepo.RemoveGroup(groupID); err != nil {
|
||||
return fmt.Sprintf("Failed to remove group: %v", err)
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) > 1 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := c.groupRepo.Delete(ctx, groupID); err != nil {
|
||||
return fmt.Sprintf("Group removed from admin but failed to delete group data: %v", err)
|
||||
groupID := msg.Chat.ID
|
||||
if len(args) == 1 {
|
||||
parsed, err := strconv.ParseInt(args[0], 10, 64)
|
||||
if err != nil {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
groupID = parsed
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Group %d has been deleted successfully.", groupID)
|
||||
removed, err := c.adminRepo.RemoveGroup(groupID)
|
||||
if err != nil {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
if !removed {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Group is not added")
|
||||
return
|
||||
}
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Group deleted successfully")
|
||||
}
|
||||
@@ -2,48 +2,21 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
)
|
||||
|
||||
type InfoCommand struct {
|
||||
BaseCommand
|
||||
}
|
||||
// /info — Java InfoCommand. Reports the chat (group) ID.
|
||||
type InfoCommand struct{ cfg *config.Config }
|
||||
|
||||
func NewInfoCommand(cfg *config.Config) *InfoCommand {
|
||||
return &InfoCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
func NewInfoCommand(cfg *config.Config) *InfoCommand { return &InfoCommand{cfg: cfg} }
|
||||
|
||||
func (c *InfoCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) != 0 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *InfoCommand) Execute(message *tgbotapi.Message) string {
|
||||
return fmt.Sprintf(`*Store Scraper Bot - Go Edition*
|
||||
|
||||
*Version:* 1.0.0
|
||||
*Environment:* %s
|
||||
*Source Commit:* %s
|
||||
*Go Version:* %s
|
||||
*Bot Username:* @%s
|
||||
|
||||
*Commands:*
|
||||
/addgroup - Add current group to monitoring
|
||||
/deletegroup - Remove current group
|
||||
/listgroup - List all monitored groups
|
||||
/addapple <appId> [country] - Add Apple app
|
||||
/deleteapple <appId> - Remove Apple app
|
||||
/addgoogle <appId> [country] - Add Google app
|
||||
/deletegoogle <appId> - Remove Google app
|
||||
/listapp - List apps in current group
|
||||
/checkapp - Check for non-updated apps
|
||||
/checkappscores - Check app scores
|
||||
/rawapple <appId> [country] - Get raw Apple data
|
||||
/rawgoogle <appId> [country] - Get raw Google data
|
||||
/info - Show this info`,
|
||||
c.cfg.Env,
|
||||
c.cfg.SourceCommit,
|
||||
runtime.Version(),
|
||||
c.cfg.TelegramBotUsername,
|
||||
)
|
||||
_ = sender.SendMessage(msg.Chat.ID, fmt.Sprintf("Id của nhóm là <code>%d</code>\n", msg.Chat.ID))
|
||||
}
|
||||
@@ -3,71 +3,59 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/model"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/util"
|
||||
)
|
||||
|
||||
// /listapp — Java ListAppCommand. Two tables (Apple / Google) of tracked apps.
|
||||
type ListAppCommand struct {
|
||||
BaseCommand
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
groupRepo *repository.GroupRepository
|
||||
}
|
||||
|
||||
func NewListAppCommand(cfg *config.Config, adminRepo *repository.AdminRepository, groupRepo *repository.GroupRepository) *ListAppCommand {
|
||||
return &ListAppCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
groupRepo: groupRepo,
|
||||
}
|
||||
return &ListAppCommand{cfg: cfg, adminRepo: adminRepo, groupRepo: groupRepo}
|
||||
}
|
||||
|
||||
func (c *ListAppCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *ListAppCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !authorizeGroup(msg.Chat.ID, c.adminRepo, sender) {
|
||||
return
|
||||
}
|
||||
|
||||
groupID := message.Chat.ID
|
||||
hasGroup, err := c.adminRepo.HasGroup(groupID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to check group: %v", err)
|
||||
if len(splitArgs(msg.CommandArguments())) != 0 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
if !hasGroup {
|
||||
return "This group is not registered."
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
group, err := c.groupRepo.Get(ctx, groupID)
|
||||
group, err := c.groupRepo.Get(ctx, msg.Chat.ID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to get group: %v", err)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("*Apps in this group:*\n\n")
|
||||
sb.WriteString("<b>Apple Apps</b>\n")
|
||||
sb.WriteString(formatAppTable(group.AppleApps))
|
||||
sb.WriteString("\n<b>Google Apps</b>\n")
|
||||
sb.WriteString(formatAppTable(group.GoogleApps))
|
||||
_ = sender.SendMessage(msg.Chat.ID, sb.String())
|
||||
}
|
||||
|
||||
if len(group.AppleApps) > 0 {
|
||||
sb.WriteString("*Apple Apps:*\n")
|
||||
for i, app := range group.AppleApps {
|
||||
sb.WriteString(fmt.Sprintf("%d. %s (%s)\n", i+1, app.AppID, app.Country))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
func formatAppTable(apps []model.AppInfo) string {
|
||||
if len(apps) == 0 {
|
||||
return "<i>(none)</i>\n"
|
||||
}
|
||||
|
||||
if len(group.GoogleApps) > 0 {
|
||||
sb.WriteString("*Google Apps:*\n")
|
||||
for i, app := range group.GoogleApps {
|
||||
sb.WriteString(fmt.Sprintf("%d. %s (%s)\n", i+1, app.AppID, app.Country))
|
||||
}
|
||||
}
|
||||
|
||||
if len(group.AppleApps) == 0 && len(group.GoogleApps) == 0 {
|
||||
return "No apps in this group."
|
||||
rows := make([][]string, 0, len(apps))
|
||||
for i, a := range apps {
|
||||
rows = append(rows, []string{strconv.Itoa(i + 1), a.AppID, a.Country})
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
return fmt.Sprintf("<pre>%s</pre>\n", util.BuildTable([]string{"#", "AppId", "Country"}, rows))
|
||||
}
|
||||
@@ -9,37 +9,37 @@ import (
|
||||
"github.com/miti99/store-scraper-bot-go/internal/repository"
|
||||
)
|
||||
|
||||
// /listgroup — Java ListGroupCommand. Admin-only. Lists authorized groups.
|
||||
type ListGroupCommand struct {
|
||||
BaseCommand
|
||||
cfg *config.Config
|
||||
adminRepo *repository.AdminRepository
|
||||
}
|
||||
|
||||
func NewListGroupCommand(cfg *config.Config, adminRepo *repository.AdminRepository) *ListGroupCommand {
|
||||
return &ListGroupCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
adminRepo: adminRepo,
|
||||
}
|
||||
return &ListGroupCommand{cfg: cfg, adminRepo: adminRepo}
|
||||
}
|
||||
|
||||
func (c *ListGroupCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *ListGroupCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
if !requireAdminUser(msg.From.ID, msg.Chat.ID, c.cfg, sender) {
|
||||
return
|
||||
}
|
||||
if len(splitArgs(msg.CommandArguments())) != 0 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
groups, err := c.adminRepo.GetAllGroups()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to get groups: %v", err)
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
if len(groups) == 0 {
|
||||
return "No groups found."
|
||||
_ = sender.SendMessage(msg.Chat.ID, "No groups found")
|
||||
return
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("*Total groups: %d*\n\n", len(groups)))
|
||||
for i, groupID := range groups {
|
||||
sb.WriteString(fmt.Sprintf("%d. %d\n", i+1, groupID))
|
||||
sb.WriteString(fmt.Sprintf("<b>Authorized groups (%d):</b>\n", len(groups)))
|
||||
for i, gid := range groups {
|
||||
sb.WriteString(fmt.Sprintf("%d. <code>%d</code>\n", i+1, gid))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
_ = sender.SendMessage(msg.Chat.ID, sb.String())
|
||||
}
|
||||
@@ -1,58 +1,48 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
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/apple/request"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
)
|
||||
|
||||
// /rawappleapp <id|appId> [country=vn] — Java RawAppleAppCommand.
|
||||
// Sends the raw upstream JSON as a Telegram document attachment.
|
||||
type RawAppleAppCommand struct {
|
||||
BaseCommand
|
||||
appleScraper *apple.AppleScraper
|
||||
cfg *config.Config
|
||||
scraper *apple.AppleScraper
|
||||
}
|
||||
|
||||
func NewRawAppleAppCommand(cfg *config.Config, appleScraper *apple.AppleScraper) *RawAppleAppCommand {
|
||||
return &RawAppleAppCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
appleScraper: appleScraper,
|
||||
}
|
||||
func NewRawAppleAppCommand(cfg *config.Config, scraper *apple.AppleScraper) *RawAppleAppCommand {
|
||||
return &RawAppleAppCommand{cfg: cfg, scraper: scraper}
|
||||
}
|
||||
|
||||
func (c *RawAppleAppCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *RawAppleAppCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
args := strings.Fields(message.CommandArguments())
|
||||
if len(args) == 0 {
|
||||
return "Usage: /rawapple <appId> [country]\nExample: /rawapple com.example.app vn"
|
||||
}
|
||||
|
||||
appID := args[0]
|
||||
country := "vn"
|
||||
if len(args) > 1 {
|
||||
if len(args) == 2 {
|
||||
country = args[1]
|
||||
}
|
||||
|
||||
app, err := c.appleScraper.GetApp(appID, country)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to fetch app: %v", err)
|
||||
var req request.AppleAppRequest
|
||||
if trackID, err := strconv.ParseInt(args[0], 10, 64); err == nil {
|
||||
req = request.ByTrackID(trackID, country)
|
||||
} else {
|
||||
req = request.ByBundleID(args[0], country)
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(app, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to marshal JSON: %v", err)
|
||||
raw, err := c.scraper.RawApp(req)
|
||||
if err != nil || raw == "" {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Error when request app info")
|
||||
return
|
||||
}
|
||||
|
||||
// Telegram has a message size limit, so we might need to truncate
|
||||
jsonStr := string(jsonData)
|
||||
if len(jsonStr) > 4000 {
|
||||
jsonStr = jsonStr[:4000] + "\n...(truncated)"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("```json\n%s\n```", jsonStr)
|
||||
_ = sender.SendDocument(msg.Chat.ID, fmt.Sprintf("%s.json", args[0]), raw)
|
||||
}
|
||||
@@ -1,58 +1,40 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/api/google"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/api/google/request"
|
||||
"github.com/miti99/store-scraper-bot-go/internal/config"
|
||||
)
|
||||
|
||||
// /rawgoogleapp <appId> [country=vn] — Java RawGoogleAppCommand.
|
||||
// Sends raw upstream JSON as a Telegram document.
|
||||
type RawGoogleAppCommand struct {
|
||||
BaseCommand
|
||||
googleScraper *google.GoogleScraper
|
||||
cfg *config.Config
|
||||
scraper *google.GoogleScraper
|
||||
}
|
||||
|
||||
func NewRawGoogleAppCommand(cfg *config.Config, googleScraper *google.GoogleScraper) *RawGoogleAppCommand {
|
||||
return &RawGoogleAppCommand{
|
||||
BaseCommand: BaseCommand{cfg: cfg},
|
||||
googleScraper: googleScraper,
|
||||
}
|
||||
func NewRawGoogleAppCommand(cfg *config.Config, scraper *google.GoogleScraper) *RawGoogleAppCommand {
|
||||
return &RawGoogleAppCommand{cfg: cfg, scraper: scraper}
|
||||
}
|
||||
|
||||
func (c *RawGoogleAppCommand) Execute(message *tgbotapi.Message) string {
|
||||
if !c.requireAdmin(message) {
|
||||
return "You are not authorized to use this command."
|
||||
func (c *RawGoogleAppCommand) Execute(msg *tgbotapi.Message, sender Sender) {
|
||||
args := splitArgs(msg.CommandArguments())
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Invalid arguments")
|
||||
return
|
||||
}
|
||||
|
||||
args := strings.Fields(message.CommandArguments())
|
||||
if len(args) == 0 {
|
||||
return "Usage: /rawgoogle <appId> [country]\nExample: /rawgoogle com.example.app vn"
|
||||
}
|
||||
|
||||
appID := args[0]
|
||||
country := "vn"
|
||||
if len(args) > 1 {
|
||||
if len(args) == 2 {
|
||||
country = args[1]
|
||||
}
|
||||
|
||||
app, err := c.googleScraper.GetApp(appID, country)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to fetch app: %v", err)
|
||||
raw, err := c.scraper.RawApp(request.New(appID, country))
|
||||
if err != nil || raw == "" {
|
||||
_ = sender.SendMessage(msg.Chat.ID, "Error when request app info")
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.MarshalIndent(app, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
|
||||
// Telegram has a message size limit, so we might need to truncate
|
||||
jsonStr := string(jsonData)
|
||||
if len(jsonStr) > 4000 {
|
||||
jsonStr = jsonStr[:4000] + "\n...(truncated)"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("```json\n%s\n```", jsonStr)
|
||||
_ = sender.SendDocument(msg.Chat.ID, fmt.Sprintf("%s.json", appID), raw)
|
||||
}
|
||||
Reference in new issue
Block a user