Files
store-scraper-bot/internal/config/config.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

179 lines
4.2 KiB
Go

package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
)
type Environment string
const (
Development Environment = "DEVELOPMENT"
Production Environment = "PRODUCTION"
)
const DefaultDatabaseName = "store-scraper-bot"
type Config struct {
// Telegram
TelegramBotToken string
TelegramBotUsername string
// MongoDB
MongoURI string
MongoDatabase string
MongoTimeout time.Duration
// Application
Env Environment
AdminIDs []int64
CreatorID int64
SourceCommit string
// Constants
AppCacheSeconds int
NumDaysWarningNotUpdated int
ScheduleCheckAppTime string
VietnamLocation *time.Location
// Logger
Logger *zap.Logger
}
var GlobalConfig *Config
func Load() (*Config, error) {
cfg := &Config{}
cfg.TelegramBotToken = getEnv("TELEGRAM_BOT_TOKEN", "")
if cfg.TelegramBotToken == "" {
return nil, fmt.Errorf("TELEGRAM_BOT_TOKEN is required")
}
cfg.TelegramBotUsername = getEnv("TELEGRAM_BOT_USERNAME", "")
if cfg.TelegramBotUsername == "" {
return nil, fmt.Errorf("TELEGRAM_BOT_USERNAME is required")
}
// Java parity: prefer MONGODB_CONNECTION_STRING. Fall back to MONGO_URI.
cfg.MongoURI = getEnv("MONGODB_CONNECTION_STRING", getEnv("MONGO_URI", "mongodb://localhost:27017"))
cfg.MongoDatabase = getEnv("MONGO_DATABASE", "")
if cfg.MongoDatabase == "" {
cfg.MongoDatabase = databaseFromURI(cfg.MongoURI)
}
cfg.MongoTimeout = time.Duration(getEnvInt("MONGO_TIMEOUT_SECONDS", 10)) * time.Second
envStr := getEnv("ENV", "DEVELOPMENT")
if envStr == "PRODUCTION" {
cfg.Env = Production
} else {
cfg.Env = Development
}
adminIDsStr := getEnv("ADMIN_IDS", "")
if adminIDsStr == "" {
return nil, fmt.Errorf("ADMIN_IDS is required")
}
cfg.AdminIDs = parseAdminIDs(adminIDsStr)
if len(cfg.AdminIDs) == 0 {
return nil, fmt.Errorf("at least one admin ID is required")
}
cfg.CreatorID = cfg.AdminIDs[0]
cfg.SourceCommit = getEnv("SOURCE_COMMIT", "unknown")
cfg.AppCacheSeconds = getEnvInt("APP_CACHE_SECONDS", 600)
cfg.NumDaysWarningNotUpdated = getEnvInt("NUM_DAYS_WARNING_NOT_UPDATED", 30)
cfg.ScheduleCheckAppTime = getEnv("SCHEDULE_CHECK_APP_TIME", "0 7 * * *")
loc, err := time.LoadLocation("Asia/Ho_Chi_Minh")
if err != nil {
return nil, fmt.Errorf("failed to load Vietnam timezone: %w", err)
}
cfg.VietnamLocation = loc
var logger *zap.Logger
if cfg.Env == Production {
logger, err = zap.NewProduction()
} else {
logger, err = zap.NewDevelopment()
}
if err != nil {
return nil, fmt.Errorf("failed to initialize logger: %w", err)
}
cfg.Logger = logger
GlobalConfig = cfg
return cfg, nil
}
// databaseFromURI extracts the database name from a Mongo connection string,
// falling back to DefaultDatabaseName (Java behavior).
func databaseFromURI(uri string) string {
opts := options.Client().ApplyURI(uri)
if opts != nil && opts.Auth != nil && opts.Auth.AuthSource != "" {
// AuthSource is not the data DB; ignore it.
_ = opts
}
// Manual parse: scheme://...host[:port]/<dbname>?<params>
rest := uri
if idx := strings.Index(rest, "://"); idx >= 0 {
rest = rest[idx+3:]
}
slash := strings.Index(rest, "/")
if slash < 0 {
return DefaultDatabaseName
}
tail := rest[slash+1:]
if q := strings.Index(tail, "?"); q >= 0 {
tail = tail[:q]
}
tail = strings.TrimSpace(tail)
if tail == "" {
return DefaultDatabaseName
}
return tail
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intVal, err := strconv.Atoi(value); err == nil {
return intVal
}
}
return defaultValue
}
func parseAdminIDs(adminIDsStr string) []int64 {
parts := strings.Split(adminIDsStr, ",")
adminIDs := make([]int64, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if id, err := strconv.ParseInt(part, 10, 64); err == nil {
adminIDs = append(adminIDs, id)
}
}
return adminIDs
}
func (c *Config) IsAdmin(userID int64) bool {
for _, adminID := range c.AdminIDs {
if adminID == userID {
return true
}
}
return false
}