Files
pulse-api/internal/bot/handlers.go
Cosmo 2a50e50771 feat(savings): Add savings module with categories, transactions, recurring plans
- Categories: regular, deposits, credits, recurring, multi-user, accounts
- Transactions: deposits and withdrawals with user tracking
- Recurring plans: monthly payment obligations per user
- Stats: overdues calculation with allocation algorithm
- Excludes is_account categories from total sums
- Documentation: docs/SAVINGS.md
2026-02-16 06:48:09 +00:00

410 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package bot
import (
"fmt"
"strconv"
"strings"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/daniil/homelab-api/internal/model"
"github.com/daniil/homelab-api/internal/repository"
)
func (b *Bot) handleMessage(msg *tgbotapi.Message) {
if !msg.IsCommand() {
return
}
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":
b.handleTasks(msg)
case "habits":
b.handleHabits(msg)
case "done":
b.handleDone(msg)
case "check":
b.handleCheck(msg)
case "help":
b.handleHelp(msg)
}
}
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]
// Handle checkhabit with optional date (checkhabit_<id> or checkhabit_<id>_yesterday)
if action == "checkhabit" {
id, _ := strconv.ParseInt(parts[1], 10, 64)
logDate := time.Now()
dateLabel := "сегодня"
if len(parts) >= 3 && parts[2] == "yesterday" {
logDate = time.Now().AddDate(0, 0, -1)
dateLabel = "вчера"
}
log := &model.HabitLog{
HabitID: id,
UserID: user.ID,
Date: logDate,
Count: 1,
}
err = b.habitRepo.CreateLog(log)
if err != nil {
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "already") {
b.answerCallback(callback.ID, "⚠️ Уже отмечено за эту дату")
} else {
b.answerCallback(callback.ID, "❌ Ошибка")
}
return
}
b.answerCallback(callback.ID, fmt.Sprintf("✅ Отмечено за %s!", dateLabel))
b.refreshHabitsMessage(chatID, messageID, user.ID)
return
}
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)
}
}
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
yesterday := time.Now().AddDate(0, 0, -1).Truncate(24 * time.Hour)
for _, habit := range todayHabits {
completedToday, _ := b.habitRepo.IsHabitCompletedToday(habit.ID, userID)
completedYesterday, _ := b.habitRepo.IsHabitCompletedOnDate(habit.ID, userID, yesterday)
status := "⬜"
if completedToday {
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"
// Build buttons row
var btnRow []tgbotapi.InlineKeyboardButton
if !completedToday {
btnRow = append(btnRow, tgbotapi.NewInlineKeyboardButtonData(
fmt.Sprintf("✅ %s", habit.Name),
fmt.Sprintf("checkhabit_%d", habit.ID),
))
}
// Add "За вчера" button if not completed yesterday
if !completedYesterday {
btnRow = append(btnRow, tgbotapi.NewInlineKeyboardButtonData(
"📅 За вчера",
fmt.Sprintf("checkhabit_%d_yesterday", habit.ID),
))
}
if len(btnRow) > 0 {
rows = append(rows, btnRow)
}
}
if len(rows) == 0 {
text += "\n✨ Всё выполнено!"
return text, nil
}
keyboard := tgbotapi.NewInlineKeyboardMarkup(rows...)
return text, &keyboard
}
func (b *Bot) handleStart(msg *tgbotapi.Message) {
text := fmt.Sprintf("👋 Привет! Я бот Pulse.\n\nТвой Chat ID: <code>%d</code>\n\nСкопируй его и вставь в настройках Pulse для получения уведомлений.\n\nДоступные команды:\n/tasks — задачи на сегодня\n/habits — привычки на сегодня\n/help — справка", msg.Chat.ID)
b.SendMessage(msg.Chat.ID, text)
}
func (b *Bot) handleHelp(msg *tgbotapi.Message) {
text := "📚 <b>Справка по командам:</b>\n\n/start — получить твой Chat ID\n/tasks — список задач на сегодня\n/habits — список привычек\n\n💡 Чтобы получать уведомления, добавь свой Chat ID в настройках Pulse."
b.SendMessage(msg.Chat.ID, text)
}
func (b *Bot) handleTasks(msg *tgbotapi.Message) {
user, err := b.userRepo.GetByTelegramChatID(msg.Chat.ID)
if err != nil {
if err == repository.ErrUserNotFound {
b.SendMessage(msg.Chat.ID, "❌ Аккаунт не найден. Добавь свой Chat ID в настройках Pulse.")
return
}
b.SendMessage(msg.Chat.ID, "❌ Ошибка при получении данных")
return
}
tasks, err := b.taskRepo.GetTodayTasks(user.ID)
if err != nil {
b.SendMessage(msg.Chat.ID, "❌ Ошибка при получении задач")
return
}
text, keyboard := b.buildTasksMessage(tasks)
b.SendMessageWithKeyboard(msg.Chat.ID, text, keyboard)
}
func (b *Bot) handleHabits(msg *tgbotapi.Message) {
user, err := b.userRepo.GetByTelegramChatID(msg.Chat.ID)
if err != nil {
if err == repository.ErrUserNotFound {
b.SendMessage(msg.Chat.ID, "❌ Аккаунт не найден. Добавь свой Chat ID в настройках Pulse.")
return
}
b.SendMessage(msg.Chat.ID, "❌ Ошибка при получении данных")
return
}
habits, err := b.habitRepo.ListByUser(user.ID, false)
if err != nil {
b.SendMessage(msg.Chat.ID, "❌ Ошибка при получении привычек")
return
}
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
}
taskID, _ := strconv.ParseInt(idStr, 10, 64)
if taskID == 0 {
b.SendMessage(msg.Chat.ID, "❌ Неверный ID задачи")
return
}
err = b.taskRepo.Complete(taskID, user.ID)
if err != nil {
if err == repository.ErrTaskNotFound {
b.SendMessage(msg.Chat.ID, "❌ Задача не найдена")
return
}
b.SendMessage(msg.Chat.ID, "❌ Ошибка при выполнении задачи")
return
}
b.SendMessage(msg.Chat.ID, "✅ Задача выполнена!")
}
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
}
habitID, _ := strconv.ParseInt(idStr, 10, 64)
if habitID == 0 {
b.SendMessage(msg.Chat.ID, "❌ Неверный ID привычки")
return
}
_, err = b.habitRepo.GetByID(habitID, user.ID)
if err != nil {
if err == repository.ErrHabitNotFound {
b.SendMessage(msg.Chat.ID, "❌ Привычка не найдена")
return
}
b.SendMessage(msg.Chat.ID, "❌ Ошибка при получении привычки")
return
}
log := &model.HabitLog{
HabitID: habitID,
UserID: user.ID,
Date: time.Now(),
Count: 1,
}
err = b.habitRepo.CreateLog(log)
if err != nil {
b.SendMessage(msg.Chat.ID, "❌ Ошибка при отметке привычки")
return
}
b.SendMessage(msg.Chat.ID, "✅ Привычка отмечена!")
}