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,12 +1,14 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
bot "github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"worktimeBot/internal/db"
|
||||
)
|
||||
@@ -15,7 +17,7 @@ const rateLimitInterval = 1 * time.Second
|
||||
|
||||
// Handler holds the bot instance, database store, user allowlist, and rate-limit state.
|
||||
type Handler struct {
|
||||
Bot *tgbotapi.BotAPI
|
||||
Bot *bot.Bot
|
||||
DB *db.Store
|
||||
AllowedUsers map[int64]bool
|
||||
lastMsgTime map[int64]time.Time
|
||||
@@ -23,7 +25,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates a Handler and starts the rate-limit cleanup goroutine.
|
||||
func NewHandler(bot *tgbotapi.BotAPI, store *db.Store, allowed map[int64]bool) *Handler {
|
||||
func NewHandler(bot *bot.Bot, store *db.Store, allowed map[int64]bool) *Handler {
|
||||
h := &Handler{
|
||||
Bot: bot,
|
||||
DB: store,
|
||||
@@ -64,33 +66,29 @@ func (h *Handler) isRateLimited(userID int64) bool {
|
||||
}
|
||||
|
||||
// handleActionMsg runs an action function and sends the result as a new message.
|
||||
func (h *Handler) handleActionMsg(msg *tgbotapi.Message, fn actionFunc) {
|
||||
func (h *Handler) handleActionMsg(ctx context.Context, msg *models.Message, fn actionFunc) {
|
||||
text, err := fn(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, err.Error())
|
||||
h.sendText(ctx, msg.Chat.ID, err.Error())
|
||||
return
|
||||
}
|
||||
reply := tgbotapi.NewMessage(msg.Chat.ID, text)
|
||||
reply.ReplyMarkup = mainKeyboard()
|
||||
h.Bot.Send(reply)
|
||||
kb := mainKeyboard()
|
||||
h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: msg.Chat.ID, Text: text, ReplyMarkup: kb})
|
||||
}
|
||||
|
||||
// handleActionCallback runs an action function and edits the callback message with the result.
|
||||
func (h *Handler) handleActionCallback(chatID int64, msgID int, callbackID string, fn actionFunc) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) handleActionCallback(ctx context.Context, chatID int64, msgID int, callbackID string, fn actionFunc) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
text, err := fn(chatID)
|
||||
if err != nil {
|
||||
text = err.Error()
|
||||
}
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
kb := backKeyboard()
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: kb})
|
||||
}
|
||||
|
||||
// HandleMessage routes incoming text messages to the appropriate handler.
|
||||
func (h *Handler) HandleMessage(update tgbotapi.Update) {
|
||||
msg := update.Message
|
||||
func (h *Handler) HandleMessage(ctx context.Context, msg *models.Message) {
|
||||
if len(h.AllowedUsers) > 0 && !h.AllowedUsers[msg.Chat.ID] {
|
||||
return
|
||||
}
|
||||
@@ -104,110 +102,109 @@ func (h *Handler) HandleMessage(update tgbotapi.Update) {
|
||||
slog.Info("message", "chat_id", msg.Chat.ID, "text", msg.Text)
|
||||
switch msg.Text {
|
||||
case "/start":
|
||||
h.handleStart(msg)
|
||||
h.handleStart(ctx, msg)
|
||||
case "/help":
|
||||
h.handleHelp(msg)
|
||||
h.handleHelp(ctx, msg)
|
||||
case "/clockin":
|
||||
h.handleActionMsg(msg, h.clockIn)
|
||||
h.handleActionMsg(ctx, msg, h.clockIn)
|
||||
case "/clockout":
|
||||
h.handleActionMsg(msg, h.clockOut)
|
||||
h.handleActionMsg(ctx, msg, h.clockOut)
|
||||
case "/worktype":
|
||||
h.handleWorkTypeMsg(msg)
|
||||
h.handleWorkTypeMsg(ctx, msg)
|
||||
case "/report":
|
||||
h.handleActionMsg(msg, h.report)
|
||||
h.handleActionMsg(ctx, msg, h.report)
|
||||
case "/export":
|
||||
h.handleExport(msg)
|
||||
h.handleExport(ctx, msg)
|
||||
case "/dayoff":
|
||||
h.handleActionMsg(msg, h.dayOff)
|
||||
h.handleActionMsg(ctx, msg, h.dayOff)
|
||||
case "/history":
|
||||
h.handleHistoryMsg(msg)
|
||||
h.handleHistoryMsg(ctx, msg)
|
||||
case "/edit":
|
||||
h.handleEditMsg(msg)
|
||||
h.handleEditMsg(ctx, msg)
|
||||
case "/reporttoggle":
|
||||
h.handleActionMsg(msg, h.toggleReport)
|
||||
h.handleActionMsg(ctx, msg, h.toggleReport)
|
||||
case "/setreporttime":
|
||||
h.handleSetReportTime(msg)
|
||||
h.handleSetReportTime(ctx, msg)
|
||||
case "/setaccent":
|
||||
h.handleAccentMsg(msg)
|
||||
h.handleAccentMsg(ctx, msg)
|
||||
case "/settimezone":
|
||||
h.handleSetTimezone(msg)
|
||||
h.handleSetTimezone(ctx, msg)
|
||||
default:
|
||||
h.sendText(msg.Chat.ID, "Unknown command")
|
||||
h.sendText(ctx, msg.Chat.ID, "Unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCallback routes inline callback queries to the appropriate handler.
|
||||
func (h *Handler) HandleCallback(update tgbotapi.Update) {
|
||||
cb := update.CallbackQuery
|
||||
if len(h.AllowedUsers) > 0 && !h.AllowedUsers[cb.Message.Chat.ID] {
|
||||
h.Bot.Request(tgbotapi.NewCallback(cb.ID, "Unauthorized"))
|
||||
func (h *Handler) HandleCallback(ctx context.Context, cb *models.CallbackQuery) {
|
||||
if len(h.AllowedUsers) > 0 && !h.AllowedUsers[cb.Message.Message.Chat.ID] {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: cb.ID, Text: "Unauthorized"})
|
||||
return
|
||||
}
|
||||
user, err := h.getOrCreateUser(cb.Message.Chat.ID)
|
||||
user, err := h.getOrCreateUser(cb.Message.Message.Chat.ID)
|
||||
if err != nil {
|
||||
h.Bot.Request(tgbotapi.NewCallback(cb.ID, ""))
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: cb.ID})
|
||||
return
|
||||
}
|
||||
if h.isRateLimited(user.ID) {
|
||||
h.Bot.Request(tgbotapi.NewCallback(cb.ID, ""))
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: cb.ID})
|
||||
return
|
||||
}
|
||||
slog.Info("callback", "chat_id", cb.Message.Chat.ID, "data", cb.Data)
|
||||
chatID := cb.Message.Chat.ID
|
||||
msgID := cb.Message.MessageID
|
||||
slog.Info("callback", "chat_id", cb.Message.Message.Chat.ID, "data", cb.Data)
|
||||
chatID := cb.Message.Message.Chat.ID
|
||||
msgID := cb.Message.Message.ID
|
||||
|
||||
switch cb.Data {
|
||||
case "clockin":
|
||||
h.handleActionCallback(chatID, msgID, cb.ID, h.clockIn)
|
||||
h.handleActionCallback(ctx, chatID, msgID, cb.ID, h.clockIn)
|
||||
case "clockout":
|
||||
h.handleActionCallback(chatID, msgID, cb.ID, h.clockOut)
|
||||
h.handleActionCallback(ctx, chatID, msgID, cb.ID, h.clockOut)
|
||||
case "worktype":
|
||||
h.workTypeCallback(chatID, msgID, cb.ID)
|
||||
h.workTypeCallback(ctx, chatID, msgID, cb.ID)
|
||||
case "report":
|
||||
h.handleActionCallback(chatID, msgID, cb.ID, h.report)
|
||||
h.handleActionCallback(ctx, chatID, msgID, cb.ID, h.report)
|
||||
case "export":
|
||||
h.exportCallback(chatID, msgID, cb.ID)
|
||||
h.exportCallback(ctx, chatID, msgID, cb.ID)
|
||||
case "noop":
|
||||
h.Bot.Request(tgbotapi.NewCallback(cb.ID, ""))
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: cb.ID})
|
||||
case "dayoff":
|
||||
h.handleActionCallback(chatID, msgID, cb.ID, h.dayOff)
|
||||
h.handleActionCallback(ctx, chatID, msgID, cb.ID, h.dayOff)
|
||||
case "settings":
|
||||
h.settingsCallback(chatID, msgID, cb.ID)
|
||||
h.settingsCallback(ctx, chatID, msgID, cb.ID)
|
||||
case "caltype":
|
||||
h.calTypeCallback(chatID, msgID, cb.ID)
|
||||
h.calTypeCallback(ctx, chatID, msgID, cb.ID)
|
||||
case "accent":
|
||||
h.accentCallback(chatID, msgID, cb.ID)
|
||||
h.accentCallback(ctx, chatID, msgID, cb.ID)
|
||||
case "timezone":
|
||||
h.timezoneCallback(chatID, msgID, cb.ID)
|
||||
h.timezoneCallback(ctx, chatID, msgID, cb.ID)
|
||||
case "history":
|
||||
h.historyCallback(chatID, msgID, cb.ID)
|
||||
h.historyCallback(ctx, chatID, msgID, cb.ID)
|
||||
case "reporttoggle":
|
||||
h.handleActionCallback(chatID, msgID, cb.ID, h.toggleReport)
|
||||
h.handleActionCallback(ctx, chatID, msgID, cb.ID, h.toggleReport)
|
||||
case "reporttime":
|
||||
h.reportTimeCallback(chatID, msgID, cb.ID)
|
||||
h.reportTimeCallback(ctx, chatID, msgID, cb.ID)
|
||||
case "back_menu":
|
||||
h.backToMenu(chatID, msgID, cb.ID)
|
||||
h.backToMenu(ctx, chatID, msgID, cb.ID)
|
||||
case "back_settings":
|
||||
h.backToSettings(chatID, msgID, cb.ID)
|
||||
h.backToSettings(ctx, chatID, msgID, cb.ID)
|
||||
default:
|
||||
// Delegate prefixed callbacks to the appropriate sub-handler
|
||||
if len(cb.Data) > 9 && cb.Data[:9] == "worktype_" {
|
||||
h.selectWorkType(chatID, msgID, cb.ID, cb.Data[9:])
|
||||
h.selectWorkType(ctx, chatID, msgID, cb.ID, cb.Data[9:])
|
||||
return
|
||||
}
|
||||
if len(cb.Data) > 7 && cb.Data[:7] == "accent_" {
|
||||
h.selectAccent(chatID, msgID, cb.ID, cb.Data[7:])
|
||||
h.selectAccent(ctx, chatID, msgID, cb.ID, cb.Data[7:])
|
||||
return
|
||||
}
|
||||
if len(cb.Data) > 8 && cb.Data[:8] == "caltype_" {
|
||||
h.selectCalendar(chatID, msgID, cb.ID, cb.Data[8:])
|
||||
h.selectCalendar(ctx, chatID, msgID, cb.ID, cb.Data[8:])
|
||||
return
|
||||
}
|
||||
if len(cb.Data) > 4 && cb.Data[:4] == "exp_" {
|
||||
h.handleExportCallback(chatID, msgID, cb.ID, cb.Data)
|
||||
h.handleExportCallback(ctx, chatID, msgID, cb.ID, cb.Data)
|
||||
return
|
||||
}
|
||||
h.handleHistoryCallback(chatID, msgID, cb.ID, cb.Data)
|
||||
h.handleHistoryCallback(ctx, chatID, msgID, cb.ID, cb.Data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,21 +232,20 @@ func (h *Handler) getUserToday(chatID int64) (*db.User, *db.Day, error) {
|
||||
}
|
||||
|
||||
// handleStart shows the main menu with the user's current status.
|
||||
func (h *Handler) handleStart(msg *tgbotapi.Message) {
|
||||
func (h *Handler) handleStart(ctx context.Context, msg *models.Message) {
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
slog.Error("start: create user", "error", err)
|
||||
h.sendText(msg.Chat.ID, "Error setting up your profile")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error setting up your profile")
|
||||
return
|
||||
}
|
||||
slog.Info("start", "user_id", user.ID, "chat_id", msg.Chat.ID)
|
||||
menu := tgbotapi.NewMessage(msg.Chat.ID, h.buildStatusText(msg.Chat.ID))
|
||||
menu.ReplyMarkup = mainKeyboard()
|
||||
h.Bot.Send(menu)
|
||||
kb := mainKeyboard()
|
||||
h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: msg.Chat.ID, Text: h.buildStatusText(msg.Chat.ID), ReplyMarkup: kb})
|
||||
}
|
||||
|
||||
// handleHelp shows the list of available commands.
|
||||
func (h *Handler) handleHelp(msg *tgbotapi.Message) {
|
||||
func (h *Handler) handleHelp(ctx context.Context, msg *models.Message) {
|
||||
text := `Commands:
|
||||
/start - Show main menu
|
||||
/clockin - Clock in
|
||||
@@ -267,64 +263,58 @@ func (h *Handler) handleHelp(msg *tgbotapi.Message) {
|
||||
|
||||
Dates use your calendar type (Gregorian/Jalali/Hijri).
|
||||
Buttons on the main menu also work.`
|
||||
h.sendText(msg.Chat.ID, text)
|
||||
h.sendText(ctx, msg.Chat.ID, text)
|
||||
}
|
||||
|
||||
// handleWorkTypeMsg shows the work type picker for today.
|
||||
func (h *Handler) handleWorkTypeMsg(msg *tgbotapi.Message) {
|
||||
func (h *Handler) handleWorkTypeMsg(ctx context.Context, msg *models.Message) {
|
||||
user, day, err := h.getUserToday(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading work types")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading work types")
|
||||
return
|
||||
}
|
||||
text, kb := h.buildWorkTypeKeyboard(user, day)
|
||||
reply := tgbotapi.NewMessage(msg.Chat.ID, text)
|
||||
reply.ReplyMarkup = &kb
|
||||
h.Bot.Send(reply)
|
||||
h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: msg.Chat.ID, Text: text, ReplyMarkup: kb})
|
||||
}
|
||||
|
||||
// workTypeCallback shows the work type picker (inline edit).
|
||||
func (h *Handler) workTypeCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) workTypeCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, day, err := h.getUserToday(chatID)
|
||||
if err != nil {
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Error loading work types")
|
||||
kb := backKeyboard()
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Error loading work types", ReplyMarkup: kb})
|
||||
return
|
||||
}
|
||||
text, kb := h.buildWorkTypeKeyboard(user, day)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: kb})
|
||||
}
|
||||
|
||||
// buildWorkTypeKeyboard returns the work type selection keyboard.
|
||||
func (h *Handler) buildWorkTypeKeyboard(user *db.User, day *db.Day) (string, tgbotapi.InlineKeyboardMarkup) {
|
||||
func (h *Handler) buildWorkTypeKeyboard(user *db.User, day *db.Day) (string, models.InlineKeyboardMarkup) {
|
||||
wts, err := h.DB.GetWorkTypes()
|
||||
if err != nil || len(wts) == 0 {
|
||||
return "No work types available.", backKeyboard()
|
||||
}
|
||||
rows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
rows := [][]models.InlineKeyboardButton{}
|
||||
for _, wt := range wts {
|
||||
label := wt.Name
|
||||
if wt.ID == day.CurrentWorkTypeID {
|
||||
label = "> " + wt.Name
|
||||
}
|
||||
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(label, fmt.Sprintf("worktype_%d", wt.ID)),
|
||||
))
|
||||
rows = append(rows, []models.InlineKeyboardButton{
|
||||
{Text: label, CallbackData: fmt.Sprintf("worktype_%d", wt.ID)},
|
||||
})
|
||||
}
|
||||
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
))
|
||||
return "Select work type:", tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
rows = append(rows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
})
|
||||
return "Select work type:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
||||
}
|
||||
|
||||
// selectWorkType updates the day's work type from a callback.
|
||||
func (h *Handler) selectWorkType(chatID int64, msgID int, callbackID, idStr string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) selectWorkType(ctx context.Context, chatID int64, msgID int, callbackID, idStr string) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
_, day, err := h.getUserToday(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -341,40 +331,40 @@ func (h *Handler) selectWorkType(chatID int64, msgID int, callbackID, idStr stri
|
||||
if err := h.DB.UpdateDay(day); err != nil {
|
||||
return
|
||||
}
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, fmt.Sprintf("Work type set to: %s", wt.Name))
|
||||
kb := backKeyboard()
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: fmt.Sprintf("Work type set to: %s", wt.Name), ReplyMarkup: kb})
|
||||
}
|
||||
|
||||
// mainKeyboard returns the persistent inline menu.
|
||||
func mainKeyboard() tgbotapi.InlineKeyboardMarkup {
|
||||
return tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Clock In", "clockin"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Clock Out", "clockout"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Work Type", "worktype"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Day Off", "dayoff"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Report", "report"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Export", "export"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Settings", "settings"),
|
||||
),
|
||||
)
|
||||
func mainKeyboard() models.InlineKeyboardMarkup {
|
||||
return models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Clock In", CallbackData: "clockin"},
|
||||
{Text: "Clock Out", CallbackData: "clockout"},
|
||||
},
|
||||
{
|
||||
{Text: "Work Type", CallbackData: "worktype"},
|
||||
{Text: "Day Off", CallbackData: "dayoff"},
|
||||
},
|
||||
{
|
||||
{Text: "Report", CallbackData: "report"},
|
||||
{Text: "Export", CallbackData: "export"},
|
||||
},
|
||||
{
|
||||
{Text: "Settings", CallbackData: "settings"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// backKeyboard returns a simple "Back to Menu" button.
|
||||
func backKeyboard() tgbotapi.InlineKeyboardMarkup {
|
||||
return tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
),
|
||||
)
|
||||
func backKeyboard() models.InlineKeyboardMarkup {
|
||||
return models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// formatDuration formats seconds as a human-readable duration (e.g. "7h 30m").
|
||||
@@ -388,8 +378,8 @@ func formatDuration(seconds int64) string {
|
||||
}
|
||||
|
||||
// sendText sends a plain text message to the given chat.
|
||||
func (h *Handler) sendText(chatID int64, text string) {
|
||||
h.Bot.Send(tgbotapi.NewMessage(chatID, text))
|
||||
func (h *Handler) sendText(ctx context.Context, chatID int64, text string) {
|
||||
h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text})
|
||||
}
|
||||
|
||||
// navMonth returns the (prevY, prevM, nextY, nextM) for month navigation.
|
||||
@@ -408,14 +398,10 @@ func navMonth(year, month int) (prevY, prevM, nextY, nextM int) {
|
||||
}
|
||||
|
||||
// sendOrEdit sends a new message or edits an existing one depending on msgID.
|
||||
func (h *Handler) sendOrEdit(chatID int64, msgID int, text string, kb *tgbotapi.InlineKeyboardMarkup) {
|
||||
func (h *Handler) sendOrEdit(ctx context.Context, chatID int64, msgID int, text string, kb *models.InlineKeyboardMarkup) {
|
||||
if msgID == 0 {
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ReplyMarkup = kb
|
||||
h.Bot.Send(msg)
|
||||
h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text, ReplyMarkup: kb})
|
||||
} else {
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: kb})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user