Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions blog/2026-11-13-v0.166.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
slug: v0.166.0
title: "gotd v0.166.0: a bootstrap for user accounts"
authors: [ernado]
tags: [release]
---

gotd v0.166.0 adds `telegram/user`, the user-account counterpart of `telegram/bot`: one
call connects, signs in only when the stored session isn't authorized, and starts the
updates and peer managers with their state on disk. It also updates the API docs and
error constants from the Telegram documentation.

{/* truncate */}

## `telegram/user`

A long-running user client (a userbot, an archiver, a service on a user account) needs
the same wiring every time: a persistent session, a sign in that never runs twice, update
state and access hashes on disk so nothing is missed across restarts, and a peer cache.
`telegram/user` does it in one call:

```go
d := tg.NewUpdateDispatcher()
u, err := user.Start(ctx, appID, appHash, user.Options{
Dir: "state/me",
Handler: d,
Auth: user.Phone(authenticator, auth.SendCodeOptions{}),
}, telegram.WithLifetime(ctx))
if err != nil {
return err
}
defer func() { _ = u.Close() }()

return u.Wait(context.Background())
```

- An authorized session in `Dir` is used as is. A bot session, or another account than
`Options.UserID`, fails with `ErrSessionMismatch` instead of running as it.
- An unauthorized session is signed in with `user.Phone` (code and 2FA), `user.QR` (login
token, imported on another DC if needed) or `user.AuthFunc`. Without `Auth`, `Start`
fails with `ErrNotAuthorized` and sends nothing.
- `FLOOD_WAIT` on sign in is waited out only where retrying can't send a second login
code: before the code was requested, for QR exports, never for `AuthFunc`.
- Updates sent while the client was stopped are delivered after the restart, and the
access hashes of channels and users are kept, so known channels need no extra
`getDifference`.

See [User authentication](/docs/authentication/user#the-telegramuser-package) and the
[`userbot-echo`](https://github.com/gotd/td/tree/main/examples/userbot-echo) example.
`telegram/bot` now shares the same internals; its API and behaviour are unchanged.

## API docs and errors

The generated docs and error constants come from getdoc v0.54.0. 42 error constants are
new (e.g. `ErrURLExpired`, `ErrTopicNotModified`, `ErrBotCreateLimitExceeded`). Eight
that Telegram no longer documents are removed:

`ErrAnonymousReactionsDisabled`, `ErrEncryptionDeclineAdminFailed`,
`ErrGroupcallAddParticipantsFailed`, `ErrNeedDocInvalid`, `ErrParticipantCallFailed`,
`ErrPasskeyAuthRestart`, `ErrSavedPeerInvalid`, `ErrVolumeMoveInvalid`.

## Upgrading

If your code uses one of the removed constants, compare with the string instead, e.g.
`tgerr.Is(err, "SAVED_PEER_INVALID")`. Nothing else changes for existing code.

Full list of changes: [gotd/td v0.166.0](https://github.com/gotd/td/releases/tag/v0.166.0).
55 changes: 55 additions & 0 deletions docs/authentication/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,61 @@ Signing in as a user means a phone number, a login code Telegram sends you, and
account has it enabled — a 2FA password. gotd wraps this multi-step dance in
[`auth.Flow`](https://ref.gotd.dev/pkg/github.com/gotd/td/telegram/auth.html).

## The `telegram/user` package

For a user account that runs as a service (a userbot, an archiver), the
[`telegram/user`](https://ref.gotd.dev/pkg/github.com/gotd/td/telegram/user.html) package
connects, signs in only if needed, and starts the updates and peer managers in one call.
It is the user counterpart of [`telegram/bot`](./bot.md#the-telegrambot-package):

```go
import "github.com/gotd/td/telegram/user"

d := tg.NewUpdateDispatcher()
u, err := user.Start(ctx, appID, appHash, user.Options{
Dir: "state/me", // session, update state and access hashes
Handler: d,
// Used only if the stored session is not authorized.
Auth: user.Phone(authenticator, auth.SendCodeOptions{}),
}, telegram.WithLifetime(ctx))
if err != nil {
return err
}
defer func() { _ = u.Close() }()

return u.Wait(context.Background())
```

* **Session in `Dir`.** An authorized session (`session.json`) is used as is and never
signed in again. A bot session, or another account than `Options.UserID`, fails with
`user.ErrSessionMismatch`.
* **Signing in.** `Options.Auth` signs in an unauthorized session: `user.Phone` (code and
2FA password through [`auth.Flow`](#the-flow)), `user.QR` (a [login token](./qr-login.md)
shown with `QROptions.Show`, imported on another DC if needed) or `user.AuthFunc` for a
custom sign in. With `Auth` nil, `Start` returns `user.ErrNotAuthorized` without sending
anything, for services that must only run with an existing session.
* **Flood waits** on sign in are waited out, up to `MaxLoginWait` (5 minutes by default),
but only where that can't send a new code: before the code was requested for
`user.Phone`, always for QR, never for `user.AuthFunc`. Otherwise the error is returned.
* **Updates while stopped.** The [updates manager](../helpers/updates-recovery.md) keeps
its state and the access hashes of channels and users in `Dir` (`updates.json`), so
updates sent while the client was down are delivered after the restart, and known
channels need no extra `getDifference`. Updates received during sign in are buffered.
* **Peers.** The peer manager is fed with the entities of every update, with its cache on.

Register handlers on the dispatcher before `Start`; use `u.Sender()`, `u.API()`,
`u.Peers()`, `u.Self()` and `u.ID()` from them. Use `user.New` and `u.Start(ctx)` to set
things up between creating and starting. Like the client, a `User` has `Stop`,
`Shutdown`, `Close`, `Done` and `Wait` — see [Running without `Run`](../advanced/without-run.md).
Use one `Dir` per account, and don't share it between running processes.

:::tip
The runnable [`userbot-echo`](https://github.com/gotd/td/tree/main/examples/userbot-echo)
example replies to private messages as a user account.
:::

The rest of this page shows the sign in on a plain `telegram.Client`.

## The flow

You provide an [`auth.UserAuthenticator`](#the-userauthenticator-interface) — something
Expand Down
2 changes: 2 additions & 0 deletions docs/basics/handling-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ Without a callback, start the client with `client.Start` and block in
[Running without `Run`](../advanced/without-run.md). The
[`telegram/bot`](../authentication/bot.md#the-telegrambot-package) package does this for
bots, together with update recovery; the [Echo bot tutorial](./echo-bot.md) uses it.
[`telegram/user`](../authentication/user.md#the-telegramuser-package) does the same for
user accounts.

## Don't miss updates

Expand Down
5 changes: 3 additions & 2 deletions docs/helpers/updates-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ gaps := updates.New(updates.Config{
})
```

The [`telegram/bot`](../authentication/bot.md#the-telegrambot-package) package sets this
up for bots.
The [`telegram/bot`](../authentication/bot.md#the-telegrambot-package) and
[`telegram/user`](../authentication/user.md#the-telegramuser-package) packages set this
up for bots and user accounts.

## Access hashes

Expand Down
Loading