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.
Files changed (192) hide show
  1. tlgr/__init__.py +3 -0
  2. tlgr/__main__.py +6 -0
  3. tlgr/actions/__init__.py +45 -0
  4. tlgr/actions/forward.py +74 -0
  5. tlgr/actions/reply.py +32 -0
  6. tlgr/cli/__init__.py +259 -0
  7. tlgr/cli/confirm.py +55 -0
  8. tlgr/cli/errors.py +84 -0
  9. tlgr/cli/gen.py +690 -0
  10. tlgr/cli/globals.py +273 -0
  11. tlgr/cli/introspect.py +170 -0
  12. tlgr/cli/params.py +189 -0
  13. tlgr/cli/render.py +418 -0
  14. tlgr/core/__init__.py +0 -0
  15. tlgr/core/accounts.py +384 -0
  16. tlgr/core/config.py +358 -0
  17. tlgr/core/custom_tl.py +170 -0
  18. tlgr/core/errors.py +687 -0
  19. tlgr/core/eventtypes.py +1170 -0
  20. tlgr/core/identity.py +127 -0
  21. tlgr/core/launchd.py +122 -0
  22. tlgr/core/logging.py +194 -0
  23. tlgr/core/media.py +134 -0
  24. tlgr/core/output.py +251 -0
  25. tlgr/core/pagination.py +227 -0
  26. tlgr/core/paths.py +360 -0
  27. tlgr/core/peers.py +427 -0
  28. tlgr/core/process.py +138 -0
  29. tlgr/core/signing.py +38 -0
  30. tlgr/core/systemd.py +96 -0
  31. tlgr/core/telethon_compat.py +295 -0
  32. tlgr/core/text.py +211 -0
  33. tlgr/core/timefmt.py +199 -0
  34. tlgr/core/tl.py +98 -0
  35. tlgr/daemon/__init__.py +0 -0
  36. tlgr/daemon/app.py +869 -0
  37. tlgr/daemon/dispatch.py +446 -0
  38. tlgr/daemon/events.py +723 -0
  39. tlgr/daemon/files.py +431 -0
  40. tlgr/daemon/idle.py +119 -0
  41. tlgr/daemon/jobs.py +68 -0
  42. tlgr/daemon/main.py +161 -0
  43. tlgr/daemon/peercred.py +75 -0
  44. tlgr/daemon/policy.py +113 -0
  45. tlgr/daemon/preauth.py +366 -0
  46. tlgr/daemon/ratelimit.py +391 -0
  47. tlgr/daemon/server.py +24 -0
  48. tlgr/daemon/session.py +648 -0
  49. tlgr/daemon/sessions.py +274 -0
  50. tlgr/daemon/singleton.py +114 -0
  51. tlgr/daemon/stream.py +193 -0
  52. tlgr/daemon/transfers.py +219 -0
  53. tlgr/daemon/webhook.py +390 -0
  54. tlgr/data/catalog_index.json +1 -0
  55. tlgr/data/parity_waivers.toml +90 -0
  56. tlgr/filters/__init__.py +42 -0
  57. tlgr/filters/compose.py +121 -0
  58. tlgr/filters/content.py +85 -0
  59. tlgr/filters/context.py +114 -0
  60. tlgr/filters/message.py +161 -0
  61. tlgr/filters/temporal.py +87 -0
  62. tlgr/filters/user.py +36 -0
  63. tlgr/gateway/__init__.py +1 -0
  64. tlgr/gateway/config.py +161 -0
  65. tlgr/gateway/engine.py +215 -0
  66. tlgr/gateway/event.py +22 -0
  67. tlgr/jobs/__init__.py +0 -0
  68. tlgr/jobs/base.py +81 -0
  69. tlgr/jobs/client.py +37 -0
  70. tlgr/models/__init__.py +1220 -0
  71. tlgr/models/admin.py +744 -0
  72. tlgr/models/auth.py +510 -0
  73. tlgr/models/base.py +81 -0
  74. tlgr/models/bot.py +576 -0
  75. tlgr/models/business.py +265 -0
  76. tlgr/models/call.py +586 -0
  77. tlgr/models/config.py +101 -0
  78. tlgr/models/contact.py +481 -0
  79. tlgr/models/daemon.py +336 -0
  80. tlgr/models/dialog.py +626 -0
  81. tlgr/models/envelope.py +68 -0
  82. tlgr/models/error.py +30 -0
  83. tlgr/models/event.py +79 -0
  84. tlgr/models/export.py +66 -0
  85. tlgr/models/gift.py +275 -0
  86. tlgr/models/inline.py +84 -0
  87. tlgr/models/location.py +115 -0
  88. tlgr/models/media.py +507 -0
  89. tlgr/models/message.py +584 -0
  90. tlgr/models/net.py +232 -0
  91. tlgr/models/notify.py +105 -0
  92. tlgr/models/page.py +32 -0
  93. tlgr/models/payment.py +172 -0
  94. tlgr/models/peer.py +400 -0
  95. tlgr/models/poll.py +119 -0
  96. tlgr/models/premium.py +161 -0
  97. tlgr/models/privacy.py +93 -0
  98. tlgr/models/profile.py +217 -0
  99. tlgr/models/reaction.py +160 -0
  100. tlgr/models/resolve.py +175 -0
  101. tlgr/models/settings.py +103 -0
  102. tlgr/models/stars.py +101 -0
  103. tlgr/models/sticker.py +243 -0
  104. tlgr/models/story.py +467 -0
  105. tlgr/models/sync.py +105 -0
  106. tlgr/models/todo.py +36 -0
  107. tlgr/models/webapp.py +89 -0
  108. tlgr/ops/__init__.py +63 -0
  109. tlgr/ops/_admin.py +313 -0
  110. tlgr/ops/_auth.py +599 -0
  111. tlgr/ops/_bots.py +586 -0
  112. tlgr/ops/_calls.py +535 -0
  113. tlgr/ops/_common.py +160 -0
  114. tlgr/ops/_layer.py +46 -0
  115. tlgr/ops/_media.py +592 -0
  116. tlgr/ops/_params.py +212 -0
  117. tlgr/ops/_rights.py +402 -0
  118. tlgr/ops/_send.py +593 -0
  119. tlgr/ops/_serialize.py +667 -0
  120. tlgr/ops/_settings.py +306 -0
  121. tlgr/ops/_spec.py +167 -0
  122. tlgr/ops/_story.py +743 -0
  123. tlgr/ops/account.py +2604 -0
  124. tlgr/ops/agent.py +937 -0
  125. tlgr/ops/auth.py +1282 -0
  126. tlgr/ops/bot.py +4880 -0
  127. tlgr/ops/business.py +1520 -0
  128. tlgr/ops/call.py +1610 -0
  129. tlgr/ops/chat.py +4025 -0
  130. tlgr/ops/chat_admin.py +929 -0
  131. tlgr/ops/chat_extra.py +1061 -0
  132. tlgr/ops/chat_invite.py +716 -0
  133. tlgr/ops/chat_manage.py +1691 -0
  134. tlgr/ops/chat_member.py +1357 -0
  135. tlgr/ops/chat_stats.py +902 -0
  136. tlgr/ops/chat_topic.py +905 -0
  137. tlgr/ops/conference.py +791 -0
  138. tlgr/ops/config.py +1698 -0
  139. tlgr/ops/contact.py +2330 -0
  140. tlgr/ops/daemon.py +1397 -0
  141. tlgr/ops/draft.py +299 -0
  142. tlgr/ops/emoji.py +343 -0
  143. tlgr/ops/events.py +1327 -0
  144. tlgr/ops/export.py +596 -0
  145. tlgr/ops/folder.py +1322 -0
  146. tlgr/ops/gif.py +522 -0
  147. tlgr/ops/gift.py +1546 -0
  148. tlgr/ops/giveaway.py +541 -0
  149. tlgr/ops/inline.py +773 -0
  150. tlgr/ops/job.py +799 -0
  151. tlgr/ops/location.py +917 -0
  152. tlgr/ops/media.py +4495 -0
  153. tlgr/ops/message.py +3769 -0
  154. tlgr/ops/net.py +536 -0
  155. tlgr/ops/notify.py +840 -0
  156. tlgr/ops/passport.py +464 -0
  157. tlgr/ops/payment.py +907 -0
  158. tlgr/ops/poll.py +1078 -0
  159. tlgr/ops/premium.py +488 -0
  160. tlgr/ops/privacy.py +794 -0
  161. tlgr/ops/profile.py +1481 -0
  162. tlgr/ops/proxy.py +750 -0
  163. tlgr/ops/reaction.py +1475 -0
  164. tlgr/ops/resolve.py +1140 -0
  165. tlgr/ops/search.py +521 -0
  166. tlgr/ops/settings.py +1066 -0
  167. tlgr/ops/stars.py +594 -0
  168. tlgr/ops/sticker.py +1602 -0
  169. tlgr/ops/story.py +3216 -0
  170. tlgr/ops/sync.py +788 -0
  171. tlgr/ops/todo.py +514 -0
  172. tlgr/ops/user.py +1406 -0
  173. tlgr/ops/vc.py +2351 -0
  174. tlgr/ops/webapp.py +717 -0
  175. tlgr/ops/webhook.py +418 -0
  176. tlgr/parity.py +386 -0
  177. tlgr/processors/__init__.py +125 -0
  178. tlgr/processors/regex.py +26 -0
  179. tlgr/processors/text.py +56 -0
  180. tlgr/registry.py +519 -0
  181. tlgr/schema.py +173 -0
  182. tlgr/transport/__init__.py +30 -0
  183. tlgr/transport/autostart.py +293 -0
  184. tlgr/transport/client.py +805 -0
  185. tlgr/transport/ndjson.py +44 -0
  186. tlgr/version.py +31 -0
  187. tlgr_cli-2.0.1.dist-info/METADATA +957 -0
  188. tlgr_cli-2.0.1.dist-info/RECORD +192 -0
  189. tlgr_cli-2.0.1.dist-info/WHEEL +5 -0
  190. tlgr_cli-2.0.1.dist-info/entry_points.txt +2 -0
  191. tlgr_cli-2.0.1.dist-info/licenses/LICENSE +21 -0
  192. tlgr_cli-2.0.1.dist-info/top_level.txt +1 -0
