maxapi-python 1.2.4__py3-none-any.whl → 2.0.0__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.
Files changed (168) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/{static/enum.py → protocol/enums.py} +36 -79
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.4.dist-info/METADATA +0 -205
  141. maxapi_python-1.2.4.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -390
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -552
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -368
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -297
  158. pymax/mixins/telemetry.py +0 -112
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -142
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -367
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -89
  166. pymax/types.py +0 -1220
  167. pymax/utils.py +0 -90
  168. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
pymax/interfaces.py DELETED
@@ -1,552 +0,0 @@
1
- import asyncio
2
- import contextlib
3
- import json
4
- import logging
5
- import time
6
- import traceback
7
- from abc import abstractmethod
8
- from collections.abc import Awaitable, Callable
9
- from typing import Any
10
-
11
- from typing_extensions import Self
12
-
13
- from pymax.exceptions import WebSocketNotConnectedError
14
- from pymax.filters import BaseFilter
15
- from pymax.formatter import ColoredFormatter
16
- from pymax.payloads import BaseWebSocketMessage, SyncPayload, UserAgentPayload
17
- from pymax.protocols import ClientProtocol
18
- from pymax.static.constant import DEFAULT_PING_INTERVAL, DEFAULT_TIMEOUT
19
- from pymax.static.enum import Opcode
20
- from pymax.types import (
21
- Channel,
22
- Chat,
23
- ChatType,
24
- Dialog,
25
- Me,
26
- Message,
27
- MessageStatus,
28
- ReactionCounter,
29
- ReactionInfo,
30
- User,
31
- )
32
- from pymax.utils import MixinsUtils
33
-
34
-
35
- class BaseClient(ClientProtocol):
36
- def _setup_logger(self) -> None:
37
- if not self.logger.handlers:
38
- if not self.logger.level:
39
- self.logger.setLevel(logging.INFO)
40
- handler = logging.StreamHandler()
41
- formatter = ColoredFormatter(
42
- "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
43
- datefmt="%Y-%m-%d %H:%M:%S",
44
- )
45
- handler.setFormatter(formatter)
46
- self.logger.addHandler(handler)
47
-
48
- async def _safe_execute(self, coro, *, context: str = "unknown") -> Any:
49
- try:
50
- return await coro
51
- except Exception as e:
52
- self.logger.error(f"Unhandled exception in {context}: {e}\n{traceback.format_exc()}")
53
-
54
- def _create_safe_task(
55
- self, coro: Awaitable[Any], name: str | None = None
56
- ) -> asyncio.Task[Any | None]:
57
- async def runner():
58
- try:
59
- return await coro
60
- except asyncio.CancelledError:
61
- raise
62
- except Exception as e:
63
- tb = traceback.format_exc()
64
- self.logger.error(f"Unhandled exception in task {name or coro}: {e}\n{tb}")
65
- raise
66
-
67
- task = asyncio.create_task(runner(), name=name)
68
- self._background_tasks.add(task)
69
- return task
70
-
71
- async def _cleanup_client(self) -> None:
72
- for task in list(self._background_tasks):
73
- task.cancel()
74
- try:
75
- await task
76
- except asyncio.CancelledError:
77
- pass
78
- except Exception:
79
- self.logger.debug("Background task raised during cancellation", exc_info=True)
80
- self._background_tasks.discard(task)
81
-
82
- if self._recv_task:
83
- self._recv_task.cancel()
84
- with contextlib.suppress(asyncio.CancelledError):
85
- await self._recv_task
86
- self._recv_task = None
87
-
88
- if self._outgoing_task:
89
- self._outgoing_task.cancel()
90
- with contextlib.suppress(asyncio.CancelledError):
91
- await self._outgoing_task
92
- self._outgoing_task = None
93
-
94
- for fut in self._pending.values():
95
- if not fut.done():
96
- fut.set_exception(WebSocketNotConnectedError())
97
- self._pending.clear()
98
-
99
- if self._ws:
100
- try:
101
- await self._ws.close()
102
- except Exception:
103
- self.logger.debug("Error closing ws during cleanup", exc_info=True)
104
- self._ws = None
105
-
106
- self.is_connected = False
107
- self.logger.info("Client start() cleaned up")
108
-
109
- async def idle(self):
110
- """
111
- Поддерживает клиента в «ожидающем» состоянии до закрытия клиента или иного прерывающего события.
112
-
113
- :return: Никогда не возвращает значение; функция блокирует выполнение.
114
- :rtype: None
115
- """
116
- await asyncio.Event().wait()
117
-
118
- def inspect(self) -> None:
119
- """
120
- Выводит в лог текущий статус клиента для отладки.
121
- """
122
- self.logger.info("Pymax")
123
- self.logger.info("---------")
124
- self.logger.info(f"Connected: {self.is_connected}")
125
- if self.me is not None:
126
- self.logger.info(f"Me: {self.me.names[0].first_name} ({self.me.id})")
127
- else:
128
- self.logger.info("Me: N/A")
129
- self.logger.info(f"Dialogs: {len(self.dialogs)}")
130
- self.logger.info(f"Chats: {len(self.chats)}")
131
- self.logger.info(f"Channels: {len(self.channels)}")
132
- self.logger.info(f"Users cached: {len(self._users)}")
133
- self.logger.info(f"Background tasks: {len(self._background_tasks)}")
134
- self.logger.info(f"Scheduled tasks: {len(self._scheduled_tasks)}")
135
- self.logger.info("---------")
136
-
137
- async def __aenter__(self) -> Self:
138
- self._create_safe_task(self.start(), name="start")
139
- while not self.is_connected:
140
- await asyncio.sleep(0.05)
141
- return self
142
-
143
- async def __aexit__(self, exc_type, exc, tb) -> None:
144
- await self.close()
145
-
146
- @abstractmethod
147
- async def login_with_code(self, temp_token: str, code: str, start: bool = False) -> None:
148
- pass
149
-
150
- @abstractmethod
151
- async def _post_login_tasks(self, sync: bool = True) -> None:
152
- pass
153
-
154
- @abstractmethod
155
- async def _wait_forever(self) -> None:
156
- pass
157
-
158
- @abstractmethod
159
- async def start(self) -> None:
160
- pass
161
-
162
- @abstractmethod
163
- async def close(self) -> None:
164
- pass
165
-
166
-
167
- class BaseTransport(ClientProtocol):
168
- @abstractmethod
169
- async def connect(
170
- self, user_agent: UserAgentPayload | None = None
171
- ) -> dict[str, Any] | None: ...
172
-
173
- @abstractmethod
174
- async def _send_and_wait(
175
- self,
176
- opcode: Opcode,
177
- payload: dict[str, Any],
178
- cmd: int = 0,
179
- timeout: float = DEFAULT_TIMEOUT,
180
- ) -> dict[str, Any]: ...
181
-
182
- @abstractmethod
183
- async def _recv_loop(self) -> None: ...
184
-
185
- def _make_message(
186
- self, opcode: Opcode, payload: dict[str, Any], cmd: int = 0
187
- ) -> dict[str, Any]:
188
- self._seq += 1
189
-
190
- msg = BaseWebSocketMessage(
191
- cmd=cmd,
192
- seq=self._seq,
193
- opcode=opcode.value,
194
- payload=payload,
195
- ).model_dump(by_alias=True)
196
-
197
- self.logger.debug("make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq)
198
- return msg
199
-
200
- async def _send_interactive_ping(self) -> None:
201
- while self.is_connected:
202
- try:
203
- await self._send_and_wait(
204
- opcode=Opcode.PING,
205
- payload={"interactive": True},
206
- cmd=0,
207
- )
208
- self.logger.debug("Interactive ping sent successfully")
209
- except Exception:
210
- self.logger.warning("Interactive ping failed", exc_info=True)
211
- await asyncio.sleep(DEFAULT_PING_INTERVAL)
212
-
213
- async def _handshake(self, user_agent: UserAgentPayload) -> dict[str, Any]:
214
- self.logger.debug(
215
- "Sending handshake with user_agent keys=%s",
216
- user_agent.model_dump(by_alias=True).keys(),
217
- )
218
-
219
- user_agent_json = user_agent.model_dump(by_alias=True)
220
- resp = await self._send_and_wait(
221
- opcode=Opcode.SESSION_INIT,
222
- payload={"deviceId": str(self._device_id), "userAgent": user_agent_json},
223
- )
224
-
225
- if resp.get("payload", {}).get("error"):
226
- MixinsUtils.handle_error(resp)
227
-
228
- self.logger.info("Handshake completed")
229
- return resp
230
-
231
- async def _process_message_handler(
232
- self,
233
- handler: Callable[[Message], Any],
234
- filter: BaseFilter[Message] | None,
235
- message: Message,
236
- ):
237
- result = None
238
- if filter:
239
- if filter(message):
240
- result = handler(message)
241
- else:
242
- return
243
- else:
244
- result = handler(message)
245
- if asyncio.iscoroutine(result):
246
- self._create_safe_task(result, name=f"handler-{handler.__name__}")
247
-
248
- def _parse_json(self, raw: Any) -> dict[str, Any] | None:
249
- try:
250
- return json.loads(raw)
251
- except Exception:
252
- self.logger.warning("JSON parse error", exc_info=True)
253
- return None
254
-
255
- def _handle_pending(self, seq: int | None, data: dict) -> bool:
256
- if isinstance(seq, int):
257
- fut = self._pending.get(seq)
258
- if fut and not fut.done():
259
- fut.set_result(data)
260
- self.logger.debug("Matched response for pending seq=%s", seq)
261
- return True
262
- return False
263
-
264
- async def _handle_incoming_queue(self, data: dict[str, Any]) -> None:
265
- if self._incoming:
266
- try:
267
- self._incoming.put_nowait(data)
268
- except asyncio.QueueFull:
269
- self.logger.warning(
270
- "Incoming queue full; dropping message seq=%s", data.get("seq")
271
- )
272
-
273
- async def _handle_file_upload(self, data: dict[str, Any]) -> None:
274
- if data.get("opcode") != Opcode.NOTIF_ATTACH:
275
- return
276
- payload = data.get("payload", {})
277
- for key in ("fileId", "videoId"):
278
- id_ = payload.get(key)
279
- if id_ is not None:
280
- fut = self._file_upload_waiters.pop(id_, None)
281
- if fut and not fut.done():
282
- fut.set_result(data)
283
- self.logger.debug("Fulfilled file upload waiter for %s=%s", key, id_)
284
-
285
- async def _send_notification_response(self, chat_id: int, message_id: str) -> None:
286
- await self._send_and_wait(
287
- opcode=Opcode.NOTIF_MESSAGE,
288
- payload={"chatId": chat_id, "messageId": message_id},
289
- cmd=0,
290
- )
291
- self.logger.debug(
292
- "Sent NOTIF_MESSAGE_RECEIVED for chat_id=%s message_id=%s", chat_id, message_id
293
- )
294
-
295
- async def _handle_message_notifications(self, data: dict) -> None:
296
- if data.get("opcode") != Opcode.NOTIF_MESSAGE.value:
297
- return
298
- payload = data.get("payload", {})
299
- msg = Message.from_dict(payload)
300
- if not msg:
301
- return
302
-
303
- if msg.chat_id and msg.id:
304
- await self._send_notification_response(msg.chat_id, str(msg.id))
305
-
306
- handlers_map = {
307
- MessageStatus.EDITED: self._on_message_edit_handlers,
308
- MessageStatus.REMOVED: self._on_message_delete_handlers,
309
- }
310
- if msg.status and msg.status in handlers_map:
311
- for handler, filter in handlers_map[msg.status]:
312
- await self._process_message_handler(handler, filter, msg)
313
- if msg.status is None:
314
- for handler, filter in self._on_message_handlers:
315
- await self._process_message_handler(handler, filter, msg)
316
-
317
- async def _handle_reactions(self, data: dict):
318
- if data.get("opcode") != Opcode.NOTIF_MSG_REACTIONS_CHANGED:
319
- return
320
-
321
- payload = data.get("payload", {})
322
- chat_id = payload.get("chatId")
323
- message_id = payload.get("messageId")
324
-
325
- if not (chat_id and message_id):
326
- return
327
-
328
- total_count = payload.get("totalCount")
329
- your_reaction = payload.get("yourReaction")
330
- counters = [ReactionCounter.from_dict(c) for c in payload.get("counters", [])]
331
-
332
- reaction_info = ReactionInfo(
333
- total_count=total_count,
334
- your_reaction=your_reaction,
335
- counters=counters,
336
- )
337
-
338
- for handler in self._on_reaction_change_handlers:
339
- try:
340
- result = handler(message_id, chat_id, reaction_info)
341
- if asyncio.iscoroutine(result):
342
- await result
343
- except Exception as e:
344
- self.logger.exception("Error in on_reaction_change_handler: %s", e)
345
-
346
- async def _handle_chat_updates(self, data: dict) -> None:
347
- if data.get("opcode") != Opcode.NOTIF_CHAT:
348
- return
349
-
350
- payload = data.get("payload", {})
351
- chat_data = payload.get("chat", {})
352
- chat = Chat.from_dict(chat_data)
353
- if not chat:
354
- return
355
-
356
- for handler in self._on_chat_update_handlers:
357
- try:
358
- result = handler(chat)
359
- if asyncio.iscoroutine(result):
360
- await result
361
- except Exception as e:
362
- self.logger.exception("Error in on_chat_update_handler: %s", e)
363
-
364
- async def _handle_raw_receive(self, data: dict[str, Any]) -> None:
365
- for handler in self._on_raw_receive_handlers:
366
- try:
367
- result = handler(data)
368
- if asyncio.iscoroutine(result):
369
- await result
370
- except Exception as e:
371
- self.logger.exception("Error in on_raw_receive_handler: %s", e)
372
-
373
- async def _dispatch_incoming(self, data: dict[str, Any]) -> None:
374
- await self._handle_raw_receive(data)
375
- await self._handle_file_upload(data)
376
- await self._handle_message_notifications(data)
377
- await self._handle_reactions(data)
378
- await self._handle_chat_updates(data)
379
-
380
- def _log_task_exception(self, fut: asyncio.Future[Any]) -> None:
381
- try:
382
- fut.result()
383
- except asyncio.CancelledError:
384
- pass
385
- except Exception as e:
386
- self.logger.exception("Error retrieving task exception: %s", e)
387
-
388
- async def _queue_message(
389
- self,
390
- opcode: int,
391
- payload: dict[str, Any],
392
- cmd: int = 0,
393
- timeout: float = DEFAULT_TIMEOUT,
394
- max_retries: int = 3,
395
- ) -> None:
396
- if self._outgoing is None:
397
- self.logger.warning("Outgoing queue not initialized")
398
- return
399
-
400
- message = {
401
- "opcode": opcode,
402
- "payload": payload,
403
- "cmd": cmd,
404
- "timeout": timeout,
405
- "retry_count": 0,
406
- "max_retries": max_retries,
407
- }
408
-
409
- await self._outgoing.put(message)
410
- self.logger.debug("Message queued for sending")
411
-
412
- async def _outgoing_loop(self) -> None:
413
- while self.is_connected:
414
- try:
415
- if self._outgoing is None:
416
- await asyncio.sleep(0.1)
417
- continue
418
-
419
- if self._circuit_breaker:
420
- if time.time() - self._last_error_time > 60:
421
- self._circuit_breaker = False
422
- self._error_count = 0
423
- self.logger.info("Circuit breaker reset")
424
- else:
425
- await asyncio.sleep(5)
426
- continue
427
-
428
- message = await self._outgoing.get() # TODO: persistent msg q mb?
429
- if not message:
430
- continue
431
-
432
- retry_count = message.get("retry_count", 0)
433
- max_retries = message.get("max_retries", 3)
434
-
435
- try:
436
- await self._send_and_wait(
437
- opcode=message["opcode"],
438
- payload=message["payload"],
439
- cmd=message.get("cmd", 0),
440
- timeout=message.get("timeout", DEFAULT_TIMEOUT),
441
- )
442
- self.logger.debug("Message sent successfully from queue")
443
- self._error_count = max(0, self._error_count - 1)
444
- except Exception as e:
445
- self._error_count += 1
446
- self._last_error_time = time.time()
447
-
448
- if self._error_count > 10:
449
- self._circuit_breaker = True
450
- self.logger.warning(
451
- "Circuit breaker activated due to %d consecutive errors",
452
- self._error_count,
453
- )
454
- await self._outgoing.put(message)
455
- continue
456
-
457
- retry_delay = self._get_retry_delay(e, retry_count)
458
- self.logger.warning(
459
- "Failed to send message from queue: %s (delay: %ds)",
460
- e,
461
- retry_delay,
462
- )
463
-
464
- if retry_count < max_retries:
465
- message["retry_count"] = retry_count + 1
466
- await asyncio.sleep(retry_delay)
467
- await self._outgoing.put(message)
468
- else:
469
- self.logger.error(
470
- "Message failed after %d retries, dropping",
471
- max_retries,
472
- )
473
-
474
- except Exception:
475
- self.logger.exception("Error in outgoing loop")
476
- await asyncio.sleep(1)
477
-
478
- def _get_retry_delay(self, error: Exception, retry_count: int) -> float:
479
- if isinstance(error, (ConnectionError, OSError)):
480
- return 1.0
481
- elif isinstance(error, TimeoutError):
482
- return 5.0
483
- elif isinstance(error, WebSocketNotConnectedError):
484
- return 2.0
485
- else:
486
- return float(2**retry_count)
487
-
488
- async def _sync(self, user_agent: UserAgentPayload | None = None) -> None:
489
- self.logger.info("Starting initial sync")
490
-
491
- if user_agent is None:
492
- user_agent = self.headers or UserAgentPayload()
493
-
494
- payload = SyncPayload(
495
- interactive=True,
496
- token=self._token,
497
- chats_sync=0,
498
- contacts_sync=0,
499
- presence_sync=0,
500
- drafts_sync=0,
501
- chats_count=40,
502
- user_agent=user_agent,
503
- ).model_dump(by_alias=True)
504
- try:
505
- data = await self._send_and_wait(opcode=Opcode.LOGIN, payload=payload)
506
- raw_payload = data.get("payload", {})
507
-
508
- if error := raw_payload.get("error"):
509
- MixinsUtils.handle_error(data)
510
-
511
- for raw_chat in raw_payload.get("chats", []):
512
- try:
513
- if raw_chat.get("type") == ChatType.DIALOG.value:
514
- self.dialogs.append(Dialog.from_dict(raw_chat))
515
- elif raw_chat.get("type") == ChatType.CHAT.value:
516
- self.chats.append(Chat.from_dict(raw_chat))
517
- elif raw_chat.get("type") == ChatType.CHANNEL.value:
518
- self.channels.append(Channel.from_dict(raw_chat))
519
- except Exception:
520
- self.logger.exception("Error parsing chat entry")
521
-
522
- for raw_user in raw_payload.get("contacts", []):
523
- try:
524
- user = User.from_dict(raw_user)
525
- if user:
526
- self.contacts.append(user)
527
- except Exception:
528
- self.logger.exception("Error parsing contact entry")
529
-
530
- if raw_payload.get("profile", {}).get("contact"):
531
- self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {}))
532
-
533
- self.logger.info(
534
- "Sync completed: dialogs=%d chats=%d channels=%d",
535
- len(self.dialogs),
536
- len(self.chats),
537
- len(self.channels),
538
- )
539
-
540
- except Exception as e:
541
- self.logger.exception("Sync failed")
542
- self.is_connected = False
543
- if self._ws:
544
- await self._ws.close()
545
- self._ws = None
546
- raise
547
-
548
- async def _get_chat(self, chat_id: int) -> Chat | None:
549
- for chat in self.chats:
550
- if chat.id == chat_id:
551
- return chat
552
- return None
pymax/mixins/__init__.py DELETED
@@ -1,40 +0,0 @@
1
- from .auth import AuthMixin
2
- from .channel import ChannelMixin
3
- from .group import GroupMixin
4
- from .handler import HandlerMixin
5
- from .message import MessageMixin
6
- from .scheduler import SchedulerMixin
7
- from .self import SelfMixin
8
- from .socket import SocketMixin
9
- from .telemetry import TelemetryMixin
10
- from .user import UserMixin
11
- from .websocket import WebSocketMixin
12
-
13
-
14
- class ApiMixin(
15
- AuthMixin,
16
- HandlerMixin,
17
- UserMixin,
18
- ChannelMixin,
19
- SelfMixin,
20
- MessageMixin,
21
- TelemetryMixin,
22
- GroupMixin,
23
- SchedulerMixin,
24
- ):
25
- pass
26
-
27
-
28
- __all__ = [
29
- "ApiMixin",
30
- "AuthMixin",
31
- "ChannelMixin",
32
- "HandlerMixin",
33
- "MessageMixin",
34
- "SchedulerMixin",
35
- "SelfMixin",
36
- "SocketMixin",
37
- "TelemetryMixin",
38
- "UserMixin",
39
- "WebSocketMixin",
40
- ]