maxapi-python 1.2.3__py3-none-any.whl → 1.2.4__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.4.dist-info}/METADATA +12 -7
- maxapi_python-1.2.4.dist-info/RECORD +33 -0
- pymax/core.py +54 -37
- pymax/files.py +28 -7
- pymax/interfaces.py +410 -115
- pymax/mixins/auth.py +2 -2
- 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 +3 -336
- pymax/mixins/telemetry.py +2 -4
- pymax/mixins/user.py +3 -5
- pymax/mixins/websocket.py +5 -363
- pymax/payloads.py +8 -1
- pymax/protocols.py +123 -0
- pymax/static/constant.py +69 -8
- pymax/static/enum.py +5 -0
- 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.4.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.4.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:
|
|
@@ -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")
|
|
@@ -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,
|
|
@@ -468,160 +289,6 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
468
289
|
finally:
|
|
469
290
|
self._pending.pop(msg["seq"], None)
|
|
470
291
|
|
|
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
|
-
)
|
|
624
|
-
|
|
625
292
|
@override
|
|
626
293
|
async def _get_chat(self, chat_id: int) -> Chat | None:
|
|
627
294
|
for chat in self.chats:
|
pymax/mixins/telemetry.py
CHANGED
|
@@ -3,20 +3,18 @@ import random
|
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
5
|
from pymax.exceptions import Error
|
|
6
|
-
from pymax.interfaces import ClientProtocol
|
|
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(
|
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)
|