maxapi-python 1.1.16__py3-none-any.whl → 1.1.18__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.16
3
+ Version: 1.1.18
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
@@ -1,16 +1,16 @@
1
- pymax/__init__.py,sha256=vsdE56xHZnvDCXqLwSJL3v9MxsXcmmxE9p0QSHG66HA,1846
2
- pymax/core.py,sha256=EKgMlOnqvsPJwOQeCgQE1tnKk4E8kMnmxCAfo4jrP_k,11083
1
+ pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
2
+ pymax/core.py,sha256=Ee5R9GfhjDhSjAd2skKsG7Sp5h7AkvN0gtEWXWL7wVE,14189
3
3
  pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
4
4
  pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
5
5
  pymax/files.py,sha256=dRuOpvoJZWiH4xa_HVGyqQ-_Zzj-sVikElHmrPjwgs0,3166
6
6
  pymax/filters.py,sha256=4hehzyQlyxBxLiEhtSH_KQdFHx8gYaqyIOOD72EsSS0,1536
7
7
  pymax/formatter.py,sha256=OVsTwambHhXlOMd0wVECJWuB_S2wSEVKdMLCzgPcvYQ,859
8
8
  pymax/formatting.py,sha256=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
9
- pymax/interfaces.py,sha256=NspCm8asUhG_LHo-CTseGMg-zKAHU5H4bl6BMEwqAK0,3416
9
+ pymax/interfaces.py,sha256=WWKNGT725GXuYneS9gCOAC6RNtySRs-BTU0fQLyh2OQ,3399
10
10
  pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
11
11
  pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
12
- pymax/payloads.py,sha256=qaafULDGBXsQ7gNFC374wZVUwN5tzJLHwkxtAmglOzU,6292
13
- pymax/types.py,sha256=tiSU_YpvOlx710SYQbYLNw_SjwefgqOu9Xk5ev_PbFU,30931
12
+ pymax/payloads.py,sha256=S1dJwDPanFfIdY_NlXN2epVyibmmL9bceltgLVmEtTA,6304
13
+ pymax/types.py,sha256=RaLn9bUpkxO0SKbDMIHnoFeqV6gqOl2pKDNCa2LxTRI,32102
14
14
  pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
15
15
  pymax/mixins/auth.py,sha256=H4Zp3n8cwpv4Q3Mn1_Kb7Oh9DbTL7T9GcWJ6R1JN7ls,6672
16
16
  pymax/mixins/channel.py,sha256=dMuJRnbqZisN8kcPFCCe1sIOOBQl2uT4P49PpZXcoKE,5206
@@ -22,10 +22,10 @@ pymax/mixins/socket.py,sha256=j6XTo_M3rNw-az2PfSW6oJ_YHg9M7cWARY4cXpMllDY,22256
22
22
  pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
23
23
  pymax/mixins/user.py,sha256=5utoK7Z-7lySOg0PEO69b6h_3kjfcnV-YydTZIdNj8g,7120
24
24
  pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
25
- pymax/mixins/websocket.py,sha256=gqGP-3XPrbo4DPqUL4H8tuOAjZQ4QKbBvJGxOFqwk9E,17254
25
+ pymax/mixins/websocket.py,sha256=LaL-okzhJCyS3uWV7xsCCKnuff_rooKjAoZ8vkaintY,16817
26
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,,
27
+ pymax/static/enum.py,sha256=c_QaLU0Ephe4SuKFIpwpmrf_HCutc34JJ6o4Ik1E6_g,4582
28
+ maxapi_python-1.1.18.dist-info/METADATA,sha256=XJlvDWCpM0QCT1HgbNgevis67UmNW8dp91ED3s9J2Wg,6245
29
+ maxapi_python-1.1.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ maxapi_python-1.1.18.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
31
+ maxapi_python-1.1.18.dist-info/RECORD,,
pymax/__init__.py CHANGED
@@ -15,6 +15,10 @@ from .exceptions import (
15
15
  SocketSendError,
16
16
  WebSocketNotConnectedError,
17
17
  )
