287 lines
11 KiB
Go
287 lines
11 KiB
Go
package laniakea
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"git.nix13.pw/scuroneko/extypes"
|
|
"git.nix13.pw/scuroneko/laniakea/tgapi"
|
|
)
|
|
|
|
// ButtonStyleDanger, ButtonStyleSuccess, ButtonStylePrimary are predefined
|
|
// Telegram keyboard button styles for visual feedback.
|
|
//
|
|
// These values map directly to Telegram Bot API's InlineKeyboardButton style field.
|
|
const (
|
|
ButtonStyleDanger tgapi.KeyboardButtonStyle = "danger"
|
|
ButtonStyleSuccess tgapi.KeyboardButtonStyle = "success"
|
|
ButtonStylePrimary tgapi.KeyboardButtonStyle = "primary"
|
|
)
|
|
|
|
// InlineKbButtonBuilder is a fluent builder for creating a single inline keyboard button.
|
|
//
|
|
// Use NewInlineKbButton() to start, then chain methods to configure:
|
|
// - SetIconCustomEmojiId() — adds a custom emoji icon
|
|
// - SetStyle() — sets visual style (danger/success/primary)
|
|
// - SetUrl() — makes button open a URL
|
|
// - SetCallbackDataJson() — attaches structured command + args for bot handling
|
|
//
|
|
// Call build() to produce the final tgapi.InlineKeyboardButton.
|
|
// Builder methods are immutable — each returns a copy.
|
|
type InlineKbButtonBuilder struct {
|
|
text string
|
|
iconCustomEmojiID string
|
|
style tgapi.KeyboardButtonStyle
|
|
url string
|
|
callbackData string
|
|
}
|
|
|
|
// NewInlineKbButton creates a new button builder with the given display text.
|
|
// The button will have no URL, no style, and no callback data by default.
|
|
func NewInlineKbButton(text string) InlineKbButtonBuilder {
|
|
return InlineKbButtonBuilder{text: text}
|
|
}
|
|
|
|
// 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 InlineKbButtonBuilder) SetIconCustomEmojiId(id string) InlineKbButtonBuilder {
|
|
b.iconCustomEmojiID = id
|
|
return b
|
|
}
|
|
|
|
// SetStyle sets the visual style of the button.
|
|
// Valid values: ButtonStyleDanger, ButtonStyleSuccess, ButtonStylePrimary.
|
|
// If not set, the button uses the default style.
|
|
func (b InlineKbButtonBuilder) SetStyle(style tgapi.KeyboardButtonStyle) InlineKbButtonBuilder {
|
|
b.style = style
|
|
return b
|
|
}
|
|
|
|
// SetUrl sets a URL that will be opened when the button is pressed.
|
|
// If both URL and CallbackData are set, Telegram will prioritize URL.
|
|
func (b InlineKbButtonBuilder) SetUrl(url string) InlineKbButtonBuilder {
|
|
b.url = url
|
|
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.
|
|
//
|
|
// Args are converted to strings using fmt.Sprint. Non-string types (e.g., int, bool)
|
|
// are safely serialized, but complex structs may not serialize usefully.
|
|
//
|
|
// Example: SetCallbackDataJson("delete_user", 123, "confirm") → {"cmd":"delete_user","args":["123","confirm"]}
|
|
func (b InlineKbButtonBuilder) SetCallbackDataJson(cmd string, args ...any) InlineKbButtonBuilder {
|
|
b.callbackData = NewCallbackData(cmd, args...).ToJson()
|
|
return b
|
|
}
|
|
|
|
// SetCallbackDataBase64 sets a structured callback payload encoded as Base64.
|
|
// 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 InlineKbButtonBuilder) SetCallbackDataBase64(cmd string, args ...any) InlineKbButtonBuilder {
|
|
b.callbackData = NewCallbackData(cmd, args...).ToBase64()
|
|
return b
|
|
}
|
|
|
|
// build converts the builder state into a tgapi.InlineKeyboardButton.
|
|
// This method is typically called internally by InlineKeyboard.AddButton().
|
|
func (b InlineKbButtonBuilder) build() tgapi.InlineKeyboardButton {
|
|
return tgapi.InlineKeyboardButton{
|
|
Text: b.text,
|
|
URL: b.url,
|
|
Style: b.style,
|
|
IconCustomEmojiID: b.iconCustomEmojiID,
|
|
CallbackData: b.callbackData,
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
// Call AddLine() to manually end a row, or Get() to finalize and retrieve the markup.
|
|
//
|
|
// 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)
|
|
|
|
payloadType BotPayloadType // Serialization format for callback data (JSON or Base64)
|
|
}
|
|
|
|
// NewInlineKeyboardJson creates a new keyboard builder with the specified maximum
|
|
// number of buttons per row.
|
|
//
|
|
// Example: NewInlineKeyboardJson(3) creates a keyboard with at most 3 buttons per line.
|
|
func NewInlineKeyboardJson(maxRow int) *InlineKeyboard {
|
|
return NewInlineKeyboard(BotPayloadJson, maxRow)
|
|
}
|
|
|
|
// NewInlineKeyboardBase64 creates a new keyboard builder with the specified maximum
|
|
// number of buttons per row, using Base64 encoding for button payloads.
|
|
//
|
|
// Example: NewInlineKeyboardBase64(3) creates a keyboard with at most 3 buttons per line.
|
|
func NewInlineKeyboardBase64(maxRow int) *InlineKeyboard {
|
|
return NewInlineKeyboard(BotPayloadBase64, maxRow)
|
|
}
|
|
|
|
// NewInlineKeyboard creates a new keyboard builder with the specified payload encoding
|
|
// type and maximum number of buttons per row.
|
|
//
|
|
// Use NewInlineKeyboardJson or NewInlineKeyboardBase64 for the common cases.
|
|
func NewInlineKeyboard(payloadType BotPayloadType, maxRow int) *InlineKeyboard {
|
|
return &InlineKeyboard{
|
|
CurrentLine: make(extypes.Slice[tgapi.InlineKeyboardButton], 0),
|
|
Lines: make([][]tgapi.InlineKeyboardButton, 0),
|
|
maxRow: maxRow,
|
|
payloadType: payloadType,
|
|
}
|
|
}
|
|
|
|
// SetPayloadType sets the serialization format for callback data added via
|
|
// AddCallbackButton and AddCallbackButtonStyle methods.
|
|
// It should be one of BotPayloadJson or BotPayloadBase64.
|
|
func (in *InlineKeyboard) SetPayloadType(t BotPayloadType) *InlineKeyboard {
|
|
in.payloadType = t
|
|
return in
|
|
}
|
|
|
|
// append adds a button to the current line. If the line is full, it auto-flushes.
|
|
// This is an internal helper used by other builder methods.
|
|
func (in *InlineKeyboard) append(button tgapi.InlineKeyboardButton) *InlineKeyboard {
|
|
if in.CurrentLine.Len() == in.maxRow {
|
|
in.AddLine()
|
|
}
|
|
in.CurrentLine = in.CurrentLine.Push(button)
|
|
return in
|
|
}
|
|
|
|
// AddUrlButton adds a button that opens a URL when pressed.
|
|
// No callback data is attached.
|
|
func (in *InlineKeyboard) AddUrlButton(text, url string) *InlineKeyboard {
|
|
return in.append(tgapi.InlineKeyboardButton{Text: text, URL: url})
|
|
}
|
|
|
|
// AddUrlButtonStyle adds a button with a visual style that opens a URL.
|
|
// Style must be one of: ButtonStyleDanger, ButtonStyleSuccess, ButtonStylePrimary.
|
|
func (in *InlineKeyboard) AddUrlButtonStyle(text string, style tgapi.KeyboardButtonStyle, url string) *InlineKeyboard {
|
|
return in.append(tgapi.InlineKeyboardButton{Text: text, Style: style, URL: url})
|
|
}
|
|
|
|
// 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 {
|
|
return in.append(tgapi.InlineKeyboardButton{
|
|
Text: text,
|
|
CallbackData: NewCallbackData(cmd, args...).Encode(in.payloadType),
|
|
})
|
|
}
|
|
|
|
// AddCallbackButtonStyle adds a styled callback button.
|
|
// Style affects visual appearance; callback data is sent to bot on press.
|
|
func (in *InlineKeyboard) AddCallbackButtonStyle(text string, style tgapi.KeyboardButtonStyle, cmd string, args ...any) *InlineKeyboard {
|
|
return in.append(tgapi.InlineKeyboardButton{
|
|
Text: text,
|
|
Style: style,
|
|
CallbackData: NewCallbackData(cmd, args...).Encode(in.payloadType),
|
|
})
|
|
}
|
|
|
|
// AddButton adds a button pre-configured via InlineKbButtonBuilder.
|
|
// This is the most flexible way to create buttons with custom emoji, style, URL, and callback.
|
|
func (in *InlineKeyboard) AddButton(b InlineKbButtonBuilder) *InlineKeyboard {
|
|
return in.append(b.build())
|
|
}
|
|
|
|
// AddLine manually ends the current row and starts a new one.
|
|
// If the current row is empty, nothing happens.
|
|
func (in *InlineKeyboard) AddLine() *InlineKeyboard {
|
|
if in.CurrentLine.Len() == 0 {
|
|
return in
|
|
}
|
|
in.Lines = append(in.Lines, in.CurrentLine)
|
|
in.CurrentLine = make(extypes.Slice[tgapi.InlineKeyboardButton], 0)
|
|
return in
|
|
}
|
|
|
|
// Get finalizes the keyboard and returns a tgapi.ReplyMarkup.
|
|
// Automatically flushes the current line if not empty.
|
|
//
|
|
// Returns a pointer to a ReplyMarkup suitable for use with tgapi.SendMessage.
|
|
func (in *InlineKeyboard) Get() *tgapi.ReplyMarkup {
|
|
if in.CurrentLine.Len() > 0 {
|
|
in.Lines = append(in.Lines, in.CurrentLine)
|
|
}
|
|
return &tgapi.ReplyMarkup{InlineKeyboard: in.Lines}
|
|
}
|
|
|
|
// CallbackData represents the structured payload sent when an inline button
|
|
// with callback data is pressed.
|
|
//
|
|
// This structure is serialized to JSON and sent to the bot as a string.
|
|
// The bot should parse this back to determine the command and arguments.
|
|
//
|
|
// Example:
|
|
//
|
|
// {"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
|
|
}
|
|
|
|
// NewCallbackData creates a new CallbackData instance with the given command and args.
|
|
//
|
|
// All args are converted to strings using fmt.Sprint. This is safe for primitives
|
|
// (int, string, bool, float64) but may not serialize complex structs meaningfully.
|
|
//
|
|
// Use this to build callback payloads for bot command routing.
|
|
func NewCallbackData(command string, args ...any) *CallbackData {
|
|
stringArgs := make([]string, len(args))
|
|
for i, arg := range args {
|
|
stringArgs[i] = fmt.Sprint(arg)
|
|
}
|
|
return &CallbackData{
|
|
Command: command,
|
|
Args: stringArgs,
|
|
}
|
|
}
|
|
|
|
// ToJson serializes the CallbackData to a JSON string.
|
|
//
|
|
// If serialization fails (e.g., due to unmarshalable fields), returns a fallback
|
|
// JSON object: {"cmd":""} to prevent breaking Telegram's API.
|
|
//
|
|
// This fallback ensures the bot receives a valid JSON payload even if internal
|
|
// errors occur — avoiding "invalid callback_data" errors from Telegram.
|
|
func (d *CallbackData) ToJson() string {
|
|
data, err := encodeJsonPayload(*d)
|
|
if err != nil {
|
|
// Fallback: return minimal valid JSON to avoid Telegram API rejection
|
|
return `{"cmd":""}`
|
|
}
|
|
return data
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return ``
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Encode serializes the CallbackData according to the specified payload type.
|
|
// Supported types: BotPayloadJson and BotPayloadBase64.
|
|
// For unknown types, returns an empty string.
|
|
func (d *CallbackData) Encode(t BotPayloadType) string {
|
|
switch t {
|
|
case BotPayloadBase64:
|
|
return d.ToBase64()
|
|
case BotPayloadJson:
|
|
return d.ToJson()
|
|
}
|
|
return ""
|
|
}
|