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,55 +1,58 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
bot "github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"worktimeBot/internal/db"
|
||||
)
|
||||
|
||||
// handleHistoryMsg shows the calendar view (new message).
|
||||
func (h *Handler) handleHistoryMsg(msg *tgbotapi.Message) {
|
||||
h.sendCalendar(msg.Chat.ID, 0, 0, 0)
|
||||
func (h *Handler) handleHistoryMsg(ctx context.Context, msg *models.Message) {
|
||||
h.sendCalendar(ctx, msg.Chat.ID, 0, 0, 0)
|
||||
}
|
||||
|
||||
// historyCallback opens or refreshes the calendar view (inline).
|
||||
func (h *Handler) historyCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
h.editCalendar(chatID, msgID, 0, 0)
|
||||
func (h *Handler) historyCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
h.editCalendar(ctx, chatID, msgID, 0, 0)
|
||||
}
|
||||
|
||||
// handleEditMsg parses a /edit YYYY-MM-DD command and shows that day's events.
|
||||
func (h *Handler) handleEditMsg(msg *tgbotapi.Message) {
|
||||
date := msg.CommandArguments()
|
||||
func (h *Handler) handleEditMsg(ctx context.Context, msg *models.Message) {
|
||||
date := strings.TrimSpace(strings.TrimPrefix(msg.Text, "/edit"))
|
||||
if date == "" {
|
||||
h.sendText(msg.Chat.ID, "Usage: /edit YYYY-MM-DD (your calendar type)")
|
||||
h.sendText(ctx, msg.Chat.ID, "Usage: /edit YYYY-MM-DD (your calendar type)")
|
||||
return
|
||||
}
|
||||
if len(date) != 10 || date[4] != '-' || date[7] != '-' {
|
||||
h.sendText(msg.Chat.ID, "Invalid date. Use: /edit YYYY-MM-DD (your calendar type)")
|
||||
h.sendText(ctx, msg.Chat.ID, "Invalid date. Use: /edit YYYY-MM-DD (your calendar type)")
|
||||
return
|
||||
}
|
||||
_, m, d := parseGregorianDateKey(date)
|
||||
if m < 1 || m > 12 || d < 1 || d > 31 {
|
||||
h.sendText(msg.Chat.ID, "Invalid date.")
|
||||
h.sendText(ctx, msg.Chat.ID, "Invalid date.")
|
||||
return
|
||||
}
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading profile")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
loc := loadLocation(user.Timezone)
|
||||
date = userDateToGregorian(date, user.Calendar)
|
||||
h.sendDayView(msg.Chat.ID, 0, user, date, loc, true)
|
||||
h.sendDayView(ctx, msg.Chat.ID, 0, user, date, loc, true)
|
||||
}
|
||||
|
||||
// handleHistoryCallback routes calendar-related callback data to the right handler.
|
||||
func (h *Handler) handleHistoryCallback(chatID int64, msgID int, callbackID, data string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) handleHistoryCallback(ctx context.Context, chatID int64, msgID int, callbackID, data string) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -57,7 +60,7 @@ func (h *Handler) handleHistoryCallback(chatID int64, msgID int, callbackID, dat
|
||||
loc := loadLocation(user.Timezone)
|
||||
|
||||
if data == "history" {
|
||||
h.editCalendar(chatID, msgID, 0, 0)
|
||||
h.editCalendar(ctx, chatID, msgID, 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,70 +70,70 @@ func (h *Handler) handleHistoryCallback(chatID int64, msgID int, callbackID, dat
|
||||
|
||||
// Navigate to previous month
|
||||
if n, _ := fmt.Sscanf(data, "cal_prev_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(chatID, msgID, y, m)
|
||||
h.editCalendar(ctx, chatID, msgID, y, m)
|
||||
return
|
||||
}
|
||||
// Navigate to next month
|
||||
if n, _ := fmt.Sscanf(data, "cal_next_%d_%d", &y, &m); n == 2 {
|
||||
h.editCalendar(chatID, msgID, y, m)
|
||||
h.editCalendar(ctx, chatID, msgID, y, m)
|
||||
return
|
||||
}
|
||||
// Open a specific day
|
||||
if n, _ := fmt.Sscanf(data, "cal_day_%s", &date); n == 1 && len(date) == 10 {
|
||||
h.editDayView(chatID, msgID, user, date, loc)
|
||||
h.editDayView(ctx, chatID, msgID, user, date, loc)
|
||||
return
|
||||
}
|
||||
// Back to day view from event editing
|
||||
if n, _ := fmt.Sscanf(data, "back_day_%d", &eid); n == 1 {
|
||||
h.backToDayView(chatID, msgID, user, eid, loc)
|
||||
h.backToDayView(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Set event work type
|
||||
var wtid int64
|
||||
if n, _ := fmt.Sscanf(data, "settype_%d_%d", &eid, &wtid); n == 2 {
|
||||
h.setEventWorkType(chatID, msgID, user, eid, wtid, loc)
|
||||
h.setEventWorkType(ctx, chatID, msgID, user, eid, wtid, loc)
|
||||
return
|
||||
}
|
||||
// Show work type picker for an event
|
||||
if n, _ := fmt.Sscanf(data, "edit_type_%d", &eid); n == 1 {
|
||||
h.editEventType(chatID, msgID, user, eid, loc)
|
||||
h.editEventType(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Delete event confirmation prompt
|
||||
if n, _ := fmt.Sscanf(data, "delete_%d", &eid); n == 1 {
|
||||
h.deleteEventPrompt(chatID, msgID, user, eid, loc)
|
||||
h.deleteEventPrompt(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Confirm event deletion
|
||||
if n, _ := fmt.Sscanf(data, "delconf_%d", &eid); n == 1 {
|
||||
h.deleteEventConfirm(chatID, msgID, user, eid, loc)
|
||||
h.deleteEventConfirm(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Show hour picker for event time
|
||||
if n, _ := fmt.Sscanf(data, "edit_time_%d", &eid); n == 1 {
|
||||
h.editEventTime(chatID, msgID, user, eid, loc)
|
||||
h.editEventTime(ctx, chatID, msgID, user, eid, loc)
|
||||
return
|
||||
}
|
||||
// Set event time (hour+minute)
|
||||
var hh, mm int
|
||||
if n, _ := fmt.Sscanf(data, "edittm_%d_%d_%d", &eid, &hh, &mm); n == 3 {
|
||||
h.editEventTimeSet(chatID, msgID, user, eid, hh, mm, loc)
|
||||
h.editEventTimeSet(ctx, chatID, msgID, user, eid, hh, mm, loc)
|
||||
return
|
||||
}
|
||||
// Show minute picker (hour already chosen)
|
||||
if n, _ := fmt.Sscanf(data, "edittm_%d_%d", &eid, &hh); n == 2 {
|
||||
h.editEventTimeMin(chatID, msgID, user, eid, hh, loc)
|
||||
h.editEventTimeMin(ctx, chatID, msgID, user, eid, hh, loc)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// sendCalendar sends a calendar view as a new message.
|
||||
func (h *Handler) sendCalendar(chatID int64, msgID int, year, month int) {
|
||||
h.editCalendar(chatID, msgID, year, month)
|
||||
func (h *Handler) sendCalendar(ctx context.Context, chatID int64, msgID int, year, month int) {
|
||||
h.editCalendar(ctx, chatID, msgID, year, month)
|
||||
}
|
||||
|
||||
// editCalendar renders a monthly calendar grid with event indicators and navigation.
|
||||
func (h *Handler) editCalendar(chatID int64, msgID int, year, month int) {
|
||||
func (h *Handler) editCalendar(ctx context.Context, chatID int64, msgID int, year, month int) {
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -172,20 +175,20 @@ func (h *Handler) editCalendar(chatID int64, msgID int, year, month int) {
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
kbRows := [][]models.InlineKeyboardButton{}
|
||||
|
||||
// Weekday header row
|
||||
headerRow := []tgbotapi.InlineKeyboardButton{}
|
||||
headerRow := []models.InlineKeyboardButton{}
|
||||
for _, wn := range cm.weekDays {
|
||||
headerRow = append(headerRow, tgbotapi.NewInlineKeyboardButtonData(wn, "noop"))
|
||||
headerRow = append(headerRow, models.InlineKeyboardButton{Text: wn, CallbackData: "noop"})
|
||||
}
|
||||
kbRows = append(kbRows, headerRow)
|
||||
|
||||
// Day cells
|
||||
row := []tgbotapi.InlineKeyboardButton{}
|
||||
row := []models.InlineKeyboardButton{}
|
||||
for i, d := range cm.days {
|
||||
if d.dayNum == 0 {
|
||||
row = append(row, tgbotapi.NewInlineKeyboardButtonData(" ", "noop"))
|
||||
row = append(row, models.InlineKeyboardButton{Text: " ", CallbackData: "noop"})
|
||||
} else {
|
||||
label := fmt.Sprintf("%d", d.dayNum)
|
||||
if d.date == todayKey {
|
||||
@@ -193,12 +196,12 @@ func (h *Handler) editCalendar(chatID int64, msgID int, year, month int) {
|
||||
} else if hasEvent[d.date] {
|
||||
label = fmt.Sprintf("%d*", d.dayNum)
|
||||
}
|
||||
row = append(row, tgbotapi.NewInlineKeyboardButtonData(label, "cal_day_"+d.date))
|
||||
row = append(row, models.InlineKeyboardButton{Text: label, CallbackData: "cal_day_" + d.date})
|
||||
}
|
||||
if len(row) == 7 || i == len(cm.days)-1 {
|
||||
// Pad the last row to 7 columns so buttons render evenly
|
||||
for len(row) < 7 {
|
||||
row = append(row, tgbotapi.NewInlineKeyboardButtonData(" ", "noop"))
|
||||
row = append(row, models.InlineKeyboardButton{Text: " ", CallbackData: "noop"})
|
||||
}
|
||||
kbRows = append(kbRows, row)
|
||||
row = nil
|
||||
@@ -208,31 +211,31 @@ func (h *Handler) editCalendar(chatID int64, msgID int, year, month int) {
|
||||
prevY, prevM, nextY, nextM := navMonth(year, month)
|
||||
|
||||
// Navigation row
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("<", fmt.Sprintf("cal_prev_%d_%d", prevY, prevM)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Today", "history"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(">", fmt.Sprintf("cal_next_%d_%d", nextY, nextM)),
|
||||
))
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: "<", CallbackData: fmt.Sprintf("cal_prev_%d_%d", prevY, prevM)},
|
||||
{Text: "Today", CallbackData: "history"},
|
||||
{Text: ">", CallbackData: fmt.Sprintf("cal_next_%d_%d", nextY, nextM)},
|
||||
})
|
||||
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
))
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
})
|
||||
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
h.sendOrEdit(chatID, msgID, text, &kb)
|
||||
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
||||
h.sendOrEdit(ctx, chatID, msgID, text, &kb)
|
||||
}
|
||||
|
||||
// editDayView opens an existing message as a day view.
|
||||
func (h *Handler) editDayView(chatID int64, msgID int, user *db.User, date string, loc *time.Location) {
|
||||
h.sendDayView(chatID, msgID, user, date, loc, false)
|
||||
func (h *Handler) editDayView(ctx context.Context, chatID int64, msgID int, user *db.User, date string, loc *time.Location) {
|
||||
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
||||
}
|
||||
|
||||
// sendDayView displays all events for a given date with inline edit/delete buttons.
|
||||
func (h *Handler) sendDayView(chatID int64, msgID int, user *db.User, date string, loc *time.Location, isNewMsg bool) {
|
||||
func (h *Handler) sendDayView(ctx context.Context, chatID int64, msgID int, user *db.User, date string, loc *time.Location, isNewMsg bool) {
|
||||
events, err := h.DB.EventsForDayByDate(user.ID, date)
|
||||
if err != nil {
|
||||
if isNewMsg {
|
||||
h.sendText(chatID, "Error loading events")
|
||||
h.sendText(ctx, chatID, "Error loading events")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -240,17 +243,17 @@ func (h *Handler) sendDayView(chatID int64, msgID int, user *db.User, date strin
|
||||
text := fmt.Sprintf("Date: %s", formatDateForCalendar(date, user.Calendar))
|
||||
if len(events) == 0 {
|
||||
text += "\nNo events for this day."
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Calendar", "history"),
|
||||
),
|
||||
)
|
||||
h.sendOrEdit(chatID, msgID, text, &kb)
|
||||
kb := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{{Text: "Back to Calendar", CallbackData: "history"}},
|
||||
},
|
||||
}
|
||||
h.sendOrEdit(ctx, chatID, msgID, text, &kb)
|
||||
return
|
||||
}
|
||||
|
||||
text += "\n\nEvents:"
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
kbRows := [][]models.InlineKeyboardButton{}
|
||||
for _, e := range events {
|
||||
t := time.Unix(e.OccurredAt, 0).In(loc).Format("15:04")
|
||||
label := "IN"
|
||||
@@ -269,100 +272,94 @@ func (h *Handler) sendDayView(chatID int64, msgID int, user *db.User, date strin
|
||||
}
|
||||
text += fmt.Sprintf("\n%s %s%s%s", label, t, wt, note)
|
||||
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Time %s", t), fmt.Sprintf("edit_time_%d", e.ID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Type", fmt.Sprintf("edit_type_%d", e.ID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("DEL", fmt.Sprintf("delete_%d", e.ID)),
|
||||
))
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: fmt.Sprintf("Time %s", t), CallbackData: fmt.Sprintf("edit_time_%d", e.ID)},
|
||||
{Text: "Type", CallbackData: fmt.Sprintf("edit_type_%d", e.ID)},
|
||||
{Text: "DEL", CallbackData: fmt.Sprintf("delete_%d", e.ID), Style: "danger"},
|
||||
})
|
||||
}
|
||||
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Calendar", "history"),
|
||||
))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
h.sendOrEdit(chatID, msgID, text, &kb)
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Calendar", CallbackData: "history"},
|
||||
})
|
||||
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
||||
h.sendOrEdit(ctx, chatID, msgID, text, &kb)
|
||||
}
|
||||
|
||||
// editEventType shows a work type picker for a specific event.
|
||||
func (h *Handler) editEventType(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
func (h *Handler) editEventType(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
wts, err := h.DB.GetWorkTypes()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text := "Select new work type:"
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
kbRows := [][]models.InlineKeyboardButton{}
|
||||
for _, wt := range wts {
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(wt.Name, fmt.Sprintf("settype_%d_%d", eventID, wt.ID)),
|
||||
))
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: wt.Name, CallbackData: fmt.Sprintf("settype_%d_%d", eventID, wt.ID)},
|
||||
})
|
||||
}
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back", fmt.Sprintf("back_day_%d", eventID)),
|
||||
))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{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})
|
||||
}
|
||||
|
||||
// setEventWorkType updates an event's work type and returns to the day view.
|
||||
func (h *Handler) setEventWorkType(chatID int64, msgID int, user *db.User, eventID, workTypeID int64, loc *time.Location) {
|
||||
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)
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
h.sendDayView(chatID, msgID, user, t.Format("2006-01-02"), loc, false)
|
||||
h.sendDayView(ctx, chatID, msgID, user, t.Format("2006-01-02"), loc, false)
|
||||
}
|
||||
|
||||
// editEventTime shows an hour picker for changing an event's time.
|
||||
func (h *Handler) editEventTime(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
func (h *Handler) editEventTime(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
text := "Select hour:"
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
hourRow := []tgbotapi.InlineKeyboardButton{}
|
||||
kbRows := [][]models.InlineKeyboardButton{}
|
||||
hourRow := []models.InlineKeyboardButton{}
|
||||
for hh := 0; hh < 24; hh++ {
|
||||
hourRow = append(hourRow, tgbotapi.NewInlineKeyboardButtonData(
|
||||
fmt.Sprintf("%02d", hh),
|
||||
fmt.Sprintf("edittm_%d_%02d", eventID, hh),
|
||||
))
|
||||
hourRow = append(hourRow, models.InlineKeyboardButton{
|
||||
Text: fmt.Sprintf("%02d", hh),
|
||||
CallbackData: fmt.Sprintf("edittm_%d_%02d", eventID, hh),
|
||||
})
|
||||
if len(hourRow) == 6 || hh == 23 {
|
||||
kbRows = append(kbRows, hourRow)
|
||||
hourRow = nil
|
||||
}
|
||||
}
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back", fmt.Sprintf("back_day_%d", eventID)),
|
||||
))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{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})
|
||||
}
|
||||
|
||||
// editEventTimeMin shows a minute picker (15-min intervals) after hour selection.
|
||||
func (h *Handler) editEventTimeMin(chatID int64, msgID int, user *db.User, eventID int64, hh int, loc *time.Location) {
|
||||
func (h *Handler) editEventTimeMin(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, hh int, loc *time.Location) {
|
||||
text := fmt.Sprintf("Select minute for hour %02d:", hh)
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
minRow := []tgbotapi.InlineKeyboardButton{}
|
||||
kbRows := [][]models.InlineKeyboardButton{}
|
||||
minRow := []models.InlineKeyboardButton{}
|
||||
for _, mm := range []int{0, 15, 30, 45} {
|
||||
minRow = append(minRow, tgbotapi.NewInlineKeyboardButtonData(
|
||||
fmt.Sprintf("%02d", mm),
|
||||
fmt.Sprintf("edittm_%d_%02d_%02d", eventID, hh, mm),
|
||||
))
|
||||
minRow = append(minRow, models.InlineKeyboardButton{
|
||||
Text: fmt.Sprintf("%02d", mm),
|
||||
CallbackData: fmt.Sprintf("edittm_%d_%02d_%02d", eventID, hh, mm),
|
||||
})
|
||||
}
|
||||
kbRows = append(kbRows, minRow)
|
||||
kbRows = append(kbRows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back", fmt.Sprintf("edit_time_%d", eventID)),
|
||||
))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: "Back", CallbackData: fmt.Sprintf("edit_time_%d", eventID)},
|
||||
})
|
||||
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: text, ReplyMarkup: &kb})
|
||||
}
|
||||
|
||||
// editEventTimeSet applies a new time to an event, checking for overlaps.
|
||||
func (h *Handler) editEventTimeSet(chatID int64, msgID int, user *db.User, eventID int64, hh, mm int, loc *time.Location) {
|
||||
func (h *Handler) editEventTimeSet(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, hh, mm int, loc *time.Location) {
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -380,8 +377,8 @@ func (h *Handler) editEventTimeSet(chatID int64, msgID int, user *db.User, event
|
||||
}
|
||||
eTime := time.Unix(e.OccurredAt, 0)
|
||||
if newTime.Unix() == eTime.Unix() {
|
||||
h.acknowledgeCallback(chatID, msgID)
|
||||
h.sendDayView(chatID, msgID, user, date, loc, false)
|
||||
h.acknowledgeCallback(ctx, chatID, msgID)
|
||||
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -410,37 +407,36 @@ func (h *Handler) editEventTimeSet(chatID int64, msgID int, user *db.User, event
|
||||
}
|
||||
for i := 1; i < len(sorted); i++ {
|
||||
if sorted[i].typ == sorted[i-1].typ {
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Edit rejected: overlapping events. Two consecutive events must be different types (in/out).")
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Edit rejected: overlapping events. Two consecutive events must be different types (in/out)."})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.DB.DB().Exec("UPDATE events SET occurred_at=? WHERE id=?", newTime.Unix(), eventID)
|
||||
h.sendDayView(chatID, msgID, user, date, loc, false)
|
||||
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
||||
}
|
||||
|
||||
// acknowledgeCallback sends an empty acknowledgement for a callback query.
|
||||
func (h *Handler) acknowledgeCallback(chatID int64, msgID int) {
|
||||
h.Bot.Request(tgbotapi.NewCallback(fmt.Sprintf("cb_%d_%d", chatID, msgID), ""))
|
||||
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)})
|
||||
}
|
||||
|
||||
// deleteEventPrompt asks the user to confirm event deletion.
|
||||
func (h *Handler) deleteEventPrompt(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Delete this event?")
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Yes, DEL", fmt.Sprintf("delconf_%d", eventID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Cancel", fmt.Sprintf("back_day_%d", eventID)),
|
||||
),
|
||||
)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
func (h *Handler) deleteEventPrompt(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
kb := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Yes, DEL", CallbackData: fmt.Sprintf("delconf_%d", eventID), Style: "danger"},
|
||||
{Text: "Cancel", CallbackData: fmt.Sprintf("back_day_%d", eventID)},
|
||||
},
|
||||
},
|
||||
}
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Delete this event?", ReplyMarkup: &kb})
|
||||
}
|
||||
|
||||
// deleteEventConfirm deletes an event and warns if the timeline becomes invalid.
|
||||
func (h *Handler) deleteEventConfirm(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
func (h *Handler) deleteEventConfirm(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -461,21 +457,21 @@ func (h *Handler) deleteEventConfirm(chatID int64, msgID int, user *db.User, eve
|
||||
}
|
||||
}
|
||||
if hasIssue {
|
||||
h.sendText(chatID, "Warning: Timeline has consecutive events of same type after deletion. Manual repair needed.")
|
||||
h.sendText(ctx, chatID, "Warning: Timeline has consecutive events of same type after deletion. Manual repair needed.")
|
||||
}
|
||||
}
|
||||
|
||||
h.sendDayView(chatID, msgID, user, date, loc, false)
|
||||
h.sendDayView(ctx, chatID, msgID, user, date, loc, false)
|
||||
}
|
||||
|
||||
// backToDayView returns from event editing to the day view.
|
||||
func (h *Handler) backToDayView(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
func (h *Handler) backToDayView(ctx context.Context, chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) {
|
||||
event, err := h.getEventByID(user.ID, eventID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t := time.Unix(event.OccurredAt, 0).In(loc)
|
||||
h.sendDayView(chatID, msgID, user, t.Format("2006-01-02"), loc, false)
|
||||
h.sendDayView(ctx, chatID, msgID, user, t.Format("2006-01-02"), loc, false)
|
||||
}
|
||||
|
||||
// getEventByID fetches a single event by ID, scoped to the user.
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
bot "github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
"github.com/xuri/excelize/v2"
|
||||
|
||||
"worktimeBot/internal/db"
|
||||
@@ -224,10 +228,10 @@ func fmtDDHHMM(seconds int64) string {
|
||||
}
|
||||
|
||||
// handleExport processes the /export command, showing the month picker or exporting directly.
|
||||
func (h *Handler) handleExport(msg *tgbotapi.Message) {
|
||||
func (h *Handler) handleExport(ctx context.Context, msg *models.Message) {
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading profile")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
loc := loadLocation(user.Timezone)
|
||||
@@ -241,23 +245,27 @@ func (h *Handler) handleExport(msg *tgbotapi.Message) {
|
||||
cy, cm, _ = gregorianToHijri(cy, cm, now.Day())
|
||||
}
|
||||
|
||||
if len(msg.Text) > 7 {
|
||||
if n, _ := fmt.Sscanf(msg.Text[7:], "%d-%d", &cy, &cm); n == 2 {
|
||||
args := strings.Fields(msg.Text)
|
||||
if len(args) > 0 && args[0][0] == '/' {
|
||||
args = args[1:]
|
||||
}
|
||||
if len(args) >= 1 {
|
||||
if n, _ := fmt.Sscanf(args[0], "%d-%d", &cy, &cm); n == 2 {
|
||||
if cy < 0 || cm < 1 || cm > 12 {
|
||||
h.sendText(msg.Chat.ID, "Invalid date. Use: /export YYYY-MM")
|
||||
h.sendText(ctx, msg.Chat.ID, "Invalid date. Use: /export YYYY-MM")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.sendExportDirect(msg.Chat.ID, user, cy, cm)
|
||||
h.sendExportDirect(ctx, msg.Chat.ID, user, cy, cm)
|
||||
}
|
||||
|
||||
// sendExportDirect generates and sends an Excel report for the given calendar month.
|
||||
func (h *Handler) sendExportDirect(chatID int64, user *db.User, calYear, calMonth int) {
|
||||
func (h *Handler) sendExportDirect(ctx context.Context, chatID int64, user *db.User, calYear, calMonth int) {
|
||||
last, err := h.DB.GetLastEvent(user.ID)
|
||||
if err == nil && last != nil && last.EventType == "in" {
|
||||
h.sendText(chatID, "You are currently clocked in. Please clock out first, then try again.")
|
||||
h.sendText(ctx, chatID, "You are currently clocked in. Please clock out first, then try again.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -277,22 +285,24 @@ func (h *Handler) sendExportDirect(chatID int64, user *db.User, calYear, calMont
|
||||
data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, calYear, calMonth, start, end)
|
||||
if err != nil {
|
||||
slog.Error("generate report", "error", err)
|
||||
h.sendText(chatID, "Error generating report")
|
||||
h.sendText(ctx, chatID, "Error generating report")
|
||||
return
|
||||
}
|
||||
slog.Info("report generated", "bytes", len(data))
|
||||
doc := tgbotapi.NewDocument(chatID, tgbotapi.FileBytes{
|
||||
Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", calYear, calMonth)),
|
||||
Bytes: data,
|
||||
})
|
||||
if _, err := h.Bot.Send(doc); err != nil {
|
||||
if _, err := h.Bot.SendDocument(ctx, &bot.SendDocumentParams{
|
||||
ChatID: chatID,
|
||||
Document: &models.InputFileUpload{
|
||||
Filename: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", calYear, calMonth)),
|
||||
Data: bytes.NewReader(data),
|
||||
},
|
||||
}); err != nil {
|
||||
slog.Error("send document", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// exportCallback opens the export month picker.
|
||||
func (h *Handler) exportCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) exportCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -306,16 +316,16 @@ func (h *Handler) exportCallback(chatID int64, msgID int, callbackID string) {
|
||||
case "hijri":
|
||||
cy, cm, _ = gregorianToHijri(cy, cm, now.Day())
|
||||
}
|
||||
h.editExportMonthPicker(chatID, msgID, cy, cm, user)
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, cy, cm, user)
|
||||
}
|
||||
|
||||
// sendExportMonthPicker sends the export month picker as a new message.
|
||||
func (h *Handler) sendExportMonthPicker(chatID int64, msgID int, year, month int, user *db.User) {
|
||||
h.editExportMonthPicker(chatID, msgID, year, month, user)
|
||||
func (h *Handler) sendExportMonthPicker(ctx context.Context, chatID int64, msgID int, year, month int, user *db.User) {
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, year, month, user)
|
||||
}
|
||||
|
||||
// editExportMonthPicker renders a year-based month picker with 12 month buttons.
|
||||
func (h *Handler) editExportMonthPicker(chatID int64, msgID int, year, _ int, user *db.User) {
|
||||
func (h *Handler) editExportMonthPicker(ctx context.Context, chatID int64, msgID int, year, _ int, user *db.User) {
|
||||
months := gregMonthNames
|
||||
switch user.Calendar {
|
||||
case "jalali":
|
||||
@@ -324,33 +334,36 @@ func (h *Handler) editExportMonthPicker(chatID int64, msgID int, year, _ int, us
|
||||
months = hijriMonthNames
|
||||
}
|
||||
|
||||
kbRows := [][]tgbotapi.InlineKeyboardButton{
|
||||
kbRows := [][]models.InlineKeyboardButton{
|
||||
// Year navigation
|
||||
{
|
||||
tgbotapi.NewInlineKeyboardButtonData("<", fmt.Sprintf("exp_year_prev_%d", year)),
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%d", year), "noop"),
|
||||
tgbotapi.NewInlineKeyboardButtonData(">", fmt.Sprintf("exp_year_next_%d", year)),
|
||||
{Text: "<", CallbackData: fmt.Sprintf("exp_year_prev_%d", year)},
|
||||
{Text: fmt.Sprintf("%d", year), CallbackData: "noop"},
|
||||
{Text: ">", CallbackData: fmt.Sprintf("exp_year_next_%d", year)},
|
||||
},
|
||||
}
|
||||
|
||||
// 4 columns x 3 rows of month buttons
|
||||
for i := 0; i < 12; i += 4 {
|
||||
row := []tgbotapi.InlineKeyboardButton{}
|
||||
row := []models.InlineKeyboardButton{}
|
||||
for j := i; j < i+4 && j < 12; j++ {
|
||||
row = append(row, tgbotapi.NewInlineKeyboardButtonData(
|
||||
months[j],
|
||||
fmt.Sprintf("exp_do_%d_%d", year, j+1),
|
||||
))
|
||||
row = append(row, models.InlineKeyboardButton{
|
||||
Text: months[j], CallbackData: fmt.Sprintf("exp_do_%d_%d", year, j+1),
|
||||
})
|
||||
}
|
||||
kbRows = append(kbRows, row)
|
||||
}
|
||||
|
||||
kbRows = append(kbRows, []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
kbRows = append(kbRows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
})
|
||||
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...)
|
||||
h.sendOrEdit(chatID, msgID, "Select month to export:", &kb)
|
||||
kb := models.InlineKeyboardMarkup{InlineKeyboard: kbRows}
|
||||
if msgID == 0 {
|
||||
h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: "Select month to export:", ReplyMarkup: &kb})
|
||||
} else {
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{ChatID: chatID, MessageID: msgID, Text: "Select month to export:", ReplyMarkup: &kb})
|
||||
}
|
||||
}
|
||||
|
||||
// formatMonthTitle returns the localized month name and year for a given calendar.
|
||||
@@ -366,8 +379,8 @@ func formatMonthTitle(cal string, year, month int) string {
|
||||
}
|
||||
|
||||
// handleExportCallback routes export inline button presses (year nav, month select).
|
||||
func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) handleExportCallback(ctx context.Context, chatID int64, msgID int, callbackID, data string) {
|
||||
defer h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -377,12 +390,12 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data
|
||||
|
||||
// Navigate to previous year
|
||||
if n, _ := fmt.Sscanf(data, "exp_year_prev_%d", &y); n == 1 {
|
||||
h.editExportMonthPicker(chatID, msgID, y-1, 0, user)
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, y-1, 0, user)
|
||||
return
|
||||
}
|
||||
// Navigate to next year
|
||||
if n, _ := fmt.Sscanf(data, "exp_year_next_%d", &y); n == 1 {
|
||||
h.editExportMonthPicker(chatID, msgID, y+1, 0, user)
|
||||
h.editExportMonthPicker(ctx, chatID, msgID, y+1, 0, user)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -390,26 +403,30 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data
|
||||
if n, _ := fmt.Sscanf(data, "exp_do_%d_%d", &y, &m); n == 2 {
|
||||
last, err := h.DB.GetLastEvent(user.ID)
|
||||
if err == nil && last != nil && last.EventType == "in" {
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
),
|
||||
)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "You are currently clocked in. Please clock out first, then try again.")
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||
ChatID: chatID,
|
||||
MessageID: msgID,
|
||||
Text: "You are currently clocked in. Please clock out first, then try again.",
|
||||
ReplyMarkup: &models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
startStr, endStr := monthGregorianRange(user.Calendar, y, m)
|
||||
start, err := time.Parse("2006-01-02", startStr)
|
||||
if err != nil {
|
||||
h.sendText(chatID, "Error processing date")
|
||||
h.sendText(ctx, chatID, "Error processing date")
|
||||
return
|
||||
}
|
||||
end, err := time.Parse("2006-01-02", endStr)
|
||||
if err != nil {
|
||||
h.sendText(chatID, "Error processing date")
|
||||
h.sendText(ctx, chatID, "Error processing date")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -417,24 +434,42 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data
|
||||
data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, y, m, start, end)
|
||||
if err != nil {
|
||||
slog.Error("generate report", "error", err)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Error generating report")
|
||||
kb := backKeyboard()
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||
ChatID: chatID,
|
||||
MessageID: msgID,
|
||||
Text: "Error generating report",
|
||||
ReplyMarkup: &models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("report generated", "bytes", len(data))
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, "Report ready:")
|
||||
kb := backKeyboard()
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
|
||||
doc := tgbotapi.NewDocument(chatID, tgbotapi.FileBytes{
|
||||
Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", y, m)),
|
||||
Bytes: data,
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||
ChatID: chatID,
|
||||
MessageID: msgID,
|
||||
Text: "Report ready:",
|
||||
ReplyMarkup: &models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{
|
||||
{Text: "Back to Menu", CallbackData: "back_menu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if _, err := h.Bot.Send(doc); err != nil {
|
||||
|
||||
if _, err := h.Bot.SendDocument(ctx, &bot.SendDocumentParams{
|
||||
ChatID: chatID,
|
||||
Document: &models.InputFileUpload{
|
||||
Filename: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", y, m)),
|
||||
Data: bytes.NewReader(data),
|
||||
},
|
||||
}); err != nil {
|
||||
slog.Error("send document", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"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"
|
||||
)
|
||||
@@ -54,13 +56,33 @@ func (h *Handler) buildStatusText(chatID int64) string {
|
||||
}
|
||||
|
||||
// backToMenu returns to the main menu from any sub-menu.
|
||||
func (h *Handler) backToMenu(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
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)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
kb := mainKeyboard()
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// buildDailyReport builds a formatted daily report string for a given user/day.
|
||||
@@ -119,7 +141,7 @@ func (h *Handler) buildDailyReport(user *db.User, day *db.Day, events []db.Event
|
||||
}
|
||||
|
||||
// SendDailyReport sends the automated daily report to the user.
|
||||
func (h *Handler) SendDailyReport(userID int64, chatID int64) {
|
||||
func (h *Handler) SendDailyReport(ctx context.Context, userID int64, chatID int64) {
|
||||
user, err := h.DB.GetUserByChatID(chatID)
|
||||
if err != nil {
|
||||
slog.Error("daily report: user not found", "user_id", userID)
|
||||
@@ -143,7 +165,6 @@ func (h *Handler) SendDailyReport(userID int64, chatID int64) {
|
||||
return
|
||||
}
|
||||
text := h.buildDailyReport(user, day, events)
|
||||
reply := tgbotapi.NewMessage(chatID, text)
|
||||
h.Bot.Send(reply)
|
||||
h.Bot.SendMessage(ctx, &bot.SendMessageParams{ChatID: chatID, Text: text})
|
||||
h.DB.MarkReportSent(user.ID, today)
|
||||
}
|
||||
|
||||
@@ -1,172 +1,174 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
bot "github.com/go-telegram/bot"
|
||||
"github.com/go-telegram/bot/models"
|
||||
|
||||
"worktimeBot/internal/db"
|
||||
)
|
||||
|
||||
// handleSetReportTime parses a HH:MM argument and updates the user's report time.
|
||||
func (h *Handler) handleSetReportTime(msg *tgbotapi.Message) {
|
||||
arg := msg.CommandArguments()
|
||||
if arg == "" {
|
||||
func (h *Handler) handleSetReportTime(ctx context.Context, msg *models.Message) {
|
||||
args := ""
|
||||
if len(msg.Text) > 16 { // "/setreporttime " is 15 chars
|
||||
args = strings.TrimSpace(msg.Text[15:])
|
||||
}
|
||||
if args == "" {
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading profile")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
h.sendText(msg.Chat.ID, fmt.Sprintf("Current report time: %s\nUsage: /setreporttime HH:MM (24-hour)", user.ReportTime))
|
||||
h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Current report time: %s\nUsage: /setreporttime HH:MM (24-hour)", user.ReportTime))
|
||||
return
|
||||
}
|
||||
if len(arg) != 5 || arg[2] != ':' {
|
||||
h.sendText(msg.Chat.ID, "Invalid format. Usage: /setreporttime HH:MM (24-hour)")
|
||||
if len(args) != 5 || args[2] != ':' {
|
||||
h.sendText(ctx, msg.Chat.ID, "Invalid format. Usage: /setreporttime HH:MM (24-hour)")
|
||||
return
|
||||
}
|
||||
hours, err1 := strconv.Atoi(arg[:2])
|
||||
mins, err2 := strconv.Atoi(arg[3:])
|
||||
hours, err1 := strconv.Atoi(args[:2])
|
||||
mins, err2 := strconv.Atoi(args[3:])
|
||||
if err1 != nil || err2 != nil || hours < 0 || hours > 23 || mins < 0 || mins > 59 {
|
||||
h.sendText(msg.Chat.ID, "Invalid time. Usage: /setreporttime HH:MM (24-hour)")
|
||||
h.sendText(ctx, msg.Chat.ID, "Invalid time. Usage: /setreporttime HH:MM (24-hour)")
|
||||
return
|
||||
}
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error updating report time")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error updating report time")
|
||||
return
|
||||
}
|
||||
user.ReportTime = arg
|
||||
user.ReportTime = args
|
||||
if err := h.DB.UpdateUser(user); err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error updating report time")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error updating report time")
|
||||
return
|
||||
}
|
||||
h.sendText(msg.Chat.ID, fmt.Sprintf("Report time set to %s", arg))
|
||||
h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Report time set to %s", args))
|
||||
}
|
||||
|
||||
// handleAccentMsg shows the accent color picker.
|
||||
func (h *Handler) handleAccentMsg(msg *tgbotapi.Message) {
|
||||
func (h *Handler) handleAccentMsg(ctx context.Context, msg *models.Message) {
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading profile")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
text, kb := h.buildAccentKeyboard(user)
|
||||
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})
|
||||
}
|
||||
|
||||
// handleSetTimezone validates and updates the user's IANA timezone.
|
||||
func (h *Handler) handleSetTimezone(msg *tgbotapi.Message) {
|
||||
arg := msg.CommandArguments()
|
||||
if arg == "" {
|
||||
func (h *Handler) handleSetTimezone(ctx context.Context, msg *models.Message) {
|
||||
args := ""
|
||||
if len(msg.Text) > 13 { // "/settimezone " is 12 chars
|
||||
args = strings.TrimSpace(msg.Text[13:])
|
||||
}
|
||||
if args == "" {
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading profile")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
h.sendText(msg.Chat.ID, fmt.Sprintf("Current timezone: %s\nUsage: /settimezone <IANA timezone>\nExamples: Asia/Tehran, Europe/London, America/New_York", user.Timezone))
|
||||
h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Current timezone: %s\nUsage: /settimezone <IANA timezone>\nExamples: Asia/Tehran, Europe/London, America/New_York", user.Timezone))
|
||||
return
|
||||
}
|
||||
loc, err := time.LoadLocation(arg)
|
||||
loc, err := time.LoadLocation(args)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, fmt.Sprintf("Invalid timezone: %s\nUse IANA format, e.g. Asia/Tehran, Europe/London", arg))
|
||||
h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Invalid timezone: %s\nUse IANA format, e.g. Asia/Tehran, Europe/London", args))
|
||||
return
|
||||
}
|
||||
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||
if err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error loading profile")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||
return
|
||||
}
|
||||
user.Timezone = arg
|
||||
user.Timezone = args
|
||||
if err := h.DB.UpdateUser(user); err != nil {
|
||||
h.sendText(msg.Chat.ID, "Error saving timezone")
|
||||
h.sendText(ctx, msg.Chat.ID, "Error saving timezone")
|
||||
return
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
h.sendText(msg.Chat.ID, fmt.Sprintf("Timezone set to %s (current time: %s)", arg, now.Format("15:04")))
|
||||
h.sendText(ctx, msg.Chat.ID, fmt.Sprintf("Timezone set to %s (current time: %s)", args, now.Format("15:04")))
|
||||
}
|
||||
|
||||
// settingsCallback shows the settings inline menu.
|
||||
func (h *Handler) settingsCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) settingsCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, kb := h.buildSettingsKeyboard(user)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// buildSettingsKeyboard returns the settings menu text and inline keyboard.
|
||||
func (h *Handler) buildSettingsKeyboard(user *db.User) (string, tgbotapi.InlineKeyboardMarkup) {
|
||||
func (h *Handler) buildSettingsKeyboard(user *db.User) (string, models.InlineKeyboardMarkup) {
|
||||
reportStatus := "disabled"
|
||||
if user.ReportEnabled {
|
||||
reportStatus = "enabled"
|
||||
}
|
||||
rows := [][]tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Timezone: %s", user.Timezone), "timezone"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Accent: %s", user.ExportAccent), "accent"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Calendar: %s", user.Calendar), "caltype"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Report: %s", reportStatus), "reporttoggle"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("Report time: %s", user.ReportTime), "reporttime"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("History", "history"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Menu", "back_menu"),
|
||||
),
|
||||
rows := [][]models.InlineKeyboardButton{
|
||||
{{Text: fmt.Sprintf("Timezone: %s", user.Timezone), CallbackData: "timezone"}},
|
||||
{{Text: fmt.Sprintf("Accent: %s", user.ExportAccent), CallbackData: "accent"}},
|
||||
{{Text: fmt.Sprintf("Calendar: %s", user.Calendar), CallbackData: "caltype"}},
|
||||
{{Text: fmt.Sprintf("Report: %s", reportStatus), CallbackData: "reporttoggle"}},
|
||||
{{Text: fmt.Sprintf("Report time: %s", user.ReportTime), CallbackData: "reporttime"}},
|
||||
{{Text: "History", CallbackData: "history"}},
|
||||
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
||||
}
|
||||
return "Settings:", tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
return "Settings:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
||||
}
|
||||
|
||||
// accentCallback shows the accent color picker (inline edit).
|
||||
func (h *Handler) accentCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) accentCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, kb := h.buildAccentKeyboard(user)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// timezoneCallback shows the current timezone and instructions to change it.
|
||||
func (h *Handler) timezoneCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) timezoneCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text := fmt.Sprintf("Current timezone: %s\n\nUse /settimezone <name> to change it.\nExamples: Asia/Tehran, Europe/London, America/New_York", user.Timezone)
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
|
||||
),
|
||||
)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
kb := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{{Text: "Back to Settings", CallbackData: "back_settings"}},
|
||||
},
|
||||
}
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||
ChatID: chatID,
|
||||
MessageID: msgID,
|
||||
Text: text,
|
||||
ReplyMarkup: &kb,
|
||||
})
|
||||
}
|
||||
|
||||
// buildAccentKeyboard returns the accent color picker keyboard.
|
||||
func (h *Handler) buildAccentKeyboard(user *db.User) (string, tgbotapi.InlineKeyboardMarkup) {
|
||||
func (h *Handler) buildAccentKeyboard(user *db.User) (string, models.InlineKeyboardMarkup) {
|
||||
type accentOption struct {
|
||||
name string
|
||||
color string
|
||||
@@ -177,25 +179,25 @@ func (h *Handler) buildAccentKeyboard(user *db.User) (string, tgbotapi.InlineKey
|
||||
{"rose", "Pink"},
|
||||
{"catppuccin", "Purple"},
|
||||
}
|
||||
rows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
rows := [][]models.InlineKeyboardButton{}
|
||||
for _, a := range accents {
|
||||
label := fmt.Sprintf("%s (%s)", a.name, a.color)
|
||||
if a.name == user.ExportAccent {
|
||||
label = "> " + label
|
||||
}
|
||||
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(label, "accent_"+a.name),
|
||||
))
|
||||
rows = append(rows, []models.InlineKeyboardButton{
|
||||
{Text: label, CallbackData: "accent_" + a.name},
|
||||
})
|
||||
}
|
||||
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
|
||||
))
|
||||
return "Select accent color:", tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
rows = append(rows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Settings", CallbackData: "back_settings"},
|
||||
})
|
||||
return "Select accent color:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
||||
}
|
||||
|
||||
// selectAccent sets the user's export accent color after validation.
|
||||
func (h *Handler) selectAccent(chatID int64, msgID int, callbackID, name string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) selectAccent(ctx context.Context, chatID int64, msgID int, callbackID, name string) {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -208,87 +210,99 @@ func (h *Handler) selectAccent(chatID int64, msgID int, callbackID, name string)
|
||||
if err := h.DB.UpdateUser(user); err != nil {
|
||||
return
|
||||
}
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, fmt.Sprintf("Accent set to: %s", name))
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
|
||||
),
|
||||
)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
kb := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{{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,
|
||||
})
|
||||
}
|
||||
|
||||
// reportTimeCallback shows the current report time setting.
|
||||
func (h *Handler) reportTimeCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) reportTimeCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text := fmt.Sprintf("Current report time: %s\nUse /setreporttime HH:MM to change it.", user.ReportTime)
|
||||
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
|
||||
),
|
||||
)
|
||||
edit.ReplyMarkup = &kb
|
||||
h.Bot.Send(edit)
|
||||
kb := models.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||
{{Text: "Back to Settings", CallbackData: "back_settings"}},
|
||||
},
|
||||
}
|
||||
h.Bot.EditMessageText(ctx, &bot.EditMessageTextParams{
|
||||
ChatID: chatID,
|
||||
MessageID: msgID,
|
||||
Text: text,
|
||||
ReplyMarkup: &kb,
|
||||
})
|
||||
}
|
||||
|
||||
// backToSettings returns to the main settings menu.
|
||||
func (h *Handler) backToSettings(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) backToSettings(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, kb := h.buildSettingsKeyboard(user)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// calTypeCallback shows the calendar type selector.
|
||||
func (h *Handler) calTypeCallback(chatID int64, msgID int, callbackID string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) calTypeCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
user, err := h.getOrCreateUser(chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, kb := h.buildCalendarKeyboard(user)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// buildCalendarKeyboard returns the calendar type selector keyboard.
|
||||
func (h *Handler) buildCalendarKeyboard(user *db.User) (string, tgbotapi.InlineKeyboardMarkup) {
|
||||
func (h *Handler) buildCalendarKeyboard(user *db.User) (string, models.InlineKeyboardMarkup) {
|
||||
cals := []string{"gregorian", "jalali", "hijri"}
|
||||
calLabels := map[string]string{
|
||||
"gregorian": "Gregorian",
|
||||
"jalali": "Jalali",
|
||||
"hijri": "Hijri",
|
||||
}
|
||||
rows := [][]tgbotapi.InlineKeyboardButton{}
|
||||
rows := [][]models.InlineKeyboardButton{}
|
||||
for _, c := range cals {
|
||||
label := calLabels[c]
|
||||
if c == user.Calendar {
|
||||
label = "> " + label
|
||||
}
|
||||
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData(label, "caltype_"+c),
|
||||
))
|
||||
rows = append(rows, []models.InlineKeyboardButton{
|
||||
{Text: label, CallbackData: "caltype_" + c},
|
||||
})
|
||||
}
|
||||
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Back to Settings", "back_settings"),
|
||||
))
|
||||
return "Select calendar type:", tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
rows = append(rows, []models.InlineKeyboardButton{
|
||||
{Text: "Back to Settings", CallbackData: "back_settings"},
|
||||
})
|
||||
return "Select calendar type:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
||||
}
|
||||
|
||||
// selectCalendar sets the user's calendar after validation.
|
||||
func (h *Handler) selectCalendar(chatID int64, msgID int, callbackID, name string) {
|
||||
defer h.Bot.Request(tgbotapi.NewCallback(callbackID, ""))
|
||||
func (h *Handler) selectCalendar(ctx context.Context, chatID int64, msgID int, callbackID, name string) {
|
||||
h.Bot.AnswerCallbackQuery(ctx, &bot.AnswerCallbackQueryParams{CallbackQueryID: callbackID})
|
||||
valid := map[string]bool{"gregorian": true, "jalali": true, "hijri": true}
|
||||
if !valid[name] {
|
||||
return
|
||||
@@ -302,7 +316,10 @@ func (h *Handler) selectCalendar(chatID int64, msgID int, callbackID, name strin
|
||||
return
|
||||
}
|
||||
text, kb := h.buildSettingsKeyboard(user)
|
||||
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