18
+ from .files import (
19
+ File,
20
+ Photo,
21
+ )
18
22
  from .static.enum import (
19
23
  AccessType,
20
24
  AttachType,
@@ -61,47 +65,49 @@ __all__ = [
61
65
  "AccessType",
62
66
  "AttachType",
63
67
  "AuthType",
64
- "ContactAction",
65
- "FormattingType",
66
- "MarkupType",
67
68
  # Типы данных
68
69
  "Channel",
69
70
  "Chat",
70
71
  "ChatType",
71
72
  "Contact",
73
+ "ContactAction",
72
74
  "ControlAttach",
73
75
  "DeviceType",
74
76
  "Dialog",
75
77
  "Element",
76
78
  "ElementType",
79
+ "File",
77
80
  "FileAttach",
78
81
  "FileRequest",
82
+ "FormattingType",
83
+ # Исключения
84
+ "InvalidPhoneError",
85
+ "LoginError",
86
+ "MarkupType",
87
+ # Клиент
88
+ "MaxClient",
79
89
  "Me",
80
90
  "Member",
91
+ "Message",
81
92
  "MessageLink",
93
+ "MessageStatus",
94
+ "MessageType",
82
95
  "Name",
83
96
  "Names",
97
+ "Opcode",
98
+ "Photo",
84
99
  "PhotoAttach",
85
100
  "Presence",
86
101
  "ReactionCounter",
87
102
  "ReactionInfo",
88
- "Session",
89
- "VideoAttach",
90
- "VideoRequest",
91
- # Исключения
92
- "InvalidPhoneError",
93
- "LoginError",
94
- "WebSocketNotConnectedError",
95
103
  "ResponseError",
96
104
  "ResponseStructureError",
105
+ "Session",
106
+ "SocketMaxClient",
97
107
  "SocketNotConnectedError",
98
108
  "SocketSendError",
99
- # Клиент
100
- "MaxClient",
101
- "Message",
102
- "MessageStatus",
103
- "MessageType",
104
- "Opcode",
105
- "SocketMaxClient",
106
109
  "User",
110
+ "VideoAttach",
111
+ "VideoRequest",
112
+ "WebSocketNotConnectedError",
107
113
  ]
pymax/core.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextlib
2
3
  import logging
3
4
  import socket
4
5
  import ssl
@@ -6,12 +7,12 @@ import time
6
7
  import traceback
7
8
  from collections.abc import Awaitable
8
9
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Any, Literal
10
+ from typing import TYPE_CHECKING, Any, Literal, Self
10
11
 
11
12
  from typing_extensions import override
12
13
 
13
14
  from .crud import Database
14
- from .exceptions import InvalidPhoneError
15
+ from .exceptions import InvalidPhoneError, WebSocketNotConnectedError
15
16
  from .formatter import ColoredFormatter
16
17
  from .mixins import ApiMixin, SocketMixin, WebSocketMixin
17
18
  from .payloads import UserAgentPayload
@@ -56,6 +57,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
56
57
  send_fake_telemetry (bool, optional): Флаг отправки фейковой телеметрии. По умолчанию True.
57
58
  proxy (str | Literal[True] | None, optional): Прокси для подключения к WebSocket.
58
59
  (См. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
60
+ reconnect (bool, optional): Флаг автоматического переподключения при потере соединения. По умолчанию True.
61
+
59
62
 
60
63
  Raises:
61
64
  InvalidPhoneError: Если формат номера телефона неверный.
@@ -76,6 +79,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
76
79
  first_name: str = "",
77
80
  last_name: str | None = None,
78
81
  logger: logging.Logger | None = None,
82
+ reconnect: bool = True,
83
+ reconnect_delay: float = 1.0,
79
84
  ) -> None:
80
85
  self.logger = logger or logging.getLogger(f"{__name__}")
81
86
  self.uri: str = uri
@@ -88,6 +93,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
88
93
  self.first_name: str = first_name
89
94
  self.last_name: str | None = last_name
90
95
  self.proxy: str | Literal[True] | None = proxy
96
+ self.reconnect: bool = reconnect
97
+ self.reconnect_delay: float = reconnect_delay
91
98
 
92
99
  self.is_connected: bool = False
93
100
 
@@ -205,14 +212,17 @@ class MaxClient(ApiMixin, WebSocketMixin):
205
212
  except Exception:
206
213
  self.logger.exception("Error closing client")
207
214
 
208
- def _create_safe_task(self, coro: Awaitable[Any], *, name: str | None = None):
215
+ @override
216
+ def _create_safe_task(
217
+ self, coro: Awaitable[Any], *, name: str | None = None
218
+ ) -> asyncio.Task[Any | None]:
209
219
  async def runner():
210
220
  try:
211
221
  return await coro
212
222
  except asyncio.CancelledError:
213
223
  raise
214
224
  except Exception as e:
215
- self.logger.error(
225
+ self.logger.exception(
216
226
  f"Unhandled exception in task {name or coro}: {e}",
217
227
  exc_info=e,
218
228
  )
@@ -226,40 +236,110 @@ class MaxClient(ApiMixin, WebSocketMixin):
226
236
  """
227
237
  Запускает клиент, подключается к WebSocket, авторизует
228
238
  пользователя (если нужно) и запускает фоновый цикл.
239
+ Теперь включает безопасный reconnect-loop, если self.reconnect=True.
229
240
  """
230
- try:
231
- self.logger.info("Client starting")
232
- await self._connect(self.user_agent)
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
-
239
- if self._token and self._database.get_auth_token() is None:
240
- self._database.update_auth_token(self._device_id, self._token)
241
-
242
- if self._token is None:
243
- await self._login()
244
- else:
245
- await self._sync()
246
-
247
- if self._on_start_handler:
248
- self.logger.debug("Calling on_start handler")
249
- result = self._on_start_handler()
250
- if asyncio.iscoroutine(result):
251
- await self._safe_execute(result, context="on_start handler")
252
-
253
- ping_task = asyncio.create_task(self._send_interactive_ping())
254
- ping_task.add_done_callback(self._log_task_exception)
255
- self._background_tasks.add(ping_task)
256
- if self._send_fake_telemetry:
257
- telemetry_task = asyncio.create_task(self._start())
258
- telemetry_task.add_done_callback(self._log_task_exception)
259
- self._background_tasks.add(telemetry_task)
260
- await self._wait_forever()
261
- except Exception:
262
- self.logger.exception("Client start failed")
241
+
242
+ while True:
243
+ try:
244
+ self.logger.info("Client starting")
245
+ await self._connect(self.user_agent)
246
+
247
+ if self.registration:
248
+ if not self.first_name:
249
+ raise ValueError("First name is required for registration")
250
+ await self._register(self.first_name, self.last_name)
251
+
252
+ if self._token and self._database.get_auth_token() is None:
253
+ self._database.update_auth_token(self._device_id, self._token)
254
+
255
+ if self._token is None:
256
+ await self._login()
257
+ else:
258
+ await self._sync()
259
+
260
+ if self._on_start_handler:
261
+ self.logger.debug("Calling on_start handler")
262
+ result = self._on_start_handler()
263
+ if asyncio.iscoroutine(result):
264
+ await self._safe_execute(result, context="on_start handler")
265
+
266
+ ping_task = asyncio.create_task(self._send_interactive_ping())
267
+ ping_task.add_done_callback(self._log_task_exception)
268
+ self._background_tasks.add(ping_task)
269
+
270
+ if self._send_fake_telemetry:
271
+ telemetry_task = asyncio.create_task(self._start())
272
+ telemetry_task.add_done_callback(self._log_task_exception)
273
+ self._background_tasks.add(telemetry_task)
274
+
275
+ await self._wait_forever()
276
+ self.logger.info("WebSocket closed (wait_forever exited)")
277
+
278
+ except Exception:
279
+ self.logger.exception("Client start iteration failed")
280
+
281
+ finally:
282
+ self.logger.debug("Cleaning up background tasks and pending futures")
283
+
284
+ for task in list(self._background_tasks):
285
+ task.cancel()
286
+ try:
287
+ await task
288
+ except asyncio.CancelledError:
289
+ pass
290
+ except Exception:
291
+ self.logger.debug(
292
+ "Background task raised during cancellation", exc_info=True
293
+ )
294
+ self._background_tasks.discard(task)
295
+
296
+ if self._recv_task:
297
+ self._recv_task.cancel()
298
+ with contextlib.suppress(asyncio.CancelledError):
299
+ await self._recv_task
300
+ self._recv_task = None
301
+
302
+ if self._outgoing_task:
303
+ self._outgoing_task.cancel()
304
+ with contextlib.suppress(asyncio.CancelledError):
305
+ await self._outgoing_task
306
+ self._outgoing_task = None
307
+
308
+ for fut in self._pending.values():
309
+ if not fut.done():
310
+ fut.set_exception(WebSocketNotConnectedError)
311
+ self._pending.clear()
312
+
313
+ if self._ws:
314
+ try:
315
+ await self._ws.close()
316
+ except Exception:
317
+ self.logger.debug(
318
+ "Error closing ws during cleanup", exc_info=True
319
+ )
320
+ self._ws = None
321
+
322
+ self.is_connected = False
323
+ self.logger.info("Client start() cleaned up")
324
+
325
+ if not self.reconnect:
326
+ self.logger.info("Reconnect disabled — exiting start()")
327
+ return
328
+
329
+ self.logger.info("Reconnect enabled — restarting client")
330
+ await asyncio.sleep(self.reconnect_delay)
331
+
332
+ async def idle(self):
333
+ await asyncio.Event().wait()
334
+
335
+ async def __aenter__(self) -> Self:
336
+ self._create_safe_task(self.start(), name="start")
337
+ while not self.is_connected:
338
+ await asyncio.sleep(0.05)
339
+ return self
340
+
341
+ async def __aexit__(self, exc_type, exc, tb) -> None:
342
+ await self.close()
263
343
 
264
344
 
265
345
  class SocketMaxClient(SocketMixin, MaxClient):
pymax/interfaces.py CHANGED
@@ -43,15 +43,14 @@ class ClientProtocol(ABC):
43
43
  self.last_name: str | None
44
44
  self._token: str | None
45
45
  self._work_dir: str
46
+ self.reconnect: bool
46
47
  self._database_path: Path
47
48
  self._ws: websockets.ClientConnection | None = None
48
49
  self._seq: int = 0
49
50
  self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
50
51
  self._recv_task: asyncio.Task[Any] | None = None
51
52
  self._incoming: asyncio.Queue[dict[str, Any]] | None = None
52
- self._file_upload_waiters: dict[
53
- int, asyncio.Future[dict[str, Any]]
54
- ] = {}
53
+ self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
55
54
  self.user_agent = UserAgentPayload()
56
55
  self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
57
56
  self._outgoing_task: asyncio.Task[Any] | None = None
@@ -70,9 +69,7 @@ class ClientProtocol(ABC):
70
69
  self._on_message_delete_handlers: list[
71
70
  tuple[Callable[[Message], Any], Filter | None]
72
71
  ] = []
73
- self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = (
74
- None
75
- )
72
+ self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
76
73
  self._background_tasks: set[asyncio.Task[Any]] = set()
77
74
  self._ssl_context: ssl.SSLContext
78
75
  self._socket: socket.socket | None = None
pymax/mixins/websocket.py CHANGED
@@ -25,9 +25,7 @@ class WebSocketMixin(ClientProtocol):
25
25
  @property
26
26
  def ws(self) -> websockets.ClientConnection:
27
27
  if self._ws is None or not self.is_connected:
28
- self.logger.critical(
29
- "WebSocket not connected when access attempted"
30
- )
28
+ self.logger.critical("WebSocket not connected when access attempted")
31
29
  raise WebSocketNotConnectedError
32
30
  return self._ws
33
31
 
@@ -138,9 +136,7 @@ class WebSocketMixin(ClientProtocol):
138
136
 
139
137
  if fut and not fut.done():
140
138
  fut.set_result(data)
141
- self.logger.debug(
142
- "Matched response for pending seq=%s", seq
143
- )
139
+ self.logger.debug("Matched response for pending seq=%s", seq)
144
140
  else:
145
141
  if self._incoming is not None:
146
142
  try:
@@ -153,13 +149,9 @@ class WebSocketMixin(ClientProtocol):
153
149
 
154
150
  try: # TODO: переделать, временное решение
155
151
  if data.get("opcode") == Opcode.NOTIF_ATTACH:
156
- file_id = data.get("payload", {}).get(
157
- "fileId", None
158
- )
152
+ file_id = data.get("payload", {}).get("fileId", None)
159
153
  if isinstance(file_id, int):
160
- fut = self._file_upload_waiters.pop(
161
- file_id, None
162
- )
154
+ fut = self._file_upload_waiters.pop(file_id, None)
163
155
  if fut and not fut.done():
164
156
  fut.set_result(data)
165
157
  self.logger.debug(
@@ -167,9 +159,7 @@ class WebSocketMixin(ClientProtocol):
167
159
  file_id,
168
160
  )
169
161
  except Exception:
170
- self.logger.exception(
171
- "Error handling file upload notification"
172
- )
162
+ self.logger.exception("Error handling file upload notification")
173
163
 
174
164
  if (
175
165
  data.get("opcode") == Opcode.NOTIF_MESSAGE.value
@@ -185,23 +175,17 @@ class WebSocketMixin(ClientProtocol):
185
175
  for (
186
176
  edit_handler,
187
177
  edit_filter,
188
- ) in (
189
- self._on_message_edit_handlers
190
- ):
178
+ ) in self._on_message_edit_handlers:
191
179
  await self._process_message_handler(
192
180
  edit_handler,
193
181
  edit_filter,
194
182
  msg,
195
183
  )
196
- elif (
197
- msg.status == MessageStatus.REMOVED
198
- ):
184
+ elif msg.status == MessageStatus.REMOVED:
199
185
  for (
200
186
  remove_handler,
201
187
  remove_filter,
202
- ) in (
203
- self._on_message_delete_handlers
204
- ):
188
+ ) in self._on_message_delete_handlers:
205
189
  await self._process_message_handler(
206
190
  remove_handler,
207
191
  remove_filter,
@@ -211,19 +195,22 @@ class WebSocketMixin(ClientProtocol):
211
195
  handler, filter, msg
212
196
  )
213
197
  except Exception:
214
- self.logger.exception(
215
- "Error in on_message_handler"
216
- )
198
+ self.logger.exception("Error in on_message_handler")
217
199
 
218
200
  except websockets.exceptions.ConnectionClosed:
219
- self.logger.info(
220
- "WebSocket connection closed; exiting recv loop"
221
- )
201
+ self.logger.info("WebSocket connection closed; exiting recv loop")
202
+ for fut in self._pending.values():
203
+ if not fut.done():
204
+ fut.set_exception(WebSocketNotConnectedError)
205
+ self._pending.clear()
206
+
207
+ self.is_connected = False
208
+ self._ws = None
209
+ self._recv_task = None
210
+
222
211
  break
223
212
  except Exception:
224
- self.logger.exception(
225
- "Error in recv_loop; backing off briefly"
226
- )
213
+ self.logger.exception("Error in recv_loop; backing off briefly")
227
214
  await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
228
215
 
229
216
  def _log_task_exception(self, fut: asyncio.Future[Any]) -> None:
@@ -312,9 +299,7 @@ class WebSocketMixin(ClientProtocol):
312
299
  await asyncio.sleep(5)
313
300
  continue
314
301
 
315
- message = (
316
- await self._outgoing.get()
317
- ) # TODO: persistent msg q mb?
302
+ message = await self._outgoing.get() # TODO: persistent msg q mb?
318
303
  if not message:
319
304
  continue
320
305
 
@@ -388,9 +373,7 @@ class WebSocketMixin(ClientProtocol):
388
373
  ).model_dump(by_alias=True)
