maxapi-python 1.1.14__py3-none-any.whl → 1.1.16__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 1.1.14
3
+ Version: 1.1.16
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/ink-developer/PyMax
6
6
  Project-URL: Repository, https://github.com/ink-developer/PyMax
@@ -145,7 +145,7 @@ if __name__ == "__main__":
145
145
 
146
146
  ## Документация
147
147
 
148
- [WIP](https://ink-developer.github.io/)
148
+ [WIP](https://ink-developer.github.io/PyMax)
149
149
 
150
150
  ## Лицензия
151
151
 
@@ -155,10 +155,14 @@ if __name__ == "__main__":
155
155
 
156
156
  [Telegram](https://t.me/pymax_news)
157
157
 
158
- ## Авторы
158
+ ## Star History
159
+
160
+ [![Star History Chart](https://api.star-history.com/svg?repos=ink-developer/PyMax&type=date&legend=top-left)](https://www.star-history.com/#ink-developer/PyMax&type=date&legend=top-left)
159
161
 
160
- - **[ink-developer](https://github.com/ink-developer)** — Оригинальный автор проекта
162
+ ## Авторы
161
163
  - **[ink](https://github.com/ink-developer)** — Главный разработчик, исследование API и его документация
164
+ - **[noxzion](https://github.com/noxzion)** — Оригинальный автор проекта
165
+
162
166
 
163
167
  ## Контрибьюторы
164
168
 
@@ -0,0 +1,31 @@
1
+ pymax/__init__.py,sha256=vsdE56xHZnvDCXqLwSJL3v9MxsXcmmxE9p0QSHG66HA,1846
2
+ pymax/core.py,sha256=EKgMlOnqvsPJwOQeCgQE1tnKk4E8kMnmxCAfo4jrP_k,11083
3
+ pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
4
+ pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
5
+ pymax/files.py,sha256=dRuOpvoJZWiH4xa_HVGyqQ-_Zzj-sVikElHmrPjwgs0,3166
6
+ pymax/filters.py,sha256=4hehzyQlyxBxLiEhtSH_KQdFHx8gYaqyIOOD72EsSS0,1536
7
+ pymax/formatter.py,sha256=OVsTwambHhXlOMd0wVECJWuB_S2wSEVKdMLCzgPcvYQ,859
8
+ pymax/formatting.py,sha256=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
9
+ pymax/interfaces.py,sha256=NspCm8asUhG_LHo-CTseGMg-zKAHU5H4bl6BMEwqAK0,3416
10
+ pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
11
+ pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
12
+ pymax/payloads.py,sha256=qaafULDGBXsQ7gNFC374wZVUwN5tzJLHwkxtAmglOzU,6292
13
+ pymax/types.py,sha256=tiSU_YpvOlx710SYQbYLNw_SjwefgqOu9Xk5ev_PbFU,30931
14
+ pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
15
+ pymax/mixins/auth.py,sha256=H4Zp3n8cwpv4Q3Mn1_Kb7Oh9DbTL7T9GcWJ6R1JN7ls,6672
16
+ pymax/mixins/channel.py,sha256=dMuJRnbqZisN8kcPFCCe1sIOOBQl2uT4P49PpZXcoKE,5206
17
+ pymax/mixins/group.py,sha256=7oa7RpiqnlcnAsnIHOfSiujNYAzUZ9lkTy9NGW5KVUE,8654
18
+ pymax/mixins/handler.py,sha256=AhxIRvwftkuWN435_CXede2ZVWrDde4zkMPZtwIm5IU,3892
19
+ pymax/mixins/message.py,sha256=ezU9d6r4MkYjH67gZ9SFLYPKqo4Nb6lswqDsEW5p-Bg,22329
20
+ pymax/mixins/self.py,sha256=tDQrUdUpsCu7qGkWLtKxTfTHPHU5_r3qsn-eptHG2KY,1198
21
+ pymax/mixins/socket.py,sha256=j6XTo_M3rNw-az2PfSW6oJ_YHg9M7cWARY4cXpMllDY,22256
22
+ pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
23
+ pymax/mixins/user.py,sha256=5utoK7Z-7lySOg0PEO69b6h_3kjfcnV-YydTZIdNj8g,7120
24
+ pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
25
+ pymax/mixins/websocket.py,sha256=gqGP-3XPrbo4DPqUL4H8tuOAjZQ4QKbBvJGxOFqwk9E,17254
26
+ pymax/static/constant.py,sha256=Q1NrmaRj17Gdhk3FmUp3HIwrad1TDorq3wFdQlOCzN8,1027
27
+ pymax/static/enum.py,sha256=ofqxOsRzi6XvZN_UOPinxug1uPEulJsQ95MWifAfCqA,4562
28
+ maxapi_python-1.1.16.dist-info/METADATA,sha256=OLNhs5zuW9ux6C_57MVhTscaZknfVXh90OYf5P2_X8A,6245
29
+ maxapi_python-1.1.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ maxapi_python-1.1.16.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
31
+ maxapi_python-1.1.16.dist-info/RECORD,,
pymax/__init__.py CHANGED
@@ -9,14 +9,22 @@ from .core import (
9
9
  from .exceptions import (
10
10
  InvalidPhoneError,
11
11
  LoginError,
12
+ ResponseError,
13
+ ResponseStructureError,
14
+ SocketNotConnectedError,
15
+ SocketSendError,
12
16
  WebSocketNotConnectedError,
13
17
  )
14
18
  from .static.enum import (
15
19
  AccessType,
20
+ AttachType,
16
21
  AuthType,
17
22
  ChatType,
23
+ ContactAction,
18
24
  DeviceType,
19
25
  ElementType,
26
+ FormattingType,
27
+ MarkupType,
20
28
  MessageStatus,
21
29
  MessageType,
22
30
  Opcode,
@@ -24,10 +32,26 @@ from .static.enum import (
24
32
  from .types import (
25
33
  Channel,
26
34
  Chat,
35
+ Contact,
36
+ ControlAttach,
27
37
  Dialog,
28
38
  Element,
39
+ FileAttach,
40
+ FileRequest,
41
+ Me,
42
+ Member,
29
43
  Message,
44
+ MessageLink,
45
+ Name,
46
+ Names,
47
+ PhotoAttach,
48
+ Presence,
49
+ ReactionCounter,
50
+ ReactionInfo,
51
+ Session,
30
52
  User,
53
+ VideoAttach,
54
+ VideoRequest,
31
55
  )
32
56
 
33
57
  __author__ = "ink-developer"
@@ -35,19 +59,43 @@ __author__ = "ink-developer"
35
59
  __all__ = [
36
60
  # Перечисления и константы
37
61
  "AccessType",
62
+ "AttachType",
38
63
  "AuthType",
64
+ "ContactAction",
65
+ "FormattingType",
66
+ "MarkupType",
39
67
  # Типы данных
40
68
  "Channel",
41
69
  "Chat",
42
70
  "ChatType",
71
+ "Contact",
72
+ "ControlAttach",
43
73
  "DeviceType",
44
74
  "Dialog",
45
75
  "Element",
46
76
  "ElementType",
77
+ "FileAttach",
78
+ "FileRequest",
79
+ "Me",
80
+ "Member",
81
+ "MessageLink",
82
+ "Name",
83
+ "Names",
84
+ "PhotoAttach",
85
+ "Presence",
86
+ "ReactionCounter",
87
+ "ReactionInfo",
88
+ "Session",
89
+ "VideoAttach",
90
+ "VideoRequest",
47
91
  # Исключения
48
92
  "InvalidPhoneError",
49
93
  "LoginError",
50
94
  "WebSocketNotConnectedError",
95
+ "ResponseError",
96
+ "ResponseStructureError",
97
+ "SocketNotConnectedError",
98
+ "SocketSendError",
51
99
  # Клиент
52
100
  "MaxClient",
53
101
  "Message",
pymax/core.py CHANGED
@@ -3,12 +3,16 @@ import logging
3
3
  import socket
4
4
  import ssl
5
5
  import time
6
+ import traceback
7
+ from collections.abc import Awaitable
6
8
  from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any, Literal
7
10
 
8
11
  from typing_extensions import override
9
12
 
10
13
  from .crud import Database
11
14
  from .exceptions import InvalidPhoneError
15
+ from .formatter import ColoredFormatter
12
16
  from .mixins import ApiMixin, SocketMixin, WebSocketMixin
13
17
  from .payloads import UserAgentPayload
14
18
  from .static.constant import (
@@ -17,6 +21,16 @@ from .static.constant import (
17
21
  WEBSOCKET_URI,
18
22
  )
19
23
 
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Awaitable, Callable
26
+ from typing import Any
27
+
28
+ import websockets
29
+
30
+ from .filters import Filter
31
+ from .types import Channel, Chat, Dialog, Me, Message, User
32
+
33
+
20
34
  logger = logging.getLogger(__name__)
21
35
 
22
36
 
@@ -36,6 +50,12 @@ class MaxClient(ApiMixin, WebSocketMixin):
36
50
  процесс логина по номеру телефона.
37
51
  host (str, optional): Хост API сервера. По умолчанию Constants.HOST.value.
38
52
  port (int, optional): Порт API сервера. По умолчанию Constants.PORT.value.
53
+ registration (bool, optional): Флаг регистрации нового пользователя. По умолчанию False.
54
+ first_name (str, optional): Имя пользователя для регистрации. Требуется, если registration=True.
55
+ last_name (str | None, optional): Фамилия пользователя для регистрации.
56
+ send_fake_telemetry (bool, optional): Флаг отправки фейковой телеметрии. По умолчанию True.
57
+ proxy (str | Literal[True] | None, optional): Прокси для подключения к WebSocket.
58
+ (См. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
39
59
 
40
60
  Raises:
41
61
  InvalidPhoneError: Если формат номера телефона неверный.
@@ -50,29 +70,73 @@ class MaxClient(ApiMixin, WebSocketMixin):
50
70
  send_fake_telemetry: bool = True,
51
71
  host: str = HOST,
52
72
  port: int = PORT,
73
+ proxy: str | Literal[True] | None = None,
53
74
  work_dir: str = ".",
75
+ registration: bool = False,
76
+ first_name: str = "",
77
+ last_name: str | None = None,
54
78
  logger: logging.Logger | None = None,
55
79
  ) -> None:
56
- logger = logger or logging.getLogger(f"{__name__}.MaxClient")
57
- ApiMixin.__init__(self, token=token, logger=logger)
58
- WebSocketMixin.__init__(self, token=token, logger=logger)
80
+ self.logger = logger or logging.getLogger(f"{__name__}")
59
81
  self.uri: str = uri
60
82
  self.phone: str = phone
61
83
  if not self._check_phone():
62
84
  raise InvalidPhoneError(self.phone)
63
85
  self.host: str = host
64
86
  self.port: int = port
87
+ self.registration: bool = registration
88
+ self.first_name: str = first_name
89
+ self.last_name: str | None = last_name
90
+ self.proxy: str | Literal[True] | None = proxy
91
+
92
+ self.is_connected: bool = False
93
+
94
+ self.chats: list[Chat] = []
95
+ self.dialogs: list[Dialog] = []
96
+ self.channels: list[Channel] = []
97
+ self.me: Me | None = None
98
+ self._users: dict[int, User] = {}
99
+
65
100
  self._work_dir: str = work_dir
66
101
  self._database_path: Path = Path(work_dir) / "session.db"
67
102
  self._database_path.parent.mkdir(parents=True, exist_ok=True)
68
103
  self._database_path.touch(exist_ok=True)
69
104
  self._database = Database(self._work_dir)
105
+
106
+ self._incoming: asyncio.Queue[dict[str, Any]] | None = None
107
+ self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
108
+ self._recv_task: asyncio.Task[Any] | None = None
109
+ self._outgoing_task: asyncio.Task[Any] | None = None
110
+ self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
111
+ self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
112
+ self._background_tasks: set[asyncio.Task[Any]] = set()
113
+
114
+ self._seq: int = 0
115
+ self._error_count: int = 0
116
+ self._circuit_breaker: bool = False
117
+ self._last_error_time: float = 0.0
118
+
70
119
  self._device_id = self._database.get_device_id()
120
+ self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
121
+
71
122
  self._token = self._database.get_auth_token() or token
72
123
  self.user_agent = headers
73
124
  self._send_fake_telemetry: bool = send_fake_telemetry
74
125
  self._session_id: int = int(time.time() * 1000)
75
126
  self._action_id: int = 1
127
+ self._current_screen: str = "chats_list_tab"
128
+
129
+ self._on_message_handlers: list[
130
+ tuple[Callable[[Message], Any], Filter | None]
131
+ ] = []
132
+ self._on_message_edit_handlers: list[
133
+ tuple[Callable[[Message], Any], Filter | None]
134
+ ] = []
135
+ self._on_message_delete_handlers: list[
136
+ tuple[Callable[[Message], Any], Filter | None]
137
+ ] = []
138
+ self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
139
+
76
140
  self._ssl_context = ssl.create_default_context()
77
141
  self._ssl_context.set_ciphers("DEFAULT")
78
142
  self._ssl_context.check_hostname = True
@@ -80,6 +144,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
80
144
  self._ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
81
145
  self._ssl_context.load_default_certs()
82
146
  self._socket: socket.socket | None = None
147
+ self._ws: websockets.ClientConnection | None = None
148
+
83
149
  self._setup_logger()
84
150
  self.logger.debug(
85
151
  "Initialized MaxClient uri=%s work_dir=%s",
@@ -88,13 +154,16 @@ class MaxClient(ApiMixin, WebSocketMixin):
88
154
  )
89
155
 
90
156
  def _setup_logger(self) -> None:
91
- if not logger.handlers:
157
+ if not self.logger.handlers:
158
+ if not self.logger.level:
159
+ self.logger.setLevel(logging.INFO)
92
160
  handler = logging.StreamHandler()
93
- formatter = logging.Formatter(
94
- "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
161
+ formatter = ColoredFormatter(
162
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
163
+ datefmt="%Y-%m-%d %H:%M:%S",
95
164
  )
96
165
  handler.setFormatter(formatter)
97
- logger.addHandler(handler)
166
+ self.logger.addHandler(handler)
98
167
 
99
168
  async def _wait_forever(self):
100
169
  try:
@@ -102,6 +171,18 @@ class MaxClient(ApiMixin, WebSocketMixin):
102
171
  except asyncio.CancelledError:
103
172
  self.logger.debug("wait_closed cancelled")
104
173
 
174
+ async def _safe_execute(self, coro, *, context: str = "unknown"):
175
+ """
176
+ Безопасно выполняет пользовательскую корутину.
177
+ Логирует traceback, но не роняет event loop.
178
+ """
179
+ try:
180
+ return await coro
181
+ except Exception as e:
182
+ self.logger.error(
183
+ f"Unhandled exception in {context}: {e}\n{traceback.format_exc()}"
184
+ )
185
+
105
186
  async def close(self) -> None:
106
187
  try:
107
188
  self.logger.info("Closing client")
@@ -111,6 +192,12 @@ class MaxClient(ApiMixin, WebSocketMixin):
111
192
  await self._recv_task
112
193
  except asyncio.CancelledError:
113
194
  self.logger.debug("recv_task cancelled")
195
+ if self._outgoing_task:
196
+ self._outgoing_task.cancel()
197
+ try:
198
+ await self._outgoing_task
199
+ except asyncio.CancelledError:
200
+ self.logger.debug("outgoing_task cancelled")
114
201
  if self._ws:
115
202
  await self._ws.close()
116
203
  self.is_connected = False
@@ -118,6 +205,23 @@ class MaxClient(ApiMixin, WebSocketMixin):
118
205
  except Exception:
119
206
  self.logger.exception("Error closing client")
120
207
 
208
+ def _create_safe_task(self, coro: Awaitable[Any], *, name: str | None = None):
209
+ async def runner():
210
+ try:
211
+ return await coro
212
+ except asyncio.CancelledError:
213
+ raise
214
+ except Exception as e:
215
+ self.logger.error(
216
+ f"Unhandled exception in task {name or coro}: {e}",
217
+ exc_info=e,
218
+ )
219
+ return None
220
+
221
+ task = asyncio.create_task(runner(), name=name)
222
+ self._background_tasks.add(task)
223
+ return task
224
+
121
225
  async def start(self) -> None:
122
226
  """
123
227
  Запускает клиент, подключается к WebSocket, авторизует
@@ -127,6 +231,11 @@ class MaxClient(ApiMixin, WebSocketMixin):
127
231
  self.logger.info("Client starting")
128
232
  await self._connect(self.user_agent)
129
233
 
234
+ if self.registration:
235
+ if not self.first_name:
236
+ raise ValueError("First name is required for registration")
237
+ await self._register(self.first_name, self.last_name)
238
+
130
239
  if self._token and self._database.get_auth_token() is None:
131
240
  self._database.update_auth_token(self._device_id, self._token)
132
241
 
@@ -139,7 +248,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
139
248
  self.logger.debug("Calling on_start handler")
140
249
  result = self._on_start_handler()
141
250
  if asyncio.iscoroutine(result):
142
- await result
251
+ await self._safe_execute(result, context="on_start handler")
143
252
 
144
253
  ping_task = asyncio.create_task(self._send_interactive_ping())
145
254
  ping_task.add_done_callback(self._log_task_exception)
pymax/exceptions.py CHANGED
@@ -39,15 +39,6 @@ class SocketSendError(Exception):
39
39
  super().__init__("Send and wait failed (socket)")
40
40
 
41
41
 
42
- class LoginError(Exception):
43
- """
44
- Исключение, вызываемое при ошибке авторизации.
45
- """
46
-
47
- def __init__(self, message: str) -> None:
48
- super().__init__(f"Login error: {message}")
49
-
50
-
51
42
  class ResponseError(Exception):
52
43
  """
53
44
  Исключение, вызываемое при ошибке в ответе от сервера.
@@ -64,3 +55,54 @@ class ResponseStructureError(Exception):
64
55
 
65
56
  def __init__(self, message: str) -> None:
66
57
  super().__init__(f"Response structure error: {message}")
58
+
59
+
60
+ class Error(Exception):
61
+ """
62
+ Базовое исключение для ошибок PyMax.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ error: str,
68
+ message: str,
69
+ title: str,
70
+ localized_message: str | None = None,
71
+ ) -> None:
72
+ self.error = error
73
+ self.message = message
74
+ self.title = title
75
+ self.localized_message = localized_message
76
+
77
+ parts = []
78
+ if localized_message:
79
+ parts.append(localized_message)
80
+ if message:
81
+ parts.append(message)
82
+ if title:
83
+ parts.append(f"({title})")
84
+ parts.append(f"[{error}]")
85
+
86
+ super().__init__("PyMax Error: " + " ".join(parts))
87
+
88
+
89
+ class RateLimitError(Error):
90
+ """
91
+ Исключение, вызываемое при превышении лимита запросов.
92
+ """
93
+
94
+ def __init__(
95
+ self, error: str, message: str, title: str, localized_message: str | None = None
96
+ ) -> None:
97
+ super().__init__(error, message, title, localized_message)
98
+
99
+
100
+ class LoginError(Error):
101
+ """
102
+ Исключение, вызываемое при ошибке авторизации.
103
+ """
104
+
105
+ def __init__(
106
+ self, error: str, message: str, title: str, localized_message: str | None = None
107
+ ) -> None:
108
+ super().__init__(error, message, title, localized_message)
pymax/files.py CHANGED
@@ -90,6 +90,20 @@ class Video(BaseFile):
90
90
 
91
91
 
92
92
  class File(BaseFile):
93
+ def __init__(
94
+ self, url: str | None = None, path: str | None = None
95
+ ) -> None:
96
+ self.file_name: str = ""
97
+ if path:
98
+ self.file_name = Path(path).name
99
+ elif url:
100
+ self.file_name = Path(url).name
101
+
102
+ if not self.file_name:
103
+ raise ValueError("Either url or path must be provided.")
104
+
105
+ super().__init__(url, path)
106
+
93
107
  @override
94
108
  async def read(self) -> bytes:
95
109
  return await super().read()
pymax/formatter.py ADDED
@@ -0,0 +1,30 @@
1
+ import logging
2
+ from typing import ClassVar
3
+
4
+
5
+ class ColoredFormatter(logging.Formatter):
6
+ COLORS: ClassVar = {
7
+ "DEBUG": "\033[37m",
8
+ "INFO": "\033[36m",
9
+ "WARNING": "\033[33m",
10
+ "ERROR": "\033[31m",
11
+ "CRITICAL": "\033[41m",
12
+ }
13
+
14
+ RESET = "\033[0m"
15
+ DIM = "\033[2m"
16
+ BOLD = "\033[1m"
17
+
18
+ def format(self, record: logging.LogRecord) -> str:
19
+ level_color = self.COLORS.get(record.levelname, self.RESET)
20
+ time_color = self.DIM
21
+ name_color = "\033[35m"
22
+ message_color = self.RESET
23
+
24
+ log = (
25
+ f"{time_color}{self.formatTime(record, '%H:%M:%S')}{self.RESET} "
26
+ f"[{level_color}{record.levelname}{self.RESET}] "
27
+ f"{name_color}{record.name}{self.RESET}: "
28
+ f"{message_color}{record.getMessage()}{self.RESET}"
29
+ )
30
+ return log
pymax/interfaces.py CHANGED
@@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
5
5
  from collections.abc import Awaitable, Callable
6
6
  from logging import Logger
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Any
8
+ from typing import TYPE_CHECKING, Any, Literal
9
9
 
10
10
  import websockets
11
11
 
@@ -37,6 +37,11 @@ class ClientProtocol(ABC):
37
37
  self.me: Me | None = None
38
38
  self.host: str
39
39
  self.port: int
40
+ self.proxy: str | Literal[True] | None
41
+ self.registration: bool
42
+ self.first_name: str
43
+ self.last_name: str | None
44
+ self._token: str | None
40
45
  self._work_dir: str
41
46
  self._database_path: Path
42
47
  self._ws: websockets.ClientConnection | None = None
@@ -44,7 +49,15 @@ class ClientProtocol(ABC):
44
49
  self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
45
50
  self._recv_task: asyncio.Task[Any] | None = None
46
51
  self._incoming: asyncio.Queue[dict[str, Any]] | None = None
52
+ self._file_upload_waiters: dict[
53
+ int, asyncio.Future[dict[str, Any]]
54
+ ] = {}
47
55
  self.user_agent = UserAgentPayload()
56
+ self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
57
+ self._outgoing_task: asyncio.Task[Any] | None = None
58
+ self._error_count: int = 0
59
+ self._circuit_breaker: bool = False
60
+ self._last_error_time: float = 0.0
48
61
  self._session_id: int
49
62
  self._action_id: int = 0
50
63
  self._current_screen: str = "chats_list_tab"
@@ -77,3 +90,20 @@ class ClientProtocol(ABC):
77
90
  @abstractmethod
78
91
  async def _get_chat(self, chat_id: int) -> Chat | None:
79
92
  pass
93
+
94
+ @abstractmethod
95
+ async def _queue_message(
96
+ self,
97
+ opcode: int,
98
+ payload: dict[str, Any],
99
+ cmd: int = 0,
100
+ timeout: float = DEFAULT_TIMEOUT,
101
+ max_retries: int = 3,
102
+ ) -> Message | None:
103
+ pass
104
+
105
+ @abstractmethod
106
+ def _create_safe_task(
107
+ self, coro: Awaitable[Any], name: str | None = None
108
+ ) -> asyncio.Task[Any]:
109
+ pass