maxapi-python 2.2.0__py3-none-any.whl → 2.3.1__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.2.0.dist-info → maxapi_python-2.3.1.dist-info}/METADATA +2 -1
- {maxapi_python-2.2.0.dist-info → maxapi_python-2.3.1.dist-info}/RECORD +28 -28
- pymax/__init__.py +1 -1
- pymax/api/chats/payloads.py +6 -0
- pymax/api/chats/service.py +18 -0
- pymax/api/messages/payloads.py +21 -1
- pymax/api/messages/service.py +42 -11
- pymax/api/users/payloads.py +22 -0
- pymax/api/users/service.py +14 -1
- pymax/app.py +26 -6
- pymax/base.py +48 -3
- pymax/dispatch/__init__.py +12 -1
- pymax/dispatch/dispatcher.py +134 -16
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/router.py +86 -4
- pymax/infra/chat.py +21 -0
- pymax/infra/message.py +29 -6
- pymax/infra/user.py +12 -1
- pymax/protocol/tcp/compression.py +18 -0
- pymax/protocol/tcp/payload.py +20 -4
- pymax/protocol/tcp/protocol.py +5 -1
- pymax/session/store.py +11 -0
- pymax/types/domain/__init__.py +1 -1
- pymax/types/domain/chat.py +21 -1
- pymax/types/domain/message.py +32 -9
- pymax/types/domain/user.py +14 -4
- {maxapi_python-2.2.0.dist-info → maxapi_python-2.3.1.dist-info}/WHEEL +0 -0
- {maxapi_python-2.2.0.dist-info → maxapi_python-2.3.1.dist-info}/licenses/LICENSE +0 -0
pymax/dispatch/dispatcher.py
CHANGED
|
@@ -19,11 +19,19 @@ from pymax.types.events import (
|
|
|
19
19
|
from .enums import EventType
|
|
20
20
|
from .mapping import EventMapper, EventResolver
|
|
21
21
|
from .router import (
|
|
22
|
+
DisconnectCallback,
|
|
23
|
+
DisconnectDecorator,
|
|
24
|
+
ErrorContext,
|
|
25
|
+
ErrorDecorator,
|
|
26
|
+
ErrorEntry,
|
|
27
|
+
ErrorScope,
|
|
28
|
+
ErrorSource,
|
|
22
29
|
FilterCallback,
|
|
23
30
|
HandlerCallback,
|
|
24
31
|
HandlerDecorator,
|
|
25
32
|
HandlerEntry,
|
|
26
33
|
Router,
|
|
34
|
+
StartCallback,
|
|
27
35
|
StartDecorator,
|
|
28
36
|
)
|
|
29
37
|
|
|
@@ -31,11 +39,12 @@ if TYPE_CHECKING:
|
|
|
31
39
|
from collections.abc import Generator
|
|
32
40
|
|
|
33
41
|
from pymax.app import App
|
|
42
|
+
from pymax.base import BaseClient
|
|
34
43
|
|
|
35
44
|
|
|
36
45
|
logger = get_logger(__name__)
|
|
37
46
|
|
|
38
|
-
ClientT = TypeVar("ClientT")
|
|
47
|
+
ClientT = TypeVar("ClientT", bound="BaseClient")
|
|
39
48
|
|
|
40
49
|
|
|
41
50
|
class Dispatcher(Generic[ClientT]):
|
|
@@ -78,6 +87,13 @@ class Dispatcher(Generic[ClientT]):
|
|
|
78
87
|
logger.debug("registering handler event=%s filters=%s", event, len(filters))
|
|
79
88
|
return self.root_router.on(event, *filters)
|
|
80
89
|
|
|
90
|
+
def on_error(self, scope: ErrorScope = ErrorScope.GLOBAL) -> ErrorDecorator[ClientT]:
|
|
91
|
+
return self.root_router.on_error(scope)
|
|
92
|
+
|
|
93
|
+
def on_disconnect(self) -> DisconnectDecorator:
|
|
94
|
+
"""Регистрирует обработчик сетевого отключения на root router."""
|
|
95
|
+
return self.root_router.on_disconnect()
|
|
96
|
+
|
|
81
97
|
def on_message(
|
|
82
98
|
self,
|
|
83
99
|
*filters: FilterCallback[Message],
|
|
@@ -146,22 +162,62 @@ class Dispatcher(Generic[ClientT]):
|
|
|
146
162
|
for child in router.children:
|
|
147
163
|
yield from self._iter_router(child)
|
|
148
164
|
|
|
149
|
-
|
|
150
|
-
|
|
165
|
+
def iter_error_entries(
|
|
166
|
+
self,
|
|
167
|
+
) -> Generator[tuple[Router[ClientT], ErrorEntry[ClientT]], Any, None]:
|
|
168
|
+
for router in self.iter_routers():
|
|
169
|
+
for entry in router.error_handlers:
|
|
170
|
+
yield router, entry
|
|
151
171
|
|
|
172
|
+
def iter_disconnect_handlers(self) -> Generator[DisconnectCallback, Any, None]:
|
|
173
|
+
"""Итерирует обработчики disconnect по root router и его детям."""
|
|
152
174
|
for router in self.iter_routers():
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
yield from router.disconnect_handlers
|
|
176
|
+
|
|
177
|
+
def iter_error_handlers(
|
|
178
|
+
self,
|
|
179
|
+
failed_router: Router[ClientT],
|
|
180
|
+
) -> Generator[ErrorEntry[ClientT], Any, None]:
|
|
181
|
+
for owner_router, entry in self.iter_error_entries():
|
|
182
|
+
if entry.scope is ErrorScope.LOCAL and owner_router is not failed_router:
|
|
155
183
|
continue
|
|
156
184
|
|
|
157
|
-
|
|
185
|
+
yield entry
|
|
158
186
|
|
|
159
|
-
|
|
160
|
-
|
|
187
|
+
async def emit_start(self, client: ClientT) -> None:
|
|
188
|
+
tasks: list[asyncio.Task[Any]] = []
|
|
189
|
+
|
|
190
|
+
for router in self.iter_routers(): # TODO: create iter_on_start_handlers
|
|
191
|
+
for handler in router.on_start_handlers:
|
|
192
|
+
task = asyncio.create_task(self._run_start_handler(router, handler, client))
|
|
161
193
|
task.add_done_callback(_log_task_error)
|
|
162
194
|
tasks.append(task)
|
|
163
195
|
|
|
164
|
-
self.startup_tasks
|
|
196
|
+
self.startup_tasks.extend(tasks)
|
|
197
|
+
|
|
198
|
+
async def _run_start_handler(
|
|
199
|
+
self,
|
|
200
|
+
router: Router[ClientT],
|
|
201
|
+
handler: StartCallback[ClientT],
|
|
202
|
+
client: ClientT,
|
|
203
|
+
) -> None:
|
|
204
|
+
try:
|
|
205
|
+
result = handler(client)
|
|
206
|
+
|
|
207
|
+
if inspect.isawaitable(result):
|
|
208
|
+
await result
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
handled = await self.emit_error(
|
|
212
|
+
e,
|
|
213
|
+
EventType.ON_START,
|
|
214
|
+
None,
|
|
215
|
+
router,
|
|
216
|
+
handler,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not handled:
|
|
220
|
+
raise
|
|
165
221
|
|
|
166
222
|
async def stop_startup_tasks(self) -> None:
|
|
167
223
|
if not self.startup_tasks:
|
|
@@ -201,13 +257,18 @@ class Dispatcher(Generic[ClientT]):
|
|
|
201
257
|
event: Any,
|
|
202
258
|
) -> None:
|
|
203
259
|
for entry in router.handlers.get(event_type, []):
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
260
|
+
try:
|
|
261
|
+
if await self._matches(entry, event):
|
|
262
|
+
logger.debug(
|
|
263
|
+
"calling handler event=%s callback=%s",
|
|
264
|
+
event_type,
|
|
265
|
+
_callback_name(entry.callback),
|
|
266
|
+
)
|
|
267
|
+
await self._call(entry.callback, event)
|
|
268
|
+
except Exception as e: # noqa: PERF203
|
|
269
|
+
handled = await self.emit_error(e, event_type, event, router, entry)
|
|
270
|
+
if not handled:
|
|
271
|
+
raise
|
|
211
272
|
|
|
212
273
|
for child in router.children:
|
|
213
274
|
await self._dispatch_to_router(child, event_type, event)
|
|
@@ -240,6 +301,63 @@ class Dispatcher(Generic[ClientT]):
|
|
|
240
301
|
|
|
241
302
|
return result
|
|
242
303
|
|
|
304
|
+
async def emit_error(
|
|
305
|
+
self,
|
|
306
|
+
exception: Exception,
|
|
307
|
+
event_type: EventType,
|
|
308
|
+
event: Any,
|
|
309
|
+
router: Router[ClientT],
|
|
310
|
+
handler: ErrorSource[ClientT] | None,
|
|
311
|
+
) -> bool:
|
|
312
|
+
client = self.client
|
|
313
|
+
handled = False
|
|
314
|
+
|
|
315
|
+
if client is None:
|
|
316
|
+
raise RuntimeError("client is not bound to dispatcher")
|
|
317
|
+
|
|
318
|
+
ctx = ErrorContext[ClientT](
|
|
319
|
+
client=client,
|
|
320
|
+
event_type=event_type,
|
|
321
|
+
event=event,
|
|
322
|
+
router=router,
|
|
323
|
+
handler=handler,
|
|
324
|
+
)
|
|
325
|
+
for entry in self.iter_error_handlers(router):
|
|
326
|
+
handled = True
|
|
327
|
+
try:
|
|
328
|
+
result = entry.callback(exception, ctx)
|
|
329
|
+
|
|
330
|
+
if inspect.isawaitable(result):
|
|
331
|
+
await result
|
|
332
|
+
except Exception as e:
|
|
333
|
+
logger.exception("Error while error handling: %s", e)
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
return handled
|
|
337
|
+
|
|
338
|
+
async def emit_disconnect(
|
|
339
|
+
self,
|
|
340
|
+
exception: Exception,
|
|
341
|
+
reconnect: bool,
|
|
342
|
+
delay: float,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Вызывает обработчики потери соединения.
|
|
345
|
+
|
|
346
|
+
Ошибки внутри disconnect-handler-ов логируются и не прерывают reconnect.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
if self.client is None:
|
|
350
|
+
raise RuntimeError("client is not bound to dispatcher")
|
|
351
|
+
|
|
352
|
+
for handler in self.iter_disconnect_handlers():
|
|
353
|
+
try:
|
|
354
|
+
result = handler(exception, reconnect, delay)
|
|
355
|
+
|
|
356
|
+
if inspect.isawaitable(result):
|
|
357
|
+
await result
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.exception("Error during disconnect handling: %s", e)
|
|
360
|
+
|
|
243
361
|
|
|
244
362
|
def _callback_name(callback: Any) -> str:
|
|
245
363
|
return getattr(
|
pymax/dispatch/enums.py
CHANGED
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,7 +11,8 @@ 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
|
|
@@ -22,8 +24,15 @@ if TYPE_CHECKING:
|
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
class ErrorScope(str, Enum):
|
|
28
|
+
"""Область действия error-handler-а."""
|
|
29
|
+
|
|
30
|
+
GLOBAL = "global"
|
|
31
|
+
LOCAL = "local"
|
|
32
|
+
|
|
33
|
+
|
|
25
34
|
_EventT = TypeVar("_EventT")
|
|
26
|
-
ClientT = TypeVar("ClientT")
|
|
35
|
+
ClientT = TypeVar("ClientT", bound="BaseClient")
|
|
27
36
|
|
|
28
37
|
HandlerCallback: TypeAlias = Callable[
|
|
29
38
|
[_EventT, ClientT],
|
|
@@ -47,12 +56,53 @@ StartDecorator: TypeAlias = Callable[
|
|
|
47
56
|
]
|
|
48
57
|
|
|
49
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
|
+
|
|
50
91
|
@dataclass(slots=True)
|
|
51
92
|
class HandlerEntry(Generic[_EventT, ClientT]):
|
|
52
93
|
callback: HandlerCallback[_EventT, ClientT]
|
|
53
94
|
filters: tuple[FilterCallback[_EventT], ...] = ()
|
|
54
95
|
|
|
55
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
|
+
|
|
56
106
|
class Router(Generic[ClientT]):
|
|
57
107
|
"""Контейнер обработчиков событий PyMax.
|
|
58
108
|
|
|
@@ -85,7 +135,39 @@ class Router(Generic[ClientT]):
|
|
|
85
135
|
] = defaultdict(list)
|
|
86
136
|
|
|
87
137
|
self.children: list[Router[ClientT]] = []
|
|
88
|
-
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
|
|
89
171
|
|
|
90
172
|
def on(
|
|
91
173
|
self,
|
|
@@ -145,7 +227,7 @@ class Router(Generic[ClientT]):
|
|
|
145
227
|
"""
|
|
146
228
|
|
|
147
229
|
def decorator(handler: StartCallback) -> StartCallback:
|
|
148
|
-
self.
|
|
230
|
+
self.on_start_handlers.append(handler)
|
|
149
231
|
return handler
|
|
150
232
|
|
|
151
233
|
return decorator
|
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
|
|
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
|
|
2
|
+
from pymax.api.messages.service import SendAttachments
|
|
3
3
|
from pymax.types import (
|
|
4
4
|
FileRequest,
|
|
5
5
|
Message,
|
|
@@ -62,6 +62,33 @@ class MessageMixin(IClientProtocol):
|
|
|
62
62
|
message_id=message_id,
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
+
async def forward_message(
|
|
66
|
+
self,
|
|
67
|
+
chat_id: int,
|
|
68
|
+
message_id: int | str,
|
|
69
|
+
source_chat_id: int | None = None,
|
|
70
|
+
*,
|
|
71
|
+
notify: bool = True,
|
|
72
|
+
) -> Message | None:
|
|
73
|
+
"""Пересылает существующее сообщение в чат.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
chat_id: ID целевого чата.
|
|
77
|
+
message_id: ID пересылаемого сообщения.
|
|
78
|
+
source_chat_id: ID исходного чата. Если не указан, используется
|
|
79
|
+
целевой чат.
|
|
80
|
+
notify: Отправить ли получателям push-уведомление.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Пересланное сообщение или ``None``, если сервер не вернул его.
|
|
84
|
+
"""
|
|
85
|
+
return await self._app.api.messages.forward_message(
|
|
86
|
+
chat_id=chat_id,
|
|
87
|
+
message_id=message_id,
|
|
88
|
+
source_chat_id=source_chat_id,
|
|
89
|
+
notify=notify,
|
|
90
|
+
)
|
|
91
|
+
|
|
65
92
|
async def get_messages(
|
|
66
93
|
self,
|
|
67
94
|
chat_id: int,
|
|
@@ -86,7 +113,6 @@ class MessageMixin(IClientProtocol):
|
|
|
86
113
|
chat_id: int,
|
|
87
114
|
message_id: int,
|
|
88
115
|
text: str,
|
|
89
|
-
attachment: SendAttachment | None = None,
|
|
90
116
|
attachments: SendAttachments = None,
|
|
91
117
|
) -> Message:
|
|
92
118
|
"""Редактирует текст и вложения сообщения.
|
|
@@ -95,9 +121,7 @@ class MessageMixin(IClientProtocol):
|
|
|
95
121
|
chat_id: ID чата.
|
|
96
122
|
message_id: ID сообщения.
|
|
97
123
|
text: Новый текст сообщения с поддержкой markdown.
|
|
98
|
-
|
|
99
|
-
attachments: Список новых вложений. Имеет приоритет над
|
|
100
|
-
``attachment``.
|
|
124
|
+
attachments: Новые файлы, фотографии или видео для сообщения.
|
|
101
125
|
|
|
102
126
|
Returns:
|
|
103
127
|
Отредактированное сообщение.
|
|
@@ -106,7 +130,6 @@ class MessageMixin(IClientProtocol):
|
|
|
106
130
|
chat_id=chat_id,
|
|
107
131
|
message_id=message_id,
|
|
108
132
|
text=text,
|
|
109
|
-
attachment=attachment,
|
|
110
133
|
attachments=attachments,
|
|
111
134
|
)
|
|
112
135
|
|
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
|
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
|
|
3
|
+
import zstandard
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
class Lz4BlockCompression:
|
|
2
7
|
def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
|
|
3
8
|
dst = bytearray()
|
|
@@ -95,3 +100,16 @@ class Lz4BlockCompression:
|
|
|
95
100
|
dst.extend(src[lit_start : lit_start + lit_len])
|
|
96
101
|
|
|
97
102
|
return bytes(dst)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ZstdCompression:
|
|
106
|
+
def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
|
|
107
|
+
try:
|
|
108
|
+
with zstandard.ZstdDecompressor().stream_reader(BytesIO(src)) as reader:
|
|
109
|
+
result = reader.read(max_output + 1)
|
|
110
|
+
except zstandard.ZstdError as e:
|
|
111
|
+
raise ValueError("Zstd: failed to decompress payload") from e
|
|
112
|
+
|
|
113
|
+
if len(result) > max_output:
|
|
114
|
+
raise ValueError("Zstd: output too large")
|
|
115
|
+
return result
|
pymax/protocol/tcp/payload.py
CHANGED
|
@@ -5,7 +5,7 @@ import msgpack
|
|
|
5
5
|
|
|
6
6
|
from pymax.logging import get_logger
|
|
7
7
|
|
|
8
|
-
from .compression import Lz4BlockCompression
|
|
8
|
+
from .compression import Lz4BlockCompression, ZstdCompression
|
|
9
9
|
|
|
10
10
|
logger = get_logger(__name__)
|
|
11
11
|
|
|
@@ -70,9 +70,11 @@ class TcpPayloadDecoder:
|
|
|
70
70
|
*,
|
|
71
71
|
serializer: MsgpackPayloadCodec,
|
|
72
72
|
compression: Lz4BlockCompression | None = None,
|
|
73
|
+
zstd_compression: ZstdCompression | None = None,
|
|
73
74
|
) -> None:
|
|
74
75
|
self.serializer = serializer
|
|
75
76
|
self.compression = compression
|
|
77
|
+
self.zstd_compression = zstd_compression
|
|
76
78
|
|
|
77
79
|
def _normalize_keys(self, obj: Any) -> Any:
|
|
78
80
|
if isinstance(obj, dict):
|
|
@@ -97,12 +99,26 @@ class TcpPayloadDecoder:
|
|
|
97
99
|
if not payload_bytes:
|
|
98
100
|
return {}
|
|
99
101
|
|
|
100
|
-
if flags
|
|
102
|
+
if flags == 0xFF:
|
|
103
|
+
if self.zstd_compression is None:
|
|
104
|
+
raise ValueError("Zstd-compressed TCP payload without a decoder")
|
|
105
|
+
try:
|
|
106
|
+
payload_bytes = self.zstd_compression.decompress(payload_bytes)
|
|
107
|
+
logger.debug("tcp payload decompressed with Zstd")
|
|
108
|
+
except ValueError:
|
|
109
|
+
logger.debug("tcp Zstd payload decompression failed", exc_info=True)
|
|
110
|
+
raise
|
|
111
|
+
elif flags > 0x7F:
|
|
112
|
+
raise ValueError(f"invalid TCP compression factor: {flags}")
|
|
113
|
+
elif flags > 0:
|
|
114
|
+
if self.compression is None:
|
|
115
|
+
raise ValueError("LZ4-compressed TCP payload without a decoder")
|
|
101
116
|
try:
|
|
102
117
|
payload_bytes = self.compression.decompress(payload_bytes)
|
|
103
|
-
logger.debug("tcp payload decompressed
|
|
118
|
+
logger.debug("tcp payload decompressed cof=%s", flags)
|
|
104
119
|
except ValueError:
|
|
105
|
-
logger.debug("tcp payload
|
|
120
|
+
logger.debug("tcp payload decompression failed cof=%s", flags, exc_info=True)
|
|
121
|
+
raise
|
|
106
122
|
|
|
107
123
|
result = self.serializer.decode(payload_bytes)
|
|
108
124
|
return self._normalize_keys(result)
|
pymax/protocol/tcp/protocol.py
CHANGED
|
@@ -7,6 +7,7 @@ from .payload import (
|
|
|
7
7
|
Lz4BlockCompression,
|
|
8
8
|
MsgpackPayloadCodec,
|
|
9
9
|
TcpPayloadDecoder,
|
|
10
|
+
ZstdCompression,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
logger = get_logger(__name__)
|
|
@@ -20,8 +21,11 @@ class TcpProtocol(BaseProtocol):
|
|
|
20
21
|
self.framer = TcpPacketFramer()
|
|
21
22
|
self.serializer = MsgpackPayloadCodec()
|
|
22
23
|
self.compression = Lz4BlockCompression()
|
|
24
|
+
self.zstd_compression = ZstdCompression()
|
|
23
25
|
self.payload_decoder = TcpPayloadDecoder(
|
|
24
|
-
serializer=self.serializer,
|
|
26
|
+
serializer=self.serializer,
|
|
27
|
+
compression=self.compression,
|
|
28
|
+
zstd_compression=self.zstd_compression,
|
|
25
29
|
)
|
|
26
30
|
|
|
27
31
|
def encode(self, frame: OutboundFrame) -> bytes:
|
pymax/session/store.py
CHANGED
|
@@ -194,6 +194,17 @@ class SessionStore:
|
|
|
194
194
|
await conn.commit()
|
|
195
195
|
logger.info("session deleted")
|
|
196
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
|
+
|
|
197
208
|
async def update_token(self, old_token: str, new_token: str) -> None:
|
|
198
209
|
conn = await self._get_connection()
|
|
199
210
|
logger.debug(
|
pymax/types/domain/__init__.py
CHANGED
pymax/types/domain/chat.py
CHANGED
|
@@ -20,7 +20,7 @@ class Chat(CamelModel):
|
|
|
20
20
|
Объекты чатов, полученные через клиент, обычно уже привязаны к сервисам
|
|
21
21
|
сообщений и чатов. После этого можно вызывать удобные методы объекта:
|
|
22
22
|
:meth:`answer`, :meth:`history`, :meth:`get_message`,
|
|
23
|
-
:meth:`get_messages`, :meth:`leave`, :meth:`invite`,
|
|
23
|
+
:meth:`get_messages`, :meth:`leave`, :meth:`delete`, :meth:`invite`,
|
|
24
24
|
:meth:`remove_users`, :meth:`pin_message`, :meth:`update_settings` и
|
|
25
25
|
:meth:`rework_invite_link`.
|
|
26
26
|
|
|
@@ -305,6 +305,26 @@ class Chat(CamelModel):
|
|
|
305
305
|
|
|
306
306
|
raise ValueError("Unknown chat type=%s", self.type)
|
|
307
307
|
|
|
308
|
+
async def delete(self, *, for_all: bool = True) -> None:
|
|
309
|
+
"""Удаляет этот чат.
|
|
310
|
+
|
|
311
|
+
Для ``last_event_time`` используется значение ``Chat.last_event_time``.
|
|
312
|
+
|
|
313
|
+
:param for_all: Удалить чат для всех участников, если сервер
|
|
314
|
+
поддерживает такой режим.
|
|
315
|
+
:type for_all: bool
|
|
316
|
+
:returns: ``None``.
|
|
317
|
+
:rtype: None
|
|
318
|
+
:raises RuntimeError: Если чат не привязан к клиенту.
|
|
319
|
+
"""
|
|
320
|
+
_, chat_actions = self._bound()
|
|
321
|
+
|
|
322
|
+
return await chat_actions.delete_chat(
|
|
323
|
+
self.id,
|
|
324
|
+
last_event_time=self.last_event_time,
|
|
325
|
+
for_all=for_all,
|
|
326
|
+
)
|
|
327
|
+
|
|
308
328
|
async def invite(
|
|
309
329
|
self,
|
|
310
330
|
user_ids: list[int],
|