389
374
 
390
375
  try:
391
- data = await self._send_and_wait(
392
- opcode=Opcode.LOGIN, payload=payload
393
- )
376
+ data = await self._send_and_wait(opcode=Opcode.LOGIN, payload=payload)
394
377
  raw_payload = data.get("payload", {})
395
378
 
396
379
  if error := raw_payload.get("error"):
pymax/payloads.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Any, Final, Literal
1
+ from typing import Any, Literal
2
2
 
3
3
  from pydantic import AliasChoices, BaseModel, Field
4
4
 
@@ -30,7 +30,7 @@ class CamelModel(BaseModel):
30
30
 
31
31
 
32
32
  class BaseWebSocketMessage(BaseModel):
33
- ver: Final[int] = 11
33
+ ver: Literal[10, 11] = 11
34
34
  cmd: int
35
35
  seq: int
36
36
  opcode: int
@@ -195,14 +195,14 @@ class ChangeGroupProfilePayload(CamelModel):
195
195
 
196
196
 
197
197
  class GetGroupMembersPayload(CamelModel):
198
- type: Final[str] = "MEMBER"
198
+ type: Literal["MEMBER"] = "MEMBER"
199
199
  marker: int
200
200
  chat_id: int
201
201
  count: int
202
202
 
203
203
 
