Skip to content
Open
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
16 changes: 15 additions & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,22 @@ pending login lives in the daemon and is mirrored to
`<account>/login-state.json` at 0600, so the two steps can run minutes apart
in different processes.

Every login needs an app registration (`api_id` + `api_hash`) from
<https://my.telegram.org/apps>. Save it once and no login asks for it again:

```
tlgr auth api set 1234567 --api-hash-env TLGR_API_HASH
→ {"configured": true, "api_id": 1234567, "api_hash": "…9f0c", "path": "~/.tlgr/api.json", "updated": true}

tlgr auth api get # configured: false → the user has to register first (register_url)
```

`--api-id` with `--api-hash-env` on any login still wins over the saved
default. `auth api set` refuses the api_ids of Telegram's own apps (they get
accounts banned) and a hash that is not 32 hex characters, with exit 2.

```
tlgr auth send-code +989123456789 --alias work --api-id 12345 --api-hash-env TLGR_API_HASH
tlgr auth send-code +989123456789 --alias work
→ {"account": "work", "phone": "989…89", "type": "app", "code_hash": "5f2a…", "timeout": 60}

tlgr auth verify-code 12345 --alias work --password-env TLGR_2FA_PASSWORD
Expand Down
40 changes: 39 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,41 @@ All notable changes to tlgr are recorded here. The format follows
semantic versioning at the CLI surface, which means the JSON shapes and exit
codes documented in `AGENT.md` are the public API.

## [Unreleased]
## [2.1.0] - 2026-09-28

Log in without registering your app's credentials again for every account:
`tlgr auth api set` saves them once. Also: `chat poster list` resumes long
walks, and `chat list` no longer drops dialogs at a page boundary.

### Added

- **Save your app credentials once: `tlgr auth api set`.** Every login needs
an `api_id`/`api_hash` from my.telegram.org, and tlgr used to ask for them
again for every account you added. Now you register one app, save it, and
every later `auth send-code`, `auth qr`, `account add` and `account import`
uses it without `--api-id`:

```bash
tlgr auth api set # at a terminal: walks you through my.telegram.org, hides the hash
tlgr auth api set 1234567 --api-hash-env TLGR_API_HASH # scripted
tlgr auth api get # what is saved, hash masked; register_url when nothing is
tlgr auth api unset # forget it
```

The pair lives in `~/.tlgr/api.json` at 0600. Flags on a login still win.
Each login keeps copying the pair into the account's own `config.json`, so
changing the default later never moves an existing account to another app.
An account with no credentials of its own now falls back to the default
instead of refusing to start.

- **tlgr refuses the api_ids of Telegram's own apps.** Telegram Desktop,
Android, iOS, macOS, Web and Telegram X credentials are published in build
files and get copied into third-party tools. Logging in with them breaks
Telegram's API terms and gets accounts banned, so `auth api set` and every
login now stop with exit 2 and say where to register instead. A hash that
is not 32 hex characters is refused the same way, before it can surface as
a confusing `API_ID_INVALID`.

- **`chat poster list` can walk deeper than one call.** A single call stops
at `--max-messages 20000` and at the operation deadline, so a longer
history could not be harvested at all: a bigger number was refused and
Expand All @@ -31,6 +62,13 @@ codes documented in `AGENT.md` are the public API.

### Fixed

- **`API_ID_INVALID` and `API_ID_PUBLISHED_FLOOD` name the fix.** Both used
to exit 1 with no hint. They are now `CONFIG_ERROR` (exit 10) and point at
`tlgr auth api get` / `tlgr auth api set`.

- **Secret flag errors spell the flag the way you typed it.** A missing
variable said `--api_hash-env names X`; it now says `--api-hash-env`.

- **`chat list` no longer loses a block of dialogs at a page boundary.** The
dialog walk paired each row with its top message by message id alone, but
only private chats share one id space: every channel and supergroup numbers
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pipx install git+https://github.com/tlgrcli/tlgr.git

```bash
tlgr config init # create config files
tlgr auth api set # once: your app from my.telegram.org/apps
tlgr login +15551234567 # authenticate (shortcut for account add)
tlgr daemon start # start background daemon
tlgr send @username "Hello from tlgr" # send a message
Expand Down Expand Up @@ -472,8 +473,22 @@ alive unless you passed `--logout`.

