maxapi-python 2.0.1__py3-none-any.whl → 2.1.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 (61) hide show
  1. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.0.dist-info}/METADATA +4 -1
  2. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.0.dist-info}/RECORD +61 -54
  3. pymax/__init__.py +1 -1
  4. pymax/api/auth/enums.py +13 -2
  5. pymax/api/auth/payloads.py +10 -6
  6. pymax/api/auth/service.py +75 -12
  7. pymax/api/bots/__init__.py +1 -0
  8. pymax/api/bots/payloads.py +7 -0
  9. pymax/api/bots/service.py +35 -0
  10. pymax/api/chats/enums.py +1 -0
  11. pymax/api/chats/payloads.py +14 -0
  12. pymax/api/chats/service.py +112 -12
  13. pymax/api/facade.py +2 -0
  14. pymax/api/messages/payloads.py +5 -1
  15. pymax/api/messages/service.py +36 -11
  16. pymax/api/self/service.py +18 -6
  17. pymax/api/session/payloads.py +9 -2
  18. pymax/api/uploads/models.py +1 -4
  19. pymax/api/uploads/payloads.py +9 -3
  20. pymax/api/uploads/service.py +103 -35
  21. pymax/api/users/service.py +15 -5
  22. pymax/app.py +15 -5
  23. pymax/auth/qr.py +11 -6
  24. pymax/auth/sms.py +13 -4
  25. pymax/base.py +1 -0
  26. pymax/client.py +4 -1
  27. pymax/client_web.py +4 -2
  28. pymax/config.py +11 -3
  29. pymax/connection/connection.py +15 -5
  30. pymax/connection/readers/tcp.py +4 -2
  31. pymax/dispatch/dispatcher.py +28 -10
  32. pymax/dispatch/mapping.py +9 -2
  33. pymax/dispatch/router.py +2 -0
  34. pymax/files/base.py +6 -1
  35. pymax/formatting/markdown.py +4 -1
  36. pymax/infra/auth.py +42 -0
  37. pymax/infra/base.py +2 -0
  38. pymax/infra/bots.py +33 -0
  39. pymax/infra/chat.py +102 -1
  40. pymax/protocol/tcp/compression.py +3 -1
  41. pymax/protocol/tcp/framing.py +6 -11
  42. pymax/protocol/tcp/payload.py +3 -2
  43. pymax/protocol/tcp/protocol.py +13 -3
  44. pymax/protocol/ws/protocol.py +9 -3
  45. pymax/session/protocol.py +6 -2
  46. pymax/session/store.py +24 -8
  47. pymax/telemetry/navigation.py +3 -1
  48. pymax/telemetry/service.py +9 -3
  49. pymax/transport/tcp.py +10 -4
  50. pymax/transport/websocket.py +0 -2
  51. pymax/types/domain/__init__.py +3 -0
  52. pymax/types/domain/bots.py +14 -0
  53. pymax/types/domain/error.py +3 -3
  54. pymax/types/domain/folder.py +1 -1
  55. pymax/types/domain/login.py +18 -6
  56. pymax/types/domain/member.py +16 -0
  57. pymax/types/domain/presence.py +15 -0
  58. pymax/types/domain/sync.py +21 -5
  59. pymax/types/domain/user.py +12 -0
  60. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.0.dist-info}/WHEEL +0 -0
  61. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,6 @@ from pymax.logging import get_logger
9
9
  from pymax.protocol import InboundFrame
10
10
  from pymax.types import Chat, MessageDeleteEvent
11
11
  from pymax.types.domain import Message
12
- from pymax.types.events import FileUploadSignal, VideoUploadSignal
13
12
 
14
13
  from .enums import EventType
15
14
  from .mapping import EventMapper, EventResolver
@@ -26,7 +25,6 @@ if TYPE_CHECKING:
26
25
  from collections.abc import Generator
27
26
 
28
27
  from pymax.app import App
