feat: add Telegram bot with notifications and scheduler

This commit is contained in:
Cosmo
2026-02-06 13:16:50 +00:00
parent 5a40127edd
commit 9e467b0448
19 changed files with 1007 additions and 110 deletions

74
internal/bot/bot.go Normal file
View File

@@ -0,0 +1,74 @@
package bot
import (
"log"
"sync"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/daniil/homelab-api/internal/repository"
)
type Bot struct {
api *tgbotapi.BotAPI
userRepo *repository.UserRepository
taskRepo *repository.TaskRepository
habitRepo *repository.HabitRepository
mu sync.Mutex
}
func New(token string, userRepo *repository.UserRepository, taskRepo *repository.TaskRepository, habitRepo *repository.HabitRepository) (*Bot, error) {
if token == "" {
return nil, nil
}
api, err := tgbotapi.NewBotAPI(token)
if err != nil {
return nil, err
}
log.Printf("Telegram bot authorized on account %s", api.Self.UserName)
return &Bot{
api: api,
userRepo: userRepo,
taskRepo: taskRepo,
habitRepo: habitRepo,
}, nil
}
func (b *Bot) Start() {
if b == nil || b.api == nil {
return
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := b.api.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
go b.handleMessage(update.Message)
}
}
func (b *Bot) SendMessage(chatID int64, text string) error {
if b == nil || b.api == nil {
return nil
}
msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "HTML"
_, err := b.api.Send(msg)
return err
}
func (b *Bot) GetAPI() *tgbotapi.BotAPI {
if b == nil {
return nil
}
return b.api
}

260
internal/bot/handlers.go Normal file
View File

@@ -0,0 +1,260 @@
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
}
switch msg.Command() {
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) handleStart(msg *tgbotapi.Message) {
text := fmt.Sprintf(`👋 Привет! Я бот Pulse.
Твой Chat ID: <code>%d</code>
Скопируй его и вставь в настройках Pulse для получения уведомлений.
Доступные команды:
/tasks — задачи на сегодня
/habits — привычки на сегодня
/done &lt;id&gt; — отметить задачу выполненной
/check &lt;id&gt; — отметить привычку выполненной
/help — справка`, msg.Chat.ID)
b.SendMessage(msg.Chat.ID, text)
}
func (b *Bot) handleHelp(msg *tgbotapi.Message) {
text := `📚 <b>Справка по командам:</b>
/start — получить твой Chat ID
/tasks — список задач на сегодня
/habits — список привычек
/done &lt;id&gt; — отметить задачу выполненной
/check &lt;id&gt; — отметить привычку выполненной
💡 Чтобы получать уведомления, добавь свой 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
}
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)
}
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
}
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)
}
func (b *Bot) handleDone(msg *tgbotapi.Message) {
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)
}
}
if taskID == 0 {
b.SendMessage(msg.Chat.ID, "❌ Укажи ID задачи: /done &lt;id&gt;")
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) {
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)
}
}
if habitID == 0 {
b.SendMessage(msg.Chat.ID, "❌ Укажи ID привычки: /check &lt;id&gt;")
return
}
// Verify habit exists and belongs to user
_, 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
}
// Create log for today
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, "✅ Привычка отмечена!")
}