306 lines
7.7 KiB
Go
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()
|
|
}
|