29
- from pymax.client import Client
30
28
 
31
29
 
32
30
  logger = get_logger(__name__)
@@ -35,7 +33,9 @@ ClientT = TypeVar("ClientT")
35
33
 
36
34
 
37
35
  class Dispatcher(Generic[ClientT]):
38
- def __init__(self, app: App, root_router: Router[ClientT] | None = None) -> None:
36
+ def __init__(
37
+ self, app: App, root_router: Router[ClientT] | None = None
38
+ ) -> None:
39
39
  self.root_router: Router[ClientT] = root_router or Router()
40
40
  self.internal_router: Router[ClientT] = Router()
41
41
  self.resolver = EventResolver()
@@ -59,7 +59,11 @@ class Dispatcher(Generic[ClientT]):
59
59
  event: EventType,
60
60
  *filters: FilterCallback[Any],
61
61
  ) -> HandlerDecorator[Any, ClientT]:
62
- logger.debug("registering internal handler event=%s filters=%s", event, len(filters))
62
+ logger.debug(
63
+ "registering internal handler event=%s filters=%s",
64
+ event,
65
+ len(filters),
66
+ )
63
67
  return self.internal_router.on(event, *filters)
64
68
 
65
69
  def on(
@@ -67,7 +71,9 @@ class Dispatcher(Generic[ClientT]):
67
71
  event: EventType,
68
72
  *filters: FilterCallback[Any],
69
73
  ) -> HandlerDecorator[Any, ClientT]:
70
- logger.debug("registering handler event=%s filters=%s", event, len(filters))
74
+ logger.debug(
75
+ "registering handler event=%s filters=%s", event, len(filters)
76
+ )
71
77
  return self.root_router.on(event, *filters)
72
78
 
73
79
  def on_message(
@@ -81,7 +87,9 @@ class Dispatcher(Generic[ClientT]):
81
87
  self,
82
88
  *filters: FilterCallback[Message],
83
89
  ) -> HandlerDecorator[Message, ClientT]:
84
- logger.debug("registering message edit handler filters=%s", len(filters))
90
+ logger.debug(
91
+ "registering message edit handler filters=%s", len(filters)
92
+ )
85
93
  return self.root_router.on_message_edit(*filters)
86
94
 
