feat: Telegram bot, notifications, profile settings, 365-day refresh tokens
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 := "📋 <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) {
|
||||
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 := "📋 <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)
|
||||
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 := "🎯 <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)
|
||||
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 <id>")
|
||||
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 <id>")
|
||||
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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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("🎯 Привычек: <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)
|
||||
}
|
||||
@@ -160,9 +294,15 @@ func (s *Scheduler) checkTaskReminders(userID, chatID int64, currentTime, today
|
||||
if 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 != "" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user