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 } func New(b *bot.Bot, userRepo *repository.UserRepository, taskRepo *repository.TaskRepository, habitRepo *repository.HabitRepository) *Scheduler { return &Scheduler{ cron: cron.New(), bot: b, userRepo: userRepo, taskRepo: taskRepo, habitRepo: habitRepo, } } 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 weekday := int(time.Now().In(loc).Weekday()) var todayHabits int for _, habit := range habits { if habit.Frequency == "daily" { todayHabits++ } else { for _, day := range habit.TargetDays { if day == weekday { todayHabits++ break } } } } 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 weekday := int(time.Now().In(loc).Weekday()) var completedHabits, incompleteHabits int var incompleteHabitNames []string for _, habit := range habits { isToday := false if habit.Frequency == "daily" { isToday = true } else { for _, day := range habit.TargetDays { if day == weekday { isToday = true break } } } if isToday { 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 } for _, habit := range habits { if habit.UserID != userID { continue } // Check if already completed today completed, _ := s.habitRepo.IsHabitCompletedToday(habit.ID, userID) if completed { 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) } }