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,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

View File

@@ -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 &lt;id&gt; — отметить задачу выполненной
/check &lt;id&gt; — отметить привычку выполненной
/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 &lt;id&gt; — отметить задачу выполненной
/check &lt;id&gt; — отметить привычку выполненной
💡 Чтобы получать уведомления, добавь свой 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 &lt;id&gt;")
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 &lt;id&gt;")
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,