From afeb3adddfd0c8fba3bd1356533365bb88e6cb06 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Fri, 6 Feb 2026 14:11:26 +0000 Subject: [PATCH] feat: Telegram bot, notifications, profile settings, 365-day refresh tokens --- internal/bot/bot.go | 23 ++- internal/bot/handlers.go | 332 ++++++++++++++++++++++---------- internal/handler/profile.go | 7 +- internal/model/user.go | 38 ++-- internal/repository/user.go | 10 + internal/scheduler/scheduler.go | 164 +++++++++++++++- internal/service/auth.go | 2 +- 7 files changed, 448 insertions(+), 128 deletions(-) diff --git a/internal/bot/bot.go b/internal/bot/bot.go index ba2fa80..d3bf759 100644 --- a/internal/bot/bot.go +++ b/internal/bot/bot.go @@ -47,11 +47,14 @@ func (b *Bot) Start() { updates := b.api.GetUpdatesChan(u) for update := range updates { - if update.Message == nil { + if update.CallbackQuery != nil { + go b.handleCallback(update.CallbackQuery) continue } - - go b.handleMessage(update.Message) + + if update.Message != nil { + go b.handleMessage(update.Message) + } } } @@ -66,6 +69,20 @@ func (b *Bot) SendMessage(chatID int64, text string) error { return err } +func (b *Bot) SendMessageWithKeyboard(chatID int64, text string, keyboard *tgbotapi.InlineKeyboardMarkup) error { + if b == nil || b.api == nil { + return nil + } + + msg := tgbotapi.NewMessage(chatID, text) + msg.ParseMode = "HTML" + if keyboard != nil { + msg.ReplyMarkup = keyboard + } + _, err := b.api.Send(msg) + return err +} + func (b *Bot) GetAPI() *tgbotapi.BotAPI { if b == nil { return nil diff --git a/internal/bot/handlers.go b/internal/bot/handlers.go index 66c0494..f89277e 100644 --- a/internal/bot/handlers.go +++ b/internal/bot/handlers.go @@ -16,7 +16,19 @@ func (b *Bot) handleMessage(msg *tgbotapi.Message) { return } - switch msg.Command() { + cmd := msg.Command() + + // Handle commands like /done_123 or /check_123 + if strings.HasPrefix(cmd, "done_") { + b.handleDoneByID(msg, strings.TrimPrefix(cmd, "done_")) + return + } + if strings.HasPrefix(cmd, "check_") { + b.handleCheckByID(msg, strings.TrimPrefix(cmd, "check_")) + return + } + + switch cmd { case "start": b.handleStart(msg) case "tasks": @@ -32,6 +44,196 @@ func (b *Bot) handleMessage(msg *tgbotapi.Message) { } } +func (b *Bot) handleCallback(callback *tgbotapi.CallbackQuery) { + data := callback.Data + chatID := callback.Message.Chat.ID + messageID := callback.Message.MessageID + + user, err := b.userRepo.GetByTelegramChatID(chatID) + if err != nil { + b.answerCallback(callback.ID, "❌ Аккаунт не найден") + return + } + + parts := strings.Split(data, "_") + if len(parts) < 2 { + return + } + + action := parts[0] + id, _ := strconv.ParseInt(parts[1], 10, 64) + + switch action { + case "donetask": + err = b.taskRepo.Complete(id, user.ID) + if err != nil { + b.answerCallback(callback.ID, "❌ Ошибка") + return + } + b.answerCallback(callback.ID, "✅ Задача выполнена!") + b.refreshTasksMessage(chatID, messageID, user.ID) + + case "deltask": + err = b.taskRepo.Delete(id, user.ID) + if err != nil { + b.answerCallback(callback.ID, "❌ Ошибка") + return + } + b.answerCallback(callback.ID, "🗑 Задача удалена") + b.refreshTasksMessage(chatID, messageID, user.ID) + + case "checkhabit": + log := &model.HabitLog{ + HabitID: id, + UserID: user.ID, + Date: time.Now(), + Count: 1, + } + err = b.habitRepo.CreateLog(log) + if err != nil { + b.answerCallback(callback.ID, "❌ Ошибка") + return + } + b.answerCallback(callback.ID, "✅ Привычка отмечена!") + b.refreshHabitsMessage(chatID, messageID, user.ID) + } +} + +func (b *Bot) answerCallback(callbackID, text string) { + callback := tgbotapi.NewCallback(callbackID, text) + b.api.Request(callback) +} + +func (b *Bot) refreshTasksMessage(chatID int64, messageID int, userID int64) { + tasks, err := b.taskRepo.GetTodayTasks(userID) + if err != nil { + return + } + + text, keyboard := b.buildTasksMessage(tasks) + + edit := tgbotapi.NewEditMessageText(chatID, messageID, text) + edit.ParseMode = "HTML" + if keyboard != nil { + edit.ReplyMarkup = keyboard + } + b.api.Send(edit) +} + +func (b *Bot) refreshHabitsMessage(chatID int64, messageID int, userID int64) { + habits, _ := b.habitRepo.ListByUser(userID, false) + + text, keyboard := b.buildHabitsMessage(habits, userID) + + edit := tgbotapi.NewEditMessageText(chatID, messageID, text) + edit.ParseMode = "HTML" + if keyboard != nil { + edit.ReplyMarkup = keyboard + } + b.api.Send(edit) +} + +func (b *Bot) buildTasksMessage(tasks []model.Task) (string, *tgbotapi.InlineKeyboardMarkup) { + if len(tasks) == 0 { + return "✨ На сегодня задач нет!", nil + } + + text := "📋 Задачи на сегодня:\n\n" + var rows [][]tgbotapi.InlineKeyboardButton + + for _, task := range tasks { + priority := "" + switch task.Priority { + case 3: + priority = "🔴 " + case 2: + priority = "🟡 " + case 1: + priority = "🔵 " + } + + status := "⬜" + if task.CompletedAt.Valid { + status = "✅" + } + + text += fmt.Sprintf("%s %s%s %s\n", status, priority, task.Icon, task.Title) + if task.Description != "" { + text += fmt.Sprintf(" %s\n", task.Description) + } + text += "\n" + + // Add buttons only for incomplete tasks + if !task.CompletedAt.Valid { + row := []tgbotapi.InlineKeyboardButton{ + tgbotapi.NewInlineKeyboardButtonData("✅ Выполнить", fmt.Sprintf("donetask_%d", task.ID)), + tgbotapi.NewInlineKeyboardButtonData("🗑 Удалить", fmt.Sprintf("deltask_%d", task.ID)), + } + rows = append(rows, row) + } + } + + if len(rows) == 0 { + return text, nil + } + + keyboard := tgbotapi.NewInlineKeyboardMarkup(rows...) + return text, &keyboard +} + +func (b *Bot) buildHabitsMessage(habits []model.Habit, userID int64) (string, *tgbotapi.InlineKeyboardMarkup) { + today := int(time.Now().Weekday()) + var todayHabits []model.Habit + + for _, habit := range habits { + if habit.Frequency == "daily" { + todayHabits = append(todayHabits, habit) + } else { + for _, day := range habit.TargetDays { + if day == today { + todayHabits = append(todayHabits, habit) + break + } + } + } + } + + if len(todayHabits) == 0 { + return "✨ На сегодня привычек нет!", nil + } + + text := "🎯 Привычки на сегодня:\n\n" + var rows [][]tgbotapi.InlineKeyboardButton + + for _, habit := range todayHabits { + completed, _ := b.habitRepo.IsHabitCompletedToday(habit.ID, userID) + status := "⬜" + if completed { + status = "✅" + } + + text += fmt.Sprintf("%s %s %s\n", status, habit.Icon, habit.Name) + if habit.Description != "" { + text += fmt.Sprintf(" %s\n", habit.Description) + } + text += "\n" + + if !completed { + row := []tgbotapi.InlineKeyboardButton{ + tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("✅ %s", habit.Name), fmt.Sprintf("checkhabit_%d", habit.ID)), + } + rows = append(rows, row) + } + } + + if len(rows) == 0 { + return text, nil + } + + keyboard := tgbotapi.NewInlineKeyboardMarkup(rows...) + return text, &keyboard +} + func (b *Bot) handleStart(msg *tgbotapi.Message) { text := fmt.Sprintf(`👋 Привет! Я бот Pulse. @@ -42,8 +244,6 @@ func (b *Bot) handleStart(msg *tgbotapi.Message) { Доступные команды: /tasks — задачи на сегодня /habits — привычки на сегодня -/done <id> — отметить задачу выполненной -/check <id> — отметить привычку выполненной /help — справка`, msg.Chat.ID) b.SendMessage(msg.Chat.ID, text) @@ -55,8 +255,6 @@ func (b *Bot) handleHelp(msg *tgbotapi.Message) { /start — получить твой Chat ID /tasks — список задач на сегодня /habits — список привычек -/done <id> — отметить задачу выполненной -/check <id> — отметить привычку выполненной 💡 Чтобы получать уведомления, добавь свой Chat ID в настройках Pulse.` @@ -80,30 +278,8 @@ func (b *Bot) handleTasks(msg *tgbotapi.Message) { return } - if len(tasks) == 0 { - b.SendMessage(msg.Chat.ID, "✨ На сегодня задач нет!") - return - } - - text := "📋 Задачи на сегодня:\n\n" - for _, task := range tasks { - priority := "" - switch task.Priority { - case 3: - priority = "🔴 " - case 2: - priority = "🟡 " - case 1: - priority = "🔵 " - } - text += fmt.Sprintf("%s%s %s\n", priority, task.Icon, task.Title) - if task.Description != "" { - text += fmt.Sprintf(" %s\n", task.Description) - } - text += fmt.Sprintf(" /done_%d\n\n", task.ID) - } - - b.SendMessage(msg.Chat.ID, text) + text, keyboard := b.buildTasksMessage(tasks) + b.SendMessageWithKeyboard(msg.Chat.ID, text, keyboard) } func (b *Bot) handleHabits(msg *tgbotapi.Message) { @@ -123,74 +299,29 @@ func (b *Bot) handleHabits(msg *tgbotapi.Message) { return } - if len(habits) == 0 { - b.SendMessage(msg.Chat.ID, "✨ У тебя пока нет привычек!") - return - } - - // Filter habits for today - today := int(time.Now().Weekday()) - var todayHabits []model.Habit - for _, habit := range habits { - if habit.Frequency == "daily" { - todayHabits = append(todayHabits, habit) - } else { - for _, day := range habit.TargetDays { - if day == today { - todayHabits = append(todayHabits, habit) - break - } - } - } - } - - if len(todayHabits) == 0 { - b.SendMessage(msg.Chat.ID, "✨ На сегодня привычек нет!") - return - } - - text := "🎯 Привычки на сегодня:\n\n" - for _, habit := range todayHabits { - completed, _ := b.habitRepo.IsHabitCompletedToday(habit.ID, user.ID) - status := "⬜" - if completed { - status = "✅" - } - text += fmt.Sprintf("%s %s %s\n", status, habit.Icon, habit.Name) - if habit.Description != "" { - text += fmt.Sprintf(" %s\n", habit.Description) - } - if !completed { - text += fmt.Sprintf(" /check_%d\n", habit.ID) - } - text += "\n" - } - - b.SendMessage(msg.Chat.ID, text) + text, keyboard := b.buildHabitsMessage(habits, user.ID) + b.SendMessageWithKeyboard(msg.Chat.ID, text, keyboard) } func (b *Bot) handleDone(msg *tgbotapi.Message) { + args := msg.CommandArguments() + if args == "" { + b.SendMessage(msg.Chat.ID, "❌ Укажи ID задачи: /done ") + return + } + b.handleDoneByID(msg, args) +} + +func (b *Bot) handleDoneByID(msg *tgbotapi.Message, idStr string) { user, err := b.userRepo.GetByTelegramChatID(msg.Chat.ID) if err != nil { b.SendMessage(msg.Chat.ID, "❌ Аккаунт не найден") return } - // Parse task ID from command argument or from command like /done_123 - var taskID int64 - args := msg.CommandArguments() - if args != "" { - taskID, _ = strconv.ParseInt(args, 10, 64) - } else { - // Try to parse from command like /done_123 - cmd := msg.Command() - if strings.HasPrefix(cmd, "done_") { - taskID, _ = strconv.ParseInt(strings.TrimPrefix(cmd, "done_"), 10, 64) - } - } - + taskID, _ := strconv.ParseInt(idStr, 10, 64) if taskID == 0 { - b.SendMessage(msg.Chat.ID, "❌ Укажи ID задачи: /done <id>") + b.SendMessage(msg.Chat.ID, "❌ Неверный ID задачи") return } @@ -208,31 +339,27 @@ func (b *Bot) handleDone(msg *tgbotapi.Message) { } func (b *Bot) handleCheck(msg *tgbotapi.Message) { + args := msg.CommandArguments() + if args == "" { + b.SendMessage(msg.Chat.ID, "❌ Укажи ID привычки: /check ") + return + } + b.handleCheckByID(msg, args) +} + +func (b *Bot) handleCheckByID(msg *tgbotapi.Message, idStr string) { user, err := b.userRepo.GetByTelegramChatID(msg.Chat.ID) if err != nil { b.SendMessage(msg.Chat.ID, "❌ Аккаунт не найден") return } - // Parse habit ID from command argument or from command like /check_123 - var habitID int64 - args := msg.CommandArguments() - if args != "" { - habitID, _ = strconv.ParseInt(args, 10, 64) - } else { - // Try to parse from command like /check_123 - cmd := msg.Command() - if strings.HasPrefix(cmd, "check_") { - habitID, _ = strconv.ParseInt(strings.TrimPrefix(cmd, "check_"), 10, 64) - } - } - + habitID, _ := strconv.ParseInt(idStr, 10, 64) if habitID == 0 { - b.SendMessage(msg.Chat.ID, "❌ Укажи ID привычки: /check <id>") + b.SendMessage(msg.Chat.ID, "❌ Неверный ID привычки") return } - // Verify habit exists and belongs to user _, err = b.habitRepo.GetByID(habitID, user.ID) if err != nil { if err == repository.ErrHabitNotFound { @@ -243,7 +370,6 @@ func (b *Bot) handleCheck(msg *tgbotapi.Message) { return } - // Create log for today log := &model.HabitLog{ HabitID: habitID, UserID: user.ID, diff --git a/internal/handler/profile.go b/internal/handler/profile.go index f27074f..56df786 100644 --- a/internal/handler/profile.go +++ b/internal/handler/profile.go @@ -28,11 +28,13 @@ func (h *ProfileHandler) Get(w http.ResponseWriter, r *http.Request) { user.ProcessForJSON() - // Return only profile fields profile := map[string]interface{}{ + "username": user.Username, "telegram_chat_id": user.TelegramChatIDValue, "notifications_enabled": user.NotificationsEnabled, "timezone": user.Timezone, + "morning_reminder_time": user.MorningTime, + "evening_reminder_time": user.EveningTime, } writeJSON(w, profile, http.StatusOK) @@ -63,9 +65,12 @@ func (h *ProfileHandler) Update(w http.ResponseWriter, r *http.Request) { user.ProcessForJSON() profile := map[string]interface{}{ + "username": user.Username, "telegram_chat_id": user.TelegramChatIDValue, "notifications_enabled": user.NotificationsEnabled, "timezone": user.Timezone, + "morning_reminder_time": user.MorningTime, + "evening_reminder_time": user.EveningTime, } writeJSON(w, profile, http.StatusOK) diff --git a/internal/model/user.go b/internal/model/user.go index 4227a95..53af0ae 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -6,23 +6,37 @@ import ( ) type User struct { - ID int64 `db:"id" json:"id"` - Email string `db:"email" json:"email"` - Username string `db:"username" json:"username"` - PasswordHash string `db:"password_hash" json:"-"` - EmailVerified bool `db:"email_verified" json:"email_verified"` - TelegramChatID sql.NullInt64 `db:"telegram_chat_id" json:"-"` - TelegramChatIDValue *int64 `db:"-" json:"telegram_chat_id"` - NotificationsEnabled bool `db:"notifications_enabled" json:"notifications_enabled"` - Timezone string `db:"timezone" json:"timezone"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID int64 `db:"id" json:"id"` + Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` + PasswordHash string `db:"password_hash" json:"-"` + EmailVerified bool `db:"email_verified" json:"email_verified"` + TelegramChatID sql.NullInt64 `db:"telegram_chat_id" json:"-"` + TelegramChatIDValue *int64 `db:"-" json:"telegram_chat_id"` + NotificationsEnabled bool `db:"notifications_enabled" json:"notifications_enabled"` + Timezone string `db:"timezone" json:"timezone"` + MorningReminderTime sql.NullString `db:"morning_reminder_time" json:"-"` + EveningReminderTime sql.NullString `db:"evening_reminder_time" json:"-"` + MorningTime string `db:"-" json:"morning_reminder_time"` + EveningTime string `db:"-" json:"evening_reminder_time"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } func (u *User) ProcessForJSON() { if u.TelegramChatID.Valid { u.TelegramChatIDValue = &u.TelegramChatID.Int64 } + if u.MorningReminderTime.Valid { + u.MorningTime = u.MorningReminderTime.String[:5] // "09:00:00" -> "09:00" + } else { + u.MorningTime = "09:00" + } + if u.EveningReminderTime.Valid { + u.EveningTime = u.EveningReminderTime.String[:5] + } else { + u.EveningTime = "21:00" + } } type RegisterRequest struct { @@ -41,6 +55,8 @@ type UpdateProfileRequest struct { TelegramChatID *int64 `json:"telegram_chat_id,omitempty"` NotificationsEnabled *bool `json:"notifications_enabled,omitempty"` Timezone *string `json:"timezone,omitempty"` + MorningReminderTime *string `json:"morning_reminder_time,omitempty"` + EveningReminderTime *string `json:"evening_reminder_time,omitempty"` } type ChangePasswordRequest struct { diff --git a/internal/repository/user.go b/internal/repository/user.go index 38cfec6..fa65d03 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -182,6 +182,16 @@ func (r *UserRepository) UpdateProfile(id int64, req *model.UpdateProfileRequest args = append(args, *req.Timezone) argIdx++ } + if req.MorningReminderTime != nil { + query += fmt.Sprintf(", morning_reminder_time = $%d", argIdx) + args = append(args, *req.MorningReminderTime) + argIdx++ + } + if req.EveningReminderTime != nil { + query += fmt.Sprintf(", evening_reminder_time = $%d", argIdx) + args = append(args, *req.EveningReminderTime) + argIdx++ + } query += " WHERE id = $1" _, err := r.db.Exec(query, args...) diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 1f3ac0d..bead38b 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -6,6 +6,7 @@ import ( "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" @@ -76,15 +77,30 @@ func (s *Scheduler) checkUserNotifications(user model.User) { today := now.Format("2006-01-02") weekday := int(now.Weekday()) - // 1. Morning briefing at 09:00 - if currentTime == "09:00" { + // 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. Task reminders + // 2. Evening summary + if currentTime == eveningTime { + s.sendEveningSummary(user.ID, chatID, loc) + } + + // 3. Task reminders s.checkTaskReminders(user.ID, chatID, currentTime, today) - // 3. Habit reminders + // 4. Habit reminders s.checkHabitReminders(user.ID, chatID, currentTime, weekday) } @@ -139,7 +155,125 @@ func (s *Scheduler) sendMorningBriefing(userID, chatID int64, loc *time.Location text += fmt.Sprintf("🎯 Привычек: %d\n", todayHabits) } - text += "\n/tasks — посмотреть задачи\n/habits — посмотреть привычки" + 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) } @@ -160,9 +294,15 @@ func (s *Scheduler) checkTaskReminders(userID, chatID int64, currentTime, today if task.Description != "" { text += fmt.Sprintf("\n%s", task.Description) } - text += fmt.Sprintf("\n\n/done_%d — отметить выполненной", task.ID) - s.bot.SendMessage(chatID, text) + 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) } } @@ -188,8 +328,14 @@ func (s *Scheduler) checkHabitReminders(userID, chatID int64, currentTime string if habit.Description != "" { text += fmt.Sprintf("\n%s", habit.Description) } - text += fmt.Sprintf("\n\n/check_%d — отметить выполненной", habit.ID) - s.bot.SendMessage(chatID, text) + 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) } } diff --git a/internal/service/auth.go b/internal/service/auth.go index 329ed2b..d5dece1 100644 --- a/internal/service/auth.go +++ b/internal/service/auth.go @@ -244,7 +244,7 @@ func (s *AuthService) generateAuthResponse(user *model.User) (*model.AuthRespons return nil, err } - refreshToken, err := s.generateToken(user.ID, "refresh", 30*24*time.Hour) + refreshToken, err := s.generateToken(user.ID, "refresh", 365*24*time.Hour) if err != nil { return nil, err }