gotd v0.164.0: built for long-running clients
gotd v0.164.0 and gotd/contrib v0.26.0 are about clients that run for weeks: archivers, userbots and services with many accounts in one process. Most changes came from running gotd in such an archiver. They close gaps where gotd went quiet, waited forever or lost data, and add the metrics needed to see that it works. Many fixes follow what Telegram Desktop and TDLib do; we checked them against both, and against the live Telegram servers.
Highlights
Start, Stop and Wait
A client no longer has to live inside a Run callback. Start connects and returns once
the client is ready; Wait blocks until it stops and tells you why; Stop, Shutdown
and Close end it:
client := telegram.NewClient(appID, appHash, opts)
if err := client.Start(ctx, telegram.WithLifetime(ctx)); err != nil {
return err
}
defer func() { _ = client.Close() }()
return client.Wait(context.Background())
telegram.Connect creates and starts a client in one call. Run works as before. See
Running without Run.
Bots in one call
The new telegram/bot package starts a bot with everything a long-running bot needs:
the session and the update state in a directory, so updates sent while it was stopped are
delivered after a restart; a check that the stored session belongs to the token's bot;
flood waits on sign in waited out; and a peer manager fed from every update.
b, err := bot.Start(ctx, appID, appHash, token, bot.Options{Dir: "state", Handler: d})
auth.ParseBotToken validates tokens with the Bot API server's rules, without putting
the secret in errors, and updates.FileStorage keeps the update state in a JSON file.
See Bot authentication and the
echo bot tutorial.
Know when something is wrong
- Lost authorization.
OnAuthLostis called once when an authorized session is terminated (AUTH_KEY_UNREGISTERED,SESSION_REVOKEDand similar). Return an error to stopRunwith*telegram.AuthLostError, so one account can be stopped and reported while others keep running. - Banned, deleted and frozen accounts.
AuthLostError.Kind()tells a ban or a deleted account from a terminated session.AUTH_KEY_DROP_*service notifications are reported throughOnAuthLost.OnFrozenandClient.FreezeStatereport a frozen account, checked through the app config as the official apps do. - Connection events with a DC.
OnConnectionStateEventandOnConnDeadsay which DC and which kind of connection (primary, data, CDN) an event is about. - Stuck requests.
RequestTimeoutsets a per-request deadline. When it passes, the client reconnects and resends the request once, like Telegram Desktop. NewPingIntervalandPingTimeoutoptions control liveness checks. - Metrics and logs. Pass a
MeterProviderto the client and the updates manager for OpenTelemetry metrics of connections, requests, pools, update recovery and downloads, with debug logs at each event. Nothing is recorded unless you set it. See Observability.
Connections that recover
- Lost connection state. After the server drops a connection's state (seen when an
account signs in on another device), requests failed with
CONNECTION_NOT_INITEDorCONNECTION_LAYER_INVALIDuntil a restart. The client now sendsinitConnectionagain with the failed request. - Slow answers. A request the server didn't acknowledge after all resends failed with "retry limit reached", and its answer was dropped when it came. On a busy DC, answers after 30–60 seconds happen. The client now keeps waiting for the answer, bounded by your context, as TDLib and Telegram Desktop do.
- Misconfiguration.
API_ID_INVALIDand other permanent errors of the connection setup made the client reconnect every few seconds forever.Runnow returns*telegram.ConfigError. - Key regeneration. After the server forgot the auth key, the new key reused the old session's state; it now starts a clean session and resends pending requests in it.
- Acks. Messages that arrived inside a container were never acknowledged, so the server kept them; they are now acked like top-level ones.
- DC transfer. Connecting to another DC could hang when its key had to be recreated.
Updates that don't go missing
- Difference contents. Updates returned inside
getDifferenceandgetChannelDifference(other_updates: edits, deletions, read marks…) went through the gap check and could be dropped. They are now applied in TDLib's order and passed to your handler. This includes fixes and tests by @igitur and @vzd3v from #1854, #1864, #1866 and #1867 — thank you. - Errors in handlers. With
StopOnApplyError, an error from your handler or from the state storage stopsRuninstead of being logged, and the state isn't advanced past the failed updates, so they are delivered again after a restart. - Messages returned with
channelDifferenceTooLongwere dropped. They are now passed toOnChannelGaptogether with the gap range. They're not sent to the handler, since they may include messages it already received. Manager.TrackChanneltracks channels joined while offline, and channels reported byOnLoadChannelStateFailed.BeforeRundecides what happens to updates that arrive before the manager starts: pass them, buffer them, or drop them for recovery to fetch again.UpdateDispatcherhandlers registered on one copy are now seen by every copy, soOnFallbackis no longer lost when the dispatcher is passed by value. The newOnAny*handlers cover private chats, groups and channels in one place, including forum topics. TheOnBeforehook sees every update.
Downloads for archivers
- Resume with
WithOffset, a knownWithSizeandWithProgress.Builder.Verifychecks a local file against the server's hashes; it no longer fails on files whose size is a multiple of the hash window. WithRefreshgets a fresh location when a file reference expires. The newfilerefpackage builds one from a message, a story or a profile photo.Scheduleris an adaptive window of part requests shared between downloads, like Telegram Desktop's. It grows on success and halves on flood waits. SetOptions.DownloadSchedulerto get one per DC.Client.FileDCgives a cached connection pool for a DC and followsFILE_MIGRATE.Client.DCto the home DC no longer fails withDC_ID_INVALID. CDN downloads now work through any invoker (DownloadVia).
Queries
- Iterators request full server pages by default (100 messages or dialogs, 200 participants) and no longer stop early on a short page.
MinID/MaxIDon history iterators.- Resumable cursors for messages and dialogs.
ForEachFolderover all dialog folders.GetMessagesWithEntities.- A new
query/storiespackage. DialogKey.InputPeer().ExtractPeerno longer builds unusable peers from min users. Min users and channels are merged into full ones the way TDLib does it.
Wire traces
Options.WireTracer sees every MTProto message after decryption and every connection
event, on all connections of a client. The wiretrace package and the cmd/wiretrace
tool decode such traces and the ones written by a traced Telegram Desktop build, and
compare them: we used it to check gotd's traffic against Telegram Desktop's, step by
step. See Debugging.
Also
- Layer 233.
- HTML widget messages for bots:
message.Widget, shared widgets and widget queries. See Sending messages. - QR login completes 2FA through
Options.Password, keeps a scan that arrives while the code is being shown, retries token refresh errors, and completes the login on the account's DC when it's another one. - Message entities are sorted like TDLib does; the old order was wrong for some inputs.
- Takeout sessions can be resumed with
takeout.NewClientand handleTAKEOUT_INIT_DELAY. tg.EntitiesFrom(users, chats).
gotd/contrib v0.26.0
- Storage: persistent updates state and access hashes for pebble, bbolt and redis.
pebble and bbolt also implement
peers.Storagefortelegram/peers. - Fewer writes: pebble honours its write options and writes a whole update's peers in one batch instead of one fsync per peer. Unchanged peers aren't rewritten.
- Encoding: a compact binary peer encoding, opt-in with
WithBinaryEncoding(). Versions before v0.26.0 can't read it, so turn it on only once you won't roll back. - Min users: they no longer overwrite stored full users.
- floodwait gains:
- concurrent dispatch, so a slow request no longer blocks the others;
- adaptive pacing, policies, exempt methods and retries of 5xx errors;
- a snapshot for metrics.
- Fixes: pebble skips only the peers it can't encode in a batch, bbolt iterates channels in a read-only transaction, and floodwait uses its clock everywhere and decays the wait after errors.
Upgrading
Most changes are additions. Check these:
- Iterators use the server's page size by default (it was 1 for some generated
iterators) and read until the last page. Set
BatchSizeif you relied on the old values. - Updates: updates inside differences now reach your handler, so you may see edits,
deletions and read marks you didn't get before. An
updatePtsChangedinside a difference makes the manager fetch the difference once more. Runreturns*telegram.ConfigErroronAPI_ID_INVALID,API_ID_PUBLISHED_FLOODand other permanent errors of the connection setup, where it used to reconnect forever.- Slow requests are no longer failed with "retry limit reached":
rpc.Engine.Dodoesn't returnRetryLimitReachedErrany more, and waits for the answer until your context is done. Use a context deadline orRequestTimeoutto bound a request. ExtractPeerreturnspeer.ErrMinPeerfor min users and channels, where it used to build an input peer from the min access hash. The iterators handle this. Check your own callers.- After
Runstops,updates.Manager.HandleandTrackChannelreturnupdates.ErrStoppedinstead of blocking. StartandRunare exclusive, and a client can be started once.- contrib storage:
storage.Peer.FromUserreturns false for min users;CreatedAtnow means "first stored with this content";- pebble writes without sync if you passed
NoSyncwrite options, which used to be ignored.
- contrib floodwait now runs requests concurrently; it used to send one per tick.
- contrib/bg still works, but
Client.Startreplaces it.
Thanks to everyone who reported issues in @gotd_en and @gotd_ru. Full list of changes: gotd/td, gotd/contrib.
