REPOSITORY / ScuroNeko/Laniakea

Pull Requests

PULL REQUESTS REPOSITORY

v1.0.0 #9

Merged
ScuroNeko merged 101 commits from dev into main 2026-05-20 13:43:34 +03:00
5 changed files with 176 additions and 31 deletions
Showing only changes of commit 09fb9261df - Show all commits
+2
View File
@@ -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
+4
View File
@@ -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 (
+68 -26
View File
@@ -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)
if err == nil {
return data, BotPayloadJSON, nil
}
if strict {
return CallbackData{}, "", fmt.Errorf("%w: expected %s", ErrPayloadTypeMismatch, BotPayloadJSON)
}
data, err = decodeBase64Payload(s)
if err != nil {
return CallbackData{}, "", err
}
return data, BotPayloadBase64, nil
return decodeJSONPayload(s)
case BotPayloadCompact:
return decodeCompactPayload(s)
case BotPayloadCompactBase64:
return decodeCompactBase64Payload(s)
}
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) {
+56 -5
View File
@@ -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 ""
}
+46
View File
@@ -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)