package shared import ( "errors" "strconv" "strings" "github.com/mymmrac/telego" "git.entityseven.com/rilaxik/pllist/internal/domain" ) const ( callbackVersion = "v1" MaxCallbackDataLength = 64 ) var ErrBadCallback = errors.New("invalid callback data") // CallbackAction is a stable, versioned callback identifier. Actions are short // because Telegram limits callback data to 64 bytes. type CallbackAction string const ( ActionNavLists CallbackAction = "lists" ActionNavNew CallbackAction = "new" ActionNavHelp CallbackAction = "help" ActionListOpen CallbackAction = "open_list" ActionListRename CallbackAction = "rename_list" ActionListDelete CallbackAction = "delete_list" ActionListDeleteYes CallbackAction = "delete_list_yes" ActionShareManage CallbackAction = "shares" ActionItemPage CallbackAction = "page" ActionItemAdd CallbackAction = "add" ActionItemOpen CallbackAction = "item" ActionItemManage CallbackAction = "manage" ActionItemEditTitle CallbackAction = "edit_title" ActionItemEditNote CallbackAction = "edit_note" ActionItemEditQuantity CallbackAction = "edit_qty" ActionItemClearNote CallbackAction = "clear_note" ActionItemUrgencyLow CallbackAction = "urgency_low" ActionItemUrgencyNorm CallbackAction = "urgency_normal" ActionItemUrgencyHigh CallbackAction = "urgency_high" ActionItemDone CallbackAction = "done_item" ActionItemReopen CallbackAction = "reopen_item" ActionItemReserve CallbackAction = "reserve_item" ActionItemRelease CallbackAction = "release_item" ActionItemDelete CallbackAction = "delete_item" ActionItemDeleteYes CallbackAction = "delete_item_yes" ActionShareAdd CallbackAction = "share_add" ActionShareRole CallbackAction = "share_role" ActionShareView CallbackAction = "share_view" ActionShareSet CallbackAction = "share_set" ActionShareRevoke CallbackAction = "share_revoke" ActionShareRevokeYes CallbackAction = "share_revoke_yes" ) // Navigation actions carry no arguments and are compared verbatim. const ( NavLists = callbackVersion + ":" + string(ActionNavLists) NavNew = callbackVersion + ":" + string(ActionNavNew) NavHelp = callbackVersion + ":" + string(ActionNavHelp) ) func ListData(id int64) string { return CallbackWithID(ActionListOpen, id) } func RenameListData(id int64) string { return CallbackWithID(ActionListRename, id) } func DeleteListData(id int64) string { return CallbackWithID(ActionListDelete, id) } func DeleteListYesData(id int64) string { return CallbackWithID(ActionListDeleteYes, id) } func SharesData(id int64) string { return CallbackWithID(ActionShareManage, id) } func CallbackPrefix(action CallbackAction) string { return callbackVersion + ":" + string(action) + ":" } func CallbackWithID(action CallbackAction, id int64) string { return encodeCallback(action, strconv.FormatInt(id, 10)) } func CallbackID(data string, action CallbackAction) (int64, error) { args, err := callbackArgs(data, action, 1) if err != nil { return 0, err } return parsePositiveID(args[0]) } // PageData and ParsePage cover the item pagination callback. func PageData(listID int64, page int) string { return encodeCallback(ActionItemPage, strconv.FormatInt(listID, 10), strconv.Itoa(page)) } func ParsePage(data string) (int64, int, error) { args, err := callbackArgs(data, ActionItemPage, 2) if err != nil { return 0, 0, err } listID, err := parsePositiveID(args[0]) if err != nil { return 0, 0, err } page, err := strconv.Atoi(args[1]) if err != nil || page < 1 { return 0, 0, ErrBadCallback } return listID, page, nil } // ShareRef identifies either an active list share or a pending one. type ShareRef struct { ListID int64 ShareID int64 Pending bool } func ShareAddData(listID int64) string { return CallbackWithID(ActionShareAdd, listID) } func ShareRoleData(listID int64, role domain.Role) string { return encodeCallback(ActionShareRole, strconv.FormatInt(listID, 10), string(role)) } func ShareViewData(ref ShareRef) string { return encodeShareRef(ActionShareView, ref) } func ShareSetData(ref ShareRef, role domain.Role) string { args := append(shareRefArgs(ref), string(role)) return encodeCallback(ActionShareSet, args...) } func ShareRevokeData(ref ShareRef) string { return encodeShareRef(ActionShareRevoke, ref) } func ShareRevokeYesData(ref ShareRef) string { return encodeShareRef(ActionShareRevokeYes, ref) } func ParseShareRole(data string) (int64, domain.Role, error) { args, err := callbackArgs(data, ActionShareRole, 2) if err != nil { return 0, "", err } listID, err := parsePositiveID(args[0]) if err != nil { return 0, "", err } role, ok := parseShareRole(args[1]) if !ok { return 0, "", ErrBadCallback } return listID, role, nil } func ParseShareRef(data string, action CallbackAction) (ShareRef, error) { args, err := callbackArgs(data, action, 3) if err != nil { return ShareRef{}, err } return parseShareRefArgs(args) } func ParseShareSet(data string) (ShareRef, domain.Role, error) { args, err := callbackArgs(data, ActionShareSet, 4) if err != nil { return ShareRef{}, "", err } ref, err := parseShareRefArgs(args[:3]) if err != nil { return ShareRef{}, "", err } role, ok := parseShareRole(args[3]) if !ok { return ShareRef{}, "", ErrBadCallback } return ref, role, nil } func encodeCallback(action CallbackAction, args ...string) string { return callbackVersion + ":" + string(action) + ":" + strings.Join(args, ":") } func callbackArgs(data string, action CallbackAction, count int) ([]string, error) { if len(data) == 0 || len(data) > MaxCallbackDataLength { return nil, ErrBadCallback } raw, ok := strings.CutPrefix(data, CallbackPrefix(action)) if !ok { return nil, ErrBadCallback } args := strings.Split(raw, ":") if len(args) != count { return nil, ErrBadCallback } for _, arg := range args { if !validCallbackArg(arg) { return nil, ErrBadCallback } } return args, nil } func validCallbackArg(arg string) bool { if len(arg) == 0 || len(arg) > 20 { return false } for i := 0; i < len(arg); i++ { char := arg[i] if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '_' { continue } return false } return true } func parsePositiveID(raw string) (int64, error) { id, err := strconv.ParseInt(raw, 10, 64) if err != nil || id <= 0 { return 0, ErrBadCallback } return id, nil } func shareRefArgs(ref ShareRef) []string { kind := "u" if ref.Pending { kind = "p" } return []string{ strconv.FormatInt(ref.ListID, 10), kind, strconv.FormatInt(ref.ShareID, 10), } } func encodeShareRef(action CallbackAction, ref ShareRef) string { return encodeCallback(action, shareRefArgs(ref)...) } func parseShareRefArgs(args []string) (ShareRef, error) { listID, err := parsePositiveID(args[0]) if err != nil { return ShareRef{}, err } shareID, err := parsePositiveID(args[2]) if err != nil { return ShareRef{}, err } pending, ok := parseShareKind(args[1]) if !ok { return ShareRef{}, ErrBadCallback } return ShareRef{ListID: listID, ShareID: shareID, Pending: pending}, nil } func parseShareKind(raw string) (bool, bool) { switch raw { case "u": return false, true case "p": return true, true default: return false, false } } func parseShareRole(raw string) (domain.Role, bool) { role := domain.Role(raw) switch role { case domain.RoleReader, domain.RoleEditor: return role, true default: return "", false } } // QueryChatID and QueryMessageID assume the callback was routed with // telegohandler.AnyCallbackQueryWithMessage. func QueryChatID(query telego.CallbackQuery) int64 { return query.Message.GetChat().ID } func QueryMessageID(query telego.CallbackQuery) int { return query.Message.GetMessageID() }