Skip to content

Request Flow

This document details how Telegram updates flow through Alita Robot, from reception to response.

+-------------+ +----------------+ +------------+ +----------+
| Telegram | --> | HTTP/Polling | --> | Dispatcher | --> | Handlers |
| Bot API | | Receiver | | (Router) | | (Modules)|
+-------------+ +----------------+ +------------+ +----------+
| |
| v
| +------+------+
| | Permission |
| | Checks |
| +------+------+
| |
| v
| +------+------+
| | Database |
| | / Cache |
| +------+------+
| |
v v
+----------+ +----------+
| Error | | Response |
| Handler | | to User |
+----------+ +----------+

When the bot starts (main.go), it performs these steps in order:

  1. Health Check Mode Optional

    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 os.Args[1] == "--health" {
    // HTTP GET to /health, exit with status code
    os.Exit(0)
    }
  2. Version Check Optional

    If the --version, -version, or -v flag is passed, the process prints the bot version and exits immediately without initializing any services.

    if os.Args[1] == "--version" || os.Args[1] == "-version" || os.Args[1] == "-v" {
    fmt.Println(config.AppConfig.BotVersion)
    os.Exit(0)
    }
  3. Panic Recovery Setup Safety

    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)
    }
    }()
  4. Cache Initialization Infrastructure

    The Redis cache is initialized with connection retry logic (exponential backoff). Cache must be available before i18n (which uses it for translation caching) and before the database layer.

    if err := cache.InitCache(); err != nil {
    log.Fatalf("Failed to initialize cache: %v", err)
    }
  5. Locale Manager Initialization i18n

    The singleton LocaleManager loads embedded YAML translation files for all supported languages.

    localeManager := i18n.GetManager()
    localeManager.Initialize(&Locales, "locales", i18n.DefaultManagerConfig())
  6. OpenTelemetry Tracing Initialization Observability

    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()
  7. HTTP Transport Configuration Networking

    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,
    }
  8. Bot Client Creation Core

    The gotgbot client is initialized with the configured HTTP transport and a 30-second timeout.

    b, err := gotgbot.NewBot(config.AppConfig.BotToken, &gotgbot.BotOpts{
    BotClient: &gotgbot.BaseBotClient{
    Client: http.Client{Transport: transport, Timeout: 30 * time.Second},
    },
    })
  9. Connection Pre-warming Performance

    Three GetMe calls run in a background goroutine to establish the HTTP connection pool before real traffic arrives.

    go func() {
    for i := 0; i < 3; i++ {
    b.GetMe(nil) // Establish connection pool
    time.Sleep(100 * time.Millisecond)
    }
    }()

    Pre-warming prevents the first real user request from paying the TCP + TLS handshake cost. The 3 calls ensure multiple connections are established in the pool.

  10. Initial Checks Validation

    Validates configuration, initializes cache, and performs startup health checks.

    alita.InitialChecks(b) // Validates config, initializes cache
  11. Async Processor Optional

    If EnableAsyncProcessing is configured, the async processor is initialized for non-critical background operations.

    if config.AppConfig.EnableAsyncProcessing {
    async.InitializeAsyncProcessor()
    }
  12. Dispatcher Creation Core

    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,
    })
  13. Monitoring Systems Observability

    Three monitoring subsystems start: background stats collection (every 30s), auto-remediation (GC triggers on memory thresholds), and activity monitoring (per-chat and per-user tracking).

    statsCollector = monitoring.NewBackgroundStatsCollector()
    autoRemediation = monitoring.NewAutoRemediationManager(statsCollector)
    activityMonitor = monitoring.NewActivityMonitor()
  14. HTTP Server & Mode Selection Core

    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)
    httpServer.RegisterHealth()
    httpServer.RegisterMetrics()
    if config.AppConfig.UseWebhooks {
    httpServer.RegisterWebhook(b, dispatcher, secret, domain)
    } else {
    updater.StartPolling(b, pollingOpts)
    }

After dispatcher creation, modules are loaded via alita/main.go:

func LoadModules(dispatcher *ext.Dispatcher) {
// Initialize help system first
modules.HelpModule.AbleMap.Init()
// Load help LAST (deferred) to collect all commands
defer modules.LoadHelp(dispatcher)
// Core modules (order matters for handler priority)
modules.LoadBotUpdates(dispatcher) // Bot status tracking
modules.LoadAntispam(dispatcher) // Spam protection
modules.LoadLanguage(dispatcher) // Language settings
modules.LoadAdmin(dispatcher) // Admin commands
modules.LoadPin(dispatcher) // Pin management
modules.LoadMisc(dispatcher) // Misc utilities
modules.LoadBans(dispatcher) // Ban/kick commands
modules.LoadMutes(dispatcher) // Mute commands
modules.LoadPurges(dispatcher) // Message purging
modules.LoadUsers(dispatcher) // User tracking
modules.LoadReports(dispatcher) // Report system
modules.LoadDev(dispatcher) // Developer tools
modules.LoadLocks(dispatcher) // Chat locks
modules.LoadFilters(dispatcher) // Message filters
modules.LoadAntiflood(dispatcher) // Flood protection
modules.LoadNotes(dispatcher) // Notes system
modules.LoadConnections(dispatcher) // Chat connections
modules.LoadDisabling(dispatcher) // Command disabling
modules.LoadRules(dispatcher) // Rules management
modules.LoadWarns(dispatcher) // Warning system
modules.LoadGreetings(dispatcher) // Welcome messages
modules.LoadCaptcha(dispatcher) // CAPTCHA verification
modules.LoadBlacklists(dispatcher) // Blacklist system
modules.LoadBackup(dispatcher) // Backup/export commands (load last)
}

Each module registers handlers using gotgbot’s handler system:

func LoadBans(dispatcher *ext.Dispatcher) {
// Register module in help system
HelpModule.AbleMap.Store(bansModule.moduleName, true)
// Command handlers
dispatcher.AddHandler(handlers.NewCommand("ban", bansModule.ban))
dispatcher.AddHandler(handlers.NewCommand("kick", bansModule.kick))
dispatcher.AddHandler(handlers.NewCommand("unban", bansModule.unban))
// Callback query handlers
dispatcher.AddHandler(handlers.NewCallback(
callbackquery.Prefix("restrict."),
bansModule.restrictButtonHandler,
))
}
TypeRegistrationTrigger
Commandhandlers.NewCommand("cmd", fn)/cmd messages
Callbackhandlers.NewCallback(filter, fn)Button presses
Messagehandlers.NewMessage(filter, fn)Text messages
ChatMemberhandlers.NewChatMemberUpdated(filter, fn)Member updates

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)

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, false) {
return ext.EndGroups
}
// 2. Require user to be admin
if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id, false) {
return ext.EndGroups
}
// 3. Require bot to be admin
if !chat_status.RequireBotAdmin(b, ctx, nil, false) {
return ext.EndGroups
}
// 4. Check specific permission (restrict members)
if !chat_status.CanUserRestrict(b, ctx, nil, user.Id, false) {
return ext.EndGroups
}
// 5. Check bot has same permission
if !chat_status.CanBotRestrict(b, ctx, nil, false) {
return ext.EndGroups
}
// Proceed with ban logic...
}
FunctionPurposeWhen to Use
RequireGroupEnsures chat is group/supergroupGroup-only commands
RequirePrivateEnsures chat is privatePM-only commands
RequireUserAdminUser must be adminAdmin commands
RequireBotAdminBot must be adminCommands needing bot admin
RequireUserOwnerUser must be creatorOwner-only commands
CanUserRestrictUser can ban/muteBan/mute commands
CanBotRestrictBot can ban/muteBan/mute commands
CanUserDeleteUser can delete messagesPurge commands
CanBotDeleteBot can delete messagesPurge commands
CanUserPinUser can pin messagesPin commands
CanBotPinBot can pin messagesPin commands
CanUserPromoteUser can promote/demoteAdmin management
CanBotPromoteBot can promote/demoteAdmin management
IsUserAdminCheck if user is adminConditional logic
IsUserInChatCheck if user is memberUser validation
IsUserBanProtectedCheck if user is protectedBefore ban/kick
// 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
// Reply to the triggering message
msg.Reply(b, "Response text", &gotgbot.SendMessageOpts{
ParseMode: helpers.HTML,
})
// Send new message to chat
b.SendMessage(chat.Id, "Message text", nil)
// Edit existing message
msg.EditText(b, "New text", nil)
// Delete message
msg.Delete(b, nil)
// Answer callback query
query.Answer(b, &gotgbot.AnswerCallbackQueryOpts{
Text: "Notification text",
})

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: 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
}
// Log and return error (propagates to dispatcher)
if err != nil {
log.Error(err)
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