maxapi-python 1.2.3__py3-none-any.whl → 1.2.5__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.2.3.dist-info → maxapi_python-1.2.5.dist-info}/METADATA +9 -7
- maxapi_python-1.2.5.dist-info/RECORD +33 -0
- pymax/core.py +63 -38
- pymax/files.py +28 -7
- pymax/interfaces.py +417 -116
- pymax/mixins/auth.py +231 -5
- pymax/mixins/channel.py +3 -5
- pymax/mixins/group.py +2 -2
- pymax/mixins/handler.py +4 -10
- pymax/mixins/message.py +64 -88
- pymax/mixins/scheduler.py +1 -1
- pymax/mixins/self.py +2 -2
- pymax/mixins/socket.py +16 -340
- pymax/mixins/telemetry.py +10 -6
- pymax/mixins/user.py +3 -5
- pymax/mixins/websocket.py +16 -365
- pymax/payloads.py +44 -1
- pymax/protocols.py +123 -0
- pymax/static/constant.py +76 -8
- pymax/static/enum.py +65 -52
- pymax/types.py +25 -0
- pymax/utils.py +90 -0
- maxapi_python-1.2.3.dist-info/RECORD +0 -32
- pymax/mixins/utils.py +0 -27
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.5.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.5.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/socket.py
CHANGED
|
@@ -12,8 +12,9 @@ from typing_extensions import override
|
|
|
12
12
|
|
|
13
13
|
from pymax.exceptions import Error, SocketNotConnectedError, SocketSendError
|
|
14
14
|
from pymax.filters import BaseFilter
|
|
15
|
-
from pymax.interfaces import
|
|
15
|
+
from pymax.interfaces import BaseTransport
|
|
16
16
|
from pymax.payloads import BaseWebSocketMessage, SyncPayload, UserAgentPayload
|
|
17
|
+
from pymax.protocols import ClientProtocol
|
|
17
18
|
from pymax.static.constant import (
|
|
18
19
|
DEFAULT_PING_INTERVAL,
|
|
19
20
|
DEFAULT_TIMEOUT,
|
|
@@ -32,7 +33,7 @@ from pymax.types import (
|
|
|
32
33
|
)
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
class SocketMixin(
|
|
36
|
+
class SocketMixin(BaseTransport):
|
|
36
37
|
@property
|
|
37
38
|
def sock(self) -> socket.socket:
|
|
38
39
|
if self._socket is None or not self.is_connected:
|
|
@@ -83,7 +84,7 @@ class SocketMixin(ClientProtocol):
|
|
|
83
84
|
) -> bytes:
|
|
84
85
|
ver_b = ver.to_bytes(1, "big")
|
|
85
86
|
cmd_b = cmd.to_bytes(2, "big")
|
|
86
|
-
seq_b = seq.to_bytes(1, "big")
|
|
87
|
+
seq_b = (seq % 256).to_bytes(1, "big")
|
|
87
88
|
opcode_b = opcode.to_bytes(2, "big")
|
|
88
89
|
payload_bytes: bytes | None = msgpack.packb(payload)
|
|
89
90
|
if payload_bytes is None:
|
|
@@ -129,25 +130,6 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
129
130
|
self.logger.info("Socket connected, starting handshake")
|
|
130
131
|
return await self._handshake(user_agent)
|
|
131
132
|
|
|
132
|
-
async def _handshake(self, user_agent: UserAgentPayload) -> dict[str, Any]:
|
|
133
|
-
try:
|
|
134
|
-
self.logger.debug(
|
|
135
|
-
"Sending handshake with user_agent keys=%s",
|
|
136
|
-
user_agent.model_dump().keys(),
|
|
137
|
-
)
|
|
138
|
-
resp = await self._send_and_wait(
|
|
139
|
-
opcode=Opcode.SESSION_INIT,
|
|
140
|
-
payload={
|
|
141
|
-
"deviceId": str(self._device_id),
|
|
142
|
-
"userAgent": user_agent,
|
|
143
|
-
},
|
|
144
|
-
)
|
|
145
|
-
self.logger.info("Handshake completed")
|
|
146
|
-
return resp
|
|
147
|
-
except Exception as e:
|
|
148
|
-
self.logger.error("Handshake failed: %s", e, exc_info=True)
|
|
149
|
-
raise ConnectionError(f"Handshake failed: {e}")
|
|
150
|
-
|
|
151
133
|
def _recv_exactly(self, sock: socket.socket, n: int) -> bytes:
|
|
152
134
|
buf = bytearray()
|
|
153
135
|
while len(buf) < n:
|
|
@@ -214,116 +196,6 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
214
196
|
else [data]
|
|
215
197
|
)
|
|
216
198
|
|
|
217
|
-
def _handle_pending(self, seq: int | None, data: dict) -> bool:
|
|
218
|
-
if isinstance(seq, int):
|
|
219
|
-
fut = self._pending.get(seq)
|
|
220
|
-
if fut and not fut.done():
|
|
221
|
-
fut.set_result(data)
|
|
222
|
-
self.logger.debug("Matched response for pending seq=%s", seq)
|
|
223
|
-
return True
|
|
224
|
-
return False
|
|
225
|
-
|
|
226
|
-
async def _handle_incoming_queue(self, data: dict[str, Any]) -> None:
|
|
227
|
-
if self._incoming:
|
|
228
|
-
try:
|
|
229
|
-
self._incoming.put_nowait(data)
|
|
230
|
-
except asyncio.QueueFull:
|
|
231
|
-
self.logger.warning(
|
|
232
|
-
"Incoming queue full; dropping message seq=%s", data.get("seq")
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
async def _handle_file_upload(self, data: dict[str, Any]) -> None:
|
|
236
|
-
if data.get("opcode") != Opcode.NOTIF_ATTACH:
|
|
237
|
-
return
|
|
238
|
-
payload = data.get("payload", {})
|
|
239
|
-
for key in ("fileId", "videoId"):
|
|
240
|
-
id_ = payload.get(key)
|
|
241
|
-
if id_ is not None:
|
|
242
|
-
fut = self._file_upload_waiters.pop(id_, None)
|
|
243
|
-
if fut and not fut.done():
|
|
244
|
-
fut.set_result(data)
|
|
245
|
-
self.logger.debug("Fulfilled file upload waiter for %s=%s", key, id_)
|
|
246
|
-
|
|
247
|
-
async def _handle_message_notifications(self, data: dict) -> None:
|
|
248
|
-
if data.get("opcode") != Opcode.NOTIF_MESSAGE.value:
|
|
249
|
-
return
|
|
250
|
-
payload = data.get("payload", {})
|
|
251
|
-
msg = Message.from_dict(payload)
|
|
252
|
-
if not msg:
|
|
253
|
-
return
|
|
254
|
-
handlers_map = {
|
|
255
|
-
MessageStatus.EDITED: self._on_message_edit_handlers,
|
|
256
|
-
MessageStatus.REMOVED: self._on_message_delete_handlers,
|
|
257
|
-
}
|
|
258
|
-
if msg.status and msg.status in handlers_map:
|
|
259
|
-
for handler, filter in handlers_map[msg.status]:
|
|
260
|
-
await self._process_message_handler(handler, filter, msg)
|
|
261
|
-
for handler, filter in self._on_message_handlers:
|
|
262
|
-
await self._process_message_handler(handler, filter, msg)
|
|
263
|
-
|
|
264
|
-
async def _handle_reactions(self, data: dict):
|
|
265
|
-
if data.get("opcode") != Opcode.NOTIF_MSG_REACTIONS_CHANGED:
|
|
266
|
-
return
|
|
267
|
-
|
|
268
|
-
payload = data.get("payload", {})
|
|
269
|
-
chat_id = payload.get("chatId")
|
|
270
|
-
message_id = payload.get("messageId")
|
|
271
|
-
|
|
272
|
-
if not (chat_id and message_id):
|
|
273
|
-
return
|
|
274
|
-
|
|
275
|
-
total_count = payload.get("totalCount")
|
|
276
|
-
your_reaction = payload.get("yourReaction")
|
|
277
|
-
counters = [ReactionCounter.from_dict(c) for c in payload.get("counters", [])]
|
|
278
|
-
|
|
279
|
-
reaction_info = ReactionInfo(
|
|
280
|
-
total_count=total_count,
|
|
281
|
-
your_reaction=your_reaction,
|
|
282
|
-
counters=counters,
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
for handler in self._on_reaction_change_handlers:
|
|
286
|
-
try:
|
|
287
|
-
result = handler(message_id, chat_id, reaction_info)
|
|
288
|
-
if asyncio.iscoroutine(result):
|
|
289
|
-
await result
|
|
290
|
-
except Exception as e:
|
|
291
|
-
self.logger.exception("Error in on_reaction_change_handler: %s", e)
|
|
292
|
-
|
|
293
|
-
async def _handle_chat_updates(self, data: dict) -> None:
|
|
294
|
-
if data.get("opcode") != Opcode.NOTIF_CHAT:
|
|
295
|
-
return
|
|
296
|
-
|
|
297
|
-
payload = data.get("payload", {})
|
|
298
|
-
chat_data = payload.get("chat", {})
|
|
299
|
-
chat = Chat.from_dict(chat_data)
|
|
300
|
-
if not chat:
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
for handler in self._on_chat_update_handlers:
|
|
304
|
-
try:
|
|
305
|
-
result = handler(chat)
|
|
306
|
-
if asyncio.iscoroutine(result):
|
|
307
|
-
await result
|
|
308
|
-
except Exception as e:
|
|
309
|
-
self.logger.exception("Error in on_chat_update_handler: %s", e)
|
|
310
|
-
|
|
311
|
-
async def _handle_raw_receive(self, data: dict[str, Any]) -> None:
|
|
312
|
-
for handler in self._on_raw_receive_handlers:
|
|
313
|
-
try:
|
|
314
|
-
result = handler(data)
|
|
315
|
-
if asyncio.iscoroutine(result):
|
|
316
|
-
await result
|
|
317
|
-
except Exception as e:
|
|
318
|
-
self.logger.exception("Error in on_raw_receive_handler: %s", e)
|
|
319
|
-
|
|
320
|
-
async def _dispatch_incoming(self, data: dict[str, Any]) -> None:
|
|
321
|
-
await self._handle_raw_receive(data)
|
|
322
|
-
await self._handle_file_upload(data)
|
|
323
|
-
await self._handle_message_notifications(data)
|
|
324
|
-
await self._handle_reactions(data)
|
|
325
|
-
await self._handle_chat_updates(data)
|
|
326
|
-
|
|
327
199
|
async def _recv_loop(self) -> None:
|
|
328
200
|
if self._socket is None:
|
|
329
201
|
self.logger.warning("Recv loop started without socket instance")
|
|
@@ -347,7 +219,7 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
347
219
|
for data_item in datas:
|
|
348
220
|
seq = data_item.get("seq")
|
|
349
221
|
|
|
350
|
-
if self._handle_pending(seq, data_item):
|
|
222
|
+
if self._handle_pending(seq % 256 if seq is not None else None, data_item):
|
|
351
223
|
continue
|
|
352
224
|
|
|
353
225
|
if self._incoming is not None:
|
|
@@ -362,57 +234,6 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
362
234
|
self.logger.exception("Error in recv_loop; backing off briefly")
|
|
363
235
|
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
364
236
|
|
|
365
|
-
def _log_task_exception(self, fut: asyncio.Future[Any]) -> None:
|
|
366
|
-
try:
|
|
367
|
-
fut.result()
|
|
368
|
-
except asyncio.CancelledError:
|
|
369
|
-
pass
|
|
370
|
-
except Exception as e:
|
|
371
|
-
self.logger.exception("Error getting task exception: %s", e)
|
|
372
|
-
pass
|
|
373
|
-
|
|
374
|
-
async def _process_message_handler(
|
|
375
|
-
self,
|
|
376
|
-
handler: Callable[[Message], Any],
|
|
377
|
-
filter: BaseFilter[Message] | None,
|
|
378
|
-
message: Message,
|
|
379
|
-
) -> None:
|
|
380
|
-
if filter is not None and not filter(message):
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
result = handler(message)
|
|
384
|
-
if asyncio.iscoroutine(result):
|
|
385
|
-
task = asyncio.create_task(result)
|
|
386
|
-
task.add_done_callback(self._log_task_exception)
|
|
387
|
-
self._background_tasks.add(task)
|
|
388
|
-
|
|
389
|
-
async def _send_interactive_ping(self) -> None:
|
|
390
|
-
while self.is_connected:
|
|
391
|
-
try:
|
|
392
|
-
await self._send_and_wait(
|
|
393
|
-
opcode=Opcode.PING,
|
|
394
|
-
payload={"interactive": True},
|
|
395
|
-
cmd=0,
|
|
396
|
-
)
|
|
397
|
-
self.logger.debug("Interactive ping sent successfully (socket)")
|
|
398
|
-
except Exception:
|
|
399
|
-
self.logger.warning("Interactive ping failed (socket)", exc_info=True)
|
|
400
|
-
await asyncio.sleep(DEFAULT_PING_INTERVAL)
|
|
401
|
-
|
|
402
|
-
def _make_message(
|
|
403
|
-
self, opcode: Opcode, payload: dict[str, Any], cmd: int = 0
|
|
404
|
-
) -> dict[str, Any]:
|
|
405
|
-
self._seq += 1
|
|
406
|
-
msg = BaseWebSocketMessage(
|
|
407
|
-
ver=10,
|
|
408
|
-
cmd=cmd,
|
|
409
|
-
seq=self._seq,
|
|
410
|
-
opcode=opcode.value,
|
|
411
|
-
payload=payload,
|
|
412
|
-
).model_dump(by_alias=True)
|
|
413
|
-
self.logger.debug("make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq)
|
|
414
|
-
return msg
|
|
415
|
-
|
|
416
237
|
@override
|
|
417
238
|
async def _send_and_wait(
|
|
418
239
|
self,
|
|
@@ -428,7 +249,13 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
428
249
|
msg = self._make_message(opcode, payload, cmd)
|
|
429
250
|
loop = asyncio.get_running_loop()
|
|
430
251
|
fut: asyncio.Future[dict[str, Any]] = loop.create_future()
|
|
431
|
-
|
|
252
|
+
seq_key = msg["seq"] % 256
|
|
253
|
+
|
|
254
|
+
old_fut = self._pending.get(seq_key)
|
|
255
|
+
if old_fut and not old_fut.done():
|
|
256
|
+
old_fut.cancel()
|
|
257
|
+
|
|
258
|
+
self._pending[seq_key] = fut
|
|
432
259
|
try:
|
|
433
260
|
self.logger.debug(
|
|
434
261
|
"Sending frame opcode=%s cmd=%s seq=%s",
|
|
@@ -461,166 +288,15 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
461
288
|
self.logger.exception("Reconnect failed")
|
|
462
289
|
raise exc from conn_err
|
|
463
290
|
raise SocketNotConnectedError from conn_err
|
|
291
|
+
except asyncio.TimeoutError:
|
|
292
|
+
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
293
|
+
raise SocketSendError from None
|
|
464
294
|
except Exception as exc:
|
|
465
295
|
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
466
296
|
raise SocketSendError from exc
|
|
467
297
|
|
|
468
298
|
finally:
|
|
469
|
-
self._pending.pop(msg["seq"], None)
|
|
470
|
-
|
|
471
|
-
async def _outgoing_loop(self) -> None:
|
|
472
|
-
while self.is_connected:
|
|
473
|
-
try:
|
|
474
|
-
if self._outgoing is None:
|
|
475
|
-
await asyncio.sleep(0.1)
|
|
476
|
-
continue
|
|
477
|
-
|
|
478
|
-
if self._circuit_breaker:
|
|
479
|
-
if time.time() - self._last_error_time > 60:
|
|
480
|
-
self._circuit_breaker = False
|
|
481
|
-
self._error_count = 0
|
|
482
|
-
self.logger.info("Circuit breaker reset (socket)")
|
|
483
|
-
else:
|
|
484
|
-
await asyncio.sleep(5)
|
|
485
|
-
continue
|
|
486
|
-
|
|
487
|
-
message = await self._outgoing.get() # TODO: persistent msg q mb?
|
|
488
|
-
|
|
489
|
-
if not message:
|
|
490
|
-
continue
|
|
491
|
-
|
|
492
|
-
retry_count = message.get("retry_count", 0)
|
|
493
|
-
max_retries = message.get("max_retries", 3)
|
|
494
|
-
|
|
495
|
-
try:
|
|
496
|
-
await self._send_and_wait(
|
|
497
|
-
opcode=message["opcode"],
|
|
498
|
-
payload=message["payload"],
|
|
499
|
-
cmd=message.get("cmd", 0),
|
|
500
|
-
timeout=message.get("timeout", 10.0),
|
|
501
|
-
)
|
|
502
|
-
self.logger.debug("Message sent successfully from queue (socket)")
|
|
503
|
-
self._error_count = max(0, self._error_count - 1)
|
|
504
|
-
except Exception as e:
|
|
505
|
-
self._error_count += 1
|
|
506
|
-
self._last_error_time = time.time()
|
|
507
|
-
|
|
508
|
-
if self._error_count > 10: # TODO: export to constant
|
|
509
|
-
self._circuit_breaker = True
|
|
510
|
-
self.logger.warning(
|
|
511
|
-
"Circuit breaker activated due to %d consecutive errors (socket)",
|
|
512
|
-
self._error_count,
|
|
513
|
-
)
|
|
514
|
-
await self._outgoing.put(message)
|
|
515
|
-
continue
|
|
516
|
-
|
|
517
|
-
retry_delay = self._get_retry_delay(e, retry_count)
|
|
518
|
-
self.logger.warning(
|
|
519
|
-
"Failed to send message from queue (socket): %s (delay: %ds)",
|
|
520
|
-
e,
|
|
521
|
-
retry_delay,
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
if retry_count < max_retries:
|
|
525
|
-
message["retry_count"] = retry_count + 1
|
|
526
|
-
await asyncio.sleep(retry_delay)
|
|
527
|
-
await self._outgoing.put(message)
|
|
528
|
-
else:
|
|
529
|
-
self.logger.error(
|
|
530
|
-
"Message failed after %d retries, dropping (socket)",
|
|
531
|
-
max_retries,
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
except Exception:
|
|
535
|
-
self.logger.exception("Error in outgoing loop (socket)")
|
|
536
|
-
await asyncio.sleep(1)
|
|
537
|
-
|
|
538
|
-
def _get_retry_delay(
|
|
539
|
-
self, error: Exception, retry_count: int
|
|
540
|
-
) -> float: # TODO: tune delays later
|
|
541
|
-
if isinstance(error, (ConnectionError, OSError, ssl.SSLError)):
|
|
542
|
-
return 1.0
|
|
543
|
-
elif isinstance(error, TimeoutError):
|
|
544
|
-
return 5.0
|
|
545
|
-
elif isinstance(error, SocketNotConnectedError):
|
|
546
|
-
return 2.0
|
|
547
|
-
else:
|
|
548
|
-
return 2**retry_count
|
|
549
|
-
|
|
550
|
-
async def _queue_message(
|
|
551
|
-
self,
|
|
552
|
-
opcode: int,
|
|
553
|
-
payload: dict[str, Any],
|
|
554
|
-
cmd: int = 0,
|
|
555
|
-
timeout: float = 10.0,
|
|
556
|
-
max_retries: int = 3,
|
|
557
|
-
) -> None:
|
|
558
|
-
if self._outgoing is None:
|
|
559
|
-
self.logger.warning("Outgoing queue not initialized (socket)")
|
|
560
|
-
return
|
|
561
|
-
|
|
562
|
-
message = {
|
|
563
|
-
"opcode": opcode,
|
|
564
|
-
"payload": payload,
|
|
565
|
-
"cmd": cmd,
|
|
566
|
-
"timeout": timeout,
|
|
567
|
-
"retry_count": 0,
|
|
568
|
-
"max_retries": max_retries,
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
await self._outgoing.put(message)
|
|
572
|
-
self.logger.debug("Message queued for sending (socket)")
|
|
573
|
-
|
|
574
|
-
async def _sync(self) -> None:
|
|
575
|
-
self.logger.info("Starting initial sync (socket)")
|
|
576
|
-
payload = SyncPayload(
|
|
577
|
-
interactive=True,
|
|
578
|
-
token=self._token,
|
|
579
|
-
chats_sync=0,
|
|
580
|
-
contacts_sync=0,
|
|
581
|
-
presence_sync=0,
|
|
582
|
-
drafts_sync=0,
|
|
583
|
-
chats_count=40,
|
|
584
|
-
).model_dump(by_alias=True)
|
|
585
|
-
data = await self._send_and_wait(opcode=Opcode.LOGIN, payload=payload)
|
|
586
|
-
raw_payload = data.get("payload", {})
|
|
587
|
-
if error := raw_payload.get("error"):
|
|
588
|
-
localized_message = raw_payload.get("localizedMessage")
|
|
589
|
-
title = raw_payload.get("title")
|
|
590
|
-
message = raw_payload.get("message")
|
|
591
|
-
raise Error(
|
|
592
|
-
error=error,
|
|
593
|
-
message=message,
|
|
594
|
-
title=title,
|
|
595
|
-
localized_message=localized_message,
|
|
596
|
-
)
|
|
597
|
-
for raw_chat in raw_payload.get("chats", []):
|
|
598
|
-
try:
|
|
599
|
-
if raw_chat.get("type") == "DIALOG":
|
|
600
|
-
self.dialogs.append(Dialog.from_dict(raw_chat))
|
|
601
|
-
elif raw_chat.get("type") == "CHAT":
|
|
602
|
-
self.chats.append(Chat.from_dict(raw_chat))
|
|
603
|
-
elif raw_chat.get("type") == "CHANNEL":
|
|
604
|
-
self.channels.append(Channel.from_dict(raw_chat))
|
|
605
|
-
except Exception:
|
|
606
|
-
self.logger.exception("Error parsing chat entry (socket)")
|
|
607
|
-
|
|
608
|
-
for raw_user in raw_payload.get("contacts", []):
|
|
609
|
-
try:
|
|
610
|
-
user = User.from_dict(raw_user)
|
|
611
|
-
if user:
|
|
612
|
-
self.contacts.append(user)
|
|
613
|
-
except Exception:
|
|
614
|
-
self.logger.exception("Error parsing contact entry (socket)")
|
|
615
|
-
|
|
616
|
-
if raw_payload.get("profile", {}).get("contact"):
|
|
617
|
-
self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {}))
|
|
618
|
-
self.logger.info(
|
|
619
|
-
"Sync completed: dialogs=%d chats=%d channels=%d",
|
|
620
|
-
len(self.dialogs),
|
|
621
|
-
len(self.chats),
|
|
622
|
-
len(self.channels),
|
|
623
|
-
)
|
|
299
|
+
self._pending.pop(msg["seq"] % 256, None)
|
|
624
300
|
|
|
625
301
|
@override
|
|
626
302
|
async def _get_chat(self, chat_id: int) -> Chat | None:
|
pymax/mixins/telemetry.py
CHANGED
|
@@ -2,21 +2,19 @@ import asyncio
|
|
|
2
2
|
import random
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
|
-
from pymax.exceptions import Error
|
|
6
|
-
from pymax.interfaces import ClientProtocol
|
|
5
|
+
from pymax.exceptions import Error, SocketNotConnectedError
|
|
7
6
|
from pymax.navigation import Navigation
|
|
8
7
|
from pymax.payloads import (
|
|
9
8
|
NavigationEventParams,
|
|
10
9
|
NavigationEventPayload,
|
|
11
10
|
NavigationPayload,
|
|
12
11
|
)
|
|
12
|
+
from pymax.protocols import ClientProtocol
|
|
13
13
|
from pymax.static.enum import Opcode
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class TelemetryMixin(ClientProtocol):
|
|
17
|
-
async def _send_navigation_event(
|
|
18
|
-
self, events: list[NavigationEventPayload]
|
|
19
|
-
) -> None:
|
|
17
|
+
async def _send_navigation_event(self, events: list[NavigationEventPayload]) -> None:
|
|
20
18
|
try:
|
|
21
19
|
payload = NavigationPayload(events=events).model_dump(by_alias=True)
|
|
22
20
|
data = await self._send_and_wait(
|
|
@@ -105,7 +103,13 @@ class TelemetryMixin(ClientProtocol):
|
|
|
105
103
|
|
|
106
104
|
try:
|
|
107
105
|
while self.is_connected:
|
|
108
|
-
|
|
106
|
+
try:
|
|
107
|
+
await self._send_random_navigation()
|
|
108
|
+
except SocketNotConnectedError:
|
|
109
|
+
self.logger.debug("Socket disconnected, exiting telemetry task")
|
|
110
|
+
break
|
|
111
|
+
except Exception:
|
|
112
|
+
self.logger.warning("Failed to send random navigation")
|
|
109
113
|
await asyncio.sleep(self._get_random_sleep_time())
|
|
110
114
|
|
|
111
115
|
except asyncio.CancelledError:
|
pymax/mixins/user.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from typing import Any, Literal
|
|
2
2
|
|
|
3
3
|
from pymax.exceptions import Error, ResponseError, ResponseStructureError
|
|
4
|
-
from pymax.interfaces import ClientProtocol
|
|
5
|
-
from pymax.mixins.utils import MixinsUtils
|
|
6
4
|
from pymax.payloads import (
|
|
7
5
|
ContactActionPayload,
|
|
8
6
|
FetchContactsPayload,
|
|
9
7
|
SearchByPhonePayload,
|
|
10
8
|
)
|
|
9
|
+
from pymax.protocols import ClientProtocol
|
|
11
10
|
from pymax.static.enum import ContactAction, Opcode
|
|
12
11
|
from pymax.types import Contact, Session, User
|
|
12
|
+
from pymax.utils import MixinsUtils
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class UserMixin(ClientProtocol):
|
|
@@ -122,9 +122,7 @@ class UserMixin(ClientProtocol):
|
|
|
122
122
|
|
|
123
123
|
payload = SearchByPhonePayload(phone=phone).model_dump(by_alias=True)
|
|
124
124
|
|
|
125
|
-
data = await self._send_and_wait(
|
|
126
|
-
opcode=Opcode.CONTACT_INFO_BY_PHONE, payload=payload
|
|
127
|
-
)
|
|
125
|
+
data = await self._send_and_wait(opcode=Opcode.CONTACT_INFO_BY_PHONE, payload=payload)
|
|
128
126
|
|
|
129
127
|
if data.get("payload", {}).get("error"):
|
|
130
128
|
MixinsUtils.handle_error(data)
|