tlgr-cli 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tlgr/__init__.py +3 -0
- tlgr/__main__.py +6 -0
- tlgr/actions/__init__.py +45 -0
- tlgr/actions/forward.py +74 -0
- tlgr/actions/reply.py +32 -0
- tlgr/cli/__init__.py +259 -0
- tlgr/cli/confirm.py +55 -0
- tlgr/cli/errors.py +84 -0
- tlgr/cli/gen.py +690 -0
- tlgr/cli/globals.py +273 -0
- tlgr/cli/introspect.py +170 -0
- tlgr/cli/params.py +189 -0
- tlgr/cli/render.py +418 -0
- tlgr/core/__init__.py +0 -0
- tlgr/core/accounts.py +384 -0
- tlgr/core/config.py +358 -0
- tlgr/core/custom_tl.py +170 -0
- tlgr/core/errors.py +687 -0
- tlgr/core/eventtypes.py +1170 -0
- tlgr/core/identity.py +127 -0
- tlgr/core/launchd.py +122 -0
- tlgr/core/logging.py +194 -0
- tlgr/core/media.py +134 -0
- tlgr/core/output.py +251 -0
- tlgr/core/pagination.py +227 -0
- tlgr/core/paths.py +360 -0
- tlgr/core/peers.py +427 -0
- tlgr/core/process.py +138 -0
- tlgr/core/signing.py +38 -0
- tlgr/core/systemd.py +96 -0
- tlgr/core/telethon_compat.py +295 -0
- tlgr/core/text.py +211 -0
- tlgr/core/timefmt.py +199 -0
- tlgr/core/tl.py +98 -0
- tlgr/daemon/__init__.py +0 -0
- tlgr/daemon/app.py +869 -0
- tlgr/daemon/dispatch.py +446 -0
- tlgr/daemon/events.py +723 -0
- tlgr/daemon/files.py +431 -0
- tlgr/daemon/idle.py +119 -0
- tlgr/daemon/jobs.py +68 -0
- tlgr/daemon/main.py +161 -0
- tlgr/daemon/peercred.py +75 -0
- tlgr/daemon/policy.py +113 -0
- tlgr/daemon/preauth.py +366 -0
- tlgr/daemon/ratelimit.py +391 -0
- tlgr/daemon/server.py +24 -0
- tlgr/daemon/session.py +648 -0
- tlgr/daemon/sessions.py +274 -0
- tlgr/daemon/singleton.py +114 -0
- tlgr/daemon/stream.py +193 -0
- tlgr/daemon/transfers.py +219 -0
- tlgr/daemon/webhook.py +390 -0
- tlgr/data/catalog_index.json +1 -0
- tlgr/data/parity_waivers.toml +90 -0
- tlgr/filters/__init__.py +42 -0
- tlgr/filters/compose.py +121 -0
- tlgr/filters/content.py +85 -0
- tlgr/filters/context.py +114 -0
- tlgr/filters/message.py +161 -0
- tlgr/filters/temporal.py +87 -0
- tlgr/filters/user.py +36 -0
- tlgr/gateway/__init__.py +1 -0
- tlgr/gateway/config.py +161 -0
- tlgr/gateway/engine.py +215 -0
- tlgr/gateway/event.py +22 -0
- tlgr/jobs/__init__.py +0 -0
- tlgr/jobs/base.py +81 -0
- tlgr/jobs/client.py +37 -0
- tlgr/models/__init__.py +1220 -0
- tlgr/models/admin.py +744 -0
- tlgr/models/auth.py +510 -0
- tlgr/models/base.py +81 -0
- tlgr/models/bot.py +576 -0
- tlgr/models/business.py +265 -0
- tlgr/models/call.py +586 -0
- tlgr/models/config.py +101 -0
- tlgr/models/contact.py +481 -0
- tlgr/models/daemon.py +336 -0
- tlgr/models/dialog.py +626 -0
- tlgr/models/envelope.py +68 -0
- tlgr/models/error.py +30 -0
- tlgr/models/event.py +79 -0
- tlgr/models/export.py +66 -0
- tlgr/models/gift.py +275 -0
- tlgr/models/inline.py +84 -0
- tlgr/models/location.py +115 -0
- tlgr/models/media.py +507 -0
- tlgr/models/message.py +584 -0
- tlgr/models/net.py +232 -0
- tlgr/models/notify.py +105 -0
- tlgr/models/page.py +32 -0
- tlgr/models/payment.py +172 -0
- tlgr/models/peer.py +400 -0
- tlgr/models/poll.py +119 -0
- tlgr/models/premium.py +161 -0
- tlgr/models/privacy.py +93 -0
- tlgr/models/profile.py +217 -0
- tlgr/models/reaction.py +160 -0
- tlgr/models/resolve.py +175 -0
- tlgr/models/settings.py +103 -0
- tlgr/models/stars.py +101 -0
- tlgr/models/sticker.py +243 -0
- tlgr/models/story.py +467 -0
- tlgr/models/sync.py +105 -0
- tlgr/models/todo.py +36 -0
- tlgr/models/webapp.py +89 -0
- tlgr/ops/__init__.py +63 -0
- tlgr/ops/_admin.py +313 -0
- tlgr/ops/_auth.py +599 -0
- tlgr/ops/_bots.py +586 -0
- tlgr/ops/_calls.py +535 -0
- tlgr/ops/_common.py +160 -0
- tlgr/ops/_layer.py +46 -0
- tlgr/ops/_media.py +592 -0
- tlgr/ops/_params.py +212 -0
- tlgr/ops/_rights.py +402 -0
- tlgr/ops/_send.py +593 -0
- tlgr/ops/_serialize.py +667 -0
- tlgr/ops/_settings.py +306 -0
- tlgr/ops/_spec.py +167 -0
- tlgr/ops/_story.py +743 -0
- tlgr/ops/account.py +2604 -0
- tlgr/ops/agent.py +937 -0
- tlgr/ops/auth.py +1282 -0
- tlgr/ops/bot.py +4880 -0
- tlgr/ops/business.py +1520 -0
- tlgr/ops/call.py +1610 -0
- tlgr/ops/chat.py +4025 -0
- tlgr/ops/chat_admin.py +929 -0
- tlgr/ops/chat_extra.py +1061 -0
- tlgr/ops/chat_invite.py +716 -0
- tlgr/ops/chat_manage.py +1691 -0
- tlgr/ops/chat_member.py +1357 -0
- tlgr/ops/chat_stats.py +902 -0
- tlgr/ops/chat_topic.py +905 -0
- tlgr/ops/conference.py +791 -0
- tlgr/ops/config.py +1698 -0
- tlgr/ops/contact.py +2330 -0
- tlgr/ops/daemon.py +1397 -0
- tlgr/ops/draft.py +299 -0
- tlgr/ops/emoji.py +343 -0
- tlgr/ops/events.py +1327 -0
- tlgr/ops/export.py +596 -0
- tlgr/ops/folder.py +1322 -0
- tlgr/ops/gif.py +522 -0
- tlgr/ops/gift.py +1546 -0
- tlgr/ops/giveaway.py +541 -0
- tlgr/ops/inline.py +773 -0
- tlgr/ops/job.py +799 -0
- tlgr/ops/location.py +917 -0
- tlgr/ops/media.py +4495 -0
- tlgr/ops/message.py +3769 -0
- tlgr/ops/net.py +536 -0
- tlgr/ops/notify.py +840 -0
- tlgr/ops/passport.py +464 -0
- tlgr/ops/payment.py +907 -0
- tlgr/ops/poll.py +1078 -0
- tlgr/ops/premium.py +488 -0
- tlgr/ops/privacy.py +794 -0
- tlgr/ops/profile.py +1481 -0
- tlgr/ops/proxy.py +750 -0
- tlgr/ops/reaction.py +1475 -0
- tlgr/ops/resolve.py +1140 -0
- tlgr/ops/search.py +521 -0
- tlgr/ops/settings.py +1066 -0
- tlgr/ops/stars.py +594 -0
- tlgr/ops/sticker.py +1602 -0
- tlgr/ops/story.py +3216 -0
- tlgr/ops/sync.py +788 -0
- tlgr/ops/todo.py +514 -0
- tlgr/ops/user.py +1406 -0
- tlgr/ops/vc.py +2351 -0
- tlgr/ops/webapp.py +717 -0
- tlgr/ops/webhook.py +418 -0
- tlgr/parity.py +386 -0
- tlgr/processors/__init__.py +125 -0
- tlgr/processors/regex.py +26 -0
- tlgr/processors/text.py +56 -0
- tlgr/registry.py +519 -0
- tlgr/schema.py +173 -0
- tlgr/transport/__init__.py +30 -0
- tlgr/transport/autostart.py +293 -0
- tlgr/transport/client.py +805 -0
- tlgr/transport/ndjson.py +44 -0
- tlgr/version.py +31 -0
- tlgr_cli-2.0.1.dist-info/METADATA +957 -0
- tlgr_cli-2.0.1.dist-info/RECORD +192 -0
- tlgr_cli-2.0.1.dist-info/WHEEL +5 -0
- tlgr_cli-2.0.1.dist-info/entry_points.txt +2 -0
- tlgr_cli-2.0.1.dist-info/licenses/LICENSE +21 -0
- tlgr_cli-2.0.1.dist-info/top_level.txt +1 -0
tlgr/ops/passport.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""The `passport` group: Telegram Passport, and the line tlgr will not cross.
|
|
2
|
+
|
|
3
|
+
Passport stores identity documents — a passport scan, a driving licence, an
|
|
4
|
+
address — encrypted end to end under a secret derived from the cloud password.
|
|
5
|
+
Telethon carries the RPCs but none of the crypto: no secure-secret KDF, no
|
|
6
|
+
`secureCredentialsEncrypted` builder, no AES-256-CBC/SHA-512 padding scheme.
|
|
7
|
+
|
|
8
|
+
So this group is deliberately lopsided, and says so rather than pretending:
|
|
9
|
+
|
|
10
|
+
* **reading works** — what is stored (`passport list`), what a service is
|
|
11
|
+
asking for and which of my values would satisfy it (`passport form get`);
|
|
12
|
+
* **deleting works** — `account.deleteSecureValue` needs no crypto at all,
|
|
13
|
+
and being able to remove documents matters more than being able to add
|
|
14
|
+
them;
|
|
15
|
+
* **verifying a phone or email works** — that exchange is plain MTProto;
|
|
16
|
+
* **authorising a service does not.** `passport authorize` is registered so
|
|
17
|
+
that the command exists and explains itself, and it raises NOT_SUPPORTED.
|
|
18
|
+
Half-implementing it would mean sending a service *something* in the
|
|
19
|
+
credentials field; a service that accepted it would be reading identity
|
|
20
|
+
documents tlgr encrypted wrongly, and one that rejected it would leave the
|
|
21
|
+
user with an error nobody can debug.
|
|
22
|
+
|
|
23
|
+
Nothing here is ever automatic. Handing identity documents to a bot from a
|
|
24
|
+
script is exactly the operation that deserves a human in front of it.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import contextlib
|
|
30
|
+
import json
|
|
31
|
+
from typing import Annotated, Any
|
|
32
|
+
|
|
33
|
+
from tlgr.core.errors import NotSupportedError, UsageError
|
|
34
|
+
from tlgr.core.pagination import PageKind
|
|
35
|
+
from tlgr.models.auth import (
|
|
36
|
+
PassportDeletion,
|
|
37
|
+
PassportForm,
|
|
38
|
+
PassportRequirement,
|
|
39
|
+
PassportValue,
|
|
40
|
+
PassportVerification,
|
|
41
|
+
)
|
|
42
|
+
from tlgr.models.base import Request
|
|
43
|
+
from tlgr.models.page import Page
|
|
44
|
+
from tlgr.models.peer import PeerRef
|
|
45
|
+
from tlgr.ops import _auth, _send
|
|
46
|
+
from tlgr.ops._params import arg, choice, opt
|
|
47
|
+
from tlgr.ops._spec import OpContext, OperationSpec
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"SPEC_AUTHORIZE",
|
|
51
|
+
"SPEC_DELETE",
|
|
52
|
+
"SPEC_FORM_GET",
|
|
53
|
+
"SPEC_LIST",
|
|
54
|
+
"SPEC_VERIFY",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
#: The message every "we cannot do the crypto" path prints. One string, so a
|
|
58
|
+
#: reader who hits it twice recognises it as the same wall.
|
|
59
|
+
_NO_CRYPTO = (
|
|
60
|
+
"Telegram Passport values are encrypted with a secret derived from the cloud "
|
|
61
|
+
"password (AES-256-CBC plus a SHA-512 KDF). Telethon 1.44 implements none of "
|
|
62
|
+
"it, and tlgr will not ship a half-correct implementation of a format that "
|
|
63
|
+
"carries identity documents. Use an official client for this step."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _value_model(raw: Any) -> PassportValue:
|
|
68
|
+
plain = getattr(raw, "plain_data", None)
|
|
69
|
+
return PassportValue(
|
|
70
|
+
type=_auth.secure_value_name(getattr(raw, "type", None)),
|
|
71
|
+
hash=_auth.b64(getattr(raw, "hash", b"")),
|
|
72
|
+
has_files=bool(getattr(raw, "files", None) or getattr(raw, "front_side", None)),
|
|
73
|
+
has_translation=bool(getattr(raw, "translation", None)),
|
|
74
|
+
plain_data=getattr(plain, "phone", None) or getattr(plain, "email", None),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# passport list
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ListReq(Request):
|
|
84
|
+
type: Annotated[
|
|
85
|
+
tuple[str, ...],
|
|
86
|
+
opt("--type", metavar="TYPE", help="Restrict to these document types (repeatable)."),
|
|
87
|
+
] = ()
|
|
88
|
+
decrypt: Annotated[
|
|
89
|
+
bool, opt("--decrypt", help="Attempt to decrypt the values (needs the crypto stack).")
|
|
90
|
+
] = False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def list_values(ctx: OpContext, req: ListReq) -> Page[PassportValue]:
|
|
94
|
+
"""List the Passport documents stored on this account, as metadata.
|
|
95
|
+
|
|
96
|
+
`phone` and `email` values are stored in the clear and come back filled;
|
|
97
|
+
every other type is end-to-end encrypted, so what is reported is the type,
|
|
98
|
+
the hash and whether it carries files. That is enough to audit what a
|
|
99
|
+
service could be given, which is the question worth asking from a CLI.
|
|
100
|
+
"""
|
|
101
|
+
from telethon.tl.functions import account as fn
|
|
102
|
+
|
|
103
|
+
if req.decrypt:
|
|
104
|
+
raise NotSupportedError(_NO_CRYPTO)
|
|
105
|
+
client = _auth.client(ctx)
|
|
106
|
+
if req.type:
|
|
107
|
+
wanted = [_auth.secure_value_type(name) for name in req.type]
|
|
108
|
+
raw = await client(fn.GetSecureValueRequest(types=wanted))
|
|
109
|
+
else:
|
|
110
|
+
raw = await client(fn.GetAllSecureValuesRequest())
|
|
111
|
+
items = [_value_model(value) for value in raw or []]
|
|
112
|
+
return Page(items=items, has_more=False, total=len(items))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
SPEC_LIST = OperationSpec(
|
|
116
|
+
id="passport.list",
|
|
117
|
+
request=ListReq,
|
|
118
|
+
response=Page[PassportValue],
|
|
119
|
+
impl=list_values,
|
|
120
|
+
summary="List the Telegram Passport documents stored on my account",
|
|
121
|
+
description=(
|
|
122
|
+
"Metadata only. The values are encrypted under a secret derived from "
|
|
123
|
+
"the cloud password, and tlgr does not implement that KDF — which is "
|
|
124
|
+
"why this feature is catalogued as partial rather than claimed."
|
|
125
|
+
),
|
|
126
|
+
paginated=PageKind.LOCAL,
|
|
127
|
+
rate_class="read",
|
|
128
|
+
columns=("type", "has_files", "plain_data"),
|
|
129
|
+
headers=("Type", "Files", "Plain value"),
|
|
130
|
+
example={"items": [{"type": "phone", "hash": "3q2-7w", "plain_data": "+989123456789"}]},
|
|
131
|
+
example_args="passport list",
|
|
132
|
+
covers=("passport.list",),
|
|
133
|
+
tags=frozenset({"agent-safe"}),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# passport form get
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class FormGetReq(Request):
|
|
143
|
+
bot: Annotated[
|
|
144
|
+
PeerRef | None,
|
|
145
|
+
arg(0, metavar="BOT", required=False, kind="peer", help="The service's bot."),
|
|
146
|
+
] = None
|
|
147
|
+
scope: Annotated[
|
|
148
|
+
str | None, opt("--scope", metavar="JSON", kind="json", help="Scope JSON from the link.")
|
|
149
|
+
] = None
|
|
150
|
+
public_key: Annotated[
|
|
151
|
+
str | None,
|
|
152
|
+
opt("--public-key", metavar="PATH", kind="path", help="The service's RSA key (PEM)."),
|
|
153
|
+
] = None
|
|
154
|
+
nonce: Annotated[str | None, opt("--nonce", help="Nonce from the request.")] = None
|
|
155
|
+
country_language: Annotated[
|
|
156
|
+
str | None,
|
|
157
|
+
opt("--country-language", metavar="ISO2", help="Only look up a country's native-name tag."),
|
|
158
|
+
] = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def form_get(ctx: OpContext, req: FormGetReq) -> PassportForm:
|
|
162
|
+
"""Show what a service is asking for, and what of it I already hold.
|
|
163
|
+
|
|
164
|
+
Read-only inspection of the request: the document types demanded, whether
|
|
165
|
+
each needs a selfie or a translation, the privacy policy, and the values
|
|
166
|
+
already on the account that would satisfy it. `--country-language` is the
|
|
167
|
+
lookup that decides whether a country's forms want native-language
|
|
168
|
+
fields, which is otherwise a silent validation failure later.
|
|
169
|
+
"""
|
|
170
|
+
from telethon.tl.functions import help as help_fn
|
|
171
|
+
|
|
172
|
+
client = _auth.client(ctx)
|
|
173
|
+
|
|
174
|
+
if req.country_language:
|
|
175
|
+
config = await client(help_fn.GetPassportConfigRequest(hash=0))
|
|
176
|
+
mapping: dict[str, Any] = {}
|
|
177
|
+
raw = getattr(getattr(config, "countries_langs", None), "data", "")
|
|
178
|
+
if raw:
|
|
179
|
+
with contextlib.suppress(ValueError):
|
|
180
|
+
mapping = json.loads(raw)
|
|
181
|
+
return PassportForm(country_language=mapping.get(req.country_language.upper()))
|
|
182
|
+
|
|
183
|
+
if not (req.bot and req.scope and req.public_key):
|
|
184
|
+
raise UsageError(
|
|
185
|
+
"reading a form needs the bot, --scope and --public-key from the service's link",
|
|
186
|
+
field="bot",
|
|
187
|
+
)
|
|
188
|
+
from pathlib import Path
|
|
189
|
+
|
|
190
|
+
from telethon.tl.functions import account as fn
|
|
191
|
+
|
|
192
|
+
entity = await _send.resolve(ctx, req.bot)
|
|
193
|
+
bot_id = int(getattr(entity, "user_id", 0) or 0)
|
|
194
|
+
form = await client(
|
|
195
|
+
fn.GetAuthorizationFormRequest(
|
|
196
|
+
bot_id=bot_id,
|
|
197
|
+
scope=req.scope,
|
|
198
|
+
public_key=Path(req.public_key).read_text(encoding="utf-8"),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
return PassportForm(
|
|
202
|
+
bot=bot_id,
|
|
203
|
+
required_types=[
|
|
204
|
+
PassportRequirement(
|
|
205
|
+
type=_auth.secure_value_name(getattr(item, "type", None)),
|
|
206
|
+
native_names=bool(getattr(item, "native_names", False)),
|
|
207
|
+
selfie_required=bool(getattr(item, "selfie_required", False)),
|
|
208
|
+
translation_required=bool(getattr(item, "translation_required", False)),
|
|
209
|
+
)
|
|
210
|
+
for item in getattr(form, "required_types", None) or []
|
|
211
|
+
],
|
|
212
|
+
privacy_policy_url=getattr(form, "privacy_policy_url", None),
|
|
213
|
+
values=[_value_model(value) for value in getattr(form, "values", None) or []],
|
|
214
|
+
errors=[str(getattr(err, "text", err)) for err in getattr(form, "errors", None) or []],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
SPEC_FORM_GET = OperationSpec(
|
|
219
|
+
id="passport.form.get",
|
|
220
|
+
request=FormGetReq,
|
|
221
|
+
response=PassportForm,
|
|
222
|
+
impl=form_get,
|
|
223
|
+
summary="Show what a service is asking for through Telegram Passport",
|
|
224
|
+
rate_class="read",
|
|
225
|
+
columns=("bot", "privacy_policy_url"),
|
|
226
|
+
example={
|
|
227
|
+
"bot": 4242,
|
|
228
|
+
"required_types": [{"type": "passport", "selfie_required": True}],
|
|
229
|
+
"privacy_policy_url": "https://example.com/privacy",
|
|
230
|
+
},
|
|
231
|
+
example_args="passport form get --country-language DE",
|
|
232
|
+
covers=("passport.country-language",),
|
|
233
|
+
covers_partial=("auth.passport-authorize", "passport.authorization"),
|
|
234
|
+
coverage_note=(
|
|
235
|
+
"The request can be read in full; accepting it needs the Passport "
|
|
236
|
+
"secure-value crypto Telethon does not provide (see `passport authorize`)."
|
|
237
|
+
),
|
|
238
|
+
tags=frozenset({"agent-safe"}),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# passport authorize — registered, and refused
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class AuthorizeReq(Request):
|
|
248
|
+
bot: Annotated[PeerRef, arg(0, metavar="BOT", kind="peer", help="The service's bot.")]
|
|
249
|
+
password: Annotated[
|
|
250
|
+
str | None,
|
|
251
|
+
opt(secret=True, envvar="TLGR_2FA_PASSWORD", help="The 2FA cloud password."),
|
|
252
|
+
] = None
|
|
253
|
+
scope: Annotated[
|
|
254
|
+
str | None, opt("--scope", metavar="JSON", kind="json", help="Scope JSON from the service.")
|
|
255
|
+
] = None
|
|
256
|
+
public_key: Annotated[
|
|
257
|
+
str | None,
|
|
258
|
+
opt("--public-key", metavar="PATH", kind="path", help="The service's RSA key (PEM)."),
|
|
259
|
+
] = None
|
|
260
|
+
values: Annotated[
|
|
261
|
+
str | None,
|
|
262
|
+
opt(
|
|
263
|
+
"--values", metavar="JSON", kind="json", help="Which stored value satisfies each type."
|
|
264
|
+
),
|
|
265
|
+
] = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
async def authorize(ctx: OpContext, req: AuthorizeReq) -> PassportForm:
|
|
269
|
+
"""Refuse to share identity documents with a half-built crypto stack.
|
|
270
|
+
|
|
271
|
+
`account.acceptAuthorization` takes a `secureCredentialsEncrypted` blob:
|
|
272
|
+
the requested values, re-encrypted under the *service's* RSA key with a
|
|
273
|
+
secret derived from the cloud password. Telethon builds none of that, and
|
|
274
|
+
a wrong blob is not a failed command — it is either a service reading
|
|
275
|
+
documents encrypted incorrectly, or an error message that nobody outside
|
|
276
|
+
Telegram can diagnose.
|
|
277
|
+
|
|
278
|
+
Exit 13 (NOT_SUPPORTED), not 6: nothing refused this, it was never asked.
|
|
279
|
+
Read the request with `passport form get` and complete the share in an
|
|
280
|
+
official client.
|
|
281
|
+
"""
|
|
282
|
+
raise NotSupportedError(
|
|
283
|
+
f"{_NO_CRYPTO} Inspect what the service wants with: tlgr passport form get {req.bot.value}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
SPEC_AUTHORIZE = OperationSpec(
|
|
288
|
+
id="passport.authorize",
|
|
289
|
+
request=AuthorizeReq,
|
|
290
|
+
response=PassportForm,
|
|
291
|
+
impl=authorize,
|
|
292
|
+
summary="Authorize a service with Telegram Passport (not supported: see the help)",
|
|
293
|
+
description=(
|
|
294
|
+
"Registered so the command exists and explains itself, and refused "
|
|
295
|
+
"with NOT_SUPPORTED. Sharing identity documents through a crypto "
|
|
296
|
+
"stack tlgr does not implement is worse than not offering it."
|
|
297
|
+
),
|
|
298
|
+
mutating=True,
|
|
299
|
+
destructive=True,
|
|
300
|
+
rate_class="send",
|
|
301
|
+
columns=("bot",),
|
|
302
|
+
example={"bot": 4242},
|
|
303
|
+
example_args="passport authorize @examplebot",
|
|
304
|
+
covers_partial=("passport.authorization",),
|
|
305
|
+
coverage_note=(
|
|
306
|
+
"The request is readable (`passport form get`); acceptance needs the "
|
|
307
|
+
"Passport secure-value crypto and raises NOT_SUPPORTED."
|
|
308
|
+
),
|
|
309
|
+
tags=frozenset({"not-supported"}),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# passport delete
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class DeleteReq(Request):
|
|
319
|
+
type: Annotated[
|
|
320
|
+
tuple[str, ...],
|
|
321
|
+
arg(0, metavar="TYPE", variadic=True, help="Document types to delete."),
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
async def delete(ctx: OpContext, req: DeleteReq) -> PassportDeletion:
|
|
326
|
+
"""Delete stored Passport documents. Needs no crypto, and is irreversible."""
|
|
327
|
+
from telethon.tl.functions import account as fn
|
|
328
|
+
|
|
329
|
+
if not req.type:
|
|
330
|
+
raise UsageError(
|
|
331
|
+
f"name the document types to delete: {', '.join(sorted(_auth.SECURE_VALUE_TYPES))}",
|
|
332
|
+
field="type",
|
|
333
|
+
)
|
|
334
|
+
client = _auth.client(ctx)
|
|
335
|
+
wanted = [_auth.secure_value_type(name) for name in req.type]
|
|
336
|
+
await client(fn.DeleteSecureValueRequest(types=wanted))
|
|
337
|
+
deleted = [_auth.secure_value_name(item) for item in wanted]
|
|
338
|
+
ctx.emit("passport_deleted", {"types": deleted})
|
|
339
|
+
return PassportDeletion(deleted=deleted)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
SPEC_DELETE = OperationSpec(
|
|
343
|
+
id="passport.delete",
|
|
344
|
+
request=DeleteReq,
|
|
345
|
+
response=PassportDeletion,
|
|
346
|
+
impl=delete,
|
|
347
|
+
summary="Delete stored Passport documents",
|
|
348
|
+
mutating=True,
|
|
349
|
+
destructive=True,
|
|
350
|
+
rate_class="send",
|
|
351
|
+
columns=("deleted",),
|
|
352
|
+
example={"deleted": ["passport"]},
|
|
353
|
+
example_args="passport delete passport",
|
|
354
|
+
covers=("passport.save-delete",),
|
|
355
|
+
coverage_note="Saving a value needs the encryption stack; deleting one does not.",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# passport verify
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class VerifyReq(Request):
|
|
365
|
+
phone: Annotated[str | None, opt("--phone", help="Phone number to verify.")] = None
|
|
366
|
+
email: Annotated[str | None, opt("--email", help="Email address to verify.")] = None
|
|
367
|
+
code: Annotated[str | None, opt("--code", help="Code that arrived; omit to request one.")] = (
|
|
368
|
+
None
|
|
369
|
+
)
|
|
370
|
+
code_hash: Annotated[
|
|
371
|
+
str | None,
|
|
372
|
+
opt("--code-hash", metavar="HASH", help="The hash the first call returned (--phone only)."),
|
|
373
|
+
] = None
|
|
374
|
+
purpose: Annotated[
|
|
375
|
+
str | None,
|
|
376
|
+
choice("passport", "login-setup", "login-change", help="EmailVerifyPurpose."),
|
|
377
|
+
] = "passport"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def verify(ctx: OpContext, req: VerifyReq) -> PassportVerification:
|
|
381
|
+
"""Verify an extra phone number or email.
|
|
382
|
+
|
|
383
|
+
The code exchange is plain MTProto and works fully; only *storing* the
|
|
384
|
+
result as a Passport secure value needs the encryption stack, which is
|
|
385
|
+
why this command stops at "verified".
|
|
386
|
+
"""
|
|
387
|
+
from telethon.tl import types
|
|
388
|
+
from telethon.tl.functions import account as fn
|
|
389
|
+
|
|
390
|
+
client = _auth.client(ctx)
|
|
391
|
+
if bool(req.phone) == bool(req.email):
|
|
392
|
+
raise UsageError("give exactly one of --phone or --email", field="phone")
|
|
393
|
+
|
|
394
|
+
if req.phone:
|
|
395
|
+
if not req.code:
|
|
396
|
+
sent = await client(
|
|
397
|
+
fn.SendVerifyPhoneCodeRequest(
|
|
398
|
+
phone_number=req.phone, settings=_auth.code_settings()
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
fields = _auth.sent_code_fields(sent)
|
|
402
|
+
return PassportVerification(
|
|
403
|
+
target=_auth.masked(req.phone),
|
|
404
|
+
sent=True,
|
|
405
|
+
code_length=fields.get("length"),
|
|
406
|
+
code_hash=fields["code_hash"],
|
|
407
|
+
)
|
|
408
|
+
if not req.code_hash:
|
|
409
|
+
raise UsageError(
|
|
410
|
+
"pass --code-hash with the value the first call returned; Telegram will not "
|
|
411
|
+
"accept a code without the hash that came with it",
|
|
412
|
+
field="code_hash",
|
|
413
|
+
)
|
|
414
|
+
await client(
|
|
415
|
+
fn.VerifyPhoneRequest(
|
|
416
|
+
phone_number=req.phone,
|
|
417
|
+
phone_code_hash=req.code_hash,
|
|
418
|
+
phone_code=req.code,
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
return PassportVerification(target=_auth.masked(req.phone), verified=True)
|
|
422
|
+
|
|
423
|
+
purposes = {
|
|
424
|
+
"passport": types.EmailVerifyPurposePassport,
|
|
425
|
+
"login-setup": types.EmailVerifyPurposeLoginSetup,
|
|
426
|
+
"login-change": types.EmailVerifyPurposeLoginChange,
|
|
427
|
+
}
|
|
428
|
+
if (req.purpose or "passport") != "passport":
|
|
429
|
+
raise UsageError(
|
|
430
|
+
"login-setup and login-change belong to the login flow: "
|
|
431
|
+
"use `tlgr auth login-email set` or `tlgr account email set --kind login`",
|
|
432
|
+
field="purpose",
|
|
433
|
+
)
|
|
434
|
+
purpose = purposes["passport"]()
|
|
435
|
+
if not req.code:
|
|
436
|
+
sent = await client(fn.SendVerifyEmailCodeRequest(purpose=purpose, email=req.email or ""))
|
|
437
|
+
return PassportVerification(
|
|
438
|
+
target=getattr(sent, "email_pattern", req.email) or "",
|
|
439
|
+
sent=True,
|
|
440
|
+
code_length=getattr(sent, "length", None),
|
|
441
|
+
)
|
|
442
|
+
verified = await client(
|
|
443
|
+
fn.VerifyEmailRequest(
|
|
444
|
+
purpose=purpose, verification=types.EmailVerificationCode(code=req.code)
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
return PassportVerification(target=getattr(verified, "email", req.email) or "", verified=True)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
SPEC_VERIFY = OperationSpec(
|
|
451
|
+
id="passport.verify",
|
|
452
|
+
request=VerifyReq,
|
|
453
|
+
response=PassportVerification,
|
|
454
|
+
impl=verify,
|
|
455
|
+
summary="Verify an extra phone number or email",
|
|
456
|
+
mutating=True,
|
|
457
|
+
rate_class="send",
|
|
458
|
+
columns=("target", "sent", "verified"),
|
|
459
|
+
example={"target": "a**@e*****e.com", "sent": True, "code_length": 6},
|
|
460
|
+
example_args="passport verify --email ada@example.com",
|
|
461
|
+
covers=("auth.passport-verify-phone-email",),
|
|
462
|
+
covers_partial=("account.verify-phone-email",),
|
|
463
|
+
coverage_note="The recovery/login addresses are owned by `account email set`.",
|
|
464
|
+
)
|