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.
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.5.dist-info}/METADATA +9 -7
- maxapi_python-1.2.5.dist-info/RECORD +33 -0
- pymax/core.py +63 -38
- pymax/files.py +28 -7
- pymax/interfaces.py +417 -116
- pymax/mixins/auth.py +231 -5
- pymax/mixins/channel.py +3 -5
- pymax/mixins/group.py +2 -2
- pymax/mixins/handler.py +4 -10
- pymax/mixins/message.py +64 -88
- pymax/mixins/scheduler.py +1 -1
- pymax/mixins/self.py +2 -2
- pymax/mixins/socket.py +16 -340
- pymax/mixins/telemetry.py +10 -6
- pymax/mixins/user.py +3 -5
- pymax/mixins/websocket.py +16 -365
- pymax/payloads.py +44 -1
- pymax/protocols.py +123 -0
- pymax/static/constant.py +76 -8
- pymax/static/enum.py +65 -52
- pymax/types.py +25 -0
- pymax/utils.py +90 -0
- maxapi_python-1.2.3.dist-info/RECORD +0 -32
- pymax/mixins/utils.py +0 -27
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.5.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.5.dist-info}/licenses/LICENSE +0 -0
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.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
)
|