87
95
  def on_message_delete(
@@ -108,7 +116,9 @@ class Dispatcher(Generic[ClientT]):
108
116
  def iter_routers(self) -> Generator[Router[ClientT], Any, None]:
109
117
  yield from self._iter_router(self.root_router)
110
118
 
111
- def _iter_router(self, router: Router[ClientT]) -> Generator[Router[ClientT], Any, None]:
119
+ def _iter_router(
120
+ self, router: Router[ClientT]
121
+ ) -> Generator[Router[ClientT], Any, None]:
112
122
  yield router
113
123
 
114
124
  for child in router.children:
@@ -151,10 +161,16 @@ class Dispatcher(Generic[ClientT]):
151
161
  if event_type is not None:
152
162
  logger.debug("dispatching event type=%s", event_type)
153
163
  event = self.mapper.map(event_type, frame)
154
- await self._dispatch_to_router(self.internal_router, event_type, event)
164
+ await self._dispatch_to_router(
165
+ self.internal_router, event_type, event
166
+ )
155
167
  await self._dispatch_to_router(self.root_router, event_type, event)
156
168
  else:
157
- logger.debug("dispatching raw event only opcode=%s cmd=%s", frame.opcode, frame.cmd)
169
+ logger.debug(
170
+ "dispatching raw event only opcode=%s cmd=%s",
171
+ frame.opcode,
172
+ frame.cmd,
173
+ )
158
174
 
159
175
  await self._dispatch_to_router(self.root_router, EventType.RAW, frame)
160
176
 
@@ -193,7 +209,9 @@ class Dispatcher(Generic[ClientT]):
193
209
  return False
194
210
  return True
195
211
 
196
- async def _call(self, callback: HandlerCallback[Any, ClientT], event: Any) -> Any:
212
+ async def _call(
213
+ self, callback: HandlerCallback[Any, ClientT], event: Any
214
+ ) -> Any:
197
215
  if self.client is None:
198
216
  raise RuntimeError("client is not bound")
199
217
 
pymax/dispatch/mapping.py CHANGED
@@ -10,7 +10,12 @@ from pymax.types.domain import Message
10
10
  from pymax.types.events import FileUploadSignal, VideoUploadSignal
11
11
 
12
12
  from .enums import EventType
13
- from .resolvers import resolve_attach, resolve_chat, resolve_message, resolve_message_delete
13
+ from .resolvers import (
14
+ resolve_attach,
15
+ resolve_chat,
16
+ resolve_message,
17
+ resolve_message_delete,
18
+ )
14
19
 
15
20
  if TYPE_CHECKING:
16
21
  from pymax.app import App
@@ -53,7 +58,9 @@ class EventMapper:
53
58
 
54
59
  if frame.payload:
55
60
  if event_type in (EventType.MESSAGE_NEW, EventType.MESSAGE_EDIT):
56
- return Message.model_validate(frame.payload).bind(self.app.api.messages)
61
+ return Message.model_validate(frame.payload).bind(
62
+ self.app.api.messages
63
+ )
57
64
  elif event_type == EventType.CHAT_UPDATE:
58
65
  return Chat.model_validate(frame.payload["chat"]).bind(
59
66
  self.app.api.messages,
pymax/dispatch/router.py CHANGED
@@ -106,6 +106,7 @@ class Router(Generic[ClientT]):
106
106
  async def raw(frame: InboundFrame, client: Client) -> None:
107
107
  print(frame.payload)
108
108
  """
109
+
109
110
  def decorator(
110
111
  handler: HandlerCallback[_EventT, ClientT],
111
112
  ) -> HandlerCallback[_EventT, ClientT]:
@@ -136,6 +137,7 @@ class Router(Generic[ClientT]):
136
137
  Returns:
137
138
  Декоратор для ``handler(client)``.
138
139
  """
140
+
139
141
  def decorator(handler: StartCallback) -> StartCallback:
140
142
  self.on_start_handler = handler
141
143
  return handler
pymax/files/base.py CHANGED
@@ -8,7 +8,12 @@ import aiohttp
8
8
 
9
9
  class BaseFile(ABC):
10
10
  def __init__(
11
- self, raw: bytes | None = None, *, path: str | None, url: str | None, name: str | None
11
+ self,
12
+ raw: bytes | None = None,
13
+ *,
14
+ path: str | None,
15
+ url: str | None,
16
+ name: str | None,
12
17
  ) -> None:
13
18
  self.path = path
14
19
  self.url = url
@@ -151,7 +151,10 @@ class Formatter:
151
151
  if marker == "```":
152
152
  closing_index = text.find(marker, i + marker_len)
153
153
 
154
- if closing_index == -1 or closing_index == i + marker_len:
154
+ if (
155
+ closing_index == -1
156
+ or closing_index == i + marker_len
157
+ ):
155
158
  clean_text += marker
156
159
  clean_pos += marker_len
157
160
  i += marker_len
pymax/infra/auth.py CHANGED
@@ -53,3 +53,45 @@ class AuthMixin(IClientProtocol):
53
53
  RuntimeError: Если удаление пароля не удалось.
54
54
  """
55
55
  return await self._app.api.auth.remove_2fa(password=password)
56
+
57
+ async def change_password(
58
+ self,
59
+ password_old: str,
60
+ password_new: str,
61
+ ) -> bool:
62
+ """Меняет пароль 2FA для текущей учетной записи.
63
+
64
+ Args:
65
+ password_old: Текущий пароль 2FA.
66
+ password_new: Новый пароль 2FA.
67
+
68
+ Returns:
69
+ ``True``, если пароль успешно изменен.
70
+
71
+ Raises:
72
+ RuntimeError: Если изменение пароля не удалось.
73
+ """
74
+ return await self._app.api.auth.change_password(
75
+ password_old=password_old,
76
+ password_new=password_new,
77
+ )
78
+
79
+ async def authorize_qr_login(self, qr_link: str) -> bool:
80
+ """Авторизует вход по QR-коду.
81
+
82
+ Args:
83
+ qr_link: Ссылка на QR-код для авторизации.
84
+
85
+ Returns:
86
+ ``True``, если вход по QR-коду успешно авторизован.
87
+ """
88
+ return await self._app.api.auth.authorize_qr_login(qr_link=qr_link)
89
+
90
+ async def check_2fa(self) -> bool:
91
+ """Проверяет, включена ли 2FA для текущей учетной записи.
92
+
93
+ Returns:
94
+ ``True``, если на аккаунте установлен пароль 2FA.
95
+ """
96
+
97
+ return await self._app.api.auth.check_2fa()
pymax/infra/base.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from .auth import AuthMixin
2
+ from .bots import BotsMixin
2
3
  from .chat import ChatMixin
3
4
  from .message import MessageMixin
4
5
  from .self import SelfMixin
@@ -10,6 +11,7 @@ class BaseMixin(
10
11
  UserMixin,
11
12
  ChatMixin,
12
13
  MessageMixin,
14
+ BotsMixin,
13
15
  AuthMixin,
14
16
  ):
15
17
  """Собирает публичные API-методы клиента."""
pymax/infra/bots.py ADDED
@@ -0,0 +1,33 @@
1
+ from pymax.types.domain import InitData
2
+
3
+ from .protocol import IClientProtocol
4
+
5
+
6
+ class BotsMixin(IClientProtocol):
7
+ """Методы клиента для взаимодействия с ботами."""
8
+
9
+ async def get_bot_init_data(
10
+ self,
11
+ bot_id: int,
12
+ chat_id: int,
13
+ start_param: str | None = None,
14
+ ) -> InitData:
15
+ """Получает начальные данные для бота в контексте конкретного чата.
16
+
17
+ Args:
18
+ bot_id: Идентификатор бота.
19
+ chat_id: Идентификатор чата, в котором бот будет использоваться.
20
+ start_param: Необязательный параметр, передаваемый при запуске
21
+ бота.
22
+
23
+ Returns:
24
+ Объект с начальными данными для бота.
25
+
26
+ Raises:
27
+ RuntimeError: Если получение данных не удалось.
28
+ """
29
+ return await self._app.api.bots.get_init_data(
30
+ bot_id=bot_id,
31
+ chat_id=chat_id,
32
+ start_param=start_param,
33
+ )
pymax/infra/chat.py CHANGED
@@ -1,4 +1,4 @@
1
- from pymax.types import Chat, Message
1
+ from pymax.types import Chat, Member, Message
2
2
 
3
3
  from .protocol import IClientProtocol
4
4
 
@@ -238,3 +238,104 @@ class ChatMixin(IClientProtocol):
238
238
  Загруженные чаты.
239
239
  """
240
240
  return await self._app.api.chats.fetch_chats(marker=marker)
241
+
242
+ async def get_join_requests(
243
+ self,
244
+ chat_id: int,
245
+ count: int = 100,
246
+ ) -> list[Member]:
247
+ """Возвращает заявки на вступление в группу или канал.
248
+
249
+ Args:
250
+ chat_id: ID группы или канала.
251
+ count: Максимальное количество заявок в ответе.
252
+
253
+ Returns:
254
+ Список пользователей, ожидающих подтверждения заявки.
255
+ """
256
+ return await self._app.api.chats.get_join_requests(
257
+ chat_id=chat_id,
258
+ count=count,
259
+ )
260
+
261
+ async def confirm_join_requests(
262
+ self,
263
+ chat_id: int,
264
+ user_ids: list[int],
265
+ show_history: bool = True,
266
+ ) -> Chat | None:
267
+ """Подтверждает несколько заявок на вступление.
268
+
269
+ Args:
270
+ chat_id: ID группы или канала.
271
+ user_ids: ID пользователей, чьи заявки нужно подтвердить.
272
+ show_history: Показать новым участникам историю сообщений.
273
+
274
+ Returns:
275
+ Обновленный чат или ``None``, если сервер не вернул чат.
276
+ """
277
+ return await self._app.api.chats.confirm_join_requests(
278
+ chat_id=chat_id,
279
+ user_ids=user_ids,
280
+ show_history=show_history,
281
+ )
282
+
283
+ async def confirm_join_request(
284
+ self,
285
+ chat_id: int,
286
+ user_id: int,
287
+ show_history: bool = True,
288
+ ) -> Chat | None:
289
+ """Подтверждает одну заявку на вступление.
290
+
291
+ Args:
292
+ chat_id: ID группы или канала.
293
+ user_id: ID пользователя, чью заявку нужно подтвердить.
294
+ show_history: Показать новому участнику историю сообщений.
295
+
296
+ Returns:
297
+ Обновленный чат или ``None``, если сервер не вернул чат.
298
+ """
299
+ return await self._app.api.chats.confirm_join_request(
300
+ chat_id=chat_id,
301
+ user_id=user_id,
302
+ show_history=show_history,
303
+ )
304
+
305
+ async def decline_join_requests(
306
+ self,
307
+ chat_id: int,
308
+ user_ids: list[int],
309
+ ) -> Chat | None:
310
+ """Отклоняет несколько заявок на вступление.
311
+
312
+ Args:
313
+ chat_id: ID группы или канала.
314
+ user_ids: ID пользователей, чьи заявки нужно отклонить.
315
+
316
+ Returns:
317
+ Обновленный чат или ``None``, если сервер не вернул чат.
318
+ """
319
+ return await self._app.api.chats.decline_join_requests(
320
+ chat_id=chat_id,
321
+ user_ids=user_ids,
322
+ )
323
+
324
+ async def decline_join_request(
325
+ self,
326
+ chat_id: int,
327
+ user_id: int,
328
+ ) -> Chat | None:
329
+ """Отклоняет одну заявку на вступление.
330
+
331
+ Args:
332
+ chat_id: ID группы или канала.
333
+ user_id: ID пользователя, чью заявку нужно отклонить.
334
+
335
+ Returns:
336
+ Обновленный чат или ``None``, если сервер не вернул чат.
337
+ """
338
+ return await self._app.api.chats.decline_join_request(
339
+ chat_id=chat_id,
340
+ user_id=user_id,
341
+ )
@@ -1,5 +1,7 @@
1
1
  class Lz4BlockCompression:
2
- def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
2
+ def decompress(
3
+ self, src: bytes, max_output: int = 5 * 1024 * 1024
4
+ ) -> bytes:
3
5
  dst = bytearray()
4
6
  pos = 0
5
7
 
@@ -4,15 +4,9 @@ from pymax.protocol import PackedPacket, TcpPacketHeader
4
4
 
5
5
 
6
6
  class TcpPacketFramer:
7
- HEADER_STRUCT = struct.Struct(">BHBHI")
7
+ HEADER_STRUCT = struct.Struct(">BBHHI")
8
8
  HEADER_SIZE = HEADER_STRUCT.size
9
9
 
10
- def _pack_cmd(self, cmd: int) -> int:
11
- return (cmd & 0xFF) << 8
12
-
13
- def _unpack_cmd(self, packed_cmd: int) -> int:
14
- return (packed_cmd >> 8) & 0xFF
15
-
16
10
  def pack(
17
11
  self,
18
12
  *,
@@ -26,7 +20,7 @@ class TcpPacketFramer:
26
20
  packed_len = ((flags & 0xFF) << 24) | (len(payload_bytes) & 0x00FFFFFF)
27
21
  header = self.HEADER_STRUCT.pack(
28
22
  ver,
29
- self._pack_cmd(cmd),
23
+ cmd,
30
24
  seq,
31
25
  opcode,
32
26
  packed_len,
@@ -37,7 +31,9 @@ class TcpPacketFramer:
37
31
  if len(data) < self.HEADER_SIZE:
38
32
  return None
39
33
 
40
- ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(data, 0)
34
+ ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(
35
+ data, 0
36
+ )
41
37
  flags = (packed_len >> 24) & 0xFF
42
38
  payload_len = packed_len & 0x00FFFFFF
43
39
 
@@ -48,7 +44,7 @@ class TcpPacketFramer:
48
44
  return PackedPacket(
49
45
  header=TcpPacketHeader(
50
46
  ver=ver,
51
- cmd=self._unpack_cmd(cmd),
47
+ cmd=cmd,
52
48
  seq=seq,
53
49
  opcode=opcode,
54
50
  flags=flags,
@@ -62,7 +58,6 @@ class TcpPacketFramer:
62
58
  return None
63
59
 
64
60
  _, _, _, _, packed_len = self.HEADER_STRUCT.unpack_from(data, 0)
65
- flags = (packed_len >> 24) & 0xFF
66
61
  payload_len = packed_len & 0x00FFFFFF
67
62
 
68
63
  return payload_len
@@ -7,7 +7,6 @@ from pymax.logging import get_logger
7
7
 
8
8
  from .compression import Lz4BlockCompression
9
9
 
10
-
11
10
  logger = get_logger(__name__)
12
11
 
13
12
 
@@ -44,7 +43,9 @@ class MsgpackPayloadCodec:
44
43
  return {}
45
44
 
46
45
  try:
47
- return msgpack.unpackb(payload_bytes, raw=False, strict_map_key=False)
46
+ return msgpack.unpackb(
47
+ payload_bytes, raw=False, strict_map_key=False
48
+ )
48
49
  except msgpack.exceptions.ExtraData as e:
49
50
  if isinstance(e.unpacked, dict):
50
51
  logger.debug(
@@ -3,7 +3,11 @@ from pymax.protocol import InboundFrame, OutboundFrame
3
3
  from pymax.protocol.base import BaseProtocol
4
4
 
5
5
  from .framing import TcpPacketFramer
6
- from .payload import Lz4BlockCompression, MsgpackPayloadCodec, TcpPayloadDecoder
6
+ from .payload import (
7
+ Lz4BlockCompression,
8
+ MsgpackPayloadCodec,
9
+ TcpPayloadDecoder,
10
+ )
7
11
 
8
12
  logger = get_logger(__name__)
9
13
 
@@ -21,7 +25,11 @@ class TcpProtocol(BaseProtocol):
21
25
  )
22
26
 
23
27
  def encode(self, frame: OutboundFrame) -> bytes:
24
- payload_bytes = self.serializer.encode(frame.payload) if frame.payload is not None else b""
28
+ payload_bytes = (
29
+ self.serializer.encode(frame.payload)
30
+ if frame.payload is not None
31
+ else b""
32
+ )
25
33
 
26
34
  flags = 0
27
35
 
@@ -44,7 +52,9 @@ class TcpProtocol(BaseProtocol):
44
52
 
45
53
  packed_packet = self.framer.unpack(raw)
46
54
  if not packed_packet:
47
- return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
55
+ return InboundFrame(
56
+ opcode=0, cmd=0, seq=None, payload=None, raw=None
57
+ )
48
58
 
49
59
  logger.debug(
50
60
  "tcp frame decoded header ver=%s cmd=%s seq=%s opcode=%s flags=%s payload_len=%s",
@@ -20,8 +20,14 @@ class WsProtocol(BaseProtocol):
20
20
  data = json.loads(raw)
21
21
  return InboundFrame.model_validate(data)
22
22
  except json.JSONDecodeError:
23
- logger.debug("failed to decode websocket frame json", exc_info=True)
24
- return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
23
+ logger.debug(
24
+ "failed to decode websocket frame json", exc_info=True
25
+ )
26
+ return InboundFrame(
27
+ opcode=0, cmd=0, seq=None, payload=None, raw=None
28
+ )
25
29
  except ValidationError:
26
30
  logger.debug("failed to validate websocket frame", exc_info=True)
27
- return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
31
+ return InboundFrame(
32
+ opcode=0, cmd=0, seq=None, payload=None, raw=None
33
+ )
pymax/session/protocol.py CHANGED
@@ -8,7 +8,11 @@ class StoreProtocol(Protocol):
8
8
  async def save_session(self, session_info: SessionInfo) -> None: ...
9
9
  async def update_token(self, old_token: str, new_token: str) -> None: ...
10
10
  async def load_session(self) -> SessionInfo | None: ...
11
- async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None: ...
12
- async def load_session_by_phone(self, phone: str) -> SessionInfo | None: ...
11
+ async def load_session_by_device_id(
12
+ self, device_id: str
13
+ ) -> SessionInfo | None: ...
14
+ async def load_session_by_phone(
15
+ self, phone: str
16
+ ) -> SessionInfo | None: ...
13
17
  async def delete_session(self, token: str) -> None: ...
14
18
  async def close(self) -> None: ...
pymax/session/store.py CHANGED
@@ -55,12 +55,24 @@ class SessionStore:
55
55
  )
56
56
  """
57
57
  )
58
- await self._ensure_column(conn, "mt_instance_id", "TEXT NOT NULL DEFAULT ''")
59
- await self._ensure_column(conn, "chats_sync", "INTEGER NOT NULL DEFAULT -1")
60
- await self._ensure_column(conn, "contacts_sync", "INTEGER NOT NULL DEFAULT -1")
61
- await self._ensure_column(conn, "drafts_sync", "INTEGER NOT NULL DEFAULT -1")
62
- await self._ensure_column(conn, "presence_sync", "INTEGER NOT NULL DEFAULT -1")
63
- await self._ensure_column(conn, "config_hash", "TEXT NOT NULL DEFAULT ''")
58
+ await self._ensure_column(
59
+ conn, "mt_instance_id", "TEXT NOT NULL DEFAULT ''"
60
+ )
61
+ await self._ensure_column(
62
+ conn, "chats_sync", "INTEGER NOT NULL DEFAULT -1"
63
+ )
64
+ await self._ensure_column(
65
+ conn, "contacts_sync", "INTEGER NOT NULL DEFAULT -1"
66
+ )
67
+ await self._ensure_column(
68
+ conn, "drafts_sync", "INTEGER NOT NULL DEFAULT -1"
69
+ )
70
+ await self._ensure_column(
71
+ conn, "presence_sync", "INTEGER NOT NULL DEFAULT -1"
72
+ )
73
+ await self._ensure_column(
74
+ conn, "config_hash", "TEXT NOT NULL DEFAULT ''"
75
+ )
64
76
  await conn.execute(
65
77
  """
66
78
  UPDATE sessions
@@ -81,7 +93,9 @@ class SessionStore:
81
93
  columns = {row["name"] for row in await cursor.fetchall()}
82
94
 
83
95
  if name not in columns:
84
- await conn.execute(f"ALTER TABLE sessions ADD COLUMN {name} {definition}")
96
+ await conn.execute(
97
+ f"ALTER TABLE sessions ADD COLUMN {name} {definition}"
98
+ )
85
99
 
86
100
  async def save_session(self, session_info: SessionInfo) -> None:
87
101
  conn = await self._get_connection()
@@ -144,7 +158,9 @@ class SessionStore:
144
158
  )
145
159
  return self._row_to_session(row)
146
160
 
147
- async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None:
161
+ async def load_session_by_device_id(
162
+ self, device_id: str
163
+ ) -> SessionInfo | None:
148
164
  conn = await self._get_connection()
149
165
  logger.debug("loading session by device_id=%s", device_id)
150
166
  async with conn.execute(
@@ -155,7 +155,9 @@ class NavigationPlanner:
155
155
  self.current_screen = self.history.pop()
156
156
  return self.current_screen
157
157
 
158
- next_screen = self._weighted_choice(self.rules.graph[self.current_screen])
158
+ next_screen = self._weighted_choice(
159
+ self.rules.graph[self.current_screen]
160
+ )
159
161
  if next_screen != self.current_screen:
160
162
  self.history.append(self.current_screen)
161
163
  if len(self.history) > 4:
@@ -88,10 +88,14 @@ class TelemetryService:
88
88
 
89
89
  while True:
90
90
  self._session_id += 1
91
- events = await self._collect_session_events(self._planner.new_profile())
91
+ events = await self._collect_session_events(
92
+ self._planner.new_profile()
93
+ )
92
94
  await self._send_events(events)
93
95
  self._planner.reset_to_background()
94
- await asyncio.sleep(self._between(self._timing.session_idle_delay))
96
+ await asyncio.sleep(
97
+ self._between(self._timing.session_idle_delay)
98
+ )
95
99
 
96
100
  except asyncio.CancelledError:
97
101
  raise
@@ -159,7 +163,9 @@ class TelemetryService:
159
163
  except Exception:
160
164
  logger.debug("telemetry send failed", exc_info=True)
161
165
 
162
- def _nav_event(self, screen_from: Screen, screen_to: Screen) -> TelemetryEvent:
166
+ def _nav_event(
167
+ self, screen_from: Screen, screen_to: Screen
168
+ ) -> TelemetryEvent:
163
169
  event = self._payloads.navigation(
164
170
  user_id=self._user_id,
165
171
  session_id=self._session_id,
pymax/transport/tcp.py CHANGED
@@ -10,7 +10,9 @@ logger = get_logger(__name__)
10
10
 
11
11
 
12
12
  class TCPTransport(Transport):
13
- def __init__(self, host: str, port: int, proxy: str | None, use_ssl: bool = True) -> None:
13
+ def __init__(
14
+ self, host: str, port: int, proxy: str | None, use_ssl: bool = True
15
+ ) -> None:
14
16
  self._host = host
15
17
  self._port = port
16
18
  self._proxy = proxy
@@ -57,10 +59,14 @@ class TCPTransport(Transport):
57
59
  )
58
60
 
59
61
  async def close(self) -> None:
60
- if self._writer:
62
+ writer = self._writer
63
+ self._reader = None
64
+ self._writer = None
65
+
66
+ if writer:
61
67
  logger.debug("tcp close")
62
- self._writer.close()
63
- await self._writer.wait_closed()
68
+ writer.close()
69
+ await writer.wait_closed()
64
70
  logger.debug("tcp closed")
65
71
 
66
72
  async def send(self, data: bytes | str) -> None:
@@ -1,5 +1,3 @@
1
- import asyncio
2
-
3
1
  from websockets import ClientConnection, Origin
4
2
  from websockets.asyncio import client
5
3
 
@@ -1,10 +1,13 @@
1
1
  from .attachments import *
2
+ from .bots import InitData
2
3
  from .chat import Chat
3
4
  from .error import MaxApiError
4
5
  from .folder import Folder, FolderList, FolderUpdate
5
6
  from .login import LoginResponse
7
+ from .member import Member
6
8
  from .message import Message, ReactionCounter, ReactionInfo, ReadState
7
9
  from .name import Name
10
+ from .presence import Presence
8
11
  from .profile import Profile
9
12
  from .session import Session
10
13
  from .sync import SyncOverrides, SyncState