(new): compact payload type
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
- Added `CommandGroup`, `NewCommandGroup(...)`, `Plugin.CommandGroup(...)`, and `Plugin.AddCommandGroup(...)` helpers for registering prefixed command groups with shared middleware.
|
||||
- Added the `tgfmt` package with typed MarkdownV2, HTML, legacy Markdown formatting helpers, and a message entity builder.
|
||||
- Added `InlineKeyboardButtonBuilder.SetPayloadType(...)`, `InlineKeyboardButtonBuilder.SetCallbackData(...)`, and `MsgContext.NewInlineKeyboardButton(...)` helpers for payload-aware button building.
|
||||
- Added compact callback payload encoding through `BotPayloadCompact`, `BotPayloadCompactBase64`, compact inline keyboard builders, and matching `CallbackData` helpers.
|
||||
- Added `tgapi.ResponseError` so Telegram API error codes, descriptions, and response parameters remain inspectable through returned errors.
|
||||
|
||||
### Changed
|
||||
@@ -35,6 +36,7 @@
|
||||
- Added regression coverage for command group prefixing, middleware order, clone behavior, and plugin registration.
|
||||
- Added formatting coverage for escaping, composition, link destinations, HTML attributes, and legacy Markdown code blocks.
|
||||
- Added regression coverage for context-aware inline keyboard button payload encoding.
|
||||
- Added regression coverage for compact and Base64-encoded compact callback payload decoding.
|
||||
- Added regression coverage for long-polling `retry_after` handling on Telegram 429 responses.
|
||||
|
||||
## v1.0.0-rc.16
|
||||
|
||||
@@ -52,6 +52,10 @@ var (
|
||||
BotPayloadBase64 BotPayloadType = "base64"
|
||||
// BotPayloadJSON encodes callback data as a JSON string.
|
||||
BotPayloadJSON BotPayloadType = "json"
|
||||
// BotPayloadCompact encodes callback data as a compact delimited string.
|
||||
BotPayloadCompact BotPayloadType = "compact"
|
||||
// BotPayloadCompactBase64 encodes compact callback data as a Base64 string.
|
||||
BotPayloadCompactBase64 BotPayloadType = "compact-base64"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+64
-22
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
@@ -153,36 +154,77 @@ func decodeBase64Payload(s string) (CallbackData, error) {
|
||||
return decodeJSONPayload(string(b))
|
||||
}
|
||||
|
||||
func decodePayload(payloadType BotPayloadType, s string, strict bool) (CallbackData, BotPayloadType, error) {
|
||||
func encodeCompactPayload(d CallbackData) (string, error) {
|
||||
args := strings.Join(d.Args, ",")
|
||||
return d.Command + "|" + args, nil
|
||||
}
|
||||
func decodeCompactPayload(s string) (CallbackData, error) {
|
||||
values := strings.SplitN(s, "|", 2)
|
||||
if len(values) != 2 {
|
||||
return CallbackData{}, errors.New("invalid payload")
|
||||
}
|
||||
cmd, argsRaw := values[0], values[1]
|
||||
var args []string
|
||||
if argsRaw != "" {
|
||||
args = strings.Split(argsRaw, ",")
|
||||
}
|
||||
return CallbackData{Command: cmd, Args: args}, nil
|
||||
}
|
||||
func encodeCompactBase64Payload(d CallbackData) (string, error) {
|
||||
payload, _ := encodeCompactPayload(d)
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(payload)), nil
|
||||
}
|
||||
func decodeCompactBase64Payload(s string) (CallbackData, error) {
|
||||
b, err := base64.RawURLEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return CallbackData{}, err
|
||||
}
|
||||
return decodeCompactPayload(string(b))
|
||||
}
|
||||
|
||||
func decodePayloadAs(payloadType BotPayloadType, s string) (CallbackData, error) {
|
||||
switch payloadType {
|
||||
case BotPayloadBase64:
|
||||
data, err := decodeBase64Payload(s)
|
||||
if err == nil {
|
||||
return data, BotPayloadBase64, nil
|
||||
}
|
||||
if strict {
|
||||
return CallbackData{}, "", fmt.Errorf("%w: expected %s", ErrPayloadTypeMismatch, BotPayloadBase64)
|
||||
}
|
||||
data, err = decodeJSONPayload(s)
|
||||
if err != nil {
|
||||
return CallbackData{}, "", err
|
||||
}
|
||||
return data, BotPayloadJSON, nil
|
||||
return decodeBase64Payload(s)
|
||||
case BotPayloadJSON:
|
||||
data, err := decodeJSONPayload(s)
|
||||
return decodeJSONPayload(s)
|
||||
case BotPayloadCompact:
|
||||
return decodeCompactPayload(s)
|
||||
case BotPayloadCompactBase64:
|
||||
return decodeCompactBase64Payload(s)
|
||||
}
|
||||
return CallbackData{}, ErrInvalidPayloadType
|
||||
}
|
||||
|
||||
func decodePayload(payloadType BotPayloadType, s string, strict bool) (CallbackData, BotPayloadType, error) {
|
||||
knownTypes := []BotPayloadType{
|
||||
BotPayloadBase64,
|
||||
BotPayloadJSON,
|
||||
BotPayloadCompact,
|
||||
BotPayloadCompactBase64,
|
||||
}
|
||||
if _, err := decodePayloadAs(payloadType, ""); errors.Is(err, ErrInvalidPayloadType) {
|
||||
return CallbackData{}, "", ErrInvalidPayloadType
|
||||
}
|
||||
|
||||
data, err := decodePayloadAs(payloadType, s)
|
||||
if err == nil {
|
||||
return data, BotPayloadJSON, nil
|
||||
return data, payloadType, nil
|
||||
}
|
||||
if strict {
|
||||
return CallbackData{}, "", fmt.Errorf("%w: expected %s", ErrPayloadTypeMismatch, BotPayloadJSON)
|
||||
return CallbackData{}, "", fmt.Errorf("%w: expected %s", ErrPayloadTypeMismatch, payloadType)
|
||||
}
|
||||
|
||||
for _, candidate := range knownTypes {
|
||||
if candidate == payloadType {
|
||||
continue
|
||||
}
|
||||
data, err = decodePayloadAs(candidate, s)
|
||||
if err == nil {
|
||||
return data, candidate, nil
|
||||
}
|
||||
}
|
||||
data, err = decodeBase64Payload(s)
|
||||
if err != nil {
|
||||
return CallbackData{}, "", err
|
||||
}
|
||||
return data, BotPayloadBase64, nil
|
||||
}
|
||||
return CallbackData{}, "", ErrInvalidPayloadType
|
||||
}
|
||||
|
||||
func (bot *Bot[T]) decodePayload(s string) (CallbackData, error) {
|
||||
|
||||
+56
-5
@@ -91,12 +91,31 @@ func (b InlineKeyboardButtonBuilder) SetCallbackDataBase64(cmd string, args ...a
|
||||
return b
|
||||
}
|
||||
|
||||
// SetCallbackDataCompact sets a structured callback payload encoded as compact text.
|
||||
func (b InlineKeyboardButtonBuilder) SetCallbackDataCompact(cmd string, args ...any) InlineKeyboardButtonBuilder {
|
||||
b.data = NewCallbackData(cmd, args...).ToCompact()
|
||||
return b
|
||||
}
|
||||
|
||||
// SetCallbackDataCompactBase64 sets a compact callback payload encoded as Base64.
|
||||
func (b InlineKeyboardButtonBuilder) SetCallbackDataCompactBase64(cmd string, args ...any) InlineKeyboardButtonBuilder {
|
||||
b.data = NewCallbackData(cmd, args...).ToCompactBase64()
|
||||
return b
|
||||
}
|
||||
|
||||
// SetCallbackData sets a structured callback payload using the configured payload type.
|
||||
// The default payload type is JSON.
|
||||
func (b InlineKeyboardButtonBuilder) SetCallbackData(cmd string, args ...any) InlineKeyboardButtonBuilder {
|
||||
if b.payloadType == BotPayloadBase64 {
|
||||
switch b.payloadType {
|
||||
case BotPayloadJSON:
|
||||
b.data = NewCallbackData(cmd, args...).ToJSON()
|
||||
case BotPayloadBase64:
|
||||
b.data = NewCallbackData(cmd, args...).ToBase64()
|
||||
} else {
|
||||
case BotPayloadCompact:
|
||||
b.data = NewCallbackData(cmd, args...).ToCompact()
|
||||
case BotPayloadCompactBase64:
|
||||
b.data = NewCallbackData(cmd, args...).ToCompactBase64()
|
||||
default:
|
||||
b.data = NewCallbackData(cmd, args...).ToJSON()
|
||||
}
|
||||
return b
|
||||
@@ -143,6 +162,16 @@ func NewInlineKeyboardBase64(maxRow int) *InlineKeyboard {
|
||||
return NewInlineKeyboard(BotPayloadBase64, maxRow)
|
||||
}
|
||||
|
||||
// NewInlineKeyboardCompact creates a keyboard builder using compact callback payloads.
|
||||
func NewInlineKeyboardCompact(maxRow int) *InlineKeyboard {
|
||||
return NewInlineKeyboard(BotPayloadCompact, maxRow)
|
||||
}
|
||||
|
||||
// NewInlineKeyboardCompactBase64 creates a keyboard builder using Base64-encoded compact payloads.
|
||||
func NewInlineKeyboardCompactBase64(maxRow int) *InlineKeyboard {
|
||||
return NewInlineKeyboard(BotPayloadCompactBase64, maxRow)
|
||||
}
|
||||
|
||||
// NewInlineKeyboard creates a new keyboard builder with the specified payload encoding
|
||||
// type and maximum number of buttons per row.
|
||||
//
|
||||
@@ -292,15 +321,33 @@ func (d CallbackData) ToJSON() string {
|
||||
// ToBase64 serializes the CallbackData to a JSON string and then encodes it as Base64.
|
||||
// Returns an empty string if serialization or encoding fails.
|
||||
func (d CallbackData) ToBase64() string {
|
||||
s, err := encodeBase64Payload(d)
|
||||
data, err := encodeBase64Payload(d)
|
||||
if err != nil {
|
||||
return ``
|
||||
}
|
||||
return s
|
||||
return data
|
||||
}
|
||||
|
||||
// ToCompact serializes the CallbackData to a compact delimited string.
|
||||
func (d CallbackData) ToCompact() string {
|
||||
data, err := encodeCompactPayload(d)
|
||||
if err != nil {
|
||||
return ``
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// ToCompactBase64 serializes the CallbackData to compact text and then encodes it as Base64.
|
||||
func (d CallbackData) ToCompactBase64() string {
|
||||
data, err := encodeCompactBase64Payload(d)
|
||||
if err != nil {
|
||||
return ``
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// Encode serializes the CallbackData according to the specified payload type.
|
||||
// Supported types: BotPayloadJSON and BotPayloadBase64.
|
||||
// Supported types: BotPayloadJSON, BotPayloadBase64, BotPayloadCompact, and BotPayloadCompactBase64.
|
||||
// For unknown types, returns an empty string.
|
||||
func (d CallbackData) Encode(t BotPayloadType) string {
|
||||
switch t {
|
||||
@@ -308,6 +355,10 @@ func (d CallbackData) Encode(t BotPayloadType) string {
|
||||
return d.ToBase64()
|
||||
case BotPayloadJSON:
|
||||
return d.ToJSON()
|
||||
case BotPayloadCompact:
|
||||
return d.ToCompact()
|
||||
case BotPayloadCompactBase64:
|
||||
return d.ToCompactBase64()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -114,6 +114,52 @@ func TestDecodePayloadAcceptsJSONKeyboardPayloadWhenBotPrefersBase64(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePayloadAcceptsCompactKeyboardPayloadWhenBotPrefersJSON(t *testing.T) {
|
||||
kb := NewInlineKeyboardCompact(1).
|
||||
AddCallbackButton("A", "cmd", 1, "two")
|
||||
|
||||
got, decodedType, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, false)
|
||||
if err != nil {
|
||||
t.Fatalf("decodePayload returned error: %v", err)
|
||||
}
|
||||
if decodedType != BotPayloadCompact {
|
||||
t.Fatalf("unexpected decoded payload type: got %q want %q", decodedType, BotPayloadCompact)
|
||||
}
|
||||
|
||||
want := CallbackData{Command: "cmd", Args: []string{"1", "two"}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected payload: got %#v want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePayloadAcceptsCompactBase64KeyboardPayloadWhenBotPrefersJSON(t *testing.T) {
|
||||
kb := NewInlineKeyboardCompactBase64(1).
|
||||
AddCallbackButton("A", "cmd", 1, "two")
|
||||
|
||||
got, decodedType, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, false)
|
||||
if err != nil {
|
||||
t.Fatalf("decodePayload returned error: %v", err)
|
||||
}
|
||||
if decodedType != BotPayloadCompactBase64 {
|
||||
t.Fatalf("unexpected decoded payload type: got %q want %q", decodedType, BotPayloadCompactBase64)
|
||||
}
|
||||
|
||||
want := CallbackData{Command: "cmd", Args: []string{"1", "two"}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected payload: got %#v want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePayloadStrictRejectsCompactMismatchedType(t *testing.T) {
|
||||
kb := NewInlineKeyboardCompact(1).
|
||||
AddCallbackButton("A", "cmd", 1)
|
||||
|
||||
_, _, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, true)
|
||||
if !errors.Is(err, ErrPayloadTypeMismatch) {
|
||||
t.Fatalf("expected ErrPayloadTypeMismatch, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePayloadStrictRejectsMismatchedType(t *testing.T) {
|
||||
kb := NewInlineKeyboardBase64(1).
|
||||
AddCallbackButton("A", "cmd", 1)
|
||||
|
||||
Reference in New Issue
Block a user