Persistence & pooling
Persistence
By default everything is kept in memory and nothing survives a restart.
Provide a Storage to persist the MTProto session, peer access hashes and peer
cache, and the update gap state. A single
storage.BBoltStorage
satisfies all of these at once, backed by one bbolt file:
Storage is optional in the API but effectively mandatory for any bot that
restarts. Without a persisted session every Run starts a fresh session and
re-authorizes the bot, and Telegram rate-limits repeated logins with
FLOOD_WAIT — the wait grows with each retry and a crash/restart loop can lock
the bot out for hours. With a persisted session the bot reuses its existing
authorization instead of logging in again.
import (
"go.etcd.io/bbolt"
"github.com/gotd/botapi"
"github.com/gotd/botapi/storage"
)
db, err := bbolt.Open("bot.bbolt", 0o666, nil)
if err != nil {
return err
}
opts := botapi.Options{
AppID: appID, AppHash: appHash,
Storage: storage.NewBBoltStorage(db),
}
The most important thing storage keeps is peer access hashes. The Bot API
speaks in bare int64 chat IDs, but MTProto needs an access hash for each peer.
botapi harvests those from every update it sees; persisting them means the bot
can keep addressing chats it has interacted with after a restart, instead of
re-discovering them. See peer resolution for the
underlying mechanism.
Running many bots
pool.Pool lazily starts and multiplexes bots by token over one process — the
multi-bot front end for a service serving many bots:
import "github.com/gotd/botapi/pool"
p, err := pool.New(pool.Options{
AppID: appID, AppHash: appHash,
StateDir: "state", // per-token <id>.bbolt files; in-memory if empty
IdleTimeout: time.Hour, // GC bots idle this long
})
if err != nil {
return err
}
go p.RunGC(ctx)
err = p.Do(ctx, token, func(b *botapi.Bot) error {
_, err := b.SendMessage(ctx, botapi.ID(chatID), "hi")
return err
})
Do starts and authorizes the bot on first use — concurrent callers share one
startup and a failure is returned to all of them — and gives each token its own
storage under StateDir. RunGC reaps bots that have been idle longer than
IdleTimeout; Kill and Close shut bots down explicitly.
The escape hatch
Anything the Bot API surface does not cover is one call away:
api := bot.Raw() // *tg.Client — direct MTProto
disp := bot.Dispatcher() // the raw update dispatcher
Raw() returns the underlying gotd/td *tg.Client,
so you can invoke any MTProto method the typed surface hasn't reached yet, and
Dispatcher() exposes the raw update dispatcher. This mirrors gotd/td's own
philosophy: a high-level API that never traps you below it.