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)
}
}