maxapi-python 2.0.0__py3-none-any.whl → 2.1.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 → maxapi_python-2.1.0.dist-info}/METADATA +4 -1
- {maxapi_python-2.0.0.dist-info → maxapi_python-2.1.0.dist-info}/RECORD +61 -54
- pymax/__init__.py +1 -1
- pymax/api/auth/enums.py +13 -2
- pymax/api/auth/payloads.py +10 -6
- pymax/api/auth/service.py +75 -12
- pymax/api/bots/__init__.py +1 -0
- pymax/api/bots/payloads.py +7 -0
- pymax/api/bots/service.py +35 -0
- pymax/api/chats/enums.py +1 -0
- pymax/api/chats/payloads.py +14 -0
- pymax/api/chats/service.py +112 -12
- pymax/api/facade.py +2 -0
- pymax/api/messages/payloads.py +5 -1
- pymax/api/messages/service.py +36 -11
- pymax/api/self/service.py +18 -6
- pymax/api/session/payloads.py +9 -2
- pymax/api/uploads/models.py +1 -4
- pymax/api/uploads/payloads.py +9 -3
- pymax/api/uploads/service.py +103 -35
- pymax/api/users/service.py +15 -5
- pymax/app.py +15 -5
- pymax/auth/qr.py +11 -6
- pymax/auth/sms.py +13 -4
- pymax/base.py +1 -0
- pymax/client.py +4 -1
- pymax/client_web.py +4 -2
- pymax/config.py +11 -3
- pymax/connection/connection.py +15 -5
- pymax/connection/readers/tcp.py +4 -2
- pymax/dispatch/dispatcher.py +28 -10
- pymax/dispatch/mapping.py +11 -4
- pymax/dispatch/router.py +2 -0
- pymax/files/base.py +6 -1
- pymax/formatting/markdown.py +4 -1
- pymax/infra/auth.py +42 -0
- pymax/infra/base.py +2 -0
- pymax/infra/bots.py +33 -0
- pymax/infra/chat.py +102 -1
- pymax/protocol/tcp/compression.py +3 -1
- pymax/protocol/tcp/framing.py +6 -11
- pymax/protocol/tcp/payload.py +3 -2
- pymax/protocol/tcp/protocol.py +13 -3
- pymax/protocol/ws/protocol.py +9 -3
- pymax/session/protocol.py +6 -2
- pymax/session/store.py +24 -8
- pymax/telemetry/navigation.py +3 -1
- pymax/telemetry/service.py +9 -3
- pymax/transport/tcp.py +10 -4
- pymax/transport/websocket.py +0 -2
- pymax/types/domain/__init__.py +3 -0
- pymax/types/domain/bots.py +14 -0
- pymax/types/domain/error.py +3 -3
- pymax/types/domain/folder.py +1 -1
- pymax/types/domain/login.py +18 -6
- pymax/types/domain/member.py +16 -0
- pymax/types/domain/presence.py +15 -0
- pymax/types/domain/sync.py +21 -5
- pymax/types/domain/user.py +12 -0
- {maxapi_python-2.0.0.dist-info → maxapi_python-2.1.0.dist-info}/WHEEL +0 -0
- {maxapi_python-2.0.0.dist-info → maxapi_python-2.1.0.dist-info}/licenses/LICENSE +0 -0
pymax/dispatch/dispatcher.py
CHANGED
|
@@ -9,7 +9,6 @@ from pymax.logging import get_logger
|
|
|
9
9
|
from pymax.protocol import InboundFrame
|
|
10
10
|
from pymax.types import Chat, MessageDeleteEvent
|
|
11
11
|
from pymax.types.domain import Message
|
|
12
|
-
from pymax.types.events import FileUploadSignal, VideoUploadSignal
|
|
13
12
|
|
|
14
13
|
from .enums import EventType
|
|
15
14
|
from .mapping import EventMapper, EventResolver
|
|
@@ -26,7 +25,6 @@ if TYPE_CHECKING:
|
|
|
26
25
|
from collections.abc import Generator
|
|
27
26
|
|
|
28
27
|
from pymax.app import App
|
|
29
|
-
from pymax.client import Client
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
logger = get_logger(__name__)
|
|
@@ -35,7 +33,9 @@ ClientT = TypeVar("ClientT")
|
|
|
35
33
|
|
|
36
34
|
|
|
37
35
|
class Dispatcher(Generic[ClientT]):
|
|
38
|
-
def __init__(
|
|
36
|
+
def __init__(
|
|
37
|
+
self, app: App, root_router: Router[ClientT] | None = None
|
|
38
|
+
) -> None:
|
|
39
39
|
self.root_router: Router[ClientT] = root_router or Router()
|
|
40
40
|
self.internal_router: Router[ClientT] = Router()
|
|
41
41
|
self.resolver = EventResolver()
|
|
@@ -59,7 +59,11 @@ class Dispatcher(Generic[ClientT]):
|
|
|
59
59
|
event: EventType,
|
|
60
60
|
*filters: FilterCallback[Any],
|
|
61
61
|
) -> HandlerDecorator[Any, ClientT]:
|
|
62
|
-
logger.debug(
|
|
62
|
+
logger.debug(
|
|
63
|
+
"registering internal handler event=%s filters=%s",
|
|
64
|
+
event,
|
|
65
|
+
len(filters),
|
|
66
|
+
)
|
|
63
67
|
return self.internal_router.on(event, *filters)
|
|
64
68
|
|
|
65
69
|
def on(
|
|
@@ -67,7 +71,9 @@ class Dispatcher(Generic[ClientT]):
|
|
|
67
71
|
event: EventType,
|
|
68
72
|
*filters: FilterCallback[Any],
|
|
69
73
|
) -> HandlerDecorator[Any, ClientT]:
|
|
70
|
-
logger.debug(
|
|
74
|
+
logger.debug(
|
|
75
|
+
"registering handler event=%s filters=%s", event, len(filters)
|
|
76
|
+
)
|
|
71
77
|
return self.root_router.on(event, *filters)
|
|
72
78
|
|
|
73
79
|
def on_message(
|
|
@@ -81,7 +87,9 @@ class Dispatcher(Generic[ClientT]):
|
|
|
81
87
|
self,
|
|
82
88
|
*filters: FilterCallback[Message],
|
|
83
89
|
) -> HandlerDecorator[Message, ClientT]:
|
|
84
|
-
logger.debug(
|
|
90
|
+
logger.debug(
|
|
91
|
+
"registering message edit handler filters=%s", len(filters)
|
|
92
|
+
)
|
|
85
93
|
return self.root_router.on_message_edit(*filters)
|
|
86
94
|
|
|
87
95
|
def on_message_delete(
|
|
@@ -108,7 +116,9 @@ class Dispatcher(Generic[ClientT]):
|
|
|
108
116
|
def iter_routers(self) -> Generator[Router[ClientT], Any, None]:
|
|
109
117
|
yield from self._iter_router(self.root_router)
|
|
110
118
|
|
|
111
|
-
def _iter_router(
|
|
119
|
+
def _iter_router(
|
|
120
|
+
self, router: Router[ClientT]
|
|
121
|
+
) -> Generator[Router[ClientT], Any, None]:
|
|
112
122
|
yield router
|
|
113
123
|
|
|
114
124
|
for child in router.children:
|
|
@@ -151,10 +161,16 @@ class Dispatcher(Generic[ClientT]):
|
|
|
151
161
|
if event_type is not None:
|
|
152
162
|
logger.debug("dispatching event type=%s", event_type)
|
|
153
163
|
event = self.mapper.map(event_type, frame)
|
|
154
|
-
await self._dispatch_to_router(
|
|
164
|
+
await self._dispatch_to_router(
|
|
165
|
+
self.internal_router, event_type, event
|
|
166
|
+
)
|
|
155
167
|
await self._dispatch_to_router(self.root_router, event_type, event)
|
|
156
168
|
else:
|
|
157
|
-
logger.debug(
|
|
169
|
+
logger.debug(
|
|
170
|
+
"dispatching raw event only opcode=%s cmd=%s",
|
|
171
|
+
frame.opcode,
|
|
172
|
+
frame.cmd,
|
|
173
|
+
)
|
|
158
174
|
|
|
159
175
|
await self._dispatch_to_router(self.root_router, EventType.RAW, frame)
|
|
160
176
|
|
|
@@ -193,7 +209,9 @@ class Dispatcher(Generic[ClientT]):
|
|
|
193
209
|
return False
|
|
194
210
|
return True
|
|
195
211
|
|
|
196
|
-
async def _call(
|
|
212
|
+
async def _call(
|
|
213
|
+
self, callback: HandlerCallback[Any, ClientT], event: Any
|
|
214
|
+
) -> Any:
|
|
197
215
|
if self.client is None:
|
|
198
216
|
raise RuntimeError("client is not bound")
|
|
199
217
|
|
pymax/dispatch/mapping.py
CHANGED
|
@@ -10,7 +10,12 @@ from pymax.types.domain import Message
|
|
|
10
10
|
from pymax.types.events import FileUploadSignal, VideoUploadSignal
|
|
11
11
|
|
|
12
12
|
from .enums import EventType
|
|
13
|
-
from .resolvers import
|
|
13
|
+
from .resolvers import (
|
|
14
|
+
resolve_attach,
|
|
15
|
+
resolve_chat,
|
|
16
|
+
resolve_message,
|
|
17
|
+
resolve_message_delete,
|
|
18
|
+
)
|
|
14
19
|
|
|
15
20
|
if TYPE_CHECKING:
|
|
16
21
|
from pymax.app import App
|
|
@@ -28,7 +33,7 @@ EVENT_MAP: dict[Opcode, Resolver] = {
|
|
|
28
33
|
|
|
29
34
|
class EventResolver:
|
|
30
35
|
def resolve(self, frame: InboundFrame) -> EventType | None:
|
|
31
|
-
if frame.cmd != Command.
|
|
36
|
+
if frame.cmd != Command.REQUEST:
|
|
32
37
|
return None
|
|
33
38
|
|
|
34
39
|
try:
|
|
@@ -48,12 +53,14 @@ class EventMapper:
|
|
|
48
53
|
self.app = app
|
|
49
54
|
|
|
50
55
|
def map(self, event_type: EventType, frame: InboundFrame):
|
|
51
|
-
if frame.cmd != Command.
|
|
56
|
+
if frame.cmd != Command.REQUEST:
|
|
52
57
|
return None
|
|
53
58
|
|
|
54
59
|
if frame.payload:
|
|
55
60
|
if event_type in (EventType.MESSAGE_NEW, EventType.MESSAGE_EDIT):
|
|
56
|
-
return Message.model_validate(frame.payload).bind(
|
|
61
|
+
return Message.model_validate(frame.payload).bind(
|
|
62
|
+
self.app.api.messages
|
|
63
|
+
)
|
|
57
64
|
elif event_type == EventType.CHAT_UPDATE:
|
|
58
65
|
return Chat.model_validate(frame.payload["chat"]).bind(
|
|
59
66
|
self.app.api.messages,
|
pymax/dispatch/router.py
CHANGED
|
@@ -106,6 +106,7 @@ class Router(Generic[ClientT]):
|
|
|
106
106
|
async def raw(frame: InboundFrame, client: Client) -> None:
|
|
107
107
|
print(frame.payload)
|
|
108
108
|
"""
|
|
109
|
+
|
|
109
110
|
def decorator(
|
|
110
111
|
handler: HandlerCallback[_EventT, ClientT],
|
|
111
112
|
) -> HandlerCallback[_EventT, ClientT]:
|
|
@@ -136,6 +137,7 @@ class Router(Generic[ClientT]):
|
|
|
136
137
|
Returns:
|
|
137
138
|
Декоратор для ``handler(client)``.
|
|
138
139
|
"""
|
|
140
|
+
|
|
139
141
|
def decorator(handler: StartCallback) -> StartCallback:
|
|
140
142
|
self.on_start_handler = handler
|
|
141
143
|
return handler
|
pymax/files/base.py
CHANGED
|
@@ -8,7 +8,12 @@ import aiohttp
|
|
|
8
8
|
|
|
9
9
|
class BaseFile(ABC):
|
|
10
10
|
def __init__(
|
|
11
|
-
self,
|
|
11
|
+
self,
|
|
12
|
+
raw: bytes | None = None,
|
|
13
|
+
*,
|
|
14
|
+
path: str | None,
|
|
15
|
+
url: str | None,
|
|
16
|
+
name: str | None,
|
|
12
17
|
) -> None:
|
|
13
18
|
self.path = path
|
|
14
19
|
self.url = url
|
pymax/formatting/markdown.py
CHANGED
|
@@ -151,7 +151,10 @@ class Formatter:
|
|
|
151
151
|
if marker == "```":
|
|
152
152
|
closing_index = text.find(marker, i + marker_len)
|
|
153
153
|
|
|
154
|
-
if
|
|
154
|
+
if (
|
|
155
|
+
closing_index == -1
|
|
156
|
+
or closing_index == i + marker_len
|
|
157
|
+
):
|
|
155
158
|
clean_text += marker
|
|
156
159
|
clean_pos += marker_len
|
|
157
160
|
i += marker_len
|
pymax/infra/auth.py
CHANGED
|
@@ -53,3 +53,45 @@ class AuthMixin(IClientProtocol):
|
|
|
53
53
|
RuntimeError: Если удаление пароля не удалось.
|
|
54
54
|
"""
|
|
55
55
|
return await self._app.api.auth.remove_2fa(password=password)
|
|
56
|
+
|
|
57
|
+
async def change_password(
|
|
58
|
+
self,
|
|
59
|
+
password_old: str,
|
|
60
|
+
password_new: str,
|
|
61
|
+
) -> bool:
|
|
62
|
+
"""Меняет пароль 2FA для текущей учетной записи.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
password_old: Текущий пароль 2FA.
|
|
66
|
+
password_new: Новый пароль 2FA.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
``True``, если пароль успешно изменен.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
RuntimeError: Если изменение пароля не удалось.
|
|
73
|
+
"""
|
|
74
|
+
return await self._app.api.auth.change_password(
|
|
75
|
+
password_old=password_old,
|
|
76
|
+
password_new=password_new,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async def authorize_qr_login(self, qr_link: str) -> bool:
|
|
80
|
+
"""Авторизует вход по QR-коду.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
qr_link: Ссылка на QR-код для авторизации.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
``True``, если вход по QR-коду успешно авторизован.
|
|
87
|
+
"""
|
|
88
|
+
return await self._app.api.auth.authorize_qr_login(qr_link=qr_link)
|
|
89
|
+
|
|
90
|
+
async def check_2fa(self) -> bool:
|
|
91
|
+
"""Проверяет, включена ли 2FA для текущей учетной записи.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
``True``, если на аккаунте установлен пароль 2FA.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
return await self._app.api.auth.check_2fa()
|
pymax/infra/base.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .auth import AuthMixin
|
|
2
|
+
from .bots import BotsMixin
|
|
2
3
|
from .chat import ChatMixin
|
|
3
4
|
from .message import MessageMixin
|
|
4
5
|
from .self import SelfMixin
|
|
@@ -10,6 +11,7 @@ class BaseMixin(
|
|
|
10
11
|
UserMixin,
|
|
11
12
|
ChatMixin,
|
|
12
13
|
MessageMixin,
|
|
14
|
+
BotsMixin,
|
|
13
15
|
AuthMixin,
|
|
14
16
|
):
|
|
15
17
|
"""Собирает публичные API-методы клиента."""
|
pymax/infra/bots.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pymax.types.domain import InitData
|
|
2
|
+
|
|
3
|
+
from .protocol import IClientProtocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BotsMixin(IClientProtocol):
|
|
7
|
+
"""Методы клиента для взаимодействия с ботами."""
|
|
8
|
+
|
|
9
|
+
async def get_bot_init_data(
|
|
10
|
+
self,
|
|
11
|
+
bot_id: int,
|
|
12
|
+
chat_id: int,
|
|
13
|
+
start_param: str | None = None,
|
|
14
|
+
) -> InitData:
|
|
15
|
+
"""Получает начальные данные для бота в контексте конкретного чата.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
bot_id: Идентификатор бота.
|
|
19
|
+
chat_id: Идентификатор чата, в котором бот будет использоваться.
|
|
20
|
+
start_param: Необязательный параметр, передаваемый при запуске
|
|
21
|
+
бота.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Объект с начальными данными для бота.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
RuntimeError: Если получение данных не удалось.
|
|
28
|
+
"""
|
|
29
|
+
return await self._app.api.bots.get_init_data(
|
|
30
|
+
bot_id=bot_id,
|
|
31
|
+
chat_id=chat_id,
|
|
32
|
+
start_param=start_param,
|
|
33
|
+
)
|
pymax/infra/chat.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from pymax.types import Chat, Message
|
|
1
|
+
from pymax.types import Chat, Member, Message
|
|
2
2
|
|
|
3
3
|
from .protocol import IClientProtocol
|
|
4
4
|
|
|
@@ -238,3 +238,104 @@ class ChatMixin(IClientProtocol):
|
|
|
238
238
|
Загруженные чаты.
|
|
239
239
|
"""
|
|
240
240
|
return await self._app.api.chats.fetch_chats(marker=marker)
|
|
241
|
+
|
|
242
|
+
async def get_join_requests(
|
|
243
|
+
self,
|
|
244
|
+
chat_id: int,
|
|
245
|
+
count: int = 100,
|
|
246
|
+
) -> list[Member]:
|
|
247
|
+
"""Возвращает заявки на вступление в группу или канал.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
chat_id: ID группы или канала.
|
|
251
|
+
count: Максимальное количество заявок в ответе.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Список пользователей, ожидающих подтверждения заявки.
|
|
255
|
+
"""
|
|
256
|
+
return await self._app.api.chats.get_join_requests(
|
|
257
|
+
chat_id=chat_id,
|
|
258
|
+
count=count,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
async def confirm_join_requests(
|
|
262
|
+
self,
|
|
263
|
+
chat_id: int,
|
|
264
|
+
user_ids: list[int],
|
|
265
|
+
show_history: bool = True,
|
|
266
|
+
) -> Chat | None:
|
|
267
|
+
"""Подтверждает несколько заявок на вступление.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
chat_id: ID группы или канала.
|
|
271
|
+
user_ids: ID пользователей, чьи заявки нужно подтвердить.
|
|
272
|
+
show_history: Показать новым участникам историю сообщений.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Обновленный чат или ``None``, если сервер не вернул чат.
|
|
276
|
+
"""
|
|
277
|
+
return await self._app.api.chats.confirm_join_requests(
|
|
278
|
+
chat_id=chat_id,
|
|
279
|
+
user_ids=user_ids,
|
|
280
|
+
show_history=show_history,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def confirm_join_request(
|
|
284
|
+
self,
|
|
285
|
+
chat_id: int,
|
|
286
|
+
user_id: int,
|
|
287
|
+
show_history: bool = True,
|
|
288
|
+
) -> Chat | None:
|
|
289
|
+
"""Подтверждает одну заявку на вступление.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
chat_id: ID группы или канала.
|
|
293
|
+
user_id: ID пользователя, чью заявку нужно подтвердить.
|
|
294
|
+
show_history: Показать новому участнику историю сообщений.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Обновленный чат или ``None``, если сервер не вернул чат.
|
|
298
|
+
"""
|
|
299
|
+
return await self._app.api.chats.confirm_join_request(
|
|
300
|
+
chat_id=chat_id,
|
|
301
|
+
user_id=user_id,
|
|
302
|
+
show_history=show_history,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
async def decline_join_requests(
|
|
306
|
+
self,
|
|
307
|
+
chat_id: int,
|
|
308
|
+
user_ids: list[int],
|
|
309
|
+
) -> Chat | None:
|
|
310
|
+
"""Отклоняет несколько заявок на вступление.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
chat_id: ID группы или канала.
|
|
314
|
+
user_ids: ID пользователей, чьи заявки нужно отклонить.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Обновленный чат или ``None``, если сервер не вернул чат.
|
|
318
|
+
"""
|
|
319
|
+
return await self._app.api.chats.decline_join_requests(
|
|
320
|
+
chat_id=chat_id,
|
|
321
|
+
user_ids=user_ids,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
async def decline_join_request(
|
|
325
|
+
self,
|
|
326
|
+
chat_id: int,
|
|
327
|
+
user_id: int,
|
|
328
|
+
) -> Chat | None:
|
|
329
|
+
"""Отклоняет одну заявку на вступление.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
chat_id: ID группы или канала.
|
|
333
|
+
user_id: ID пользователя, чью заявку нужно отклонить.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Обновленный чат или ``None``, если сервер не вернул чат.
|
|
337
|
+
"""
|
|
338
|
+
return await self._app.api.chats.decline_join_request(
|
|
339
|
+
chat_id=chat_id,
|
|
340
|
+
user_id=user_id,
|
|
341
|
+
)
|
pymax/protocol/tcp/framing.py
CHANGED
|
@@ -4,15 +4,9 @@ from pymax.protocol import PackedPacket, TcpPacketHeader
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class TcpPacketFramer:
|
|
7
|
-
HEADER_STRUCT = struct.Struct(">
|
|
7
|
+
HEADER_STRUCT = struct.Struct(">BBHHI")
|
|
8
8
|
HEADER_SIZE = HEADER_STRUCT.size
|
|
9
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
10
|
def pack(
|
|
17
11
|
self,
|
|
18
12
|
*,
|
|
@@ -26,7 +20,7 @@ class TcpPacketFramer:
|
|
|
26
20
|
packed_len = ((flags & 0xFF) << 24) | (len(payload_bytes) & 0x00FFFFFF)
|
|
27
21
|
header = self.HEADER_STRUCT.pack(
|
|
28
22
|
ver,
|
|
29
|
-
|
|
23
|
+
cmd,
|
|
30
24
|
seq,
|
|
31
25
|
opcode,
|
|
32
26
|
packed_len,
|
|
@@ -37,7 +31,9 @@ class TcpPacketFramer:
|
|
|
37
31
|
if len(data) < self.HEADER_SIZE:
|
|
38
32
|
return None
|
|
39
33
|
|
|
40
|
-
ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(
|
|
34
|
+
ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(
|
|
35
|
+
data, 0
|
|
36
|
+
)
|
|
41
37
|
flags = (packed_len >> 24) & 0xFF
|
|
42
38
|
payload_len = packed_len & 0x00FFFFFF
|
|
43
39
|
|
|
@@ -48,7 +44,7 @@ class TcpPacketFramer:
|
|
|
48
44
|
return PackedPacket(
|
|
49
45
|
header=TcpPacketHeader(
|
|
50
46
|
ver=ver,
|
|
51
|
-
cmd=
|
|
47
|
+
cmd=cmd,
|
|
52
48
|
seq=seq,
|
|
53
49
|
opcode=opcode,
|
|
54
50
|
flags=flags,
|
|
@@ -62,7 +58,6 @@ class TcpPacketFramer:
|
|
|
62
58
|
return None
|
|
63
59
|
|
|
64
60
|
_, _, _, _, packed_len = self.HEADER_STRUCT.unpack_from(data, 0)
|
|
65
|
-
flags = (packed_len >> 24) & 0xFF
|
|
66
61
|
payload_len = packed_len & 0x00FFFFFF
|
|
67
62
|
|
|
68
63
|
return payload_len
|
pymax/protocol/tcp/payload.py
CHANGED
|
@@ -7,7 +7,6 @@ from pymax.logging import get_logger
|
|
|
7
7
|
|
|
8
8
|
from .compression import Lz4BlockCompression
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
logger = get_logger(__name__)
|
|
12
11
|
|
|
13
12
|
|
|
@@ -44,7 +43,9 @@ class MsgpackPayloadCodec:
|
|
|
44
43
|
return {}
|
|
45
44
|
|
|
46
45
|
try:
|
|
47
|
-
return msgpack.unpackb(
|
|
46
|
+
return msgpack.unpackb(
|
|
47
|
+
payload_bytes, raw=False, strict_map_key=False
|
|
48
|
+
)
|
|
48
49
|
except msgpack.exceptions.ExtraData as e:
|
|
49
50
|
if isinstance(e.unpacked, dict):
|
|
50
51
|
logger.debug(
|
pymax/protocol/tcp/protocol.py
CHANGED
|
@@ -3,7 +3,11 @@ from pymax.protocol import InboundFrame, OutboundFrame
|
|
|
3
3
|
from pymax.protocol.base import BaseProtocol
|
|
4
4
|
|
|
5
5
|
from .framing import TcpPacketFramer
|
|
6
|
-
from .payload import
|
|
6
|
+
from .payload import (
|
|
7
|
+
Lz4BlockCompression,
|
|
8
|
+
MsgpackPayloadCodec,
|
|
9
|
+
TcpPayloadDecoder,
|
|
10
|
+
)
|
|
7
11
|
|
|
8
12
|
logger = get_logger(__name__)
|
|
9
13
|
|
|
@@ -21,7 +25,11 @@ class TcpProtocol(BaseProtocol):
|
|
|
21
25
|
)
|
|
22
26
|
|
|
23
27
|
def encode(self, frame: OutboundFrame) -> bytes:
|
|
24
|
-
payload_bytes =
|
|
28
|
+
payload_bytes = (
|
|
29
|
+
self.serializer.encode(frame.payload)
|
|
30
|
+
if frame.payload is not None
|
|
31
|
+
else b""
|
|
32
|
+
)
|
|
25
33
|
|
|
26
34
|
flags = 0
|
|
27
35
|
|
|
@@ -44,7 +52,9 @@ class TcpProtocol(BaseProtocol):
|
|
|
44
52
|
|
|
45
53
|
packed_packet = self.framer.unpack(raw)
|
|
46
54
|
if not packed_packet:
|
|
47
|
-
return InboundFrame(
|
|
55
|
+
return InboundFrame(
|
|
56
|
+
opcode=0, cmd=0, seq=None, payload=None, raw=None
|
|
57
|
+
)
|
|
48
58
|
|
|
49
59
|
logger.debug(
|
|
50
60
|
"tcp frame decoded header ver=%s cmd=%s seq=%s opcode=%s flags=%s payload_len=%s",
|
pymax/protocol/ws/protocol.py
CHANGED
|
@@ -20,8 +20,14 @@ class WsProtocol(BaseProtocol):
|
|
|
20
20
|
data = json.loads(raw)
|
|
21
21
|
return InboundFrame.model_validate(data)
|
|
22
22
|
except json.JSONDecodeError:
|
|
23
|
-
logger.debug(
|
|
24
|
-
|
|
23
|
+
logger.debug(
|
|
24
|
+
"failed to decode websocket frame json", exc_info=True
|
|
25
|
+
)
|
|
26
|
+
return InboundFrame(
|
|
27
|
+
opcode=0, cmd=0, seq=None, payload=None, raw=None
|
|
28
|
+
)
|
|
25
29
|
except ValidationError:
|
|
26
30
|
logger.debug("failed to validate websocket frame", exc_info=True)
|
|
27
|
-
return InboundFrame(
|
|
31
|
+
return InboundFrame(
|
|
32
|
+
opcode=0, cmd=0, seq=None, payload=None, raw=None
|
|
33
|
+
)
|
pymax/session/protocol.py
CHANGED
|
@@ -8,7 +8,11 @@ class StoreProtocol(Protocol):
|
|
|
8
8
|
async def save_session(self, session_info: SessionInfo) -> None: ...
|
|
9
9
|
async def update_token(self, old_token: str, new_token: str) -> None: ...
|
|
10
10
|
async def load_session(self) -> SessionInfo | None: ...
|
|
11
|
-
async def load_session_by_device_id(
|
|
12
|
-
|
|
11
|
+
async def load_session_by_device_id(
|
|
12
|
+
self, device_id: str
|
|
13
|
+
) -> SessionInfo | None: ...
|
|
14
|
+
async def load_session_by_phone(
|
|
15
|
+
self, phone: str
|
|
16
|
+
) -> SessionInfo | None: ...
|
|
13
17
|
async def delete_session(self, token: str) -> None: ...
|
|
14
18
|
async def close(self) -> None: ...
|
pymax/session/store.py
CHANGED
|
@@ -55,12 +55,24 @@ class SessionStore:
|
|
|
55
55
|
)
|
|
56
56
|
"""
|
|
57
57
|
)
|
|
58
|
-
await self._ensure_column(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await self._ensure_column(
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
await self._ensure_column(
|
|
59
|
+
conn, "mt_instance_id", "TEXT NOT NULL DEFAULT ''"
|
|
60
|
+
)
|
|
61
|
+
await self._ensure_column(
|
|
62
|
+
conn, "chats_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
63
|
+
)
|
|
64
|
+
await self._ensure_column(
|
|
65
|
+
conn, "contacts_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
66
|
+
)
|
|
67
|
+
await self._ensure_column(
|
|
68
|
+
conn, "drafts_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
69
|
+
)
|
|
70
|
+
await self._ensure_column(
|
|
71
|
+
conn, "presence_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
72
|
+
)
|
|
73
|
+
await self._ensure_column(
|
|
74
|
+
conn, "config_hash", "TEXT NOT NULL DEFAULT ''"
|
|
75
|
+
)
|
|
64
76
|
await conn.execute(
|
|
65
77
|
"""
|
|
66
78
|
UPDATE sessions
|
|
@@ -81,7 +93,9 @@ class SessionStore:
|
|
|
81
93
|
columns = {row["name"] for row in await cursor.fetchall()}
|
|
82
94
|
|
|
83
95
|
if name not in columns:
|
|
84
|
-
await conn.execute(
|
|
96
|
+
await conn.execute(
|
|
97
|
+
f"ALTER TABLE sessions ADD COLUMN {name} {definition}"
|
|
98
|
+
)
|
|
85
99
|
|
|
86
100
|
async def save_session(self, session_info: SessionInfo) -> None:
|
|
87
101
|
conn = await self._get_connection()
|
|
@@ -144,7 +158,9 @@ class SessionStore:
|
|
|
144
158
|
)
|
|
145
159
|
return self._row_to_session(row)
|
|
146
160
|
|
|
147
|
-
async def load_session_by_device_id(
|
|
161
|
+
async def load_session_by_device_id(
|
|
162
|
+
self, device_id: str
|
|
163
|
+
) -> SessionInfo | None:
|
|
148
164
|
conn = await self._get_connection()
|
|
149
165
|
logger.debug("loading session by device_id=%s", device_id)
|
|
150
166
|
async with conn.execute(
|
pymax/telemetry/navigation.py
CHANGED
|
@@ -155,7 +155,9 @@ class NavigationPlanner:
|
|
|
155
155
|
self.current_screen = self.history.pop()
|
|
156
156
|
return self.current_screen
|
|
157
157
|
|
|
158
|
-
next_screen = self._weighted_choice(
|
|
158
|
+
next_screen = self._weighted_choice(
|
|
159
|
+
self.rules.graph[self.current_screen]
|
|
160
|
+
)
|
|
159
161
|
if next_screen != self.current_screen:
|
|
160
162
|
self.history.append(self.current_screen)
|
|
161
163
|
if len(self.history) > 4:
|
pymax/telemetry/service.py
CHANGED
|
@@ -88,10 +88,14 @@ class TelemetryService:
|
|
|
88
88
|
|
|
89
89
|
while True:
|
|
90
90
|
self._session_id += 1
|
|
91
|
-
events = await self._collect_session_events(
|
|
91
|
+
events = await self._collect_session_events(
|
|
92
|
+
self._planner.new_profile()
|
|
93
|
+
)
|
|
92
94
|
await self._send_events(events)
|
|
93
95
|
self._planner.reset_to_background()
|
|
94
|
-
await asyncio.sleep(
|
|
96
|
+
await asyncio.sleep(
|
|
97
|
+
self._between(self._timing.session_idle_delay)
|
|
98
|
+
)
|
|
95
99
|
|
|
96
100
|
except asyncio.CancelledError:
|
|
97
101
|
raise
|
|
@@ -159,7 +163,9 @@ class TelemetryService:
|
|
|
159
163
|
except Exception:
|
|
160
164
|
logger.debug("telemetry send failed", exc_info=True)
|
|
161
165
|
|
|
162
|
-
def _nav_event(
|
|
166
|
+
def _nav_event(
|
|
167
|
+
self, screen_from: Screen, screen_to: Screen
|
|
168
|
+
) -> TelemetryEvent:
|
|
163
169
|
event = self._payloads.navigation(
|
|
164
170
|
user_id=self._user_id,
|
|
165
171
|
session_id=self._session_id,
|
pymax/transport/tcp.py
CHANGED
|
@@ -10,7 +10,9 @@ logger = get_logger(__name__)
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TCPTransport(Transport):
|
|
13
|
-
def __init__(
|
|
13
|
+
def __init__(
|
|
14
|
+
self, host: str, port: int, proxy: str | None, use_ssl: bool = True
|
|
15
|
+
) -> None:
|
|
14
16
|
self._host = host
|
|
15
17
|
self._port = port
|
|
16
18
|
self._proxy = proxy
|
|
@@ -57,10 +59,14 @@ class TCPTransport(Transport):
|
|
|
57
59
|
)
|
|
58
60
|
|
|
59
61
|
async def close(self) -> None:
|
|
60
|
-
|
|
62
|
+
writer = self._writer
|
|
63
|
+
self._reader = None
|
|
64
|
+
self._writer = None
|
|
65
|
+
|
|
66
|
+
if writer:
|
|
61
67
|
logger.debug("tcp close")
|
|
62
|
-
|
|
63
|
-
await
|
|
68
|
+
writer.close()
|
|
69
|
+
await writer.wait_closed()
|
|
64
70
|
logger.debug("tcp closed")
|
|
65
71
|
|
|
66
72
|
async def send(self, data: bytes | str) -> None:
|