maxapi-python 1.1.16__py3-none-any.whl → 1.1.17__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.16.dist-info → maxapi_python-1.1.17.dist-info}/METADATA +1 -1
- {maxapi_python-1.1.16.dist-info → maxapi_python-1.1.17.dist-info}/RECORD +10 -10
- pymax/__init__.py +23 -17
- pymax/core.py +104 -36
- pymax/interfaces.py +3 -6
- pymax/mixins/websocket.py +22 -39
- pymax/static/enum.py +1 -0
- pymax/types.py +60 -33
- {maxapi_python-1.1.16.dist-info → maxapi_python-1.1.17.dist-info}/WHEEL +0 -0
- {maxapi_python-1.1.16.dist-info → maxapi_python-1.1.17.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
pymax/__init__.py,sha256=
|
|
2
|
-
pymax/core.py,sha256=
|
|
1
|
+
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
+
pymax/core.py,sha256=uSmIYVBqiO-3v0wtehObnvOEJ3n_dpG62TXTKwmdFf4,13838
|
|
3
3
|
pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
|
|
4
4
|
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
5
|
pymax/files.py,sha256=dRuOpvoJZWiH4xa_HVGyqQ-_Zzj-sVikElHmrPjwgs0,3166
|
|
6
6
|
pymax/filters.py,sha256=4hehzyQlyxBxLiEhtSH_KQdFHx8gYaqyIOOD72EsSS0,1536
|
|
7
7
|
pymax/formatter.py,sha256=OVsTwambHhXlOMd0wVECJWuB_S2wSEVKdMLCzgPcvYQ,859
|
|
8
8
|
pymax/formatting.py,sha256=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
|
|
9
|
-
pymax/interfaces.py,sha256=
|
|
9
|
+
pymax/interfaces.py,sha256=WWKNGT725GXuYneS9gCOAC6RNtySRs-BTU0fQLyh2OQ,3399
|
|
10
10
|
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
11
|
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
12
|
pymax/payloads.py,sha256=qaafULDGBXsQ7gNFC374wZVUwN5tzJLHwkxtAmglOzU,6292
|
|
13
|
-
pymax/types.py,sha256=
|
|
13
|
+
pymax/types.py,sha256=RaLn9bUpkxO0SKbDMIHnoFeqV6gqOl2pKDNCa2LxTRI,32102
|
|
14
14
|
pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
|
|
15
15
|
pymax/mixins/auth.py,sha256=H4Zp3n8cwpv4Q3Mn1_Kb7Oh9DbTL7T9GcWJ6R1JN7ls,6672
|
|
16
16
|
pymax/mixins/channel.py,sha256=dMuJRnbqZisN8kcPFCCe1sIOOBQl2uT4P49PpZXcoKE,5206
|
|
@@ -22,10 +22,10 @@ pymax/mixins/socket.py,sha256=j6XTo_M3rNw-az2PfSW6oJ_YHg9M7cWARY4cXpMllDY,22256
|
|
|
22
22
|
pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
|
|
23
23
|
pymax/mixins/user.py,sha256=5utoK7Z-7lySOg0PEO69b6h_3kjfcnV-YydTZIdNj8g,7120
|
|
24
24
|
pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
|
|
25
|
-
pymax/mixins/websocket.py,sha256=
|
|
25
|
+
pymax/mixins/websocket.py,sha256=LaL-okzhJCyS3uWV7xsCCKnuff_rooKjAoZ8vkaintY,16817
|
|
26
26
|
pymax/static/constant.py,sha256=Q1NrmaRj17Gdhk3FmUp3HIwrad1TDorq3wFdQlOCzN8,1027
|
|
27
|
-
pymax/static/enum.py,sha256=
|
|
28
|
-
maxapi_python-1.1.
|
|
29
|
-
maxapi_python-1.1.
|
|
30
|
-
maxapi_python-1.1.
|
|
31
|
-
maxapi_python-1.1.
|
|
27
|
+
pymax/static/enum.py,sha256=c_QaLU0Ephe4SuKFIpwpmrf_HCutc34JJ6o4Ik1E6_g,4582
|
|
28
|
+
maxapi_python-1.1.17.dist-info/METADATA,sha256=0gplAk5x9ujWD7fkCZu_yvwataKPPq3fMETJbwHb3HQ,6245
|
|
29
|
+
maxapi_python-1.1.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
30
|
+
maxapi_python-1.1.17.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
31
|
+
maxapi_python-1.1.17.dist-info/RECORD,,
|
pymax/__init__.py
CHANGED
|
@@ -15,6 +15,10 @@ from .exceptions import (
|
|
|
15
15
|
SocketSendError,
|
|
16
16
|
WebSocketNotConnectedError,
|
|
17
17
|
)
|
|
18
|
+
from .files import (
|
|
19
|
+
File,
|
|
20
|
+
Photo,
|
|
21
|
+
)
|
|
18
22
|
from .static.enum import (
|
|
19
23
|
AccessType,
|
|
20
24
|
AttachType,
|
|
@@ -61,47 +65,49 @@ __all__ = [
|
|
|
61
65
|
"AccessType",
|
|
62
66
|
"AttachType",
|
|
63
67
|
"AuthType",
|
|
64
|
-
"ContactAction",
|
|
65
|
-
"FormattingType",
|
|
66
|
-
"MarkupType",
|
|
67
68
|
# Типы данных
|
|
68
69
|
"Channel",
|
|
69
70
|
"Chat",
|
|
70
71
|
"ChatType",
|
|
71
72
|
"Contact",
|
|
73
|
+
"ContactAction",
|
|
72
74
|
"ControlAttach",
|
|
73
75
|
"DeviceType",
|
|
74
76
|
"Dialog",
|
|
75
77
|
"Element",
|
|
76
78
|
"ElementType",
|
|
79
|
+
"File",
|
|
77
80
|
"FileAttach",
|
|
78
81
|
"FileRequest",
|
|
82
|
+
"FormattingType",
|
|
83
|
+
# Исключения
|
|
84
|
+
"InvalidPhoneError",
|
|
85
|
+
"LoginError",
|
|
86
|
+
"MarkupType",
|
|
87
|
+
# Клиент
|
|
88
|
+
"MaxClient",
|
|
79
89
|
"Me",
|
|
80
90
|
"Member",
|
|
91
|
+
"Message",
|
|
81
92
|
"MessageLink",
|
|
93
|
+
"MessageStatus",
|
|
94
|
+
"MessageType",
|
|
82
95
|
"Name",
|
|
83
96
|
"Names",
|
|
97
|
+
"Opcode",
|
|
98
|
+
"Photo",
|
|
84
99
|
"PhotoAttach",
|
|
85
100
|
"Presence",
|
|
86
101
|
"ReactionCounter",
|
|
87
102
|
"ReactionInfo",
|
|
88
|
-
"Session",
|
|
89
|
-
"VideoAttach",
|
|
90
|
-
"VideoRequest",
|
|
91
|
-
# Исключения
|
|
92
|
-
"InvalidPhoneError",
|
|
93
|
-
"LoginError",
|
|
94
|
-
"WebSocketNotConnectedError",
|
|
95
103
|
"ResponseError",
|
|
96
104
|
"ResponseStructureError",
|
|
105
|
+
"Session",
|
|
106
|
+
"SocketMaxClient",
|
|
97
107
|
"SocketNotConnectedError",
|
|
98
108
|
"SocketSendError",
|
|
99
|
-
# Клиент
|
|
100
|
-
"MaxClient",
|
|
101
|
-
"Message",
|
|
102
|
-
"MessageStatus",
|
|
103
|
-
"MessageType",
|
|
104
|
-
"Opcode",
|
|
105
|
-
"SocketMaxClient",
|
|
106
109
|
"User",
|
|
110
|
+
"VideoAttach",
|
|
111
|
+
"VideoRequest",
|
|
112
|
+
"WebSocketNotConnectedError",
|
|
107
113
|
]
|
pymax/core.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import logging
|
|
3
4
|
import socket
|
|
4
5
|
import ssl
|
|
@@ -11,7 +12,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
|
11
12
|
from typing_extensions import override
|
|
12
13
|
|
|
13
14
|
from .crud import Database
|
|
14
|
-
from .exceptions import InvalidPhoneError
|
|
15
|
+
from .exceptions import InvalidPhoneError, WebSocketNotConnectedError
|
|
15
16
|
from .formatter import ColoredFormatter
|
|
16
17
|
from .mixins import ApiMixin, SocketMixin, WebSocketMixin
|
|
17
18
|
from .payloads import UserAgentPayload
|
|
@@ -56,6 +57,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
56
57
|
send_fake_telemetry (bool, optional): Флаг отправки фейковой телеметрии. По умолчанию True.
|
|
57
58
|
proxy (str | Literal[True] | None, optional): Прокси для подключения к WebSocket.
|
|
58
59
|
(См. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
|
|
60
|
+
reconnect (bool, optional): Флаг автоматического переподключения при потере соединения. По умолчанию True.
|
|
61
|
+
|
|
59
62
|
|
|
60
63
|
Raises:
|
|
61
64
|
InvalidPhoneError: Если формат номера телефона неверный.
|
|
@@ -76,6 +79,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
76
79
|
first_name: str = "",
|
|
77
80
|
last_name: str | None = None,
|
|
78
81
|
logger: logging.Logger | None = None,
|
|
82
|
+
reconnect: bool = True,
|
|
83
|
+
reconnect_delay: float = 1.0,
|
|
79
84
|
) -> None:
|
|
80
85
|
self.logger = logger or logging.getLogger(f"{__name__}")
|
|
81
86
|
self.uri: str = uri
|
|
@@ -88,6 +93,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
88
93
|
self.first_name: str = first_name
|
|
89
94
|
self.last_name: str | None = last_name
|
|
90
95
|
self.proxy: str | Literal[True] | None = proxy
|
|
96
|
+
self.reconnect: bool = reconnect
|
|
97
|
+
self.reconnect_delay: float = reconnect_delay
|
|
91
98
|
|
|
92
99
|
self.is_connected: bool = False
|
|
93
100
|
|
|
@@ -205,14 +212,17 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
205
212
|
except Exception:
|
|
206
213
|
self.logger.exception("Error closing client")
|
|
207
214
|
|
|
208
|
-
|
|
215
|
+
@override
|
|
216
|
+
def _create_safe_task(
|
|
217
|
+
self, coro: Awaitable[Any], *, name: str | None = None
|
|
218
|
+
) -> asyncio.Task[Any | None]:
|
|
209
219
|
async def runner():
|
|
210
220
|
try:
|
|
211
221
|
return await coro
|
|
212
222
|
except asyncio.CancelledError:
|
|
213
223
|
raise
|
|
214
224
|
except Exception as e:
|
|
215
|
-
self.logger.
|
|
225
|
+
self.logger.exception(
|
|
216
226
|
f"Unhandled exception in task {name or coro}: {e}",
|
|
217
227
|
exc_info=e,
|
|
218
228
|
)
|
|
@@ -226,40 +236,98 @@ class MaxClient(ApiMixin, WebSocketMixin):
|
|
|
226
236
|
"""
|
|
227
237
|
Запускает клиент, подключается к WebSocket, авторизует
|
|
228
238
|
пользователя (если нужно) и запускает фоновый цикл.
|
|
239
|
+
Теперь включает безопасный reconnect-loop, если self.reconnect=True.
|
|
229
240
|
"""
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
self.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
241
|
+
|
|
242
|
+
while True:
|
|
243
|
+
try:
|
|
244
|
+
self.logger.info("Client starting")
|
|
245
|
+
await self._connect(self.user_agent)
|
|
246
|
+
|
|
247
|
+
if self.registration:
|
|
248
|
+
if not self.first_name:
|
|
249
|
+
raise ValueError("First name is required for registration")
|
|
250
|
+
await self._register(self.first_name, self.last_name)
|
|
251
|
+
|
|
252
|
+
if self._token and self._database.get_auth_token() is None:
|
|
253
|
+
self._database.update_auth_token(self._device_id, self._token)
|
|
254
|
+
|
|
255
|
+
if self._token is None:
|
|
256
|
+
await self._login()
|
|
257
|
+
else:
|
|
258
|
+
await self._sync()
|
|
259
|
+
|
|
260
|
+
if self._on_start_handler:
|
|
261
|
+
self.logger.debug("Calling on_start handler")
|
|
262
|
+
result = self._on_start_handler()
|
|
263
|
+
if asyncio.iscoroutine(result):
|
|
264
|
+
await self._safe_execute(result, context="on_start handler")
|
|
265
|
+
|
|
266
|
+
ping_task = asyncio.create_task(self._send_interactive_ping())
|
|
267
|
+
ping_task.add_done_callback(self._log_task_exception)
|
|
268
|
+
self._background_tasks.add(ping_task)
|
|
269
|
+
|
|
270
|
+
if self._send_fake_telemetry:
|
|
271
|
+
telemetry_task = asyncio.create_task(self._start())
|
|
272
|
+
telemetry_task.add_done_callback(self._log_task_exception)
|
|
273
|
+
self._background_tasks.add(telemetry_task)
|
|
274
|
+
|
|
275
|
+
await self._wait_forever()
|
|
276
|
+
self.logger.info("WebSocket closed (wait_forever exited)")
|
|
277
|
+
|
|
278
|
+
except Exception:
|
|
279
|
+
self.logger.exception("Client start iteration failed")
|
|
280
|
+
|
|
281
|
+
finally:
|
|
282
|
+
self.logger.debug("Cleaning up background tasks and pending futures")
|
|
283
|
+
|
|
284
|
+
for task in list(self._background_tasks):
|
|
285
|
+
task.cancel()
|
|
286
|
+
try:
|
|
287
|
+
await task
|
|
288
|
+
except asyncio.CancelledError:
|
|
289
|
+
pass
|
|
290
|
+
except Exception:
|
|
291
|
+
self.logger.debug(
|
|
292
|
+
"Background task raised during cancellation", exc_info=True
|
|
293
|
+
)
|
|
294
|
+
self._background_tasks.discard(task)
|
|
295
|
+
|
|
296
|
+
if self._recv_task:
|
|
297
|
+
self._recv_task.cancel()
|
|
298
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
299
|
+
await self._recv_task
|
|
300
|
+
self._recv_task = None
|
|
301
|
+
|
|
302
|
+
if self._outgoing_task:
|
|
303
|
+
self._outgoing_task.cancel()
|
|
304
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
305
|
+
await self._outgoing_task
|
|
306
|
+
self._outgoing_task = None
|
|
307
|
+
|
|
308
|
+
for fut in self._pending.values():
|
|
309
|
+
if not fut.done():
|
|
310
|
+
fut.set_exception(WebSocketNotConnectedError)
|
|
311
|
+
self._pending.clear()
|
|
312
|
+
|
|
313
|
+
if self._ws:
|
|
314
|
+
try:
|
|
315
|
+
await self._ws.close()
|
|
316
|
+
except Exception:
|
|
317
|
+
self.logger.debug(
|
|
318
|
+
"Error closing ws during cleanup", exc_info=True
|
|
319
|
+
)
|
|
320
|
+
self._ws = None
|
|
321
|
+
|
|
322
|
+
self.is_connected = False
|
|
323
|
+
self.logger.info("Client start() cleaned up")
|
|
324
|
+
|
|
325
|
+
if not self.reconnect:
|
|
326
|
+
self.logger.info("Reconnect disabled — exiting start()")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
self.logger.info("Reconnect enabled — restarting client")
|
|
330
|
+
await asyncio.sleep(self.reconnect_delay)
|
|
263
331
|
|
|
264
332
|
|
|
265
333
|
class SocketMaxClient(SocketMixin, MaxClient):
|
pymax/interfaces.py
CHANGED
|
@@ -43,15 +43,14 @@ class ClientProtocol(ABC):
|
|
|
43
43
|
self.last_name: str | None
|
|
44
44
|
self._token: str | None
|
|
45
45
|
self._work_dir: str
|
|
46
|
+
self.reconnect: bool
|
|
46
47
|
self._database_path: Path
|
|
47
48
|
self._ws: websockets.ClientConnection | None = None
|
|
48
49
|
self._seq: int = 0
|
|
49
50
|
self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
50
51
|
self._recv_task: asyncio.Task[Any] | None = None
|
|
51
52
|
self._incoming: asyncio.Queue[dict[str, Any]] | None = None
|
|
52
|
-
self._file_upload_waiters: dict[
|
|
53
|
-
int, asyncio.Future[dict[str, Any]]
|
|
54
|
-
] = {}
|
|
53
|
+
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
55
54
|
self.user_agent = UserAgentPayload()
|
|
56
55
|
self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
|
|
57
56
|
self._outgoing_task: asyncio.Task[Any] | None = None
|
|
@@ -70,9 +69,7 @@ class ClientProtocol(ABC):
|
|
|
70
69
|
self._on_message_delete_handlers: list[
|
|
71
70
|
tuple[Callable[[Message], Any], Filter | None]
|
|
72
71
|
] = []
|
|
73
|
-
self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None =
|
|
74
|
-
None
|
|
75
|
-
)
|
|
72
|
+
self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
|
|
76
73
|
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
77
74
|
self._ssl_context: ssl.SSLContext
|
|
78
75
|
self._socket: socket.socket | None = None
|
pymax/mixins/websocket.py
CHANGED
|
@@ -25,9 +25,7 @@ class WebSocketMixin(ClientProtocol):
|
|
|
25
25
|
@property
|
|
26
26
|
def ws(self) -> websockets.ClientConnection:
|
|
27
27
|
if self._ws is None or not self.is_connected:
|
|
28
|
-
self.logger.critical(
|
|
29
|
-
"WebSocket not connected when access attempted"
|
|
30
|
-
)
|
|
28
|
+
self.logger.critical("WebSocket not connected when access attempted")
|
|
31
29
|
raise WebSocketNotConnectedError
|
|
32
30
|
return self._ws
|
|
33
31
|
|
|
@@ -138,9 +136,7 @@ class WebSocketMixin(ClientProtocol):
|
|
|
138
136
|
|
|
139
137
|
if fut and not fut.done():
|
|
140
138
|
fut.set_result(data)
|
|
141
|
-
self.logger.debug(
|
|
142
|
-
"Matched response for pending seq=%s", seq
|
|
143
|
-
)
|
|
139
|
+
self.logger.debug("Matched response for pending seq=%s", seq)
|
|
144
140
|
else:
|
|
145
141
|
if self._incoming is not None:
|
|
146
142
|
try:
|
|
@@ -153,13 +149,9 @@ class WebSocketMixin(ClientProtocol):
|
|
|
153
149
|
|
|
154
150
|
try: # TODO: переделать, временное решение
|
|
155
151
|
if data.get("opcode") == Opcode.NOTIF_ATTACH:
|
|
156
|
-
file_id = data.get("payload", {}).get(
|
|
157
|
-
"fileId", None
|
|
158
|
-
)
|
|
152
|
+
file_id = data.get("payload", {}).get("fileId", None)
|
|
159
153
|
if isinstance(file_id, int):
|
|
160
|
-
fut = self._file_upload_waiters.pop(
|
|
161
|
-
file_id, None
|
|
162
|
-
)
|
|
154
|
+
fut = self._file_upload_waiters.pop(file_id, None)
|
|
163
155
|
if fut and not fut.done():
|
|
164
156
|
fut.set_result(data)
|
|
165
157
|
self.logger.debug(
|
|
@@ -167,9 +159,7 @@ class WebSocketMixin(ClientProtocol):
|
|
|
167
159
|
file_id,
|
|
168
160
|
)
|
|
169
161
|
except Exception:
|
|
170
|
-
self.logger.exception(
|
|
171
|
-
"Error handling file upload notification"
|
|
172
|
-
)
|
|
162
|
+
self.logger.exception("Error handling file upload notification")
|
|
173
163
|
|
|
174
164
|
if (
|
|
175
165
|
data.get("opcode") == Opcode.NOTIF_MESSAGE.value
|
|
@@ -185,23 +175,17 @@ class WebSocketMixin(ClientProtocol):
|
|
|
185
175
|
for (
|
|
186
176
|
edit_handler,
|
|
187
177
|
edit_filter,
|
|
188
|
-
) in
|
|
189
|
-
self._on_message_edit_handlers
|
|
190
|
-
):
|
|
178
|
+
) in self._on_message_edit_handlers:
|
|
191
179
|
await self._process_message_handler(
|
|
192
180
|
edit_handler,
|
|
193
181
|
edit_filter,
|
|
194
182
|
msg,
|
|
195
183
|
)
|
|
196
|
-
elif
|
|
197
|
-
msg.status == MessageStatus.REMOVED
|
|
198
|
-
):
|
|
184
|
+
elif msg.status == MessageStatus.REMOVED:
|
|
199
185
|
for (
|
|
200
186
|
remove_handler,
|
|
201
187
|
remove_filter,
|
|
202
|
-
) in
|
|
203
|
-
self._on_message_delete_handlers
|
|
204
|
-
):
|
|
188
|
+
) in self._on_message_delete_handlers:
|
|
205
189
|
await self._process_message_handler(
|
|
206
190
|
remove_handler,
|
|
207
191
|
remove_filter,
|
|
@@ -211,19 +195,22 @@ class WebSocketMixin(ClientProtocol):
|
|
|
211
195
|
handler, filter, msg
|
|
212
196
|
)
|
|
213
197
|
except Exception:
|
|
214
|
-
self.logger.exception(
|
|
215
|
-
"Error in on_message_handler"
|
|
216
|
-
)
|
|
198
|
+
self.logger.exception("Error in on_message_handler")
|
|
217
199
|
|
|
218
200
|
except websockets.exceptions.ConnectionClosed:
|
|
219
|
-
self.logger.info(
|
|
220
|
-
|
|
221
|
-
|
|
201
|
+
self.logger.info("WebSocket connection closed; exiting recv loop")
|
|
202
|
+
for fut in self._pending.values():
|
|
203
|
+
if not fut.done():
|
|
204
|
+
fut.set_exception(WebSocketNotConnectedError)
|
|
205
|
+
self._pending.clear()
|
|
206
|
+
|
|
207
|
+
self.is_connected = False
|
|
208
|
+
self._ws = None
|
|
209
|
+
self._recv_task = None
|
|
210
|
+
|
|
222
211
|
break
|
|
223
212
|
except Exception:
|
|
224
|
-
self.logger.exception(
|
|
225
|
-
"Error in recv_loop; backing off briefly"
|
|
226
|
-
)
|
|
213
|
+
self.logger.exception("Error in recv_loop; backing off briefly")
|
|
227
214
|
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
228
215
|
|
|
229
216
|
def _log_task_exception(self, fut: asyncio.Future[Any]) -> None:
|
|
@@ -312,9 +299,7 @@ class WebSocketMixin(ClientProtocol):
|
|
|
312
299
|
await asyncio.sleep(5)
|
|
313
300
|
continue
|
|
314
301
|
|
|
315
|
-
message = (
|
|
316
|
-
await self._outgoing.get()
|
|
317
|
-
) # TODO: persistent msg q mb?
|
|
302
|
+
message = await self._outgoing.get() # TODO: persistent msg q mb?
|
|
318
303
|
if not message:
|
|
319
304
|
continue
|
|
320
305
|
|
|
@@ -388,9 +373,7 @@ class WebSocketMixin(ClientProtocol):
|
|
|
388
373
|
).model_dump(by_alias=True)
|
|
389
374
|
|
|
390
375
|
try:
|
|
391
|
-
data = await self._send_and_wait(
|
|
392
|
-
opcode=Opcode.LOGIN, payload=payload
|
|
393
|
-
)
|
|
376
|
+
data = await self._send_and_wait(opcode=Opcode.LOGIN, payload=payload)
|
|
394
377
|
raw_payload = data.get("payload", {})
|
|
395
378
|
|
|
396
379
|
if error := raw_payload.get("error"):
|
pymax/static/enum.py
CHANGED
pymax/types.py
CHANGED
|
@@ -163,9 +163,7 @@ class Contact:
|
|
|
163
163
|
|
|
164
164
|
@override
|
|
165
165
|
def __str__(self) -> str:
|
|
166
|
-
return (
|
|
167
|
-
f"Contact {self.id}: {', '.join(str(n) for n in self.names or [])}"
|
|
168
|
-
)
|
|
166
|
+
return f"Contact {self.id}: {', '.join(str(n) for n in self.names or [])}"
|
|
169
167
|
|
|
170
168
|
|
|
171
169
|
class Member:
|
|
@@ -279,9 +277,7 @@ class StickerAttach:
|
|
|
279
277
|
|
|
280
278
|
|
|
281
279
|
class ControlAttach:
|
|
282
|
-
def __init__(
|
|
283
|
-
self, type: AttachType, event: str, **kwargs: dict[str, Any]
|
|
284
|
-
) -> None:
|
|
280
|
+
def __init__(self, type: AttachType, event: str, **kwargs: dict[str, Any]) -> None:
|
|
285
281
|
self.type = type
|
|
286
282
|
self.event = event
|
|
287
283
|
self.extra = kwargs
|
|
@@ -306,6 +302,50 @@ class ControlAttach:
|
|
|
306
302
|
return f"ControlAttach: {self.event}"
|
|
307
303
|
|
|
308
304
|
|
|
305
|
+
class AudioAttach:
|
|
306
|
+
def __init__(
|
|
307
|
+
self,
|
|
308
|
+
duration: int,
|
|
309
|
+
audio_id: int,
|
|
310
|
+
url: str,
|
|
311
|
+
wave: str,
|
|
312
|
+
transcription_status: str, # TODO: сделать энам
|
|
313
|
+
token: str,
|
|
314
|
+
type: AttachType,
|
|
315
|
+
) -> None:
|
|
316
|
+
self.duration = duration
|
|
317
|
+
self.audio_id = audio_id
|
|
318
|
+
self.url = url
|
|
319
|
+
self.wave = wave
|
|
320
|
+
self.transcription_status = transcription_status
|
|
321
|
+
self.token = token
|
|
322
|
+
self.type = type
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
326
|
+
return cls(
|
|
327
|
+
duration=data["duration"],
|
|
328
|
+
audio_id=data["audioId"],
|
|
329
|
+
url=data["url"],
|
|
330
|
+
wave=data["wave"],
|
|
331
|
+
transcription_status=data["transcriptionStatus"],
|
|
332
|
+
token=data["token"],
|
|
333
|
+
type=AttachType(data["_type"]),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@override
|
|
337
|
+
def __repr__(self) -> str:
|
|
338
|
+
return (
|
|
339
|
+
f"AudioAttach(duration={self.duration!r}, audio_id={self.audio_id!r}, "
|
|
340
|
+
f"url={self.url!r}, wave={self.wave!r}, transcription_status={self.transcription_status!r}, "
|
|
341
|
+
f"token={self.token!r}, type={self.type!r})"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@override
|
|
345
|
+
def __str__(self) -> str:
|
|
346
|
+
return f"AudioAttach: {self.audio_id}"
|
|
347
|
+
|
|
348
|
+
|
|
309
349
|
class PhotoAttach:
|
|
310
350
|
def __init__(
|
|
311
351
|
self,
|
|
@@ -522,13 +562,13 @@ class Element:
|
|
|
522
562
|
|
|
523
563
|
@classmethod
|
|
524
564
|
def from_dict(cls, data: dict[Any, Any]) -> Self:
|
|
525
|
-
return cls(
|
|
526
|
-
type=data["type"], length=data["length"], from_=data.get("from")
|
|
527
|
-
)
|
|
565
|
+
return cls(type=data["type"], length=data["length"], from_=data.get("from"))
|
|
528
566
|
|
|
529
567
|
@override
|
|
530
568
|
def __repr__(self) -> str:
|
|
531
|
-
return
|
|
569
|
+
return (
|
|
570
|
+
f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
|
|
571
|
+
)
|
|
532
572
|
|
|
533
573
|
@override
|
|
534
574
|
def __str__(self) -> str:
|
|
@@ -591,9 +631,7 @@ class ReactionInfo:
|
|
|
591
631
|
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
592
632
|
return cls(
|
|
593
633
|
total_count=data.get("totalCount", 0),
|
|
594
|
-
counters=[
|
|
595
|
-
ReactionCounter.from_dict(c) for c in data.get("counters", [])
|
|
596
|
-
],
|
|
634
|
+
counters=[ReactionCounter.from_dict(c) for c in data.get("counters", [])],
|
|
597
635
|
your_reaction=data.get("yourReaction"),
|
|
598
636
|
)
|
|
599
637
|
|
|
@@ -619,6 +657,7 @@ class Message:
|
|
|
619
657
|
| FileAttach
|
|
620
658
|
| ControlAttach
|
|
621
659
|
| StickerAttach
|
|
660
|
+
| AudioAttach
|
|
622
661
|
]
|
|
623
662
|
| None
|
|
624
663
|
),
|
|
@@ -640,11 +679,7 @@ class Message:
|
|
|
640
679
|
def from_dict(cls, data: dict[Any, Any]) -> Self:
|
|
641
680
|
message = data["message"] if data.get("message") else data
|
|
642
681
|
attaches: list[
|
|
643
|
-
PhotoAttach
|
|
644
|
-
| VideoAttach
|
|
645
|
-
| FileAttach
|
|
646
|
-
| ControlAttach
|
|
647
|
-
| StickerAttach
|
|
682
|
+
PhotoAttach | VideoAttach | FileAttach | ControlAttach | StickerAttach
|
|
648
683
|
] = []
|
|
649
684
|
for a in message.get("attaches", []):
|
|
650
685
|
if a["_type"] == AttachType.PHOTO:
|
|
@@ -657,6 +692,8 @@ class Message:
|
|
|
657
692
|
attaches.append(ControlAttach.from_dict(a))
|
|
658
693
|
elif a["_type"] == AttachType.STICKER:
|
|
659
694
|
attaches.append(StickerAttach.from_dict(a))
|
|
695
|
+
elif a["_type"] == AttachType.AUDIO:
|
|
696
|
+
attaches.append(AudioAttach.from_dict(a))
|
|
660
697
|
link_value = message.get("link")
|
|
661
698
|
if isinstance(link_value, dict):
|
|
662
699
|
link = MessageLink.from_dict(link_value)
|
|
@@ -670,9 +707,7 @@ class Message:
|
|
|
670
707
|
return cls(
|
|
671
708
|
chat_id=data.get("chatId"),
|
|
672
709
|
sender=message.get("sender"),
|
|
673
|
-
elements=[
|
|
674
|
-
Element.from_dict(e) for e in message.get("elements", [])
|
|
675
|
-
],
|
|
710
|
+
elements=[Element.from_dict(e) for e in message.get("elements", [])],
|
|
676
711
|
options=message.get("options"),
|
|
677
712
|
id=message["id"],
|
|
678
713
|
time=message["time"],
|
|
@@ -834,13 +869,9 @@ class Chat:
|
|
|
834
869
|
int(k): v for k, v in raw_admins.items()
|
|
835
870
|
}
|
|
836
871
|
raw_participants = data.get("participants", {}) or {}
|
|
837
|
-
participants: dict[int, int] = {
|
|
838
|
-
int(k): v for k, v in raw_participants.items()
|
|
839
|
-
}
|
|
872
|
+
participants: dict[int, int] = {int(k): v for k, v in raw_participants.items()}
|
|
840
873
|
last_msg = (
|
|
841
|
-
Message.from_dict(data["lastMessage"])
|
|
842
|
-
if data.get("lastMessage")
|
|
843
|
-
else None
|
|
874
|
+
Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
|
|
844
875
|
)
|
|
845
876
|
return cls(
|
|
846
877
|
participants_count=data.get("participantsCount", 0),
|
|
@@ -852,9 +883,7 @@ class Chat:
|
|
|
852
883
|
description=data.get("description"),
|
|
853
884
|
chat_type=ChatType(data.get("type", ChatType.CHAT.value)),
|
|
854
885
|
title=data.get("title"),
|
|
855
|
-
last_fire_delayed_error_time=data.get(
|
|
856
|
-
"lastFireDelayedErrorTime", 0
|
|
857
|
-
),
|
|
886
|
+
last_fire_delayed_error_time=data.get("lastFireDelayedErrorTime", 0),
|
|
858
887
|
last_delayed_update_time=data.get("lastDelayedUpdateTime", 0),
|
|
859
888
|
options=data.get("options", {}),
|
|
860
889
|
modified=data.get("modified", 0),
|
|
@@ -876,9 +905,7 @@ class Chat:
|
|
|
876
905
|
|
|
877
906
|
@override
|
|
878
907
|
def __repr__(self) -> str:
|
|
879
|
-
return (
|
|
880
|
-
f"Chat(id={self.id!r}, title={self.title!r}, type={self.type!r})"
|
|
881
|
-
)
|
|
908
|
+
return f"Chat(id={self.id!r}, title={self.title!r}, type={self.type!r})"
|
|
882
909
|
|
|
883
910
|
@override
|
|
884
911
|
def __str__(self) -> str:
|
|
File without changes
|
|
File without changes
|