maxapi-python 1.2.5__py3-none-any.whl → 2.0.0__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 (169) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.5.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/protocol/enums.py +180 -0
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.5.dist-info/METADATA +0 -202
  141. maxapi_python-1.2.5.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -398
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -558
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -594
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -306
  158. pymax/mixins/telemetry.py +0 -118
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -151
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -403
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -96
  166. pymax/static/enum.py +0 -231
  167. pymax/types.py +0 -1220
  168. pymax/utils.py +0 -90
  169. {maxapi_python-1.2.5.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/auth.py DELETED
@@ -1,594 +0,0 @@
1
- import asyncio
2
- import datetime
3
- import re
4
- import sys
5
- from typing import Any
6
-
7
- import qrcode
8
-
9
- from pymax.exceptions import Error
10
- from pymax.payloads import (
11
- Capability,
12
- CheckPasswordChallengePayload,
13
- CreateTrackPayload,
14
- RegisterPayload,
15
- RequestCodePayload,
16
- RequestEmailCodePayload,
17
- SendCodePayload,
18
- SendEmailCodePayload,
19
- SetHintPayload,
20
- SetPasswordPayload,
21
- SetTwoFactorPayload,
22
- )
23
- from pymax.protocols import ClientProtocol
24
- from pymax.static.constant import PHONE_REGEX, UNSET, _Unset
25
- from pymax.static.enum import AuthType, DeviceType, Opcode
26
- from pymax.utils import MixinsUtils
27
-
28
-
29
- class AuthMixin(ClientProtocol):
30
- def _check_phone(self) -> bool:
31
- return bool(re.match(PHONE_REGEX, self.phone))
32
-
33
- async def request_code(self, phone: str, language: str = "ru") -> str:
34
- """
35
- Запрашивает код аутентификации для указанного номера телефона и возвращает временный токен.
36
-
37
- Метод отправляет запрос на получение кода верификации на переданный номер телефона.
38
- Используется в процессе аутентификации или регистрации.
39
-
40
- :param phone: Номер телефона в международном формате.
41
- :type phone: str
42
- :param language: Язык для сообщения с кодом. По умолчанию "ru".
43
- :type language: str
44
- :return: Временный токен для дальнейшей аутентификации.
45
- :rtype: str
46
- :raises ValueError: Если полученные данные имеют неверный формат.
47
- :raises Error: Если сервер вернул ошибку.
48
-
49
- .. note::
50
- Используется только в пользовательском flow аутентификации.
51
- """
52
- self.logger.info("Requesting auth code")
53
-
54
- payload = RequestCodePayload(
55
- phone=phone, type=AuthType.START_AUTH, language=language
56
- ).model_dump(by_alias=True)
57
-
58
- data = await self._send_and_wait(opcode=Opcode.AUTH_REQUEST, payload=payload)
59
-
60
- if data.get("payload", {}).get("error"):
61
- MixinsUtils.handle_error(data)
62
-
63
- self.logger.debug(
64
- "Code request response opcode=%s seq=%s",
65
- data.get("opcode"),
66
- data.get("seq"),
67
- )
68
- payload_data = data.get("payload")
69
- if isinstance(payload_data, dict):
70
- return payload_data["token"]
71
- else:
72
- self.logger.error("Invalid payload data received")
73
- raise ValueError("Invalid payload data received")
74
-
75
- async def resend_code(self, phone: str, language: str = "ru") -> str:
76
- """
77
- Повторно запрашивает код аутентификации для указанного номера телефона и возвращает временный токен.
78
-
79
- :param phone: Номер телефона в международном формате.
80
- :type phone: str
81
- :param language: Язык для сообщения с кодом. По умолчанию "ru".
82
- :type language: str
83
- :return: Временный токен для дальнейшей аутентификации.
84
- :rtype: str
85
- :raises ValueError: Если полученные данные имеют неверный формат.
86
- :raises Error: Если сервер вернул ошибку.
87
- """
88
- self.logger.info("Resending auth code")
89
-
90
- payload = RequestCodePayload(
91
- phone=phone, type=AuthType.RESEND, language=language
92
- ).model_dump(by_alias=True)
93
-
94
- data = await self._send_and_wait(opcode=Opcode.AUTH_REQUEST, payload=payload)
95
-
96
- if data.get("payload", {}).get("error"):
97
- MixinsUtils.handle_error(data)
98
-
99
- self.logger.debug(
100
- "Code resend response opcode=%s seq=%s",
101
- data.get("opcode"),
102
- data.get("seq"),
103
- )
104
- payload_data = data.get("payload")
105
- if isinstance(payload_data, dict):
106
- return payload_data["token"]
107
- else:
108
- self.logger.error("Invalid payload data received")
109
- raise ValueError("Invalid payload data received")
110
-
111
- async def _send_code(self, code: str, token: str) -> dict[str, Any]:
112
- """
113
- Отправляет код верификации на сервер для подтверждения.
114
-
115
- :param code: Код верификации (6 цифр).
116
- :type code: str
117
- :param token: Временный токен, полученный из request_code.
118
- :type token: str
119
- :return: Словарь с данными ответа сервера, содержащий токены аутентификации.
120
- :rtype: dict[str, Any]
121
- :raises Error: Если сервер вернул ошибку.
122
- """
123
- self.logger.info("Sending verification code")
124
-
125
- payload = SendCodePayload(
126
- token=token,
127
- verify_code=code,
128
- auth_token_type=AuthType.CHECK_CODE,
129
- ).model_dump(by_alias=True)
130
-
131
- data = await self._send_and_wait(opcode=Opcode.AUTH, payload=payload)
132
-
133
- if data.get("payload", {}).get("error"):
134
- MixinsUtils.handle_error(data)
135
-
136
- self.logger.debug(
137
- "Send code response opcode=%s seq=%s",
138
- data.get("opcode"),
139
- data.get("seq"),
140
- )
141
- payload_data = data.get("payload")
142
- if isinstance(payload_data, dict):
143
- return payload_data
144
- else:
145
- self.logger.error("Invalid payload data received")
146
- raise ValueError("Invalid payload data received")
147
-
148
- def _print_qr(self, qr_link: str) -> None:
149
- qr = qrcode.QRCode(
150
- version=1,
151
- error_correction=qrcode.ERROR_CORRECT_L,
152
- box_size=1,
153
- border=1,
154
- )
155
- qr.add_data(qr_link)
156
- qr.make(fit=True)
157
-
158
- qr.print_ascii()
159
-
160
- async def _request_qr_login(self) -> dict[str, Any]:
161
- self.logger.info("Requesting QR login data")
162
-
163
- data = await self._send_and_wait(opcode=Opcode.GET_QR, payload={})
164
-
165
- if data.get("payload", {}).get("error"):
166
- MixinsUtils.handle_error(data)
167
-
168
- self.logger.debug(
169
- "QR login data response opcode=%s seq=%s",
170
- data.get("opcode"),
171
- data.get("seq"),
172
- )
173
- payload_data = data.get("payload")
174
- if isinstance(payload_data, dict):
175
- return payload_data
176
- else:
177
- self.logger.error("Invalid payload data received")
178
- raise ValueError("Invalid payload data received")
179
-
180
- def _validate_version(self, version: str, min_version: str) -> bool:
181
- def version_tuple(v: str) -> tuple[int, ...]:
182
- return tuple(map(int, (v.split("."))))
183
-
184
- return version_tuple(version) >= version_tuple(min_version)
185
-
186
- async def _login(self) -> None:
187
- self.logger.info("Starting login flow")
188
-
189
- if self.user_agent.device_type == DeviceType.WEB.value and self._ws:
190
- if not self._validate_version(self.user_agent.app_version, "25.12.13"):
191
- self.logger.error("Your app version is too old")
192
- raise ValueError("Your app version is too old")
193
-
194
- login_resp = await self._login_by_qr()
195
- else:
196
- temp_token = await self.request_code(self.phone)
197
- if not temp_token or not isinstance(temp_token, str):
198
- self.logger.critical("Failed to request code: token missing")
199
- raise ValueError("Failed to request code")
200
-
201
- print("Введите код: ", end="", flush=True)
202
- code = await asyncio.to_thread(lambda: sys.stdin.readline().strip())
203
- if len(code) != 6 or not code.isdigit():
204
- self.logger.error("Invalid code format entered")
205
- raise ValueError("Invalid code format")
206
-
207
- login_resp = await self._send_code(code, temp_token)
208
-
209
- password_challenge = login_resp.get("passwordChallenge")
210
- login_attrs = login_resp.get("tokenAttrs", {}).get("LOGIN", {})
211
-
212
- if password_challenge and not login_attrs:
213
- token = await self._two_factor_auth(password_challenge)
214
- else:
215
- token = login_attrs.get("token")
216
-
217
- if not token:
218
- self.logger.critical("Failed to login, token not received")
219
- raise ValueError("Failed to login, token not received")
220
-
221
- self._token = token
222
- self._database.update_auth_token((self._device_id), self._token)
223
- self.logger.info("Login successful, token saved to database")
224
-
225
- async def _poll_qr_login(self, track_id: str, poll_interval: int) -> bool:
226
- self.logger.info("Polling for QR login confirmation")
227
-
228
- while True:
229
- data = await self._send_and_wait(
230
- opcode=Opcode.GET_QR_STATUS,
231
- payload={"trackId": track_id},
232
- )
233
-
234
- payload = data.get("payload", {})
235
-
236
- if payload.get("error"):
237
- MixinsUtils.handle_error(data)
238
- status = payload.get("status")
239
-
240
- if not status:
241
- self.logger.warning("No status in QR login response")
242
- continue
243
-
244
- if status.get("loginAvailable"):
245
- self.logger.info("QR login confirmed")
246
- return True
247
- else:
248
- exp_at = status.get("expiresAt")
249
- if (
250
- exp_at
251
- and isinstance(exp_at, (int, float))
252
- and exp_at < datetime.datetime.now().timestamp() * 1000
253
- ):
254
- self.logger.warning("QR code expired")
255
- return False
256
-
257
- await asyncio.sleep(poll_interval / 1000)
258
-
259
- async def _get_qr_login_data(self, track_id: str) -> dict[str, Any]:
260
- self.logger.info("Getting QR login data")
261
-
262
- data = await self._send_and_wait(
263
- opcode=Opcode.LOGIN_BY_QR,
264
- payload={"trackId": track_id},
265
- )
266
-
267
- self.logger.debug(
268
- "QR login data response opcode=%s seq=%s",
269
- data.get("opcode"),
270
- data.get("seq"),
271
- )
272
- payload_data = data.get("payload")
273
- if isinstance(payload_data, dict):
274
- return payload_data
275
- else:
276
- self.logger.error("Invalid payload data received")
277
- raise ValueError("Invalid payload data received")
278
-
279
- async def _login_by_qr(self) -> dict[str, Any]:
280
- data = await self._request_qr_login()
281
-
282
- poll_interval = data.get("pollingInterval")
283
- link = data.get("qrLink")
284
- track_id = data.get("trackId")
285
- expires_at = data.get("expiresAt")
286
-
287
- if not poll_interval or not link or not track_id or not expires_at:
288
- self.logger.critical("Invalid QR login data received")
289
- raise ValueError("Invalid QR login data received")
290
-
291
- self.logger.info("Starting QR login flow")
292
- self._print_qr(link)
293
-
294
- poll_qr_task = asyncio.create_task(self._poll_qr_login(track_id, poll_interval))
295
-
296
- while True:
297
- now_ms = datetime.datetime.now().timestamp() * 1000
298
-
299
- done, pending = await asyncio.wait(
300
- [poll_qr_task],
301
- timeout=1,
302
- return_when=asyncio.FIRST_COMPLETED,
303
- )
304
-
305
- if now_ms >= expires_at:
306
- poll_qr_task.cancel()
307
- self.logger.error("QR code expired before confirmation")
308
- raise RuntimeError("QR code expired before confirmation")
309
-
310
- if poll_qr_task in done:
311
- exc = poll_qr_task.exception()
312
- if exc is not None:
313
- raise exc
314
- elif poll_qr_task.result():
315
- self.logger.info("QR login successful")
316
-
317
- data = await self._get_qr_login_data(track_id)
318
-
319
- return data
320
-
321
- else:
322
- self.logger.error("QR login failed or expired")
323
- raise RuntimeError("QR login failed or expired")
324
-
325
- async def _submit_reg_info(
326
- self, first_name: str, last_name: str | None, token: str
327
- ) -> dict[str, Any]:
328
- try:
329
- self.logger.info("Submitting registration info")
330
-
331
- payload = RegisterPayload(
332
- first_name=first_name,
333
- last_name=last_name,
334
- token=token,
335
- ).model_dump(by_alias=True)
336
-
337
- data = await self._send_and_wait(opcode=Opcode.AUTH_CONFIRM, payload=payload)
338
- if data.get("payload", {}).get("error"):
339
- MixinsUtils.handle_error(data)
340
-
341
- self.logger.debug(
342
- "Registration info response opcode=%s seq=%s",
343
- data.get("opcode"),
344
- data.get("seq"),
345
- )
346
- payload_data = data.get("payload")
347
- if isinstance(payload_data, dict):
348
- return payload_data
349
- raise ValueError("Invalid payload data received")
350
- except Exception:
351
- self.logger.error("Submit registration info failed", exc_info=True)
352
- raise RuntimeError("Submit registration info failed")
353
-
354
- async def _register(self, first_name: str, last_name: str | None = None) -> None:
355
- self.logger.info("Starting registration flow")
356
-
357
- request_code_payload = await self.request_code(self.phone)
358
- temp_token = request_code_payload
359
-
360
- if not temp_token or not isinstance(temp_token, str):
361
- self.logger.critical("Failed to request code: token missing")
362
- raise ValueError("Failed to request code")
363
-
364
- print("Введите код: ", end="", flush=True)
365
- code = await asyncio.to_thread(lambda: sys.stdin.readline().strip())
366
- if len(code) != 6 or not code.isdigit():
367
- self.logger.error("Invalid code format entered")
368
- raise ValueError("Invalid code format")
369
-
370
- registration_response = await self._send_code(code, temp_token)
371
- token = registration_response.get("tokenAttrs", {}).get("REGISTER", {}).get("token", "")
372
- if not token:
373
- self.logger.critical("Failed to register, token not received")
374
- raise ValueError("Failed to register, token not received")
375
-
376
- data = await self._submit_reg_info(first_name, last_name, token)
377
- self._token = data.get("token")
378
- if not self._token:
379
- self.logger.critical("Failed to register, token not received")
380
- raise ValueError("Failed to register, token not received")
381
-
382
- self.logger.info("Registration successful")
383
- self.logger.info("Token: %s", self._token)
384
- self.logger.warning(
385
- "IMPORTANT: Use this token ONLY with device_type='DESKTOP' and the special init user agent"
386
- )
387
- self.logger.warning("This token MUST NOT be used in web clients")
388
-
389
- async def _check_password(self, password: str, track_id: str) -> dict[str, Any] | None:
390
- payload = CheckPasswordChallengePayload(
391
- track_id=track_id,
392
- password=password,
393
- ).model_dump(by_alias=True)
394
-
395
- data = await self._send_and_wait(opcode=Opcode.AUTH_LOGIN_CHECK_PASSWORD, payload=payload)
396
-
397
- token_attrs = data.get("payload", {}).get("tokenAttrs", {})
398
- if data.get("payload", {}).get("error"):
399
- return None
400
- return token_attrs
401
-
402
- async def _two_factor_auth(self, password_challenge: dict[str, Any]) -> None:
403
- self.logger.info("Starting two-factor authentication flow")
404
-
405
- track_id = password_challenge.get("trackId")
406
- if not track_id:
407
- self.logger.critical("Password challenge missing track ID")
408
- raise ValueError("Password challenge missing track ID")
409
-
410
- hint = password_challenge.get("hint", "No hint provided")
411
-
412
- while True:
413
- password = await asyncio.to_thread(
414
- lambda: input(f"Введите пароль (Подсказка: {hint}): ").strip()
415
- )
416
- if not password:
417
- self.logger.warning("Password is empty, please try again")
418
- continue
419
-
420
- token_attrs = await self._check_password(password, track_id)
421
- if not token_attrs:
422
- self.logger.error("Incorrect password, please try again")
423
- continue
424
-
425
- login_attrs = token_attrs.get("LOGIN", {})
426
- if login_attrs:
427
- token = login_attrs.get("token")
428
- if not token:
429
- self.logger.critical("Login response did not contain tokenAttrs.LOGIN.token")
430
- raise ValueError("Login response did not contain tokenAttrs.LOGIN.token")
431
- return token
432
-
433
- async def _set_password(self, password: str, track_id: str) -> bool:
434
- payload = SetPasswordPayload(
435
- track_id=track_id,
436
- password=password,
437
- ).model_dump(by_alias=True)
438
-
439
- data = await self._send_and_wait(opcode=Opcode.AUTH_VALIDATE_PASSWORD, payload=payload)
440
- payload = data.get("payload", {})
441
- return not payload
442
-
443
- async def _set_hint(self, hint: str, track_id: str) -> bool:
444
- payload = SetHintPayload(
445
- track_id=track_id,
446
- hint=hint,
447
- ).model_dump(by_alias=True)
448
-
449
- data = await self._send_and_wait(opcode=Opcode.AUTH_VALIDATE_HINT, payload=payload)
450
- payload = data.get("payload", {})
451
- return not payload
452
-
453
- async def _set_email(self, email: str, track_id: str) -> bool:
454
- payload = RequestEmailCodePayload(
455
- track_id=track_id,
456
- email=email,
457
- )
458
-
459
- data = await self._send_and_wait(
460
- opcode=Opcode.AUTH_VERIFY_EMAIL,
461
- payload=payload.model_dump(by_alias=True),
462
- )
463
-
464
- if data.get("payload", {}).get("error"):
465
- MixinsUtils.handle_error(data)
466
-
467
- while True:
468
- verify_code = await asyncio.to_thread(
469
- lambda: input(f"Введите код подтверждения, отправленный на {email}: ").strip()
470
- )
471
- if not verify_code:
472
- self.logger.warning("Verification code is empty, please try again")
473
- continue
474
-
475
- payload = SendEmailCodePayload(
476
- track_id=track_id,
477
- verify_code=verify_code,
478
- )
479
-
480
- data = await self._send_and_wait(
481
- opcode=Opcode.AUTH_CHECK_EMAIL,
482
- payload=payload.model_dump(by_alias=True),
483
- )
484
-
485
- if data.get("payload", {}).get("error"):
486
- self.logger.error("Incorrect verification code, please try again")
487
- continue
488
-
489
- return True
490
-
491
- async def set_password(
492
- self,
493
- password: str,
494
- email: str | None = None,
495
- hint: str | None | _Unset = UNSET,
496
- ):
497
- """
498
- Устанавливает пароль для аккаунта
499
-
500
- .. warning::
501
- Метод не будет работать, если на аккаунте уже установлен пароль.
502
-
503
- :param password: Новый пароль для аккаунта.
504
- :type password: str
505
- :param email: Адрес электронной почты для восстановления пароля.
506
- :type email: str
507
- :param hint: Подсказка для пароля. По умолчанию None.
508
- :type hint: str | None
509
- :return: None
510
- :rtype: None
511
- """
512
- self.logger.info("Setting account password")
513
-
514
- payload = CreateTrackPayload().model_dump(by_alias=True)
515
-
516
- data = await self._send_and_wait(
517
- opcode=Opcode.AUTH_CREATE_TRACK,
518
- payload=payload,
519
- )
520
- print(data)
521
- if data.get("payload", {}).get("error"):
522
- MixinsUtils.handle_error(data)
523
-
524
- track_id = data.get("payload", {}).get("trackId")
525
- if not track_id:
526
- self.logger.critical("Failed to create password track: track ID missing")
527
- raise ValueError("Failed to create password track")
528
-
529
- while True:
530
- if not password:
531
- password = await asyncio.to_thread(lambda: input("Введите пароль: ").strip())
532
- if not password:
533
- self.logger.warning("Password is empty, please try again")
534
- continue
535
-
536
- success = await self._set_password(password, track_id)
537
- if success:
538
- self.logger.info("Password set successfully")
539
- break
540
- else:
541
- self.logger.error("Failed to set password, please try again")
542
-
543
- while True:
544
- if hint is UNSET:
545
- hint = await asyncio.to_thread(
546
- lambda: input("Введите подсказку для пароля (пустая - пропустить): ").strip()
547
- )
548
- if not hint:
549
- break
550
-
551
- if hint is None:
552
- break
553
-
554
- success = await self._set_hint(hint, track_id)
555
- if success:
556
- self.logger.info("Password hint set successfully")
557
- break
558
- else:
559
- self.logger.error("Failed to set password hint, please try again")
560
-
561
- while True:
562
- if not email:
563
- email = await asyncio.to_thread(
564
- lambda: input("Введите email для восстановления пароля: ").strip()
565
- )
566
- if not email:
567
- self.logger.warning("Email is empty, please try again")
568
- continue
569
-
570
- success = await self._set_email(email, track_id)
571
- if success:
572
- self.logger.info("Recovery email set successfully")
573
- break
574
-
575
- payload = SetTwoFactorPayload(
576
- expected_capabilities=[
577
- Capability.DEFAULT,
578
- Capability.SECOND_FACTOR_HAS_HINT,
579
- Capability.SECOND_FACTOR_HAS_EMAIL,
580
- ],
581
- track_id=track_id,
582
- password=password,
583
- hint=hint if isinstance(hint, (str, type(None))) else None,
584
- )
585
-
586
- data = await self._send_and_wait(
587
- opcode=Opcode.AUTH_SET_2FA,
588
- payload=payload.model_dump(by_alias=True),
589
- )
590
- payload = data.get("payload", {})
591
- if payload and payload.get("error"):
592
- MixinsUtils.handle_error(data)
593
-
594
- return True