maxapi-python 1.1.18__py3-none-any.whl → 1.1.20__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.18
3
+ Version: 1.1.20
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
@@ -0,0 +1,31 @@
1
+ pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
2
+ pymax/core.py,sha256=LqX56a5BagUYl1vpB55Y1pLZQdMoC86t6mIQVlkVByo,17322
3
+ pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
4
+ pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
5
+ pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
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=Re8o5N7FSQ-5OgVlK4-WBltX27GheEbfFjoIYl9_u6I,3723
10
+ pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
11
+ pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
12
+ pymax/payloads.py,sha256=cEXY_cVL6SPyhoFTTZnn7dyUx9MMdtNT5SuQSQtL4rg,6983
13
+ pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
14
+ pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
15
+ pymax/mixins/auth.py,sha256=Emv-0WVB_orwv9L_V5gAHfp-VYVaVcbW6AlclW_K6W4,6731
16
+ pymax/mixins/channel.py,sha256=7c8GANyxZuNbIHNBVcPAmMa1qqA1IRf9cGPBS1oK_q4,5159
17
+ pymax/mixins/group.py,sha256=XWXNWluCvq4KkZWqv4sxLpzkXfH33U1yEP20_ZFtSM4,10624
18
+ pymax/mixins/handler.py,sha256=ZuYX8wSgNXJoSMArcwyHvY_bL9A7X0AXnAOz22ATA3k,5897
19
+ pymax/mixins/message.py,sha256=wYvkMPE9ORCSFd_9J-6ltf__4ELG_zaZ_Uey4rmCzHg,25460
20
+ pymax/mixins/self.py,sha256=3BdHfUyqw3dn3ctJX9_hilP1jOaTaunstZ7nH8Y_xcU,5436
21
+ pymax/mixins/socket.py,sha256=GEOscQKKC48bOAXXiDLoz8GusCLtjdxQnB5AK5xJbms,26997
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=toiXt9qxx6yTgnWJdEOeNfp414MD4zmbHp1qVgVkjnY,19850
26
+ pymax/static/constant.py,sha256=Q1NrmaRj17Gdhk3FmUp3HIwrad1TDorq3wFdQlOCzN8,1027
27
+ pymax/static/enum.py,sha256=ddw5SEVfRb2J9TXOa5IGhssNd-7RyKfwZBKx_UionEM,4562
28
+ maxapi_python-1.1.20.dist-info/METADATA,sha256=9yhRv1m8PbJJhnUdloacxlK0jAE0veq6zfXDP-Ok5nk,6245
29
+ maxapi_python-1.1.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
30
+ maxapi_python-1.1.20.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
31
+ maxapi_python-1.1.20.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
pymax/core.py CHANGED
@@ -7,12 +7,16 @@ import time
7
7
  import traceback
8
8
  from collections.abc import Awaitable
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, Literal, Self
10
+ from typing import TYPE_CHECKING, Any, Literal
11
11
 
12
- from typing_extensions import override
12
+ from typing_extensions import Self, override
13
13
 
14
14
  from .crud import Database
15
- from .exceptions import InvalidPhoneError, WebSocketNotConnectedError
15
+ from .exceptions import (
16
+ InvalidPhoneError,
17
+ SocketNotConnectedError,
18
+ WebSocketNotConnectedError,
19
+ )
16
20
  from .formatter import ColoredFormatter
17
21
  from .mixins import ApiMixin, SocketMixin, WebSocketMixin
18
22
  from .payloads import UserAgentPayload
@@ -29,7 +33,7 @@ if TYPE_CHECKING:
29
33
  import websockets
30
34
 
31
35
  from .filters import Filter
32
- from .types import Channel, Chat, Dialog, Me, Message, User
36
+ from .types import Channel, Chat, Dialog, Me, Message, ReactionInfo, User
33
37
 
34
38
 
35
39
  logger = logging.getLogger(__name__)
