maxapi-python 1.2.4__py3-none-any.whl → 1.2.5__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-1.2.4.dist-info → maxapi_python-1.2.5.dist-info}/METADATA +1 -4
- {maxapi_python-1.2.4.dist-info → maxapi_python-1.2.5.dist-info}/RECORD +13 -13
- pymax/core.py +9 -1
- pymax/interfaces.py +8 -2
- pymax/mixins/auth.py +229 -3
- pymax/mixins/socket.py +13 -4
- pymax/mixins/telemetry.py +8 -2
- pymax/mixins/websocket.py +11 -2
- pymax/payloads.py +37 -1
- pymax/static/constant.py +7 -0
- pymax/static/enum.py +60 -52
- {maxapi_python-1.2.4.dist-info → maxapi_python-1.2.5.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.4.dist-info → maxapi_python-1.2.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxapi-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: Python wrapper для API мессенджера Max
|
|
5
5
|
Project-URL: Homepage, https://github.com/MaxApiTeam/PyMax
|
|
6
6
|
Project-URL: Repository, https://github.com/MaxApiTeam/PyMax
|
|
@@ -37,9 +37,6 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
<strong>Python wrapper для API мессенджера Max</strong>
|
|
38
38
|
</p>
|
|
39
39
|
|
|
40
|
-
> [!IMPORTANT]
|
|
41
|
-
> (29.12.2025) Снова неожиданное изменение апи, теперь `MaxClient` с `device_type` любым кроме `WEB` не работает, для вохда по номеру телефона используйте `SocketMaxClient`
|
|
42
|
-
|
|
43
40
|
|
|
44
41
|
<p align="center">
|
|
45
42
|
<img src="https://img.shields.io/badge/python-3.10+-3776AB.svg" alt="Python 3.11+">
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
-
pymax/core.py,sha256=
|
|
2
|
+
pymax/core.py,sha256=rJKUFlFjdCPywkOP5-lFUwIMnxNHk4oJLf1VYfEhqak,16137
|
|
3
3
|
pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
|
|
4
4
|
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
5
|
pymax/files.py,sha256=nx7oZfIJ8ZvO-TuG5LzSmk8esbBtNrkKdFQgTQVbUA8,4063
|
|
6
6
|
pymax/filters.py,sha256=gSHPJ1Vi37HKPxf0jRRv9Q3iGwhiQjw1MGrCaouqHzs,4325
|
|
7
7
|
pymax/formatter.py,sha256=RJ_5VbY7Li8UM3xL1AvcXo8v1iYnY8GvDDkreaFqtnY,860
|
|
8
8
|
pymax/formatting.py,sha256=XRtuXJGweuNZevJFdPxksDftIrfuMGEA-AOUc_v6IhQ,2484
|
|
9
|
-
pymax/interfaces.py,sha256=
|
|
9
|
+
pymax/interfaces.py,sha256=GXHi4TjmXPb60KtLXe7CduQ8hSIVHXhA5Ak1OcvSS2w,19793
|
|
10
10
|
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
11
|
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
-
pymax/payloads.py,sha256
|
|
12
|
+
pymax/payloads.py,sha256=-hUgJYwCFfWFcWedAGYg1v82NliEfoTEteEEI7sgUEQ,8756
|
|
13
13
|
pymax/protocols.py,sha256=PoNvri9jFH6WBXGwugrkU6lwtwJEw0DO2s13HOH8_KI,4025
|
|
14
14
|
pymax/types.py,sha256=z1HXNl8CP_X3jTUlENlF9_vzZKdb7gF5PHG5d4rG3BY,37209
|
|
15
15
|
pymax/utils.py,sha256=HK6E6UYyjtUoJ2KXWeDycyiXm_9j5shZme6VFA2ixeM,2960
|
|
16
16
|
pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
|
|
17
|
-
pymax/mixins/auth.py,sha256=
|
|
17
|
+
pymax/mixins/auth.py,sha256=f6IH_gvwB8hLvj7hNI21vgVhR1kju7ABRNjcRS656_o,22843
|
|
18
18
|
pymax/mixins/channel.py,sha256=Qi5ujw5X7QYx4Lq1XnvlJ3BYjmTFmYDfu7_jRcx4Mx8,5335
|
|
19
19
|
pymax/mixins/group.py,sha256=6bsWSx0ULZonDM_dJSC0EkqpZWd6tv9lmmKWj_gEaaw,15903
|
|
20
20
|
pymax/mixins/handler.py,sha256=duL3Q5Bvv8tjUhOKaDr_k7w09BeCwjVdws2ga7v_zNE,12432
|
|
21
21
|
pymax/mixins/message.py,sha256=MpUED92iWONkJRjb0f7lwPj9gYJhDuv0KKzbveOEaAk,33397
|
|
22
22
|
pymax/mixins/scheduler.py,sha256=K4HB9IfksnXPujJnkipIS5um9nuzC8EjbtQn65RtbfI,963
|
|
23
23
|
pymax/mixins/self.py,sha256=Wn9l3zDF5VzFzzisryOQknf3Ngl81Q98_9jqqbE9ZAw,9174
|
|
24
|
-
pymax/mixins/socket.py,sha256=
|
|
25
|
-
pymax/mixins/telemetry.py,sha256=
|
|
24
|
+
pymax/mixins/socket.py,sha256=0WCK9FDzkfgRpKeTfCB9GwuNkccww3RTI7P-W8_409c,10904
|
|
25
|
+
pymax/mixins/telemetry.py,sha256=9gbzEXTp9W9BLJ1wJVXOaHU7Q4inL981b-xq93xJyrw,3913
|
|
26
26
|
pymax/mixins/user.py,sha256=Xwb2fWM8RCq0SbVhlRyr1RBQGyjlaImtp0lT2PbgEqE,9420
|
|
27
|
-
pymax/mixins/websocket.py,sha256=
|
|
28
|
-
pymax/static/constant.py,sha256=
|
|
29
|
-
pymax/static/enum.py,sha256=
|
|
30
|
-
maxapi_python-1.2.
|
|
31
|
-
maxapi_python-1.2.
|
|
32
|
-
maxapi_python-1.2.
|
|
33
|
-
maxapi_python-1.2.
|
|
27
|
+
pymax/mixins/websocket.py,sha256=CvrfMv0iNYpPtfwZXKm0V-ZOlSohtXZ7QH5fDET5Pec,5223
|
|
28
|
+
pymax/static/constant.py,sha256=hZby1gZmzWTzDgwW_EX1NGgqwdQxwcjl7pOE0thfxT0,2304
|
|
29
|
+
pymax/static/enum.py,sha256=YWNzDfPqSBBT8w9m3XesedjiTETn1juJn5UEOZ3zMe0,5477
|
|
30
|
+
maxapi_python-1.2.5.dist-info/METADATA,sha256=BbvzqfDdavSaBRJSDJ0DqxV_f5Nrzs-2-V0LXMlV52o,6790
|
|
31
|
+
maxapi_python-1.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
+
maxapi_python-1.2.5.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
33
|
+
maxapi_python-1.2.5.dist-info/RECORD,,
|
pymax/core.py
CHANGED
|
@@ -252,7 +252,15 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
252
252
|
:rtype: None
|
|
253
253
|
"""
|
|
254
254
|
resp = await self._send_code(code, temp_token)
|
|
255
|
-
|
|
255
|
+
|
|
256
|
+
login_attrs = resp.get("tokenAttrs", {}).get("LOGIN", {})
|
|
257
|
+
password_challenge = resp.get("passwordChallenge")
|
|
258
|
+
|
|
259
|
+
if password_challenge and not login_attrs:
|
|
260
|
+
token = await self._two_factor_auth(password_challenge)
|
|
261
|
+
else:
|
|
262
|
+
token = login_attrs.get("token")
|
|
263
|
+
|
|
256
264
|
if not token:
|
|
257
265
|
raise ValueError("Login response did not contain tokenAttrs.LOGIN.token")
|
|
258
266
|
self._token = token
|
pymax/interfaces.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from typing_extensions import Self
|
|
12
12
|
|
|
13
|
-
from pymax.exceptions import WebSocketNotConnectedError
|
|
13
|
+
from pymax.exceptions import SocketNotConnectedError, WebSocketNotConnectedError
|
|
14
14
|
from pymax.filters import BaseFilter
|
|
15
15
|
from pymax.formatter import ColoredFormatter
|
|
16
16
|
from pymax.payloads import BaseWebSocketMessage, SyncPayload, UserAgentPayload
|
|
@@ -188,6 +188,7 @@ class BaseTransport(ClientProtocol):
|
|
|
188
188
|
self._seq += 1
|
|
189
189
|
|
|
190
190
|
msg = BaseWebSocketMessage(
|
|
191
|
+
ver=11,
|
|
191
192
|
cmd=cmd,
|
|
192
193
|
seq=self._seq,
|
|
193
194
|
opcode=opcode.value,
|
|
@@ -206,8 +207,11 @@ class BaseTransport(ClientProtocol):
|
|
|
206
207
|
cmd=0,
|
|
207
208
|
)
|
|
208
209
|
self.logger.debug("Interactive ping sent successfully")
|
|
210
|
+
except SocketNotConnectedError:
|
|
211
|
+
self.logger.debug("Socket disconnected, exiting ping loop")
|
|
212
|
+
break
|
|
209
213
|
except Exception:
|
|
210
|
-
self.logger.warning("Interactive ping failed"
|
|
214
|
+
self.logger.warning("Interactive ping failed")
|
|
211
215
|
await asyncio.sleep(DEFAULT_PING_INTERVAL)
|
|
212
216
|
|
|
213
217
|
async def _handshake(self, user_agent: UserAgentPayload) -> dict[str, Any]:
|
|
@@ -283,6 +287,8 @@ class BaseTransport(ClientProtocol):
|
|
|
283
287
|
self.logger.debug("Fulfilled file upload waiter for %s=%s", key, id_)
|
|
284
288
|
|
|
285
289
|
async def _send_notification_response(self, chat_id: int, message_id: str) -> None:
|
|
290
|
+
if self._socket is not None and self.is_connected:
|
|
291
|
+
return
|
|
286
292
|
await self._send_and_wait(
|
|
287
293
|
opcode=Opcode.NOTIF_MESSAGE,
|
|
288
294
|
payload={"chatId": chat_id, "messageId": message_id},
|
pymax/mixins/auth.py
CHANGED
|
@@ -7,9 +7,21 @@ from typing import Any
|
|
|
7
7
|
import qrcode
|
|
8
8
|
|
|
9
9
|
from pymax.exceptions import Error
|
|
10
|
-
from pymax.payloads import
|
|
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
|
+
)
|
|
11
23
|
from pymax.protocols import ClientProtocol
|
|
12
|
-
from pymax.static.constant import PHONE_REGEX
|
|
24
|
+
from pymax.static.constant import PHONE_REGEX, UNSET, _Unset
|
|
13
25
|
from pymax.static.enum import AuthType, DeviceType, Opcode
|
|
14
26
|
from pymax.utils import MixinsUtils
|
|
15
27
|
|
|
@@ -194,7 +206,13 @@ class AuthMixin(ClientProtocol):
|
|
|
194
206
|
|
|
195
207
|
login_resp = await self._send_code(code, temp_token)
|
|
196
208
|
|
|
197
|
-
|
|
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")
|
|
198
216
|
|
|
199
217
|
if not token:
|
|
200
218
|
self.logger.critical("Failed to login, token not received")
|
|
@@ -297,6 +315,7 @@ class AuthMixin(ClientProtocol):
|
|
|
297
315
|
self.logger.info("QR login successful")
|
|
298
316
|
|
|
299
317
|
data = await self._get_qr_login_data(track_id)
|
|
318
|
+
|
|
300
319
|
return data
|
|
301
320
|
|
|
302
321
|
else:
|
|
@@ -366,3 +385,210 @@ class AuthMixin(ClientProtocol):
|
|
|
366
385
|
"IMPORTANT: Use this token ONLY with device_type='DESKTOP' and the special init user agent"
|
|
367
386
|
)
|
|
368
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
|
pymax/mixins/socket.py
CHANGED
|
@@ -84,7 +84,7 @@ class SocketMixin(BaseTransport):
|
|
|
84
84
|
) -> bytes:
|
|
85
85
|
ver_b = ver.to_bytes(1, "big")
|
|
86
86
|
cmd_b = cmd.to_bytes(2, "big")
|
|
87
|
-
seq_b = seq.to_bytes(1, "big")
|
|
87
|
+
seq_b = (seq % 256).to_bytes(1, "big")
|
|
88
88
|
opcode_b = opcode.to_bytes(2, "big")
|
|
89
89
|
payload_bytes: bytes | None = msgpack.packb(payload)
|
|
90
90
|
if payload_bytes is None:
|
|
@@ -219,7 +219,7 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
219
219
|
for data_item in datas:
|
|
220
220
|
seq = data_item.get("seq")
|
|
221
221
|
|
|
222
|
-
if self._handle_pending(seq, data_item):
|
|
222
|
+
if self._handle_pending(seq % 256 if seq is not None else None, data_item):
|
|
223
223
|
continue
|
|
224
224
|
|
|
225
225
|
if self._incoming is not None:
|
|
@@ -249,7 +249,13 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
249
249
|
msg = self._make_message(opcode, payload, cmd)
|
|
250
250
|
loop = asyncio.get_running_loop()
|
|
251
251
|
fut: asyncio.Future[dict[str, Any]] = loop.create_future()
|
|
252
|
-
|
|
252
|
+
seq_key = msg["seq"] % 256
|
|
253
|
+
|
|
254
|
+
old_fut = self._pending.get(seq_key)
|
|
255
|
+
if old_fut and not old_fut.done():
|
|
256
|
+
old_fut.cancel()
|
|
257
|
+
|
|
258
|
+
self._pending[seq_key] = fut
|
|
253
259
|
try:
|
|
254
260
|
self.logger.debug(
|
|
255
261
|
"Sending frame opcode=%s cmd=%s seq=%s",
|
|
@@ -282,12 +288,15 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
282
288
|
self.logger.exception("Reconnect failed")
|
|
283
289
|
raise exc from conn_err
|
|
284
290
|
raise SocketNotConnectedError from conn_err
|
|
291
|
+
except asyncio.TimeoutError:
|
|
292
|
+
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
293
|
+
raise SocketSendError from None
|
|
285
294
|
except Exception as exc:
|
|
286
295
|
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
287
296
|
raise SocketSendError from exc
|
|
288
297
|
|
|
289
298
|
finally:
|
|
290
|
-
self._pending.pop(msg["seq"], None)
|
|
299
|
+
self._pending.pop(msg["seq"] % 256, None)
|
|
291
300
|
|
|
292
301
|
@override
|
|
293
302
|
async def _get_chat(self, chat_id: int) -> Chat | None:
|
pymax/mixins/telemetry.py
CHANGED
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
import random
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
|
-
from pymax.exceptions import Error
|
|
5
|
+
from pymax.exceptions import Error, SocketNotConnectedError
|
|
6
6
|
from pymax.navigation import Navigation
|
|
7
7
|
from pymax.payloads import (
|
|
8
8
|
NavigationEventParams,
|
|
@@ -103,7 +103,13 @@ class TelemetryMixin(ClientProtocol):
|
|
|
103
103
|
|
|
104
104
|
try:
|
|
105
105
|
while self.is_connected:
|
|
106
|
-
|
|
106
|
+
try:
|
|
107
|
+
await self._send_random_navigation()
|
|
108
|
+
except SocketNotConnectedError:
|
|
109
|
+
self.logger.debug("Socket disconnected, exiting telemetry task")
|
|
110
|
+
break
|
|
111
|
+
except Exception:
|
|
112
|
+
self.logger.warning("Failed to send random navigation")
|
|
107
113
|
await asyncio.sleep(self._get_random_sleep_time())
|
|
108
114
|
|
|
109
115
|
except asyncio.CancelledError:
|
pymax/mixins/websocket.py
CHANGED
|
@@ -111,7 +111,13 @@ class WebSocketMixin(BaseTransport):
|
|
|
111
111
|
msg = self._make_message(opcode, payload, cmd)
|
|
112
112
|
loop = asyncio.get_running_loop()
|
|
113
113
|
fut: asyncio.Future[dict[str, Any]] = loop.create_future()
|
|
114
|
-
|
|
114
|
+
seq_key = msg["seq"]
|
|
115
|
+
|
|
116
|
+
old_fut = self._pending.get(seq_key)
|
|
117
|
+
if old_fut and not old_fut.done():
|
|
118
|
+
old_fut.cancel()
|
|
119
|
+
|
|
120
|
+
self._pending[seq_key] = fut
|
|
115
121
|
|
|
116
122
|
try:
|
|
117
123
|
self.logger.debug(
|
|
@@ -128,11 +134,14 @@ class WebSocketMixin(BaseTransport):
|
|
|
128
134
|
data.get("opcode"),
|
|
129
135
|
)
|
|
130
136
|
return data
|
|
137
|
+
except asyncio.TimeoutError:
|
|
138
|
+
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
139
|
+
raise RuntimeError("Send and wait failed")
|
|
131
140
|
except Exception:
|
|
132
141
|
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
133
142
|
raise RuntimeError("Send and wait failed")
|
|
134
143
|
finally:
|
|
135
|
-
self._pending.pop(
|
|
144
|
+
self._pending.pop(seq_key, None)
|
|
136
145
|
|
|
137
146
|
@override
|
|
138
147
|
async def _get_chat(self, chat_id: int) -> Chat | None:
|
pymax/payloads.py
CHANGED
|
@@ -15,7 +15,7 @@ from pymax.static.constant import (
|
|
|
15
15
|
DEFAULT_TIMEZONE,
|
|
16
16
|
DEFAULT_USER_AGENT,
|
|
17
17
|
)
|
|
18
|
-
from pymax.static.enum import AttachType, AuthType, ContactAction, ReadAction
|
|
18
|
+
from pymax.static.enum import AttachType, AuthType, Capability, ContactAction, ReadAction
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def to_camel(string: str) -> str:
|
|
@@ -365,3 +365,39 @@ class ReadMessagesPayload(CamelModel):
|
|
|
365
365
|
chat_id: int
|
|
366
366
|
message_id: str
|
|
367
367
|
mark: int
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class CheckPasswordChallengePayload(CamelModel):
|
|
371
|
+
track_id: str
|
|
372
|
+
password: str
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class CreateTrackPayload(CamelModel):
|
|
376
|
+
type: int = 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class SetPasswordPayload(CamelModel):
|
|
380
|
+
track_id: str
|
|
381
|
+
password: str
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class SetHintPayload(CamelModel):
|
|
385
|
+
track_id: str
|
|
386
|
+
hint: str
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class SetTwoFactorPayload(CamelModel):
|
|
390
|
+
expected_capabilities: list[Capability]
|
|
391
|
+
track_id: str
|
|
392
|
+
password: str
|
|
393
|
+
hint: str | None = None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class RequestEmailCodePayload(CamelModel):
|
|
397
|
+
track_id: str
|
|
398
|
+
email: str
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class SendEmailCodePayload(CamelModel):
|
|
402
|
+
track_id: str
|
|
403
|
+
verify_code: str
|
pymax/static/constant.py
CHANGED
pymax/static/enum.py
CHANGED
|
@@ -2,28 +2,28 @@ from enum import Enum
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class Opcode(int, Enum):
|
|
5
|
-
PING = 1
|
|
5
|
+
PING = 1 # ✅
|
|
6
6
|
DEBUG = 2
|
|
7
7
|
RECONNECT = 3
|
|
8
|
-
LOG = 5
|
|
9
|
-
SESSION_INIT = 6
|
|
10
|
-
PROFILE = 16
|
|
11
|
-
AUTH_REQUEST = 17
|
|
12
|
-
AUTH = 18
|
|
13
|
-
LOGIN = 19
|
|
14
|
-
LOGOUT = 20
|
|
8
|
+
LOG = 5 # ✅
|
|
9
|
+
SESSION_INIT = 6 # ✅
|
|
10
|
+
PROFILE = 16 # ✅
|
|
11
|
+
AUTH_REQUEST = 17 # ✅
|
|
12
|
+
AUTH = 18 # ✅
|
|
13
|
+
LOGIN = 19 # ✅
|
|
14
|
+
LOGOUT = 20 # ✅
|
|
15
15
|
SYNC = 21
|
|
16
16
|
CONFIG = 22
|
|
17
|
-
AUTH_CONFIRM = 23
|
|
17
|
+
AUTH_CONFIRM = 23 # ✅
|
|
18
18
|
PRESET_AVATARS = 25
|
|
19
19
|
ASSETS_GET = 26
|
|
20
20
|
ASSETS_UPDATE = 27
|
|
21
21
|
ASSETS_GET_BY_IDS = 28
|
|
22
22
|
ASSETS_ADD = 29
|
|
23
23
|
SEARCH_FEEDBACK = 31
|
|
24
|
-
CONTACT_INFO = 32
|
|
24
|
+
CONTACT_INFO = 32 # ✅
|
|
25
25
|
CONTACT_ADD = 33
|
|
26
|
-
CONTACT_UPDATE = 34
|
|
26
|
+
CONTACT_UPDATE = 34 # ✅
|
|
27
27
|
CONTACT_PRESENCE = 35
|
|
28
28
|
CONTACT_LIST = 36
|
|
29
29
|
CONTACT_SEARCH = 37
|
|
@@ -32,26 +32,26 @@ class Opcode(int, Enum):
|
|
|
32
32
|
CONTACT_SORT = 40
|
|
33
33
|
CONTACT_VERIFY = 42
|
|
34
34
|
REMOVE_CONTACT_PHOTO = 43
|
|
35
|
-
CONTACT_INFO_BY_PHONE = 46
|
|
36
|
-
CHAT_INFO = 48
|
|
37
|
-
CHAT_HISTORY = 49
|
|
38
|
-
CHAT_MARK = 50
|
|
35
|
+
CONTACT_INFO_BY_PHONE = 46 # ✅
|
|
36
|
+
CHAT_INFO = 48 # ✅
|
|
37
|
+
CHAT_HISTORY = 49 # ✅
|
|
38
|
+
CHAT_MARK = 50 # ✅
|
|
39
39
|
CHAT_MEDIA = 51
|
|
40
40
|
CHAT_DELETE = 52
|
|
41
|
-
CHATS_LIST = 53
|
|
41
|
+
CHATS_LIST = 53 # ✅
|
|
42
42
|
CHAT_CLEAR = 54
|
|
43
|
-
CHAT_UPDATE = 55
|
|
43
|
+
CHAT_UPDATE = 55 # ✅
|
|
44
44
|
CHAT_CHECK_LINK = 56
|
|
45
|
-
CHAT_JOIN = 57
|
|
46
|
-
CHAT_LEAVE = 58
|
|
47
|
-
CHAT_MEMBERS = 59
|
|
45
|
+
CHAT_JOIN = 57 # ✅
|
|
46
|
+
CHAT_LEAVE = 58 # ✅
|
|
47
|
+
CHAT_MEMBERS = 59 # ✅
|
|
48
48
|
PUBLIC_SEARCH = 60
|
|
49
49
|
CHAT_CLOSE = 61
|
|
50
50
|
CHAT_CREATE = 63
|
|
51
|
-
MSG_SEND = 64
|
|
51
|
+
MSG_SEND = 64 # ✅
|
|
52
52
|
MSG_TYPING = 65
|
|
53
|
-
MSG_DELETE = 66
|
|
54
|
-
MSG_EDIT = 67
|
|
53
|
+
MSG_DELETE = 66 # ✅
|
|
54
|
+
MSG_EDIT = 67 # ✅
|
|
55
55
|
CHAT_SEARCH = 68
|
|
56
56
|
MSG_SHARE_PREVIEW = 70
|
|
57
57
|
MSG_GET = 71
|
|
@@ -60,31 +60,31 @@ class Opcode(int, Enum):
|
|
|
60
60
|
MSG_GET_STAT = 74
|
|
61
61
|
CHAT_SUBSCRIBE = 75
|
|
62
62
|
VIDEO_CHAT_START = 76
|
|
63
|
-
CHAT_MEMBERS_UPDATE = 77
|
|
63
|
+
CHAT_MEMBERS_UPDATE = 77 # ✅
|
|
64
64
|
VIDEO_CHAT_HISTORY = 79
|
|
65
|
-
PHOTO_UPLOAD = 80
|
|
65
|
+
PHOTO_UPLOAD = 80 # ✅
|
|
66
66
|
STICKER_UPLOAD = 81
|
|
67
|
-
VIDEO_UPLOAD = 82
|
|
68
|
-
VIDEO_PLAY = 83
|
|
67
|
+
VIDEO_UPLOAD = 82 # ✅
|
|
68
|
+
VIDEO_PLAY = 83 # ✅
|
|
69
69
|
CHAT_PIN_SET_VISIBILITY = 86
|
|
70
|
-
FILE_UPLOAD = 87
|
|
71
|
-
FILE_DOWNLOAD = 88
|
|
72
|
-
LINK_INFO = 89
|
|
70
|
+
FILE_UPLOAD = 87 # ✅
|
|
71
|
+
FILE_DOWNLOAD = 88 # ✅
|
|
72
|
+
LINK_INFO = 89 # ✅
|
|
73
73
|
MSG_DELETE_RANGE = 92
|
|
74
|
-
SESSIONS_INFO = 96
|
|
75
|
-
SESSIONS_CLOSE = 97
|
|
74
|
+
SESSIONS_INFO = 96 # ✅
|
|
75
|
+
SESSIONS_CLOSE = 97 # ✅
|
|
76
76
|
PHONE_BIND_REQUEST = 98
|
|
77
77
|
PHONE_BIND_CONFIRM = 99
|
|
78
78
|
CONFIRM_PRESENT = 101
|
|
79
79
|
GET_INBOUND_CALLS = 103
|
|
80
80
|
EXTERNAL_CALLBACK = 105
|
|
81
|
-
AUTH_VALIDATE_PASSWORD = 107
|
|
82
|
-
AUTH_VALIDATE_HINT = 108
|
|
83
|
-
AUTH_VERIFY_EMAIL = 109
|
|
84
|
-
AUTH_CHECK_EMAIL = 110
|
|
85
|
-
AUTH_SET_2FA = 111
|
|
86
|
-
AUTH_CREATE_TRACK = 112
|
|
87
|
-
AUTH_LOGIN_CHECK_PASSWORD = 115
|
|
81
|
+
AUTH_VALIDATE_PASSWORD = 107 # ✅
|
|
82
|
+
AUTH_VALIDATE_HINT = 108 # ✅
|
|
83
|
+
AUTH_VERIFY_EMAIL = 109 # ✅
|
|
84
|
+
AUTH_CHECK_EMAIL = 110 # ✅
|
|
85
|
+
AUTH_SET_2FA = 111 # ✅
|
|
86
|
+
AUTH_CREATE_TRACK = 112 # ✅
|
|
87
|
+
AUTH_LOGIN_CHECK_PASSWORD = 115 # ✅
|
|
88
88
|
CHAT_COMPLAIN = 117
|
|
89
89
|
MSG_SEND_CALLBACK = 118
|
|
90
90
|
SUSPEND_BOT = 119
|
|
@@ -92,14 +92,14 @@ class Opcode(int, Enum):
|
|
|
92
92
|
LOCATION_SEND = 125
|
|
93
93
|
LOCATION_REQUEST = 126
|
|
94
94
|
GET_LAST_MENTIONS = 127
|
|
95
|
-
NOTIF_MESSAGE = 128
|
|
95
|
+
NOTIF_MESSAGE = 128 # ✅
|
|
96
96
|
NOTIF_TYPING = 129
|
|
97
97
|
NOTIF_MARK = 130
|
|
98
98
|
NOTIF_CONTACT = 131
|
|
99
99
|
NOTIF_PRESENCE = 132
|
|
100
100
|
NOTIF_CONFIG = 134
|
|
101
|
-
NOTIF_CHAT = 135
|
|
102
|
-
NOTIF_ATTACH = 136
|
|
101
|
+
NOTIF_CHAT = 135 # ✅
|
|
102
|
+
NOTIF_ATTACH = 136 # ✅
|
|
103
103
|
NOTIF_CALL_START = 137
|
|
104
104
|
NOTIF_CONTACT_SORT = 139
|
|
105
105
|
NOTIF_MSG_DELETE_RANGE = 140
|
|
@@ -113,16 +113,16 @@ class Opcode(int, Enum):
|
|
|
113
113
|
NOTIF_DRAFT = 152
|
|
114
114
|
NOTIF_DRAFT_DISCARD = 153
|
|
115
115
|
NOTIF_MSG_DELAYED = 154
|
|
116
|
-
NOTIF_MSG_REACTIONS_CHANGED = 155
|
|
116
|
+
NOTIF_MSG_REACTIONS_CHANGED = 155 # ✅
|
|
117
117
|
NOTIF_MSG_YOU_REACTED = 156
|
|
118
118
|
CALLS_TOKEN = 158
|
|
119
119
|
NOTIF_PROFILE = 159
|
|
120
120
|
WEB_APP_INIT_DATA = 160
|
|
121
121
|
DRAFT_SAVE = 176
|
|
122
122
|
DRAFT_DISCARD = 177
|
|
123
|
-
MSG_REACTION = 178
|
|
124
|
-
MSG_CANCEL_REACTION = 179
|
|
125
|
-
MSG_GET_REACTIONS = 180
|
|
123
|
+
MSG_REACTION = 178 # ✅
|
|
124
|
+
MSG_CANCEL_REACTION = 179 # ✅
|
|
125
|
+
MSG_GET_REACTIONS = 180 # ✅
|
|
126
126
|
MSG_GET_DETAILED_REACTIONS = 181
|
|
127
127
|
STICKER_CREATE = 193
|
|
128
128
|
STICKER_SUGGEST = 194
|
|
@@ -134,16 +134,16 @@ class Opcode(int, Enum):
|
|
|
134
134
|
ASSETS_REMOVE = 259
|
|
135
135
|
ASSETS_MOVE = 260
|
|
136
136
|
ASSETS_LIST_MODIFY = 261
|
|
137
|
-
FOLDERS_GET = 272
|
|
137
|
+
FOLDERS_GET = 272 # ✅
|
|
138
138
|
FOLDERS_GET_BY_ID = 273
|
|
139
|
-
FOLDERS_UPDATE = 274
|
|
139
|
+
FOLDERS_UPDATE = 274 # ✅
|
|
140
140
|
FOLDERS_REORDER = 275
|
|
141
|
-
FOLDERS_DELETE = 276
|
|
141
|
+
FOLDERS_DELETE = 276 # ✅
|
|
142
142
|
NOTIF_FOLDERS = 277
|
|
143
143
|
|
|
144
|
-
GET_QR = 288
|
|
145
|
-
GET_QR_STATUS = 289
|
|
146
|
-
LOGIN_BY_QR = 291
|
|
144
|
+
GET_QR = 288 # ✅
|
|
145
|
+
GET_QR_STATUS = 289 # ✅
|
|
146
|
+
LOGIN_BY_QR = 291 # ✅
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
class ChatType(str, Enum):
|
|
@@ -221,3 +221,11 @@ class ContactAction(str, Enum):
|
|
|
221
221
|
class ReadAction(str, Enum):
|
|
222
222
|
READ_MESSAGE = "READ_MESSAGE"
|
|
223
223
|
READ_REACTION = "READ_REACTION"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class Capability(int, Enum):
|
|
227
|
+
DEFAULT = 0 # В душе не чаю что это такое но при первой установке 2фа там 0 3 4 так что пусть будет дефолт
|
|
228
|
+
ESIA_VERIFIED_FLAG = 1
|
|
229
|
+
SECOND_FACTOR_PASSWORD_ENABLED = 2
|
|
230
|
+
SECOND_FACTOR_HAS_EMAIL = 3
|
|
231
|
+
SECOND_FACTOR_HAS_HINT = 4
|
|
File without changes
|
|
File without changes
|