refactor: migrate from go-telegram-bot-api/v5 to go-telegram/bot
- Replaced tgbotapi with go-telegram/bot (zero-dependency, context-aware, Bot API 10.0) - All handler methods now accept context.Context; threading ctx through all sends/edits - Changed from function-based (NewMessage/NewEditMessageText/NewInlineKeyboardMarkup) to struct-param API (SendMessageParams/EditMessageTextParams/InlineKeyboardMarkup) - Added colored buttons: DEL buttons in calendar use Style: 'danger' (red) - Both polling and webhook modes preserved with new library patterns - Context-based shutdown (signal.NotifyContext) replaces stop channel
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
bot "github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
"github.com/xuri/excelize/v2"
|
||||
|
||||
"worktimeBot/internal/db"
|
||||
@@ -224,10 +228,10 @@ func fmtDDHHMM(seconds int64) string {
|
||||
}
|
||||
|
||||
// handleExport processes the /export command, showing the month picker or exporting directly.
|
||||
func (h *Handler) handleExport(msg *tgbotapi.Message) {
|
||||
func (h *Handler) handleExport(ctx context.Context, msg *models.Message) {
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading profile")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
loc := loadLocation(user.Timezone)
|
||||
@@ -241,23 +245,27 @@ func (h *Handler) handleExport(msg *tgbotapi.Message) {
|
||||
cy, cm, _ = gregorianToHijri(cy, cm, now.Day())
|
||||
}
|
||||
|
||||
if len(msg.Text) > 7 {
|
||||
if n, _ := fmt.Sscanf(msg.Text[7:], "%d-%d", &cy, &cm); n == 2 {
|
||||
args := strings.Fields(msg.Text)
|
||||
if len(args) > 0 && args[0][0] == '/' {
|
||||
args = args[1:]
|
||||
}
|
||||
if len(args) >= 1 {
|
||||
if n, _ := fmt.Sscanf(args[0], "%d-%d", &cy, &cm); n == 2 {
|
||||
if cy < 0 || cm < 1 || cm > 12 {
|
||||
h.sendText(msg.Chat.ID, "Invalid date. Use: /export YYYY-MM")
|
||||
h.sendText(ctx, msg.Chat.ID, "Invalid date. Use: /export YYYY-MM")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.sendExportDirect(msg.Chat.ID, user, cy, cm)
|
||||
h.sendExportDirect(ctx, msg.Chat.ID, user, cy, cm)
|
||||
}
|
||||
|
||||
// sendExportDirect generates and sends an Excel report for the given calendar month.
|
||||
func (h *Handler) sendExportDirect(chatID int64, user *db.User, calYear, calMonth int) {
|
||||
func (h *Handler) sendExportDirect(ctx context.Context, chatID int64, user *db.User, calYear, calMonth int) {
|
||||
last, err := h.DB.GetLastEvent(user.ID)
|
||||
if err == nil && last != nil && last.EventType == "in" {
|
||||
h.sendText(chatID, "You are currently clocked in. Please clock out first, then try again.")
|
||||
h.sendText(ctx, chatID, "You are currently clocked in. Please clock out first, then try again.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -277,22 +285,24 @@ func (h *Handler) sendExportDirect(chatID int64, user *db.User, calYear, calMont
|
||||
data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, calYear, calMonth, start, end)
|
||||
if err != nil {
|
||||
slog.Error("generate report", "error", err)
|
||||
h.sendText(chatID, "Error generating report")
|
||||
h.sendText(ctx, chatID, "Error generating report")
|
||||
return
|
||||
}
|
||||
slog.Info("report generated", "bytes", len(data))
|
||||
doc := tgbotapi.NewDocument(chatID, tgbotapi.FileBytes{
|
||||
Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", calYear, calMonth)),
|
||||
Bytes: data,
|
||||
})
|
||||
if _, err := h.Bot.Send(doc); err != nil {
|
||||
if _, err := h.Bot.SendDocument(ctx, &bot.SendDocumentParams{
|
||||
ChatID: chatID,
|
||||
Document: &models.InputFileUpload{
|
||||
Filename: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", calYear, calMonth)),
|
||||
Data: bytes.NewReader(data),
|
||||
},
|
||||
}); err != nil {
|
||||
slog.Error("send document", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// exportCallback opens the export month picker.
|
||||
func (h *Handler) exportCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) exportCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -306,16 +316,16 @@ func (h *Handler) exportCallback(chatID int64, msgID int, callbackID string) {
|
||||
case "hijri":
|
||||
cy, cm, _ = gregorianToHijri(cy, cm, now.Day())
|
||||
}
|
||||
h.editExportMonthPicker(chatID, msgID, cy, cm, user)
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, cy, cm, user)
|
||||
}
|
||||
|
||||
// sendExportMonthPicker sends the export month picker as a new message.
|
||||
func (h *Handler) sendExportMonthPicker(chatID int64, msgID int, year, month int, user *db.User) {
|
||||
h.editExportMonthPicker(chatID, msgID, year, month, user)
|
||||
func (h *Handler) sendExportMonthPicker(ctx context.Context, chatID int64, msgID int, year, month int, user *db.User) {
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, year, month, user)
|
||||
}
|
||||
|
||||
// editExportMonthPicker renders a year-based month picker with 12 month buttons.
|
||||
func (h *Handler) editExportMonthPicker(chatID int64, msgID int, year, _ int, user *db.User) {
|
||||
func (h *Handler) editExportMonthPicker(ctx context.Context, chatID int64, msgID int, year, _ int, user *db.User) {
|
||||
months := gregMonthNames
|
||||
switch user.Calendar {
|
||||
case "jalali":
|
||||
@@ -324,33 +334,36 @@ func (h *Handler) editExportMonthPicker(chatID int64, msgID int, year, _ int, us
|
||||
months = hijriMonthNames
|
||||
}
|
||||
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{
|
||||
kbRows := [][]models.InlineKeyboardButton{
|
||||
// Year navigation
|
||||
{
|
||||
tgbotapi.NewInlineKeyboardButtonData("<", fmt.Sprintf("exp_year_prev_%d", year)),
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%d", year), "noop"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(">", fmt.Sprintf("exp_year_next_%d", year)),
|
||||
{Text: "<", CallbackData: fmt.Sprintf("exp_year_prev_%d", year)},
|
||||
{Text: fmt.Sprintf("%d", year), CallbackData: "noop"},
|
||||
{Text: ">", CallbackData: fmt.Sprintf("exp_year_next_%d", year)},
|
||||
},
|
||||
}
|
||||
|
||||
// 4 columns x 3 rows of month buttons
|
||||
for i := 0; i < 12; i += 4 {
|
||||
row := []tgbotapi.InlineKeyboardButton{}
|
||||
row := []models.InlineKeyboardButton{}
|
||||
for j := i; j < i+4 && j < 12; j++ {
|
||||
row = append(row, tgbotapi.NewInlineKeyboardButtonData(
|
||||
months[j],
|
||||
fmt.Sprintf("exp_do_%d_%d", year, j+1),
|
||||
))
|
||||
row = append(row, models.InlineKeyboardButton{
|
||||
Text: months[j], CallbackData: fmt.Sprintf("exp_do_%d_%d", year, j+1),
|
||||
})
|
||||
}
|
||||
kbRows = append(kbRows, row)
|
||||
}
|
||||
|
||||
kbRows = append(kbRows, []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
})
|
||||
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
h.sendOrEdit(chatID, msgID, "Select month to export:", &kb)
|
||||
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
||||
if msgID == 0 {
|
||||
h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: "Select month to export:", ReplyMarkup: &kb})
|
||||
} else {
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Select month to export:", ReplyMarkup: &kb})
|
||||
}
|
||||
}
|
||||
|
||||
// formatMonthTitle returns the localized month name and year for a given calendar.
|
||||
@@ -366,8 +379,8 @@ func formatMonthTitle(cal string, year, month int) string {
|
||||
}
|
||||
|
||||
// handleExportCallback routes export inline button presses (year nav, month select).
|
||||
func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) handleExportCallback(ctx context.Context, chatID int64, msgID int, callbackID, data string) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -377,12 +390,12 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data
|
||||
|
||||
// Navigate to previous year
|
||||
if n, _ := fmt.Sscanf(data, "exp_year_prev_%d", &y); n == 1 {
|
||||
h.editExportMonthPicker(chatID, msgID, y-1, 0, user)
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, y-1, 0, user)
|
||||
return
|
||||
}
|
||||
// Navigate to next year
|
||||
if n, _ := fmt.Sscanf(data, "exp_year_next_%d", &y); n == 1 {
|
||||
h.editExportMonthPicker(chatID, msgID, y+1, 0, user)
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, y+1, 0, user)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -390,26 +403,30 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data
|
||||
if n, _ := fmt.Sscanf(data, "exp_do_%d_%d", &y, &m); n == 2 {
|
||||
last, err := h.DB.GetLastEvent(user.ID)
|
||||
if err == nil && last != nil && last.EventType == "in" {
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
),
|
||||
)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "You are currently clocked in. Please clock out first, then try again.")
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||
ChatID: chatID,
|
||||
MessageID: msgID,
|
||||
Text: "You are currently clocked in. Please clock out first, then try again.",
|
||||
ReplyMarkup: &models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
startStr, endStr := monthGregorianRange(user.Calendar, y, m)
|
||||
start, err := time.Parse("2006-01-02", startStr)
|
||||
if err != nil {
|
||||
h.sendText(chatID, "Error processing date")
|
||||
h.sendText(ctx, chatID, "Error processing date")
|
||||
return
|
||||
}
|
||||
end, err := time.Parse("2006-01-02", endStr)
|
||||
if err != nil {
|
||||
h.sendText(chatID, "Error processing date")
|
||||
h.sendText(ctx, chatID, "Error processing date")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -417,24 +434,42 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data
|
||||
data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, y, m, start, end)
|
||||
if err != nil {
|
||||
slog.Error("generate report", "error", err)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Error generating report")
|
||||
kb := backKeyboard()
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||
ChatID: chatID,
|
||||
MessageID: msgID,
|
||||
Text: "Error generating report",
|
||||
ReplyMarkup: &models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("report generated", "bytes", len(data))
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Report ready:")
|
||||
kb := backKeyboard()
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
|
||||
doc := tgbotapi.NewDocument(chatID, tgbotapi.FileBytes{
|
||||
Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", y, m)),
|
||||
Bytes: data,
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||
ChatID: chatID,
|
||||
MessageID: msgID,
|
||||
Text: "Report ready:",
|
||||
ReplyMarkup: &models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if _, err := h.Bot.Send(doc); err != nil {
|
||||
|
||||
if _, err := h.Bot.SendDocument(ctx, &bot.SendDocumentParams{
|
||||
ChatID: chatID,
|
||||
Document: &models.InputFileUpload{
|
||||
Filename: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", y, m)),
|
||||
Data: bytes.NewReader(data),
|
||||
},
|
||||
}); err != nil {
|
||||
slog.Error("send document", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user