@@ -143,6 +147,10 @@ class MaxClient(ApiMixin, WebSocketMixin):
143
147
  tuple[Callable[[Message], Any], Filter | None]
144
148
  ] = []
145
149
  self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
150
+ self._on_reaction_change_handlers: list[
151
+ tuple[Callable[[str, int, ReactionInfo], Any]]
152
+ ] = []
153
+ self._on_chat_update_handlers: list[tuple[Callable[[Chat], Any]]] = []
146
154
 
147
155
  self._ssl_context = ssl.create_default_context()
148
156
  self._ssl_context.set_ciphers("DEFAULT")
@@ -172,13 +180,13 @@ class MaxClient(ApiMixin, WebSocketMixin):
172
180
  handler.setFormatter(formatter)
173
181
  self.logger.addHandler(handler)
174
182
 
175
- async def _wait_forever(self):
183
+ async def _wait_forever(self) -> None:
176
184
  try:
177
185
  await self.ws.wait_closed()
178
186
  except asyncio.CancelledError:
179
187
  self.logger.debug("wait_closed cancelled")
180
188
 
181
- async def _safe_execute(self, coro, *, context: str = "unknown"):
189
+ async def _safe_execute(self, coro, *, context: str = "unknown") -> Any:
182
190
  """
183
191
  Безопасно выполняет пользовательскую корутину.
184
192
  Логирует traceback, но не роняет event loop.
@@ -232,6 +240,97 @@ class MaxClient(ApiMixin, WebSocketMixin):
232
240
  self._background_tasks.add(task)
233
241
  return task
234
242
 
243
+ async def _post_login_tasks(self, sync: bool = True) -> None:
244
+ if sync:
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
+
257
+ if self._send_fake_telemetry:
258
+ telemetry_task = asyncio.create_task(self._start())
259
+ telemetry_task.add_done_callback(self._log_task_exception)
260
+ self._background_tasks.add(telemetry_task)
261
+
262
+ async def _cleanup_client(self) -> None:
263
+ for task in list(self._background_tasks):
264
+ task.cancel()
265
+ try:
266
+ await task
267
+ except asyncio.CancelledError:
268
+ pass
269
+ except Exception:
270
+ self.logger.debug(
271
+ "Background task raised during cancellation", exc_info=True
272
+ )
273
+ self._background_tasks.discard(task)
274
+
275
+ if self._recv_task:
276
+ self._recv_task.cancel()
277
+ with contextlib.suppress(asyncio.CancelledError):
278
+ await self._recv_task
279
+ self._recv_task = None
280
+
281
+ if self._outgoing_task:
282
+ self._outgoing_task.cancel()
283
+ with contextlib.suppress(asyncio.CancelledError):
284
+ await self._outgoing_task
285
+ self._outgoing_task = None
286
+
287
+ for fut in self._pending.values():
288
+ if not fut.done():
289
+ fut.set_exception(WebSocketNotConnectedError)
290
+ self._pending.clear()
291
+
292
+ if self._ws:
293
+ try:
294
+ await self._ws.close()
295
+ except Exception:
296
+ self.logger.debug("Error closing ws during cleanup", exc_info=True)
297
+ self._ws = None
298
+
299
+ self.is_connected = False
300
+ self.logger.info("Client start() cleaned up")
301
+
302
+ async def login_with_code(
303
+ self, temp_token: str, code: str, start: bool = False
304
+ ) -> None:
305
+ """
306
+ Завершает кастомный login flow: отправляет код, сохраняет токен и запускает пост-логин задачи.
307
+
308
+ Args:
309
+ temp_token (str): Временный токен, полученный из request_code()
310
+ code (str): Код, введённый пользователем
311
+
312
+ Returns:
313
+ str: Токен для входа
314
+ """
315
+ resp = await self._send_code(code, temp_token)
316
+ token = resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token")
317
+ self._token = token
318
+ self._database.update_auth_token(self._device_id, token)
319
+ if start:
320
+ while True:
321
+ try:
322
+ await self._post_login_tasks()
323
+ await self._wait_forever()
324
+ except Exception:
325
+ self.logger.exception("Error during post-login tasks")
326
+ finally:
327
+ await self._cleanup_client()
328
+
329
+ self.logger.info("Reconnecting after post-login tasks failure")
330
+ await asyncio.sleep(self.reconnect_delay)
331
+ else:
332
+ self.logger.info("Login successful, token saved to database, exiting...")
333
+
235
334
  async def start(self) -> None:
236
335
  """
