package scheduler import ( "fmt" "log" "time" "github.com/robfig/cron/v3" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/daniil/homelab-api/internal/bot" "github.com/daniil/homelab-api/internal/model" "github.com/daniil/homelab-api/internal/repository" ) type Scheduler struct { cron *cron.Cron bot *bot.Bot userRepo *repository.UserRepository taskRepo *repository.TaskRepository habitRepo *repository.HabitRepository freezeRepo *repository.HabitFreezeRepository } func New(b *bot.Bot, userRepo *repository.UserRepository, taskRepo *repository.TaskRepository, habitRepo *repository.HabitRepository, freezeRepo *repository.HabitFreezeRepository) *Scheduler { return &Scheduler{ cron: cron.New(), bot: b, userRepo: userRepo, taskRepo: taskRepo, habitRepo: habitRepo, freezeRepo: freezeRepo, } } func (s *Scheduler) Start() { if s.bot == nil || s.bot.GetAPI() == nil { log.Println("Scheduler: bot not configured, skipping") return } // Run every minute s.cron.AddFunc("* * * * *", s.checkNotifications) s.cron.Start() log.Println("Scheduler started") } func (s *Scheduler) Stop() { s.cron.Stop() } func (s *Scheduler) checkNotifications() { users, err := s.userRepo.GetUsersWithNotifications() if err != nil { log.Printf("Scheduler: error getting users: %v", err) return } for _, user := range users { go s.checkUserNotifications(user) } } func (s *Scheduler) checkUserNotifications(user model.User) { if !user.TelegramChatID.Valid { return } chatID := user.TelegramChatID.Int64 // Get user timezone loc, err := time.LoadLocation(user.Timezone) if err != nil { loc = time.FixedZone("Europe/Moscow", 3*60*60) } now := time.Now().In(loc) currentTime := now.Format("15:04") today := now.Format("2006-01-02") weekday := int(now.Weekday()) // Get user's reminder times morningTime := "09:00" if user.MorningReminderTime.Valid && len(user.MorningReminderTime.String) >= 5 { morningTime = user.MorningReminderTime.String[:5] } eveningTime := "21:00" if user.EveningReminderTime.Valid && len(user.EveningReminderTime.String) >= 5 { eveningTime = user.EveningReminderTime.String[:5] } // 1. Morning briefing if currentTime == morningTime { s.sendMorningBriefing(user.ID, chatID, loc) } // 2. Evening summary if currentTime == eveningTime { s.sendEveningSummary(user.ID, chatID, loc) } // 3. Task reminders s.checkTaskReminders(user.ID, chatID, currentTime, today) // 4. Habit reminders s.checkHabitReminders(user.ID, chatID, currentTime, weekday) } func (s *Scheduler) sendMorningBriefing(userID, chatID int64, loc *time.Location) { tasks, err := s.taskRepo.GetTodayTasks(userID) if err != nil { log.Printf("Scheduler: error getting tasks for user %d: %v", userID, err) return } habits, err := s.habitRepo.ListByUser(userID, false) if err != nil { log.Printf("Scheduler: error getting habits for user %d: %v", userID, err) return } // Filter habits for today (excluding frozen ones) weekday := int(time.Now().In(loc).Weekday()) today := time.Now().In(loc).Truncate(24 * time.Hour) var todayHabits int for _, habit := range habits { if s.shouldShowHabitToday(habit, userID, weekday, today) { todayHabits++ } } if len(tasks) == 0 && todayHabits == 0 { return // Nothing to report } text := "☀️ Доброе утро!\n\n" if len(tasks) > 0 { text += fmt.Sprintf("📋 Задач на сегодня: %d\n", len(tasks)) for i, task := range tasks { if i >= 3 { text += fmt.Sprintf(" ... и ещё %d\n", len(tasks)-3) break } text += fmt.Sprintf(" • %s %s\n", task.Icon, task.Title) } text += "\n" } if todayHabits > 0 { text += fmt.Sprintf("🎯 Привычек: %d\n", todayHabits) } text += "\nИспользуй /tasks и /habits для просмотра" s.bot.SendMessage(chatID, text) } func (s *Scheduler) sendEveningSummary(userID, chatID int64, loc *time.Location) { // Get tasks tasks, err := s.taskRepo.GetTodayTasks(userID) if err != nil { log.Printf("Scheduler: error getting tasks for user %d: %v", userID, err) return } // Count completed/incomplete tasks var completedTasks, incompleteTasks int for _, task := range tasks { if task.CompletedAt.Valid { completedTasks++ } else { incompleteTasks++ } } // Get habits habits, err := s.habitRepo.ListByUser(userID, false) if err != nil { log.Printf("Scheduler: error getting habits for user %d: %v", userID, err) return } // Filter and count today's habits (excluding frozen ones) weekday := int(time.Now().In(loc).Weekday()) today := time.Now().In(loc).Truncate(24 * time.Hour) var completedHabits, incompleteHabits int var incompleteHabitNames []string for _, habit := range habits { if s.shouldShowHabitToday(habit, userID, weekday, today) { completed, _ := s.habitRepo.IsHabitCompletedToday(habit.ID, userID) if completed { completedHabits++ } else { incompleteHabits++ incompleteHabitNames = append(incompleteHabitNames, habit.Icon+" "+habit.Name) } } } // Don't send if nothing to report totalTasks := completedTasks + incompleteTasks totalHabits := completedHabits + incompleteHabits if totalTasks == 0 && totalHabits == 0 { return } text := "🌙 Итоги дня\n\n" // Tasks summary if totalTasks > 0 { text += "📋 Задачи:\n" text += fmt.Sprintf(" ✅ Выполнено: %d\n", completedTasks) text += fmt.Sprintf(" ⬜ Осталось: %d\n", incompleteTasks) text += "\n" } // Habits summary if totalHabits > 0 { text += "🎯 Привычки:\n" text += fmt.Sprintf(" ✅ Выполнено: %d\n", completedHabits) text += fmt.Sprintf(" ⬜ Осталось: %d\n", incompleteHabits) // Show incomplete habits if len(incompleteHabitNames) > 0 && len(incompleteHabitNames) <= 5 { text += "\n Не выполнены:\n" for _, name := range incompleteHabitNames { text += fmt.Sprintf(" • %s\n", name) } } text += "\n" } // Motivational message taskPercent := 0 if totalTasks > 0 { taskPercent = completedTasks * 100 / totalTasks } habitPercent := 0 if totalHabits > 0 { habitPercent = completedHabits * 100 / totalHabits } avgPercent := (taskPercent + habitPercent) / 2 if totalTasks == 0 { avgPercent = habitPercent } if totalHabits == 0 { avgPercent = taskPercent } if avgPercent == 100 { text += "🎉 Отличный день! Всё выполнено!" } else if avgPercent >= 75 { text += "👍 Хороший день! Почти всё сделано." } else if avgPercent >= 50 { text += "💪 Неплохо! Завтра будет лучше." } else { text += "🌱 Бывает. Завтра новый день!" } s.bot.SendMessage(chatID, text) } func (s *Scheduler) checkTaskReminders(userID, chatID int64, currentTime, today string) { tasks, err := s.taskRepo.GetTasksWithReminder(currentTime, today) if err != nil { log.Printf("Scheduler: error getting task reminders: %v", err) return } for _, task := range tasks { if task.UserID != userID { continue } text := fmt.Sprintf("⏰ Напоминание о задаче:\n\n%s %s", task.Icon, task.Title) if task.Description != "" { text += fmt.Sprintf("\n%s", task.Description) } keyboard := tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardButtonData("✅ Выполнено", fmt.Sprintf("donetask_%d", task.ID)), tgbotapi.NewInlineKeyboardButtonData("⏰ +30 мин", fmt.Sprintf("snoozetask_%d", task.ID)), ), ) s.bot.SendMessageWithKeyboard(chatID, text, &keyboard) } } func (s *Scheduler) checkHabitReminders(userID, chatID int64, currentTime string, weekday int) { habits, err := s.habitRepo.GetHabitsWithReminder(currentTime, weekday) if err != nil { log.Printf("Scheduler: error getting habit reminders: %v", err) return } today := time.Now().Truncate(24 * time.Hour) for _, habit := range habits { if habit.UserID != userID { continue } // Check if habit is frozen today frozen, err := s.freezeRepo.IsHabitFrozenOnDate(habit.ID, today) if err != nil { log.Printf("Scheduler: error checking freeze for habit %d: %v", habit.ID, err) continue } if frozen { continue } // Check if already completed today completed, _ := s.habitRepo.IsHabitCompletedToday(habit.ID, userID) if completed { continue } // For interval habits, check if it should be shown today if (habit.Frequency == "interval" || habit.Frequency == "custom") && habit.TargetCount > 0 { shouldShow, err := s.habitRepo.ShouldShowIntervalHabitToday(habit.ID, userID, habit.TargetCount, habit.StartDate) if err != nil || !shouldShow { continue } } text := fmt.Sprintf("⏰ Напоминание о привычке:\n\n%s %s", habit.Icon, habit.Name) if habit.Description != "" { text += fmt.Sprintf("\n%s", habit.Description) } keyboard := tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardButtonData("✅ Выполнено", fmt.Sprintf("checkhabit_%d", habit.ID)), tgbotapi.NewInlineKeyboardButtonData("⏰ +30 мин", fmt.Sprintf("snoozehabit_%d", habit.ID)), ), ) s.bot.SendMessageWithKeyboard(chatID, text, &keyboard) } } // shouldShowHabitToday checks if a habit should be shown today based on its frequency and freeze status func (s *Scheduler) shouldShowHabitToday(habit model.Habit, userID int64, weekday int, today time.Time) bool { // Check if habit is frozen today frozen, err := s.freezeRepo.IsHabitFrozenOnDate(habit.ID, today) if err != nil { log.Printf("Scheduler: error checking freeze for habit %d: %v", habit.ID, err) return false } if frozen { return false } if habit.Frequency == "daily" { return true } if habit.Frequency == "weekly" { for _, day := range habit.TargetDays { if day == weekday { return true } } return false } // For interval habits if (habit.Frequency == "interval" || habit.Frequency == "custom") && habit.TargetCount > 0 { shouldShow, err := s.habitRepo.ShouldShowIntervalHabitToday(habit.ID, userID, habit.TargetCount, habit.StartDate) if err != nil { return false } return shouldShow } return true }