maxapi-python 2.1.3__py3-none-any.whl → 2.3.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 (64) hide show
  1. {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/METADATA +3 -11
  2. {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/RECORD +64 -59
  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/payloads.py +6 -0
  8. pymax/api/chats/service.py +52 -47
  9. pymax/api/messages/enums.py +1 -0
  10. pymax/api/messages/payloads.py +16 -1
  11. pymax/api/messages/service.py +78 -34
  12. pymax/api/models.py +4 -6
  13. pymax/api/response.py +2 -2
  14. pymax/api/self/service.py +17 -26
  15. pymax/api/session/payloads.py +2 -9
  16. pymax/api/session/service.py +1 -3
  17. pymax/api/uploads/payloads.py +3 -9
  18. pymax/api/uploads/service.py +33 -99
  19. pymax/api/users/payloads.py +22 -0
  20. pymax/api/users/service.py +22 -17
  21. pymax/app.py +28 -6
  22. pymax/auth/qr.py +3 -9
  23. pymax/auth/sms.py +23 -11
  24. pymax/base.py +86 -4
  25. pymax/client.py +2 -1
  26. pymax/client_web.py +1 -2
  27. pymax/config.py +42 -3
  28. pymax/connection/connection.py +2 -0
  29. pymax/connection/readers/tcp.py +1 -3
  30. pymax/dispatch/__init__.py +12 -1
  31. pymax/dispatch/dispatcher.py +170 -34
  32. pymax/dispatch/enums.py +5 -0
  33. pymax/dispatch/mapping.py +34 -11
  34. pymax/dispatch/resolvers.py +18 -0
  35. pymax/dispatch/router.py +120 -4
  36. pymax/formatting/markdown.py +22 -13
  37. pymax/infra/chat.py +33 -0
  38. pymax/infra/message.py +69 -2
  39. pymax/infra/user.py +12 -1
  40. pymax/logging.py +2 -0
  41. pymax/protocol/tcp/compression.py +1 -3
  42. pymax/protocol/tcp/framing.py +1 -3
  43. pymax/protocol/ws/protocol.py +3 -9
  44. pymax/session/protocol.py +2 -6
  45. pymax/session/store.py +19 -24
  46. pymax/telemetry/navigation.py +1 -3
  47. pymax/telemetry/service.py +5 -17
  48. pymax/transport/tcp.py +1 -3
  49. pymax/types/domain/__init__.py +1 -1
  50. pymax/types/domain/attachments/unknown.py +1 -3
  51. pymax/types/domain/auth.py +24 -2
  52. pymax/types/domain/chat.py +58 -1
  53. pymax/types/domain/message.py +28 -2
  54. pymax/types/domain/presence.py +3 -3
  55. pymax/types/domain/sync.py +5 -21
  56. pymax/types/domain/user.py +8 -0
  57. pymax/types/events/__init__.py +4 -0
  58. pymax/types/events/mark.py +23 -0
  59. pymax/types/events/message.py +57 -5
  60. pymax/types/events/presence.py +15 -0
  61. pymax/types/events/reaction.py +21 -0
  62. pymax/types/events/typing.py +14 -0
  63. {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/WHEEL +0 -0
  64. {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/licenses/LICENSE +0 -0
pymax/dispatch/router.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from collections import defaultdict
4
4
  from collections.abc import Awaitable, Callable
5
5
  from dataclasses import dataclass
6
+ from enum import Enum
6
7
  from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
7
8
 
8
9
  from pymax.types import MessageDeleteEvent
@@ -10,14 +11,28 @@ from pymax.types import MessageDeleteEvent
10
11
  from .enums import EventType
11
12
 
12
13
  if TYPE_CHECKING:
13
- from pymax.client import Client
14
+ from pymax import Client
15
+ from pymax.base import BaseClient
14
16
  from pymax.protocol import InboundFrame
15
17
  from pymax.types import Chat
16
18
  from pymax.types.domain import Message
19
+ from pymax.types.events import (
20
+ MessageReadEvent,
21
+ PresenceEvent,
22
+ ReactionUpdateEvent,
23
+ TypingEvent,
24
+ )
25
+
26
+
27
+ class ErrorScope(str, Enum):
28
+ """Область действия error-handler-а."""
29
+
30
+ GLOBAL = "global"
31
+ LOCAL = "local"
17
32
 
18
33
 
19
34
  _EventT = TypeVar("_EventT")
20
- ClientT = TypeVar("ClientT")
35
+ ClientT = TypeVar("ClientT", bound="BaseClient")
21
36
 
22
37
  HandlerCallback: TypeAlias = Callable[
23
38
  [_EventT, ClientT],
@@ -41,12 +56,53 @@ StartDecorator: TypeAlias = Callable[
41
56
  ]
42
57
 
43
58
 
59
+ @dataclass(slots=True)
60
+ class ErrorContext(Generic[ClientT]):
61
+ """Контекст ошибки, передаваемый в ``on_error`` callback."""
62
+
63
+ client: ClientT
64
+ event_type: EventType
65
+ event: Any
66
+ handler: HandlerEntry[Any, ClientT] | StartCallback | None
67
+ router: Router[ClientT]
68
+
69
+
70
+ ErrorCallback: TypeAlias = Callable[
71
+ [Exception, ErrorContext[ClientT]],
72
+ Awaitable[Any] | Any,
73
+ ]
74
+
75
+ ErrorDecorator: TypeAlias = Callable[
76
+ [ErrorCallback[ClientT]],
77
+ ErrorCallback[ClientT],
78
+ ]
79
+
80
+ DisconnectCallback: TypeAlias = Callable[
81
+ [Exception, bool, float],
82
+ Awaitable[Any] | Any,
83
+ ]
84
+
85
+ DisconnectDecorator: TypeAlias = Callable[
86
+ [DisconnectCallback],
87
+ DisconnectCallback,
88
+ ]
89
+
90
+
44
91
  @dataclass(slots=True)
45
92
  class HandlerEntry(Generic[_EventT, ClientT]):
46
93
  callback: HandlerCallback[_EventT, ClientT]
47
94
  filters: tuple[FilterCallback[_EventT], ...] = ()
48
95
 
49
96
 
97
+ @dataclass(slots=True)
98
+ class ErrorEntry(Generic[ClientT]):
99
+ callback: ErrorCallback[ClientT]
100
+ scope: ErrorScope = ErrorScope.GLOBAL
101
+
102
+
103
+ ErrorSource: TypeAlias = HandlerEntry[Any, ClientT] | StartCallback[ClientT]
104
+
105
+
50
106
  class Router(Generic[ClientT]):
51
107
  """Контейнер обработчиков событий PyMax.
52
108
 
@@ -79,7 +135,39 @@ class Router(Generic[ClientT]):
79
135
  ] = defaultdict(list)
80
136
 
81
137
  self.children: list[Router[ClientT]] = []
82
- self.on_start_handler: StartCallback[ClientT] | None = None
138
+ self.on_start_handlers: list[StartCallback[ClientT]] = []
139
+ self.error_handlers: list[ErrorEntry[ClientT]] = []
140
+ self.disconnect_handlers: list[DisconnectCallback] = []
141
+
142
+ def on_error(
143
+ self,
144
+ scope: ErrorScope = ErrorScope.GLOBAL,
145
+ ) -> ErrorDecorator[ClientT]:
146
+ """Регистрирует обработчик ошибок для текущего router-а.
147
+
148
+ ``GLOBAL``-handler видит ошибки всего дерева подключенных router-ов.
149
+ ``LOCAL``-handler видит только ошибки своего router-а.
150
+ """
151
+ scope = ErrorScope(scope)
152
+
153
+ def decorator(callback: ErrorCallback[ClientT]) -> ErrorCallback[ClientT]:
154
+ self.error_handlers.append(ErrorEntry(callback=callback, scope=scope))
155
+ return callback
156
+
157
+ return decorator
158
+
159
+ def on_disconnect(self) -> DisconnectDecorator:
160
+ """Регистрирует обработчик потери соединения.
161
+
162
+ Callback вызывается как ``handler(exception, reconnect, delay)``:
163
+ исходная ошибка, будет ли reconnect и задержка перед ним.
164
+ """
165
+
166
+ def decorator(callback: DisconnectCallback) -> DisconnectCallback:
167
+ self.disconnect_handlers.append(callback)
168
+ return callback
169
+
170
+ return decorator
83
171
 
84
172
  def on(
85
173
  self,
@@ -139,7 +227,7 @@ class Router(Generic[ClientT]):
139
227
  """
140
228
 
141
229
  def decorator(handler: StartCallback) -> StartCallback:
142
- self.on_start_handler = handler
230
+ self.on_start_handlers.append(handler)
143
231
  return handler
144
232
 
145
233
  return decorator
@@ -186,6 +274,34 @@ class Router(Generic[ClientT]):
186
274
  """
187
275
  return self.on(EventType.MESSAGE_DELETE, *filters)
188
276
 
277
+ def on_message_read(
278
+ self,
279
+ *filters: FilterCallback[MessageReadEvent],
280
+ ) -> HandlerDecorator[MessageReadEvent, ClientT]:
281
+ """Регистрирует обработчик изменения отметки прочтения."""
282
+ return self.on(EventType.MESSAGE_READ, *filters)
283
+
284
+ def on_typing(
285
+ self,
286
+ *filters: FilterCallback[TypingEvent],
287
+ ) -> HandlerDecorator[TypingEvent, ClientT]:
288
+ """Регистрирует обработчик набора текста."""
289
+ return self.on(EventType.TYPING, *filters)
290
+
291
+ def on_presence(
292
+ self,
293
+ *filters: FilterCallback[PresenceEvent],
294
+ ) -> HandlerDecorator[PresenceEvent, ClientT]:
295
+ """Регистрирует обработчик изменения присутствия пользователя."""
296
+ return self.on(EventType.PRESENCE, *filters)
297
+
298
+ def on_reaction_update(
299
+ self,
300
+ *filters: FilterCallback[ReactionUpdateEvent],
301
+ ) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
302
+ """Регистрирует обработчик обновления реакций сообщения."""
303
+ return self.on(EventType.REACTION_UPDATE, *filters)
304
+
189
305
  def on_chat_update(
190
306
  self,
191
307
  *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
@@ -227,6 +227,27 @@ class ChatMixin(IClientProtocol):
227
227
  """
228
228
  await self._app.api.chats.leave_channel(chat_id)
229
229
 
230
+ async def delete_chat(
231
+ self,
232
+ chat_id: int,
233
+ last_event_time: int | None = None,
234
+ for_all: bool = True,
235
+ ) -> None:
236
+ """Удаляет чат.
237
+
238
+ Args:
239
+ chat_id: ID чата.
240
+ last_event_time: Время последнего события чата. Для объекта
241
+ ``Chat`` это поле ``Chat.last_event_time``.
242
+ for_all: Удалить чат для всех участников, если сервер поддерживает
243
+ такой режим.
244
+ """
245
+ await self._app.api.chats.delete_chat(
246
+ chat_id=chat_id,
247
+ last_event_time=last_event_time,
248
+ for_all=for_all,
249
+ )
250
+
230
251
  async def fetch_chats(self, marker: int | None = None) -> list[Chat]:
231
252
  """Загружает список чатов с сервера и обновляет кеш клиента.
232
253
 
@@ -339,3 +360,15 @@ class ChatMixin(IClientProtocol):
339
360
  chat_id=chat_id,
340
361
  user_id=user_id,
341
362
  )
363
+
364
+ async def join_channel(self, link: str) -> Chat:
365
+ """Вступает в канал по ссылке.
366
+
367
+ Args:
368
+ link: Полная ссылка на канал, invite-ссылка или ее часть с
369
+ join-токеном Max.
370
+
371
+ Returns:
372
+ Канал, в который вступил клиент.
373
+ """
374
+ return await self._app.api.chats.join_channel(link=link)
pymax/infra/message.py CHANGED
@@ -43,6 +43,69 @@ 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
+ attachments: SendAttachments = None,
90
+ ) -> Message:
91
+ """Редактирует текст и вложения сообщения.
92
+
93
+ Args:
94
+ chat_id: ID чата.
95
+ message_id: ID сообщения.
96
+ text: Новый текст сообщения с поддержкой markdown.
97
+ attachments: Новые файлы, фотографии или видео для сообщения.
98
+
99
+ Returns:
100
+ Отредактированное сообщение.
101
+ """
102
+ return await self._app.api.messages.edit_message(
103
+ chat_id=chat_id,
104
+ message_id=message_id,
105
+ text=text,
106
+ attachments=attachments,
107
+ )
108
+
46
109
  async def fetch_history(
47
110
  self,
48
111
  chat_id: int,
@@ -236,11 +299,15 @@ class MessageMixin(IClientProtocol):
236
299
  message_id=message_id,
237
300
  )
238
301
 
239
- async def read_message(self, message_id: int, chat_id: int) -> ReadState:
302
+ async def read_message(self, message_id: int | str, chat_id: int) -> ReadState:
240
303
  """Отмечает сообщение как прочитанное.
241
304
 
305
+ У Max различается wire-формат ``message_id`` для отметки прочтения:
306
+ TCP-клиент ожидает ``int``, WebSocket-клиент - ``str``.
307
+
242
308
  Args:
243
- message_id: ID сообщения.
309
+ message_id: ID сообщения. Передавайте ``int`` для ``Client`` и
310
+ ``str`` для ``WebClient``.
244
311
  chat_id: ID чата.
245
312
 
246
313
  Returns:
pymax/infra/user.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import Literal
2
2
 
3
- from pymax.types import Session, User
3
+ from pymax.types import ContactInfo, Session, User
4
4
 
5
5
  from .protocol import IClientProtocol
6
6
 
@@ -94,6 +94,17 @@ class UserMixin(IClientProtocol):
94
94
  """
95
95
  return await self._app.api.users.remove_contact(contact_id)
96
96
 
97
+ async def import_contacts(self, contacts: list[ContactInfo]) -> list[User]:
98
+ """Импортирует контакты из телефонной книги.
99
+
100
+ Args:
101
+ contacts: Контакты с телефоном и именем.
102
+
103
+ Returns:
104
+ Контакты Max, найденные или созданные сервером.
105
+ """
106
+ return await self._app.api.users.import_contacts(contacts)
107
+
97
108
  def get_chat_id(self, first_user_id: int, second_user_id: int) -> int:
98
109
  """Вычисляет ID личного чата для пары пользователей.
99
110
 
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(
@@ -210,6 +194,17 @@ class SessionStore:
210
194
  await conn.commit()
211
195
  logger.info("session deleted")
212
196
 
197
+ async def delete_all_sessions(self) -> None:
198
+ conn = await self._get_connection()
199
+ logger.warning("deleting all sessions")
200
+ await conn.execute(
201
+ """
202
+ DELETE FROM sessions
203
+ """
204
+ )
205
+ await conn.commit()
206
+ logger.info("all sessions deleted")
207
+
213
208
  async def update_token(self, old_token: str, new_token: str) -> None:
214
209
  conn = await self._get_connection()
215
210
  logger.debug(
@@ -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
@@ -11,4 +11,4 @@ from .presence import Presence
11
11
  from .profile import Profile
12
12
  from .session import Session
13
13
  from .sync import SyncOverrides, SyncState
14
- from .user import User
14
+ from .user import ContactInfo, User
@@ -30,8 +30,6 @@ class UnknownAttachment(CamelModel):
30
30
 
31
31
  attachment_type = value.get("_type", value.get("type"))
32
32
  if attachment_type in KNOWN_ATTACHMENT_TYPES:
33
- raise ValueError(
34
- "Known attachment type should be parsed by its own model"
35
- )
33
+ raise ValueError("Known attachment type should be parsed by its own model")
36
34
 
37
35
  return value