settings: profile, privacy, notifications, business, premium, gifts, stars — and the v1 surface retired (2.0.0) - #15
Merged
Merged
Conversation
…um, stars and gift shapes Eight files for the last group, and four of them exist because the server shape and the human shape genuinely differ. A username is a row, not a string: a Fragment collectible can be active or parked and only the first active one is the handle a link resolves to, so flattening the vector is how a script deactivates the wrong name. A privacy key keeps `raw_rules` alongside the four exception lists, because `account.setPrivacy` replaces the whole ordered vector and a rule tlgr does not recognise still has to survive the round trip. `mute_until` carries its unix twin so the model itself says the timestamp is absolute — v1 computed it from the asyncio loop clock and muted chats into 1970. And a gift is addressed by a `ref` string everywhere, so a reference read from a listing can be handed straight to convert, transfer or set without a lookup table. `BotRights` enumerates every flag of `businessBotRights` rather than carrying a mask: connecting a business bot hands another program the right to read, reply, edit the profile and move Stars, and a right that is absent from the model is a right nobody can audit.
The Edit Profile screen, whole: names and bio, usernames including the Fragment collectibles, the avatar history, the accent palettes, the emoji status, the presence switch, the personal channel, the saved music, the wallpaper and the public link. Three of these are corrections rather than features. `profile get` now fetches `users.getFullUser`, so `bio` is the bio instead of the `""` v1 hard-coded for every account. `profile photo set` uploads the file and sends raw `photos.uploadProfilePhoto`, because v1 called `client.upload_profile_photo()`, which Telethon 1.44 does not have — the command could never have worked. And `profile update` is one command over four RPCs, because the GUI shows one screen and `updateProfile` / `updateBirthday` / `updatePersonalChannel` is the server's decomposition, not a vocabulary an agent should have to learn. `profile presence set` exists because a daemon needs a presence *policy*: always reporting online advertises a machine, and reading history while reporting offline is the classic bot tell, so tlgr reports neither unless asked. The last v1 command module goes with it — `tlgr/cli/legacy/profile.py` is deleted and both its paths stay invocable through `legacy_paths`.
`account.setPrivacy` replaces the whole ordered rule vector and `account.setGlobalPrivacySettings` replaces the whole constructor, so both commands read first and write back complete. That is what `--add-allow`, `--add-disallow` and `--remove` are for: a script that wants to add one exception should never have to re-state a list it did not mean to touch, and a global flag nobody passed is written back exactly as it was found. `notify get`/`notify set` take a target — a scope, a chat, a topic, `reactions` or `contact-joined` — and pick between three unrelated server APIs, because the official clients show them as one Notifications screen. `mute_until` 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. `contact-joined` is reported the way a human reads it, even though the wire stores the opposite (`silent=true` means off). `privacy set stories` is refused with a sentence naming the two commands that do own story visibility, rather than accepted and silently ignored: there is no `inputPrivacyKeyStories`, and pretending otherwise is worse than saying so.
`settings get`/`settings set` address fifteen cloud-synced keys by name. A dozen thin toggle commands would be a dozen names to learn, a dozen response shapes and a dozen places to get the same read-modify-write wrong; here every row carries `accepts`, the exact token vocabulary its setter takes, so a read can be piped straight back into a write. Where another group already implements a setting — sensitive media, auto-download presets, the quick reaction, paid-reaction privacy, saved tags, top peers, folder tags — the dispatcher calls that operation instead of sending the RPC a second time. One implementation, two entry points, and no second place for the two to disagree. `settings theme *` is deliberately metadata only: a theme is a rendering instruction and tlgr renders nothing, but publishing a theme file, installing one and listing what is installed are server-side facts the phone sharing the account will act on.
Telegram Business, whole: opening hours, location and chat intro, the greeting and away messages, the quick replies they send, the public chat links, and the chatbot that may act on the account's behalf. `business bot set` grants another program the right to read my messages, reply as me, rewrite my name, bio and photo, manage my gifts and transfer my Stars. So every right is opt-in by name — there is deliberately no `--all` — the operation is destructive, and the reply enumerates exactly what was granted, because an audit should not have to trust the flags somebody typed. Opening hours are minutes-of-week arithmetic and the easy thing to get wrong. `--open 'mon-fri 09:00-18:00'` is expanded, sorted and merged before it is sent, because the server rejects overlapping intervals and writing two lines for the same day is the normal way a human produces them; a range that ends before it starts is a usage error rather than a silent wrap, since "22:00-02:00" almost always means the next day and guessing is worse than asking. `business stars transfer` prices the transfer and refuses to make it. PR-10 settled that tlgr never signs a payment form; this group inherits the policy rather than opening a second door onto the same money.
…held The limit table is what a script actually needs out of Premium: caption length, upload size, folder count, pinned chats and public usernames all change with the subscription, and a script that guesses them writes a message the server then refuses. It has no MTProto method of its own — it is assembled from appConfig — so every row says where it came from. The Stars group reads and nothing else. A Stars amount keeps its `nanos` because a rounded ledger cannot be reconciled, and the transactions cursor is the server's opaque `next_offset` string rather than an integer: passing a number where the API wants a token silently restarts the walk, which is how an export ends up with its first page repeated. Giveaways get a noun of their own because almost all of the surface is free: joining spends a boost slot I already own, redeeming a code activates a subscription somebody else paid for, and launching a *prepaid* giveaway spends nothing at all. Buying a new one is a purchase and is absent. `premium gift send`, `stars subscription refulfill` and `stars url get` are where the policy shows. PR-10 named `sendStarsForm`, `sendPaymentForm`, `validateRequestedInfo` and `fulfillStarsSubscription` as deliberately absent from tlgr's surface; PR-12 does not re-add any of them behind a flag. Each of these three reports the price, or the URL, or whether the server would allow it — and stops there.
…them A gift is addressed by a `ref`: `msg:<id>` for one received in a private chat, `<peer>:<saved_id>` for one a channel holds, or a bare collectible slug (a t.me/nft/ link is reduced to one). One spelling means a reference read out of `gift list` goes straight into `gift set`, `gift convert`, `gift transfer` or `gift upgrade` with no lookup table in between. Every time gate the server publishes is surfaced instead of collapsed. "Can I transfer this?" has three answers — yes, not yet and here is when, never — and a client that reports only the first two sends its user to wait for a date that will not come. The dividing line is cost, not danger. Displaying, pinning, wearing, converting back into Stars, a free transfer, a prepaid upgrade, listing a collectible for sale, declining an offer and crafting are all performed; anything that would need a payment form signed is priced and refused with the reason attached. `gift craft` is the exception that proves it: free, and still gated behind `--yes`, because it burns every input gift whatever the outcome. Three flags name a method Telethon 1.44 has no request class for (`canSendStarGift`, `getStarGiftCraftCandidates`, `getStarGiftAttributes`) and refuse with exit 13 saying which; the rest of each command still works.
238 tests over a fake Telegram that holds state rather than answering canned replies, because this group's whole job is round trips: a privacy vector that is rewritten and read back, a notification exception that a later listing finds, a gift that leaves one profile and arrives on another, a code that can only be redeemed once. Four of them are the ones worth having. `mute_until` is asserted against the wall clock, which is the bug v1 shipped. `--add-allow` is asserted to keep what was already there, because `setPrivacy` replaces the whole vector. One test walks the registry's *source* and fails if anything in this group names `sendStarsForm`, `sendPaymentForm`, `validateRequestedInfo` or `fulfillStarsSubscription` — so a future addition cannot quietly re-open the door PR-10 closed. And every flag that needs a request class Telethon 1.44 lacks is asserted to exit 13 with the method named, not 1. Two things had to change to make the answers honest. Fields that carry the answer — `ok`, `state`, `kind`, `upgraded`, `transferred`, `version`, `resolvable_by_strangers`, the privacy `base` — lost their defaults, because `omit_defaults` was dropping exactly the value a caller most needs to see; absent must mean "not applicable", never "the interesting case". And ten server error names this surface can produce (`THEME_INVALID`, `GIFT_SLUG_INVALID`, `CHATLINKS_TOO_MUCH`, `USERNAME_PURCHASE_AVAILABLE`, …) joined the error table, so they exit 5 or 2 with a reason instead of 1. `profile` was the last hand-written group, so the sandbox's two-exit-code rule collapses to one: every refusal is PERMISSION_DENIED now.
…nd here is why" The waiver file was a backlog. Until this PR a waiver was a promise with a PR number on it — "waived until PR-9" — which is exactly right while there are still PRs to come, and meaningless once there are not. Every one of those 177 promises has been kept, so the file now holds nine ids and each one names a permanent reason: seven layer-229 methods Telethon 1.44 has no constructors for, and two methods the layer has that the pinned library does not ship a request class for. `kind` is the new required field — `layer-gap`, `absent-method`, `prohibited` or `not-applicable` — and the gate enforces the whole shape rather than trusting the file: no domain may be waived, no unknown `kind` may appear, a layer or method waiver must name its method, and an id that is *covered* may not also be waived, which is the one way a number could lie about itself. P0 is 178 of 178. That is ARCHITECTURE §1.3's condition for 2.0.0 final, and it now has a test of its own rather than being a floor that could be raised one id at a time forever. Total coverage is 1788 of 1797.
…any more
Five modules go, and with them the last places where a command could reach
Telegram without passing through the registry:
* `tlgr/cli/legacy/` — the v1 command package. `profile get` and `profile
update` were the last two commands in it, and they are registry operations
now. Every path v1 documented is still invocable, as `legacy_paths` aliases
on the ops that replaced them; `tests/test_agentmd_compat.py` walks the
whole list and proves it.
* `tlgr/daemon/ipc.py` — the v1 route table. `POST /v1/op`, `GET /v1/events`,
`GET /v1/status` and `POST /v1/admin/{action}` are the entire HTTP surface
now, which means the peer-uid check, the policy allowlist, the version
handshake, the flood budget and the §7.2 error classification apply to
every command without exception rather than by being registered alongside.
* `tlgr/ipc_client.py` and `transport.legacy_request` — the shim those routes
were called through, and the transport-level `flood_wait_max` default that
existed only because the hand-written commands did not thread it into their
own request bodies (COR-15). Every command threads it now.
* `tlgr/core/client.py` — `ClientWrapper`. 460 lines that owned a Telethon
client, logged accounts in and out, and serialised messages, held alive
because the job engine needed *two* of its methods. Those two are now
`jobs/client.py`'s `JobClient` protocol — the raw client and a resolver —
and `AccountSession.job_client` supplies them, so a reconnect swaps the
client underneath a running job instead of leaving it holding a dead one.
`Daemon.status()` goes too: it was v1's `/daemon/status` body, and nothing
has served it over HTTP since PR-4. The COR-37 claim it carried — that a
client object existing and the link being usable are different facts — is
made where it now lives, on `AccountSession.connected` and in the per-account
`state` that `daemon status` answers from.
Three test modules exercised only `ClientWrapper.get_messages`; that surface
is `message list` and `tests/test_ops_message.py` owns it. `media_details`
was the classifier those tests were really about, so its table moved onto
`media_summary` rather than being deleted with it. The layering lint loses
its one exemption, because there is nothing left to exempt.
The Unreleased section becomes 2.0.0, dated 2026-09-04, with the breaking-change table intact — twenty-eight rows across the groups, and the two new ones are the only two commands v1 had in the settings surface: `profile get` now reports the real bio, and `profile update` reports only the fields it wrote. The Breaking section loses its preamble listing which groups were migrated, because the answer is all of them. The Removed section gains the v1 surface itself. `README.md` and `AGENT.md` describe v2 only: no "commands still under `tlgr/cli/legacy/` print v1's bare object", no "generated commands wrap their answer" — every command does, because every command is generated. `PARITY.md` stops saying "waived until PR-N". Its gap section is now "What this build cannot do", and each row names the MTProto method the pinned Telethon has no request class for, which is the only kind of gap left.
Written for the case the release was actually built against: a pipx tlgr with a live v1 daemon holding four accounts, and an agent parsing its JSON. The order matters and the document says why. Stop the daemon *and* whatever restarts it before upgrading, because two processes on one `session.session` earns `database is locked` and then `AUTH_KEY_DUPLICATED`, which Telegram answers by revoking the authorization. The verification is two commands, not one: no `tlgr.daemon` process, and no open handle on the session files — a stale socket is harmless, an open handle is not. Then the production marker, which is a step people will otherwise discover as a refusal: remove it, or set `TLGR_ALLOW_PRODUCTION_HOME=1` in the deployed tlgr's own environment and nowhere else, since setting it in an interactive shell is the same as not having the guard. The output section lists six shapes and the one flag that sidesteps all of them (`--results-only`), plus two answers that changed because v1's were wrong — the bio it never fetched, and the mute it computed from the event loop's clock. Rollback is reinstall-and-restart, with the two things that do not roll back on their own named.
The ones a reviewer would otherwise have to reconstruct from the diff: why three commands that the work list designs as sends are control-only (PR-10 named those four methods absent, and a property beats a default); why seven aliases from the work list are dropped (each is the group its own operation lives under, and placing a command there deletes the group); why `update` joined the verb vocabulary; why the fields that carry an answer lost their defaults (`omit_defaults` was hiding refusals); why `privacy set stories` refuses with two pointers instead of guessing; why an absent request class is a different gap from an absent layer; why the waiver file stopped being a backlog; and what `ClientWrapper` became.
Both criteria that were partly met when the foundation landed are met now, and neither was met by moving a goalpost. §1 wanted 80 % coverage and got 77 %; it is 82 % over 13 292 tests. What closed it was behavioural tests for the P2/P3 tails, group by group. Three pre-foundation test modules are gone — the only three — because they drove `ClientWrapper.get_messages`, which PR-12 deleted; the claim they were really making moved onto `media_summary` rather than going with them. §17 wanted `messages_core` at 95 % and got 79.6 %, with 34 ids waived to later PRs because they were catalogued under messages while their command home was another noun. Every one of those PRs landed: the domain is 167 of 167. The whole catalog is 1788 of 1797 and all 178 P0, which is §1.3's condition for 2.0.0 final. §8/§9 keep their "simulated" caveat and gain a note about where the claim now lives: `Daemon.status()` went with `ClientWrapper`, and the distinction it drew — an object existing versus a link being usable — is `AccountSession. connected` and the per-account state.
The rule was written while the migration was in flight, so it said a domain not yet reached is waived with the PR that closes it. Every one of those promises is kept; what the file holds now is ids this build cannot cover, each naming its reason and the missing method, and the gate refuses a domain waiver outright.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR-12, the last of the v2 plan: the remaining settings groups on the operation registry —
profile,privacy,notify,settings,business,premium,stars,gift,giveaway(~90 operations) — and the retirement of the v1 surface. After this PR no hand-written command, no v1 IPC route and noClientWrapperremain; the daemon serves/v1/*only; every v1 command path is still invocable throughlegacy_paths(superset test).get/setpair over every privacy key with allow/disallow rules and exceptions, plus global privacy. Notifications: per-scope defaults, exceptions, ringtones, reaction/story notifications. Settings: a generic get/set over the cloud settings (sensitive content, default TTL, language/translation, themes, web-browser settings, suggestions).docs/UPGRADING.mddocuments the v1 → v2 cutover for an installed pipx tlgr with a running v1 daemon, acceptance criteria re-run.Numbers
15 commits, 13,292 tests (238 new), ruff + mypy clean, coverage 82% (gate 80%). Parity: profile_settings_privacy 178/178 → P0 178/178 (100%), 1,788 catalog ids covered, 9 remaining waivers each naming the MTProto method Telethon 1.44 lacks; the gate refuses domain waivers, unknown kinds and waivers on covered ids. Three v1 bugs fixed on the way:
profile getreturned an empty bio,profile update --photocalled a Telethon method that does not exist, and a mute computed from the event-loop clock. Money policy inherited from PR-10: paid actions report the price and stop, enforced by a test walking the registry. (Numbers before the rebase onto main, which since gained only PR #14.)