Skip to content

contacts: contacts, users, blocking and the resolver on the registry - #10

Merged
erfnzdeh merged 8 commits into
mainfrom
feat/pr5-contacts
Sep 3, 2026
Merged

contacts: contacts, users, blocking and the resolver on the registry#10
erfnzdeh merged 8 commits into
mainfrom
feat/pr5-contacts

Conversation

@erfnzdeh

@erfnzdeh erfnzdeh commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR-5 of the v2 plan: contacts, users, blocking and the link resolver on the operation registry — 38 operations under contact, user, resolve and link — and the legacy contact/user modules, their IPC routes and ClientWrapper methods deleted.

  • Contacts: list/add/rename/remove/search, import (CSV/vCard) and export, sync, birthdays, blocked list (messages and stories), top peers and rating reset.
  • Users: full profile (bio, common chats, photos, personal channel, business info, status/last seen with privacy caveats), block/unblock, can-message, dialog-status and hide-stories exactly as AGENT.md documents them. Indeterminate answers now travel as meta.indeterminate on the envelope so the CLI exits 13 instead of reading an HTTP 200 as success.
  • Resolver: resolve link <url> normalises every t.me / tg:// link kind (user, chat, message, invite, folder, story, bot start, proxy, boost, gift, stars, wallpaper, theme, phone) into a typed object; username, phone and id resolution follow the cache → server → dialog-scan order from the MTProto notes.

Numbers

6 commits, 3,440 tests, ruff + mypy clean. Parity: contacts_users P0 20/20; 99/121 required ids covered and 100% accounted (privacy keys → PR-12, nearby → PR-9, live presence → PR-4). Ten decisions in DECISIONS.md, notably omit_defaults=False on contact/user models so already: false and has_dialog: null are always emitted.

The three shapes that carry provenance rather than a verdict: UserStatus
keeps by_me so a coarse last-seen bucket is never reported as the peer
hiding; ContactAdded keeps both imported and retry so an empty import is
not read as 'no such number'; DialogStatus stays three-valued, with
has_dialog null whenever resolved is false.
…p deleted

The address book, one person's profile, blocking, and the resolver every
other group already leans on. Three semantics carry over verbatim because
AGENT.md freezes them: dialog-status stays three-valued, hide-stories stays
idempotent and silent, and contact rename still writes only our own view of
a name (empty first name still becomes '.').

Two shapes are new. A result can now be marked indeterminate, so
dialog-status returns its body AND exits 13 — an error envelope would have
thrown away the reason a caller needs. And 'resolve link' classifies without
acting: it names the command that would act in delegated_to.

Deletes tlgr/cli/legacy/{contact,user}.py, the eight contact/user IPC routes
and the eight ClientWrapper methods behind them; every v1 path stays
invocable through legacy_paths.
The fake grows a contact world — the list, the two blocklists, top peers,
birthdays, the phonebook, saved music, profile photos — so a test asserts
that adding a contact really flipped user.contact and that a second
hide-stories pass sent nothing at all.

The two frozen contracts get the coverage they earned: dialog-status is
checked in all three outcomes including the capped scan that must exit 13,
and the v1 paths (contact list/add/rename/remove/search, user get/
dialog-status/hide-stories, and the bare 'contacts') are asserted to still
resolve and still be invocable.

Replaces tests/test_dialog_status.py and tests/test_stories_hidden.py, whose
ClientWrapper methods this PR deleted.
… owners

The domain-wide waiver is gone. The 22 required ids still uncovered in it
each name the group that does own them — privacy keys to PR-12, People
Nearby to the location group, live presence to the event bus — which is
what makes 'the contact group is done' checkable rather than asserted.

P0 floor 53 -> 73, covered floor 350 -> 472, with the 20 new P0 ids named
rather than counted so a swap cannot pass silently.
AGENT.md gains the whole group with the two ambiguities spelled out where an
agent will read them: an empty contact import is not 'no such user', and
by_me on a coarse last-seen bucket is our own privacy, not theirs. The
resolve section is new and says plainly that classification never acts.

CHANGELOG rows 10-12 cover the three shape changes (contact list/search
became Page envelopes, contact add and user get gained keys, --field is gone
in favour of the global --select), and DECISIONS records the ten calls this
PR had to make.
Each classified kind gets its follow-up read exercised against a real reply
shape — an invite preview, a sticker set, a boost status, a gift code, a
private post reached through the peer cache — and the assertion is that no
acting request was sent alongside it.

Also covers the three paths that were assumed rather than checked:
--with-stories against real read marks, --translate-bio, and a min user
addressed through inputUserFromMessage, which Telethon builds for nobody.
Three assertions were written against a tree where `media`, the sticker sets
and the wallpapers were still v1's. On `main` they are not:

- `test_sandbox` picked `media download` as its example of a still-hand-written
  group. PR-6 generated it, and `profile` is now the only one left.
- the fake grew a stub `messages.getStickerSet` / `account.getWallPaper` /
  `stories.getStoriesByID` beside PR-6's world-backed handlers. Python keeps
  the last definition, so the stubs were dead and the reads came back empty.
  The stubs are deleted and the address book seeds the two worlds instead,
  which is the stronger test: `resolve link --open` now reads the same replies
  the media group's own ops read.
Each of the seven was written when `location`, `search`, `events` and
`account` were still ahead of this branch; all four have landed, and the
group each waiver names is the group that now covers the id. A waiver on a
covered id is a promise that has already been kept, so it is noise in the
one file whose whole job is naming what is still missing.
@erfnzdeh
erfnzdeh merged commit c0d45ba into main Sep 3, 2026
20 checks passed
@erfnzdeh
erfnzdeh deleted the feat/pr5-contacts branch September 3, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant