maxapi-python 1.2.5__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.
Files changed (169) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.5.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/protocol/enums.py +180 -0
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.5.dist-info/METADATA +0 -202
  141. maxapi_python-1.2.5.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -398
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -558
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -594
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -306
  158. pymax/mixins/telemetry.py +0 -118
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -151
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -403
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -96
  166. pymax/static/enum.py +0 -231
  167. pymax/types.py +0 -1220
  168. pymax/utils.py +0 -90
  169. {maxapi_python-1.2.5.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())
@@ -0,0 +1,11 @@
1
+ from .enums import Command, Opcode
2
+ from .models import InboundFrame, OutboundFrame, PackedPacket, TcpPacketHeader
3
+
4
+ __all__ = (
5
+ "Command",
6
+ "InboundFrame",
7
+ "Opcode",
8
+ "OutboundFrame",
9
+ "PackedPacket",
10
+ "TcpPacketHeader",
11
+ )
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: ...
@@ -0,0 +1,180 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Command(int, Enum):
5
+ """Низкоуровневый тип frame-а в протоколе Max."""
6
+
7
+ REQUEST = 0
8
+ RESPONSE = 1
9
+ EVENT = 2
10
+ ERROR = 3
11
+
12
+
13
+ class Opcode(int, Enum):
14
+ """Низкоуровневые opcode-ы внутреннего протокола Max."""
15
+
16
+ PING = 1
17
+ DEBUG = 2
18
+ RECONNECT = 3
19
+ LOG = 5
20
+ SESSION_INIT = 6
21
+ PROFILE = 16
22
+ AUTH_REQUEST = 17
23
+ AUTH = 18
24
+ LOGIN = 19
25
+ LOGOUT = 20
26
+ SYNC = 21
27
+ CONFIG = 22
28
+ AUTH_CONFIRM = 23
29
+ PRESET_AVATARS = 25
30
+ ASSETS_GET = 26
31
+ ASSETS_UPDATE = 27
32
+ ASSETS_GET_BY_IDS = 28
33
+ ASSETS_ADD = 29
34
+ SEARCH_FEEDBACK = 31
35
+ CONTACT_INFO = 32
36
+ CONTACT_ADD = 33
37
+ CONTACT_UPDATE = 34
38
+ CONTACT_PRESENCE = 35
39
+ CONTACT_LIST = 36
40
+ CONTACT_SEARCH = 37
41
+ CONTACT_MUTUAL = 38
42
+ CONTACT_PHOTOS = 39
43
+ CONTACT_SORT = 40
44
+ CONTACT_VERIFY = 42
45
+ REMOVE_CONTACT_PHOTO = 43
46
+ CONTACT_INFO_BY_PHONE = 46
47
+ CHAT_INFO = 48
48
+ CHAT_HISTORY = 49
49
+ CHAT_MARK = 50
50
+ CHAT_MEDIA = 51
51
+ CHAT_DELETE = 52
52
+ CHATS_LIST = 53
53
+ CHAT_CLEAR = 54
54
+ CHAT_UPDATE = 55
55
+ CHAT_CHECK_LINK = 56
56
+ CHAT_JOIN = 57
57
+ CHAT_LEAVE = 58
58
+ CHAT_MEMBERS = 59
59
+ PUBLIC_SEARCH = 60
60
+ CHAT_PERSONAL_CONFIG = 61
61
+ CHAT_LIVESTREAM_INFO = 62
62
+ CHAT_CREATE = 63
63
+ MSG_SEND = 64
64
+ MSG_TYPING = 65
65
+ MSG_DELETE = 66
66
+ MSG_EDIT = 67
67
+ CHAT_SEARCH = 68
68
+ MSG_SHARE_PREVIEW = 70
69
+ MSG_GET = 71
70
+ MSG_SEARCH_TOUCH = 72
71
+ MSG_SEARCH = 73
72
+ MSG_GET_STAT = 74
73
+ CHAT_SUBSCRIBE = 75
74
+ VIDEO_CHAT_START = 76
75
+ CHAT_MEMBERS_UPDATE = 77
76
+ VIDEO_CHAT_START_ACTIVE = 78
77
+ VIDEO_CHAT_HISTORY = 79
78
+ PHOTO_UPLOAD = 80
79
+ STICKER_UPLOAD = 81
80
+ VIDEO_UPLOAD = 82
81
+ VIDEO_PLAY = 83
82
+ VIDEO_CHAT_CREATE_JOIN_LINK = 84
83
+ CHAT_PIN_SET_VISIBILITY = 86
84
+ FILE_UPLOAD = 87
85
+ FILE_DOWNLOAD = 88
86
+ LINK_INFO = 89
87
+ MSG_DELETE_RANGE = 92
88
+ SESSIONS_INFO = 96
89
+ SESSIONS_CLOSE = 97
90
+ PHONE_BIND_REQUEST = 98
91
+ PHONE_BIND_CONFIRM = 99
92
+ AUTH_LOGIN_RESTORE_PASSWORD = 101
93
+ GET_INBOUND_CALLS = 103
94
+ AUTH_2FA_DETAILS = 104
95
+ EXTERNAL_CALLBACK = 105
96
+ AUTH_VALIDATE_PASSWORD = 107
97
+ AUTH_VALIDATE_HINT = 108
98
+ AUTH_VERIFY_EMAIL = 109
99
+ AUTH_CHECK_EMAIL = 110
100
+ AUTH_SET_2FA = 111
101
+ AUTH_CREATE_TRACK = 112
102
+ AUTH_CHECK_PASSWORD = 113
103
+ AUTH_LOGIN_CHECK_PASSWORD = 115
104
+ AUTH_LOGIN_PROFILE_DELETE = 116
105
+ CHAT_COMPLAIN = 117
106
+ MSG_SEND_CALLBACK = 118
107
+ SUSPEND_BOT = 119
108
+ LOCATION_STOP = 124
109
+ LOCATION_SEND = 125
110
+ LOCATION_REQUEST = 126
111
+ GET_LAST_MENTIONS = 127
112
+ NOTIF_MESSAGE = 128
113
+ NOTIF_TYPING = 129
114
+ NOTIF_MARK = 130
115
+ NOTIF_CONTACT = 131
116
+ NOTIF_PRESENCE = 132
117
+ NOTIF_CONFIG = 134
118
+ NOTIF_CHAT = 135
119
+ NOTIF_ATTACH = 136
120
+ NOTIF_CALL_START = 137
121
+ NOTIF_CONTACT_SORT = 139
122
+ NOTIF_MSG_DELETE_RANGE = 140
123
+ NOTIF_MSG_DELETE = 142
124
+ NOTIF_CALLBACK_ANSWER = 143
125
+ CHAT_BOT_COMMANDS = 144
126
+ BOT_INFO = 145
127
+ NOTIF_LOCATION = 147
128
+ NOTIF_LOCATION_REQUEST = 148
129
+ NOTIF_ASSETS_UPDATE = 150
130
+ NOTIF_DRAFT = 152
131
+ NOTIF_DRAFT_DISCARD = 153
132
+ NOTIF_MSG_DELAYED = 154
133
+ NOTIF_MSG_REACTIONS_CHANGED = 155
134
+ NOTIF_MSG_YOU_REACTED = 156
135
+ CALLS_TOKEN = 158
136
+ NOTIF_PROFILE = 159
137
+ WEB_APP_INIT_DATA = 160
138
+ COMPLAIN = 161
139
+ COMPLAIN_REASONS_GET = 162
140
+ VIDEO_CHAT_JOIN = 166
141
+ DRAFT_SAVE = 176
142
+ DRAFT_DISCARD = 177
143
+ MSG_REACTION = 178
144
+ MSG_CANCEL_REACTION = 179
145
+ MSG_GET_REACTIONS = 180
146
+ MSG_GET_DETAILED_REACTIONS = 181
147
+ STICKER_CREATE = 193
148
+ STICKER_SUGGEST = 194
149
+ VIDEO_CHAT_MEMBERS = 195
150
+ CHAT_HIDE = 196
151
+ CHAT_SEARCH_COMMON_PARTICIPANTS = 198
152
+ PROFILE_DELETE = 199
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
158
+ ASSETS_REMOVE = 259
159
+ ASSETS_MOVE = 260
160
+ ASSETS_LIST_MODIFY = 261
161
+ FOLDERS_GET = 272
162
+ FOLDERS_GET_BY_ID = 273
163
+ FOLDERS_UPDATE = 274
164
+ FOLDERS_REORDER = 275
165
+ FOLDERS_DELETE = 276
166
+ NOTIF_FOLDERS = 277
167
+
168
+ GET_QR = 288
169
+ GET_QR_STATUS = 289
170
+ AUTH_QR_APPROVE = 290
171
+ LOGIN_BY_QR = 291
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
@@ -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