+98
-8
@@ -24,7 +24,7 @@ const (
|
||||
// - SetURL() — makes button open a URL
|
||||
// - SetCallbackDataJSON() — attaches structured command + args for bot handling
|
||||
//
|
||||
// Call build() to produce the final tgapi.InlineKeyboardButton.
|
||||
// Call Build to validate and produce the final tgapi.InlineKeyboardButton.
|
||||
// Builder methods are immutable — each returns a copy.
|
||||
type InlineKeyboardButtonBuilder struct {
|
||||
text string
|
||||
@@ -138,6 +138,20 @@ func (b InlineKeyboardButtonBuilder) build() tgapi.InlineKeyboardButton {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that the button has exactly one action and valid callback data.
|
||||
func (b InlineKeyboardButtonBuilder) Validate() error {
|
||||
return validateInlineKeyboardButton(b.build())
|
||||
}
|
||||
|
||||
// Build validates and returns the configured inline keyboard button.
|
||||
func (b InlineKeyboardButtonBuilder) Build() (tgapi.InlineKeyboardButton, error) {
|
||||
button := b.build()
|
||||
if err := validateInlineKeyboardButton(button); err != nil {
|
||||
return tgapi.InlineKeyboardButton{}, err
|
||||
}
|
||||
return button, nil
|
||||
}
|
||||
|
||||
// InlineKeyboard is a stateful builder for constructing Telegram inline keyboard layouts.
|
||||
//
|
||||
// Buttons are added row-by-row. When a row reaches maxRow, it is automatically flushed.
|
||||
@@ -145,9 +159,11 @@ func (b InlineKeyboardButtonBuilder) build() tgapi.InlineKeyboardButton {
|
||||
//
|
||||
// The keyboard is not thread-safe. Build it in a single goroutine.
|
||||
type InlineKeyboard struct {
|
||||
CurrentLine extypes.Slice[tgapi.InlineKeyboardButton] // Current row being built
|
||||
Lines [][]tgapi.InlineKeyboardButton // Completed rows
|
||||
maxRow int // Max buttons per row (e.g., 3 or 4)
|
||||
// CurrentLine is the row currently being built.
|
||||
CurrentLine extypes.Slice[tgapi.InlineKeyboardButton]
|
||||
// Lines contains completed keyboard rows.
|
||||
Lines [][]tgapi.InlineKeyboardButton
|
||||
maxRow int // Max buttons per row (e.g., 3 or 4)
|
||||
|
||||
payloadType BotPayloadType // Serialization format for callback data (JSON or Base64)
|
||||
}
|
||||
@@ -203,7 +219,8 @@ func (in *InlineKeyboard) SetPayloadType(t BotPayloadType) *InlineKeyboard {
|
||||
func (in *InlineKeyboard) GetPayloadType() BotPayloadType { return in.payloadType }
|
||||
|
||||
// SetMaxRow sets the maximum number of buttons appended to a row before the
|
||||
// keyboard automatically starts a new line.
|
||||
// keyboard automatically starts a new line. Values <= 0 retain the legacy
|
||||
// unlimited-row behavior; this convention is subject to change in v2.
|
||||
func (in *InlineKeyboard) SetMaxRow(maxRow int) *InlineKeyboard {
|
||||
in.maxRow = maxRow
|
||||
return in
|
||||
@@ -213,7 +230,7 @@ func (in *InlineKeyboard) SetMaxRow(maxRow int) *InlineKeyboard {
|
||||
func (in *InlineKeyboard) GetMaxRow() int { return in.maxRow }
|
||||
|
||||
func (in *InlineKeyboard) append(button tgapi.InlineKeyboardButton) *InlineKeyboard {
|
||||
if in.CurrentLine.Len() == in.maxRow {
|
||||
if in.maxRow > 0 && in.CurrentLine.Len() >= in.maxRow {
|
||||
in.AddLine()
|
||||
}
|
||||
in.CurrentLine = in.CurrentLine.Push(button)
|
||||
@@ -283,6 +300,62 @@ func (in *InlineKeyboard) Get() *tgapi.ReplyMarkup {
|
||||
return &tgapi.ReplyMarkup{InlineKeyboard: lines}
|
||||
}
|
||||
|
||||
// GetValidated finalizes and validates the keyboard before returning it.
|
||||
//
|
||||
// Existing fluent Add* methods remain error-free for v1 compatibility. Their
|
||||
// signatures are subject to change in v2; new code should use GetValidated.
|
||||
func (in *InlineKeyboard) GetValidated() (*tgapi.ReplyMarkup, error) {
|
||||
markup := in.Get()
|
||||
if err := in.validateMarkup(markup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return markup, nil
|
||||
}
|
||||
|
||||
// Validate checks completed and pending rows without finalizing the keyboard.
|
||||
func (in *InlineKeyboard) Validate() error {
|
||||
lines := make([][]tgapi.InlineKeyboardButton, 0, len(in.Lines)+1)
|
||||
lines = append(lines, in.Lines...)
|
||||
if len(in.CurrentLine) > 0 {
|
||||
lines = append(lines, in.CurrentLine)
|
||||
}
|
||||
return in.validateMarkup(&tgapi.ReplyMarkup{InlineKeyboard: lines})
|
||||
}
|
||||
|
||||
func (in *InlineKeyboard) validateMarkup(markup *tgapi.ReplyMarkup) error {
|
||||
for rowIndex, row := range markup.InlineKeyboard {
|
||||
if in.maxRow > 0 && len(row) > in.maxRow {
|
||||
return fmt.Errorf("%w: row %d has %d buttons, limit %d", ErrInlineKeyboardRowTooLong, rowIndex, len(row), in.maxRow)
|
||||
}
|
||||
for columnIndex, button := range row {
|
||||
if err := validateInlineKeyboardButton(button); err != nil {
|
||||
return fmt.Errorf("row %d button %d: %w", rowIndex, columnIndex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateInlineKeyboardButton(button tgapi.InlineKeyboardButton) error {
|
||||
actions := 0
|
||||
if button.URL != "" {
|
||||
actions++
|
||||
}
|
||||
if button.CallbackData != "" {
|
||||
actions++
|
||||
}
|
||||
if actions != 1 {
|
||||
return ErrInlineKeyboardButtonAction
|
||||
}
|
||||
if button.CallbackData != "" {
|
||||
length := len([]byte(button.CallbackData))
|
||||
if length < 1 || length > 64 {
|
||||
return fmt.Errorf("%w: got %d", ErrCallbackDataLength, length)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallbackData represents the structured payload sent when an inline button
|
||||
// with callback data is pressed.
|
||||
//
|
||||
@@ -293,8 +366,10 @@ func (in *InlineKeyboard) Get() *tgapi.ReplyMarkup {
|
||||
//
|
||||
// {"cmd":"delete_user","args":["123","confirm"]}
|
||||
type CallbackData struct {
|
||||
Command string `json:"cmd"` // The command name to route to
|
||||
Args []string `json:"args"` // Arguments passed as strings
|
||||
// Command is the command name used for payload routing.
|
||||
Command string `json:"cmd"`
|
||||
// Args contains the string arguments passed to the payload handler.
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
// NewCallbackData creates a new CallbackData instance with the given command and args.
|
||||
@@ -377,3 +452,18 @@ func (d CallbackData) Encode(t BotPayloadType) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// EncodeValidated serializes callback data and enforces Telegram's 1-64 byte limit.
|
||||
func (d CallbackData) EncodeValidated(t BotPayloadType) (string, error) {
|
||||
encoded := d.Encode(t)
|
||||
if encoded == "" {
|
||||
if t != BotPayloadBase64 && t != BotPayloadJSON && t != BotPayloadCompact && t != BotPayloadCompactBase64 {
|
||||
return "", ErrInvalidPayloadType
|
||||
}
|
||||
return "", ErrCallbackDataLength
|
||||
}
|
||||
if length := len([]byte(encoded)); length > 64 {
|
||||
return "", fmt.Errorf("%w: got %d", ErrCallbackDataLength, length)
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user