237
336
  Запускает клиент, подключается к WebSocket, авторизует
@@ -242,7 +341,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
242
341
  while True:
243
342
  try:
244
343
  self.logger.info("Client starting")
245
- await self._connect(self.user_agent)
344
+ await self.connect(self.user_agent)
246
345
 
247
346
  if self.registration:
248
347
  if not self.first_name:
@@ -257,70 +356,18 @@ class MaxClient(ApiMixin, WebSocketMixin):
257
356
  else:
258
357
  await self._sync()
259
358
 
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)
359
+ await self._post_login_tasks(sync=False)
274
360
 
275
361
  await self._wait_forever()
276
362
  self.logger.info("WebSocket closed (wait_forever exited)")
277
-
278
- except Exception:
363
+ except Exception as e:
279
364
  self.logger.exception("Client start iteration failed")
365
+ raise e
280
366
 
281
367
  finally:
282
368
  self.logger.debug("Cleaning up background tasks and pending futures")
283
369
 
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")
370
+ await self._cleanup_client()
324
371
 
325
372
  if not self.reconnect:
326
373
  self.logger.info("Reconnect disabled — exiting start()")
@@ -352,3 +399,51 @@ class SocketMaxClient(SocketMixin, MaxClient):
352
399
  self.logger.debug("Socket recv_task cancelled")
353
400
  except Exception as e:
354
401
  self.logger.exception("Socket recv_task failed: %s", e)
402
+
403
+ @override
404
+ async def _cleanup_client(self):
405
+ """
406
+ Socket-specific cleanup: cancel background tasks, set pending futures
407
+ exceptions to SocketNotConnectedError, and close socket.
408
+ """
409
+ from .exceptions import SocketNotConnectedError
410
+
411
+ for task in list(self._background_tasks):
412
+ task.cancel()
413
+ try:
414
+ await task
415
+ except asyncio.CancelledError:
416
+ pass
417
+ except Exception:
418
+ self.logger.debug(
419
+ "Background task raised during cancellation (socket)",
420
+ exc_info=True,
421
+ )
422
+ self._background_tasks.discard(task)
423
+
424
+ if self._recv_task:
425
+ self._recv_task.cancel()
426
+ with contextlib.suppress(asyncio.CancelledError):
427
+ await self._recv_task
428
+ self._recv_task = None
429
+
430
+ if self._outgoing_task:
431
+ self._outgoing_task.cancel()
432
+ with contextlib.suppress(asyncio.CancelledError):
433
+ await self._outgoing_task
434
+ self._outgoing_task = None
435
+
436
+ for fut in self._pending.values():
437
+ if not fut.done():
438
+ fut.set_exception(SocketNotConnectedError())
439
+ self._pending.clear()
440
+
441
+ if self._socket:
442
+ try:
443
+ self._socket.close()
444
+ except Exception:
445
+ self.logger.debug("Error closing socket during cleanup", exc_info=True)
446
+ self._socket = None
447
+
448
+ self.is_connected = False
449
+ self.logger.info("Client start() cleaned up (socket)")
pymax/files.py CHANGED
@@ -9,9 +9,7 @@ from typing_extensions import override
9
9
 
10
10
 
11
11
  class BaseFile(ABC):
12
- def __init__(
13
- self, url: str | None = None, path: str | None = None
14
- ) -> None:
12
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
15
13
  self.url = url
16
14
  self.path = path
17
15
 
@@ -47,9 +45,7 @@ class Photo(BaseFile):
47
45
  ".bmp",
