REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
4
Inline Keyboards and Payloads
ScuroNeko edited this page 2026-05-20 13:19:27 +03:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Inline Keyboards and Payloads

Russian version: Inline-Keyboards-and-Payloads-RU

Inline keyboards in Laniakea are built explicitly: you choose a row width, add URL or callback buttons, and decide how callback payloads are encoded. The high-level goal is simple: keep button construction ergonomic while keeping payload routing predictable in handlers.

The important model first

  • InlineKeyboard builds Telegram inline keyboard markup row by row.
  • callback buttons store a CallbackData payload with a command name and string arguments.
  • payloads can be encoded as JSON or Base64.
  • the bot has a default payload type, but each keyboard can override it locally.
  • payload decoding can be tolerant or strict depending on bot configuration.
  • callback payload handlers are normal plugin payload handlers, not a separate routing subsystem.

Building a keyboard

The most direct constructors are:

  • laniakea.NewInlineKeyboardJson(maxRow)
  • laniakea.NewInlineKeyboardBase64(maxRow)
  • laniakea.NewInlineKeyboard(payloadType, maxRow)

maxRow controls how many buttons fit into one row before the builder automatically starts the next row.

Example:

kb := laniakea.NewInlineKeyboardJson(2).
	AddCallbackButton("Open", "open", 42).
	AddCallbackButton("Delete", "delete", 42).
	AddUrlButton("Docs", "https://example.com/docs")

In that example:

  • the first two buttons share the first row;
  • the URL button starts the second row automatically.

Use AddLine() when you want to end the current row manually.

When the keyboard is ready, Laniakea usually converts it for you through helpers such as:

  • ctx.Keyboard(...)
  • ctx.KeyboardLong(...)
  • ctx.EditCallback(...)

If you need the raw Telegram markup yourself, call kb.Get().

Row behavior

maxRow controls automatic wrapping.

Behavior:

  • buttons are appended to the current row until it reaches maxRow;
  • once full, the next button starts a new row automatically;
  • AddLine() forces a row break early;
  • Get() flushes the current unfinished row automatically.

This makes the builder easy to use in loops without manually managing row slices.

Callback payload structure

Callback buttons carry a CallbackData value:

type CallbackData struct {
	Command string   `json:"cmd"`
	Args    []string `json:"args"`
}

You normally do not build JSON by hand. Use:

  • AddCallbackButton(text, cmd, args...)
  • AddCallbackButtonStyle(text, style, cmd, args...)
  • NewCallbackData(cmd, args...)

All payload arguments are converted with fmt.Sprint, so handlers receive strings regardless of whether you originally passed int, bool, or another basic value.

Example:

kb := laniakea.NewInlineKeyboardJson(1).
	AddCallbackButton("Ban", "ban_user", 12345, "spam")

That produces a payload equivalent to:

{"cmd":"ban_user","args":["12345","spam"]}

Inside the payload handler, those values are available through:

  • ctx.Args for the decoded argument list;
  • the payload command name used to choose the matched handler.

There is no separate ctx.Payload object in the current API. Payload handlers receive the same MessageContext structure used elsewhere, with ctx.Args populated from callback data.

JSON vs Base64 payloads

Laniakea supports two payload encodings:

  • BotPayloadJson
  • BotPayloadBase64

JSON is easier to inspect in logs and tests. Base64 is useful when you want a denser transport representation of the same JSON payload.

The encoding choice affects how callback data is serialized into the Telegram button. It does not change the logical payload shape seen by your handler after decoding.

Callback payload size strategy

Even though Laniakea makes encoding ergonomic, callback data should still stay compact.

Good payloads usually contain:

  • an action name;
  • a small identifier;
  • one or two short arguments.

Avoid putting large serialized state into callback data. A better pattern is:

  • store compact identifiers in the payload;
  • look up the full state on the server side.

Bot default vs keyboard-local override

The bot has a default callback encoding:

bot.SetPayloadType(laniakea.BotPayloadBase64)

That default is copied into MessageContext, so ctx.NewInlineKeyboard(...) starts with the bots current payload type.

For a single keyboard, you can override it locally:

kb := ctx.NewInlineKeyboard(2).
	SetPayloadType(laniakea.BotPayloadJson).
	AddCallbackButton("Inspect", "inspect", 7)