### Logging in

Register an app once at <https://my.telegram.org/apps> (two minutes: any
title, any short name) and save its `api_id` and `api_hash`. Every account
you log in after that uses it:

```bash
tlgr auth api set # prompts; the hash is not echoed
tlgr auth api set 12345 --api-hash-env TLGR_API_HASH # the scripted form
tlgr auth api get # what is saved (hash masked)
```

tlgr refuses the api_ids of Telegram's own apps, such as the Telegram Desktop
credentials in its public build files. Logging in with them breaks Telegram's
API terms and gets accounts banned.

```bash
tlgr auth send-code <phone> --alias work --api-id 12345 --api-hash-env TLGR_API_HASH
tlgr auth send-code <phone> --alias work
tlgr auth verify-code <code> --alias work --password-env TLGR_2FA_PASSWORD
tlgr auth qr --alias work # streams tg://login tokens until one is approved
tlgr auth recover # forgot the cloud password (recovery email)
Expand Down
4 changes: 4 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ different machine.
- Use your own `api_id`/`api_hash` from <https://my.telegram.org>. Never
borrow an official client's credentials, and never spoof `device_model` to
obtain official-app behaviour: it violates the ToS and gets accounts banned.
`tlgr auth api set` and every login refuse the known official api_ids.
- `~/.tlgr/api.json` (written by `tlgr auth api set`, 0600) holds the default
`api_hash`. It is not account access on its own, but it is your app's
identity: a leaked hash lets someone else's traffic count against it.
- Use `https://` for webhooks, and verify `X-Tlgr-Signature` over the raw body
before trusting a delivery.
- `account export --string` prints full account access. It requires `--yes`
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ Coverage against the Telegram feature catalog, computed from the registry: every
`covered` is implemented today. `acct%` is covered **plus** waived — an id this build genuinely cannot cover, named in `tlgr/data/parity_waivers.toml` with the reason and the MTProto method that is missing. Ids whose feasibility is `not-applicable` or `prohibited` are excluded from the denominator once and never counted again.

