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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 1.2.4
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=oJEgLVWHjtTPqDd8I7mdHgYxw-Z567FZx-su-iIhVK8,15904
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=ZFmgZ9sK5j3jG8z7DuVovBkXIvPrN4H4rI0UBb2g4BY,19543
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=m0Fn0eEOo6hrdLB6ACh27ioT3SkYIMDMfLKaPbs2vdo,8147
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=nA1fX2ERzk6_WBSrbtBhvwkA5aUXgUdgweytqhZSAJU,14708
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=00uO4-8xer94i6Z6NDTUzzEQbs3NVMH1DB-KoPY6shU,10525
25
- pymax/mixins/telemetry.py,sha256=EAbGyk8EB6QxijaFQ16vUmFPe6l-gEraCxXnAfhA3kY,3594
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=XZ7lE8rKiNd3MXQRlS7Waz8TuoVWms9ldjCOF12pNTw,4891
28
- pymax/static/constant.py,sha256=Gu1j4ibpaZL3tI6fUayS_jkyYOYNe9K-QlTUCOGviwQ,2260
29
- pymax/static/enum.py,sha256=Scyi1pAUaaQlec1YQsU_nvlfxTiQZ6p7gntOJhWfBk4,4773
30
- maxapi_python-1.2.4.dist-info/METADATA,sha256=hVzJYUCzdYWHmbcvAVlNYrEmvLRoWSVxwSbeNCL0Rqc,7069
31
- maxapi_python-1.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
32
- maxapi_python-1.2.4.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
33
- maxapi_python-1.2.4.dist-info/RECORD,,
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
- token = resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token")
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", exc_info=True)
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 RegisterPayload, RequestCodePayload, SendCodePayload
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
- token = login_resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token", "")
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
- self._pending[msg["seq"]] = fut
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
- await self._send_random_navigation()
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
- self._pending[msg["seq"]] = fut
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(msg["seq"], None)
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
@@ -87,3 +87,10 @@ DEFAULT_CHAT_MEMBERS_LIMIT: Final[int] = 50
87
87
  DEFAULT_MARKER_VALUE: Final[int] = 0
88
88
  DEFAULT_PING_INTERVAL: Final[float] = 30.0
89
89
  RECV_LOOP_BACKOFF_DELAY: Final[float] = 0.5
90
+
91
+
92
+ class _Unset:
93
+ pass
94
+
95
+
96
+ UNSET = _Unset()
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