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.
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/METADATA +3 -11
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/RECORD +64 -59
- pymax/__init__.py +18 -3
- pymax/api/auth/payloads.py +7 -0
- pymax/api/auth/service.py +33 -30
- pymax/api/binding.py +57 -0
- pymax/api/chats/payloads.py +6 -0
- pymax/api/chats/service.py +52 -47
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +16 -1
- pymax/api/messages/service.py +78 -34
- pymax/api/models.py +4 -6
- pymax/api/response.py +2 -2
- pymax/api/self/service.py +17 -26
- pymax/api/session/payloads.py +2 -9
- pymax/api/session/service.py +1 -3
- pymax/api/uploads/payloads.py +3 -9
- pymax/api/uploads/service.py +33 -99
- pymax/api/users/payloads.py +22 -0
- pymax/api/users/service.py +22 -17
- pymax/app.py +28 -6
- pymax/auth/qr.py +3 -9
- pymax/auth/sms.py +23 -11
- pymax/base.py +86 -4
- pymax/client.py +2 -1
- pymax/client_web.py +1 -2
- pymax/config.py +42 -3
- pymax/connection/connection.py +2 -0
- pymax/connection/readers/tcp.py +1 -3
- pymax/dispatch/__init__.py +12 -1
- pymax/dispatch/dispatcher.py +170 -34
- pymax/dispatch/enums.py +5 -0
- pymax/dispatch/mapping.py +34 -11
- pymax/dispatch/resolvers.py +18 -0
- pymax/dispatch/router.py +120 -4
- pymax/formatting/markdown.py +22 -13
- pymax/infra/chat.py +33 -0
- pymax/infra/message.py +69 -2
- pymax/infra/user.py +12 -1
- pymax/logging.py +2 -0
- pymax/protocol/tcp/compression.py +1 -3
- pymax/protocol/tcp/framing.py +1 -3
- pymax/protocol/ws/protocol.py +3 -9
- pymax/session/protocol.py +2 -6
- pymax/session/store.py +19 -24
- pymax/telemetry/navigation.py +1 -3
- pymax/telemetry/service.py +5 -17
- pymax/transport/tcp.py +1 -3
- pymax/types/domain/__init__.py +1 -1
- pymax/types/domain/attachments/unknown.py +1 -3
- pymax/types/domain/auth.py +24 -2
- pymax/types/domain/chat.py +58 -1
- pymax/types/domain/message.py +28 -2
- pymax/types/domain/presence.py +3 -3
- pymax/types/domain/sync.py +5 -21
- pymax/types/domain/user.py +8 -0
- pymax/types/events/__init__.py +4 -0
- pymax/types/events/mark.py +23 -0
- pymax/types/events/message.py +57 -5
- pymax/types/events/presence.py +15 -0
- pymax/types/events/reaction.py +21 -0
- pymax/types/events/typing.py +14 -0
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/WHEEL +0 -0
- {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
|
|
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.
|
|
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.
|
|
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],
|
pymax/formatting/markdown.py
CHANGED
|
@@ -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 +=
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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()
|
pymax/protocol/tcp/framing.py
CHANGED
|
@@ -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
|
|
pymax/protocol/ws/protocol.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
)
|
|
61
|
-
await self._ensure_column(
|
|
62
|
-
|
|
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(
|
pymax/telemetry/navigation.py
CHANGED
|
@@ -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:
|
pymax/telemetry/service.py
CHANGED
|
@@ -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
|
pymax/types/domain/__init__.py
CHANGED
|
@@ -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
|