Files
2026-11-08 03:46:11 +00:00

306 lines
7.7 KiB
Go

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()
}