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.
- maxapi_python-2.0.0.dist-info/METADATA +217 -0
- maxapi_python-2.0.0.dist-info/RECORD +140 -0
- {maxapi_python-1.2.5.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +50 -105
- pymax/api/__init__.py +17 -0
- pymax/api/auth/__init__.py +1 -0
- pymax/api/auth/enums.py +17 -0
- pymax/api/auth/payloads.py +129 -0
- pymax/api/auth/service.py +313 -0
- pymax/api/auth/types.py +13 -0
- pymax/api/chats/__init__.py +8 -0
- pymax/api/chats/enums.py +27 -0
- pymax/api/chats/payloads.py +103 -0
- pymax/api/chats/service.py +277 -0
- pymax/api/facade.py +32 -0
- pymax/api/messages/__init__.py +1 -0
- pymax/api/messages/enums.py +17 -0
- pymax/api/messages/payloads.py +92 -0
- pymax/api/messages/service.py +337 -0
- pymax/api/models.py +13 -0
- pymax/api/response.py +123 -0
- pymax/api/self/__init__.py +2 -0
- pymax/api/self/enums.py +11 -0
- pymax/api/self/payloads.py +41 -0
- pymax/api/self/service.py +142 -0
- pymax/api/session/__init__.py +1 -0
- pymax/api/session/enums.py +10 -0
- pymax/api/session/payloads.py +76 -0
- pymax/api/session/service.py +72 -0
- pymax/api/uploads/__init__.py +1 -0
- pymax/api/uploads/models.py +49 -0
- pymax/api/uploads/payloads.py +25 -0
- pymax/api/uploads/service.py +458 -0
- pymax/api/users/__init__.py +2 -0
- pymax/api/users/enums.py +12 -0
- pymax/api/users/payloads.py +16 -0
- pymax/api/users/service.py +124 -0
- pymax/app.py +273 -0
- pymax/auth/__init__.py +25 -0
- pymax/auth/base.py +37 -0
- pymax/auth/email.py +0 -0
- pymax/auth/models.py +5 -0
- pymax/auth/providers.py +127 -0
- pymax/auth/qr.py +135 -0
- pymax/auth/service.py +25 -0
- pymax/auth/sms.py +122 -0
- pymax/base.py +204 -0
- pymax/client.py +106 -0
- pymax/client_web.py +83 -0
- pymax/config.py +215 -0
- pymax/connection/__init__.py +1 -0
- pymax/connection/connection.py +205 -0
- pymax/connection/pending.py +46 -0
- pymax/connection/readers/__init__.py +2 -0
- pymax/connection/readers/base.py +6 -0
- pymax/connection/readers/tcp.py +29 -0
- pymax/connection/readers/ws.py +14 -0
- pymax/dispatch/__init__.py +10 -0
- pymax/dispatch/dispatcher.py +222 -0
- pymax/dispatch/enums.py +12 -0
- pymax/dispatch/mapping.py +73 -0
- pymax/dispatch/resolvers.py +52 -0
- pymax/dispatch/router.py +216 -0
- pymax/exceptions.py +22 -89
- pymax/files/__init__.py +9 -0
- pymax/files/base.py +82 -0
- pymax/files/file.py +76 -0
- pymax/files/photo.py +108 -0
- pymax/files/static.py +10 -0
- pymax/files/video.py +74 -0
- pymax/formatting/__init__.py +0 -0
- pymax/formatting/markdown.py +217 -0
- pymax/infra/__init__.py +1 -0
- pymax/infra/auth.py +55 -0
- pymax/infra/base.py +15 -0
- pymax/infra/chat.py +240 -0
- pymax/infra/message.py +252 -0
- pymax/infra/protocol.py +9 -0
- pymax/infra/self.py +139 -0
- pymax/infra/user.py +107 -0
- pymax/logging.py +129 -0
- pymax/protocol/__init__.py +11 -0
- pymax/protocol/base.py +13 -0
- pymax/protocol/enums.py +180 -0
- pymax/protocol/models.py +33 -0
- pymax/protocol/tcp/__init__.py +1 -0
- pymax/protocol/tcp/compression.py +97 -0
- pymax/protocol/tcp/framing.py +68 -0
- pymax/protocol/tcp/payload.py +127 -0
- pymax/protocol/tcp/protocol.py +68 -0
- pymax/protocol/ws/__init__.py +1 -0
- pymax/protocol/ws/protocol.py +27 -0
- pymax/py.typed +0 -0
- pymax/routers.py +8 -0
- pymax/session/__init__.py +3 -0
- pymax/session/models.py +11 -0
- pymax/session/protocol.py +14 -0
- pymax/session/store.py +232 -0
- pymax/telemetry/__init__.py +3 -0
- pymax/telemetry/navigation.py +181 -0
- pymax/telemetry/payloads.py +142 -0
- pymax/telemetry/service.py +225 -0
- pymax/transport/__init__.py +0 -0
- pymax/transport/base.py +14 -0
- pymax/transport/tcp.py +93 -0
- pymax/transport/websocket.py +50 -0
- pymax/types/__init__.py +2 -0
- pymax/types/domain/__init__.py +11 -0
- pymax/types/domain/attachments/__init__.py +11 -0
- pymax/types/domain/attachments/audio.py +35 -0
- pymax/types/domain/attachments/call.py +26 -0
- pymax/types/domain/attachments/contact.py +32 -0
- pymax/types/domain/attachments/control.py +20 -0
- pymax/types/domain/attachments/enums.py +27 -0
- pymax/types/domain/attachments/file.py +56 -0
- pymax/types/domain/attachments/keyboards/__init__.py +1 -0
- pymax/types/domain/attachments/keyboards/inline.py +19 -0
- pymax/types/domain/attachments/photo.py +45 -0
- pymax/types/domain/attachments/share.py +29 -0
- pymax/types/domain/attachments/sticker.py +50 -0
- pymax/types/domain/attachments/video.py +90 -0
- pymax/types/domain/auth.py +161 -0
- pymax/types/domain/base.py +17 -0
- pymax/types/domain/chat.py +426 -0
- pymax/types/domain/element.py +24 -0
- pymax/types/domain/enums.py +24 -0
- pymax/types/domain/error.py +20 -0
- pymax/types/domain/folder.py +74 -0
- pymax/types/domain/login.py +35 -0
- pymax/types/domain/message.py +378 -0
- pymax/types/domain/name.py +20 -0
- pymax/types/domain/profile.py +15 -0
- pymax/types/domain/session.py +52 -0
- pymax/types/domain/sync.py +80 -0
- pymax/types/domain/user.py +117 -0
- pymax/types/events/__init__.py +3 -0
- pymax/types/events/file.py +5 -0
- pymax/types/events/message.py +37 -0
- pymax/types/events/video.py +5 -0
- maxapi_python-1.2.5.dist-info/METADATA +0 -202
- maxapi_python-1.2.5.dist-info/RECORD +0 -33
- pymax/core.py +0 -398
- pymax/crud.py +0 -96
- pymax/files.py +0 -138
- pymax/filters.py +0 -164
- pymax/formatter.py +0 -31
- pymax/formatting.py +0 -74
- pymax/interfaces.py +0 -558
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -594
- pymax/mixins/channel.py +0 -130
- pymax/mixins/group.py +0 -458
- pymax/mixins/handler.py +0 -285
- pymax/mixins/message.py +0 -879
- pymax/mixins/scheduler.py +0 -28
- pymax/mixins/self.py +0 -259
- pymax/mixins/socket.py +0 -306
- pymax/mixins/telemetry.py +0 -118
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -151
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -403
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -96
- pymax/static/enum.py +0 -231
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {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
|