(new): keyboard helpers
(tests): payload encoding (doc): changelog
This commit is contained in:
@@ -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 `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 `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.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Version metadata now reports the stable `v1.0.0` release instead of `v1.0.0-rc.16`.
|
- 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 inline callback keyboard upserts and callback target detection.
|
||||||
- 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.
|
||||||
|
|
||||||
## v1.0.0-rc.16
|
## v1.0.0-rc.16
|
||||||
|
|
||||||
|
|||||||
+31
-11
@@ -27,11 +27,14 @@ const (
|
|||||||
// Call build() to produce the final tgapi.InlineKeyboardButton.
|
// Call build() to produce the final tgapi.InlineKeyboardButton.
|
||||||
// Builder methods are immutable — each returns a copy.
|
// Builder methods are immutable — each returns a copy.
|
||||||
type InlineKeyboardButtonBuilder struct {
|
type InlineKeyboardButtonBuilder struct {
|
||||||
text string
|
text string
|
||||||
iconCustomEmojiID string
|
emojiID string
|
||||||
style tgapi.KeyboardButtonStyle
|
style tgapi.KeyboardButtonStyle
|
||||||
url string
|
|
||||||
callbackData string
|
url string
|
||||||
|
data string
|
||||||
|
|
||||||
|
payloadType BotPayloadType
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInlineKeyboardButton creates a new button builder with the given display text.
|
// 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.
|
// SetIconCustomEmojiID sets a custom emoji ID to display as the button's icon.
|
||||||
// This is a Telegram Bot API feature for custom emoji icons.
|
// This is a Telegram Bot API feature for custom emoji icons.
|
||||||
func (b InlineKeyboardButtonBuilder) SetIconCustomEmojiID(id string) InlineKeyboardButtonBuilder {
|
func (b InlineKeyboardButtonBuilder) SetIconCustomEmojiID(id string) InlineKeyboardButtonBuilder {
|
||||||
b.iconCustomEmojiID = id
|
b.emojiID = id
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +65,12 @@ func (b InlineKeyboardButtonBuilder) SetURL(url string) InlineKeyboardButtonBuil
|
|||||||
return b
|
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
|
// 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.
|
// 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"]}.
|
// Example: SetCallbackDataJSON("delete_user", 123, "confirm") → {"cmd":"delete_user","args":["123","confirm"]}.
|
||||||
func (b InlineKeyboardButtonBuilder) SetCallbackDataJSON(cmd string, args ...any) InlineKeyboardButtonBuilder {
|
func (b InlineKeyboardButtonBuilder) SetCallbackDataJSON(cmd string, args ...any) InlineKeyboardButtonBuilder {
|
||||||
b.callbackData = NewCallbackData(cmd, args...).ToJSON()
|
b.data = NewCallbackData(cmd, args...).ToJSON()
|
||||||
return b
|
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.
|
// This can be useful when the JSON payload exceeds Telegram's callback data length limit.
|
||||||
// Args are converted to strings using fmt.Sprint.
|
// Args are converted to strings using fmt.Sprint.
|
||||||
func (b InlineKeyboardButtonBuilder) SetCallbackDataBase64(cmd string, args ...any) InlineKeyboardButtonBuilder {
|
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
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +108,8 @@ func (b InlineKeyboardButtonBuilder) build() tgapi.InlineKeyboardButton {
|
|||||||
Text: b.text,
|
Text: b.text,
|
||||||
URL: b.url,
|
URL: b.url,
|
||||||
Style: b.style,
|
Style: b.style,
|
||||||
IconCustomEmojiID: b.iconCustomEmojiID,
|
IconCustomEmojiID: b.emojiID,
|
||||||
CallbackData: b.callbackData,
|
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.
|
// AddCallbackButton adds a button that sends a structured callback payload to the bot.
|
||||||
// The command and args are serialized according to the current payloadType.
|
// 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{
|
return in.append(tgapi.InlineKeyboardButton{
|
||||||
Text: text,
|
Text: text,
|
||||||
CallbackData: NewCallbackData(cmd, args...).Encode(in.payloadType),
|
CallbackData: NewCallbackData(cmd, args...).Encode(in.payloadType),
|
||||||
|
|||||||
@@ -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) {
|
func TestInlineKeyboardGetPayloadTypeReturnsLocalOverride(t *testing.T) {
|
||||||
kb := NewInlineKeyboardJSON(2)
|
kb := NewInlineKeyboardJSON(2)
|
||||||
if got := kb.GetPayloadType(); got != BotPayloadJSON {
|
if got := kb.GetPayloadType(); got != BotPayloadJSON {
|
||||||
|
|||||||
@@ -594,6 +594,11 @@ func (ctx *MsgContext) NewInlineKeyboard(maxRow int) *InlineKeyboard {
|
|||||||
return NewInlineKeyboard(ctx.payloadType, maxRow)
|
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 {
|
func bindPositional(args []string, dst any) error {
|
||||||
v := reflect.ValueOf(dst)
|
v := reflect.ValueOf(dst)
|
||||||
if v.Kind() != reflect.Pointer || v.IsNil() {
|
if v.Kind() != reflect.Pointer || v.IsNil() {
|
||||||
|
|||||||
@@ -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) {
|
func TestBindArgsLeavesTrailingFieldsZeroWhenArgsRunOut(t *testing.T) {
|
||||||
type input struct {
|
type input struct {
|
||||||
ID int
|
ID int
|
||||||
|
|||||||
Reference in New Issue
Block a user