maxapi-python 1.2.4__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.4.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/{static/enum.py → protocol/enums.py} +36 -79
- 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.4.dist-info/METADATA +0 -205
- maxapi_python-1.2.4.dist-info/RECORD +0 -33
- pymax/core.py +0 -390
- 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 -552
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -368
- 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 -297
- pymax/mixins/telemetry.py +0 -112
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -142
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -367
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -89
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
pymax/infra/user.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pymax.types import Session, User
|
|
4
|
+
|
|
5
|
+
from .protocol import IClientProtocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserMixin(IClientProtocol):
|
|
9
|
+
def get_cached_user(self, user_id: int) -> User | None:
|
|
10
|
+
"""Возвращает пользователя из локального кеша без сетевого запроса.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
user_id: ID пользователя.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Контакт из кеша или ``None``, если клиент еще не знает такого
|
|
17
|
+
пользователя.
|
|
18
|
+
"""
|
|
19
|
+
return self._app.api.users.get_cached_user(user_id)
|
|
20
|
+
|
|
21
|
+
async def get_users(self, user_ids: list[int]) -> list[User]:
|
|
22
|
+
"""Возвращает пользователей, беря известные контакты из кеша.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
user_ids: ID пользователей в нужном порядке.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Найденные контакты в порядке ``user_ids``. Недостающие контакты
|
|
29
|
+
будут загружены с сервера и сохранены в кеш.
|
|
30
|
+
"""
|
|
31
|
+
return await self._app.api.users.get_users(user_ids)
|
|
32
|
+
|
|
33
|
+
async def get_user(self, user_id: int) -> User | None:
|
|
34
|
+
"""Возвращает пользователя из кеша или загружает его с сервера.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
user_id: ID пользователя.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Контакт или ``None``, если пользователь не найден.
|
|
41
|
+
"""
|
|
42
|
+
return await self._app.api.users.get_user(user_id)
|
|
43
|
+
|
|
44
|
+
async def fetch_users(self, user_ids: list[int]) -> list[User]:
|
|
45
|
+
"""Загружает пользователей с сервера и обновляет кеш клиента.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
user_ids: ID пользователей.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Контакты, которые вернул сервер.
|
|
52
|
+
"""
|
|
53
|
+
return await self._app.api.users.fetch_users(user_ids)
|
|
54
|
+
|
|
55
|
+
async def search_by_phone(self, phone: str) -> User:
|
|
56
|
+
"""Ищет пользователя Max по номеру телефона.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
phone: Телефон в формате, который принимает Max. Обычно это
|
|
60
|
+
международный формат с кодом страны.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Найденный контакт. Результат сохраняется в кеш клиента.
|
|
64
|
+
"""
|
|
65
|
+
return await self._app.api.users.search_by_phone(phone)
|
|
66
|
+
|
|
67
|
+
async def get_sessions(self) -> list[Session]:
|
|
68
|
+
"""Возвращает активные сессии текущего аккаунта.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Список сессий, известных серверу.
|
|
72
|
+
"""
|
|
73
|
+
return await self._app.api.users.get_sessions()
|
|
74
|
+
|
|
75
|
+
async def add_contact(self, contact_id: int) -> User:
|
|
76
|
+
"""Добавляет пользователя в контакты.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
contact_id: ID пользователя.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Обновленный контакт. Результат сохраняется в кеш клиента.
|
|
83
|
+
"""
|
|
84
|
+
return await self._app.api.users.add_contact(contact_id)
|
|
85
|
+
|
|
86
|
+
async def remove_contact(self, contact_id: int) -> Literal[True]:
|
|
87
|
+
"""Удаляет пользователя из контактов.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
contact_id: ID пользователя.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
``True``, если сервер принял запрос.
|
|
94
|
+
"""
|
|
95
|
+
return await self._app.api.users.remove_contact(contact_id)
|
|
96
|
+
|
|
97
|
+
def get_chat_id(self, first_user_id: int, second_user_id: int) -> int:
|
|
98
|
+
"""Вычисляет ID личного чата для пары пользователей.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
first_user_id: ID первого пользователя.
|
|
102
|
+
second_user_id: ID второго пользователя.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
ID личного чата. Метод работает локально, без запроса к серверу.
|
|
106
|
+
"""
|
|
107
|
+
return self._app.api.users.get_chat_id(first_user_id, second_user_id)
|
pymax/logging.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from typing import TextIO
|
|
5
|
+
|
|
6
|
+
DATE_FORMAT = "%H:%M:%S"
|
|
7
|
+
|
|
8
|
+
RESET = "\x1b[0m"
|
|
9
|
+
DIM = "\x1b[2m"
|
|
10
|
+
BOLD = "\x1b[1m"
|
|
11
|
+
|
|
12
|
+
LEVEL_STYLES = {
|
|
13
|
+
logging.DEBUG: ("\x1b[90m", "DEBUG"),
|
|
14
|
+
logging.INFO: ("\x1b[36m", "INFO"),
|
|
15
|
+
logging.WARNING: ("\x1b[33m", "WARN"),
|
|
16
|
+
logging.ERROR: ("\x1b[31m", "ERROR"),
|
|
17
|
+
logging.CRITICAL: ("\x1b[1;37;41m", "CRIT"),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
LEVELS = {
|
|
21
|
+
"CRITICAL": logging.CRITICAL,
|
|
22
|
+
"FATAL": logging.FATAL,
|
|
23
|
+
"ERROR": logging.ERROR,
|
|
24
|
+
"WARNING": logging.WARNING,
|
|
25
|
+
"WARN": logging.WARNING,
|
|
26
|
+
"INFO": logging.INFO,
|
|
27
|
+
"DEBUG": logging.DEBUG,
|
|
28
|
+
"NOTSET": logging.NOTSET,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PrettyFormatter(logging.Formatter):
|
|
33
|
+
def __init__(self, *, use_colors: bool = True) -> None:
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.use_colors = use_colors
|
|
36
|
+
|
|
37
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
38
|
+
color, level = LEVEL_STYLES.get(record.levelno, ("", "???"))
|
|
39
|
+
|
|
40
|
+
time = self.formatTime(record, DATE_FORMAT)
|
|
41
|
+
message = record.getMessage()
|
|
42
|
+
|
|
43
|
+
line = f"{DIM}{time}{RESET} {color}{BOLD}{level}{RESET} {message}"
|
|
44
|
+
|
|
45
|
+
if record.exc_info:
|
|
46
|
+
line += "\n" + self.formatException(record.exc_info)
|
|
47
|
+
|
|
48
|
+
if not self.use_colors:
|
|
49
|
+
line = _strip_ansi(line)
|
|
50
|
+
|
|
51
|
+
return line
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def configure_logging(
|
|
55
|
+
level: int | str = logging.INFO,
|
|
56
|
+
*,
|
|
57
|
+
stream: TextIO | None = None,
|
|
58
|
+
use_colors: bool | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Настраивает pretty-логи для logger-а ``pymax``.
|
|
61
|
+
|
|
62
|
+
Обычно уровень логов задают через ``ExtraConfig(log_level="DEBUG")``.
|
|
63
|
+
Вызывайте эту функцию вручную, если хотите управлять stream или цветами.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
level: Уровень логирования: строка вроде ``"DEBUG"`` или число из
|
|
67
|
+
модуля ``logging``.
|
|
68
|
+
stream: Поток для вывода. По умолчанию ``sys.stderr``.
|
|
69
|
+
use_colors: Включить ANSI-цвета. Если ``None``, определяется по TTY.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
``None``.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
.. code-block:: python
|
|
76
|
+
|
|
77
|
+
from pymax import configure_logging
|
|
78
|
+
|
|
79
|
+
configure_logging("DEBUG", use_colors=False)
|
|
80
|
+
"""
|
|
81
|
+
stream = stream or sys.stderr
|
|
82
|
+
|
|
83
|
+
if use_colors is None:
|
|
84
|
+
use_colors = hasattr(stream, "isatty") and stream.isatty()
|
|
85
|
+
|
|
86
|
+
logger = logging.getLogger("pymax")
|
|
87
|
+
logger.handlers.clear()
|
|
88
|
+
logger.setLevel(_normalize_level(level))
|
|
89
|
+
logger.propagate = False
|
|
90
|
+
|
|
91
|
+
handler = logging.StreamHandler(stream)
|
|
92
|
+
handler.setLevel(_normalize_level(level))
|
|
93
|
+
handler.setFormatter(
|
|
94
|
+
PrettyFormatter(
|
|
95
|
+
use_colors=use_colors,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
logger.addHandler(handler)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
103
|
+
if not name:
|
|
104
|
+
return logging.getLogger("pymax")
|
|
105
|
+
|
|
106
|
+
if name.startswith("pymax"):
|
|
107
|
+
return logging.getLogger(name)
|
|
108
|
+
|
|
109
|
+
return logging.getLogger(f"pymax.{name}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _normalize_level(level: int | str) -> int:
|
|
113
|
+
if isinstance(level, int):
|
|
114
|
+
return level
|
|
115
|
+
|
|
116
|
+
value = LEVELS.get(level.upper())
|
|
117
|
+
|
|
118
|
+
if isinstance(value, int):
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
raise ValueError(f"Unknown log level: {level}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _strip_ansi(text: str) -> str:
|
|
125
|
+
|
|
126
|
+
return re.sub(r"\x1b\[[0-9;]*m", "", text)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
logging.getLogger("pymax").addHandler(logging.NullHandler())
|
pymax/protocol/base.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from .models import InboundFrame, OutboundFrame
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseProtocol(ABC):
|
|
7
|
+
version: int
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def encode(self, frame: OutboundFrame) -> bytes | str: ...
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def decode(self, raw: bytes | str) -> InboundFrame: ...
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
class Command(int, Enum):
|
|
5
|
+
"""Низкоуровневый тип frame-а в протоколе Max."""
|
|
6
|
+
|
|
7
|
+
REQUEST = 0
|
|
8
|
+
RESPONSE = 1
|
|
9
|
+
EVENT = 2
|
|
10
|
+
ERROR = 3
|
|
11
|
+
|
|
12
|
+
|
|
4
13
|
class Opcode(int, Enum):
|
|
14
|
+
"""Низкоуровневые opcode-ы внутреннего протокола Max."""
|
|
15
|
+
|
|
5
16
|
PING = 1
|
|
6
17
|
DEBUG = 2
|
|
7
18
|
RECONNECT = 3
|
|
@@ -46,7 +57,8 @@ class Opcode(int, Enum):
|
|
|
46
57
|
CHAT_LEAVE = 58
|
|
47
58
|
CHAT_MEMBERS = 59
|
|
48
59
|
PUBLIC_SEARCH = 60
|
|
49
|
-
|
|
60
|
+
CHAT_PERSONAL_CONFIG = 61
|
|
61
|
+
CHAT_LIVESTREAM_INFO = 62
|
|
50
62
|
CHAT_CREATE = 63
|
|
51
63
|
MSG_SEND = 64
|
|
52
64
|
MSG_TYPING = 65
|
|
@@ -61,11 +73,13 @@ class Opcode(int, Enum):
|
|
|
61
73
|
CHAT_SUBSCRIBE = 75
|
|
62
74
|
VIDEO_CHAT_START = 76
|
|
63
75
|
CHAT_MEMBERS_UPDATE = 77
|
|
76
|
+
VIDEO_CHAT_START_ACTIVE = 78
|
|
64
77
|
VIDEO_CHAT_HISTORY = 79
|
|
65
78
|
PHOTO_UPLOAD = 80
|
|
66
79
|
STICKER_UPLOAD = 81
|
|
67
80
|
VIDEO_UPLOAD = 82
|
|
68
81
|
VIDEO_PLAY = 83
|
|
82
|
+
VIDEO_CHAT_CREATE_JOIN_LINK = 84
|
|
69
83
|
CHAT_PIN_SET_VISIBILITY = 86
|
|
70
84
|
FILE_UPLOAD = 87
|
|
71
85
|
FILE_DOWNLOAD = 88
|
|
@@ -75,8 +89,9 @@ class Opcode(int, Enum):
|
|
|
75
89
|
SESSIONS_CLOSE = 97
|
|
76
90
|
PHONE_BIND_REQUEST = 98
|
|
77
91
|
PHONE_BIND_CONFIRM = 99
|
|
78
|
-
|
|
92
|
+
AUTH_LOGIN_RESTORE_PASSWORD = 101
|
|
79
93
|
GET_INBOUND_CALLS = 103
|
|
94
|
+
AUTH_2FA_DETAILS = 104
|
|
80
95
|
EXTERNAL_CALLBACK = 105
|
|
81
96
|
AUTH_VALIDATE_PASSWORD = 107
|
|
82
97
|
AUTH_VALIDATE_HINT = 108
|
|
@@ -84,7 +99,9 @@ class Opcode(int, Enum):
|
|
|
84
99
|
AUTH_CHECK_EMAIL = 110
|
|
85
100
|
AUTH_SET_2FA = 111
|
|
86
101
|
AUTH_CREATE_TRACK = 112
|
|
102
|
+
AUTH_CHECK_PASSWORD = 113
|
|
87
103
|
AUTH_LOGIN_CHECK_PASSWORD = 115
|
|
104
|
+
AUTH_LOGIN_PROFILE_DELETE = 116
|
|
88
105
|
CHAT_COMPLAIN = 117
|
|
89
106
|
MSG_SEND_CALLBACK = 118
|
|
90
107
|
SUSPEND_BOT = 119
|
|
@@ -118,6 +135,9 @@ class Opcode(int, Enum):
|
|
|
118
135
|
CALLS_TOKEN = 158
|
|
119
136
|
NOTIF_PROFILE = 159
|
|
120
137
|
WEB_APP_INIT_DATA = 160
|
|
138
|
+
COMPLAIN = 161
|
|
139
|
+
COMPLAIN_REASONS_GET = 162
|
|
140
|
+
VIDEO_CHAT_JOIN = 166
|
|
121
141
|
DRAFT_SAVE = 176
|
|
122
142
|
DRAFT_DISCARD = 177
|
|
123
143
|
MSG_REACTION = 178
|
|
@@ -131,6 +151,10 @@ class Opcode(int, Enum):
|
|
|
131
151
|
CHAT_SEARCH_COMMON_PARTICIPANTS = 198
|
|
132
152
|
PROFILE_DELETE = 199
|
|
133
153
|
PROFILE_DELETE_TIME = 200
|
|
154
|
+
TRANSCRIBE_MEDIA = 202
|
|
155
|
+
ORG_INFO = 256
|
|
156
|
+
CHAT_REACTIONS_SETTINGS_SET = 257
|
|
157
|
+
REACTIONS_SETTINGS_GET_BY_CHAT_ID = 258
|
|
134
158
|
ASSETS_REMOVE = 259
|
|
135
159
|
ASSETS_MOVE = 260
|
|
136
160
|
ASSETS_LIST_MODIFY = 261
|
|
@@ -143,81 +167,14 @@ class Opcode(int, Enum):
|
|
|
143
167
|
|
|
144
168
|
GET_QR = 288
|
|
145
169
|
GET_QR_STATUS = 289
|
|
170
|
+
AUTH_QR_APPROVE = 290
|
|
146
171
|
LOGIN_BY_QR = 291
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
TEXT = "TEXT"
|
|
157
|
-
SYSTEM = "SYSTEM"
|
|
158
|
-
SERVICE = "SERVICE"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class MessageStatus(str, Enum):
|
|
162
|
-
EDITED = "EDITED"
|
|
163
|
-
REMOVED = "REMOVED"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
class ElementType(str, Enum):
|
|
167
|
-
TEXT = "text"
|
|
168
|
-
MENTION = "mention"
|
|
169
|
-
LINK = "link"
|
|
170
|
-
EMOJI = "emoji"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class AuthType(str, Enum):
|
|
174
|
-
START_AUTH = "START_AUTH"
|
|
175
|
-
CHECK_CODE = "CHECK_CODE"
|
|
176
|
-
REGISTER = "REGISTER"
|
|
177
|
-
RESEND = "RESEND"
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
class AccessType(str, Enum):
|
|
181
|
-
PUBLIC = "PUBLIC"
|
|
182
|
-
PRIVATE = "PRIVATE"
|
|
183
|
-
SECRET = "SECRET" # nosec B105
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class DeviceType(str, Enum):
|
|
187
|
-
WEB = "WEB"
|
|
188
|
-
ANDROID = "ANDROID"
|
|
189
|
-
IOS = "IOS"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
class AttachType(str, Enum):
|
|
193
|
-
PHOTO = "PHOTO"
|
|
194
|
-
VIDEO = "VIDEO"
|
|
195
|
-
FILE = "FILE"
|
|
196
|
-
STICKER = "STICKER"
|
|
197
|
-
AUDIO = "AUDIO"
|
|
198
|
-
CONTROL = "CONTROL"
|
|
199
|
-
CONTACT = "CONTACT"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
class FormattingType(str, Enum):
|
|
203
|
-
STRONG = "STRONG"
|
|
204
|
-
EMPHASIZED = "EMPHASIZED"
|
|
205
|
-
UNDERLINE = "UNDERLINE"
|
|
206
|
-
STRIKETHROUGH = "STRIKETHROUGH"
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
class MarkupType(str, Enum):
|
|
210
|
-
BOLD = "**"
|
|
211
|
-
ITALIC = "*"
|
|
212
|
-
UNDERLINE = "__"
|
|
213
|
-
STRIKETHROUGH = "~~"
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
class ContactAction(str, Enum):
|
|
217
|
-
ADD = "ADD"
|
|
218
|
-
REMOVE = "REMOVE"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
class ReadAction(str, Enum):
|
|
222
|
-
READ_MESSAGE = "READ_MESSAGE"
|
|
223
|
-
READ_REACTION = "READ_REACTION"
|
|
172
|
+
NOTIF_BANNERS = 292
|
|
173
|
+
NOTIF_TRANSCRIPTION = 293
|
|
174
|
+
CHAT_SUGGEST = 300
|
|
175
|
+
AUDIO_PLAY = 301
|
|
176
|
+
BANNERS_GET = 302
|
|
177
|
+
MSG_DELIVERY = 303
|
|
178
|
+
SEND_VOTE = 304
|
|
179
|
+
VOTERS_LIST_BY_ANSWER = 305
|
|
180
|
+
GET_POLL_UPDATES = 306
|
pymax/protocol/models.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OutboundFrame(BaseModel):
|
|
7
|
+
ver: int
|
|
8
|
+
opcode: int
|
|
9
|
+
cmd: int = 0
|
|
10
|
+
seq: int
|
|
11
|
+
payload: dict[Any, Any] | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InboundFrame(BaseModel):
|
|
15
|
+
opcode: int
|
|
16
|
+
cmd: int = 0
|
|
17
|
+
seq: int | None = None
|
|
18
|
+
payload: dict[Any, Any] | None = None
|
|
19
|
+
raw: dict[Any, Any] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TcpPacketHeader(BaseModel):
|
|
23
|
+
ver: int
|
|
24
|
+
cmd: int
|
|
25
|
+
seq: int
|
|
26
|
+
opcode: int
|
|
27
|
+
flags: int = 0
|
|
28
|
+
payload_len: int = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PackedPacket(BaseModel):
|
|
32
|
+
header: TcpPacketHeader
|
|
33
|
+
payload_bytes: bytes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .protocol import TcpProtocol
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
class Lz4BlockCompression:
|
|
2
|
+
def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
|
|
3
|
+
dst = bytearray()
|
|
4
|
+
pos = 0
|
|
5
|
+
|
|
6
|
+
while pos < len(src):
|
|
7
|
+
token = src[pos]
|
|
8
|
+
pos += 1
|
|
9
|
+
|
|
10
|
+
lit_len = token >> 4
|
|
11
|
+
if lit_len == 15:
|
|
12
|
+
while pos < len(src):
|
|
13
|
+
b = src[pos]
|
|
14
|
+
pos += 1
|
|
15
|
+
lit_len += b
|
|
16
|
+
if b != 255:
|
|
17
|
+
break
|
|
18
|
+
|
|
19
|
+
if lit_len > 0:
|
|
20
|
+
if pos + lit_len > len(src):
|
|
21
|
+
raise ValueError("LZ4: literal length out of bounds")
|
|
22
|
+
dst.extend(src[pos : pos + lit_len])
|
|
23
|
+
pos += lit_len
|
|
24
|
+
if len(dst) > max_output:
|
|
25
|
+
raise ValueError("LZ4: output too large")
|
|
26
|
+
|
|
27
|
+
if pos >= len(src):
|
|
28
|
+
break
|
|
29
|
+
|
|
30
|
+
if pos + 1 >= len(src):
|
|
31
|
+
raise ValueError("LZ4: incomplete offset")
|
|
32
|
+
|
|
33
|
+
offset = src[pos] | (src[pos + 1] << 8)
|
|
34
|
+
pos += 2
|
|
35
|
+
|
|
36
|
+
if offset == 0:
|
|
37
|
+
raise ValueError("LZ4: zero offset")
|
|
38
|
+
|
|
39
|
+
match_len = (token & 0x0F) + 4
|
|
40
|
+
if (token & 0x0F) == 0x0F:
|
|
41
|
+
while pos < len(src):
|
|
42
|
+
b = src[pos]
|
|
43
|
+
pos += 1
|
|
44
|
+
match_len += b
|
|
45
|
+
if b != 255:
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
match_pos = len(dst) - offset
|
|
49
|
+
if match_pos < 0:
|
|
50
|
+
raise ValueError("LZ4: match out of bounds")
|
|
51
|
+
|
|
52
|
+
for i in range(match_len):
|
|
53
|
+
dst.append(dst[match_pos + (i % offset)])
|
|
54
|
+
|
|
55
|
+
if len(dst) > max_output:
|
|
56
|
+
raise ValueError("LZ4: output too large")
|
|
57
|
+
|
|
58
|
+
return bytes(dst)
|
|
59
|
+
|
|
60
|
+
def compress(self, src: bytes) -> bytes:
|
|
61
|
+
dst = bytearray()
|
|
62
|
+
pos = 0
|
|
63
|
+
|
|
64
|
+
while pos < len(src):
|
|
65
|
+
lit_start = pos
|
|
66
|
+
while pos < len(src) and (pos - lit_start) < 15:
|
|
67
|
+
pos += 1
|
|
68
|
+
|
|
69
|
+
lit_len = pos - lit_start
|
|
70
|
+
token = (lit_len << 4) & 0xF0
|
|
71
|
+
|
|
72
|
+
match_offset = 0
|
|
73
|
+
match_len = 0
|
|
74
|
+
|
|
75
|
+
for i in range(max(0, lit_start - 65535), lit_start):
|
|
76
|
+
j = i
|
|
77
|
+
k = lit_start
|
|
78
|
+
while j < i + 65535 and k < len(src) and src[j] == src[k]:
|
|
79
|
+
j += 1
|
|
80
|
+
k += 1
|
|
81
|
+
if j - i > match_len:
|
|
82
|
+
match_offset = lit_start - i
|
|
83
|
+
match_len = j - i
|
|
84
|
+
|
|
85
|
+
if match_len >= 4:
|
|
86
|
+
token |= (match_len - 4) & 0x0F
|
|
87
|
+
dst.append(token)
|
|
88
|
+
dst.extend(src[lit_start : lit_start + lit_len])
|
|
89
|
+
dst.append(match_offset & 0xFF)
|
|
90
|
+
dst.append((match_offset >> 8) & 0xFF)
|
|
91
|
+
pos += match_len
|
|
92
|
+
else:
|
|
93
|
+
token |= lit_len & 0x0F
|
|
94
|
+
dst.append(token)
|
|
95
|
+
dst.extend(src[lit_start : lit_start + lit_len])
|
|
96
|
+
|
|
97
|
+
return bytes(dst)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
|
|
3
|
+
from pymax.protocol import PackedPacket, TcpPacketHeader
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TcpPacketFramer:
|
|
7
|
+
HEADER_STRUCT = struct.Struct(">BHBHI")
|
|
8
|
+
HEADER_SIZE = HEADER_STRUCT.size
|
|
9
|
+
|
|
10
|
+
def _pack_cmd(self, cmd: int) -> int:
|
|
11
|
+
return (cmd & 0xFF) << 8
|
|
12
|
+
|
|
13
|
+
def _unpack_cmd(self, packed_cmd: int) -> int:
|
|
14
|
+
return (packed_cmd >> 8) & 0xFF
|
|
15
|
+
|
|
16
|
+
def pack(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
ver: int,
|
|
20
|
+
cmd: int,
|
|
21
|
+
seq: int,
|
|
22
|
+
opcode: int,
|
|
23
|
+
flags: int,
|
|
24
|
+
payload_bytes: bytes,
|
|
25
|
+
) -> bytes:
|
|
26
|
+
packed_len = ((flags & 0xFF) << 24) | (len(payload_bytes) & 0x00FFFFFF)
|
|
27
|
+
header = self.HEADER_STRUCT.pack(
|
|
28
|
+
ver,
|
|
29
|
+
self._pack_cmd(cmd),
|
|
30
|
+
seq,
|
|
31
|
+
opcode,
|
|
32
|
+
packed_len,
|
|
33
|
+
)
|
|
34
|
+
return header + payload_bytes
|
|
35
|
+
|
|
36
|
+
def unpack(self, data: bytes) -> PackedPacket | None:
|
|
37
|
+
if len(data) < self.HEADER_SIZE:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(data, 0)
|
|
41
|
+
flags = (packed_len >> 24) & 0xFF
|
|
42
|
+
payload_len = packed_len & 0x00FFFFFF
|
|
43
|
+
|
|
44
|
+
total_len = self.HEADER_SIZE + payload_len
|
|
45
|
+
if len(data) < total_len:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
return PackedPacket(
|
|
49
|
+
header=TcpPacketHeader(
|
|
50
|
+
ver=ver,
|
|
51
|
+
cmd=self._unpack_cmd(cmd),
|
|
52
|
+
seq=seq,
|
|
53
|
+
opcode=opcode,
|
|
54
|
+
flags=flags,
|
|
55
|
+
payload_len=payload_len,
|
|
56
|
+
),
|
|
57
|
+
payload_bytes=data[self.HEADER_SIZE : total_len],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def unpack_header(self, data: bytes) -> int | None:
|
|
61
|
+
if len(data) < self.HEADER_SIZE:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
_, _, _, _, packed_len = self.HEADER_STRUCT.unpack_from(data, 0)
|
|
65
|
+
flags = (packed_len >> 24) & 0xFF
|
|
66
|
+
payload_len = packed_len & 0x00FFFFFF
|
|
67
|
+
|
|
68
|
+
return payload_len
|