maxapi-python 2.1.3__py3-none-any.whl → 2.2.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 (58) hide show
  1. {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/METADATA +3 -11
  2. {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/RECORD +58 -53
  3. pymax/__init__.py +18 -3
  4. pymax/api/auth/payloads.py +7 -0
  5. pymax/api/auth/service.py +33 -30
  6. pymax/api/binding.py +57 -0
  7. pymax/api/chats/service.py +34 -47
  8. pymax/api/messages/enums.py +1 -0
  9. pymax/api/messages/payloads.py +16 -1
  10. pymax/api/messages/service.py +85 -33
  11. pymax/api/models.py +4 -6
  12. pymax/api/response.py +2 -2
  13. pymax/api/self/service.py +17 -26
  14. pymax/api/session/payloads.py +2 -9
  15. pymax/api/session/service.py +1 -3
  16. pymax/api/uploads/payloads.py +3 -9
  17. pymax/api/uploads/service.py +33 -99
  18. pymax/api/users/service.py +8 -16
  19. pymax/app.py +2 -0
  20. pymax/auth/qr.py +3 -9
  21. pymax/auth/sms.py +23 -11
  22. pymax/base.py +38 -1
  23. pymax/client.py +2 -1
  24. pymax/client_web.py +1 -2
  25. pymax/config.py +42 -3
  26. pymax/connection/connection.py +2 -0
  27. pymax/connection/readers/tcp.py +1 -3
  28. pymax/dispatch/dispatcher.py +36 -18
  29. pymax/dispatch/enums.py +4 -0
  30. pymax/dispatch/mapping.py +34 -11
  31. pymax/dispatch/resolvers.py +18 -0
  32. pymax/dispatch/router.py +34 -0
  33. pymax/formatting/markdown.py +22 -13
  34. pymax/infra/chat.py +12 -0
  35. pymax/infra/message.py +74 -3
  36. pymax/logging.py +2 -0
  37. pymax/protocol/tcp/compression.py +1 -3
  38. pymax/protocol/tcp/framing.py +1 -3
  39. pymax/protocol/ws/protocol.py +3 -9
  40. pymax/session/protocol.py +2 -6
  41. pymax/session/store.py +8 -24
  42. pymax/telemetry/navigation.py +1 -3
  43. pymax/telemetry/service.py +5 -17
  44. pymax/transport/tcp.py +1 -3
  45. pymax/types/domain/attachments/unknown.py +1 -3
  46. pymax/types/domain/auth.py +24 -2
  47. pymax/types/domain/chat.py +38 -1
  48. pymax/types/domain/message.py +31 -1
  49. pymax/types/domain/presence.py +3 -3
  50. pymax/types/domain/sync.py +5 -21
  51. pymax/types/events/__init__.py +4 -0
  52. pymax/types/events/mark.py +23 -0
  53. pymax/types/events/message.py +57 -5
  54. pymax/types/events/presence.py +15 -0
  55. pymax/types/events/reaction.py +21 -0
  56. pymax/types/events/typing.py +14 -0
  57. {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/WHEEL +0 -0
  58. {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,6 +9,12 @@ 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 (
13
+ MessageReadEvent,
14
+ PresenceEvent,
15
+ ReactionUpdateEvent,
16
+ TypingEvent,
17
+ )
12
18
 
13
19
  from .enums import EventType
14
20
  from .mapping import EventMapper, EventResolver
@@ -33,9 +39,7 @@ ClientT = TypeVar("ClientT")
33
39
 
34
40
 
35
41
  class Dispatcher(Generic[ClientT]):
36
- def __init__(
37
- self, app: App, root_router: Router[ClientT] | None = None
38
- ) -> None:
42
+ def __init__(self, app: App, root_router: Router[ClientT] | None = None) -> None:
39
43
  self.root_router: Router[ClientT] = root_router or Router()
40
44
  self.internal_router: Router[ClientT] = Router()
41
45
  self.resolver = EventResolver()
@@ -71,9 +75,7 @@ class Dispatcher(Generic[ClientT]):
71
75
  event: EventType,
72
76
  *filters: FilterCallback[Any],
73
77
  ) -> HandlerDecorator[Any, ClientT]:
74
- logger.debug(
75
- "registering handler event=%s filters=%s", event, len(filters)
76
- )
78
+ logger.debug("registering handler event=%s filters=%s", event, len(filters))
77
79
  return self.root_router.on(event, *filters)
78
80
 
79
81
  def on_message(
@@ -87,9 +89,7 @@ class Dispatcher(Generic[ClientT]):
87
89
  self,
88
90
  *filters: FilterCallback[Message],
89
91
  ) -> HandlerDecorator[Message, ClientT]:
90
- logger.debug(
91
- "registering message edit handler filters=%s", len(filters)
92
- )
92
+ logger.debug("registering message edit handler filters=%s", len(filters))
93
93
  return self.root_router.on_message_edit(*filters)
94
94
 
95
95
  def on_message_delete(
@@ -98,6 +98,30 @@ class Dispatcher(Generic[ClientT]):
98
98
  ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
99
99
  return self.root_router.on_message_delete(*filters)
100
100
 
101
+ def on_message_read(
102
+ self,
103
+ *filters: FilterCallback[MessageReadEvent],
104
+ ) -> HandlerDecorator[MessageReadEvent, ClientT]:
105
+ return self.root_router.on_message_read(*filters)
106
+
107
+ def on_typing(
108
+ self,
109
+ *filters: FilterCallback[TypingEvent],
110
+ ) -> HandlerDecorator[TypingEvent, ClientT]:
111
+ return self.root_router.on_typing(*filters)
112
+
113
+ def on_presence(
114
+ self,
115
+ *filters: FilterCallback[PresenceEvent],
116
+ ) -> HandlerDecorator[PresenceEvent, ClientT]:
117
+ return self.root_router.on_presence(*filters)
118
+
119
+ def on_reaction_update(
120
+ self,
121
+ *filters: FilterCallback[ReactionUpdateEvent],
122
+ ) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
123
+ return self.root_router.on_reaction_update(*filters)
124
+
101
125
  def on_chat_update(
102
126
  self,
103
127
  *filters: FilterCallback[Chat],
@@ -116,9 +140,7 @@ class Dispatcher(Generic[ClientT]):
116
140
  def iter_routers(self) -> Generator[Router[ClientT], Any, None]:
117
141
  yield from self._iter_router(self.root_router)
118
142
 
119
- def _iter_router(
120
- self, router: Router[ClientT]
121
- ) -> Generator[Router[ClientT], Any, None]:
143
+ def _iter_router(self, router: Router[ClientT]) -> Generator[Router[ClientT], Any, None]:
122
144
  yield router
123
145
 
124
146
  for child in router.children:
@@ -161,9 +183,7 @@ class Dispatcher(Generic[ClientT]):
161
183
  if event_type is not None:
162
184
  logger.debug("dispatching event type=%s", event_type)
163
185
  event = self.mapper.map(event_type, frame)
164
- await self._dispatch_to_router(
165
- self.internal_router, event_type, event
166
- )
186
+ await self._dispatch_to_router(self.internal_router, event_type, event)
167
187
  await self._dispatch_to_router(self.root_router, event_type, event)
168
188
  else:
169
189
  logger.debug(
@@ -209,9 +229,7 @@ class Dispatcher(Generic[ClientT]):
209
229
  return False
210
230
  return True
211
231
 
212
- async def _call(
213
- self, callback: HandlerCallback[Any, ClientT], event: Any
214
- ) -> Any:
232
+ async def _call(self, callback: HandlerCallback[Any, ClientT], event: Any) -> Any:
215
233
  if self.client is None:
216
234
  raise RuntimeError("client is not bound")
217
235
 
pymax/dispatch/enums.py CHANGED
@@ -5,6 +5,10 @@ class EventType(str, Enum):
5
5
  MESSAGE_NEW = "message_new"
6
6
  MESSAGE_EDIT = "message_edit"
7
7
  MESSAGE_DELETE = "message_delete"
8
+ MESSAGE_READ = "message_read"
9
+ TYPING = "typing"
10
+ PRESENCE = "presence"
11
+ REACTION_UPDATE = "reaction_update"
8
12
  CHAT_UPDATE = "chat_update"
9
13
  USER_UPDATE = "user_update"
10
14
  VIDEO_READY = "video_ready"
pymax/dispatch/mapping.py CHANGED
@@ -3,11 +3,19 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from pymax.api.binding import bind_api_model
6
7
  from pymax.protocol import InboundFrame, Opcode
7
8
  from pymax.protocol.enums import Command
8
9
  from pymax.types import Chat, MessageDeleteEvent
9
10
  from pymax.types.domain import Message
10
- from pymax.types.events import FileUploadSignal, VideoUploadSignal
11
+ from pymax.types.events import (
12
+ FileUploadSignal,
13
+ MessageReadEvent,
14
+ PresenceEvent,
15
+ ReactionUpdateEvent,
16
+ TypingEvent,
17
+ VideoUploadSignal,
18
+ )
11
19
 
12
20
  from .enums import EventType
13
21
  from .resolvers import (
@@ -15,6 +23,10 @@ from .resolvers import (
15
23
  resolve_chat,
16
24
  resolve_message,
17
25
  resolve_message_delete,
26
+ resolve_message_read,
27
+ resolve_presence,
28
+ resolve_reaction_update,
29
+ resolve_typing,
18
30
  )
19
31
 
20
32
  if TYPE_CHECKING:
@@ -28,6 +40,10 @@ EVENT_MAP: dict[Opcode, Resolver] = {
28
40
  Opcode.NOTIF_CHAT: resolve_chat,
29
41
  Opcode.NOTIF_MSG_DELETE: resolve_message_delete,
30
42
  Opcode.NOTIF_ATTACH: resolve_attach,
43
+ Opcode.NOTIF_TYPING: resolve_typing,
44
+ Opcode.NOTIF_MARK: resolve_message_read,
45
+ Opcode.NOTIF_PRESENCE: resolve_presence,
46
+ Opcode.NOTIF_MSG_REACTIONS_CHANGED: resolve_reaction_update,
31
47
  }
32
48
 
33
49
 
@@ -58,21 +74,28 @@ class EventMapper:
58
74
 
59
75
  if frame.payload:
60
76
  if event_type in (EventType.MESSAGE_NEW, EventType.MESSAGE_EDIT):
61
- return Message.model_validate(frame.payload).bind(
62
- self.app.api.messages
77
+ return bind_api_model(
78
+ self.app,
79
+ Message.model_validate(frame.payload),
63
80
  )
64
81
  elif event_type == EventType.CHAT_UPDATE:
65
- return Chat.model_validate(frame.payload["chat"]).bind(
66
- self.app.api.messages,
67
- self.app.api.chats,
82
+ return bind_api_model(
83
+ self.app,
84
+ Chat.model_validate(frame.payload["chat"]),
68
85
  )
69
86
  elif event_type == EventType.MESSAGE_DELETE:
70
- model = MessageDeleteEvent.model_validate(frame.payload)
71
- model.chat.bind(
72
- self.app.api.messages,
73
- self.app.api.chats,
87
+ return bind_api_model(
88
+ self.app,
89
+ MessageDeleteEvent.model_validate(frame.payload),
74
90
  )
75
- return model
91
+ elif event_type == EventType.MESSAGE_READ:
92
+ return MessageReadEvent.model_validate(frame.payload)
93
+ elif event_type == EventType.TYPING:
94
+ return TypingEvent.model_validate(frame.payload)
95
+ elif event_type == EventType.PRESENCE:
96
+ return PresenceEvent.model_validate(frame.payload)
97
+ elif event_type == EventType.REACTION_UPDATE:
98
+ return ReactionUpdateEvent.model_validate(frame.payload)
76
99
  elif event_type == EventType.VIDEO_READY:
77
100
  return VideoUploadSignal.model_validate(frame.payload)
78
101
  elif event_type == EventType.FILE_READY:
@@ -20,6 +20,22 @@ def resolve_message_delete(_: InboundFrame) -> EventType | None:
20
20
  return EventType.MESSAGE_DELETE
21
21
 
22
22
 
23
+ def resolve_message_read(_: InboundFrame) -> EventType | None:
24
+ return EventType.MESSAGE_READ
25
+
26
+
27
+ def resolve_typing(_: InboundFrame) -> EventType | None:
28
+ return EventType.TYPING
29
+
30
+
31
+ def resolve_presence(_: InboundFrame) -> EventType | None:
32
+ return EventType.PRESENCE
33
+
34
+
35
+ def resolve_reaction_update(_: InboundFrame) -> EventType | None:
36
+ return EventType.REACTION_UPDATE
37
+
38
+
23
39
  def resolve_attach(frame: InboundFrame) -> EventType | None:
24
40
  try:
25
41
  FileUploadSignal.model_validate(frame.payload)
@@ -45,6 +61,8 @@ def resolve_message(frame: InboundFrame) -> EventType | None:
45
61
 
46
62
  if model.status == MessageStatus.EDITED:
47
63
  return EventType.MESSAGE_EDIT
64
+ if model.status == MessageStatus.REMOVED:
65
+ return EventType.MESSAGE_DELETE
48
66
  else:
49
67
  return EventType.MESSAGE_NEW
50
68
  except ValidationError:
pymax/dispatch/router.py CHANGED
@@ -14,6 +14,12 @@ if TYPE_CHECKING:
14
14
  from pymax.protocol import InboundFrame
15
15
  from pymax.types import Chat
16
16
  from pymax.types.domain import Message
17
+ from pymax.types.events import (
18
+ MessageReadEvent,
19
+ PresenceEvent,
20
+ ReactionUpdateEvent,
21
+ TypingEvent,
22
+ )
17
23
 
18
24
 
19
25
  _EventT = TypeVar("_EventT")
@@ -186,6 +192,34 @@ class Router(Generic[ClientT]):
186
192
  """
187
193
  return self.on(EventType.MESSAGE_DELETE, *filters)
188
194
 
195
+ def on_message_read(
196
+ self,
197
+ *filters: FilterCallback[MessageReadEvent],
198
+ ) -> HandlerDecorator[MessageReadEvent, ClientT]:
199
+ """Регистрирует обработчик изменения отметки прочтения."""
200
+ return self.on(EventType.MESSAGE_READ, *filters)
201
+
202
+ def on_typing(
203
+ self,
204
+ *filters: FilterCallback[TypingEvent],
205
+ ) -> HandlerDecorator[TypingEvent, ClientT]:
206
+ """Регистрирует обработчик набора текста."""
207
+ return self.on(EventType.TYPING, *filters)
208
+
209
+ def on_presence(
210
+ self,
211
+ *filters: FilterCallback[PresenceEvent],
212
+ ) -> HandlerDecorator[PresenceEvent, ClientT]:
213
+ """Регистрирует обработчик изменения присутствия пользователя."""
214
+ return self.on(EventType.PRESENCE, *filters)
215
+
216
+ def on_reaction_update(
217
+ self,
218
+ *filters: FilterCallback[ReactionUpdateEvent],
219
+ ) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
220
+ """Регистрирует обработчик обновления реакций сообщения."""
221
+ return self.on(EventType.REACTION_UPDATE, *filters)
222
+
189
223
  def on_chat_update(
190
224
  self,
191
225
  *filters: FilterCallback[Chat],
@@ -2,6 +2,10 @@ from pymax.types.domain.element import Element, ElementAttributes
2
2
 
3
3
 
4
4
  class Formatter:
5
+ # Characters above this value are encoded as surrogate pairs in UTF-16,
6
+ # occupying 2 code units instead of 1.
7
+ BMP_MAX = 0xFFFF
8
+
5
9
  MARKERS = {
6
10
  "```": "CODE",
7
11
  "**": "STRONG",
@@ -14,6 +18,10 @@ class Formatter:
14
18
 
15
19
  MARKER_ORDER = ["```", "**", "__", "~~", "`", "_", "*"]
16
20
 
21
+ @staticmethod
22
+ def _code_units_len(text: str) -> int:
23
+ return len(text.encode("utf-16-le")) // 2
24
+
17
25
  @staticmethod
18
26
  def _parse_link(
19
27
  text: str,
@@ -64,15 +72,16 @@ class Formatter:
64
72
  label, url, next_i = parsed_link
65
73
 
66
74
  start = clean_pos
75
+ utf16_label_len = Formatter._code_units_len(label)
67
76
 
68
77
  clean_text += label
69
- clean_pos += len(label)
78
+ clean_pos += utf16_label_len
70
79
 
71
80
  entities.append(
72
81
  Element(
73
82
  type="LINK",
74
83
  from_=start,
75
- length=len(label),
84
+ length=utf16_label_len,
76
85
  attributes=ElementAttributes(url=url),
77
86
  )
78
87
  )
@@ -93,9 +102,10 @@ class Formatter:
93
102
  start = clean_pos
94
103
 
95
104
  while i < len(text) and text[i] != "\n":
96
- clean_text += text[i]
105
+ ch = text[i]
106
+ clean_text += ch
97
107
  i += 1
98
- clean_pos += 1
108
+ clean_pos += 2 if ord(ch) > Formatter.BMP_MAX else 1
99
109
 
100
110
  length = clean_pos - start
101
111
 
@@ -123,9 +133,10 @@ class Formatter:
123
133
  start = clean_pos
124
134
 
125
135
  while i < len(text) and text[i] != "\n":
126
- clean_text += text[i]
136
+ ch = text[i]
137
+ clean_text += ch
127
138
  i += 1
128
- clean_pos += 1
139
+ clean_pos += 2 if ord(ch) > Formatter.BMP_MAX else 1
129
140
 
130
141
  length = clean_pos - start
131
142
 
@@ -151,10 +162,7 @@ class Formatter:
151
162
  if marker == "```":
152
163
  closing_index = text.find(marker, i + marker_len)
153
164
 
154
- if (
155
- closing_index == -1
156
- or closing_index == i + marker_len
157
- ):
165
+ if closing_index == -1 or closing_index == i + marker_len:
158
166
  clean_text += marker
159
167
  clean_pos += marker_len
160
168
  i += marker_len
@@ -211,10 +219,11 @@ class Formatter:
211
219
  line_start = False
212
220
  continue
213
221
 
214
- clean_text += text[i]
215
- line_start = text[i] == "\n"
222
+ ch = text[i]
223
+ clean_text += ch
224
+ line_start = ch == "\n"
216
225
 
217
226
  i += 1
218
- clean_pos += 1
227
+ clean_pos += 2 if ord(ch) > Formatter.BMP_MAX else 1
219
228
 
220
229
  return clean_text, entities
pymax/infra/chat.py CHANGED
@@ -339,3 +339,15 @@ class ChatMixin(IClientProtocol):
339
339
  chat_id=chat_id,
340
340
  user_id=user_id,
341
341
  )
342
+
343
+ async def join_channel(self, link: str) -> Chat:
344
+ """Вступает в канал по ссылке.
345
+
346
+ Args:
347
+ link: Полная ссылка на канал, invite-ссылка или ее часть с
348
+ join-токеном Max.
349
+
350
+ Returns:
351
+ Канал, в который вступил клиент.
352
+ """
353
+ return await self._app.api.chats.join_channel(link=link)
pymax/infra/message.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from pymax.api.messages.enums import ItemType
2
- from pymax.api.messages.service import SendAttachments
2
+ from pymax.api.messages.service import SendAttachment, SendAttachments
3
3
  from pymax.types import (
4
4
  FileRequest,
5
5
  Message,
@@ -43,6 +43,73 @@ class MessageMixin(IClientProtocol):
43
43
  notify=notify,
44
44
  )
45
45
 
46
+ async def get_message(
47
+ self,
48
+ chat_id: int,
49
+ message_id: int,
50
+ ) -> Message | None:
51
+ """Возвращает сообщение по ID.
52
+
53
+ Args:
54
+ chat_id: ID чата.
55
+ message_id: ID сообщения.
56
+
57
+ Returns:
58
+ Сообщение или ``None``, если сервер его не вернул.
59
+ """
60
+ return await self._app.api.messages.get_message(
61
+ chat_id=chat_id,
62
+ message_id=message_id,
63
+ )
64
+
65
+ async def get_messages(
66
+ self,
67
+ chat_id: int,
68
+ message_ids: list[int],
69
+ ) -> list[Message]:
70
+ """Возвращает сообщения по ID.
71
+
72
+ Args:
73
+ chat_id: ID чата.
74
+ message_ids: ID сообщений.
75
+
76
+ Returns:
77
+ Список найденных сообщений.
78
+ """
79
+ return await self._app.api.messages.get_messages(
80
+ chat_id=chat_id,
81
+ message_ids=message_ids,
82
+ )
83
+
84
+ async def edit_message(
85
+ self,
86
+ chat_id: int,
87
+ message_id: int,
88
+ text: str,
89
+ attachment: SendAttachment | None = None,
90
+ attachments: SendAttachments = None,
91
+ ) -> Message:
92
+ """Редактирует текст и вложения сообщения.
93
+
94
+ Args:
95
+ chat_id: ID чата.
96
+ message_id: ID сообщения.
97
+ text: Новый текст сообщения с поддержкой markdown.
98
+ attachment: Одно новое вложение.
99
+ attachments: Список новых вложений. Имеет приоритет над
100
+ ``attachment``.
101
+
102
+ Returns:
103
+ Отредактированное сообщение.
104
+ """
105
+ return await self._app.api.messages.edit_message(
106
+ chat_id=chat_id,
107
+ message_id=message_id,
108
+ text=text,
109
+ attachment=attachment,
110
+ attachments=attachments,
111
+ )
112
+
46
113
  async def fetch_history(
47
114
  self,
48
115
  chat_id: int,
@@ -236,11 +303,15 @@ class MessageMixin(IClientProtocol):
236
303
  message_id=message_id,
237
304
  )
238
305
 
239
- async def read_message(self, message_id: int, chat_id: int) -> ReadState:
306
+ async def read_message(self, message_id: int | str, chat_id: int) -> ReadState:
240
307
  """Отмечает сообщение как прочитанное.
241
308
 
309
+ У Max различается wire-формат ``message_id`` для отметки прочтения:
310
+ TCP-клиент ожидает ``int``, WebSocket-клиент - ``str``.
311
+
242
312
  Args:
243
- message_id: ID сообщения.
313
+ message_id: ID сообщения. Передавайте ``int`` для ``Client`` и
314
+ ``str`` для ``WebClient``.
244
315
  chat_id: ID чата.
245
316
 
246
317
  Returns:
pymax/logging.py CHANGED
@@ -85,6 +85,8 @@ def configure_logging(
85
85
  configure_logging("DEBUG", use_colors=False)
86
86
  """
87
87
  stream = stream or sys.stderr
88
+ if stream is None:
89
+ raise RuntimeError("No logging stream is available")
88
90
 
89
91
  if use_colors is None:
90
92
  use_colors = hasattr(stream, "isatty") and stream.isatty()
@@ -1,7 +1,5 @@
1
1
  class Lz4BlockCompression:
2
- def decompress(
3
- self, src: bytes, max_output: int = 5 * 1024 * 1024
4
- ) -> bytes:
2
+ def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
5
3
  dst = bytearray()
6
4
  pos = 0
7
5
 
@@ -31,9 +31,7 @@ class TcpPacketFramer:
31
31
  if len(data) < self.HEADER_SIZE:
32
32
  return None
33
33
 
34
- ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(
35
- data, 0
36
- )
34
+ ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(data, 0)
37
35
  flags = (packed_len >> 24) & 0xFF
38
36
  payload_len = packed_len & 0x00FFFFFF
39
37
 
@@ -20,14 +20,8 @@ class WsProtocol(BaseProtocol):
20
20
  data = json.loads(raw)
21
21
  return InboundFrame.model_validate(data)
22
22
  except json.JSONDecodeError:
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
- )
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)
29
25
  except ValidationError:
30
26
  logger.debug("failed to validate websocket frame", exc_info=True)
31
- return InboundFrame(
32
- opcode=0, cmd=0, seq=None, payload=None, raw=None
33
- )
27
+ return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
pymax/session/protocol.py CHANGED
@@ -8,11 +8,7 @@ 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(
12
- self, device_id: str
13
- ) -> SessionInfo | None: ...
14
- async def load_session_by_phone(
15
- self, phone: str
16
- ) -> 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: ...
17
13
  async def delete_session(self, token: str) -> None: ...
18
14
  async def close(self) -> None: ...
pymax/session/store.py CHANGED
@@ -55,24 +55,12 @@ class SessionStore:
55
55
  )
56
56
  """
57
57
  )
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
- )
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 ''")
76
64
  await conn.execute(
77
65
  """
78
66
  UPDATE sessions
@@ -93,9 +81,7 @@ class SessionStore:
93
81
  columns = {row["name"] for row in await cursor.fetchall()}
94
82
 
95
83
  if name not in columns:
96
- await conn.execute(
97
- f"ALTER TABLE sessions ADD COLUMN {name} {definition}"
98
- )
84
+ await conn.execute(f"ALTER TABLE sessions ADD COLUMN {name} {definition}")
99
85
 
100
86
  async def save_session(self, session_info: SessionInfo) -> None:
101
87
  conn = await self._get_connection()
@@ -158,9 +144,7 @@ class SessionStore:
158
144
  )
159
145
  return self._row_to_session(row)
160
146
 
161
- async def load_session_by_device_id(
162
- self, device_id: str
163
- ) -> SessionInfo | None:
147
+ async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None:
164
148
  conn = await self._get_connection()
165
149
  logger.debug("loading session by device_id=%s", device_id)
166
150
  async with conn.execute(
@@ -155,9 +155,7 @@ class NavigationPlanner:
155
155
  self.current_screen = self.history.pop()
156
156
  return self.current_screen
157
157
 
158
- next_screen = self._weighted_choice(
159
- self.rules.graph[self.current_screen]
160
- )
158
+ next_screen = self._weighted_choice(self.rules.graph[self.current_screen])
161
159
  if next_screen != self.current_screen:
162
160
  self.history.append(self.current_screen)
163
161
  if len(self.history) > 4:
@@ -82,20 +82,14 @@ class TelemetryService:
82
82
  async def _run(self) -> None:
83
83
  try:
84
84
  await asyncio.sleep(self._between(self._timing.startup_delay))
85
- await self._send_events(
86
- [self._payloads.login(self._user_id, self._session_id)]
87
- )
85
+ await self._send_events([self._payloads.login(self._user_id, self._session_id)])
88
86
 
89
87
  while True:
90
88
  self._session_id += 1
91
- events = await self._collect_session_events(
92
- self._planner.new_profile()
93
- )
89
+ events = await self._collect_session_events(self._planner.new_profile())
94
90
  await self._send_events(events)
95
91
  self._planner.reset_to_background()
96
- await asyncio.sleep(
97
- self._between(self._timing.session_idle_delay)
98
- )
92
+ await asyncio.sleep(self._between(self._timing.session_idle_delay))
99
93
 
100
94
  except asyncio.CancelledError:
101
95
  raise
@@ -163,9 +157,7 @@ class TelemetryService:
163
157
  except Exception:
164
158
  logger.debug("telemetry send failed", exc_info=True)
165
159
 
166
- def _nav_event(
167
- self, screen_from: Screen, screen_to: Screen
168
- ) -> TelemetryEvent:
160
+ def _nav_event(self, screen_from: Screen, screen_to: Screen) -> TelemetryEvent:
169
161
  event = self._payloads.navigation(
170
162
  user_id=self._user_id,
171
163
  session_id=self._session_id,
@@ -212,11 +204,7 @@ class TelemetryService:
212
204
 
213
205
  @property
214
206
  def _ready(self) -> bool:
215
- return (
216
- self.app.started
217
- and self.app.me is not None
218
- and self.app.connection.is_open
219
- )
207
+ return self.app.started and self.app.me is not None and self.app.connection.is_open
220
208
 
221
209
  @property
222
210
  def _user_id(self) -> int:
pymax/transport/tcp.py CHANGED
@@ -10,9 +10,7 @@ logger = get_logger(__name__)
10
10
 
11
11
 
12
12
  class TCPTransport(Transport):
13
- def __init__(
14
- self, host: str, port: int, proxy: str | None, use_ssl: bool = True
15
- ) -> None:
13
+ def __init__(self, host: str, port: int, proxy: str | None, use_ssl: bool = True) -> None:
16
14
  self._host = host
17
15
  self._port = port
18
16
  self._proxy = proxy