From e7e4dc7bfed45e1db476dcd2020fd96e9d7fa380 Mon Sep 17 00:00:00 2001 From: db123 Date: Wed, 24 Jun 2026 20:03:24 +0330 Subject: [PATCH] fix: unsilence all error returns from SQL Exec, Bot API calls, and migrations --- cmd/bot/main.go | 5 ++- cmd/bot/webhook.go | 4 +- internal/bot/calendar.go | 65 +++++++++++++++------------ internal/bot/export.go | 60 +++++++++---------------- internal/bot/handlers.go | 76 ++++++++++++++++++++++++-------- internal/bot/report.go | 40 +++++------------ internal/bot/settings.go | 75 +++++++------------------------ internal/db/store.go | 95 ++++++++++++++++++++++++++++++++-------- 8 files changed, 227 insertions(+), 193 deletions(-) diff --git a/cmd/bot/main.go b/cmd/bot/main.go index b870ea1..82e7e07 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -61,7 +61,9 @@ func main() { if url := os.Getenv("BOT_WEBHOOK_URL"); url != "" { startWebhook(ctx, b, h, url) } else { - b.DeleteWebhook(ctx, &tgbot.DeleteWebhookParams{}) + if _, err := b.DeleteWebhook(ctx, &tgbot.DeleteWebhookParams{}); err != nil { + slog.Warn("failed to delete webhook", "error", err) + } go dailyReportScheduler(ctx, h, dbStore) slog.Info("bot started", "allowed_users", len(allowedUsers), @@ -105,6 +107,7 @@ func checkDailyReports(ctx context.Context, h *bot.Handler, store *db.Store) { loc, err := time.LoadLocation(u.Timezone) if err != nil { + slog.Error("invalid timezone for user, skipping report", "chat_id", u.ChatID, "timezone", u.Timezone, "error", err) continue } localNow := now.In(loc) diff --git a/cmd/bot/webhook.go b/cmd/bot/webhook.go index 8ab7091..85546ae 100644 --- a/cmd/bot/webhook.go +++ b/cmd/bot/webhook.go @@ -13,7 +13,9 @@ import ( // startWebhook registers the webhook with Telegram and starts the HTTPS/HTTP listener loop. func startWebhook(ctx context.Context, b *tgbot.Bot, h *bot.Handler, webhookURL string) { - b.DeleteWebhook(ctx, &tgbot.DeleteWebhookParams{}) + if _, err := b.DeleteWebhook(ctx, &tgbot.DeleteWebhookParams{}); err != nil { + slog.Warn("failed to delete webhook", "error", err) + } if _, err := b.SetWebhook(ctx, &tgbot.SetWebhookParams{URL: webhookURL}); err != nil { slog.Error("set webhook", "error", err) diff --git a/internal/bot/calendar.go b/internal/bot/calendar.go index 8f8b0dd..a55af23 100644 --- a/internal/bot/calendar.go +++ b/internal/bot/calendar.go @@ -4,11 +4,11 @@ import ( "context" "database/sql" "fmt" + "log/slog" "strconv" "strings" "time" - bot "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" "worktimeBot/internal/db" @@ -21,7 +21,7 @@ func (h *Handler) handleHistoryMsg(ctx context.Context, msg *models.Message) { // historyCallback opens or refreshes the calendar view (inline). func (h *Handler) historyCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { - defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + defer h.answerCb(ctx, callbackID) h.editCalendar(ctx, chatID, msgID, 0, 0) } @@ -53,7 +53,7 @@ func (h *Handler) handleEditMsg(ctx context.Context, msg *models.Message) { // handleHistoryCallback routes calendar-related callback data to the right handler. func (h *Handler) handleHistoryCallback(ctx context.Context, chatID int64, msgID int, callbackID, data string) { - defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return @@ -256,10 +256,15 @@ func (h *Handler) editCalendar(ctx context.Context, chatID int64, msgID int, yea if err == nil { for rows.Next() { var d string - rows.Scan(&d) + if err := rows.Scan(&d); err != nil { + slog.Error("failed to scan day row", "error", err) + continue + } hasEvent[d] = true } - rows.Close() + if err := rows.Close(); err != nil { + slog.Error("failed to close rows", "error", err) + } } kbRows := [][]models.InlineKeyboardButton{} @@ -325,9 +330,7 @@ func (h *Handler) editCalendarYearPicker(ctx context.Context, chatID int64, msgI backData := fmt.Sprintf("cal_years_back_%d_%d", refY, refM) navSuffix := fmt.Sprintf("_%d_%d", refY, refM) kb := buildYearPicker("cal_", backData, navSuffix, centerYear) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, MessageID: msgID, Text: "Select year:", ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, "Select year:", &kb) } // editDayView opens an existing message as a day view. @@ -416,12 +419,17 @@ func (h *Handler) editEventType(ctx context.Context, chatID int64, msgID int, us {Text: "Back", CallbackData: fmt.Sprintf("back_day_%d", eventID)}, }) kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows} - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: &kb}) + h.editText(ctx, chatID, msgID, text, &kb) } // setEventWorkType updates an event's work type and returns to the day view. func (h *Handler) setEventWorkType(ctx context.Context, chatID int64, msgID int, user *db.User, eventID, workTypeID int64, loc *time.Location) { - h.DB.DB().Exec("UPDATE events SET work_type_id=? WHERE id=?", workTypeID, eventID) + _, err := h.DB.DB().Exec("UPDATE events SET work_type_id=? WHERE id=?", workTypeID, eventID) + if err != nil { + slog.Error("failed to update event work type", "event_id", eventID, "error", err) + h.sendDayView(ctx, chatID, msgID, user, "", loc, false) + return + } event, err := h.getEventByID(user.ID, eventID) if err != nil { return @@ -467,7 +475,7 @@ func (h *Handler) editEventTime(ctx context.Context, chatID int64, msgID int, us func(hh int) string { return fmt.Sprintf("edittm_%d_%02d", eventID, hh) }, fmt.Sprintf("back_day_%d", eventID), ) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Select hour:", ReplyMarkup: &kb}) + h.editText(ctx, chatID, msgID, "Select hour:", &kb) } // editEventTimeMin shows a minute picker (15-min intervals) after hour selection. @@ -476,7 +484,7 @@ func (h *Handler) editEventTimeMin(ctx context.Context, chatID int64, msgID int, func(mm int) string { return fmt.Sprintf("edittm_%d_%02d_%02d", eventID, hh, mm) }, fmt.Sprintf("edit_time_%d", eventID), ) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: fmt.Sprintf("Select minute for hour %02d:", hh), ReplyMarkup: &kb}) + h.editText(ctx, chatID, msgID, fmt.Sprintf("Select minute for hour %02d:", hh), &kb) } // editEventTimeSet applies a new time to an event, checking for overlaps. @@ -528,19 +536,24 @@ func (h *Handler) editEventTimeSet(ctx context.Context, chatID int64, msgID int, } for i := 1; i < len(sorted); i++ { if sorted[i].typ == sorted[i-1].typ { - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Edit rejected: overlapping events. Two consecutive events must be different types (in/out)."}) + h.editText(ctx, chatID, msgID, "Edit rejected: overlapping events. Two consecutive events must be different types (in/out).", nil) return } } } - h.DB.DB().Exec("UPDATE events SET occurred_at=? WHERE id=?", newTime.Unix(), eventID) + _, err = h.DB.DB().Exec("UPDATE events SET occurred_at=? WHERE id=?", newTime.Unix(), eventID) + if err != nil { + slog.Error("failed to update event occurred_at", "event_id", eventID, "error", err) + h.sendDayView(ctx, chatID, msgID, user, date, loc, false) + return + } h.sendDayView(ctx, chatID, msgID, user, date, loc, false) } // acknowledgeCallback sends an empty acknowledgement for a callback query. func (h *Handler) acknowledgeCallback(ctx context.Context, chatID int64, msgID int) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: fmt.Sprintf("cb_%d_%d", chatID, msgID)}) + h.answerCb(ctx, fmt.Sprintf("cb_%d_%d", chatID, msgID)) } // promptNote asks the user to type a note for the event, then re-renders the day view. @@ -560,10 +573,7 @@ func (h *Handler) promptNote(ctx context.Context, chatID int64, msgID int, user {{Text: "Cancel", CallbackData: fmt.Sprintf("note_cancel_%d", eventID)}}, }, } - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, MessageID: msgID, - Text: fmt.Sprintf("Send the note for event at %s:", t), ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, fmt.Sprintf("Send the note for event at %s:", t), &kb) } // deleteEventPrompt asks the user to confirm event deletion. @@ -576,7 +586,7 @@ func (h *Handler) deleteEventPrompt(ctx context.Context, chatID int64, msgID int }, }, } - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Delete this event?", ReplyMarkup: &kb}) + h.editText(ctx, chatID, msgID, "Delete this event?", &kb) } // deleteEventConfirm deletes an event and warns if the timeline becomes invalid. @@ -588,7 +598,12 @@ func (h *Handler) deleteEventConfirm(ctx context.Context, chatID int64, msgID in t := time.Unix(event.OccurredAt, 0).In(loc) date := t.Format("2006-01-02") - h.DB.DB().Exec("DELETE FROM events WHERE id=?", eventID) + _, err = h.DB.DB().Exec("DELETE FROM events WHERE id=?", eventID) + if err != nil { + slog.Error("failed to delete event", "event_id", eventID, "error", err) + h.sendDayView(ctx, chatID, msgID, user, date, loc, false) + return + } // Warn if consecutive events now have the same type events, err := h.DB.EventsForDayByDate(user.ID, date) @@ -641,9 +656,7 @@ func (h *Handler) addEventTime(ctx context.Context, chatID int64, msgID int, use func(hh int) string { return fmt.Sprintf("addtm_%s_%s_%02d", eventType, date, hh) }, "cal_day_"+date, ) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, MessageID: msgID, Text: fmt.Sprintf("Select hour for %s event:", eventType), ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, fmt.Sprintf("Select hour for %s event:", eventType), &kb) } // addEventTimeMin shows a minute picker (15-min intervals) after hour selection for adding an event. @@ -652,9 +665,7 @@ func (h *Handler) addEventTimeMin(ctx context.Context, chatID int64, msgID int, func(mm int) string { return fmt.Sprintf("addtm_%s_%s_%02d_%02d", eventType, date, hh, mm) }, "add"+eventType+"_"+date, ) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, MessageID: msgID, Text: fmt.Sprintf("Select minute for hour %02d:", hh), ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, fmt.Sprintf("Select minute for hour %02d:", hh), &kb) } // addEventDo creates a new event at the specified time and returns to the day view. diff --git a/internal/bot/export.go b/internal/bot/export.go index 5525759..3bead1a 100644 --- a/internal/bot/export.go +++ b/internal/bot/export.go @@ -341,7 +341,7 @@ func (h *Handler) sendExportDirect(ctx context.Context, chatID int64, user *db.U // exportCallback opens the export month picker. func (h *Handler) exportCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { - defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return @@ -399,9 +399,9 @@ func (h *Handler) editExportMonthPicker(ctx context.Context, chatID int64, msgID kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows} if msgID == 0 { - h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: "Select month to export:", ReplyMarkup: &kb}) + h.sendWithKB(ctx, chatID, "Select month to export:", kb) } else { - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Select month to export:", ReplyMarkup: &kb}) + h.editText(ctx, chatID, msgID, "Select month to export:", &kb) } } @@ -411,9 +411,7 @@ func (h *Handler) editExportYearPicker(ctx context.Context, chatID int64, msgID backData := fmt.Sprintf("exp_years_back_%d", refY) navSuffix := fmt.Sprintf("_%d", refY) kb := buildYearPicker("exp_", backData, navSuffix, centerYear) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, MessageID: msgID, Text: "Select year:", ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, "Select year:", &kb) } // formatMonthTitle returns the localized month name and year for a given calendar. @@ -430,7 +428,7 @@ func formatMonthTitle(cal string, year, month int) string { // handleExportCallback routes export inline button presses (year nav, month select). func (h *Handler) handleExportCallback(ctx context.Context, chatID int64, msgID int, callbackID, data string) { - defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + defer h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return @@ -474,18 +472,12 @@ func (h *Handler) handleExportCallback(ctx context.Context, chatID int64, msgID 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" { - 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"}, - }, - }, + kb := models.InlineKeyboardMarkup{ + InlineKeyboard: [][]models.InlineKeyboardButton{ + {{Text: "Back to Menu", CallbackData: "back_menu"}}, }, - }) + } + h.editText(ctx, chatID, msgID, "You are currently clocked in. Please clock out first, then try again.", &kb) return } @@ -505,34 +497,22 @@ func (h *Handler) handleExportCallback(ctx context.Context, chatID int64, msgID 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) - 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"}, - }, - }, + kb := models.InlineKeyboardMarkup{ + InlineKeyboard: [][]models.InlineKeyboardButton{ + {{Text: "Back to Menu", CallbackData: "back_menu"}}, }, - }) + } + h.editText(ctx, chatID, msgID, "Error generating report", &kb) return } slog.Info("report generated", "bytes", len(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"}, - }, - }, + kb := models.InlineKeyboardMarkup{ + InlineKeyboard: [][]models.InlineKeyboardButton{ + {{Text: "Back to Menu", CallbackData: "back_menu"}}, }, - }) + } + h.editText(ctx, chatID, msgID, "Report ready:", &kb) if _, err := h.Bot.SendDocument(ctx, &bot.SendDocumentParams{ ChatID: chatID, diff --git a/internal/bot/handlers.go b/internal/bot/handlers.go index 637fbdd..de87a49 100644 --- a/internal/bot/handlers.go +++ b/internal/bot/handlers.go @@ -87,18 +87,18 @@ func (h *Handler) handleActionMsg(ctx context.Context, msg *models.Message, fn a return } kb := mainKeyboard() - h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: msg.Chat.ID, Text: text, ReplyMarkup: kb}) + h.sendWithKB(ctx, msg.Chat.ID, text, kb) } // handleActionCallback runs an action function and edits the callback message with the result. func (h *Handler) handleActionCallback(ctx context.Context, chatID int64, msgID int, callbackID string, fn actionFunc) { - defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + defer h.answerCb(ctx, callbackID) text, err := fn(chatID) if err != nil { text = err.Error() } kb := backKeyboard() - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: kb}) + h.editText(ctx, chatID, msgID, text, &kb) } // HandleMessage routes incoming text messages to the appropriate handler. @@ -126,7 +126,9 @@ func (h *Handler) HandleMessage(ctx context.Context, msg *models.Message) { loc := loadLocation(user.Timezone) event, err := h.getEventByID(user.ID, pn.eventID) if err == nil { - h.DB.SetEventNote(pn.eventID, msg.Text) + if err := h.DB.SetEventNote(pn.eventID, msg.Text); err != nil { + slog.Error("failed to save event note", "event_id", pn.eventID, "error", err) + } date := time.Unix(event.OccurredAt, 0).In(loc).Format("2006-01-02") h.sendDayView(ctx, msg.Chat.ID, 0, user, date, loc, true) } @@ -172,16 +174,16 @@ func (h *Handler) HandleMessage(ctx context.Context, msg *models.Message) { // HandleCallback routes inline callback queries to the appropriate handler. 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"}) + h.answerCbWithText(ctx, cb.ID, "Unauthorized") return } user, err := h.getOrCreateUser(cb.Message.Message.Chat.ID) if err != nil { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: cb.ID}) + h.answerCb(ctx, cb.ID) return } if h.isRateLimited(user.ID) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: cb.ID}) + h.answerCb(ctx, cb.ID) return } slog.Info("callback", "chat_id", cb.Message.Message.Chat.ID, "data", cb.Data) @@ -200,7 +202,7 @@ func (h *Handler) HandleCallback(ctx context.Context, cb *models.CallbackQuery) case "export": h.exportCallback(ctx, chatID, msgID, cb.ID) case "noop": - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: cb.ID}) + h.answerCb(ctx, cb.ID) case "dayoff": h.handleActionCallback(ctx, chatID, msgID, cb.ID, h.dayOff) case "settings": @@ -276,7 +278,7 @@ func (h *Handler) handleStart(ctx context.Context, msg *models.Message) { } slog.Info("start", "user_id", user.ID, "chat_id", msg.Chat.ID) kb := mainKeyboard() - h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: msg.Chat.ID, Text: h.buildStatusText(msg.Chat.ID), ReplyMarkup: kb}) + h.sendWithKB(ctx, msg.Chat.ID, h.buildStatusText(msg.Chat.ID), kb) } // handleHelp shows the list of available commands. @@ -367,20 +369,20 @@ func (h *Handler) handleWorkTypeMsg(ctx context.Context, msg *models.Message) { return } text, kb := h.buildWorkTypeKeyboard(user, day) - h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: msg.Chat.ID, Text: text, ReplyMarkup: kb}) + h.sendWithKB(ctx, msg.Chat.ID, text, kb) } // workTypeCallback shows the work type picker (inline edit). func (h *Handler) workTypeCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { - defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + defer h.answerCb(ctx, callbackID) user, day, err := h.getUserToday(chatID) if err != nil { kb := backKeyboard() - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Error loading work types", ReplyMarkup: kb}) + h.editText(ctx, chatID, msgID, "Error loading work types", &kb) return } text, kb := h.buildWorkTypeKeyboard(user, day) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: kb}) + h.editText(ctx, chatID, msgID, text, &kb) } // buildWorkTypeKeyboard returns the work type selection keyboard. @@ -407,7 +409,7 @@ func (h *Handler) buildWorkTypeKeyboard(user *db.User, day *db.Day) (string, mod // selectWorkType updates the day's work type from a callback. func (h *Handler) selectWorkType(ctx context.Context, chatID int64, msgID int, callbackID, idStr string) { - defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + defer h.answerCb(ctx, callbackID) _, day, err := h.getUserToday(chatID) if err != nil { return @@ -425,7 +427,7 @@ func (h *Handler) selectWorkType(ctx context.Context, chatID int64, msgID int, c return } kb := backKeyboard() - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: fmt.Sprintf("Work type set to: %s", wt.Name), ReplyMarkup: kb}) + h.editText(ctx, chatID, msgID, fmt.Sprintf("Work type set to: %s", wt.Name), &kb) } // mainKeyboard returns the persistent inline menu. @@ -472,7 +474,41 @@ func formatDuration(seconds int64) string { // sendText sends a plain text message to the given chat. func (h *Handler) sendText(ctx context.Context, chatID int64, text string) { - h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text}) + if _, err := h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text}); err != nil { + slog.Warn("send text failed", "chat_id", chatID, "error", err) + } +} + +// editText edits an existing message, logging errors. +func (h *Handler) editText(ctx context.Context, chatID int64, msgID int, text string, kb *models.InlineKeyboardMarkup) { + p := &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text} + if kb != nil { + p.ReplyMarkup = kb + } + if _, err := h.Bot.EditMessageText(ctx, p); err != nil { + slog.Warn("edit text failed", "chat_id", chatID, "msg_id", msgID, "error", err) + } +} + +// sendWithKB sends a message with an inline keyboard, logging errors. +func (h *Handler) sendWithKB(ctx context.Context, chatID int64, text string, kb models.InlineKeyboardMarkup) { + if _, err := h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text, ReplyMarkup: kb}); err != nil { + slog.Warn("send with keyboard failed", "chat_id", chatID, "error", err) + } +} + +// answerCb answers a callback query, logging errors. +func (h *Handler) answerCb(ctx context.Context, callbackID string) { + if _, err := h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}); err != nil { + slog.Debug("answer callback failed", "error", err) + } +} + +// answerCbWithText answers a callback query with a toast message, logging errors. +func (h *Handler) answerCbWithText(ctx context.Context, callbackID, text string) { + if _, err := h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID, Text: text}); err != nil { + slog.Debug("answer callback with text failed", "error", err) + } } // navMonth returns the (prevY, prevM, nextY, nextM) for month navigation. @@ -493,8 +529,12 @@ 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(ctx context.Context, chatID int64, msgID int, text string, kb *models.InlineKeyboardMarkup) { if msgID == 0 { - h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text, ReplyMarkup: kb}) + if kb != nil { + h.sendWithKB(ctx, chatID, text, *kb) + } else { + h.sendText(ctx, chatID, text) + } } else { - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: kb}) + h.editText(ctx, chatID, msgID, text, kb) } } diff --git a/internal/bot/report.go b/internal/bot/report.go index 0fc207e..09ffcf5 100644 --- a/internal/bot/report.go +++ b/internal/bot/report.go @@ -7,7 +7,6 @@ import ( "time" bot "github.com/go-telegram/bot" - "github.com/go-telegram/bot/models" "worktimeBot/internal/db" ) @@ -56,32 +55,9 @@ func (h *Handler) buildStatusText(chatID int64) string { // backToMenu returns to the main menu from any sub-menu. func (h *Handler) backToMenu(ctx context.Context, chatID int64, msgID int, callbackID string) { - defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) - text := h.buildStatusText(chatID) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: text, - ReplyMarkup: &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"}, - }, - }, - }, - }) + defer h.answerCb(ctx, callbackID) + kb := mainKeyboard() + h.editText(ctx, chatID, msgID, h.buildStatusText(chatID), &kb) } // buildDailyReport builds a formatted daily report string for a given user/day. @@ -164,6 +140,12 @@ func (h *Handler) SendDailyReport(ctx context.Context, userID int64, chatID int6 return } text := h.buildDailyReport(user, day, events) - h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text}) - h.DB.MarkReportSent(user.ID, today) + _, err = h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text}) + if err != nil { + slog.Error("daily report: send message failed", "chat_id", chatID, "error", err) + return + } + if err := h.DB.MarkReportSent(user.ID, today); err != nil { + slog.Error("daily report: mark sent failed", "chat_id", chatID, "error", err) + } } diff --git a/internal/bot/settings.go b/internal/bot/settings.go index 77913bd..36611c5 100644 --- a/internal/bot/settings.go +++ b/internal/bot/settings.go @@ -7,7 +7,6 @@ import ( "strings" "time" - bot "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" "worktimeBot/internal/db" @@ -59,7 +58,7 @@ func (h *Handler) handleAccentMsg(ctx context.Context, msg *models.Message) { return } text, kb := h.buildAccentKeyboard(user) - h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: msg.Chat.ID, Text: text, ReplyMarkup: &kb}) + h.sendWithKB(ctx, msg.Chat.ID, text, kb) } // handleSetTimezone validates and updates the user's IANA timezone. @@ -98,18 +97,13 @@ func (h *Handler) handleSetTimezone(ctx context.Context, msg *models.Message) { // settingsCallback shows the settings inline menu. func (h *Handler) settingsCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text, kb := h.buildSettingsKeyboard(user) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: text, - ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, text, &kb) } // buildSettingsKeyboard returns the settings menu text and inline keyboard. @@ -132,23 +126,18 @@ func (h *Handler) buildSettingsKeyboard(user *db.User) (string, models.InlineKey // accentCallback shows the accent color picker (inline edit). func (h *Handler) accentCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text, kb := h.buildAccentKeyboard(user) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: text, - ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, text, &kb) } // timezoneCallback shows the current timezone and instructions to change it. func (h *Handler) timezoneCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return @@ -159,12 +148,7 @@ func (h *Handler) timezoneCallback(ctx context.Context, chatID int64, msgID int, {{Text: "Back to Settings", CallbackData: "back_settings"}}, }, } - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: text, - ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, text, &kb) } // buildAccentKeyboard returns the accent color picker keyboard. @@ -197,7 +181,7 @@ func (h *Handler) buildAccentKeyboard(user *db.User) (string, models.InlineKeybo // selectAccent sets the user's export accent color after validation. func (h *Handler) selectAccent(ctx context.Context, chatID int64, msgID int, callbackID, name string) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return @@ -215,17 +199,12 @@ func (h *Handler) selectAccent(ctx context.Context, chatID int64, msgID int, cal {{Text: "Back to Settings", CallbackData: "back_settings"}}, }, } - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: fmt.Sprintf("Accent set to: %s", name), - ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, fmt.Sprintf("Accent set to: %s", name), &kb) } // reportTimeCallback shows the current report time setting. func (h *Handler) reportTimeCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return @@ -236,44 +215,29 @@ func (h *Handler) reportTimeCallback(ctx context.Context, chatID int64, msgID in {{Text: "Back to Settings", CallbackData: "back_settings"}}, }, } - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: text, - ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, text, &kb) } // backToSettings returns to the main settings menu. func (h *Handler) backToSettings(ctx context.Context, chatID int64, msgID int, callbackID string) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text, kb := h.buildSettingsKeyboard(user) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: text, - ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, text, &kb) } // calTypeCallback shows the calendar type selector. func (h *Handler) calTypeCallback(ctx context.Context, chatID int64, msgID int, callbackID string) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + h.answerCb(ctx, callbackID) user, err := h.getOrCreateUser(chatID) if err != nil { return } text, kb := h.buildCalendarKeyboard(user) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: text, - ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, text, &kb) } // buildCalendarKeyboard returns the calendar type selector keyboard. @@ -302,7 +266,7 @@ func (h *Handler) buildCalendarKeyboard(user *db.User) (string, models.InlineKey // selectCalendar sets the user's calendar after validation. func (h *Handler) selectCalendar(ctx context.Context, chatID int64, msgID int, callbackID, name string) { - h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID}) + h.answerCb(ctx, callbackID) valid := map[string]bool{"gregorian": true, "jalali": true, "hijri": true} if !valid[name] { return @@ -316,10 +280,5 @@ func (h *Handler) selectCalendar(ctx context.Context, chatID int64, msgID int, c return } text, kb := h.buildSettingsKeyboard(user) - h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ - ChatID: chatID, - MessageID: msgID, - Text: text, - ReplyMarkup: &kb, - }) + h.editText(ctx, chatID, msgID, text, &kb) } diff --git a/internal/db/store.go b/internal/db/store.go index ab37cc9..7a3e6b6 100644 --- a/internal/db/store.go +++ b/internal/db/store.go @@ -77,7 +77,9 @@ func NewStore(path string) (*Store, error) { if err := db.Ping(); err != nil { return nil, err } - db.Exec("PRAGMA journal_mode=WAL") + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { + slog.Warn("failed to set WAL mode", "error", err) + } if err := runMigrations(db); err != nil { return nil, err } @@ -96,7 +98,10 @@ func (s *Store) DB() *sql.DB { func hasTable(db *sql.DB, name string) bool { var exists int - db.QueryRow("SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?", name).Scan(&exists) + if err := db.QueryRow("SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?", name).Scan(&exists); err != nil { + slog.Warn("hasTable query failed", "table", name, "error", err) + return false + } return exists > 0 } @@ -139,15 +144,21 @@ func runMigrations(db *sql.DB) error { func hasCol(db *sql.DB, table, column string) bool { rows, err := db.Query("PRAGMA table_info(" + table + ")") if err != nil { + slog.Warn("hasCol query failed", "table", table, "error", err) return false } - defer rows.Close() + defer func() { + if err := rows.Close(); err != nil { + slog.Warn("hasCol rows.Close failed", "table", table, "error", err) + } + }() for rows.Next() { var cid int var name, ctype string var notnull, pk int var dflt sql.NullString if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { + slog.Warn("hasCol scan failed", "table", table, "error", err) return false } if name == column { @@ -165,7 +176,9 @@ func migrateFromOldSchemaData(db *sql.DB) error { for _, t := range []string{"days_off_old", "settings_old", "events_old"} { if hasTable(db, t) { - db.Exec("DROP TABLE IF EXISTS " + t) + if _, err := db.Exec("DROP TABLE IF EXISTS " + t); err != nil { + slog.Warn("failed to drop old table", "table", t, "error", err) + } } } @@ -179,20 +192,27 @@ func migrateUsers(db *sql.DB) { } rows, err := db.Query("SELECT DISTINCT chat_id FROM events_old") if err != nil { - slog.Warn("migration: no events table to migrate users from") + slog.Warn("migration: failed to query events_old for users", "error", err) return } - defer rows.Close() + defer func() { + if err := rows.Close(); err != nil { + slog.Warn("migration: rows.Close failed in migrateUsers", "error", err) + } + }() for rows.Next() { var chatID int64 if err := rows.Scan(&chatID); err != nil { + slog.Warn("migration: scan failed in migrateUsers", "error", err) continue } - db.Exec( + if _, err := db.Exec( "INSERT OR IGNORE INTO users (chat_id, timezone) VALUES (?, 'Asia/Tehran')", chatID, - ) + ); err != nil { + slog.Warn("migration: failed to insert user", "chat_id", chatID, "error", err) + } } } @@ -202,25 +222,34 @@ func migrateDaysOff(db *sql.DB) { } rows, err := db.Query("SELECT chat_id, date, reason FROM days_off_old") if err != nil { + slog.Warn("migration: failed to query days_off_old", "error", err) return } - defer rows.Close() + defer func() { + if err := rows.Close(); err != nil { + slog.Warn("migration: rows.Close failed in migrateDaysOff", "error", err) + } + }() for rows.Next() { var chatID int64 var date, reason string if err := rows.Scan(&chatID, &date, &reason); err != nil { + slog.Warn("migration: scan failed in migrateDaysOff", "error", err) continue } var userID int64 err := db.QueryRow("SELECT id FROM users WHERE chat_id=?", chatID).Scan(&userID) if err != nil { + slog.Warn("migration: user not found for days_off", "chat_id", chatID, "error", err) continue } - db.Exec( + if _, err := db.Exec( "INSERT INTO days (user_id, date, is_day_off, day_off_reason) VALUES (?, ?, 1, ?) ON CONFLICT(user_id, date) DO UPDATE SET is_day_off=1, day_off_reason=excluded.day_off_reason", userID, date, reason, - ) + ); err != nil { + slog.Warn("migration: failed to insert day_off", "user_id", userID, "date", date, "error", err) + } } } @@ -230,34 +259,49 @@ func migrateEvents(db *sql.DB) { } rows, err := db.Query("SELECT chat_id, event_type, occurred_at, note, created_at FROM events_old ORDER BY occurred_at ASC") if err != nil { + slog.Warn("migration: failed to query events_old", "error", err) return } - defer rows.Close() + defer func() { + if err := rows.Close(); err != nil { + slog.Warn("migration: rows.Close failed in migrateEvents", "error", err) + } + }() for rows.Next() { var chatID int64 var eventType, note string var occurredAt, createdAt int64 if err := rows.Scan(&chatID, &eventType, &occurredAt, ¬e, &createdAt); err != nil { + slog.Warn("migration: scan failed in migrateEvents", "error", err) continue } var userID int64 err := db.QueryRow("SELECT id FROM users WHERE chat_id=?", chatID).Scan(&userID) if err != nil { + slog.Warn("migration: user not found for event", "chat_id", chatID, "error", err) continue } date := time.Unix(occurredAt, 0).UTC().Format("2006-01-02") - db.Exec("INSERT OR IGNORE INTO days (user_id, date) VALUES (?, ?)", userID, date) + if _, err := db.Exec("INSERT OR IGNORE INTO days (user_id, date) VALUES (?, ?)", userID, date); err != nil { + slog.Warn("migration: failed to insert day", "user_id", userID, "date", date, "error", err) + continue + } var dayID int64 - db.QueryRow("SELECT id FROM days WHERE user_id=? AND date=?", userID, date).Scan(&dayID) + if err := db.QueryRow("SELECT id FROM days WHERE user_id=? AND date=?", userID, date).Scan(&dayID); err != nil { + slog.Warn("migration: day not found after insert", "user_id", userID, "date", date, "error", err) + continue + } - db.Exec( + if _, err := db.Exec( "INSERT INTO events (user_id, day_id, event_type, occurred_at, note, created_at) VALUES (?, ?, ?, ?, ?, ?)", userID, dayID, eventType, occurredAt, note, createdAt, - ) + ); err != nil { + slog.Warn("migration: failed to insert event", "user_id", userID, "error", err) + } } } @@ -267,27 +311,36 @@ func migrateSettings(db *sql.DB) { } rows, err := db.Query("SELECT chat_id, key, value FROM settings_old") if err != nil { + slog.Warn("migration: failed to query settings_old", "error", err) return } - defer rows.Close() + defer func() { + if err := rows.Close(); err != nil { + slog.Warn("migration: rows.Close failed in migrateSettings", "error", err) + } + }() for rows.Next() { var chatID int64 var key, value string if err := rows.Scan(&chatID, &key, &value); err != nil { + slog.Warn("migration: scan failed in migrateSettings", "error", err) continue } if key == "remote_flag" { var userID int64 err := db.QueryRow("SELECT id FROM users WHERE chat_id=?", chatID).Scan(&userID) if err != nil { + slog.Warn("migration: user not found for setting", "chat_id", chatID, "error", err) continue } wt := int64(1) if value == "remote" { wt = 2 } - db.Exec("UPDATE days SET current_work_type_id=? WHERE user_id=? AND date=?", wt, userID, time.Now().UTC().Format("2006-01-02")) + if _, err := db.Exec("UPDATE days SET current_work_type_id=? WHERE user_id=? AND date=?", wt, userID, time.Now().UTC().Format("2006-01-02")); err != nil { + slog.Warn("migration: failed to update work type", "user_id", userID, "error", err) + } } } } @@ -527,7 +580,11 @@ func (s *Store) GetLastEvent(userID int64) (*Event, error) { if err != nil { return nil, err } - defer row.Close() + defer func() { + if err := row.Close(); err != nil { + slog.Warn("GetLastEvent: row.Close failed", "error", err) + } + }() if !row.Next() { return nil, sql.ErrNoRows }