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/mixins/scheduler.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import traceback
|
|
3
|
-
from collections.abc import Awaitable, Callable
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from pymax.protocols import ClientProtocol
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class SchedulerMixin(ClientProtocol):
|
|
10
|
-
async def _run_periodic(
|
|
11
|
-
self, func: Callable[[], Any | Awaitable[Any]], interval: float
|
|
12
|
-
) -> None:
|
|
13
|
-
while True:
|
|
14
|
-
try:
|
|
15
|
-
result = func()
|
|
16
|
-
if asyncio.iscoroutine(result):
|
|
17
|
-
await result
|
|
18
|
-
except Exception as e:
|
|
19
|
-
tb = traceback.format_exc()
|
|
20
|
-
self.logger.error(f"Error in scheduled task {func}: {e}")
|
|
21
|
-
raise
|
|
22
|
-
await asyncio.sleep(interval)
|
|
23
|
-
|
|
24
|
-
async def _start_scheduled_tasks(self) -> None:
|
|
25
|
-
for func, interval in self._scheduled_tasks:
|
|
26
|
-
task = asyncio.create_task(self._run_periodic(func, interval))
|
|
27
|
-
self._background_tasks.add(task)
|
|
28
|
-
task.add_done_callback(self._background_tasks.discard)
|
pymax/mixins/self.py
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import urllib.parse
|
|
2
|
-
from http import HTTPStatus
|
|
3
|
-
from typing import Any
|
|
4
|
-
from urllib.parse import parse_qs, urlparse
|
|
5
|
-
from uuid import uuid4
|
|
6
|
-
|
|
7
|
-
import aiohttp
|
|
8
|
-
|
|
9
|
-
from pymax.exceptions import Error
|
|
10
|
-
from pymax.files import Photo
|
|
11
|
-
from pymax.payloads import (
|
|
12
|
-
ChangeProfilePayload,
|
|
13
|
-
CreateFolderPayload,
|
|
14
|
-
DeleteFolderPayload,
|
|
15
|
-
GetFolderPayload,
|
|
16
|
-
UpdateFolderPayload,
|
|
17
|
-
UploadPayload,
|
|
18
|
-
)
|
|
19
|
-
from pymax.protocols import ClientProtocol
|
|
20
|
-
from pymax.static.enum import Opcode
|
|
21
|
-
from pymax.types import Folder, FolderList, FolderUpdate, Me
|
|
22
|
-
from pymax.utils import MixinsUtils
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class SelfMixin(ClientProtocol):
|
|
26
|
-
async def _request_photo_upload_url(self) -> str:
|
|
27
|
-
self.logger.info("Requesting profile photo upload URL")
|
|
28
|
-
|
|
29
|
-
data = await self._send_and_wait(
|
|
30
|
-
opcode=Opcode.PHOTO_UPLOAD,
|
|
31
|
-
payload=UploadPayload(profile=True).model_dump(by_alias=True),
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
if data.get("payload", {}).get("error"):
|
|
35
|
-
MixinsUtils.handle_error(data)
|
|
36
|
-
|
|
37
|
-
return data["payload"]["url"]
|
|
38
|
-
|
|
39
|
-
async def _upload_profile_photo(self, upload_url: str, photo: Photo) -> str:
|
|
40
|
-
self.logger.info("Uploading profile photo")
|
|
41
|
-
|
|
42
|
-
parsed_url = urlparse(upload_url)
|
|
43
|
-
photo_id = parse_qs(parsed_url.query)["photoIds"][0]
|
|
44
|
-
|
|
45
|
-
form = aiohttp.FormData()
|
|
46
|
-
form.add_field(
|
|
47
|
-
"file",
|
|
48
|
-
await photo.read(),
|
|
49
|
-
filename=photo.file_name,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
async with (
|
|
53
|
-
aiohttp.ClientSession() as session,
|
|
54
|
-
session.post(upload_url, data=form) as response,
|
|
55
|
-
):
|
|
56
|
-
if response.status != HTTPStatus.OK:
|
|
57
|
-
raise Error(
|
|
58
|
-
"Failed to upload profile photo.", message="UploadError", title="Upload Error"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
self.logger.info("Upload successful")
|
|
62
|
-
data = await response.json()
|
|
63
|
-
return data["photos"][photo_id][
|
|
64
|
-
"token"
|
|
65
|
-
] # TODO: сделать нормальную типизацию и чекнинг ответа
|
|
66
|
-
|
|
67
|
-
async def change_profile(
|
|
68
|
-
self,
|
|
69
|
-
first_name: str,
|
|
70
|
-
last_name: str | None = None,
|
|
71
|
-
description: str | None = None,
|
|
72
|
-
photo: Photo | None = None,
|
|
73
|
-
) -> bool:
|
|
74
|
-
"""
|
|
75
|
-
Изменяет информацию профиля текущего пользователя.
|
|
76
|
-
|
|
77
|
-
:param first_name: Имя пользователя.
|
|
78
|
-
:type first_name: str
|
|
79
|
-
:param last_name: Фамилия пользователя. По умолчанию None.
|
|
80
|
-
:type last_name: str | None
|
|
81
|
-
:param description: Описание профиля. По умолчанию None.
|
|
82
|
-
:type description: str | None
|
|
83
|
-
:return: True, если профиль успешно изменен.
|
|
84
|
-
:rtype: bool
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
if photo:
|
|
88
|
-
upload_url = await self._request_photo_upload_url()
|
|
89
|
-
photo_token = await self._upload_profile_photo(upload_url, photo)
|
|
90
|
-
|
|
91
|
-
payload = ChangeProfilePayload(
|
|
92
|
-
first_name=first_name,
|
|
93
|
-
last_name=last_name,
|
|
94
|
-
description=description,
|
|
95
|
-
photo_token=photo_token,
|
|
96
|
-
).model_dump(
|
|
97
|
-
by_alias=True,
|
|
98
|
-
exclude_none=True,
|
|
99
|
-
)
|
|
100
|
-
else:
|
|
101
|
-
payload = ChangeProfilePayload(
|
|
102
|
-
first_name=first_name,
|
|
103
|
-
last_name=last_name,
|
|
104
|
-
description=description,
|
|
105
|
-
).model_dump(
|
|
106
|
-
by_alias=True,
|
|
107
|
-
exclude_none=True,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
data = await self._send_and_wait(opcode=Opcode.PROFILE, payload=payload)
|
|
111
|
-
|
|
112
|
-
if data.get("payload", {}).get("error"):
|
|
113
|
-
MixinsUtils.handle_error(data)
|
|
114
|
-
|
|
115
|
-
self.me = Me.from_dict(data["payload"]["profile"]["contact"])
|
|
116
|
-
|
|
117
|
-
return True
|
|
118
|
-
|
|
119
|
-
async def create_folder(
|
|
120
|
-
self, title: str, chat_include: list[int], filters: list[Any] | None = None
|
|
121
|
-
) -> FolderUpdate:
|
|
122
|
-
"""
|
|
123
|
-
Создает новую папку для группировки чатов.
|
|
124
|
-
|
|
125
|
-
:param title: Название папки.
|
|
126
|
-
:type title: str
|
|
127
|
-
:param chat_include: Список ID чатов для включения в папку.
|
|
128
|
-
:type chat_include: list[int]
|
|
129
|
-
:param filters: Список фильтров для папки (опциональный параметр).
|
|
130
|
-
:type filters: list[Any] | None
|
|
131
|
-
:return: Объект FolderUpdate с информацией о созданной папке.
|
|
132
|
-
:rtype: FolderUpdate
|
|
133
|
-
"""
|
|
134
|
-
self.logger.info("Creating folder")
|
|
135
|
-
|
|
136
|
-
payload = CreateFolderPayload(
|
|
137
|
-
id=str(uuid4()),
|
|
138
|
-
title=title,
|
|
139
|
-
include=chat_include,
|
|
140
|
-
filters=filters or [],
|
|
141
|
-
).model_dump(by_alias=True)
|
|
142
|
-
|
|
143
|
-
data = await self._send_and_wait(opcode=Opcode.FOLDERS_UPDATE, payload=payload)
|
|
144
|
-
|
|
145
|
-
if data.get("payload", {}).get("error"):
|
|
146
|
-
MixinsUtils.handle_error(data)
|
|
147
|
-
|
|
148
|
-
return FolderUpdate.from_dict(data.get("payload", {}))
|
|
149
|
-
|
|
150
|
-
async def get_folders(self, folder_sync: int = 0) -> FolderList:
|
|
151
|
-
"""
|
|
152
|
-
Получает список всех папок пользователя.
|
|
153
|
-
|
|
154
|
-
:param folder_sync: Синхронизационный маркер папок. По умолчанию 0.
|
|
155
|
-
:type folder_sync: int
|
|
156
|
-
:return: Объект FolderList с информацией о папках.
|
|
157
|
-
:rtype: FolderList
|
|
158
|
-
"""
|
|
159
|
-
self.logger.info("Fetching folders")
|
|
160
|
-
|
|
161
|
-
payload = GetFolderPayload(folder_sync=folder_sync).model_dump(by_alias=True)
|
|
162
|
-
|
|
163
|
-
data = await self._send_and_wait(opcode=Opcode.FOLDERS_GET, payload=payload)
|
|
164
|
-
|
|
165
|
-
if data.get("payload", {}).get("error"):
|
|
166
|
-
MixinsUtils.handle_error(data)
|
|
167
|
-
|
|
168
|
-
return FolderList.from_dict(data.get("payload", {}))
|
|
169
|
-
|
|
170
|
-
async def update_folder(
|
|
171
|
-
self,
|
|
172
|
-
folder_id: str,
|
|
173
|
-
title: str,
|
|
174
|
-
chat_include: list[int] | None = None,
|
|
175
|
-
filters: list[Any] | None = None,
|
|
176
|
-
options: list[Any] | None = None,
|
|
177
|
-
) -> FolderUpdate | None:
|
|
178
|
-
"""
|
|
179
|
-
Обновляет параметры существующей папки.
|
|
180
|
-
|
|
181
|
-
:param folder_id: Идентификатор папки.
|
|
182
|
-
:type folder_id: str
|
|
183
|
-
:param title: Название папки.
|
|
184
|
-
:type title: str
|
|
185
|
-
:param chat_include: Список ID чатов для включения в папку.
|
|
186
|
-
:type chat_include: list[int] | None
|
|
187
|
-
:param filters: Список фильтров для папки.
|
|
188
|
-
:type filters: list[Any] | None
|
|
189
|
-
:param options: Список опций для папки.
|
|
190
|
-
:type options: list[Any] | None
|
|
191
|
-
:return: Объект FolderUpdate с результатом или None.
|
|
192
|
-
:rtype: FolderUpdate | None
|
|
193
|
-
"""
|
|
194
|
-
self.logger.info("Updating folder")
|
|
195
|
-
|
|
196
|
-
payload = UpdateFolderPayload(
|
|
197
|
-
id=folder_id,
|
|
198
|
-
title=title,
|
|
199
|
-
include=chat_include or [],
|
|
200
|
-
filters=filters or [],
|
|
201
|
-
options=options or [],
|
|
202
|
-
).model_dump(by_alias=True, exclude_none=True)
|
|
203
|
-
|
|
204
|
-
data = await self._send_and_wait(opcode=Opcode.FOLDERS_UPDATE, payload=payload)
|
|
205
|
-
|
|
206
|
-
if data.get("payload", {}).get("error"):
|
|
207
|
-
MixinsUtils.handle_error(data)
|
|
208
|
-
|
|
209
|
-
return FolderUpdate.from_dict(data.get("payload", {}))
|
|
210
|
-
|
|
211
|
-
async def delete_folder(self, folder_id: str) -> FolderUpdate | None:
|
|
212
|
-
"""
|
|
213
|
-
Удаляет папку.
|
|
214
|
-
|
|
215
|
-
:param folder_id: Идентификатор папки для удаления.
|
|
216
|
-
:type folder_id: str
|
|
217
|
-
:return: Объект FolderUpdate с результатом операции или None.
|
|
218
|
-
:rtype: FolderUpdate | None
|
|
219
|
-
"""
|
|
220
|
-
self.logger.info("Deleting folder")
|
|
221
|
-
|
|
222
|
-
payload = DeleteFolderPayload(folder_ids=[folder_id]).model_dump(by_alias=True)
|
|
223
|
-
data = await self._send_and_wait(opcode=Opcode.FOLDERS_DELETE, payload=payload)
|
|
224
|
-
if data.get("payload", {}).get("error"):
|
|
225
|
-
MixinsUtils.handle_error(data)
|
|
226
|
-
|
|
227
|
-
return FolderUpdate.from_dict(data.get("payload", {}))
|
|
228
|
-
|
|
229
|
-
async def close_all_sessions(self) -> bool:
|
|
230
|
-
"""
|
|
231
|
-
Закрывает все активные сессии, кроме текущей.
|
|
232
|
-
|
|
233
|
-
:return: True, если операция выполнена успешно.
|
|
234
|
-
:rtype: bool
|
|
235
|
-
"""
|
|
236
|
-
self.logger.info("Closing all other sessions")
|
|
237
|
-
|
|
238
|
-
data = await self._send_and_wait(opcode=Opcode.SESSIONS_CLOSE, payload={})
|
|
239
|
-
|
|
240
|
-
if data.get("payload", {}).get("error"):
|
|
241
|
-
MixinsUtils.handle_error(data)
|
|
242
|
-
|
|
243
|
-
return True
|
|
244
|
-
|
|
245
|
-
async def logout(self) -> bool:
|
|
246
|
-
"""
|
|
247
|
-
Выполняет выход из текущей сессии.
|
|
248
|
-
|
|
249
|
-
:return: True, если выход выполнен успешно.
|
|
250
|
-
:rtype: bool
|
|
251
|
-
"""
|
|
252
|
-
self.logger.info("Logging out")
|
|
253
|
-
|
|
254
|
-
data = await self._send_and_wait(opcode=Opcode.LOGOUT, payload={})
|
|
255
|
-
|
|
256
|
-
if data.get("payload", {}).get("error"):
|
|
257
|
-
MixinsUtils.handle_error(data)
|
|
258
|
-
|
|
259
|
-
return True
|
pymax/mixins/socket.py
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import socket
|
|
3
|
-
import ssl
|
|
4
|
-
import sys
|
|
5
|
-
import time
|
|
6
|
-
from collections.abc import Callable
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
import lz4.block
|
|
10
|
-
import msgpack
|
|
11
|
-
from typing_extensions import override
|
|
12
|
-
|
|
13
|
-
from pymax.exceptions import Error, SocketNotConnectedError, SocketSendError
|
|
14
|
-
from pymax.filters import BaseFilter
|
|
15
|
-
from pymax.interfaces import BaseTransport
|
|
16
|
-
from pymax.payloads import BaseWebSocketMessage, SyncPayload, UserAgentPayload
|
|
17
|
-
from pymax.protocols import ClientProtocol
|
|
18
|
-
from pymax.static.constant import (
|
|
19
|
-
DEFAULT_PING_INTERVAL,
|
|
20
|
-
DEFAULT_TIMEOUT,
|
|
21
|
-
RECV_LOOP_BACKOFF_DELAY,
|
|
22
|
-
)
|
|
23
|
-
from pymax.static.enum import ChatType, MessageStatus, Opcode
|
|
24
|
-
from pymax.types import (
|
|
25
|
-
Channel,
|
|
26
|
-
Chat,
|
|
27
|
-
Dialog,
|
|
28
|
-
Me,
|
|
29
|
-
Message,
|
|
30
|
-
ReactionCounter,
|
|
31
|
-
ReactionInfo,
|
|
32
|
-
User,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class SocketMixin(BaseTransport):
|
|
37
|
-
@property
|
|
38
|
-
def sock(self) -> socket.socket:
|
|
39
|
-
if self._socket is None or not self.is_connected:
|
|
40
|
-
self.logger.critical("Socket not connected when access attempted")
|
|
41
|
-
raise SocketNotConnectedError()
|
|
42
|
-
return self._socket
|
|
43
|
-
|
|
44
|
-
def _unpack_packet(self, data: bytes) -> dict[str, Any] | None:
|
|
45
|
-
ver = int.from_bytes(data[0:1], "big")
|
|
46
|
-
cmd = int.from_bytes(data[1:3], "big")
|
|
47
|
-
seq = int.from_bytes(data[3:4], "big")
|
|
48
|
-
opcode = int.from_bytes(data[4:6], "big")
|
|
49
|
-
packed_len = int.from_bytes(data[6:10], "big", signed=False)
|
|
50
|
-
comp_flag = packed_len >> 24
|
|
51
|
-
payload_length = packed_len & 0xFFFFFF
|
|
52
|
-
payload_bytes = data[10 : 10 + payload_length]
|
|
53
|
-
|
|
54
|
-
payload = None
|
|
55
|
-
if payload_bytes:
|
|
56
|
-
if comp_flag != 0:
|
|
57
|
-
# TODO: надо выяснить правильный размер распаковки
|
|
58
|
-
# uncompressed_size = int.from_bytes(payload_bytes[0:4], "big")
|
|
59
|
-
compressed_data = payload_bytes
|
|
60
|
-
try:
|
|
61
|
-
payload_bytes = lz4.block.decompress(
|
|
62
|
-
compressed_data,
|
|
63
|
-
uncompressed_size=99999,
|
|
64
|
-
)
|
|
65
|
-
except lz4.block.LZ4BlockError:
|
|
66
|
-
return None
|
|
67
|
-
payload = msgpack.unpackb(payload_bytes, raw=False, strict_map_key=False)
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
"ver": ver,
|
|
71
|
-
"cmd": cmd,
|
|
72
|
-
"seq": seq,
|
|
73
|
-
"opcode": opcode,
|
|
74
|
-
"payload": payload,
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
def _pack_packet(
|
|
78
|
-
self,
|
|
79
|
-
ver: int,
|
|
80
|
-
cmd: int,
|
|
81
|
-
seq: int,
|
|
82
|
-
opcode: int,
|
|
83
|
-
payload: dict[str, Any],
|
|
84
|
-
) -> bytes:
|
|
85
|
-
ver_b = ver.to_bytes(1, "big")
|
|
86
|
-
cmd_b = cmd.to_bytes(2, "big")
|
|
87
|
-
seq_b = seq.to_bytes(1, "big")
|
|
88
|
-
opcode_b = opcode.to_bytes(2, "big")
|
|
89
|
-
payload_bytes: bytes | None = msgpack.packb(payload)
|
|
90
|
-
if payload_bytes is None:
|
|
91
|
-
payload_bytes = b""
|
|
92
|
-
payload_len = len(payload_bytes) & 0xFFFFFF
|
|
93
|
-
self.logger.debug("Packing message: payload size=%d bytes", len(payload_bytes))
|
|
94
|
-
payload_len_b = payload_len.to_bytes(4, "big")
|
|
95
|
-
return ver_b + cmd_b + seq_b + opcode_b + payload_len_b + payload_bytes
|
|
96
|
-
|
|
97
|
-
async def connect(self, user_agent: UserAgentPayload | None = None) -> dict[str, Any]:
|
|
98
|
-
"""
|
|
99
|
-
Устанавливает соединение с сервером и выполняет handshake.
|
|
100
|
-
|
|
101
|
-
:param user_agent: Пользовательский агент для handshake. Если None, используется значение по умолчанию.
|
|
102
|
-
:type user_agent: UserAgentPayload | None
|
|
103
|
-
:return: Результат handshake.
|
|
104
|
-
:rtype: dict[str, Any] | None
|
|
105
|
-
"""
|
|
106
|
-
if user_agent is None:
|
|
107
|
-
user_agent = UserAgentPayload()
|
|
108
|
-
if sys.version_info[:2] == (3, 12):
|
|
109
|
-
self.logger.warning(
|
|
110
|
-
"""
|
|
111
|
-
===============================================================
|
|
112
|
-
⚠️⚠️ \033[0;31mWARNING: Python 3.12 detected!\033[0m ⚠️⚠️
|
|
113
|
-
Socket connections may be unstable, SSL issues are possible.
|
|
114
|
-
===============================================================
|
|
115
|
-
"""
|
|
116
|
-
)
|
|
117
|
-
self.logger.info("Connecting to socket %s:%s", self.host, self.port)
|
|
118
|
-
loop = asyncio.get_running_loop()
|
|
119
|
-
raw_sock = await loop.run_in_executor(
|
|
120
|
-
None, lambda: socket.create_connection((self.host, self.port))
|
|
121
|
-
)
|
|
122
|
-
self._socket = self._ssl_context.wrap_socket(raw_sock, server_hostname=self.host)
|
|
123
|
-
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
124
|
-
self.is_connected = True
|
|
125
|
-
self._incoming = asyncio.Queue()
|
|
126
|
-
self._outgoing = asyncio.Queue()
|
|
127
|
-
self._pending = {}
|
|
128
|
-
self._recv_task = asyncio.create_task(self._recv_loop())
|
|
129
|
-
self._outgoing_task = asyncio.create_task(self._outgoing_loop())
|
|
130
|
-
self.logger.info("Socket connected, starting handshake")
|
|
131
|
-
return await self._handshake(user_agent)
|
|
132
|
-
|
|
133
|
-
def _recv_exactly(self, sock: socket.socket, n: int) -> bytes:
|
|
134
|
-
buf = bytearray()
|
|
135
|
-
while len(buf) < n:
|
|
136
|
-
chunk = sock.recv(n - len(buf))
|
|
137
|
-
if not chunk:
|
|
138
|
-
return bytes(buf)
|
|
139
|
-
buf.extend(chunk)
|
|
140
|
-
return bytes(buf)
|
|
141
|
-
|
|
142
|
-
async def _parse_header(
|
|
143
|
-
self, loop: asyncio.AbstractEventLoop, sock: socket.socket
|
|
144
|
-
) -> bytes | None:
|
|
145
|
-
header = await loop.run_in_executor(None, lambda: self._recv_exactly(sock=sock, n=10))
|
|
146
|
-
if not header or len(header) < 10:
|
|
147
|
-
self.logger.info("Socket connection closed; exiting recv loop")
|
|
148
|
-
self.is_connected = False
|
|
149
|
-
try:
|
|
150
|
-
sock.close()
|
|
151
|
-
except Exception:
|
|
152
|
-
return None
|
|
153
|
-
|
|
154
|
-
return header
|
|
155
|
-
|
|
156
|
-
async def _recv_data(
|
|
157
|
-
self, loop: asyncio.AbstractEventLoop, header: bytes, sock: socket.socket
|
|
158
|
-
) -> list[dict[str, Any]] | None:
|
|
159
|
-
packed_len = int.from_bytes(header[6:10], "big", signed=False)
|
|
160
|
-
payload_length = packed_len & 0xFFFFFF
|
|
161
|
-
remaining = payload_length
|
|
162
|
-
payload = bytearray()
|
|
163
|
-
|
|
164
|
-
while remaining > 0:
|
|
165
|
-
min_read = min(remaining, 8192)
|
|
166
|
-
chunk = await loop.run_in_executor(None, lambda: self._recv_exactly(sock, min_read))
|
|
167
|
-
if not chunk:
|
|
168
|
-
self.logger.error("Connection closed while reading payload")
|
|
169
|
-
break
|
|
170
|
-
payload.extend(chunk)
|
|
171
|
-
remaining -= len(chunk)
|
|
172
|
-
|
|
173
|
-
if remaining > 0:
|
|
174
|
-
self.logger.error("Incomplete payload received; skipping packet")
|
|
175
|
-
return None
|
|
176
|
-
|
|
177
|
-
raw = header + payload
|
|
178
|
-
if len(raw) < 10 + payload_length:
|
|
179
|
-
self.logger.error(
|
|
180
|
-
"Incomplete packet: expected %d bytes, got %d",
|
|
181
|
-
10 + payload_length,
|
|
182
|
-
len(raw),
|
|
183
|
-
)
|
|
184
|
-
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
data = self._unpack_packet(raw)
|
|
188
|
-
if not data:
|
|
189
|
-
self.logger.warning("Failed to unpack packet, skipping")
|
|
190
|
-
return None
|
|
191
|
-
|
|
192
|
-
payload_objs = data.get("payload")
|
|
193
|
-
return (
|
|
194
|
-
[{**data, "payload": obj} for obj in payload_objs]
|
|
195
|
-
if isinstance(payload_objs, list)
|
|
196
|
-
else [data]
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
async def _recv_loop(self) -> None:
|
|
200
|
-
if self._socket is None:
|
|
201
|
-
self.logger.warning("Recv loop started without socket instance")
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
sock = self._socket
|
|
205
|
-
loop = asyncio.get_running_loop()
|
|
206
|
-
|
|
207
|
-
while True:
|
|
208
|
-
try:
|
|
209
|
-
header = await self._parse_header(loop, sock)
|
|
210
|
-
|
|
211
|
-
if not header:
|
|
212
|
-
break
|
|
213
|
-
|
|
214
|
-
datas = await self._recv_data(loop, header, sock)
|
|
215
|
-
|
|
216
|
-
if not datas:
|
|
217
|
-
continue
|
|
218
|
-
|
|
219
|
-
for data_item in datas:
|
|
220
|
-
seq = data_item.get("seq")
|
|
221
|
-
|
|
222
|
-
if self._handle_pending(seq, data_item):
|
|
223
|
-
continue
|
|
224
|
-
|
|
225
|
-
if self._incoming is not None:
|
|
226
|
-
await self._handle_incoming_queue(data_item)
|
|
227
|
-
|
|
228
|
-
await self._dispatch_incoming(data_item)
|
|
229
|
-
|
|
230
|
-
except asyncio.CancelledError:
|
|
231
|
-
self.logger.debug("Recv loop cancelled")
|
|
232
|
-
raise
|
|
233
|
-
except Exception:
|
|
234
|
-
self.logger.exception("Error in recv_loop; backing off briefly")
|
|
235
|
-
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
236
|
-
|
|
237
|
-
@override
|
|
238
|
-
async def _send_and_wait(
|
|
239
|
-
self,
|
|
240
|
-
opcode: Opcode,
|
|
241
|
-
payload: dict[str, Any],
|
|
242
|
-
cmd: int = 0,
|
|
243
|
-
timeout: float = DEFAULT_TIMEOUT,
|
|
244
|
-
) -> dict[str, Any]:
|
|
245
|
-
if not self.is_connected or self._socket is None:
|
|
246
|
-
raise SocketNotConnectedError
|
|
247
|
-
|
|
248
|
-
sock = self.sock
|
|
249
|
-
msg = self._make_message(opcode, payload, cmd)
|
|
250
|
-
loop = asyncio.get_running_loop()
|
|
251
|
-
fut: asyncio.Future[dict[str, Any]] = loop.create_future()
|
|
252
|
-
self._pending[msg["seq"]] = fut
|
|
253
|
-
try:
|
|
254
|
-
self.logger.debug(
|
|
255
|
-
"Sending frame opcode=%s cmd=%s seq=%s",
|
|
256
|
-
opcode,
|
|
257
|
-
cmd,
|
|
258
|
-
msg["seq"],
|
|
259
|
-
)
|
|
260
|
-
packet = self._pack_packet(
|
|
261
|
-
msg["ver"],
|
|
262
|
-
msg["cmd"],
|
|
263
|
-
msg["seq"],
|
|
264
|
-
msg["opcode"],
|
|
265
|
-
msg["payload"],
|
|
266
|
-
)
|
|
267
|
-
await loop.run_in_executor(None, lambda: sock.sendall(packet))
|
|
268
|
-
data = await asyncio.wait_for(fut, timeout=timeout)
|
|
269
|
-
self.logger.debug(
|
|
270
|
-
"Received frame for seq=%s opcode=%s",
|
|
271
|
-
data.get("seq"),
|
|
272
|
-
data.get("opcode"),
|
|
273
|
-
)
|
|
274
|
-
return data
|
|
275
|
-
|
|
276
|
-
except (ssl.SSLEOFError, ssl.SSLError, ConnectionError) as conn_err:
|
|
277
|
-
self.logger.warning("Connection lost, reconnecting...")
|
|
278
|
-
self.is_connected = False
|
|
279
|
-
try:
|
|
280
|
-
await self.connect(self.user_agent)
|
|
281
|
-
except Exception as exc:
|
|
282
|
-
self.logger.exception("Reconnect failed")
|
|
283
|
-
raise exc from conn_err
|
|
284
|
-
raise SocketNotConnectedError from conn_err
|
|
285
|
-
except Exception as exc:
|
|
286
|
-
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
287
|
-
raise SocketSendError from exc
|
|
288
|
-
|
|
289
|
-
finally:
|
|
290
|
-
self._pending.pop(msg["seq"], None)
|
|
291
|
-
|
|
292
|
-
@override
|
|
293
|
-
async def _get_chat(self, chat_id: int) -> Chat | None:
|
|
294
|
-
for chat in self.chats:
|
|
295
|
-
if chat.id == chat_id:
|
|
296
|
-
return chat
|
|
297
|
-
return None
|
pymax/mixins/telemetry.py
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import random
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
from pymax.exceptions import Error
|
|
6
|
-
from pymax.navigation import Navigation
|
|
7
|
-
from pymax.payloads import (
|
|
8
|
-
NavigationEventParams,
|
|
9
|
-
NavigationEventPayload,
|
|
10
|
-
NavigationPayload,
|
|
11
|
-
)
|
|
12
|
-
from pymax.protocols import ClientProtocol
|
|
13
|
-
from pymax.static.enum import Opcode
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class TelemetryMixin(ClientProtocol):
|
|
17
|
-
async def _send_navigation_event(self, events: list[NavigationEventPayload]) -> None:
|
|
18
|
-
try:
|
|
19
|
-
payload = NavigationPayload(events=events).model_dump(by_alias=True)
|
|
20
|
-
data = await self._send_and_wait(
|
|
21
|
-
opcode=Opcode.LOG,
|
|
22
|
-
payload=payload,
|
|
23
|
-
)
|
|
24
|
-
payload_data = data.get("payload", {})
|
|
25
|
-
if payload_data and payload_data.get("error"):
|
|
26
|
-
error = payload_data.get("error")
|
|
27
|
-
self.logger.error("Navigation event error: %s", error)
|
|
28
|
-
except Exception:
|
|
29
|
-
self.logger.warning("Failed to send navigation event", exc_info=True)
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
async def _send_cold_start(self) -> None:
|
|
33
|
-
if not self.me:
|
|
34
|
-
self.logger.error("Cannot send cold start, user not set")
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
payload = NavigationEventPayload(
|
|
38
|
-
event="COLD_START",
|
|
39
|
-
time=int(time.time() * 1000),
|
|
40
|
-
user_id=self.me.id,
|
|
41
|
-
params=NavigationEventParams(
|
|
42
|
-
action_id=self._action_id,
|
|
43
|
-
screen_to=Navigation.get_screen_id("chats_list_tab"),
|
|
44
|
-
screen_from=1,
|
|
45
|
-
source_id=1,
|
|
46
|
-
session_id=self._session_id,
|
|
47
|
-
),
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
self._action_id += 1
|
|
51
|
-
|
|
52
|
-
await self._send_navigation_event([payload])
|
|
53
|
-
|
|
54
|
-
async def _send_random_navigation(self) -> None:
|
|
55
|
-
if not self.me:
|
|
56
|
-
self.logger.error("Cannot send navigation event, user not set")
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
screen_from = self._current_screen
|
|
60
|
-
screen_to = Navigation.get_random_navigation(screen_from)
|
|
61
|
-
|
|
62
|
-
self._action_id += 1
|
|
63
|
-
self._current_screen = screen_to
|
|
64
|
-
|
|
65
|
-
payload = NavigationEventPayload(
|
|
66
|
-
event="NAV",
|
|
67
|
-
time=int(time.time() * 1000),
|
|
68
|
-
user_id=self.me.id,
|
|
69
|
-
params=NavigationEventParams(
|
|
70
|
-
action_id=self._action_id,
|
|
71
|
-
screen_from=Navigation.get_screen_id(screen_from),
|
|
72
|
-
screen_to=Navigation.get_screen_id(screen_to),
|
|
73
|
-
source_id=1,
|
|
74
|
-
session_id=self._session_id,
|
|
75
|
-
),
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
await self._send_navigation_event([payload])
|
|
79
|
-
|
|
80
|
-
def _get_random_sleep_time(self) -> int:
|
|
81
|
-
# TODO: вынести в статик
|
|
82
|
-
sleep_options = [
|
|
83
|
-
(1000, 3000),
|
|
84
|
-
(300, 1000),
|
|
85
|
-
(60, 300),
|
|
86
|
-
(5, 60),
|
|
87
|
-
(5, 20),
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
weights = [0.05, 0.10, 0.15, 0.20, 0.50]
|
|
91
|
-
|
|
92
|
-
low, high = random.choices( # nosec B311
|
|
93
|
-
sleep_options, weights=weights, k=1
|
|
94
|
-
)[0]
|
|
95
|
-
return random.randint(low, high) # nosec B311
|
|
96
|
-
|
|
97
|
-
async def _start(self) -> None:
|
|
98
|
-
if not self.is_connected:
|
|
99
|
-
self.logger.error("Cannot start telemetry, client not connected")
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
await self._send_cold_start()
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
while self.is_connected:
|
|
106
|
-
await self._send_random_navigation()
|
|
107
|
-
await asyncio.sleep(self._get_random_sleep_time())
|
|
108
|
-
|
|
109
|
-
except asyncio.CancelledError:
|
|
110
|
-
self.logger.debug("Telemetry task cancelled")
|
|
111
|
-
except Exception:
|
|
112
|
-
self.logger.warning("Telemetry task failed", exc_info=True)
|