package main import ( "context" "log/slog" "os" "os/signal" "strconv" "strings" "syscall" "time" tgbot "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" "github.com/joho/godotenv" "worktimeBot/internal/bot" "worktimeBot/internal/db" ) func main() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelInfo, }))) godotenv.Load() token := os.Getenv("BOT_TOKEN") dbPath := getEnvDefault("DB_PATH", "db.sqlite3") dbStore, err := db.NewStore(dbPath) if err != nil { slog.Error("failed to open database", "path", dbPath, "error", err) os.Exit(1) } allowedUsers := parseAllowedUsers(os.Getenv("BOT_ALLOWED_USERS")) var h *bot.Handler dispatch := func(ctx context.Context, b *tgbot.Bot, update *models.Update) { if update.Message != nil { h.HandleMessage(ctx, update.Message) } if update.CallbackQuery != nil { h.HandleCallback(ctx, update.CallbackQuery) } } b, err := tgbot.New(token, tgbot.WithDefaultHandler(dispatch)) if err != nil { slog.Error("failed to create bot", "error", err) os.Exit(1) } h = bot.NewHandler(b, dbStore, allowedUsers) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() if url := os.Getenv("BOT_WEBHOOK_URL"); url != "" { startWebhook(ctx, b, h, url) } else { b.DeleteWebhook(ctx, &tgbot.DeleteWebhookParams{}) go dailyReportScheduler(ctx, h, dbStore) slog.Info("bot started", "allowed_users", len(allowedUsers), "mode", "polling", ) b.Start(ctx) } dbStore.Close() } // dailyReportScheduler runs a periodic ticker that triggers daily report checks every 30 seconds. func dailyReportScheduler(ctx context.Context, h *bot.Handler, store *db.Store) { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: checkDailyReports(ctx, h, store) } } } // checkDailyReports iterates all users and sends a daily report to those whose report time has arrived. func checkDailyReports(ctx context.Context, h *bot.Handler, store *db.Store) { users, err := store.GetAllUsers() if err != nil { slog.Error("report scheduler: get users", "error", err) return } now := time.Now().UTC() for _, u := range users { if !u.ReportEnabled || !u.IsActive { continue } loc, err := time.LoadLocation(u.Timezone) if err != nil { continue } localNow := now.In(loc) today := localNow.Format("2006-01-02") currentHHMM := localNow.Format("15:04") if currentHHMM != u.ReportTime { continue } if u.LastReportDate != nil && *u.LastReportDate == today { continue } slog.Info("sending daily report", "user_id", u.ID, "chat_id", u.ChatID, "timezone", u.Timezone, ) h.SendDailyReport(ctx, u.ID, u.ChatID) } } // parseAllowedUsers parses a comma-separated list of Telegram user IDs into a set. func parseAllowedUsers(raw string) map[int64]bool { m := make(map[int64]bool) for _, s := range strings.Split(raw, ",") { s = strings.TrimSpace(s) if s == "" { continue } id, err := strconv.ParseInt(s, 10, 64) if err == nil { m[id] = true } else { slog.Warn("invalid user ID in BOT_ALLOWED_USERS", "value", s) } } return m } // getEnvDefault returns the environment variable value or a default if unset or empty. func getEnvDefault(key, defaultValue string) string { if val := os.Getenv(key); val != "" { return val } return defaultValue }