48
46
  } # FIXME: костыль ✅
49
47
 
50
- def __init__(
51
- self, url: str | None = None, path: str | None = None
52
- ) -> None:
48
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
53
49
  super().__init__(url, path)
54
50
 
55
51
  def validate_photo(self) -> tuple[str, str] | None:
@@ -71,9 +67,7 @@ class Photo(BaseFile):
71
67
  mime_type = mimetypes.guess_type(self.url)[0]
72
68
 
73
69
  if not mime_type or not mime_type.startswith("image/"):
74
- raise ValueError(
75
- f"URL does not appear to be an image: {self.url}"
76
- )
70
+ raise ValueError(f"URL does not appear to be an image: {self.url}")
77
71
 
78
72
  return (extension[1:], mime_type)
79
73
  return None
@@ -84,15 +78,24 @@ class Photo(BaseFile):
84
78
 
85
79
 
86
80
  class Video(BaseFile):
81
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
82
+ self.file_name: str = ""
83
+ if path:
84
+ self.file_name = Path(path).name
85
+ elif url:
86
+ self.file_name = Path(url).name
87
+
88
+ if not self.file_name:
89
+ raise ValueError("Either url or path must be provided.")
90
+ super().__init__(url, path)
91
+
87
92
  @override
88
93
  async def read(self) -> bytes:
89
94
  return await super().read()
90
95
 
91
96
 
92
97
  class File(BaseFile):
93
- def __init__(
94
- self, url: str | None = None, path: str | None = None
95
- ) -> None:
98
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
96
99
  self.file_name: str = ""
97
100
  if path:
98
101
  self.file_name = Path(path).name
pymax/interfaces.py CHANGED
@@ -4,21 +4,24 @@ import ssl
4
4
  from abc import ABC, abstractmethod
5
5
  from collections.abc import Awaitable, Callable
6
6
  from logging import Logger
7
- from pathlib import Path
8
7
  from typing import TYPE_CHECKING, Any, Literal
9
8
 
10
- import websockets
11
-
12
- from .filters import Filter
13
9
  from .payloads import UserAgentPayload
14
10
  from .static.constant import DEFAULT_TIMEOUT
15
11
  from .static.enum import Opcode
16
12
  from .types import Channel, Chat, Dialog, Me, Message, User
17
13
 
18
14
  if TYPE_CHECKING:
15
+ from pathlib import Path
19
16
  from uuid import UUID
20
17
 
18
+ import websockets
19
+
20
+ from pymax import AttachType
21
+ from pymax.types import ReactionInfo
22
+
21
23
  from .crud import Database
24
+ from .filters import Filter
22
25
 
23
26
 
24
27
  class ClientProtocol(ABC):
@@ -50,7 +53,10 @@ class ClientProtocol(ABC):
50
53
  self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
51
54
  self._recv_task: asyncio.Task[Any] | None = None
52
55
  self._incoming: asyncio.Queue[dict[str, Any]] | None = None
53
- self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
56
+ self._file_upload_waiters: dict[
57
+ int,
58
+ asyncio.Future[dict[str, Any]],
59
+ ] = {}
54
60
  self.user_agent = UserAgentPayload()
55
61
  self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
56
62
  self._outgoing_task: asyncio.Task[Any] | None = None
@@ -69,6 +75,10 @@ class ClientProtocol(ABC):
69
75
  self._on_message_delete_handlers: list[
70
76
  tuple[Callable[[Message], Any], Filter | None]
71
77
  ] = []
78
+ self._on_reaction_change_handlers: list[
79
+ tuple[Callable[[str, int, ReactionInfo], Any]]
80
+ ] = []
81
+ self._on_chat_update_handlers: list[tuple[Callable[[Chat], Any]]] = []
72
82
  self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
73
83
  self._background_tasks: set[asyncio.Task[Any]] = set()
74
84
  self._ssl_context: ssl.SSLContext
