diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a49e67..ed2f98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Added `MsgContext.UpsertKeyboard(...)` and `MsgContext.UpsertKeyboardMarkdown(...)` helpers that edit callback messages, replace photo callback messages with a fresh chat message, and send a new chat message outside callback flow. - 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. ### Changed - Version metadata now reports the stable `v1.0.0` release instead of `v1.0.0-rc.16`. @@ -31,6 +32,7 @@ - Added regression coverage for inline callback keyboard upserts and callback target detection. - 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. ## v1.0.0-rc.16 diff --git a/keyboard.go b/keyboard.go index b874afa..85958c8 100644 --- a/keyboard.go +++ b/keyboard.go @@ -27,11 +27,14 @@ const ( // Call build() to produce the final tgapi.InlineKeyboardButton. // Builder methods are immutable — each returns a copy. type InlineKeyboardButtonBuilder struct { - text string - iconCustomEmojiID string - style tgapi.KeyboardButtonStyle - url string - callbackData string + text string + emojiID string + style tgapi.KeyboardButtonStyle + + url string + data string + + payloadType BotPayloadType } // NewInlineKeyboardButton creates a new button builder with the given display text. @@ -43,7 +46,7 @@ func NewInlineKeyboardButton(text string) InlineKeyboardButtonBuilder { // SetIconCustomEmojiID sets a custom emoji ID to display as the button's icon. // This is a Telegram Bot API feature for custom emoji icons. func (b InlineKeyboardButtonBuilder) SetIconCustomEmojiID(id string) InlineKeyboardButtonBuilder { - b.iconCustomEmojiID = id + b.emojiID = id return b } @@ -62,6 +65,12 @@ func (b InlineKeyboardButtonBuilder) SetURL(url string) InlineKeyboardButtonBuil return b } +// SetPayloadType sets the encoding used by SetCallbackData. +func (b InlineKeyboardButtonBuilder) SetPayloadType(t BotPayloadType) InlineKeyboardButtonBuilder { + b.payloadType = t + return b +} + // SetCallbackDataJSON sets a structured callback payload that will be sent to the bot // when the button is pressed. The command and arguments are serialized as JSON. // @@ -70,7 +79,7 @@ func (b InlineKeyboardButtonBuilder) SetURL(url string) InlineKeyboardButtonBuil // // Example: SetCallbackDataJSON("delete_user", 123, "confirm") → {"cmd":"delete_user","args":["123","confirm"]}. func (b InlineKeyboardButtonBuilder) SetCallbackDataJSON(cmd string, args ...any) InlineKeyboardButtonBuilder { - b.callbackData = NewCallbackData(cmd, args...).ToJSON() + b.data = NewCallbackData(cmd, args...).ToJSON() return b } @@ -78,7 +87,18 @@ func (b InlineKeyboardButtonBuilder) SetCallbackDataJSON(cmd string, args ...any // This can be useful when the JSON payload exceeds Telegram's callback data length limit. // Args are converted to strings using fmt.Sprint. func (b InlineKeyboardButtonBuilder) SetCallbackDataBase64(cmd string, args ...any) InlineKeyboardButtonBuilder { - b.callbackData = NewCallbackData(cmd, args...).ToBase64() + b.data = NewCallbackData(cmd, args...).ToBase64() + 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 { + b.data = NewCallbackData(cmd, args...).ToBase64() + } else { + b.data = NewCallbackData(cmd, args...).ToJSON() + } return b } @@ -88,8 +108,8 @@ func (b InlineKeyboardButtonBuilder) build() tgapi.InlineKeyboardButton { Text: b.text, URL: b.url, Style: b.style, - IconCustomEmojiID: b.iconCustomEmojiID, - CallbackData: b.callbackData, + IconCustomEmojiID: b.emojiID, + CallbackData: b.data, } } @@ -177,7 +197,7 @@ func (in *InlineKeyboard) AddURLButtonStyle(text string, style tgapi.KeyboardBut // AddCallbackButton adds a button that sends a structured callback payload to the bot. // The command and args are serialized according to the current payloadType. -func (in *InlineKeyboard) AddCallbackButton(text string, cmd string, args ...any) *InlineKeyboard { +func (in *InlineKeyboard) AddCallbackButton(text, cmd string, args ...any) *InlineKeyboard { return in.append(tgapi.InlineKeyboardButton{ Text: text, CallbackData: NewCallbackData(cmd, args...).Encode(in.payloadType), diff --git a/keyboard_test.go b/keyboard_test.go index ac1f5ac..03d495f 100644 --- a/keyboard_test.go +++ b/keyboard_test.go @@ -45,6 +45,34 @@ func TestInlineKeyboardBuilderPreservesConfiguredButtonFields(t *testing.T) { } } +func TestInlineKeyboardButtonBuilderSetCallbackDataDefaultsToJSON(t *testing.T) { + kb := NewInlineKeyboardBase64(1). + AddButton(NewInlineKeyboardButton("A").SetCallbackData("cmd", 1, "two")) + + button := kb.Get().InlineKeyboard[0][0] + if !strings.Contains(button.CallbackData, `"cmd":"cmd"`) { + t.Fatalf("expected JSON callback payload, got %q", button.CallbackData) + } +} + +func TestInlineKeyboardButtonBuilderSetCallbackDataUsesConfiguredPayloadType(t *testing.T) { + kb := NewInlineKeyboardJSON(1). + AddButton(NewInlineKeyboardButton("A"). + SetPayloadType(BotPayloadBase64). + SetCallbackData("cmd", 1, "two"), + ) + + got, _, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, false) + if err != nil { + t.Fatalf("decodePayload returned error: %v", err) + } + + want := CallbackData{Command: "cmd", Args: []string{"1", "two"}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("unexpected payload: got %#v want %#v", got, want) + } +} + func TestInlineKeyboardGetPayloadTypeReturnsLocalOverride(t *testing.T) { kb := NewInlineKeyboardJSON(2) if got := kb.GetPayloadType(); got != BotPayloadJSON { diff --git a/msg_context.go b/msg_context.go index cdbf42a..f5841f9 100644 --- a/msg_context.go +++ b/msg_context.go @@ -594,6 +594,11 @@ func (ctx *MsgContext) NewInlineKeyboard(maxRow int) *InlineKeyboard { return NewInlineKeyboard(ctx.payloadType, maxRow) } +// NewInlineKeyboardButton creates a button builder using the context payload encoding. +func (ctx *MsgContext) NewInlineKeyboardButton(text string) InlineKeyboardButtonBuilder { + return NewInlineKeyboardButton(text).SetPayloadType(ctx.payloadType) +} + func bindPositional(args []string, dst any) error { v := reflect.ValueOf(dst) if v.Kind() != reflect.Pointer || v.IsNil() { diff --git a/msg_context_test.go b/msg_context_test.go index f9f71bb..3b9f2a6 100644 --- a/msg_context_test.go +++ b/msg_context_test.go @@ -91,6 +91,23 @@ func TestBindArgsBindsScalarFields(t *testing.T) { } } +func TestNewInlineKeyboardButtonUsesContextPayloadType(t *testing.T) { + ctx := &MsgContext{payloadType: BotPayloadBase64} + + kb := NewInlineKeyboardJSON(1). + AddButton(ctx.NewInlineKeyboardButton("A").SetCallbackData("cmd", 1, "two")) + + got, _, err := decodePayload(BotPayloadJSON, kb.Get().InlineKeyboard[0][0].CallbackData, false) + if err != nil { + t.Fatalf("decodePayload returned error: %v", err) + } + + want := CallbackData{Command: "cmd", Args: []string{"1", "two"}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("unexpected payload: got %#v want %#v", got, want) + } +} + func TestBindArgsLeavesTrailingFieldsZeroWhenArgsRunOut(t *testing.T) { type input struct { ID int