(new): v1.2 release
Golang lint / lint (push) Successful in 11m32s

This commit is contained in:
2026-08-19 14:58:25 +03:00
parent f03a081ed6
commit 29b208eeec
79 changed files with 7301 additions and 2060 deletions
+98 -8
View File
@@ -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
}