Use a keyboard-local override when:

  • one flow benefits from human-readable JSON during debugging;
  • an older button set still needs a different encoding during migration;
  • you want explicit control instead of relying on the bot-wide default.

Strict vs tolerant decoding

When a callback arrives, the bot first tries to decode it using the bots configured payload type.

Default behavior is tolerant:

  • if the configured type is Base64 and Base64 decoding fails, Laniakea falls back to JSON;
  • if the configured type is JSON and JSON decoding fails, Laniakea falls back to Base64.

This is useful when:

  • old buttons are still in chats after a payload-format change;
  • different keyboards were generated with different local overrides;
  • you want compatibility during rollout.

Strict mode disables that fallback:

opts := (&laniakea.BotOpts{}).SetStrictPayloadType(true)

or:

bot.SetStrictPayloadType(true)

In strict mode, a mismatched payload format returns ErrPayloadTypeMismatch instead of silently trying the other decoder.

Use strict mode when payload-format drift should be treated as a real bug rather than a migration convenience.

Debug logging and payload decoding

When the bot is in debug mode and a callback is decoded from Base64, Laniakea logs the decoded JSON form for inspection.

That is useful when:

  • you want readable payload traces in logs;
  • the bot default is Base64 but you still want observability during development.

Choosing an encoding

Use JSON when:

  • you want debuggable callback payloads;
  • payloads are already short;
  • you are writing tests and want readable expectations.

Use Base64 when:

  • you want the bot-wide default to stay compact and opaque;
  • you prefer one stable encoded transport form everywhere;
  • you are migrating from earlier Base64-based usage and want consistency.

In both cases, keep payloads short and intentional. Telegram callback data is limited, so do not treat buttons as a place to serialize large state blobs. Prefer storing compact identifiers in the payload and looking up the rest on the server side.

Button builder for advanced cases

InlineKbButtonBuilder is the flexible path when you want button-specific styling or custom emoji icons.

Example:

button := laniakea.NewInlineKbButton("Confirm").
	SetStyle(laniakea.ButtonStyleSuccess).
	SetCallbackDataJson("confirm_order", 99)

kb := laniakea.NewInlineKeyboardJson(2).AddButton(button)

The builder supports:

  • SetStyle(...)
  • SetUrl(...)
  • SetIconCustomEmojiId(...)
  • SetCallbackDataJson(...)
  • SetCallbackDataBase64(...)

Use it when AddCallbackButton(...) is not expressive enough.

Styled and URL buttons

Laniakea exposes three convenience style constants:

  • ButtonStylePrimary
  • ButtonStyleSuccess
  • ButtonStyleDanger

You can use them with:

  • AddUrlButtonStyle(...)
  • AddCallbackButtonStyle(...)
  • InlineKbButtonBuilder.SetStyle(...)

URL buttons and callback buttons can live in the same keyboard. Use URL buttons for external navigation and callback buttons for bot-side actions.

Long replies with keyboards

If your plain-text reply may exceed Telegrams message length limit, use:

  • ctx.AnswerLong(...)
  • ctx.AnswerLongf(...)
  • ctx.KeyboardLong(...)

KeyboardLong(...) attaches the inline keyboard only to the final message chunk. That keeps multi-part replies readable and avoids duplicating the same keyboard on every chunk.

Routing payloads to handlers

Payloads are registered on plugins with:

  • Plugin.NewPayload(...)
  • Plugin.AddPayload(...)

Example:

plugin.NewPayload(func(ctx *laniakea.MessageContext, db *App) error {
	id := ctx.Args[0]
	ctx.AnswerCbQueryText("Handled " + id)
	return nil
}, "inspect")

When a callback arrives:

  • the bot decodes the payload;
  • finds the plugin payload handler by command name;
  • copies decoded arguments into ctx.Args;
  • runs plugin middleware;
  • runs command-specific payload middleware if configured;
  • executes the payload handler.

Related page:

Practical recommendations

  • Use ctx.NewInlineKeyboard(...) when you want the keyboard to inherit the bots current payload policy.
  • Use keyboard-local payload overrides sparingly and intentionally.
  • Prefer JSON payloads when debugging and Base64 when you want one opaque default across the project.
  • Keep callback arguments compact and reconstruct richer state on the server side.