pymax/mixins/auth.py CHANGED
@@ -15,71 +15,73 @@ class AuthMixin(ClientProtocol):
15
15
  def _check_phone(self) -> bool:
16
16
  return bool(re.match(PHONE_REGEX, self.phone))
17
17
 
18
- async def _request_code(
19
- self, phone: str, language: str = "ru"
20
- ) -> dict[str, int | str]:
21
- try:
22
- self.logger.info("Requesting auth code")
18
+ async def request_code(self, phone: str, language: str = "ru") -> str:
19
+ """
20
+ Запрашивает код аутентификации для указанного номера телефона и возвращает временный токен.
23
21
 
24
- payload = RequestCodePayload(
25
- phone=phone, type=AuthType.START_AUTH, language=language
26
- ).model_dump(by_alias=True)
22
+ Note:
23
+ Использовать только в кастомном login flow.
27
24
 
28
- data = await self._send_and_wait(
29
- opcode=Opcode.AUTH_REQUEST, payload=payload
30
- )
31
- if data.get("payload", {}).get("error"):
32
- MixinsUtils.handle_error(data)
25
+ Args:
26
+ phone (str): Номер телефона в международном формате.
27
+ language (str, optional): Язык для сообщения с кодом. По умолчанию "ru".
33
28
 
34
- self.logger.debug(
35
- "Code request response opcode=%s seq=%s",
36
- data.get("opcode"),
37
- data.get("seq"),
38
- )
39
- payload_data = data.get("payload")
40
- if isinstance(payload_data, dict):
41
- return payload_data
42
- else:
43
- self.logger.error("Invalid payload data received")
44
- raise ValueError("Invalid payload data received")
45
- except Exception:
46
- self.logger.error("Request code failed", exc_info=True)
47
- raise RuntimeError("Request code failed")
29
+ Returns:
30
+ str: Временный токен для дальнейшей аутентификации.
31
+ """
32
+ self.logger.info("Requesting auth code")
33
+
34
+ payload = RequestCodePayload(
35
+ phone=phone, type=AuthType.START_AUTH, language=language
36
+ ).model_dump(by_alias=True)
37
+
38
+ data = await self._send_and_wait(opcode=Opcode.AUTH_REQUEST, payload=payload)
39
+
40
+ if data.get("payload", {}).get("error"):
41
+ MixinsUtils.handle_error(data)
42
+
43
+ self.logger.debug(
44
+ "Code request response opcode=%s seq=%s",
45
+ data.get("opcode"),
46
+ data.get("seq"),
47
+ )
48
+ payload_data = data.get("payload")
49
+ if isinstance(payload_data, dict):
50
+ return payload_data["token"]
51
+ else:
52
+ self.logger.error("Invalid payload data received")
53
+ raise ValueError("Invalid payload data received")
48
54
 
49
55
  async def _send_code(self, code: str, token: str) -> dict[str, Any]:
50
- try:
51
- self.logger.info("Sending verification code")
56
+ self.logger.info("Sending verification code")
52
57
 
53
- payload = SendCodePayload(
54
- token=token,
55
- verify_code=code,
56
- auth_token_type=AuthType.CHECK_CODE,
57
- ).model_dump(by_alias=True)
58
+ payload = SendCodePayload(
59
+ token=token,
60
+ verify_code=code,
61
+ auth_token_type=AuthType.CHECK_CODE,
62
+ ).model_dump(by_alias=True)
58
63
 
59
- data = await self._send_and_wait(opcode=Opcode.AUTH, payload=payload)
60
- if data.get("payload", {}).get("error"):
61
- MixinsUtils.handle_error(data)
64
+ data = await self._send_and_wait(opcode=Opcode.AUTH, payload=payload)
62
65
 
