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
47 changes: 47 additions & 0 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,53 @@ outreach campaign has contacted. Idempotent: it reads the fresh
there is nothing to do, so repeating a pass over hundreds of peers is nearly
free.

### Bots, inline mode, mini apps and payments

```
tlgr bot get <bot> # the whole profile card in one call
→ {"id": …, "username": "…", "about": "…", "description": "…",
"commands": [{"command": "start", "description": "…", "has_help": true}],
"menu_button": {"kind": "commands"}, "bot_info_version": 3, …}

tlgr bot start <bot> [--param X] # /start, with a hidden deep-link payload
tlgr bot stop <bot> [--delete-chat] # block, optionally wiping the history
tlgr bot command send <bot> <cmd> [args] # --chat adds '@botusername' for you
tlgr bot press <chat> <msg_id> --button N # every button kind, one dispatcher
→ {"kind": "callback", "n": 0, "message": "Saved", "alert": false}

tlgr inline query <bot> <query> # @bot query, paginated by the bot
tlgr inline send <bot> <query> --chat <c> --pick 0
tlgr webapp open <bot> --main # prints the signed URL; never opens it
tlgr payment form get --slug <slug> # read a checkout form; never pays
```

Four rules matter before scripting against these:

- **`message get --json` prints the keyboard, and `n` is the address.** Each
button carries a flat row-major `n`; `bot press --button <n>` takes exactly
that number. `--button "<text>"` and `--button "<row>,<col>"` also work, and
`--data` addresses a callback button by its payload.
- **A button that discloses something is not pressed without saying so.**
`request_phone`, `request_geo`, `request_poll` and `request_peer` need
`--share-phone`, `--share-geo`, `--poll` and `--peers`; without one tlgr
prints what it *would* send and exits 2. A Pay button exits 6, always.
- **tlgr never spends money.** Reading a form, a receipt or a subscription is
implemented; creating an invoice (asking somebody else to pay) is
implemented; paying is absent from the surface. `payment form get` reports
`"payable_here": false` with the reason in `reason`.
- **A mini-app URL is a credential.** `webapp open` prints the signed URL and
stops — it carries your signed init data. A session that comes back with a
`query_id` dies in about a minute unless `webapp watch` keeps it alive
(`needs_prolong` says which).

The bot-only half of the surface — `bot answer`, `bot command set`,
`bot menu set`, `bot api send`, `inline edit`, `payment invoice export` —
needs an account added from a bot token and exits **4** on a user session.
Five commands (`bot ephemeral send|delete`, `bot welcome list|set|delete`) are
registered and exit **13** with `NOT_SUPPORTED`: their methods need API layer
229, which this build does not speak. That is deliberately different from "no
such command".

### Agent Helpers

