feat: add Telegram bot with notifications and scheduler
This commit is contained in:
74
internal/bot/bot.go
Normal file
74
internal/bot/bot.go
Normal 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
260
internal/bot/handlers.go
Normal 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 <id> — отметить задачу выполненной
|
||||
/check <id> — отметить привычку выполненной
|
||||
/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 <id> — отметить задачу выполненной
|
||||
/check <id> — отметить привычку выполненной
|
||||
|
||||
💡 Чтобы получать уведомления, добавь свой 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 <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) {
|
||||
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 <id>")
|
||||
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, "✅ Привычка отмечена!")
|
||||
}
|
||||
Reference in New Issue
Block a user