63
- self.logger.debug(
64
- "Send code response opcode=%s seq=%s",
65
- data.get("opcode"),
66
- data.get("seq"),
67
- )
68
- payload_data = data.get("payload")
69
- if isinstance(payload_data, dict):
70
- return payload_data
71
- else:
72
- self.logger.error("Invalid payload data received")
73
- raise ValueError("Invalid payload data received")
74
- except Exception:
75
- self.logger.error("Send code failed", exc_info=True)
76
- raise RuntimeError("Send code failed")
66
+ if data.get("payload", {}).get("error"):
67
+ MixinsUtils.handle_error(data)
68
+
69
+ self.logger.debug(
70
+ "Send code response opcode=%s seq=%s",
71
+ data.get("opcode"),
72
+ data.get("seq"),
73
+ )
74
+ payload_data = data.get("payload")
75
+ if isinstance(payload_data, dict):
76
+ return payload_data
77
+ else:
78
+ self.logger.error("Invalid payload data received")
79
+ raise ValueError("Invalid payload data received")
77
80
 
78
81
  async def _login(self) -> None:
79
82
  self.logger.info("Starting login flow")
80
83
 
81
- request_code_payload = await self._request_code(self.phone)
82
- temp_token = request_code_payload.get("token")
84
+ temp_token = await self.request_code(self.phone)
83
85
  if not temp_token or not isinstance(temp_token, str):
84
86
  self.logger.critical("Failed to request code: token missing")
85
87
  raise ValueError("Failed to request code")
@@ -136,8 +138,8 @@ class AuthMixin(ClientProtocol):
136
138
  async def _register(self, first_name: str, last_name: str | None = None) -> None:
137
139
  self.logger.info("Starting registration flow")
138
140
 
139
- request_code_payload = await self._request_code(self.phone)
140
- temp_token = request_code_payload.get("token")
141
+ request_code_payload = await self.request_code(self.phone)
142
+ temp_token = request_code_payload
141
143
 
142
144
  if not temp_token or not isinstance(temp_token, str):
143
145
  self.logger.critical("Failed to request code: token missing")
pymax/mixins/channel.py CHANGED
@@ -67,11 +67,11 @@ class ChannelMixin(ClientProtocol):
67
67
  ) -> tuple[list[Member], int | None]:
68
68
  data = await self._send_and_wait(
69
69
  opcode=Opcode.CHAT_MEMBERS,
70
- payload=payload.model_dump(by_alias=True),
70
+ payload=payload.model_dump(by_alias=True, exclude_none=True),
71
71
  )
72
72
  response_payload = data.get("payload", {})
73
- if error := response_payload.get("error"):
74
- raise ResponseError(error)
73
+ if data.get("payload", {}).get("error"):
74
+ MixinsUtils.handle_error(data)
75
75
  marker = response_payload.get("marker")
76
76
  if isinstance(marker, str):
77
77
  marker = int(marker)
@@ -96,7 +96,7 @@ class ChannelMixin(ClientProtocol):
96
96
  async def load_members(
97
97
  self,
98
98
  chat_id: int,
99
- marker: int = DEFAULT_MARKER_VALUE,
99
+ marker: int | None = DEFAULT_MARKER_VALUE,
100
100
  count: int = DEFAULT_CHAT_MEMBERS_LIMIT,
101
101
  ) -> tuple[list[Member], int | None]:
102
102
  """
@@ -106,11 +106,11 @@ class ChannelMixin(ClientProtocol):
106
106
  chat_id (int): Идентификатор канала
107
107
  marker (int, optional): Маркер для пагинации. По умолчанию DEFAULT_MARKER_VALUE
108
108
  count (int, optional): Количество членов для загрузки. По умолчанию DEFAULT_CHAT_MEMBERS_LIMIT.
109
- Данное значение лучше не менять, так как веб-клиент загружает именно столько.
110
109
 
111
110
  Returns:
112
- list[Member]: Список участников канала
111
+ tuple[list[Member], int | None]: Список участников канала и маркер для следующей страницы
113
112
  """
113
+
114
114
  payload = GetGroupMembersPayload(chat_id=chat_id, marker=marker, count=count)
115
115
  return await self._query_members(payload)
116
116