Request Flow
How Telegram updates are processed through the Alita Robot pipeline.
This document details how Telegram updates flow through Alita Robot, from reception to response.
Update Processing Pipeline
+-------------+ +----------------+ +------------+ +----------+
| Telegram | --> | HTTP/Polling | --> | Dispatcher | --> | Handlers |
| Bot API | | Receiver | | (Router) | | (Modules)|
+-------------+ +----------------+ +------------+ +----------+
| |
| v
| +------+------+
| | Permission |
| | Checks |
| +------+------+
| |
| v
| +------+------+
| | Database |
| | / Cache |
| +------+------+
| |
v v
+----------+ +----------+
| Error | | Response |
| Handler | | to User |
+----------+ +----------+
Initialization Sequence
When the bot starts (main.go), it performs these steps in order:
Health Check Mode
If the --health flag is passed, the process performs an HTTP GET to /health and exits with the status code. This is used by Docker health checks.
if len(os.Args) > 1 && (os.Args[1] == "--health" || os.Args[1] == "-health") {
// HTTP GET to /health, exit with status code
os.Exit(0)
}Version Check
If the --version, -version, or -v flag is passed, the process prints the bot version and exits immediately without initializing any services.
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-version" || os.Args[1] == "-v") {
fmt.Println(config.AppConfig.BotVersion)
os.Exit(0)
}Panic Recovery Setup
A top-level deferred recovery ensures the process logs the panic and exits cleanly rather than crashing silently.
defer func() {
if r := recover(); r != nil {
log.Errorf("[Main] Panic recovered: %v", r)
os.Exit(1)
}
}()Cache Initialization
The Redis cache is initialized with connection retry logic (exponential backoff). Startup fails if Redis is unavailable.
if err := cache.InitCache(); err != nil {
log.Fatalf("Failed to initialize cache: %v", err)
}Locale Manager Initialization
The singleton LocaleManager loads embedded YAML translation files for all supported languages.
localeManager := i18n.GetManager()
localeManager.Initialize(&Locales, "locales", i18n.DefaultManagerConfig())OpenTelemetry Tracing Initialization
Sets up distributed tracing with OTLP or console exporters based on environment configuration (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_TRACES_SAMPLE_RATE).
tracing.InitTracing()HTTP Transport Configuration
Connection pooling is configured for outbound HTTP requests to the Telegram API.
httpTransport := &http.Transport{
MaxIdleConns: config.AppConfig.HTTPMaxIdleConns,
MaxIdleConnsPerHost: config.AppConfig.HTTPMaxIdleConnsPerHost,
IdleConnTimeout: 120 * time.Second,
ForceAttemptHTTP2: true,
}Bot Client Creation
The gotgbot client is initialized with the configured HTTP transport, a 30-second timeout, and the optional custom Bot API URL.
b, err := gotgbot.NewBot(config.AppConfig.BotToken, &gotgbot.BotOpts{
BotClient: &gotgbot.BaseBotClient{
Client: http.Client{Transport: transport, Timeout: 30 * time.Second},
DefaultRequestOpts: &gotgbot.RequestOpts{
APIURL: resolveBotAPIURL(config.AppConfig.ApiServer),
},
},
})Initial Checks
Ensures the bot user exists in the database before modules load, providing the foreign-key anchor used by downstream records. Configuration has already been loaded and validated by the config package.
if err := alita.InitialChecks(b); err != nil {
log.Fatalf("Initial checks failed: %v", err)
}Dispatcher Creation
The dispatcher routes updates to handlers with bounded concurrency. It uses a TracingProcessor wrapper for trace context propagation in polling mode.
dispatcher := ext.NewDispatcher(&ext.DispatcherOpts{
Error: errorHandler,
MaxRoutines: config.AppConfig.DispatcherMaxRoutines,
Processor: tracing.TracingProcessor{},
})Monitoring Systems
Monitoring systems are started based on configuration.
if config.AppConfig.EnableBackgroundStats {
statsCollector = monitoring.NewBackgroundStatsCollector()
}
if config.AppConfig.EnablePerformanceMonitoring {
autoRemediation = monitoring.NewAutoRemediationManager(statsCollector)
}
activityMonitor = monitoring.NewActivityMonitor() // Always startsHTTP Server & Mode Selection
The unified HTTP server registers health, metrics, and optionally pprof endpoints. Then either webhook or polling mode is activated.
httpServer := httpserver.New(config.AppConfig.HTTPPort, appStartTime)
httpServer.RegisterHealth()
httpServer.RegisterMetrics()
httpServer.RegisterDBMetrics()
if config.AppConfig.EnablePPROF {
httpServer.RegisterPPROF()
}
if config.AppConfig.UseWebhooks {
httpServer.RegisterWebhook(b, dispatcher, secret, domain)
} else {
updater.StartPolling(b, pollingOpts)
}Module Loading Order
After dispatcher creation and before Telegram updates are accepted, postInit
loads modules and starts the persisted captcha lifecycle:
func LoadModules(dispatcher *ext.Dispatcher) {
// Reset the help registry (under its lock)
modules.ResetHelpRegistry()
// Load help LAST (deferred) to collect all commands
defer modules.LoadHelp(dispatcher)
// Loads all registered modules in priority order
modules.LoadAllModules(dispatcher)
}
alita.LoadModules(dispatcher)
if err := modules.StartCaptchaLifecycle(bot); err != nil {
log.Fatalf("[Captcha] Failed to start lifecycle: %v", err)
}
Captcha startup recovery releases incomplete attempts, finalizes expired attempts, and reschedules still-valid attempts. It then starts the periodic attempt-cleanup and scheduled-unmute workers. A recovery failure aborts startup instead of accepting updates with an unknown captcha state. Challenge photo/text delivery failures use the same rollback: release the attempt and restore permissions, without retrying the send or generating a replacement. Only successful cleanup marks that challenge skipped, logs one warning on the update context, and lets the join continue to welcome. Cleanup failures propagate to the dispatcher and retain the durable scheduled-unmute retry. Captcha setting writes also ensure their foreign-key chat row with the Telegram title supplied by commands, settings callbacks, or panel access, enriching previously empty chat names without a second chat write. Synthetic member IDs never trigger captcha membership lookups; known missing members skip the join challenge, and missing/left/banned members receive no timeout moderation or fallback unmute.
Registered modules, in priority order: BotUpdates, AISpam, SpamDB, Languages, Admin, Approvals, Pins, LogChannels, Misc, Bans, Mutes, Purges, Users, Reports, Dev, Locks, Filters, Antiflood, Notes, Connections, Disabling, Rules, Warns, Greetings, Captcha, AntiRaid, Federations, Blacklists, Reactions, Formatting, Backup. Help loads last.
Handler Registration Pattern
Each module registers commands through the command pipeline (helpers.WrapCommand) and other handlers through gotgbot’s handler system. Abridged from alita/modules/bans.go:
func LoadBans(dispatcher *ext.Dispatcher) {
// Register module in help system (takes the registry lock)
SetModuleEnabled(bansModule.moduleName, true)
initBanDescs() // fills each descriptor's RequiredChecks
// Command handlers: WrapCommand runs the descriptor's checks first
helpers.WrapCommand(dispatcher, banDesc, pipelineHandler(bansModule.ban))
helpers.WrapCommand(dispatcher, kickDesc, pipelineHandler(bansModule.kick))
helpers.WrapCommand(dispatcher, unbanDesc, pipelineHandler(bansModule.unban))
// Callback query handlers
dispatcher.AddHandler(handlers.NewCallback(
callbackquery.Prefix("restrict"),
bansModule.restrictButtonHandler,
))
}
func init() {
RegisterLegacyModule("Bans", 70, LoadBans)
initBanDescs()
// Anonymous admins bypass WrapCommand; this re-runs the same checks after they prove admin rights
RegisterAnonymousAdminHandler("ban", anonPipelineHandler(banDesc, bansModule.ban))
}
Handler Types
| Type | Registration | Trigger |
|---|---|---|
| Command (pipeline) | helpers.WrapCommand(dispatcher, desc, handler) |
/cmd messages, after the descriptor’s RequiredChecks |
| Command (legacy) | handlers.NewCommand("cmd", fn) |
/cmd messages |
| Callback | handlers.NewCallback(filter, fn) |
Button presses |
| Message | handlers.NewMessage(filter, fn) |
Text messages |
| ChatMember | handlers.NewChatMember(filter, fn) |
Other members’ status changes |
| MyChatMember | handlers.NewMyChatMember(filter, fn) |
The bot’s own member status changes |
Handler Groups
Handlers can be assigned to groups for priority control:
// Negative group = higher priority (runs first)
dispatcher.AddHandlerToGroup(handler, -10)
// Group 0 = default
dispatcher.AddHandler(handler) // Same as group 0
// Positive group = lower priority
dispatcher.AddHandlerToGroup(handler, 10)
Permission Check Flow
Most admin commands follow this permission checking pattern:
func (m moduleStruct) ban(b *gotgbot.Bot, ctx *ext.Context) error {
chat := ctx.EffectiveChat
user := ctx.EffectiveSender.User
msg := ctx.EffectiveMessage
// 1. Require group chat (not private)
if !chat_status.RequireGroup(b, ctx, nil) {
return ext.EndGroups
}
// 2. Require user to be admin
if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id) {
return ext.EndGroups
}
// 3. Require bot to be admin
if !chat_status.RequireBotAdmin(b, ctx, nil) {
return ext.EndGroups
}
// 4. Check specific permission (restrict members)
if !chat_status.CanUserRestrict(b, ctx, nil, user.Id) {
return ext.EndGroups
}
// 5. Check bot has same permission
if !chat_status.CanBotRestrict(b, ctx, nil) {
return ext.EndGroups
}
// Proceed with ban logic...
}
Permission Functions Reference
| Function | Purpose | When to Use |
|---|---|---|
RequireGroup |
Ensures chat is group/supergroup | Group-only commands |
RequirePrivate |
Ensures chat is private | PM-only commands |
RequireUserAdmin |
User must be admin | Admin commands |
RequireBotAdmin |
Bot must be admin | Commands needing bot admin |
RequireUserOwner |
User must be creator | Owner-only commands |
CanUserRestrict |
User can ban/mute | Ban/mute commands |
CanBotRestrict |
Bot can ban/mute | Ban/mute commands |
CanUserDelete |
User can delete messages | Purge commands |
CanBotDelete |
Bot can delete messages | Purge commands |
CanUserPin |
User can pin messages | Pin commands |
CanBotPin |
Bot can pin messages | Pin commands |
CanUserPromote |
User can promote/demote | Admin management |
CanBotPromote |
Bot can promote/demote | Admin management |
IsUserAdmin |
Check if user is admin | Conditional logic |
IsUserInChat |
Check if user is member | User validation |
IsUserBanProtected |
Check if user is protected | Before ban/kick |
Response Patterns
Handler Return Values
// Stop processing, no more handlers run
return ext.EndGroups
// Continue to subsequent handler groups
return ext.ContinueGroups
// Error propagates to dispatcher error handler
return err
Response Actions
// Reply to the triggering message
msg.Reply(b, "Response text", &gotgbot.SendMessageOpts{
ParseMode: formatting.HTML,
})
// Send new message to chat
b.SendMessage(chat.Id, "Message text", nil)
// Edit existing message
msg.EditText(b, &gotgbot.EditMessageTextOpts{Text: "New text"})
// Delete message
msg.Delete(b, nil)
// Answer callback query
query.Answer(b, &gotgbot.AnswerCallbackQueryOpts{
Text: "Notification text",
})
Async Processing
Non-critical operations can be processed asynchronously:
// Fire and forget (with panic recovery)
go func() {
defer func() {
if r := recover(); r != nil {
log.Error("Panic in async operation")
}
}()
// Async work here
}()
// With timeout protection
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
select {
case <-time.After(2 * time.Second):
// Do delayed work
case <-ctx.Done():
log.Warn("Operation timed out")
}
}()
Error Handling in Request Flow
Dispatcher Error Handler
Error: func(b *gotgbot.Bot, ctx *ext.Context, err error) ext.DispatcherAction {
// 1. Recover from panics
defer error_handling.RecoverFromPanic("DispatcherErrorHandler", "Main")
// 2. Extract context for logging
logFields := log.Fields{
"update_id": ctx.UpdateId,
"error_type": fmt.Sprintf("%T", err),
}
// 3. Check for expected/suppressible errors
if helpers.IsExpectedTelegramError(err) {
log.WithFields(logFields).Warn("Expected Telegram API error")
return ext.DispatcherActionNoop
}
// 4. Log the error
log.WithFields(logFields).Error("Handler error")
// 5. Continue processing other updates
return ext.DispatcherActionNoop
}
Common Error Patterns
// Return to the dispatcher, which classifies and logs the error once.
// Do not also log it in the command or its deep-link helpers.
if err != nil {
return err
}
// Log but continue (non-fatal)
if err != nil {
log.Warn("Non-fatal error:", err)
}
// Silent failure for expected cases
_, _ = msg.Delete(b, nil) // Ignore delete errors
The Bot API transport counts unexpected HTTP failures by method and status.
getChatMember responses identifying a missing user/member are negative
membership lookups, not operational errors; permission and chat-access errors
still count. Failed responses on traced calls carry a bounded
telegram.error.reason category, never the raw description or request text.
The bounded alita_telegram_api_error_reasons_total{method,code,reason} counter
also counts unexpected HTTP errors on requests without a trace context.
Membership probes reject chat IDs and Telegram’s synthetic sender accounts before contacting the API; the existing anonymous-admin proof rules still apply. After a valid anonymous-admin confirmation, permissions use the confirming admin for that chat only. The original message sender remains anonymous so warning records do not disclose the confirming admin. Shared admin-cache loads retain the originating trace but use independent timeouts, so one canceled waiter cannot cancel other callers’ lookup.
The production BotClient enables AllowSendingWithoutReply for ordinary
same-chat text replies, including commands with nil or custom reply options.
Deleting the triggering message therefore does not discard the response.
Cross-chat and ephemeral replies retain Telegram’s stricter reply semantics.
Next Steps
- Module Pattern - Creating new feature modules
- Caching - Redis cache integration
- Project Structure - File organization