```
Expand Down
34 changes: 33 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,45 @@ peers included: `story hide` is now the single implementation of the toggle
and `user hide-stories` is a legacy path on it, so the two spellings cannot
drift apart.

`bot`, `inline`, `webapp` and `payment` follow — 79 operations where v1 had
none. The whole bot surface as a person uses it (profile cards, `/start` with a
hidden deep-link payload, slash commands, every kind of button, Telegram
Login), the whole surface a bot uses (answering queries, publishing commands,
the menu button, inline results, invoices), mini apps, and payments read
end to end. Nothing is deleted, because there was no v1 bot code to delete.

Three deliberate absences in that group are worth naming here.

* **tlgr never spends money.** `payments.sendPaymentForm`, `sendStarsForm`,
`validateRequestedInfo` and `fulfillStarsSubscription` are not behind a flag
or an environment variable — they are not on the surface at all.
`payment form get` returns `payable_here: false` with the reason in the
payload, and a test asserts the property against the registry rather than
against a list of commands.
* **`webapp open` prints the mini-app URL and stops.** There is no `--open`:
the URL carries the user's signed init data and is a credential.
* **A button that discloses something is not pressed without its flag.**
`--share-phone`, `--share-geo`, `--poll`, `--peers`; without one, `bot press`
prints what it would have sent and exits 2. A Pay button exits 6.

Five commands (`bot ephemeral send|delete`, `bot welcome list|set|delete`) are
registered and exit 13 `NOT_SUPPORTED`: they need API layer 229, which the
pinned Telethon does not speak. They exist so that "unavailable in this build"
is a different answer from "no such command".

One bug fix rides along: `message get --json` now actually prints
`reply_markup`. PR-1 declared the shape and nothing ever populated it, which
made its two keyboard-rendering P0 ids true only on paper — a caller could see
no button, so a caller could press none.

### Breaking

Every change below applies **only to commands generated from the operation
registry** — in this release that is the `message`, `draft`, `chat`,
`folder`, `auth`, `account`, `passport`, `media`, `sticker`, `gif`, `emoji`,
`story`, `events`, `watch`, `daemon`, `sync`, `net`, `proxy`, `config`, `job`,
`webhook`, `export`, `contact`, `user` and `resolve` groups,
`webhook`, `export`, `contact`, `user`, `resolve`, `bot`, `inline`,
`webapp` and `payment` groups,
`tlgr completion`, `tlgr status`, `tlgr schema` and the `agent` group. Commands still
hand-written under `tlgr/cli/legacy/` behave exactly as they did in v1 until
their own migration PR, at which point these rules apply to them too.
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,38 @@ complete; **joining**, **removing somebody** and **sending inside** need a
signed `e2e.chain` block that tlgr cannot build — pass one with `--block` and
`--public-key`, or the command exits 2 naming what is missing.

### Bots, inline mode, mini apps and payments

```bash
tlgr bot get <bot> # the profile card: commands, menu, flags
tlgr bot list --owned|--similar-to|--popular-apps|--recent
tlgr bot start <bot> --param <payload> # /start with a hidden deep-link payload
tlgr bot command send <bot> start # '@botusername' added in a group
tlgr bot press <chat> <msg_id> --button 0
tlgr bot url-auth get|accept|decline # Telegram Login, inspected before granted
tlgr bot menu|permission|access|preview|affiliate|verification|token …

tlgr inline query <bot> <query> # @bot query, the bot's own paging
tlgr inline send <bot> <q> --chat <c> --pick 0
tlgr inline search gif|venue|image # the built-in bots, named by the server

tlgr webapp get|open|send|invoke|watch # mini apps; `open` prints the URL only
tlgr payment form get|receipt get|info get|card get
tlgr payment invoice export|send # asking someone else to pay
tlgr payment subscription list|set # cancel or resume a Star subscription
```

Three things this group does *not* do, on purpose:

- **It never pays.** `sendPaymentForm`, `sendStarsForm`, `validateRequestedInfo`
and `fulfillStarsSubscription` are absent from the surface, not hidden behind
a flag. `payment form get` reports `payable_here: false` and says why.
- **It never opens a browser.** `webapp open` prints the signed mini-app URL,
which carries your init data and is a credential, not a link.
- **It never presses a button that discloses something without being told to.**
A phone number, a location, a chat or a poll each needs its own flag;
without one tlgr prints what it would send and exits 2.

### Profile

```bash
Expand Down
87 changes: 87 additions & 0 deletions docs/design/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1235,3 +1235,90 @@ it. The close-friends list and the live story's call — comments, RTMP key,
send-as identity — were on that list too until PR-5 and PR-11 landed ahead of
this one and covered them outright. `stories` is 94.2 % covered and 100 %
accounted.

## 2026-09-04 — an operation that only refuses claims no catalog coverage

Five layer-229 commands (`bot ephemeral send|delete`, `bot welcome
list|set|delete`) are registered and exit 13. They are registered because
"unavailable in this build" and "no such command" are different answers and an
agent has to be able to tell them apart. They claim **nothing** in the parity
catalog, because a `covers_partial` on an operation that cannot run would
raise the coverage number while doing no work. Registry lint L13 gained a
second exemption tag, `not-supported`, so the two facts can both be true: the
op exists, and it counts for nothing.

The nine catalog ids they would have claimed are waived to PR-12 with the layer
named in the reason. The same rule removed `bots.ephemeral-callback-press` and
`bots.rich-message-buttons` from `bot press`, and `bots.chat-join-webview` from
`webapp open`: each is one flag on an otherwise working command, and the flag
exits 13.

## 2026-09-04 — a confirmation an *operation* must see is spelled as its own flag

`--yes/-y` is a CLI-level gate: `cli/gen.py` reads it out of the Click context
and uses it for the `destructive` confirmation. It never reaches the daemon, so
an implementation cannot ask whether the user typed it. Three work-list items
asked for exactly that, and each is spelled differently:

* **`bot press` consent.** The consent flag *is* the consent: `--share-phone`,
`--share-geo`, `--poll`, `--peers`. Requiring `--yes` on top would be a
second word for the same decision, and pressing an ordinary button would
start prompting.
* **`bot score set --yes` ("allow the score to decrease")** is Telegram's
`force`, and is now `--allow-lower` — which says what it does.
* **`webapp download --yes` ("actually download")** is `--fetch`. Checking is
the default and the fetch is a separate verb, not a confirmation of the check.
* **`inline send --paid-stars N`** names the amount, and naming it is the
consent. There is no flag that agrees to an unspecified charge.

Where `--yes` genuinely fits — the whole operation is destructive — the spec
says `destructive=True` and the existing gate does the work: `bot url-auth
accept`, `bot token export`, `bot stop`, `bot affiliate revoke|unset`,
`bot attach toggle`, `bot preview delete`, `bot verification set`,
`payment info get|delete`, `payment subscription set`.

## 2026-09-04 — `bot id` is a sub-noun, so the alias cannot also be a command

The work list gives `bot.id.get` the alias `bot id`. It cannot have one: the
canonical path already puts a *group* at `bot id`, and placing a hidden command
there replaces the group, which takes `bot id get` with it. The alias is
dropped. The same collision decided `pay saved-info`: `payment info get` and
`payment info delete` are aliased `pay saved-info get` and `pay saved-info
clear`, so both live under one group instead of one of them replacing it.

## 2026-09-04 — `bot connection invoke` is registered and refuses

Wrapping an arbitrary tlgr command in `invokeWithBusinessConnection` would mean
re-entering the daemon's dispatcher from inside an operation, and `ops/` may
not import `daemon/` (§2.2) — the import lint checks the AST, so a
function-level import does not help either. The wrapper itself *is*
implemented: it is `--business-connection` on `bot command send`, `bot press`
and `inline send`, where it is built, sent to the connection's own DC through
an exported sender, and tested. The command exists so that the gap has a name
and a pointer; it exits 13.

## 2026-09-04 — the reply-markup schema is one vocabulary, read and write

`models/message.ReplyMarkup` was declared by PR-1 and never populated, which
made `bots.inline-keyboard-render` and `bots.reply-keyboard-render` — both P0 —
true only on paper. `message_to_model` now fills it, and `models/bot.Keyboard`
is the write side of the *same* button-type vocabulary, so a keyboard read out
of `message get --json` can be pressed with `bot press --button <n>` and sent
back through `--keyboard`. Two schemas for one object is how a button that can
be read stops being a button that can be pressed.

Each button carries `n`, the flat row-major index, with a default of `-1`:
`omit_defaults` would otherwise drop the index of the first button and only the
first button. The same reasoning moved every decision-carrying boolean —
`payable_here`, `available`, `restricted`, `allowed`, `cancelled` — to a
tri-state, so that "false" is emitted rather than inferred from an absence.

## 2026-09-04 — `bot game get` reports what the method actually returns

The work list describes `messages.getEmojiGameInfo` as returning stakes and
payouts. It takes no arguments and returns `emojiGameDiceInfo(game_hash,
prev_stake, current_streak, params, plays_left)` or `emojiGameUnavailable`.
tlgr reports those fields under those names rather than inventing
`stakes`/`payouts` over an opaque `params` vector, and keeps `--emoji` as a
label echoed back on the answer so a caller can tell which game they asked
about. Staking TON on one is a financial action and is not implemented.
Loading
Loading