fix: unsilence all error returns from SQL Exec, Bot API calls, and migrations
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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{
|
||||
kb := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
{{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{
|
||||
kb := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
{{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{
|
||||
kb := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
{{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,
|
||||
|
||||
@@ -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.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: kb})
|
||||
h.sendText(ctx, chatID, text)
|
||||
}
|
||||
} else {
|
||||
h.editText(ctx, chatID, msgID, text, kb)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user