How Callback Queries Are Processed in the Zakirullin Files Bot: A Complete Guide
The Zakirullin Files Bot processes Telegram callback queries through a centralized pipeline in server/bot.go that detects the callback, extracts JSON-encoded commands, routes to specific handlers, and finally answers the query via the Telegram Bot API.
The Zakirullin Files Bot handles interactive button presses in Telegram by treating callback queries as standard update events. When a user taps an inline keyboard button, the bot receives a callback query that must be parsed, routed to business logic, and acknowledged to Telegram's API. Understanding this flow requires examining the handler architecture in server/bot.go and the API wrapper in server/pkg/tg/tg.go.
The Callback Query Processing Pipeline
The bot implements a seven-step pipeline that transforms an incoming Telegram update into executed business logic and user feedback.
Entry Point and Detection
All updates—whether messages, inline queries, or callbacks—enter through the (*Bot).Reply method in server/bot.go. The code checks for the presence of a callback identifier using u.CallbackQueryID(), which returns the query ID and a boolean flag indicating whether the update is a callback query.
// server/bot.go (lines 84-86)
if callbackQueryID, ok := u.CallbackQueryID(); ok {
// This is a callback query
}
Command Extraction and Routing
Once detected, the bot extracts the command embedded in the callback data. Because Telegram imposes a strict 64-byte limit on callback_data, the bot uses compact command strings defined as constants like CmdComplete ("c") and CmdShare ("share"). The extractCmd function unmarshals the JSON payload into a tg.Cmd struct, then looks up the command name in the handlers() map to retrieve the appropriate function.
// server/bot.go (lines 66-68, 76-84)
cmd, err := b.extractCmd(u)
if err != nil {
return err
}
handler, ok := b.handlers()[cmd.Name]
if !ok {
return fmt.Errorf("unknown command: %s", cmd.Name)
}
err = handler(cmd.Params)
Handler Execution
The mapped handler receives the command parameters and executes the business logic—for example, marking a file as complete via CmdComplete. This decouples the Telegram transport layer from domain operations, allowing handlers to focus purely on data manipulation without managing API communication.
Answering the Callback
After handler execution, Bot.Reply retrieves the original callbackQueryID and invokes b.tg.AnswerCallbackQuery(callbackQueryID, text). This sends a tgbotapi.CallbackConfig to Telegram's API, displaying a toast notification to the user. An empty string suppresses the notification while still satisfying API requirements.
// server/bot.go (lines 87-95)
if callbackQueryID, ok := u.CallbackQueryID(); ok {
err = handler(cmd.Params)
if err != nil {
return err
}
// Answer with empty string for silent acknowledgment
_ = b.tg.AnswerCallbackQuery(callbackQueryID, "")
}
The low-level implementation resides in server/pkg/tg/tg.go, where AnswerCallbackQuery constructs the actual API request:
// server/pkg/tg/tg.go (lines 70-76)
func (tg *TG) AnswerCallbackQuery(queryID, text string) error {
_, err := tg.api.Send(tgbotapi.CallbackConfig{
CallbackQueryID: queryID,
Text: text,
})
return err
}
Creating Callback Buttons with 64-Byte Constraints
Inline keyboards are constructed using helpers from the tg package. Developers create buttons by encoding concise commands to stay within Telegram's payload limit. The tg.NewCmd function serializes the command and parameters into a JSON string that fits within the 64-byte restriction.
import "github.com/zakirullin/files.md/server/pkg/tg"
func makeCompleteBtn(hash string) tg.Btn {
// CmdComplete is the short identifier "c"
cmd := tg.NewCmd(tg.CmdComplete, []string{hash})
// Button displays with checkmark emoji and text
return tg.NewBtn("✅ Complete", cmd)
}
Source: server/bot.go (lines 80-84)
When the user presses this button, Telegram sends a callback query containing the JSON representation of the tg.Cmd in the CallbackData field. The bot's extractCmd routine unmarshals this payload and routes execution to the appropriate handler.
Key Implementation Files
| File | Role |
|---|---|
server/bot.go |
Central router that detects callbacks via CallbackQueryID(), extracts commands with extractCmd(), and orchestrates handler execution. |
server/pkg/tg/tg.go |
Low-level Telegram API wrapper implementing AnswerCallbackQuery() to send CallbackConfig structs and complete the protocol handshake. |
server/pkg/tg/cmd.go |
Defines the Cmd struct and short string constants (e.g., "c", "share") used within the 64-byte callback data limit. |
server/pkg/tg/keyboard.go |
Provides NewBtn() and keyboard builders that serialize commands into JSON for the CallbackData field. |
Summary
- Unified entry point: All updates route through
(*Bot).Replyinserver/bot.go, which detects callbacks viau.CallbackQueryID(). - Compact encoding: Commands use single-character constants to respect the 64-byte
callback_datalimit imposed by Telegram. - Handler mapping: The
handlers()map routes parsedtg.Cmdstructs to business logic functions likeCmdComplete, keeping transport and domain logic separate. - Mandatory acknowledgment: Every callback must be answered via
AnswerCallbackQuery()inserver/pkg/tg/tg.goto prevent infinite loading states on buttons.
Frequently Asked Questions
Why does the Zakirullin Files Bot use single-character command codes?
Telegram restricts callback_data to 64 bytes. To maximize space for parameters while ensuring unique command identification, the bot defines short constants like "c" for complete and "share" for sharing, leaving more room for JSON-encoded arguments in the payload.
What happens if a callback query is not answered?
Telegram displays a persistent loading clock icon on the button and may retry the request multiple times. The bot's Bot.Reply method ensures every callback receives an answer via AnswerCallbackQuery(), even if the response text is an empty string to suppress notifications.
How does the bot differentiate between regular messages and callback queries?
The Update interface in server/bot.go provides CallbackQueryID(), which returns a non-empty string and true only for callback queries. This boolean check allows the same Reply method to handle both message updates and button presses transparently without separate routing logic.
Where is the callback handler mapped to its execution function?
The handlers() method in server/bot.go returns a map that associates command names (e.g., CmdComplete) with handler functions. After extractCmd parses the callback data, the bot retrieves the corresponding function from this map and invokes it with handler(cmd.Params).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →