Skip to content
Alita Robot
Esc
↑↓navigate↵open⌘Jpreview
On this page

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

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 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

Optional

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

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)
    }
}()

Cache Initialization

Infrastructure

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

i18n

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

localeManager := i18n.GetManager()
localeManager.Initialize(&Locales, "locales", i18n.DefaultManagerConfig())

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()

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,
}

Bot Client Creation

Core

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

Validation

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

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,
    Processor:   tracing.TracingProcessor{},
})

Monitoring Systems

Observability

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 starts

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, 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

Was this page helpful?