204
204
  class SearchGroupMembersPayload(CamelModel):
205
- type: Final[str] = "MEMBER"
205
+ type: Literal["MEMBER"] = "MEMBER"
206
206
  query: str
207
207
  chat_id: int
208
208
 
pymax/static/enum.py CHANGED
@@ -190,6 +190,7 @@ class AttachType(str, Enum):
190
190
  VIDEO = "VIDEO"
191
191
  FILE = "FILE"
192
192
  STICKER = "STICKER"
193
+ AUDIO = "AUDIO"
193
194
  CONTROL = "CONTROL"
194
195
 
195
196
 
pymax/types.py CHANGED
@@ -163,9 +163,7 @@ class Contact:
163
163
 
164
164
  @override
165
165
  def __str__(self) -> str:
166
- return (
167
- f"Contact {self.id}: {', '.join(str(n) for n in self.names or [])}"
168
- )
166
+ return f"Contact {self.id}: {', '.join(str(n) for n in self.names or [])}"
169
167
 
170
168
 
171
169
  class Member:
@@ -279,9 +277,7 @@ class StickerAttach:
279
277
 
280
278
 
281
279
  class ControlAttach:
282
- def __init__(
283
- self, type: AttachType, event: str, **kwargs: dict[str, Any]
284
- ) -> None:
280
+ def __init__(self, type: AttachType, event: str, **kwargs: dict[str, Any]) -> None:
285
281
  self.type = type
