maxapi-python 1.1.19__py3-none-any.whl → 1.1.20__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-1.1.19.dist-info → maxapi_python-1.1.20.dist-info}/METADATA +1 -1
- maxapi_python-1.1.20.dist-info/RECORD +31 -0
- {maxapi_python-1.1.19.dist-info → maxapi_python-1.1.20.dist-info}/WHEEL +1 -1
- pymax/core.py +58 -7
- pymax/files.py +15 -12
- pymax/interfaces.py +9 -5
- pymax/mixins/auth.py +1 -1
- pymax/mixins/channel.py +6 -6
- pymax/mixins/group.py +60 -0
- pymax/mixins/handler.py +2 -1
- pymax/mixins/message.py +89 -12
- pymax/mixins/self.py +115 -1
- pymax/mixins/socket.py +53 -26
- pymax/mixins/websocket.py +10 -1
- pymax/payloads.py +36 -3
- pymax/static/enum.py +0 -1
- pymax/types.py +114 -2
- maxapi_python-1.1.19.dist-info/RECORD +0 -31
- {maxapi_python-1.1.19.dist-info → maxapi_python-1.1.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
+
pymax/core.py,sha256=LqX56a5BagUYl1vpB55Y1pLZQdMoC86t6mIQVlkVByo,17322
|
|
3
|
+
pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
|
|
4
|
+
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
|
+
pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
|
|
6
|
+
pymax/filters.py,sha256=4hehzyQlyxBxLiEhtSH_KQdFHx8gYaqyIOOD72EsSS0,1536
|
|
7
|
+
pymax/formatter.py,sha256=OVsTwambHhXlOMd0wVECJWuB_S2wSEVKdMLCzgPcvYQ,859
|
|
8
|
+
pymax/formatting.py,sha256=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
|
|
9
|
+
pymax/interfaces.py,sha256=Re8o5N7FSQ-5OgVlK4-WBltX27GheEbfFjoIYl9_u6I,3723
|
|
10
|
+
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
|
+
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
+
pymax/payloads.py,sha256=cEXY_cVL6SPyhoFTTZnn7dyUx9MMdtNT5SuQSQtL4rg,6983
|
|
13
|
+
pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
|
|
14
|
+
pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
|
|
15
|
+
pymax/mixins/auth.py,sha256=Emv-0WVB_orwv9L_V5gAHfp-VYVaVcbW6AlclW_K6W4,6731
|
|
16
|
+
pymax/mixins/channel.py,sha256=7c8GANyxZuNbIHNBVcPAmMa1qqA1IRf9cGPBS1oK_q4,5159
|
|
17
|
+
pymax/mixins/group.py,sha256=XWXNWluCvq4KkZWqv4sxLpzkXfH33U1yEP20_ZFtSM4,10624
|
|
18
|
+
pymax/mixins/handler.py,sha256=ZuYX8wSgNXJoSMArcwyHvY_bL9A7X0AXnAOz22ATA3k,5897
|
|
19
|
+
pymax/mixins/message.py,sha256=wYvkMPE9ORCSFd_9J-6ltf__4ELG_zaZ_Uey4rmCzHg,25460
|
|
20
|
+
pymax/mixins/self.py,sha256=3BdHfUyqw3dn3ctJX9_hilP1jOaTaunstZ7nH8Y_xcU,5436
|
|
21
|
+
pymax/mixins/socket.py,sha256=GEOscQKKC48bOAXXiDLoz8GusCLtjdxQnB5AK5xJbms,26997
|
|
22
|
+
pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
|
|
23
|
+
pymax/mixins/user.py,sha256=5utoK7Z-7lySOg0PEO69b6h_3kjfcnV-YydTZIdNj8g,7120
|
|
24
|
+
pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
|
|
25
|
+
pymax/mixins/websocket.py,sha256=toiXt9qxx6yTgnWJdEOeNfp414MD4zmbHp1qVgVkjnY,19850
|
|
26
|
+
pymax/static/constant.py,sha256=Q1NrmaRj17Gdhk3FmUp3HIwrad1TDorq3wFdQlOCzN8,1027
|
|
27
|
+
pymax/static/enum.py,sha256=ddw5SEVfRb2J9TXOa5IGhssNd-7RyKfwZBKx_UionEM,4562
|
|
28
|
+
maxapi_python-1.1.20.dist-info/METADATA,sha256=9yhRv1m8PbJJhnUdloacxlK0jAE0veq6zfXDP-Ok5nk,6245
|
|
29
|
+
maxapi_python-1.1.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
30
|
+
maxapi_python-1.1.20.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
31
|
+
maxapi_python-1.1.20.dist-info/RECORD,,
|
pymax/core.py
CHANGED
|
@@ -7,12 +7,16 @@ import time
|
|
|
7
7
|
import traceback
|
|
8
8
|
from collections.abc import Awaitable
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
11
11
|
|
|
12
|
-
from typing_extensions import override
|
|
12
|
+
from typing_extensions import Self, override
|
|
13
13
|
|
|
14
14
|
from .crud import Database
|
|
15
|
-
from .exceptions import
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
InvalidPhoneError,
|
|
17
|
+
SocketNotConnectedError,
|
|
18
|
+
WebSocketNotConnectedError,
|
|
19
|
+
)
|
|
16
20
|
from .formatter import ColoredFormatter
|
|
17
21
|
from .mixins import ApiMixin, SocketMixin, WebSocketMixin
|
|
18
22
|
from .payloads import UserAgentPayload
|
|
@@ -176,13 +180,13 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
176
180
|
handler.setFormatter(formatter)
|
|
177
181
|
self.logger.addHandler(handler)
|
|
178
182
|
|
|
179
|
-
async def _wait_forever(self):
|
|
183
|
+
async def _wait_forever(self) -> None:
|
|
180
184
|
try:
|
|
181
185
|
await self.ws.wait_closed()
|
|
182
186
|
except asyncio.CancelledError:
|
|
183
187
|
self.logger.debug("wait_closed cancelled")
|
|
184
188
|
|
|
185
|
-
async def _safe_execute(self, coro, *, context: str = "unknown"):
|
|
189
|
+
async def _safe_execute(self, coro, *, context: str = "unknown") -> Any:
|
|
186
190
|
"""
|
|
187
191
|
Безопасно выполняет пользовательскую корутину.
|
|
188
192
|
Логирует traceback, но не роняет event loop.
|
|
@@ -255,7 +259,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
255
259
|
telemetry_task.add_done_callback(self._log_task_exception)
|
|
256
260
|
self._background_tasks.add(telemetry_task)
|
|
257
261
|
|
|
258
|
-
async def _cleanup_client(self):
|
|
262
|
+
async def _cleanup_client(self) -> None:
|
|
259
263
|
for task in list(self._background_tasks):
|
|
260
264
|
task.cancel()
|
|
261
265
|
try:
|
|
@@ -356,7 +360,6 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
356
360
|
|
|
357
361
|
await self._wait_forever()
|
|
358
362
|
self.logger.info("WebSocket closed (wait_forever exited)")
|
|
359
|
-
|
|
360
363
|
except Exception as e:
|
|
361
364
|
self.logger.exception("Client start iteration failed")
|
|
362
365
|
raise e
|
|
@@ -396,3 +399,51 @@ class SocketMaxClient(SocketMixin, MaxClient):
|
|
|
396
399
|
self.logger.debug("Socket recv_task cancelled")
|
|
397
400
|
except Exception as e:
|
|
398
401
|
self.logger.exception("Socket recv_task failed: %s", e)
|
|
402
|
+
|
|
403
|
+
@override
|
|
404
|
+
async def _cleanup_client(self):
|
|
405
|
+
"""
|
|
406
|
+
Socket-specific cleanup: cancel background tasks, set pending futures
|
|
407
|
+
exceptions to SocketNotConnectedError, and close socket.
|
|
408
|
+
"""
|
|
409
|
+
from .exceptions import SocketNotConnectedError
|
|
410
|
+
|
|
411
|
+
for task in list(self._background_tasks):
|
|
412
|
+
task.cancel()
|
|
413
|
+
try:
|
|
414
|
+
await task
|
|
415
|
+
except asyncio.CancelledError:
|
|
416
|
+
pass
|
|
417
|
+
except Exception:
|
|
418
|
+
self.logger.debug(
|
|
419
|
+
"Background task raised during cancellation (socket)",
|
|
420
|
+
exc_info=True,
|
|
421
|
+
)
|
|
422
|
+
self._background_tasks.discard(task)
|
|
423
|
+
|
|
424
|
+
if self._recv_task:
|
|
425
|
+
self._recv_task.cancel()
|
|
426
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
427
|
+
await self._recv_task
|
|
428
|
+
self._recv_task = None
|
|
429
|
+
|
|
430
|
+
if self._outgoing_task:
|
|
431
|
+
self._outgoing_task.cancel()
|
|
432
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
433
|
+
await self._outgoing_task
|
|
434
|
+
self._outgoing_task = None
|
|
435
|
+
|
|
436
|
+
for fut in self._pending.values():
|
|
437
|
+
if not fut.done():
|
|
438
|
+
fut.set_exception(SocketNotConnectedError())
|
|
439
|
+
self._pending.clear()
|
|
440
|
+
|
|
441
|
+
if self._socket:
|
|
442
|
+
try:
|
|
443
|
+
self._socket.close()
|
|
444
|
+
except Exception:
|
|
445
|
+
self.logger.debug("Error closing socket during cleanup", exc_info=True)
|
|
446
|
+
self._socket = None
|
|
447
|
+
|
|
448
|
+
self.is_connected = False
|
|
449
|
+
self.logger.info("Client start() cleaned up (socket)")
|
pymax/files.py
CHANGED
|
@@ -9,9 +9,7 @@ from typing_extensions import override
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BaseFile(ABC):
|
|
12
|
-
def __init__(
|
|
13
|
-
self, url: str | None = None, path: str | None = None
|
|
14
|
-
) -> None:
|
|
12
|
+
def __init__(self, url: str | None = None, path: str | None = None) -> None:
|
|
15
13
|
self.url = url
|
|
16
14
|
self.path = path
|
|
17
15
|
|
|
@@ -47,9 +45,7 @@ class Photo(BaseFile):
|
|
|
47
45
|
".bmp",
|
|
48
46
|
} # FIXME: костыль ✅
|
|
49
47
|
|
|
50
|
-
def __init__(
|
|
51
|
-
self, url: str | None = None, path: str | None = None
|
|
52
|
-
) -> None:
|
|
48
|
+
def __init__(self, url: str | None = None, path: str | None = None) -> None:
|
|
53
49
|
super().__init__(url, path)
|
|
54
50
|
|
|
55
51
|
def validate_photo(self) -> tuple[str, str] | None:
|
|
@@ -71,9 +67,7 @@ class Photo(BaseFile):
|
|
|
71
67
|
mime_type = mimetypes.guess_type(self.url)[0]
|
|
72
68
|
|
|
73
69
|
if not mime_type or not mime_type.startswith("image/"):
|
|
74
|
-
raise ValueError(
|
|
75
|
-
f"URL does not appear to be an image: {self.url}"
|
|
76
|
-
)
|
|
70
|
+
raise ValueError(f"URL does not appear to be an image: {self.url}")
|
|
77
71
|
|
|
78
72
|
return (extension[1:], mime_type)
|
|
79
73
|
return None
|
|
@@ -84,15 +78,24 @@ class Photo(BaseFile):
|
|
|
84
78
|
|
|
85
79
|
|
|
86
80
|
class Video(BaseFile):
|
|
81
|
+
def __init__(self, url: str | None = None, path: str | None = None) -> None:
|
|
82
|
+
self.file_name: str = ""
|
|
83
|
+
if path:
|
|
84
|
+
self.file_name = Path(path).name
|
|
85
|
+
elif url:
|
|
86
|
+
self.file_name = Path(url).name
|
|
87
|
+
|
|
88
|
+
if not self.file_name:
|
|
89
|
+
raise ValueError("Either url or path must be provided.")
|
|
90
|
+
super().__init__(url, path)
|
|
91
|
+
|
|
87
92
|
@override
|
|
88
93
|
async def read(self) -> bytes:
|
|
89
94
|
return await super().read()
|
|
90
95
|
|
|
91
96
|
|
|
92
97
|
class File(BaseFile):
|
|
93
|
-
def __init__(
|
|
94
|
-
self, url: str | None = None, path: str | None = None
|
|
95
|
-
) -> None:
|
|
98
|
+
def __init__(self, url: str | None = None, path: str | None = None) -> None:
|
|
96
99
|
self.file_name: str = ""
|
|
97
100
|
if path:
|
|
98
101
|
self.file_name = Path(path).name
|
pymax/interfaces.py
CHANGED
|
@@ -4,23 +4,24 @@ import ssl
|
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from collections.abc import Awaitable, Callable
|
|
6
6
|
from logging import Logger
|
|
7
|
-
from pathlib import Path
|
|
8
7
|
from typing import TYPE_CHECKING, Any, Literal
|
|
9
8
|
|
|
10
|
-
import websockets
|
|
11
|
-
|
|
12
|
-
from .filters import Filter
|
|
13
9
|
from .payloads import UserAgentPayload
|
|
14
10
|
from .static.constant import DEFAULT_TIMEOUT
|
|
15
11
|
from .static.enum import Opcode
|
|
16
12
|
from .types import Channel, Chat, Dialog, Me, Message, User
|
|
17
13
|
|
|
18
14
|
if TYPE_CHECKING:
|
|
15
|
+
from pathlib import Path
|
|
19
16
|
from uuid import UUID
|
|
20
17
|
|
|
18
|
+
import websockets
|
|
19
|
+
|
|
20
|
+
from pymax import AttachType
|
|
21
21
|
from pymax.types import ReactionInfo
|
|
22
22
|
|
|
23
23
|
from .crud import Database
|
|
24
|
+
from .filters import Filter
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class ClientProtocol(ABC):
|
|
@@ -52,7 +53,10 @@ class ClientProtocol(ABC):
|
|
|
52
53
|
self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
53
54
|
self._recv_task: asyncio.Task[Any] | None = None
|
|
54
55
|
self._incoming: asyncio.Queue[dict[str, Any]] | None = None
|
|
55
|
-
self._file_upload_waiters: dict[
|
|
56
|
+
self._file_upload_waiters: dict[
|
|
57
|
+
int,
|
|
58
|
+
asyncio.Future[dict[str, Any]],
|
|
59
|
+
] = {}
|
|
56
60
|
self.user_agent = UserAgentPayload()
|
|
57
61
|
self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
|
|
58
62
|
self._outgoing_task: asyncio.Task[Any] | None = None
|
pymax/mixins/auth.py
CHANGED
|
@@ -139,7 +139,7 @@ class AuthMixin(ClientProtocol):
|
|
|
139
139
|
self.logger.info("Starting registration flow")
|
|
140
140
|
|
|
141
141
|
request_code_payload = await self.request_code(self.phone)
|
|
142
|
-
temp_token = request_code_payload
|
|
142
|
+
temp_token = request_code_payload
|
|
143
143
|
|
|
144
144
|
if not temp_token or not isinstance(temp_token, str):
|
|
145
145
|
self.logger.critical("Failed to request code: token missing")
|
pymax/mixins/channel.py
CHANGED
|
@@ -67,11 +67,11 @@ class ChannelMixin(ClientProtocol):
|
|
|
67
67
|
) -> tuple[list[Member], int | None]:
|
|
68
68
|
data = await self._send_and_wait(
|
|
69
69
|
opcode=Opcode.CHAT_MEMBERS,
|
|
70
|
-
payload=payload.model_dump(by_alias=True),
|
|
70
|
+
payload=payload.model_dump(by_alias=True, exclude_none=True),
|
|
71
71
|
)
|
|
72
72
|
response_payload = data.get("payload", {})
|
|
73
|
-
if
|
|
74
|
-
|
|
73
|
+
if data.get("payload", {}).get("error"):
|
|
74
|
+
MixinsUtils.handle_error(data)
|
|
75
75
|
marker = response_payload.get("marker")
|
|
76
76
|
if isinstance(marker, str):
|
|
77
77
|
marker = int(marker)
|
|
@@ -96,7 +96,7 @@ class ChannelMixin(ClientProtocol):
|
|
|
96
96
|
async def load_members(
|
|
97
97
|
self,
|
|
98
98
|
chat_id: int,
|
|
99
|
-
marker: int = DEFAULT_MARKER_VALUE,
|
|
99
|
+
marker: int | None = DEFAULT_MARKER_VALUE,
|
|
100
100
|
count: int = DEFAULT_CHAT_MEMBERS_LIMIT,
|
|
101
101
|
) -> tuple[list[Member], int | None]:
|
|
102
102
|
"""
|
|
@@ -106,11 +106,11 @@ class ChannelMixin(ClientProtocol):
|
|
|
106
106
|
chat_id (int): Идентификатор канала
|
|
107
107
|
marker (int, optional): Маркер для пагинации. По умолчанию DEFAULT_MARKER_VALUE
|
|
108
108
|
count (int, optional): Количество членов для загрузки. По умолчанию DEFAULT_CHAT_MEMBERS_LIMIT.
|
|
109
|
-
Данное значение лучше не менять, так как веб-клиент загружает именно столько.
|
|
110
109
|
|
|
111
110
|
Returns:
|
|
112
|
-
list[Member]: Список участников канала
|
|
111
|
+
tuple[list[Member], int | None]: Список участников канала и маркер для следующей страницы
|
|
113
112
|
"""
|
|
113
|
+
|
|
114
114
|
payload = GetGroupMembersPayload(chat_id=chat_id, marker=marker, count=count)
|
|
115
115
|
return await self._query_members(payload)
|
|
116
116
|
|
pymax/mixins/group.py
CHANGED
|
@@ -10,6 +10,7 @@ from pymax.payloads import (
|
|
|
10
10
|
CreateGroupAttach,
|
|
11
11
|
CreateGroupMessage,
|
|
12
12
|
CreateGroupPayload,
|
|
13
|
+
GetChatInfoPayload,
|
|
13
14
|
InviteUsersPayload,
|
|
14
15
|
JoinChatPayload,
|
|
15
16
|
RemoveUsersPayload,
|
|
@@ -258,3 +259,62 @@ class GroupMixin(ClientProtocol):
|
|
|
258
259
|
raise Error("no_chat", "Chat data missing in response", "Chat Error")
|
|
259
260
|
|
|
260
261
|
return chat
|
|
262
|
+
|
|
263
|
+
async def get_chats(self, chat_ids: list[int]) -> list[Chat]:
|
|
264
|
+
"""
|
|
265
|
+
Получает информацию о группах по их ID
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
chat_ids (list[int]): Список идентификаторов групп.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
list[Chat]: Список объектов Chat.
|
|
272
|
+
"""
|
|
273
|
+
missed_chat_ids = [
|
|
274
|
+
chat_id for chat_id in chat_ids if await self._get_chat(chat_id) is None
|
|
275
|
+
]
|
|
276
|
+
if missed_chat_ids:
|
|
277
|
+
payload = GetChatInfoPayload(chat_ids=missed_chat_ids).model_dump(
|
|
278
|
+
by_alias=True
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
chats: list[Chat] = [
|
|
282
|
+
chat
|
|
283
|
+
for chat_id in chat_ids
|
|
284
|
+
if (chat := await self._get_chat(chat_id)) is not None
|
|
285
|
+
]
|
|
286
|
+
return chats
|
|
287
|
+
|
|
288
|
+
data = await self._send_and_wait(opcode=Opcode.CHAT_INFO, payload=payload)
|
|
289
|
+
|
|
290
|
+
if data.get("payload", {}).get("error"):
|
|
291
|
+
MixinsUtils.handle_error(data)
|
|
292
|
+
|
|
293
|
+
chats_data = data["payload"].get("chats", [])
|
|
294
|
+
chats: list[Chat] = []
|
|
295
|
+
for chat_dict in chats_data:
|
|
296
|
+
chat = Chat.from_dict(chat_dict)
|
|
297
|
+
chats.append(chat)
|
|
298
|
+
cached_chat = await self._get_chat(chat.id)
|
|
299
|
+
if cached_chat is None:
|
|
300
|
+
self.chats.append(chat)
|
|
301
|
+
else:
|
|
302
|
+
idx = self.chats.index(cached_chat)
|
|
303
|
+
self.chats[idx] = chat
|
|
304
|
+
|
|
305
|
+
return chats
|
|
306
|
+
|
|
307
|
+
async def get_chat(self, chat_id: int) -> Chat:
|
|
308
|
+
"""
|
|
309
|
+
Получает информацию о группе по ее ID
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
chat_id (int): Идентификатор группы.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Chat: Объект Chat.
|
|
316
|
+
"""
|
|
317
|
+
chats = await self.get_chats([chat_id])
|
|
318
|
+
if not chats:
|
|
319
|
+
raise Error("no_chat", "Chat not found in response", "Chat Error")
|
|
320
|
+
return chats[0]
|
pymax/mixins/handler.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from collections.abc import Awaitable, Callable
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
|
-
from pymax.
|
|
4
|
+
from pymax.filters import Filter
|
|
5
|
+
from pymax.interfaces import ClientProtocol
|
|
5
6
|
from pymax.types import Chat, Message, ReactionInfo
|
|
6
7
|
|
|
7
8
|
|
pymax/mixins/message.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
|
+
from http import HTTPStatus
|
|
3
4
|
|
|
4
5
|
import aiohttp
|
|
5
6
|
from aiohttp import ClientSession
|
|
6
7
|
|
|
7
8
|
from pymax.exceptions import Error
|
|
8
|
-
from pymax.files import File, Photo
|
|
9
|
+
from pymax.files import File, Photo, Video
|
|
9
10
|
from pymax.formatting import Formatting
|
|
10
11
|
from pymax.interfaces import ClientProtocol
|
|
11
12
|
from pymax.mixins.utils import MixinsUtils
|
|
@@ -27,6 +28,7 @@ from pymax.payloads import (
|
|
|
27
28
|
SendMessagePayload,
|
|
28
29
|
SendMessagePayloadMessage,
|
|
29
30
|
UploadPayload,
|
|
31
|
+
VideoAttachPayload,
|
|
30
32
|
)
|
|
31
33
|
from pymax.static.constant import DEFAULT_TIMEOUT
|
|
32
34
|
from pymax.static.enum import AttachType, Opcode
|
|
@@ -35,6 +37,7 @@ from pymax.types import (
|
|
|
35
37
|
FileRequest,
|
|
36
38
|
Message,
|
|
37
39
|
ReactionInfo,
|
|
40
|
+
VideoAttach,
|
|
38
41
|
VideoRequest,
|
|
39
42
|
)
|
|
40
43
|
|
|
@@ -79,9 +82,8 @@ class MessageMixin(ClientProtocol):
|
|
|
79
82
|
data=file_bytes,
|
|
80
83
|
) as response,
|
|
81
84
|
):
|
|
82
|
-
if response.status !=
|
|
85
|
+
if response.status != HTTPStatus.OK:
|
|
83
86
|
self.logger.error(f"Upload failed with status {response.status}")
|
|
84
|
-
# cleanup waiter
|
|
85
87
|
self._file_upload_waiters.pop(int(file_id), None)
|
|
86
88
|
return None
|
|
87
89
|
|
|
@@ -97,7 +99,76 @@ class MessageMixin(ClientProtocol):
|
|
|
97
99
|
return None
|
|
98
100
|
except Exception as e:
|
|
99
101
|
self.logger.exception("Upload file failed: %s", str(e))
|
|
100
|
-
|
|
102
|
+
raise e
|
|
103
|
+
|
|
104
|
+
async def _upload_video(self, video: Video) -> None | Attach:
|
|
105
|
+
try:
|
|
106
|
+
self.logger.info("Uploading video")
|
|
107
|
+
payload = UploadPayload().model_dump(by_alias=True)
|
|
108
|
+
data = await self._send_and_wait(
|
|
109
|
+
opcode=Opcode.VIDEO_UPLOAD,
|
|
110
|
+
payload=payload,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if data.get("payload", {}).get("error"):
|
|
114
|
+
MixinsUtils.handle_error(data)
|
|
115
|
+
|
|
116
|
+
url = data.get("payload", {}).get("info", [None])[0].get("url", None)
|
|
117
|
+
video_id = (
|
|
118
|
+
data.get("payload", {}).get("info", [None])[0].get("videoId", None)
|
|
119
|
+
)
|
|
120
|
+
if not url or not video_id:
|
|
121
|
+
self.logger.error("No upload URL or video ID received")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
token = data.get("payload", {}).get("info", [None])[0].get("token", None)
|
|
125
|
+
if not token:
|
|
126
|
+
self.logger.error("No upload token received")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
file_bytes = await video.read()
|
|
130
|
+
|
|
131
|
+
headers = {
|
|
132
|
+
"Content-Disposition": f"attachment; filename={video.file_name}",
|
|
133
|
+
"Content-Range": f"0-{len(file_bytes) - 1}/{len(file_bytes)}",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
loop = asyncio.get_running_loop()
|
|
137
|
+
fut: asyncio.Future[dict] = loop.create_future()
|
|
138
|
+
try:
|
|
139
|
+
self._file_upload_waiters[int(video_id)] = fut
|
|
140
|
+
except Exception:
|
|
141
|
+
self.logger.exception("Failed to register file upload waiter")
|
|
142
|
+
|
|
143
|
+
async with (
|
|
144
|
+
ClientSession() as session,
|
|
145
|
+
session.post(
|
|
146
|
+
url=url,
|
|
147
|
+
headers=headers,
|
|
148
|
+
data=file_bytes,
|
|
149
|
+
) as response,
|
|
150
|
+
):
|
|
151
|
+
if response.status != HTTPStatus.OK:
|
|
152
|
+
self.logger.error(f"Upload failed with status {response.status}")
|
|
153
|
+
self._file_upload_waiters.pop(int(video_id), None)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
await asyncio.wait_for(fut, timeout=DEFAULT_TIMEOUT)
|
|
158
|
+
return Attach(
|
|
159
|
+
_type=AttachType.VIDEO, video_id=video_id, token=token
|
|
160
|
+
)
|
|
161
|
+
except asyncio.TimeoutError:
|
|
162
|
+
self.logger.error(
|
|
163
|
+
"Timed out waiting for video processing notification for videoId=%s",
|
|
164
|
+
video_id,
|
|
165
|
+
)
|
|
166
|
+
self._file_upload_waiters.pop(int(video_id), None)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self.logger.exception("Upload video failed: %s", str(e))
|
|
171
|
+
raise e
|
|
101
172
|
|
|
102
173
|
async def _upload_photo(self, photo: Photo) -> None | Attach:
|
|
103
174
|
try:
|
|
@@ -137,7 +208,7 @@ class MessageMixin(ClientProtocol):
|
|
|
137
208
|
data=form,
|
|
138
209
|
) as response,
|
|
139
210
|
):
|
|
140
|
-
if response.status !=
|
|
211
|
+
if response.status != HTTPStatus.OK:
|
|
141
212
|
self.logger.error(f"Upload failed with status {response.status}")
|
|
142
213
|
return None
|
|
143
214
|
|
|
@@ -161,7 +232,7 @@ class MessageMixin(ClientProtocol):
|
|
|
161
232
|
self.logger.exception("Upload photo failed: %s", str(e))
|
|
162
233
|
return None
|
|
163
234
|
|
|
164
|
-
async def _upload_attachment(self, attach: Photo | File) -> dict | None:
|
|
235
|
+
async def _upload_attachment(self, attach: Photo | File | Video) -> dict | None:
|
|
165
236
|
if isinstance(attach, Photo):
|
|
166
237
|
uploaded = await self._upload_photo(attach)
|
|
167
238
|
if uploaded and uploaded.photo_token:
|
|
@@ -174,6 +245,12 @@ class MessageMixin(ClientProtocol):
|
|
|
174
245
|
return AttachFilePayload(file_id=uploaded.file_id).model_dump(
|
|
175
246
|
by_alias=True
|
|
176
247
|
)
|
|
248
|
+
elif isinstance(attach, Video):
|
|
249
|
+
uploaded = await self._upload_video(attach)
|
|
250
|
+
if uploaded and uploaded.video_id and uploaded.token:
|
|
251
|
+
return VideoAttachPayload(
|
|
252
|
+
video_id=uploaded.video_id, token=uploaded.token
|
|
253
|
+
).model_dump(by_alias=True)
|
|
177
254
|
self.logger.error(f"Attachment upload failed for {attach}")
|
|
178
255
|
return None
|
|
179
256
|
|
|
@@ -181,9 +258,9 @@ class MessageMixin(ClientProtocol):
|
|
|
181
258
|
self,
|
|
182
259
|
text: str,
|
|
183
260
|
chat_id: int,
|
|
184
|
-
notify: bool,
|
|
185
|
-
attachment: Photo | File | None = None,
|
|
186
|
-
attachments: list[Photo | File] | None = None,
|
|
261
|
+
notify: bool = True,
|
|
262
|
+
attachment: Photo | File | Video | None = None,
|
|
263
|
+
attachments: list[Photo | File | Video] | None = None,
|
|
187
264
|
reply_to: int | None = None,
|
|
188
265
|
use_queue: bool = False,
|
|
189
266
|
) -> Message | None:
|
|
@@ -268,8 +345,8 @@ class MessageMixin(ClientProtocol):
|
|
|
268
345
|
chat_id: int,
|
|
269
346
|
message_id: int,
|
|
270
347
|
text: str,
|
|
271
|
-
attachment: Photo | None = None,
|
|
272
|
-
attachments: list[Photo] | None = None,
|
|
348
|
+
attachment: Photo | File | Video | None = None,
|
|
349
|
+
attachments: list[Photo | Video | File] | None = None,
|
|
273
350
|
use_queue: bool = False,
|
|
274
351
|
) -> Message | None:
|
|
275
352
|
self.logger.info(
|
|
@@ -480,7 +557,7 @@ class MessageMixin(ClientProtocol):
|
|
|
480
557
|
video_id=video_id,
|
|
481
558
|
).model_dump(by_alias=True)
|
|
482
559
|
|
|
483
|
-
|
|
560
|
+
data = await self._send_and_wait(opcode=Opcode.VIDEO_PLAY, payload=payload)
|
|
484
561
|
|
|
485
562
|
if data.get("payload", {}).get("error"):
|
|
486
563
|
MixinsUtils.handle_error(data)
|
pymax/mixins/self.py
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
1
4
|
from pymax.exceptions import Error
|
|
2
5
|
from pymax.interfaces import ClientProtocol
|
|
3
6
|
from pymax.mixins.utils import MixinsUtils
|
|
4
|
-
from pymax.payloads import
|
|
7
|
+
from pymax.payloads import (
|
|
8
|
+
ChangeProfilePayload,
|
|
9
|
+
CreateFolderPayload,
|
|
10
|
+
DeleteFolderPayload,
|
|
11
|
+
GetFolderPayload,
|
|
12
|
+
UpdateFolderPayload,
|
|
13
|
+
)
|
|
5
14
|
from pymax.static.enum import Opcode
|
|
15
|
+
from pymax.types import Folder, FolderList, FolderUpdate
|
|
6
16
|
|
|
7
17
|
|
|
8
18
|
class SelfMixin(ClientProtocol):
|
|
@@ -39,3 +49,107 @@ class SelfMixin(ClientProtocol):
|
|
|
39
49
|
MixinsUtils.handle_error(data)
|
|
40
50
|
|
|
41
51
|
return True
|
|
52
|
+
|
|
53
|
+
async def create_folder(
|
|
54
|
+
self, title: str, chat_include: list[int], filters: list[Any] | None = None
|
|
55
|
+
) -> FolderUpdate:
|
|
56
|
+
"""
|
|
57
|
+
Создает папку для чатов
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
title (str): Название папки
|
|
61
|
+
chat_include (list[int]): Список ID чатов для включения в папку
|
|
62
|
+
filters (list[Any] | None, optional): Список фильтров для папки (Неизвестный параметр, использование на свой страх и риск)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
bool: True, если папка создана
|
|
66
|
+
"""
|
|
67
|
+
self.logger.info("Creating folder")
|
|
68
|
+
|
|
69
|
+
payload = CreateFolderPayload(
|
|
70
|
+
id=str(uuid4()),
|
|
71
|
+
title=title,
|
|
72
|
+
include=chat_include,
|
|
73
|
+
filters=filters or [],
|
|
74
|
+
).model_dump(by_alias=True)
|
|
75
|
+
|
|
76
|
+
data = await self._send_and_wait(opcode=Opcode.FOLDERS_UPDATE, payload=payload)
|
|
77
|
+
|
|
78
|
+
if data.get("payload", {}).get("error"):
|
|
79
|
+
MixinsUtils.handle_error(data)
|
|
80
|
+
|
|
81
|
+
return FolderUpdate.from_dict(data.get("payload", {}))
|
|
82
|
+
|
|
83
|
+
async def get_folders(self, folder_sync: int = 0) -> FolderList:
|
|
84
|
+
"""
|
|
85
|
+
Получает список папок
|
|
86
|
+
Args:
|
|
87
|
+
folder_sync (int, optional): Синхронизационный маркер папок. По умолчанию 0. (Неизвестный параметр, использование на свой страх и риск)
|
|
88
|
+
Returns:
|
|
89
|
+
FolderList: Список папок
|
|
90
|
+
"""
|
|
91
|
+
self.logger.info("Fetching folders")
|
|
92
|
+
|
|
93
|
+
payload = GetFolderPayload(folder_sync=folder_sync).model_dump(by_alias=True)
|
|
94
|
+
|
|
95
|
+
data = await self._send_and_wait(opcode=Opcode.FOLDERS_GET, payload=payload)
|
|
96
|
+
|
|
97
|
+
if data.get("payload", {}).get("error"):
|
|
98
|
+
MixinsUtils.handle_error(data)
|
|
99
|
+
|
|
100
|
+
return FolderList.from_dict(data.get("payload", {}))
|
|
101
|
+
|
|
102
|
+
async def update_folder(
|
|
103
|
+
self,
|
|
104
|
+
folder_id: str,
|
|
105
|
+
title: str,
|
|
106
|
+
chat_include: list[int] | None = None,
|
|
107
|
+
filters: list[Any] | None = None,
|
|
108
|
+
options: list[Any] | None = None,
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Обновляет папку для чатов
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
folder_id (str): ID папки
|
|
115
|
+
title (str): Название папки
|
|
116
|
+
chat_include (list[int] | None, optional): Список ID чатов для включения в папку. По умолчанию None.
|
|
117
|
+
filters (list[Any] | None, optional): Список фильтров для папки. По умолчанию None.
|
|
118
|
+
options (list[Any] | None, optional): Список опций для папки. По умолчанию None.
|
|
119
|
+
Returns:
|
|
120
|
+
"""
|
|
121
|
+
self.logger.info("Updating folder")
|
|
122
|
+
|
|
123
|
+
payload = UpdateFolderPayload(
|
|
124
|
+
id=folder_id,
|
|
125
|
+
title=title,
|
|
126
|
+
include=chat_include or [],
|
|
127
|
+
filters=filters or [],
|
|
128
|
+
options=options or [],
|
|
129
|
+
).model_dump(by_alias=True, exclude_none=True)
|
|
130
|
+
|
|
131
|
+
data = await self._send_and_wait(opcode=Opcode.FOLDERS_UPDATE, payload=payload)
|
|
132
|
+
|
|
133
|
+
if data.get("payload", {}).get("error"):
|
|
134
|
+
MixinsUtils.handle_error(data)
|
|
135
|
+
|
|
136
|
+
return FolderUpdate.from_dict(data.get("payload", {}))
|
|
137
|
+
|
|
138
|
+
async def delete_folder(self, folder_id: str) -> FolderUpdate:
|
|
139
|
+
"""
|
|
140
|
+
Удаляет папку для чатов
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
folder_id (str): ID папки
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
bool: True, если папка удалена
|
|
147
|
+
"""
|
|
148
|
+
self.logger.info("Deleting folder")
|
|
149
|
+
|
|
150
|
+
payload = DeleteFolderPayload(folder_ids=[folder_id]).model_dump(by_alias=True)
|
|
151
|
+
data = await self._send_and_wait(opcode=Opcode.FOLDERS_DELETE, payload=payload)
|
|
152
|
+
if data.get("payload", {}).get("error"):
|
|
153
|
+
MixinsUtils.handle_error(data)
|
|
154
|
+
|
|
155
|
+
return FolderUpdate.from_dict(data.get("payload", {}))
|
pymax/mixins/socket.py
CHANGED
|
@@ -92,37 +92,33 @@ class SocketMixin(ClientProtocol):
|
|
|
92
92
|
payload_len_b = payload_len.to_bytes(4, "big")
|
|
93
93
|
return ver_b + cmd_b + seq_b + opcode_b + payload_len_b + payload_bytes
|
|
94
94
|
|
|
95
|
-
async def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"""
|
|
95
|
+
async def connect(self, user_agent: UserAgentPayload) -> dict[str, Any]:
|
|
96
|
+
if sys.version_info[:2] == (3, 12):
|
|
97
|
+
self.logger.warning(
|
|
98
|
+
"""
|
|
100
99
|
===============================================================
|
|
101
100
|
⚠️⚠️ \033[0;31mWARNING: Python 3.12 detected!\033[0m ⚠️⚠️
|
|
102
101
|
Socket connections may be unstable, SSL issues are possible.
|
|
103
102
|
===============================================================
|
|
104
103
|
"""
|
|
105
|
-
)
|
|
106
|
-
self.logger.info("Connecting to socket %s:%s", self.host, self.port)
|
|
107
|
-
loop = asyncio.get_running_loop()
|
|
108
|
-
raw_sock = await loop.run_in_executor(
|
|
109
|
-
None, lambda: socket.create_connection((self.host, self.port))
|
|
110
104
|
)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
105
|
+
self.logger.info("Connecting to socket %s:%s", self.host, self.port)
|
|
106
|
+
loop = asyncio.get_running_loop()
|
|
107
|
+
raw_sock = await loop.run_in_executor(
|
|
108
|
+
None, lambda: socket.create_connection((self.host, self.port))
|
|
109
|
+
)
|
|
110
|
+
self._socket = self._ssl_context.wrap_socket(
|
|
111
|
+
raw_sock, server_hostname=self.host
|
|
112
|
+
)
|
|
113
|
+
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
114
|
+
self.is_connected = True
|
|
115
|
+
self._incoming = asyncio.Queue()
|
|
116
|
+
self._outgoing = asyncio.Queue()
|
|
117
|
+
self._pending = {}
|
|
118
|
+
self._recv_task = asyncio.create_task(self._recv_loop())
|
|
119
|
+
self._outgoing_task = asyncio.create_task(self._outgoing_loop())
|
|
120
|
+
self.logger.info("Socket connected, starting handshake")
|
|
121
|
+
return await self._handshake(user_agent)
|
|
126
122
|
|
|
127
123
|
async def _handshake(self, user_agent: UserAgentPayload) -> dict[str, Any]:
|
|
128
124
|
try:
|
|
@@ -345,6 +341,35 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
345
341
|
self.logger.exception(
|
|
346
342
|
"Error in on_chat_update_handler: %s", e
|
|
347
343
|
)
|
|
344
|
+
|
|
345
|
+
try: # TODO: переделать, временное решение
|
|
346
|
+
if data_item.get("opcode") == Opcode.NOTIF_ATTACH:
|
|
347
|
+
file_id = data_item.get("payload", {}).get(
|
|
348
|
+
"fileId", None
|
|
349
|
+
)
|
|
350
|
+
video_id = data_item.get("payload", {}).get(
|
|
351
|
+
"videoId", None
|
|
352
|
+
)
|
|
353
|
+
if file_id is not None:
|
|
354
|
+
fut = self._file_upload_waiters.pop(file_id, None)
|
|
355
|
+
if fut and not fut.done():
|
|
356
|
+
fut.set_result(data)
|
|
357
|
+
self.logger.debug(
|
|
358
|
+
"Fulfilled file upload waiter for fileId=%s",
|
|
359
|
+
file_id,
|
|
360
|
+
)
|
|
361
|
+
elif video_id is not None:
|
|
362
|
+
fut = self._file_upload_waiters.pop(video_id, None)
|
|
363
|
+
if fut and not fut.done():
|
|
364
|
+
fut.set_result(data)
|
|
365
|
+
self.logger.debug(
|
|
366
|
+
"Fulfilled file upload waiter for videoId=%s",
|
|
367
|
+
video_id,
|
|
368
|
+
)
|
|
369
|
+
except Exception:
|
|
370
|
+
self.logger.exception(
|
|
371
|
+
"Error handling file upload notification"
|
|
372
|
+
)
|
|
348
373
|
except asyncio.CancelledError:
|
|
349
374
|
self.logger.debug("Recv loop cancelled")
|
|
350
375
|
break
|
|
@@ -396,6 +421,7 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
396
421
|
) -> dict[str, Any]:
|
|
397
422
|
self._seq += 1
|
|
398
423
|
msg = BaseWebSocketMessage(
|
|
424
|
+
ver=10,
|
|
399
425
|
cmd=cmd,
|
|
400
426
|
seq=self._seq,
|
|
401
427
|
opcode=opcode.value,
|
|
@@ -416,6 +442,7 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
416
442
|
) -> dict[str, Any]:
|
|
417
443
|
if not self.is_connected or self._socket is None:
|
|
418
444
|
raise SocketNotConnectedError
|
|
445
|
+
|
|
419
446
|
sock = self.sock
|
|
420
447
|
msg = self._make_message(opcode, payload, cmd)
|
|
421
448
|
loop = asyncio.get_running_loop()
|
|
@@ -448,7 +475,7 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
448
475
|
self.logger.warning("Connection lost, reconnecting...")
|
|
449
476
|
self.is_connected = False
|
|
450
477
|
try:
|
|
451
|
-
await self.
|
|
478
|
+
await self.connect(self.user_agent)
|
|
452
479
|
except Exception as exc:
|
|
453
480
|
self.logger.exception("Reconnect failed")
|
|
454
481
|
raise exc from conn_err
|
pymax/mixins/websocket.py
CHANGED
|
@@ -167,7 +167,8 @@ class WebSocketMixin(ClientProtocol):
|
|
|
167
167
|
try: # TODO: переделать, временное решение
|
|
168
168
|
if data.get("opcode") == Opcode.NOTIF_ATTACH:
|
|
169
169
|
file_id = data.get("payload", {}).get("fileId", None)
|
|
170
|
-
|
|
170
|
+
video_id = data.get("payload", {}).get("videoId", None)
|
|
171
|
+
if file_id is not None:
|
|
171
172
|
fut = self._file_upload_waiters.pop(file_id, None)
|
|
172
173
|
if fut and not fut.done():
|
|
173
174
|
fut.set_result(data)
|
|
@@ -175,6 +176,14 @@ class WebSocketMixin(ClientProtocol):
|
|
|
175
176
|
"Fulfilled file upload waiter for fileId=%s",
|
|
176
177
|
file_id,
|
|
177
178
|
)
|
|
179
|
+
elif video_id is not None:
|
|
180
|
+
fut = self._file_upload_waiters.pop(video_id, None)
|
|
181
|
+
if fut and not fut.done():
|
|
182
|
+
fut.set_result(data)
|
|
183
|
+
self.logger.debug(
|
|
184
|
+
"Fulfilled file upload waiter for videoId=%s",
|
|
185
|
+
video_id,
|
|
186
|
+
)
|
|
178
187
|
except Exception:
|
|
179
188
|
self.logger.exception("Error handling file upload notification")
|
|
180
189
|
|
pymax/payloads.py
CHANGED
|
@@ -73,6 +73,12 @@ class AttachPhotoPayload(CamelModel):
|
|
|
73
73
|
photo_token: str
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
class VideoAttachPayload(CamelModel):
|
|
77
|
+
type: AttachType = Field(default=AttachType.VIDEO, alias="_type")
|
|
78
|
+
video_id: int
|
|
79
|
+
token: str
|
|
80
|
+
|
|
81
|
+
|
|
76
82
|
class AttachFilePayload(CamelModel):
|
|
77
83
|
type: AttachType = Field(default=AttachType.FILE, alias="_type")
|
|
78
84
|
file_id: int
|
|
@@ -88,7 +94,7 @@ class SendMessagePayloadMessage(CamelModel):
|
|
|
88
94
|
text: str
|
|
89
95
|
cid: int
|
|
90
96
|
elements: list[MessageElement]
|
|
91
|
-
attaches: list[AttachPhotoPayload | AttachFilePayload]
|
|
97
|
+
attaches: list[AttachPhotoPayload | AttachFilePayload | VideoAttachPayload]
|
|
92
98
|
link: ReplyLink | None = None
|
|
93
99
|
|
|
94
100
|
|
|
@@ -103,7 +109,7 @@ class EditMessagePayload(CamelModel):
|
|
|
103
109
|
message_id: int
|
|
104
110
|
text: str
|
|
105
111
|
elements: list[MessageElement]
|
|
106
|
-
attaches: list[AttachPhotoPayload]
|
|
112
|
+
attaches: list[AttachPhotoPayload | AttachFilePayload | VideoAttachPayload]
|
|
107
113
|
|
|
108
114
|
|
|
109
115
|
class DeleteMessagePayload(CamelModel):
|
|
@@ -196,7 +202,7 @@ class ChangeGroupProfilePayload(CamelModel):
|
|
|
196
202
|
|
|
197
203
|
class GetGroupMembersPayload(CamelModel):
|
|
198
204
|
type: Literal["MEMBER"] = "MEMBER"
|
|
199
|
-
marker: int
|
|
205
|
+
marker: int | None = None
|
|
200
206
|
chat_id: int
|
|
201
207
|
count: int
|
|
202
208
|
|
|
@@ -295,3 +301,30 @@ class RegisterPayload(CamelModel):
|
|
|
295
301
|
first_name: str
|
|
296
302
|
token: str
|
|
297
303
|
token_type: AuthType = AuthType.REGISTER
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class CreateFolderPayload(CamelModel):
|
|
307
|
+
id: str
|
|
308
|
+
title: str
|
|
309
|
+
include: list[int]
|
|
310
|
+
filters: list[Any] = []
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class GetChatInfoPayload(CamelModel):
|
|
314
|
+
chat_ids: list[int]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class GetFolderPayload(CamelModel):
|
|
318
|
+
folder_sync: int = 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class UpdateFolderPayload(CamelModel):
|
|
322
|
+
id: str
|
|
323
|
+
title: str
|
|
324
|
+
include: list[int]
|
|
325
|
+
filters: list[Any] = []
|
|
326
|
+
options: list[Any] = []
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class DeleteFolderPayload(CamelModel):
|
|
330
|
+
folder_ids: list[str]
|
pymax/static/enum.py
CHANGED
pymax/types.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
|
-
from typing_extensions import override
|
|
3
|
+
from typing_extensions import Self, override
|
|
4
4
|
|
|
5
5
|
from .static.enum import (
|
|
6
6
|
AccessType,
|
|
@@ -1052,3 +1052,115 @@ class Session:
|
|
|
1052
1052
|
@override
|
|
1053
1053
|
def __str__(self) -> str:
|
|
1054
1054
|
return f"Session: {self.client} from {self.location} at {self.time} (current={self.current})"
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
class Folder:
|
|
1058
|
+
def __init__(
|
|
1059
|
+
self,
|
|
1060
|
+
source_id: int,
|
|
1061
|
+
include: list[int],
|
|
1062
|
+
options: list[Any],
|
|
1063
|
+
update_time: int,
|
|
1064
|
+
id: str,
|
|
1065
|
+
filters: list[Any],
|
|
1066
|
+
title: str,
|
|
1067
|
+
) -> None:
|
|
1068
|
+
self.source_id = source_id
|
|
1069
|
+
self.include = include
|
|
1070
|
+
self.options = options
|
|
1071
|
+
self.update_time = update_time
|
|
1072
|
+
self.id = id
|
|
1073
|
+
self.filters = filters
|
|
1074
|
+
self.title = title
|
|
1075
|
+
|
|
1076
|
+
@classmethod
|
|
1077
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
1078
|
+
return cls(
|
|
1079
|
+
source_id=data.get("sourceId", 0),
|
|
1080
|
+
include=data.get("include", []),
|
|
1081
|
+
options=data.get("options", []),
|
|
1082
|
+
update_time=data.get("updateTime", 0),
|
|
1083
|
+
id=data.get("id", ""),
|
|
1084
|
+
filters=data.get("filters", []),
|
|
1085
|
+
title=data.get("title", ""),
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
@override
|
|
1089
|
+
def __repr__(self) -> str:
|
|
1090
|
+
return (
|
|
1091
|
+
f"Folder(id={self.id!r}, title={self.title!r}, source_id={self.source_id!r}, "
|
|
1092
|
+
f"include={self.include!r}, options={self.options!r}, "
|
|
1093
|
+
f"update_time={self.update_time!r}, filters={self.filters!r})"
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
@override
|
|
1097
|
+
def __str__(self) -> str:
|
|
1098
|
+
return f"Folder: {self.title} ({self.id})"
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
class FolderUpdate:
|
|
1102
|
+
def __init__(
|
|
1103
|
+
self, folder_order: list[str] | None, folder: Folder | None, folder_sync: int
|
|
1104
|
+
) -> None:
|
|
1105
|
+
self.folder_order = folder_order
|
|
1106
|
+
self.folder = folder
|
|
1107
|
+
self.folder_sync = folder_sync
|
|
1108
|
+
|
|
1109
|
+
@classmethod
|
|
1110
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
1111
|
+
folder_order = data.get("foldersOrder", [])
|
|
1112
|
+
folder_sync = data.get("folderSync", 0)
|
|
1113
|
+
folder_data = data.get("folder", {})
|
|
1114
|
+
folder = Folder.from_dict(folder_data)
|
|
1115
|
+
return cls(
|
|
1116
|
+
folder_order=folder_order,
|
|
1117
|
+
folder=folder,
|
|
1118
|
+
folder_sync=folder_sync,
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
@override
|
|
1122
|
+
def __repr__(self) -> str:
|
|
1123
|
+
return (
|
|
1124
|
+
f"FolderUpdate(folder_order={self.folder_order!r}, "
|
|
1125
|
+
f"folder={self.folder!r}, folder_sync={self.folder_sync!r})"
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
@override
|
|
1129
|
+
def __str__(self) -> str:
|
|
1130
|
+
return f"FolderUpdate: {self.folder.title} ({self.folder.id})"
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
class FolderList:
|
|
1134
|
+
def __init__(
|
|
1135
|
+
self,
|
|
1136
|
+
folders_order: list[str],
|
|
1137
|
+
folders: list[Folder],
|
|
1138
|
+
folder_sync: int,
|
|
1139
|
+
all_filter_exclude_folders: list[Any] | None = None,
|
|
1140
|
+
) -> None:
|
|
1141
|
+
self.folders_order = folders_order
|
|
1142
|
+
self.folders = folders
|
|
1143
|
+
self.all_filter_exclude_folders = all_filter_exclude_folders or []
|
|
1144
|
+
self.folder_sync = folder_sync
|
|
1145
|
+
|
|
1146
|
+
@classmethod
|
|
1147
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
1148
|
+
return cls(
|
|
1149
|
+
folders_order=data.get("foldersOrder", []),
|
|
1150
|
+
folders=[Folder.from_dict(f) for f in data.get("folders", [])],
|
|
1151
|
+
all_filter_exclude_folders=data.get("allFilterExcludeFolders", []),
|
|
1152
|
+
folder_sync=data.get("folderSync", 0),
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
@override
|
|
1156
|
+
def __repr__(self) -> str:
|
|
1157
|
+
return (
|
|
1158
|
+
f"FolderList(folders_order={self.folders_order!r}, "
|
|
1159
|
+
f"folders={self.folders!r}, "
|
|
1160
|
+
f"all_filter_exclude_folders={self.all_filter_exclude_folders!r}, "
|
|
1161
|
+
f"folder_sync={self.folder_sync!r})"
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
@override
|
|
1165
|
+
def __str__(self) -> str:
|
|
1166
|
+
return f"FolderList: {len(self.folders)} folders"
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
-
pymax/core.py,sha256=QEERtU32ODmEIf1XFCDhD18a29yRW5TB97NjtusMUCg,15644
|
|
3
|
-
pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
|
|
4
|
-
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
|
-
pymax/files.py,sha256=dRuOpvoJZWiH4xa_HVGyqQ-_Zzj-sVikElHmrPjwgs0,3166
|
|
6
|
-
pymax/filters.py,sha256=4hehzyQlyxBxLiEhtSH_KQdFHx8gYaqyIOOD72EsSS0,1536
|
|
7
|
-
pymax/formatter.py,sha256=OVsTwambHhXlOMd0wVECJWuB_S2wSEVKdMLCzgPcvYQ,859
|
|
8
|
-
pymax/formatting.py,sha256=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
|
|
9
|
-
pymax/interfaces.py,sha256=OqYTiTUs6HqTkx3I3CU34q-En8nLS1Rx2hRlmEq65V4,3643
|
|
10
|
-
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
|
-
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
-
pymax/payloads.py,sha256=S1dJwDPanFfIdY_NlXN2epVyibmmL9bceltgLVmEtTA,6304
|
|
13
|
-
pymax/types.py,sha256=RaLn9bUpkxO0SKbDMIHnoFeqV6gqOl2pKDNCa2LxTRI,32102
|
|
14
|
-
pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
|
|
15
|
-
pymax/mixins/auth.py,sha256=zkMkALjvb2427g1DMcvUIKsOQgw1y8d-tEo3jlyNiWQ,6744
|
|
16
|
-
pymax/mixins/channel.py,sha256=dMuJRnbqZisN8kcPFCCe1sIOOBQl2uT4P49PpZXcoKE,5206
|
|
17
|
-
pymax/mixins/group.py,sha256=7oa7RpiqnlcnAsnIHOfSiujNYAzUZ9lkTy9NGW5KVUE,8654
|
|
18
|
-
pymax/mixins/handler.py,sha256=TuO5bHK6qwJ-Wdh3lMg6uWaG6IwNPOUTCnMw2PkCFjA,5872
|
|
19
|
-
pymax/mixins/message.py,sha256=ezU9d6r4MkYjH67gZ9SFLYPKqo4Nb6lswqDsEW5p-Bg,22329
|
|
20
|
-
pymax/mixins/self.py,sha256=tDQrUdUpsCu7qGkWLtKxTfTHPHU5_r3qsn-eptHG2KY,1198
|
|
21
|
-
pymax/mixins/socket.py,sha256=VsQSDyzP2xvQp-V8SgeOBn3vVQfWwi0m_wMIvlzu2lY,25517
|
|
22
|
-
pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
|
|
23
|
-
pymax/mixins/user.py,sha256=5utoK7Z-7lySOg0PEO69b6h_3kjfcnV-YydTZIdNj8g,7120
|
|
24
|
-
pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
|
|
25
|
-
pymax/mixins/websocket.py,sha256=Rfn3PFfmey2u3e3xnebNeT9VoxBF9Dq20xM8ljaDiII,19286
|
|
26
|
-
pymax/static/constant.py,sha256=Q1NrmaRj17Gdhk3FmUp3HIwrad1TDorq3wFdQlOCzN8,1027
|
|
27
|
-
pymax/static/enum.py,sha256=c_QaLU0Ephe4SuKFIpwpmrf_HCutc34JJ6o4Ik1E6_g,4582
|
|
28
|
-
maxapi_python-1.1.19.dist-info/METADATA,sha256=2tRU1Um8ZR6LHzm83ze-bWs0V5FI6ru6DNYHbi4JBw8,6245
|
|
29
|
-
maxapi_python-1.1.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
30
|
-
maxapi_python-1.1.19.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
31
|
-
maxapi_python-1.1.19.dist-info/RECORD,,
|
|
File without changes
|