```
catalog 2026-09-02 — 678 operations, 951 invocable paths
catalog 2026-09-02 — 681 operations, 954 invocable paths

domain covered req % acct% ops
auth_sessions_security 89 89 100.0% 100.0% 45
auth_sessions_security 89 89 100.0% 100.0% 48
bots_inline_payments 167 175 95.4% 100.0% 87
calls_voicechats 133 133 100.0% 100.0% 56
contacts_users 121 121 100.0% 100.0% 56
Expand Down Expand Up @@ -38,7 +38,7 @@ uncovered: 9 (9 waived with a reason)

| Domain | Covered | Required | % | Accounted % | Ops |
|---|---:|---:|---:|---:|---:|
| `auth_sessions_security` | 89 | 89 | 100.0% | 100.0% | 45 |
| `auth_sessions_security` | 89 | 89 | 100.0% | 100.0% | 48 |
| `bots_inline_payments` | 167 | 175 | 95.4% | 100.0% | 87 |
| `calls_voicechats` | 133 | 133 | 100.0% | 100.0% | 56 |
| `contacts_users` | 121 | 121 | 100.0% | 100.0% | 56 |
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

# Command reference

678 operations across 47 groups, generated from the operation registry. Groups still served by v1's hand-written commands are not listed here; they arrive with their own PR.
681 operations across 47 groups, generated from the operation registry. Groups still served by v1's hand-written commands are not listed here; they arrive with their own PR.

| Group | Operations | Reference |
|---|---:|---|
| `account` | 35 | [account.md](account.md) |
| `agent` | 7 | [agent.md](agent.md) |
| `auth` | 11 | [auth.md](auth.md) |
| `auth` | 14 | [auth.md](auth.md) |
| `boost` | 3 | [boost.md](boost.md) |
| `bot` | 57 | [bot.md](bot.md) |
| `business` | 14 | [business.md](business.md) |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/account.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ tlgr account add [PHONE] [OPTIONS]
|---|---|---|---|
| `--alias` | text | | Local name for the account. |
| `--api-hash` | text | | api_hash for this account. |
| `--api-id` | int | | api_id for this account. |
| `--api-id` | int | | api_id; default: `auth api set`'s. |
| `--bot` | flag | | Log in as a bot with a token instead. |
| `--test-dc` | flag | | Use the Telegram test DCs. |
| `--token` | text | | The bot token. |
Expand Down
89 changes: 86 additions & 3 deletions docs/reference/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

# `tlgr auth`

11 operations. Every one takes the global flags (`--json`, `--plain`, `-a/--account`, `--results-only`, `--select`, `--dry-run`, `--yes`, `--no-input`, `--flood-wait-max`, `-v`) anywhere on the line.
14 operations. Every one takes the global flags (`--json`, `--plain`, `-a/--account`, `--results-only`, `--select`, `--dry-run`, `--yes`, `--no-input`, `--flood-wait-max`, `-v`) anywhere on the line.

| Command | Summary |
|---|---|
| [`auth api get`](#tlgr-auth-api-get) | Show the default api_id a login uses |
| [`auth api set`](#tlgr-auth-api-set) | Save the api_id/api_hash every login uses by default |
| [`auth api unset`](#tlgr-auth-api-unset) | Forget the default api_id/api_hash |
| [`auth autologin-url get`](#tlgr-auth-autologin-url-get) | Append the autologin token to a telegram.org URL |
| [`auth code list`](#tlgr-auth-code-list) | Read login codes Telegram delivered to this session, and burn leaked ones |
| [`auth login-email set`](#tlgr-auth-login-email-set) | Set, verify or reset the login email the server demands during login |
Expand All @@ -18,6 +21,86 @@
| [`auth tos`](#tlgr-auth-tos) | Show, accept or decline the Terms of Service |
| [`auth verify-code`](#tlgr-auth-verify-code) | Finish a pending login: submit the code and, if asked, the password |

### `auth api get`

Show the default api_id a login uses.

Reads the default saved by `auth api set`. The hash is masked to its last four characters. `configured: false` means a login with no `--api-id` has nothing to use, and `register_url` is where to get one.

```
tlgr auth api get [OPTIONS]
```

**idempotent (reports `already`) · runs without an account · returns `ApiCredentials`**

```console
$ tlgr auth api get --json
```

<details><summary>Catalog coverage (0 full, 1 partial)</summary>

Partial: `auth.api-credentials`

Registration itself happens at my.telegram.org; tlgr stores the result.

</details>

### `auth api set`

Save the api_id/api_hash every login uses by default.

Register an app once at my.telegram.org/apps and save it here; `auth send-code`, `auth qr`, `account add --bot` and `account import` then need no `--api-id`. Each login still copies the pair into the account, so changing the default later never moves an existing account to a different app. Written to `api.json` at 0600. At a terminal with no arguments it prompts (the hash without echo). Refuses a malformed hash and the api_ids of Telegram's own apps.

```
tlgr auth api set [API_ID] [OPTIONS]
```

**mutating · idempotent (reports `already`) · runs without an account · returns `ApiCredentials`**

| Argument | Type | Required | Meaning |
|---|---|---|---|
| `API_ID` | int | no | The App api_id from my.telegram.org/apps; prompted for at a terminal. |

| Flag | Type | Default | Meaning |
|---|---|---|---|
| `--api-hash` | text | | The App api_hash; never on argv. |

```console
$ tlgr auth api set 1234567 --json
```

<details><summary>Catalog coverage (0 full, 1 partial)</summary>

Partial: `auth.api-credentials`

Registration itself happens at my.telegram.org; tlgr stores the result.

</details>

### `auth api unset`

Forget the default api_id/api_hash.

Deletes `api.json`. Accounts that already logged in keep working: each holds its own copy of the pair it was made with. Later logins need `--api-id` again, or a new `auth api set`.

```
tlgr auth api unset [OPTIONS]
```

**mutating · idempotent (reports `already`) · runs without an account · returns `ApiCredentials`**

```console
$ tlgr auth api unset --json
```

<details><summary>Catalog coverage (0 full, 1 partial)</summary>

Partial: `auth.api-credentials`

Registration itself happens at my.telegram.org; tlgr stores the result.

</details>

### `auth autologin-url get`

Append the autologin token to a telegram.org URL.
Expand Down Expand Up @@ -117,7 +200,7 @@ tlgr auth qr [OPTIONS]
|---|---|---|---|
| `--alias` | text | | Account alias to create. |
| `--api-hash` | text | | api_hash for this account. |
| `--api-id` | int | | api_id for this account. |
| `--api-id` | int | | api_id; default: `auth api set`'s. |
| `--password` | text | | The 2FA cloud password. |
| `--test-dc` | flag | | Use the Telegram test DCs. |
| `--url-only` | flag | | Print only the tg://login URL (pipe it into qrencode). |
Expand Down Expand Up @@ -240,7 +323,7 @@ tlgr auth send-code <PHONE> [OPTIONS]
| `--allow-flashcall` | flag | | Permit a flash-call code; you type the number. |
| `--allow-missed-call` | flag | | Permit a missed-call code (read the caller id). |
| `--api-hash` | text | | api_hash — never on the command line. |
| `--api-id` | int | | api_id; else TLGR_API_ID, then the config. |
| `--api-id` | int | | api_id; else TLGR_API_ID, then `auth api set`'s. |
| `--current-number` | flag | | codeSettings.current_number: this device owns it. |
| `--no-future-tokens` | flag | | Do not offer stored tokens; force a real code. |
| `--recaptcha-token` | text | | A reCAPTCHA token you solved. |
Expand Down
8 changes: 7 additions & 1 deletion plugin/skills/tlgr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,16 @@ bot-only, admin-only) and what tlgr refuses on purpose, each with a reason.
Login is two ordinary commands, so only reading the code needs a person:

```bash
tlgr auth send-code +15551234567 --alias main --api-id 12345 --api-hash-env TLGR_API_HASH --json
tlgr auth send-code +15551234567 --alias main --json
tlgr auth verify-code 12345 --alias main --json
```

`send-code` exits 10 with `CONFIG_ERROR` when no app is saved. Check with
`tlgr auth api get --json`: if `configured` is false, the user registers an
app at https://my.telegram.org/apps and runs `tlgr auth api set` themselves
(it prompts and hides the hash). Do not ask them to paste the hash into the
chat, and never use an official Telegram client's api_id.

Ask the user for the code Telegram sends them. Exit 4 with
`AUTH_PASSWORD_REQUIRED` means two-step verification is on: rerun
`verify-code` with `--password-env TLGR_2FA_PASSWORD` after the user sets
Expand Down
8 changes: 8 additions & 0 deletions tests/test_errors_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ERROR_MAP,
EXIT_AUTH,
EXIT_CODE_MAP,
EXIT_CONFIG,
EXIT_GENERIC,
EXIT_NOT_FOUND,
EXIT_PERMISSION,
Expand Down Expand Up @@ -147,6 +148,13 @@ def test_frozen_rpc_message_wins_over_the_class(self):
exc = type("RPCError", (Exception,), {})("FROZEN_METHOD_INVALID")
assert classify(exc).code == "ACCOUNT_FROZEN"

@pytest.mark.parametrize("rpc", ["API_ID_INVALID", "API_ID_PUBLISHED_FLOOD"])
def test_a_bad_app_is_a_config_error_that_names_the_fix(self, rpc):
"""The account is fine; the api_id is what needs changing, and exit 1 said nothing."""
body = classify(type("RPCError", (Exception,), {})(f"RPCError 400: {rpc}"))
assert (body.code, body.exit_code) == ("CONFIG_ERROR", EXIT_CONFIG)
assert "tlgr auth api" in (body.hint or "")

def test_msgspec_validation_error_is_usage_with_a_field(self):
with pytest.raises(msgspec.ValidationError) as excinfo:
msgspec.json.decode(b'{"chat":{"kind":1}}', type=_Outer)
Expand Down
Loading
Loading