telega/bot
Core bot actor and chat instance management.
This module implements the actor-based architecture for handling Telegram updates.
It contains the Bot actor (the central dispatcher) and ChatInstance actors
(one per unique {chat_id}:{from_id} combination).
Supervision tree
Both the Bot actor and ChatInstance actors run inside a supervision tree
created by telega.init() or telega.init_for_polling():
TelegaRootSupervisor (static_supervisor, OneForOne)
├── ChatInstances (factory_supervisor, Transient children)
│ ├── ChatInstance {chat1:user1}
│ ├── ChatInstance {chat2:user2}
│ └── ...
├── Bot actor (worker, Permanent)
└── Polling worker (worker, Permanent) — only for polling mode
- The
Botactor is aPermanentworker — it always restarts on crash. ChatInstanceactors areTransient— they restart only on abnormal exit, not on normal shutdown. On restart aChatInstancere-registers itself in the ETS registry, overwriting the stale subject.- The
Botcreates newChatInstanceactors viafactory_supervisor.start_child, which ensures they are supervised from the moment they start.
Handler pattern
All handlers follow this signature:
fn handler(ctx: Context(session, error, dependencies), data: Type) -> Result(Context(session, error, dependencies), error)
Always return the updated context — it carries the (potentially modified) session.
Conversation API
The wait_handler function and the Handler type enable multi-message
conversations: the chat instance suspends its main handler and waits for a
specific update type. See telega.wait_text, telega.wait_command, etc.
Types
Stores information about running bot instance
pub opaque type Bot(session, error, dependencies)
pub opaque type BotMessage
pub type BotSubject =
process.Subject(BotMessage)
pub type CallbackQueryFilter {
CallbackQueryFilter(re: regexp.Regexp)
}
Constructors
-
CallbackQueryFilter(re: regexp.Regexp)
Handler called when an error occurs in handler
If handler returns Error, the bot will be stopped and the error will be logged
The default handler is fn(_) -> Ok(Nil), which will do nothing if handler returns an error
pub type CatchHandler(session, error, dependencies) =
fn(Context(session, error, dependencies), error) -> Result(
Nil,
error,
)
Arguments for starting a chat instance via factory supervisor.
pub type ChatInstanceArgs(session, error, dependencies) {
ChatInstanceArgs(
key: String,
config: @internal Config,
session_settings: SessionSettings(session, error),
catch_handler: fn(
Context(session, error, dependencies),
error,
) -> Result(Nil, error),
dependencies: dependencies,
router_handler: fn(
Context(session, error, dependencies),
update.Update,
) -> Result(Context(session, error, dependencies), error),
bot_info: types.User,
registry: @internal Registry(
ChatInstanceMessage(session, error, dependencies),
),
bot_subject: process.Subject(BotMessage),
)
}
Constructors
-
ChatInstanceArgs( key: String, config: @internal Config, session_settings: SessionSettings(session, error), catch_handler: fn(Context(session, error, dependencies), error) -> Result( Nil, error, ), dependencies: dependencies, router_handler: fn( Context(session, error, dependencies), update.Update, ) -> Result(Context(session, error, dependencies), error), bot_info: types.User, registry: @internal Registry( ChatInstanceMessage(session, error, dependencies), ), bot_subject: process.Subject(BotMessage), )
pub opaque type ChatInstanceMessage(session, error, dependencies)
pub type ChatInstanceSubject(session, error, dependencies) =
process.Subject(
ChatInstanceMessage(session, error, dependencies),
)
Context holds information needed for the bot instance and the current update.
pub type Context(session, error, dependencies) {
Context(
key: String,
update: update.Update,
config: @internal Config,
session: session,
dependencies: dependencies,
chat_subject: process.Subject(
ChatInstanceMessage(session, error, dependencies),
),
start_time: option.Option(timestamp.Timestamp),
log_prefix: option.Option(String),
bot_info: types.User,
)
}
Constructors
-
Context( key: String, update: update.Update, config: @internal Config, session: session, dependencies: dependencies, chat_subject: process.Subject( ChatInstanceMessage(session, error, dependencies), ), start_time: option.Option(timestamp.Timestamp), log_prefix: option.Option(String), bot_info: types.User, )Arguments
- dependencies
-
Non-persisted services/dependencies injected at bot init (DI container). Unlike
session,dependenciesis never persisted — it holds things like a db pool, http client, or i18n catalog. Seetelega.with_dependencies. - start_time
-
Used to calculate the duration of the conversation in logs
pub type Handler(session, error, dependencies) {
HandleAll(
handler: fn(
Context(session, error, dependencies),
update.Update,
) -> Result(Context(session, error, dependencies), error),
)
HandleCommand(
command: String,
handler: fn(
Context(session, error, dependencies),
update.Command,
) -> Result(Context(session, error, dependencies), error),
)
HandleCommands(
commands: List(String),
handler: fn(
Context(session, error, dependencies),
update.Command,
) -> Result(Context(session, error, dependencies), error),
)
HandleText(
handler: fn(Context(session, error, dependencies), String) -> Result(
Context(session, error, dependencies),
error,
),
)
HandleHears(
hears: Hears,
handler: fn(Context(session, error, dependencies), String) -> Result(
Context(session, error, dependencies),
error,
),
)
HandleMessage(
handler: fn(
Context(session, error, dependencies),
types.Message,
) -> Result(Context(session, error, dependencies), error),
)
HandleVoice(
handler: fn(
Context(session, error, dependencies),
types.Voice,
) -> Result(Context(session, error, dependencies), error),
)
HandleAudio(
handler: fn(
Context(session, error, dependencies),
types.Audio,
) -> Result(Context(session, error, dependencies), error),
)
HandleVideo(
handler: fn(
Context(session, error, dependencies),
types.Video,
) -> Result(Context(session, error, dependencies), error),
)
HandlePhotos(
handler: fn(
Context(session, error, dependencies),
List(types.PhotoSize),
) -> Result(Context(session, error, dependencies), error),
)
HandleWebAppData(
handler: fn(
Context(session, error, dependencies),
types.WebAppData,
) -> Result(Context(session, error, dependencies), error),
)
HandleCallbackQuery(
filter: CallbackQueryFilter,
handler: fn(
Context(session, error, dependencies),
String,
String,
) -> Result(Context(session, error, dependencies), error),
)
HandleChatMember(
handler: fn(
Context(session, error, dependencies),
types.ChatMemberUpdated,
) -> Result(Context(session, error, dependencies), error),
)
}
Constructors
-
HandleAll( handler: fn( Context(session, error, dependencies), update.Update, ) -> Result(Context(session, error, dependencies), error), )Handle all messages.
-
HandleCommand( command: String, handler: fn( Context(session, error, dependencies), update.Command, ) -> Result(Context(session, error, dependencies), error), )Handle a specific command.
-
HandleCommands( commands: List(String), handler: fn( Context(session, error, dependencies), update.Command, ) -> Result(Context(session, error, dependencies), error), )Handle multiple commands.
-
HandleText( handler: fn(Context(session, error, dependencies), String) -> Result( Context(session, error, dependencies), error, ), )Handle text messages.
-
HandleHears( hears: Hears, handler: fn(Context(session, error, dependencies), String) -> Result( Context(session, error, dependencies), error, ), )Handle text message with a specific substring.
-
HandleMessage( handler: fn( Context(session, error, dependencies), types.Message, ) -> Result(Context(session, error, dependencies), error), )Handle any message.
-
HandleVoice( handler: fn(Context(session, error, dependencies), types.Voice) -> Result( Context(session, error, dependencies), error, ), )Handle voice messages.
-
HandleAudio( handler: fn(Context(session, error, dependencies), types.Audio) -> Result( Context(session, error, dependencies), error, ), )Handle audio messages.
-
HandleVideo( handler: fn(Context(session, error, dependencies), types.Video) -> Result( Context(session, error, dependencies), error, ), )Handle video messages.
-
HandlePhotos( handler: fn( Context(session, error, dependencies), List(types.PhotoSize), ) -> Result(Context(session, error, dependencies), error), )Handle photo messages.
-
HandleWebAppData( handler: fn( Context(session, error, dependencies), types.WebAppData, ) -> Result(Context(session, error, dependencies), error), )Handle web app data messages.
-
HandleCallbackQuery( filter: CallbackQueryFilter, handler: fn( Context(session, error, dependencies), String, String, ) -> Result(Context(session, error, dependencies), error), )Handle callback query. Context, data from callback query and
callback_query_idare passed to the handler. -
HandleChatMember( handler: fn( Context(session, error, dependencies), types.ChatMemberUpdated, ) -> Result(Context(session, error, dependencies), error), )Handle chat member update (when user joins/leaves a group). The bot must be an administrator in the chat and must explicitly specify “chat_member” in the list of
allowed_updatesto receive these updates.
pub type Hears {
HearText(text: String)
HearTexts(texts: List(String))
HearRegex(regex: regexp.Regexp)
HearRegexes(regexes: List(regexp.Regexp))
}
Constructors
-
HearText(text: String) -
HearTexts(texts: List(String)) -
HearRegex(regex: regexp.Regexp) -
HearRegexes(regexes: List(regexp.Regexp))
Limited context handed to pre-router middleware. A PreHandler runs once per
incoming update inside the Bot actor — before any chat instance is
spawned or session is loaded — so it only carries update-level data, not a
session. Use it for cross-cutting concerns that apply to every update:
anti-spam, analytics, and update deduplication (telega/idempotency).
pub type PreContext(dependencies) {
PreContext(
update: update.Update,
config: @internal Config,
dependencies: dependencies,
bot_info: types.User,
)
}
Constructors
-
PreContext( update: update.Update, config: @internal Config, dependencies: dependencies, bot_info: types.User, )Arguments
- dependencies
-
The same injected services available to handlers via
Context.
Pre-router middleware: a single global pass over every update, run before
routing. Registered with telega.use_pre_handler and executed in the order
added; the first one that returns Stop short-circuits the rest and the
router. Because they run sequentially inside the single Bot actor,
read-then-write logic (e.g. dedup) is race-free across concurrent updates.
pub type PreHandler(dependencies) =
fn(PreContext(dependencies)) -> PreRouterResult
Decision returned by a PreHandler: keep processing the update through the
router, or stop it here (drop it before routing).
pub type PreRouterResult {
Continue
Stop
}
Constructors
-
ContinueContinue to the next pre-router middleware and, eventually, the router.
-
StopStop processing this update. The webhook/poller is told the update was acknowledged (so Telegram does not retry it) but no handler runs.
pub type SessionSettings(session, error) {
SessionSettings(
persist_session: fn(String, session) -> Result(session, error),
get_session: fn(String) -> Result(
option.Option(session),
error,
),
default_session: fn() -> session,
)
}
Constructors
-
SessionSettings( persist_session: fn(String, session) -> Result(session, error), get_session: fn(String) -> Result(option.Option(session), error), default_session: fn() -> session, )
Values
pub fn cancel_conversation(
bot bot: Bot(session, error, dependencies),
key key: String,
) -> Nil
Stops waiting for any handler for specific key (chat_id)
pub fn drain(
bot_subject bot_subject: process.Subject(BotMessage),
timeout timeout: Int,
) -> Int
Begin a graceful drain of the bot.
Stops accepting new updates and blocks until all in-flight updates finish or
timeout milliseconds elapse. Returns the number of updates that were
in-flight when the drain started, or -1 if the timeout was reached before
draining completed.
pub fn get_session(
session_settings: SessionSettings(session, error),
update: update.Update,
) -> Result(option.Option(session), error)
pub fn is_draining(
bot_subject bot_subject: process.Subject(BotMessage),
) -> Bool
Whether the bot is currently draining (no longer accepting new updates).
Webhook adapters use this to answer 503 so Telegram retries the update
after the deploy instead of dropping it.
pub fn next_session(
ctx ctx: Context(session, error, dependencies),
session session: session,
) -> Result(Context(session, error, dependencies), error)
pub fn start(
registry registry: @internal Registry(
ChatInstanceMessage(session, error, dependencies),
),
config config: @internal Config,
bot_info bot_info: types.User,
router_handler router_handler: fn(
Context(session, error, dependencies),
update.Update,
) -> Result(Context(session, error, dependencies), error),
pre_handlers pre_handlers: List(
fn(PreContext(dependencies)) -> PreRouterResult,
),
session_settings session_settings: SessionSettings(
session,
error,
),
catch_handler catch_handler: fn(
Context(session, error, dependencies),
error,
) -> Result(Nil, error),
dependencies dependencies: dependencies,
chat_factory chat_factory: factory_supervisor.Supervisor(
ChatInstanceArgs(session, error, dependencies),
process.Subject(
ChatInstanceMessage(session, error, dependencies),
),
),
name name: option.Option(process.Name(BotMessage)),
) -> Result(
actor.Started(process.Subject(BotMessage)),
actor.StartError,
)
pub fn start_chat_instance(
args: ChatInstanceArgs(session, error, dependencies),
) -> Result(
actor.Started(
process.Subject(
ChatInstanceMessage(session, error, dependencies),
),
),
actor.StartError,
)
Start a chat instance. Used as the template function for factory_supervisor. Self-registers in the registry on start (handles both first start and restart after crash).
pub fn wait_handler(
ctx ctx: Context(session, error, dependencies),
handler handler: Handler(session, error, dependencies),
handle_else handle_else: option.Option(
Handler(session, error, dependencies),
),
timeout timeout: option.Option(Int),
) -> Result(Context(session, error, dependencies), error)
Pass any handler to start waiting
or - calls if there are any other updates
timeout - the conversation will be canceled after this timeout