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.
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 ClientProtocol
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(ClientProtocol):
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)