286
282
  self.event = event
287
283
  self.extra = kwargs
@@ -306,6 +302,50 @@ class ControlAttach:
306
302
  return f"ControlAttach: {self.event}"
307
303
 
308
304
 
305
+ class AudioAttach:
306
+ def __init__(
307
+ self,
308
+ duration: int,
309
+ audio_id: int,
310
+ url: str,
311
+ wave: str,
312
+ transcription_status: str, # TODO: сделать энам
313
+ token: str,
314
+ type: AttachType,
315
+ ) -> None:
316
+ self.duration = duration
317
+ self.audio_id = audio_id
318
+ self.url = url
319
+ self.wave = wave
320
+ self.transcription_status = transcription_status
321
+ self.token = token
322
+ self.type = type
323
+
324
+ @classmethod
325
+ def from_dict(cls, data: dict[str, Any]) -> Self:
326
+ return cls(
327
+ duration=data["duration"],
328
+ audio_id=data["audioId"],
329
+ url=data["url"],
330
+ wave=data["wave"],
331
+ transcription_status=data["transcriptionStatus"],
332
+ token=data["token"],
333
+ type=AttachType(data["_type"]),
334
+ )
335
+
336
+ @override
337
+ def __repr__(self) -> str:
338
+ return (
339
+ f"AudioAttach(duration={self.duration!r}, audio_id={self.audio_id!r}, "
340
+ f"url={self.url!r}, wave={self.wave!r}, transcription_status={self.transcription_status!r}, "
341
+ f"token={self.token!r}, type={self.type!r})"
342
+ )
343
+
344
+ @override
345
+ def __str__(self) -> str:
346
+ return f"AudioAttach: {self.audio_id}"
347
+
348
+
309
349
  class PhotoAttach:
