maxapi-python 1.2.3__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.
pymax/mixins/auth.py CHANGED
@@ -7,11 +7,23 @@ from typing import Any
7
7
  import qrcode
8
8
 
9
9
  from pymax.exceptions import Error
10
- from pymax.interfaces import ClientProtocol
11
- from pymax.mixins.utils import MixinsUtils
12
- from pymax.payloads import RegisterPayload, RequestCodePayload, SendCodePayload
13
- from pymax.static.constant import PHONE_REGEX
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
14
25
  from pymax.static.enum import AuthType, DeviceType, Opcode
26
+ from pymax.utils import MixinsUtils
15
27
 
16
28
 
17
29
  class AuthMixin(ClientProtocol):
@@ -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/channel.py CHANGED
@@ -1,18 +1,18 @@
1
1
  from pymax.exceptions import Error, ResponseError, ResponseStructureError
2
- from pymax.interfaces import ClientProtocol
3
- from pymax.mixins.utils import MixinsUtils
4
2
  from pymax.payloads import (
5
3
  GetGroupMembersPayload,
6
4
  JoinChatPayload,
7
5
  ResolveLinkPayload,
8
6
  SearchGroupMembersPayload,
9
7
  )
8
+ from pymax.protocols import ClientProtocol
10
9
  from pymax.static.constant import (
11
10
  DEFAULT_CHAT_MEMBERS_LIMIT,
12
11
  DEFAULT_MARKER_VALUE,
13
12
  )
14
13
  from pymax.static.enum import Opcode
15
14
  from pymax.types import Channel, Member
15
+ from pymax.utils import MixinsUtils
16
16
 
17
17
 
18
18
  class ChannelMixin(ClientProtocol):
@@ -113,9 +113,7 @@ class ChannelMixin(ClientProtocol):
113
113
  payload = GetGroupMembersPayload(chat_id=chat_id, marker=marker, count=count)
114
114
  return await self._query_members(payload)
115
115
 
116
- async def find_members(
117
- self, chat_id: int, query: str
118
- ) -> tuple[list[Member], int | None]:
116
+ async def find_members(self, chat_id: int, query: str) -> tuple[list[Member], int | None]:
119
117
  """
120
118
  Поиск участников канала по строке
121
119
  Внимание! веб-клиент всегда возвращает только определённое количество пользователей,
pymax/mixins/group.py CHANGED
@@ -1,8 +1,6 @@
1
1
  import time
2
2
 
3
3
  from pymax.exceptions import Error
4
- from pymax.interfaces import ClientProtocol
5
- from pymax.mixins.utils import MixinsUtils
6
4
  from pymax.payloads import (
7
5
  ChangeGroupProfilePayload,
8
6
  ChangeGroupSettingsOptions,
@@ -18,8 +16,10 @@ from pymax.payloads import (
18
16
  RemoveUsersPayload,
19
17
  ReworkInviteLinkPayload,
20
18
  )
19
+ from pymax.protocols import ClientProtocol
21
20
  from pymax.static.enum import Opcode
22
21
  from pymax.types import Chat, Message
22
+ from pymax.utils import MixinsUtils
23
23
 
24
24
 
25
25
  class GroupMixin(ClientProtocol):
pymax/mixins/handler.py CHANGED
@@ -2,7 +2,7 @@ from collections.abc import Awaitable, Callable
2
2
  from typing import Any
3
3
 
4
4
  from pymax.filters import BaseFilter
5
- from pymax.interfaces import ClientProtocol
5
+ from pymax.protocols import ClientProtocol
6
6
  from pymax.types import Chat, Message, ReactionInfo
7
7
 
8
8
 
@@ -62,9 +62,7 @@ class HandlerMixin(ClientProtocol):
62
62
  handler: Callable[[Any], Any | Awaitable[Any]],
63
63
  ) -> Callable[[Any], Any | Awaitable[Any]]:
64
64
  self._on_message_edit_handlers.append((handler, filter))
65
- self.logger.debug(
66
- f"on_message_edit handler set: {handler}, filter: {filter}"
67
- )
65
+ self.logger.debug(f"on_message_edit handler set: {handler}, filter: {filter}")
68
66
  return handler
69
67
 
70
68
  return decorator
@@ -88,9 +86,7 @@ class HandlerMixin(ClientProtocol):
88
86
  handler: Callable[[Any], Any | Awaitable[Any]],
89
87
  ) -> Callable[[Any], Any | Awaitable[Any]]:
90
88
  self._on_message_delete_handlers.append((handler, filter))
91
- self.logger.debug(
92
- f"on_message_delete handler set: {handler}, filter: {filter}"
93
- )
89
+ self.logger.debug(f"on_message_delete handler set: {handler}, filter: {filter}")
94
90
  return handler
95
91
 
96
92
  return decorator
@@ -179,9 +175,7 @@ class HandlerMixin(ClientProtocol):
179
175
  def decorator(
180
176
  handler: Callable[[], Any | Awaitable[Any]],
181
177
  ) -> Callable[[], Any | Awaitable[Any]]:
182
- self._scheduled_tasks.append(
183
- (handler, seconds + minutes * 60 + hours * 3600)
184
- )
178
+ self._scheduled_tasks.append((handler, seconds + minutes * 60 + hours * 3600))
185
179
  self.logger.debug(
186
180
  f"task scheduled: {handler}, interval: {seconds + minutes * 60 + hours * 3600}s"
187
181
  )