tlgr/models/auth.py ADDED
@@ -0,0 +1,510 @@
1
+ """The shapes the auth, account, session, password and passport groups emit.
2
+
3
+ Three of them deserve a note, because their fields are the whole point.
4
+
5
+ * `SentCode` is a *resumable* login. Telegram's `auth.sendCode` answers with a
6
+ `phone_code_hash` that the next call needs, and a code *type* that decides
7
+ what the human should be looking at (another Telegram session, an SMS, a
8
+ Fragment page, an email). v1 kept both in memory and therefore had to hold
9
+ one process open across a human reading their phone; here they are a
10
+ response, so `auth send-code` and `auth verify-code` are two commands.
11
+ * `PasswordState` is the read side of 2-step verification and it deliberately
12
+ carries **no** SRP material. `srp_B`, `secure_random` and the KDF salts are
13
+ live cryptographic parameters; printing them buys nothing and leaks the
14
+ shape of the exchange, so they stop at the operation boundary.
15
+ * `Session` is one row of the Devices list. `deny_deadline` and
16
+ `sensitive_actions_eligible_at` are derived here rather than left to the
17
+ reader: "unconfirmed" is only actionable while the auto-confirm window is
18
+ open, and `SESSION_TOO_FRESH_X` is a 24-hour rule nobody can guess from
19
+ `date_created` alone.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from tlgr.models.base import Model
25
+ from tlgr.models.message import MessageEntity
26
+
27
+ __all__ = [
28
+ "AccountDeletion",
29
+ "AccountRecord",
30
+ "AccountState",
31
+ "AccountTtl",
32
+ "AutologinUrl",
33
+ "DeviceLock",
34
+ "LoginCodes",
35
+ "LoginEmail",
36
+ "LoginResult",
37
+ "Passkey",
38
+ "PassportDeletion",
39
+ "PassportForm",
40
+ "PassportRequirement",
41
+ "PassportValue",
42
+ "PassportVerification",
43
+ "PasswordReset",
44
+ "PasswordState",
45
+ "PhoneChange",
46
+ "QrLogin",
47
+ "RecoveryEmail",
48
+ "SentCode",
49
+ "Session",
50
+ "SessionChange",
51
+ "SessionTermination",
52
+ "SmsJobs",
53
+ "Suggestion",
54
+ "SupportInfo",
55
+ "TempPassword",
56
+ "Terms",
57
+ "WebSession",
58
+ "WebSessionRevocation",
59
+ ]
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Logging in
64
+ # ---------------------------------------------------------------------------
65
+
66
+
67
+ class SentCode(Model):
68
+ """What `auth.sendCode` (and the two phone-change flows) answered.
69
+
70
+ `type` is reported verbatim — `app`, `sms`, `sms_word`, `sms_phrase`,
71
+ `call`, `flash_call`, `missed_call`, `fragment`, `email`,
72
+ `setup_email_required` — because each one asks the human to look
73
+ somewhere different, and "we sent you a code" is not enough instruction
74
+ for any of them.
75
+ """
76
+
77
+ phone: str = ""
78
+ code_hash: str = ""
79
+ type: str = ""
80
+ length: int | None = None
81
+ next_type: str | None = None
82
+ timeout: int | None = None
83
+ #: `sentCodeTypeFragmentSms.url` — the page the code is waiting on.
84
+ fragment_url: str | None = None
85
+ email_pattern: str | None = None
86
+ #: The first characters of a word/phrase SMS, so the human can tell which
87
+ #: message is the login one.
88
+ beginning: str | None = None
89
+ google_signin_allowed: bool = False
90
+ apple_signin_allowed: bool = False
91
+ reset_available_period: int | None = None
92
+ reset_pending_date: str | None = None
93
+ #: A `auth.sentCodeSuccess`: a stored future auth token matched and there
94
+ #: is no code to type at all.
95
+ already: bool = False
96
+ account: str = ""
97
+ cancelled: bool = False
98
+ changed: bool = False
99
+
100
+
101
+ class LoginResult(Model):
102
+ """The terminal state of a login attempt.
103
+
104
+ `status` is one of `authorized`, `password_required`, `signup_required`.
105
+ The last two are *not* failures of the command, they are the next step,
106
+ which is why they carry the hint and the recovery pattern an agent needs
107
+ to take it.
108
+ """
109
+
110
+ status: str = ""
111
+ user_id: int | None = None
112
+ username: str | None = None
113
+ alias: str = ""
114
+ #: `auth.authorization.setup_password_required`: Telegram will log this
115
+ #: session out after `otherwise_relogin_days` unless a cloud password is
116
+ #: set, which is a loud thing to be quiet about.
117
+ setup_password_required: bool = False
118
+ otherwise_relogin_days: int | None = None
119
+ hint: str | None = None
120
+ has_recovery: bool = False
121
+ email_pattern: str | None = None
122
+ tos_id: str | None = None
123
+
124
+
125
+ class QrLogin(Model):
126
+ """One frame of a QR login: the token to show, and what happened to it."""
127
+
128
+ url: str = ""
129
+ token: str = ""
130
+ expires: str | None = None
131
+ status: str = ""
132
+ user_id: int | None = None
133
+ alias: str = ""
134
+ ascii: str | None = None
135
+ png: str | None = None
136
+
137
+
138
+ class LoginEmail(Model):
139
+ """The login email the server demands during a pending login."""
140
+
141
+ email_pattern: str | None = None
142
+ verified: bool = False
143
+ sent_code: bool = False
144
+ reset_pending_date: str | None = None
145
+ length: int | None = None
146
+
147
+
148
+ class LoginCodes(Model):
149
+ """Login codes Telegram delivered into the 777000 service chat."""
150
+
151
+ codes: list[str] = []
152
+ messages: list[str] = []
153
+ invalidated: list[str] = []
154
+
155
+
156
+ class Terms(Model):
157
+ """A Terms-of-Service document and what was done about it."""
158
+
159
+ id: str | None = None
160
+ text: str = ""
161
+ entities: list[MessageEntity] = []
162
+ min_age_confirm: int | None = None
163
+ popup: bool = False
164
+ expires: str | None = None
165
+ update_available: bool = False
166
+ accepted: bool = False
167
+ declined: bool = False
168
+
169
+
170
+ class AccountDeletion(Model):
171
+ """`account.deleteAccount`, whether it happened or started a countdown."""
172
+
173
+ deleted: bool = False
174
+ status: str = ""
175
+ wait_seconds: int | None = None
176
+ until: str | None = None
177
+ confirm_hint: str | None = None
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # The local account record
182
+ # ---------------------------------------------------------------------------
183
+
184
+
185
+ class AccountRecord(Model, omit_defaults=False):
186
+ """One row of `account list`.
187
+
188
+ `alias` and `name` are both here on purpose: v1 printed the alias under
189
+ the key `alias` and the display name under `name`, and §12.4 says a
190
+ documented key does not move. `omit_defaults=False` for the same reason:
191
+ `active: false` is an answer, and dropping it would make every row of a
192
+ table look ragged.
193
+ """
194
+
195
+ alias: str = ""
196
+ name: str = ""
197
+ user_id: int | None = None
198
+ username: str | None = None
199
+ phone: str | None = None
200
+ active: bool = False
201
+ connected: bool = False
202
+ state: str = "unknown"
203
+ kind: str = "user"
204
+ created_at: str | None = None
205
+
206
+
207
+ class AccountState(Model):
208
+ """`account info` / `account check` / `account switch` / `account rename`.
209
+
210
+ One struct for the whole local-record surface: the commands differ in
211
+ which subset they fill, and a reader that learns the field names once can
212
+ read all of them.
213
+ """
214
+
215
+ alias: str = ""
216
+ account: str = ""
217
+ ok: bool = False
218
+ already: bool = False
219
+ removed: bool = False
220
+ server_logout: bool = False
221
+ logged_out: bool = False
222
+ future_auth_token_stored: bool = False
223
+ imported: bool = False
224
+ authorized: bool = False
225
+ old: str | None = None
226
+ new: str | None = None
227
+ user_id: int | None = None
228
+ username: str | None = None
229
+ first_name: str | None = None
230
+ phone: str | None = None
231
+ dc_id: int | None = None
232
+ premium: bool = False
233
+ kind: str = "user"
234
+ test_dc: bool = False
235
+ session_path: str | None = None
236
+ created_at: str | None = None
237
+ #: `account check` only.
238
+ state: str = ""
239
+ error: str | None = None
240
+ frozen_since: str | None = None
241
+ frozen_until: str | None = None
242
+ appeal_url: str | None = None
243
+ hint: str | None = None
244
+ #: `account sync` only.
245
+ dialogs: int | None = None
246
+ users: int | None = None
247
+ chats: int | None = None
248
+ pts: int | None = None
249
+ #: `account export` only.
250
+ format: str | None = None
251
+ path: str | None = None
252
+ session: str | None = None
253
+
254
+
255
+ class AccountTtl(Model):
256
+ """The self-destruct timer, in days."""
257
+
258
+ days: int = 0
259
+
260
+
261
+ class DeviceLock(Model):
262
+ locked_for: int = 0
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Sessions (the Devices list)
267
+ # ---------------------------------------------------------------------------
268
+
269
+
270
+ class Session(Model):
271
+ """One authorization — a device logged into this account."""
272
+
273
+ hash: str = ""
274
+ current: bool = False
275
+ official_app: bool = False
276
+ unconfirmed: bool = False
277
+ password_pending: bool = False
278
+ app_name: str = ""
279
+ app_version: str = ""
280
+ api_id: int | None = None
281
+ device_model: str = ""
282
+ platform: str = ""
283
+ system_version: str = ""
284
+ ip: str = ""
285
+ country: str = ""
286
+ region: str = ""
287
+ date_created: str | None = None
288
+ date_active: str | None = None
289
+ call_requests_disabled: bool = False
290
+ encrypted_requests_disabled: bool = False
291
+ #: `date_created + authorization_autoconfirm_period`: how long a cron
292
+ #: security check still has to deny an unrecognised login.
293
+ deny_deadline: str | None = None
294
+ #: `date_created + 24 h`. What `SESSION_TOO_FRESH_X` is counting down to.
295
+ sensitive_actions_eligible_at: str | None = None
296
+ #: Account-wide, repeated on every row so one `--json` read answers it.
297
+ ttl_days: int | None = None
298
+ #: A business bot connected to the account, which official clients show
299
+ #: in the same list.
300
+ bot: bool = False
301
+ bot_username: str | None = None
302
+
303
+
304
+ class SessionChange(Model):
305
+ hash: str = ""
306
+ confirmed: bool = False
307
+ already: bool = False
308
+ call_requests_disabled: bool | None = None
309
+ encrypted_requests_disabled: bool | None = None
310
+ authorization_ttl_days: int | None = None
311
+ #: `account session accept-qr` — the authorization that was just created.
312
+ device_model: str | None = None
313
+ app_name: str | None = None
314
+ ip: str | None = None
315
+ country: str | None = None
316
+
317
+
318
+ class SessionTermination(Model):
319
+ terminated: int = 0
320
+ hashes: list[str] = []
321
+ advice: str | None = None
322
+
323
+
324
+ class WebSession(Model):
325
+ """A website or bot logged in through Telegram Login."""
326
+
327
+ hash: str = ""
328
+ bot: int | None = None
329
+ bot_username: str | None = None
330
+ domain: str = ""
331
+ browser: str = ""
332
+ platform: str = ""
333
+ ip: str = ""
334
+ region: str = ""
335
+ date_created: str | None = None
336
+ date_active: str | None = None
337
+
338
+
339
+ class WebSessionRevocation(Model):
340
+ revoked: int = 0
341
+ hashes: list[str] = []
342
+ blocked: list[int] = []
343
+
344
+
345
+ class Passkey(Model):
346
+ id: str = ""
347
+ name: str = ""
348
+ date: str | None = None
349
+ date_unix: int | None = None
350
+ last_usage_date: str | None = None
351
+ software_emoji_id: int | None = None
352
+ deleted: bool = False
353
+
354
+
355
+ # ---------------------------------------------------------------------------
356
+ # The cloud password
357
+ # ---------------------------------------------------------------------------
358
+
359
+
360
+ class PasswordState(Model):
361
+ """2-step verification, as much of it as is safe to print.
362
+
363
+ Deliberately missing: `srp_B`, `srp_id`, `secure_random` and the KDF
364
+ salts. They are live parameters of an in-flight exchange, not status.
365
+ """
366
+
367
+ has_password: bool = False
368
+ has_recovery: bool = False
369
+ has_secure_values: bool = False
370
+ hint: str | None = None
371
+ email_unconfirmed_pattern: str | None = None
372
+ login_email_pattern: str | None = None
373
+ pending_reset_date: str | None = None
374
+ #: Only with the password supplied: `account.getPasswordSettings`.
375
+ recovery_email: str | None = None
376
+ #: Only with `--verify`: did the supplied password check out.
377
+ password_ok: bool | None = None
378
+ #: `PASSWORD_TOO_FRESH_X` counts down to this.
379
+ sensitive_actions_eligible_at: str | None = None
380
+ changed: bool = False
381
+
382
+
383
+ class RecoveryEmail(Model):
384
+ kind: str = "recovery"
385
+ email_pattern: str | None = None
386
+ confirmed: bool = False
387
+ sent_code_length: int | None = None
388
+ cancelled: bool = False
389
+ resent: bool = False
390
+
391
+
392
+ class PasswordReset(Model):
393
+ """`account.resetPassword` — the 7-day path for a password nobody has."""
394
+
395
+ status: str = ""
396
+ until_date: str | None = None
397
+ retry_date: str | None = None
398
+ cancelled: bool = False
399
+
400
+
401
+ class TempPassword(Model):
402
+ tmp_password: str = ""
403
+ valid_until: str | None = None
404
+
405
+
406
+ class PhoneChange(Model):
407
+ phone: str = ""
408
+ code_hash: str = ""
409
+ type: str = ""
410
+ timeout: int | None = None
411
+ changed: bool = False
412
+ confirmed: bool = False
413
+ cancelled: bool = False
414
+ resent: bool = False
415
+
416
+
417
+ class AutologinUrl(Model):
418
+ url: str = ""
419
+ domain_allowed: bool = False
420
+
421
+
422
+ class Suggestion(Model):
423
+ """A server-side nudge (`SETUP_PASSKEY`, `VALIDATE_PASSWORD`, a PSA)."""
424
+
425
+ suggestion: str = ""
426
+ dismissible: bool = True
427
+ dismissed: bool = False
428
+ promo_peer: int | None = None
429
+ psa_type: str | None = None
430
+ hidden: bool = False
431
+
432
+
433
+ class SupportInfo(Model):
434
+ support_user: int | None = None
435
+ support_name: str | None = None
436
+ support_phone: str | None = None
437
+ faq_url: str | None = None
438
+ privacy_url: str | None = None
439
+ features_url: str | None = None
440
+ invite_text: str | None = None
441
+ my_link: str | None = None
442
+ note: str | None = None
443
+ author: str | None = None
444
+ date: str | None = None
445
+ date_unix: int | None = None
446
+
447
+
448
+ class SmsJobs(Model):
449
+ """The Peer-to-Peer Login Program, control side only."""
450
+
451
+ eligible: bool = False
452
+ joined: bool = False
453
+ allow_international: bool = False
454
+ recent_sent: int | None = None
455
+ recent_since: str | None = None
456
+ recent_remains: int | None = None
457
+ terms_url: str | None = None
458
+
459
+
460
+ # ---------------------------------------------------------------------------
461
+ # Passport
462
+ # ---------------------------------------------------------------------------
463
+
464
+
465
+ class PassportValue(Model):
466
+ """One stored Passport document, as metadata.
467
+
468
+ `plain_data` is the only field that can ever be filled without the
469
+ Passport crypto stack: phone and email secure values are stored in the
470
+ clear, every other type is encrypted under a secret derived from the
471
+ cloud password.
472
+ """
473
+
474
+ type: str = ""
475
+ hash: str = ""
476
+ has_files: bool = False
477
+ has_translation: bool = False
478
+ plain_data: str | None = None
479
+
480
+
481
+ class PassportRequirement(Model):
482
+ type: str = ""
483
+ native_names: bool = False
484
+ selfie_required: bool = False
485
+ translation_required: bool = False
486
+
487
+
488
+ class PassportForm(Model):
489
+ """What a service is asking for, and what of it we already hold."""
490
+
491
+ bot: int | None = None
492
+ required_types: list[PassportRequirement] = []
493
+ privacy_policy_url: str | None = None
494
+ values: list[PassportValue] = []
495
+ errors: list[str] = []
496
+ country_language: str | None = None
497
+
498
+
499
+ class PassportVerification(Model):
500
+ target: str = ""
501
+ sent: bool = False
502
+ verified: bool = False
503
+ code_length: int | None = None
504
+ #: Telegram will not accept the code without the hash that came with it,
505
+ #: and the second call is a different process.
506
+ code_hash: str | None = None
507
+
508
+
509
+ class PassportDeletion(Model):
510
+ deleted: list[str] = []
tlgr/models/base.py ADDED
@@ -0,0 +1,81 @@
1
+ """Shared msgspec configuration for every wire shape tlgr emits or accepts.
2
+
3
+ `models/` is the only place a wire shape is defined, and it imports nothing
4
+ else from tlgr — not even the error types — so that `import tlgr.models` works
5
+ without Telethon, without click and without a config file. The import lint in
6
+ `tests/test_layering.py` keeps it that way.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, TypeVar, Union
12
+
13
+ import msgspec
14
+
15
+ __all__ = ["UNSET", "Model", "Request", "Unset", "decode", "encode", "to_builtins"]
16
+
17
+
18
+ class Model(
19
+ msgspec.Struct,
20
+ kw_only=True,
21
+ omit_defaults=True,
22
+ forbid_unknown_fields=False,
23
+ ):
24
+ """A response/domain model.
25
+
26
+ Unknown fields are tolerated on decode: an older client talking to a newer
27
+ daemon must keep working, and dropping a field it does not understand is
28
+ the only forward-compatible answer.
29
+
30
+ `omit_defaults=True` keeps the JSON small and — more importantly — makes
31
+ "absent" meaningful: a field that is missing was not applicable or not
32
+ requested, a field that is `null` is known to be empty.
33
+ """
34
+
35
+
36
+ class Request(
37
+ msgspec.Struct,
38
+ kw_only=True,
39
+ omit_defaults=True,
40
+ forbid_unknown_fields=True,
41
+ ):
42
+ """An operation request.
43
+
44
+ Unknown fields are a USAGE error, the opposite of `Model`: a newer CLI must
45
+ not silently lose a field against an older daemon. The version handshake
46
+ catches the mismatch first; this is the backstop.
47
+ """
48
+
49
+
50
+ UNSET = msgspec.UNSET
51
+ """Sentinel for "the caller did not supply this field at all".
52
+
53
+ Distinct from an explicit `null`, which means "clear it". That tri-state is
54
+ what makes one `edit` operation able to express leave-alone, set and clear
55
+ without three flags per field.
56
+ """
57
+
58
+ _T = TypeVar("_T")
59
+
60
+ # PEP 695's `type Unset[T] = ...` needs 3.12 and we target 3.10.
61
+ Unset = Union[_T, msgspec.UnsetType]
62
+ """`Unset[str]` is "a string, or nothing was supplied".
63
+
64
+ `Unset[str | None]` adds "explicitly cleared" — the `| None` is what makes a
65
+ clear expressible at all, because `Unset[str]` alone rejects a literal null.
66
+ """
67
+
68
+
69
+ def encode(value: Any) -> bytes:
70
+ """Encode any model (or builtin) to compact UTF-8 JSON."""
71
+ return msgspec.json.encode(value)
72
+
73
+
74
+ def decode(data: bytes | str, type: type[_T]) -> _T:
75
+ """Decode JSON into *type*, raising `msgspec.ValidationError` on a mismatch."""
76
+ return msgspec.json.decode(data, type=type)
77
+
78
+
79
+ def to_builtins(value: Any) -> Any:
80
+ """Convert a model tree to plain dicts/lists, honouring `omit_defaults`."""
81
+ return msgspec.to_builtins(value)