310
350
  def __init__(
311
351
  self,
@@ -522,13 +562,13 @@ class Element:
522
562
 
523
563
  @classmethod
524
564
  def from_dict(cls, data: dict[Any, Any]) -> Self:
525
- return cls(
526
- type=data["type"], length=data["length"], from_=data.get("from")
527
- )
565
+ return cls(type=data["type"], length=data["length"], from_=data.get("from"))
528
566
 
529
567
  @override
530
568
  def __repr__(self) -> str:
531
- return f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
569
+ return (
570
+ f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
571
+ )
532
572
 
533
573
  @override
534
574
  def __str__(self) -> str:
@@ -591,9 +631,7 @@ class ReactionInfo:
591
631
  def from_dict(cls, data: dict[str, Any]) -> Self:
592
632
  return cls(
593
633
  total_count=data.get("totalCount", 0),
594
- counters=[
595
- ReactionCounter.from_dict(c) for c in data.get("counters", [])
596
- ],
634
+ counters=[ReactionCounter.from_dict(c) for c in data.get("counters", [])],
597
635
  your_reaction=data.get("yourReaction"),
598
636
  )
599
637
 
@@ -619,6 +657,7 @@ class Message:
619
657
  | FileAttach
620
658
  | ControlAttach
621
659
  | StickerAttach
660
+ | AudioAttach
622
661
  ]
623
662
  | None
624
663
  ),
@@ -640,11 +679,7 @@ class Message:
640
679
  def from_dict(cls, data: dict[Any, Any]) -> Self:
641
680
  message = data["message"] if data.get("message") else data
642
681
  attaches: list[
643
- PhotoAttach
644
- | VideoAttach
645
- | FileAttach
646
- | ControlAttach
647
- | StickerAttach
682
+ PhotoAttach | VideoAttach | FileAttach | ControlAttach | StickerAttach
648
683
  ] = []
649
684
  for a in message.get("attaches", []):
650
685
  if a["_type"] == AttachType.PHOTO:
@@ -657,6 +692,8 @@ class Message:
657
692
  attaches.append(ControlAttach.from_dict(a))
658
693
  elif a["_type"] == AttachType.STICKER:
659
694
  attaches.append(StickerAttach.from_dict(a))
695
+ elif a["_type"] == AttachType.AUDIO:
696
+ attaches.append(AudioAttach.from_dict(a))
660
697
  link_value = message.get("link")
661
698
  if isinstance(link_value, dict):
662
699
  link = MessageLink.from_dict(link_value)
@@ -670,9 +707,7 @@ class Message:
670
707
  return cls(
671
708
  chat_id=data.get("chatId"),
672
709
  sender=message.get("sender"),
673
- elements=[
674
- Element.from_dict(e) for e in message.get("elements", [])
675
- ],
710
+ elements=[Element.from_dict(e) for e in message.get("elements", [])],
676
711
  options=message.get("options"),
677
712
  id=message["id"],
678
713
  time=message["time"],
@@ -834,13 +869,9 @@ class Chat:
834
869
  int(k): v for k, v in raw_admins.items()
835
870
  }
836
871
  raw_participants = data.get("participants", {}) or {}
837
- participants: dict[int, int] = {
838
- int(k): v for k, v in raw_participants.items()
839
- }
872
+ participants: dict[int, int] = {int(k): v for k, v in raw_participants.items()}
840
873
  last_msg = (
841
- Message.from_dict(data["lastMessage"])
842
- if data.get("lastMessage")
843
- else None
874
+ Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
844
875
  )
845
876
  return cls(
846
877
  participants_count=data.get("participantsCount", 0),
@@ -852,9 +883,7 @@ class Chat:
852
883
  description=data.get("description"),
853
884
  chat_type=ChatType(data.get("type", ChatType.CHAT.value)),
854
885
  title=data.get("title"),
855
- last_fire_delayed_error_time=data.get(
856
- "lastFireDelayedErrorTime", 0
857
- ),
886
+ last_fire_delayed_error_time=data.get("lastFireDelayedErrorTime", 0),
858
887
  last_delayed_update_time=data.get("lastDelayedUpdateTime", 0),
859
888
  options=data.get("options", {}),
860
889
  modified=data.get("modified", 0),
@@ -876,9 +905,7 @@ class Chat:
876
905
 
877
906
  @override
878
907
  def __repr__(self) -> str:
879
- return (
880
- f"Chat(id={self.id!r}, title={self.title!r}, type={self.type!r})"
881
- )
908
+ return f"Chat(id={self.id!r}, title={self.title!r}, type={self.type!r})"
882
909
 
883
910
  @override
884
911
  def __str__(self) -> str: