feat: Telegram bot, notifications, profile settings, 365-day refresh tokens

This commit is contained in:
Cosmo
2026-02-06 14:11:26 +00:00
parent 9e467b0448
commit afeb3adddf
7 changed files with 448 additions and 128 deletions

View File

@@ -47,12 +47,15 @@ func (b *Bot) Start() {
updates := b.api.GetUpdatesChan(u) updates := b.api.GetUpdatesChan(u)
for update := range updates { for update := range updates {
if update.Message == nil { if update.CallbackQuery != nil {
go b.handleCallback(update.CallbackQuery)
continue continue
} }
if update.Message != nil {
go b.handleMessage(update.Message) go b.handleMessage(update.Message)
} }
}
} }
func (b *Bot) SendMessage(chatID int64, text string) error { func (b *Bot) SendMessage(chatID int64, text string) error {
@@ -66,6 +69,20 @@ func (b *Bot) SendMessage(chatID int64, text string) error {
return err 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 { func (b *Bot) GetAPI() *tgbotapi.BotAPI {
if b == nil { if b == nil {
return nil return nil

View File

@@ -16,7 +16,19 @@ func (b *Bot) handleMessage(msg *tgbotapi.Message) {
return 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": case "start":
b.handleStart(msg) b.handleStart(msg)
case "tasks": 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 := "📋 <b>Задачи на сегодня:</b>\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 <b>%s</b>\n", status, priority, task.Icon, task.Title)
if task.Description != "" {
text += fmt.Sprintf(" <i>%s</i>\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 := "🎯 <b>Привычки на сегодня:</b>\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 <b>%s</b>\n", status, habit.Icon, habit.Name)
if habit.Description != "" {
text += fmt.Sprintf(" <i>%s</i>\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) { func (b *Bot) handleStart(msg *tgbotapi.Message) {
text := fmt.Sprintf(`👋 Привет! Я бот Pulse. text := fmt.Sprintf(`👋 Привет! Я бот Pulse.
@@ -42,8 +244,6 @@ func (b *Bot) handleStart(msg *tgbotapi.Message) {
Доступные команды: Доступные команды:
/tasks — задачи на сегодня /tasks — задачи на сегодня
/habits — привычки на сегодня /habits — привычки на сегодня
/done &lt;id&gt; — отметить задачу выполненной
/check &lt;id&gt; — отметить привычку выполненной
/help — справка`, msg.Chat.ID) /help — справка`, msg.Chat.ID)
b.SendMessage(msg.Chat.ID, text) b.SendMessage(msg.Chat.ID, text)
@@ -55,8 +255,6 @@ func (b *Bot) handleHelp(msg *tgbotapi.Message) {
/start — получить твой Chat ID /start — получить твой Chat ID
/tasks — список задач на сегодня /tasks — список задач на сегодня
/habits — список привычек /habits — список привычек
/done &lt;id&gt; — отметить задачу выполненной
/check &lt;id&gt; — отметить привычку выполненной
💡 Чтобы получать уведомления, добавь свой Chat ID в настройках Pulse.` 💡 Чтобы получать уведомления, добавь свой Chat ID в настройках Pulse.`
@@ -80,30 +278,8 @@ func (b *Bot) handleTasks(msg *tgbotapi.Message) {
return return
} }
if len(tasks) == 0 { text, keyboard := b.buildTasksMessage(tasks)
b.SendMessage(msg.Chat.ID, "✨ На сегодня задач нет!") b.SendMessageWithKeyboard(msg.Chat.ID, text, keyboard)
return
}
text := "📋 <b>Задачи на сегодня:</b>\n\n"
for _, task := range tasks {
priority := ""
switch task.Priority {
case 3:
priority = "🔴 "
case 2:
priority = "🟡 "
case 1:
priority = "🔵 "
}
text += fmt.Sprintf("%s%s <b>%s</b>\n", priority, task.Icon, task.Title)
if task.Description != "" {
text += fmt.Sprintf(" <i>%s</i>\n", task.Description)
}
text += fmt.Sprintf(" /done_%d\n\n", task.ID)
}
b.SendMessage(msg.Chat.ID, text)
} }
func (b *Bot) handleHabits(msg *tgbotapi.Message) { func (b *Bot) handleHabits(msg *tgbotapi.Message) {
@@ -123,74 +299,29 @@ func (b *Bot) handleHabits(msg *tgbotapi.Message) {
return return
} }
if len(habits) == 0 { text, keyboard := b.buildHabitsMessage(habits, user.ID)
b.SendMessage(msg.Chat.ID, "✨ У тебя пока нет привычек!") b.SendMessageWithKeyboard(msg.Chat.ID, text, keyboard)
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 := "🎯 <b>Привычки на сегодня:</b>\n\n"
for _, habit := range todayHabits {
completed, _ := b.habitRepo.IsHabitCompletedToday(habit.ID, user.ID)
status := "⬜"
if completed {
status = "✅"
}
text += fmt.Sprintf("%s %s <b>%s</b>\n", status, habit.Icon, habit.Name)
if habit.Description != "" {
text += fmt.Sprintf(" <i>%s</i>\n", habit.Description)
}
if !completed {
text += fmt.Sprintf(" /check_%d\n", habit.ID)
}
text += "\n"
}
b.SendMessage(msg.Chat.ID, text)
} }
func (b *Bot) handleDone(msg *tgbotapi.Message) { func (b *Bot) handleDone(msg *tgbotapi.Message) {
args := msg.CommandArguments()
if args == "" {
b.SendMessage(msg.Chat.ID, "❌ Укажи ID задачи: /done <id>")
return
}
b.handleDoneByID(msg, args)
}
func (b *Bot) handleDoneByID(msg *tgbotapi.Message, idStr string) {
user, err := b.userRepo.GetByTelegramChatID(msg.Chat.ID) user, err := b.userRepo.GetByTelegramChatID(msg.Chat.ID)
if err != nil { if err != nil {
b.SendMessage(msg.Chat.ID, "❌ Аккаунт не найден") b.SendMessage(msg.Chat.ID, "❌ Аккаунт не найден")
return return
} }
// Parse task ID from command argument or from command like /done_123 taskID, _ := strconv.ParseInt(idStr, 10, 64)
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)
}
}
if taskID == 0 { if taskID == 0 {
b.SendMessage(msg.Chat.ID, "❌ Укажи ID задачи: /done &lt;id&gt;") b.SendMessage(msg.Chat.ID, "❌ Неверный ID задачи")
return return
} }
@@ -208,31 +339,27 @@ func (b *Bot) handleDone(msg *tgbotapi.Message) {
} }
func (b *Bot) handleCheck(msg *tgbotapi.Message) { func (b *Bot) handleCheck(msg *tgbotapi.Message) {
args := msg.CommandArguments()
if args == "" {
b.SendMessage(msg.Chat.ID, "❌ Укажи ID привычки: /check <id>")
return
}
b.handleCheckByID(msg, args)
}
func (b *Bot) handleCheckByID(msg *tgbotapi.Message, idStr string) {
user, err := b.userRepo.GetByTelegramChatID(msg.Chat.ID) user, err := b.userRepo.GetByTelegramChatID(msg.Chat.ID)
if err != nil { if err != nil {
b.SendMessage(msg.Chat.ID, "❌ Аккаунт не найден") b.SendMessage(msg.Chat.ID, "❌ Аккаунт не найден")
return return
} }
// Parse habit ID from command argument or from command like /check_123 habitID, _ := strconv.ParseInt(idStr, 10, 64)
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)
}
}
if habitID == 0 { if habitID == 0 {
b.SendMessage(msg.Chat.ID, "❌ Укажи ID привычки: /check &lt;id&gt;") b.SendMessage(msg.Chat.ID, "❌ Неверный ID привычки")
return return
} }
// Verify habit exists and belongs to user
_, err = b.habitRepo.GetByID(habitID, user.ID) _, err = b.habitRepo.GetByID(habitID, user.ID)
if err != nil { if err != nil {
if err == repository.ErrHabitNotFound { if err == repository.ErrHabitNotFound {
@@ -243,7 +370,6 @@ func (b *Bot) handleCheck(msg *tgbotapi.Message) {
return return
} }
// Create log for today
log := &model.HabitLog{ log := &model.HabitLog{
HabitID: habitID, HabitID: habitID,
UserID: user.ID, UserID: user.ID,

View File

@@ -28,11 +28,13 @@ func (h *ProfileHandler) Get(w http.ResponseWriter, r *http.Request) {
user.ProcessForJSON() user.ProcessForJSON()
// Return only profile fields
profile := map[string]interface{}{ profile := map[string]interface{}{
"username": user.Username,
"telegram_chat_id": user.TelegramChatIDValue, "telegram_chat_id": user.TelegramChatIDValue,
"notifications_enabled": user.NotificationsEnabled, "notifications_enabled": user.NotificationsEnabled,
"timezone": user.Timezone, "timezone": user.Timezone,
"morning_reminder_time": user.MorningTime,
"evening_reminder_time": user.EveningTime,
} }
writeJSON(w, profile, http.StatusOK) writeJSON(w, profile, http.StatusOK)
@@ -63,9 +65,12 @@ func (h *ProfileHandler) Update(w http.ResponseWriter, r *http.Request) {
user.ProcessForJSON() user.ProcessForJSON()
profile := map[string]interface{}{ profile := map[string]interface{}{
"username": user.Username,
"telegram_chat_id": user.TelegramChatIDValue, "telegram_chat_id": user.TelegramChatIDValue,
"notifications_enabled": user.NotificationsEnabled, "notifications_enabled": user.NotificationsEnabled,
"timezone": user.Timezone, "timezone": user.Timezone,
"morning_reminder_time": user.MorningTime,
"evening_reminder_time": user.EveningTime,
} }
writeJSON(w, profile, http.StatusOK) writeJSON(w, profile, http.StatusOK)

View File

@@ -15,6 +15,10 @@ type User struct {
TelegramChatIDValue *int64 `db:"-" json:"telegram_chat_id"` TelegramChatIDValue *int64 `db:"-" json:"telegram_chat_id"`
NotificationsEnabled bool `db:"notifications_enabled" json:"notifications_enabled"` NotificationsEnabled bool `db:"notifications_enabled" json:"notifications_enabled"`
Timezone string `db:"timezone" json:"timezone"` 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"` CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
} }
@@ -23,6 +27,16 @@ func (u *User) ProcessForJSON() {
if u.TelegramChatID.Valid { if u.TelegramChatID.Valid {
u.TelegramChatIDValue = &u.TelegramChatID.Int64 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 { type RegisterRequest struct {
@@ -41,6 +55,8 @@ type UpdateProfileRequest struct {
TelegramChatID *int64 `json:"telegram_chat_id,omitempty"` TelegramChatID *int64 `json:"telegram_chat_id,omitempty"`
NotificationsEnabled *bool `json:"notifications_enabled,omitempty"` NotificationsEnabled *bool `json:"notifications_enabled,omitempty"`
Timezone *string `json:"timezone,omitempty"` Timezone *string `json:"timezone,omitempty"`
MorningReminderTime *string `json:"morning_reminder_time,omitempty"`
EveningReminderTime *string `json:"evening_reminder_time,omitempty"`
} }
type ChangePasswordRequest struct { type ChangePasswordRequest struct {

View File

@@ -182,6 +182,16 @@ func (r *UserRepository) UpdateProfile(id int64, req *model.UpdateProfileRequest
args = append(args, *req.Timezone) args = append(args, *req.Timezone)
argIdx++ 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" query += " WHERE id = $1"
_, err := r.db.Exec(query, args...) _, err := r.db.Exec(query, args...)

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/robfig/cron/v3" "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/bot"
"github.com/daniil/homelab-api/internal/model" "github.com/daniil/homelab-api/internal/model"
"github.com/daniil/homelab-api/internal/repository" "github.com/daniil/homelab-api/internal/repository"
@@ -76,15 +77,30 @@ func (s *Scheduler) checkUserNotifications(user model.User) {
today := now.Format("2006-01-02") today := now.Format("2006-01-02")
weekday := int(now.Weekday()) weekday := int(now.Weekday())
// 1. Morning briefing at 09:00 // Get user's reminder times
if currentTime == "09:00" { 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) 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) s.checkTaskReminders(user.ID, chatID, currentTime, today)
// 3. Habit reminders // 4. Habit reminders
s.checkHabitReminders(user.ID, chatID, currentTime, weekday) s.checkHabitReminders(user.ID, chatID, currentTime, weekday)
} }
@@ -139,7 +155,125 @@ func (s *Scheduler) sendMorningBriefing(userID, chatID int64, loc *time.Location
text += fmt.Sprintf("🎯 Привычек: <b>%d</b>\n", todayHabits) text += fmt.Sprintf("🎯 Привычек: <b>%d</b>\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 := "🌙 <b>Итоги дня</b>\n\n"
// Tasks summary
if totalTasks > 0 {
text += "📋 <b>Задачи:</b>\n"
text += fmt.Sprintf(" ✅ Выполнено: %d\n", completedTasks)
text += fmt.Sprintf(" ⬜ Осталось: %d\n", incompleteTasks)
text += "\n"
}
// Habits summary
if totalHabits > 0 {
text += "🎯 <b>Привычки:</b>\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) s.bot.SendMessage(chatID, text)
} }
@@ -160,9 +294,15 @@ func (s *Scheduler) checkTaskReminders(userID, chatID int64, currentTime, today
if task.Description != "" { if task.Description != "" {
text += fmt.Sprintf("\n<i>%s</i>", task.Description) text += fmt.Sprintf("\n<i>%s</i>", 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 != "" { if habit.Description != "" {
text += fmt.Sprintf("\n<i>%s</i>", habit.Description) text += fmt.Sprintf("\n<i>%s</i>", 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)
} }
} }

View File

@@ -244,7 +244,7 @@ func (s *AuthService) generateAuthResponse(user *model.User) (*model.AuthRespons
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }