Skip to content

Releases

v2.0.1

Choose a tag to compare

@github-actions github-actions released this 10 Sep 21:25
9e68adf

Changed

  • The distribution is published as tlgr-cli. PyPI refuses the name
    tlgr as too similar to an existing tlgrm, so the name on the index is
    tlgr-cli. Nothing else moves: the import package is still tlgr, the
    command is still tlgr, and every module path is unchanged. Only the
    install line differs.

    pipx install tlgr-cli      # was: pipx install git+https://github.com/tlgrcli/tlgr.git

    If you already run tlgr from a git or editable install, you do not need to
    do anything. Do not pipx install tlgr-cli alongside an existing tlgr
    pipx install: both provide the same tlgr command and the second one will
    collide with the first. pipx uninstall tlgr first, or keep the install
    you have.

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 17:07
80ed4b5

Every command tlgr has is generated from the operation registry. There is
no hand-written command left, no v1 route left and no ClientWrapper left;
the daemon serves /v1/* and nothing else. Feature parity against the
official clients is 1788 of 1797 catalogued behaviours (99.5 %), and all 178
P0 behaviours
— the ones ARCHITECTURE §1.3 required before this release.
The nine that remain are individually waived and each names the MTProto method
this build has no request class for.

The foundation of v2: operations are defined once, as an OperationSpec, and
the command, its JSON Schema, its docs and its contract tests are generated
from that one definition. See docs/design/ARCHITECTURE.md.

The whole message group and draft are migrated — 43 operations, generated
from one registry — which is what makes the model worth trusting: it is proven
on the busiest group, not on a toy one. tlgr/cli/message.py and
tlgr/cli/draft.py are deleted rather than shadowed.

The chat and folder groups follow: 47 more operations covering the dialog
list, the per-chat settings and the chat folders.

Then the groups-and-channels group: 86 more operations covering members,
admins, invites, join requests, topics, the Manage screen, the admin log,
statistics, revenue and boosts. tlgr/cli/legacy/chat.py is deleted along
with the /chat/create and /chat/members IPC routes and
ClientWrapper.create_chat / list_participants; both v1 paths keep working
through the registry. Seven commands — chat community create|list|set|ban
and chat welcome list|set|delete — are registered and refuse with
NOT_SUPPORTED (exit 13) because they need MTProto layer 229 and Telethon
1.44 speaks 227.

auth, account and passport follow: 51 more operations covering logging
in, the Devices list, 2-step verification, connected websites, passkeys and
Telegram Passport. tlgr/cli/legacy/account.py, completion.py and agent.py
are deleted, not shadowed.

media, sticker, gif and emoji follow — 56 operations where v1 had two
(media download and media upload, one Telethon call each). The two v1
paths and their dl/up shortcuts still work; tlgr/cli/legacy/media.py,
the /media/* IPC routes and the two ClientWrapper methods behind them are
deleted rather than shadowed.

The update transport follows: events, watch, daemon, sync, net,
proxy, config, job, webhook, export and agent — 66 more
operations, 114 event types, and the updates_sync_network domain fully
accounted for.

Then contact, user and resolve: 38 operations covering the address book,
one person's profile, both blocklists, the phonebook and the reference
resolver every other group already leans on. tlgr/cli/legacy/contact.py and
tlgr/cli/legacy/user.py are deleted, along with their eight IPC routes and
the eight ClientWrapper methods behind them. The two semantics AGENT.md
freezes are unchanged: user dialog-status is still three-valued and still
exits 13 for "could not establish", and user hide-stories still reports
already and sends nothing when there is nothing to do.

story follows — 31 operations covering posting, the feed, viewers, albums,
the story blocklist, stealth mode and live stories, where v1 had exactly one
command (user hide-stories). That path still works, --unhide and bulk
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".

user dialog-status gained the peer's read state — read_outbox_max_id (the
highest message of ours they have read), unread_count and top_message —
on every return path, null when nothing could be established and 0 on the
definitive negative. The server's dialog object always carried these and the op
threw them away, so the command could not answer "have they read our message?"
and answered it wrongly by omission: the key was simply absent, so a caller
reading it back got null, which is indistinguishable from "not read". chat list has always reported the same field, so the two routes disagreed about the
same peer. No derived they_read_it boolean, on purpose: the comparison only
means "they saw our last message" when the last message is in fact ours, which
only the caller knows. The three keys are additions — nothing moved or was
renamed, and the three-valued contract and exit 13 are untouched.

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.

Last, the settings surface: profile, privacy, notify, settings,
business, premium, stars, gift and giveaway — 90 operations covering
Settings ▸ Edit Profile, Privacy and Security, Notifications and Sounds,
Telegram Business, Premium and boosts, Stars and the gift and giveaway
screens, where v1 had two commands (profile get and profile update). Both
still work.

Three of those two commands' behaviours were wrong and are now right.
profile get fetches users.getFullUser, so bio is the bio rather than the
"" v1 reported for every account. profile photo set uploads the file and
sends raw photos.uploadProfilePhoto; v1 called
client.upload_profile_photo(), which Telethon 1.44 does not have, so the
command could never have worked. And a mute is computed from the wall clock:
v1 used the asyncio event loop's clock, whose origin is arbitrary, so "mute
for an hour" produced a timestamp in 1970 and muted nothing.

Two of Telegram's APIs in this group replace their whole payload —
account.setPrivacy replaces the ordered rule vector, and
account.setGlobalPrivacySettings replaces the constructor — so both commands
read first and write back complete. privacy set --add-allow and --remove
exist precisely so a script never has to re-state a list it did not mean to
touch.

The no-spend policy PR-10 set for the payment group holds across this one:
premium gift send, business stars transfer, stars subscription refulfill
and the paid halves of gift upgrade, gift transfer and gift offer approve report the price and stop. A test walks the registry's source and
fails if anything in the group names sendStarsForm, sendPaymentForm,
validateRequestedInfo or fulfillStarsSubscription.

Breaking

Every change below applies to every command: they are all generated from
the operation registry now. Earlier releases in this cycle carried a list of
which groups were migrated and a note that the hand-written ones behaved as
they did in v1 until their own PR; there are no hand-written ones left, so the
list is gone and the rules are universal.

No documented command path disappears. Every migrated operation declares its
v1 paths, so tlgr send, tlgr msg list, tlgr message react and the rest
still work; tests/test_agentmd_compat.py asserts it, and asserts that every
JSON key v1's AGENT.md documented is still there — except for the
thirteen changes in the table below, which is the whole list.

# Change v1 v2 Migration
1 Timestamps "2025-03-06 12:00:00+00:00" "2026-09-02T09:14:07Z" + a *_unix sibling RFC-3339 parses everywhere str(datetime) did not; [defaults] legacy_dates = true restores the old spelling for one minor release
2 draft.list (and chat get) ids raw entity id (123) marked id (-100…123), with raw_id beside it this was COR-10, a bug: the raw id was ambiguous between a user and a channel. raw_id carries the old value
3 Default parse_mode md, which silently ate _, * and backticks in ordinary text none [defaults] parse_mode = "md" restores it; --parse md is explicit (COR-21)
4 --results-only on a scalar result printed a bare 2 for message delete prints the result object this was COR-18; --select deleted covers the scalar case
5 Error envelope {"error","code","exit_code"} on stdout {"ok":false,"error":{…}} with the same three keys inside error --results-only emits the inner error object, byte-for-byte v1's shape
6 List envelopes: message.list, message.search, message.forward, draft.list {"messages":[…],"has_more":…}, `{"forwarded":2,"ids...
Read more

v1.0.0

Choose a tag to compare

@erfnzdeh erfnzdeh released this 06 Mar 12:21
Update project documentation to reflect name changes

- Renamed contributing guidelines and README sections to align with the updated project name, simplifying the title to "Telegram CLI" for clarity.
- Ensured consistency in project documentation following recent structural updates.