REPOSITORY / ScuroNeko/Laniakea
Pull Requests
v1.0.0 #9
@@ -15,6 +15,7 @@
|
|||||||
- Added `CommandGroup`, `NewCommandGroup(...)`, `Plugin.CommandGroup(...)`, and `Plugin.AddCommandGroup(...)` helpers for registering prefixed command groups with shared middleware.
|
- 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 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 `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.
|
- Added `tgapi.ResponseError` so Telegram API error codes, descriptions, and response parameters remain inspectable through returned errors.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
- Added regression coverage for command group prefixing, middleware order, clone behavior, and plugin registration.
|
- 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 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 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.
|
- Added regression coverage for long-polling `retry_after` handling on Telegram 429 responses.
|
||||||
|
|
||||||
## v1.0.0-rc.16
|
## v1.0.0-rc.16
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ var (
|
|||||||
BotPayloadBase64 BotPayloadType = "base64"
|
BotPayloadBase64 BotPayloadType = "base64"
|
||||||
// BotPayloadJSON encodes callback data as a JSON string.
|
// BotPayloadJSON encodes callback data as a JSON string.
|
||||||
BotPayloadJSON BotPayloadType = "json"
|
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 (
|
var (
|
||||||
|
|||||||
+68
-26
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
@@ -153,36 +154,77 @@ func decodeBase64Payload(s string) (CallbackData, error) {
|
|||||||
return decodeJSONPayload(string(b))
|
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 {
|
switch payloadType {
|
||||||
case BotPayloadBase64:
|
case BotPayloadBase64:
|
||||||
data, err := decodeBase64Payload(s)
|
return 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
|
|
||||||
case BotPayloadJSON:
|
case BotPayloadJSON:
|
||||||
data, err := decodeJSONPayload(s)
|
return decodeJSONPayload(s)
|
||||||
if err == nil {
|
case BotPayloadCompact:
|
||||||
return data, BotPayloadJSON, nil
|
return decodeCompactPayload(s)
|
||||||
}
|
case BotPayloadCompactBase64:
|
||||||
if strict {
|
return decodeCompactBase64Payload(s)
|
||||||
return CallbackData{}, "", fmt.Errorf("%w: expected %s", ErrPayloadTypeMismatch, BotPayloadJSON)
|
|
||||||
}
|
|
||||||
data, err = decodeBase64Payload(s)
|
|
||||||
if err != nil {
|
|
||||||
return CallbackData{}, "", err
|
|
||||||
}
|
|
||||||
return data, BotPayloadBase64, nil
|
|
||||||
}
|
}
|
||||||
return CallbackData{}, "", ErrInvalidPayloadType
|
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, payloadType, nil
|
||||||
|
}
|
||||||
|
if strict {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CallbackData{}, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot[T]) decodePayload(s string) (CallbackData, error) {
|
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
|
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.
|
// SetCallbackData sets a structured callback payload using the configured payload type.
|
||||||
// The default payload type is JSON.
|
// The default payload type is JSON.
|
||||||
func (b InlineKeyboardButtonBuilder) SetCallbackData(cmd string, args ...any) InlineKeyboardButtonBuilder {
|
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()
|
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()
|
b.data = NewCallbackData(cmd, args...).ToJSON()
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
@@ -143,6 +162,16 @@ func NewInlineKeyboardBase64(maxRow int) *InlineKeyboard {
|
|||||||
return NewInlineKeyboard(BotPayloadBase64, maxRow)
|
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
|
// NewInlineKeyboard creates a new keyboard builder with the specified payload encoding
|
||||||
// type and maximum number of buttons per row.
|
// 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.
|
// ToBase64 serializes the CallbackData to a JSON string and then encodes it as Base64.
|
||||||
// Returns an empty string if serialization or encoding fails.
|
// Returns an empty string if serialization or encoding fails.
|
||||||
func (d CallbackData) ToBase64() string {
|
func (d CallbackData) ToBase64() string {
|
||||||
s, err := encodeBase64Payload(d)
|
data, err := encodeBase64Payload(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ``
|
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.
|
// 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.
|
// For unknown types, returns an empty string.
|
||||||
func (d CallbackData) Encode(t BotPayloadType) string {
|
func (d CallbackData) Encode(t BotPayloadType) string {
|
||||||
switch t {
|
switch t {
|
||||||
@@ -308,6 +355,10 @@ func (d CallbackData) Encode(t BotPayloadType) string {
|
|||||||
return d.ToBase64()
|
return d.ToBase64()
|
||||||
case BotPayloadJSON:
|
case BotPayloadJSON:
|
||||||
return d.ToJSON()
|
return d.ToJSON()
|
||||||
|
case BotPayloadCompact:
|
||||||
|
return d.ToCompact()
|
||||||
|
case BotPayloadCompactBase64:
|
||||||
|
return d.ToCompactBase64()
|
||||||
}
|
}
|
||||||
return ""
|
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) {
|
func TestDecodePayloadStrictRejectsMismatchedType(t *testing.T) {
|
||||||
kb := NewInlineKeyboardBase64(1).
|
kb := NewInlineKeyboardBase64(1).
|
||||||
AddCallbackButton("A", "cmd", 1)
|
AddCallbackButton("A", "cmd", 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user