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.
Files changed (168) 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.4.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/{static/enum.py → protocol/enums.py} +36 -79
  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.4.dist-info/METADATA +0 -205
  141. maxapi_python-1.2.4.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -390
  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 -552
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -368
  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 -297
  158. pymax/mixins/telemetry.py +0 -112
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -142
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -367
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -89
  166. pymax/types.py +0 -1220
  167. pymax/utils.py +0 -90
  168. {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())
@@ -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: ...
@@ -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
- CHAT_CLOSE = 61
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
- CONFIRM_PRESENT = 101
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
- class ChatType(str, Enum):
150
- DIALOG = "DIALOG"
151
- CHAT = "CHAT"
152
- CHANNEL = "CHANNEL"
153
-
154
-
155
- class MessageType(str, Enum):
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
@@ -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