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/config.py
CHANGED
|
@@ -84,6 +84,21 @@ class DeviceConfig(BaseModel):
|
|
|
84
84
|
client_session_id: int = Field(default_factory=lambda: randint(1, 70))
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
class RegistrationConfig(BaseModel):
|
|
88
|
+
"""Данные профиля для регистрации нового аккаунта по SMS.
|
|
89
|
+
|
|
90
|
+
Передайте объект через ``ExtraConfig.registration_config``. Он используется
|
|
91
|
+
только если после подтверждения SMS-кода Max вернул токен регистрации.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
first_name: Имя нового пользователя.
|
|
95
|
+
last_name: Фамилия нового пользователя.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
first_name: str
|
|
99
|
+
last_name: str | None = None
|
|
100
|
+
|
|
101
|
+
|
|
87
102
|
class ClientConfig(BaseModel):
|
|
88
103
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
89
104
|
|
|
@@ -93,6 +108,7 @@ class ClientConfig(BaseModel):
|
|
|
93
108
|
device: DeviceConfig
|
|
94
109
|
token: str | None = None
|
|
95
110
|
proxy: str | None = None
|
|
111
|
+
registration_config: RegistrationConfig | None = None
|
|
96
112
|
|
|
97
113
|
host: str = "api.oneme.ru"
|
|
98
114
|
port: int = 443
|
|
@@ -109,9 +125,7 @@ class ClientConfig(BaseModel):
|
|
|
109
125
|
|
|
110
126
|
def ensure_config(self) -> None:
|
|
111
127
|
if not self.phone:
|
|
112
|
-
raise ValueError(
|
|
113
|
-
"Phone must be provided when no saved session exists."
|
|
114
|
-
)
|
|
128
|
+
raise ValueError("Phone must be provided when no saved session exists.")
|
|
115
129
|
|
|
116
130
|
|
|
117
131
|
class ExtraConfig(BaseModel):
|
|
@@ -122,6 +136,8 @@ class ExtraConfig(BaseModel):
|
|
|
122
136
|
|
|
123
137
|
Args:
|
|
124
138
|
token: Готовый token для создания сессии без SMS/QR.
|
|
139
|
+
registration_config: Имя и фамилия для автоматического завершения
|
|
140
|
+
регистрации нового аккаунта по SMS.
|
|
125
141
|
host: TCP host Max API.
|
|
126
142
|
port: TCP port Max API.
|
|
127
143
|
url: WebSocket URL для ``WebClient``.
|
|
@@ -156,6 +172,7 @@ class ExtraConfig(BaseModel):
|
|
|
156
172
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
157
173
|
|
|
158
174
|
token: str | None = None
|
|
175
|
+
registration_config: RegistrationConfig | None = None
|
|
159
176
|
|
|
160
177
|
host: str = "api.oneme.ru"
|
|
161
178
|
port: int = 443
|
|
@@ -221,3 +238,25 @@ class ExtraConfig(BaseModel):
|
|
|
221
238
|
device_locale=locale,
|
|
222
239
|
header_user_agent=DEFAULT_WEB_HEADER_USER_AGENT,
|
|
223
240
|
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ignore. for future upd
|
|
244
|
+
|
|
245
|
+
# class TcpOptions(BaseModel):
|
|
246
|
+
# host: str = "api.oneme.ru"
|
|
247
|
+
# port: int = 443
|
|
248
|
+
# use_ssl: bool = True
|
|
249
|
+
# proxy: str | None = None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# class RuntimeOptions(BaseModel):
|
|
253
|
+
# request_timeout: float = 30.0
|
|
254
|
+
# reconnect: bool = True
|
|
255
|
+
# reconnect_delay: float = 1.0
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# class DeviceOptions(BaseModel):
|
|
259
|
+
# device_id: str | None = None
|
|
260
|
+
# device_type: DeviceType = DeviceType.ANDROID
|
|
261
|
+
# user_agent: MobileUserAgentPayload | None = None
|
|
262
|
+
# mt_instance_id: str = Field(default_factory=lambda: str(uuid4()))
|
pymax/connection/connection.py
CHANGED
|
@@ -183,7 +183,9 @@ class ConnectionManager:
|
|
|
183
183
|
except Exception as e:
|
|
184
184
|
exc = ConnectionError(f"Connection error: {e}")
|
|
185
185
|
logger.exception("connection receive loop failed")
|
|
186
|
+
|
|
186
187
|
self.requests.cancel_all(exc=exc)
|
|
188
|
+
|
|
187
189
|
self._connection_lost = True
|
|
188
190
|
self._mark_closed(exc)
|
|
189
191
|
raise e
|
pymax/connection/readers/tcp.py
CHANGED
|
@@ -8,9 +8,7 @@ logger = get_logger(__name__)
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class TCPReader(BaseReader):
|
|
11
|
-
def __init__(
|
|
12
|
-
self, transport: TCPTransport, framer: TcpPacketFramer
|
|
13
|
-
) -> None:
|
|
11
|
+
def __init__(self, transport: TCPTransport, framer: TcpPacketFramer) -> None:
|
|
14
12
|
super().__init__()
|
|
15
13
|
self.transport = transport
|
|
16
14
|
self.framer = framer
|
pymax/dispatch/__init__.py
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
from .dispatcher import Dispatcher
|
|
2
2
|
from .enums import EventType
|
|
3
|
-
from .router import
|
|
3
|
+
from .router import (
|
|
4
|
+
ClientRouter,
|
|
5
|
+
DisconnectCallback,
|
|
6
|
+
DisconnectDecorator,
|
|
7
|
+
ErrorContext,
|
|
8
|
+
ErrorScope,
|
|
9
|
+
Router,
|
|
10
|
+
)
|
|
4
11
|
|
|
5
12
|
__all__ = (
|
|
6
13
|
"ClientRouter",
|
|
14
|
+
"DisconnectCallback",
|
|
15
|
+
"DisconnectDecorator",
|
|
7
16
|
"Dispatcher",
|
|
17
|
+
"ErrorContext",
|
|
18
|
+
"ErrorScope",
|
|
8
19
|
"EventType",
|
|
9
20
|
"Router",
|
|
10
21
|
)
|
pymax/dispatch/dispatcher.py
CHANGED
|
@@ -9,15 +9,29 @@ 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
|
|
15
21
|
from .router import (
|
|
22
|
+
DisconnectCallback,
|
|
23
|
+
DisconnectDecorator,
|
|
24
|
+
ErrorContext,
|
|
25
|
+
ErrorDecorator,
|
|
26
|
+
ErrorEntry,
|
|
27
|
+
ErrorScope,
|
|
28
|
+
ErrorSource,
|
|
16
29
|
FilterCallback,
|
|
17
30
|
HandlerCallback,
|
|
18
31
|
HandlerDecorator,
|
|
19
32
|
HandlerEntry,
|
|
20
33
|
Router,
|
|
34
|
+
StartCallback,
|
|
21
35
|
StartDecorator,
|
|
22
36
|
)
|
|
23
37
|
|
|
@@ -25,17 +39,16 @@ if TYPE_CHECKING:
|
|
|
25
39
|
from collections.abc import Generator
|
|
26
40
|
|
|
27
41
|
from pymax.app import App
|
|
42
|
+
from pymax.base import BaseClient
|
|
28
43
|
|
|
29
44
|
|
|
30
45
|
logger = get_logger(__name__)
|
|
31
46
|
|
|
32
|
-
ClientT = TypeVar("ClientT")
|
|
47
|
+
ClientT = TypeVar("ClientT", bound="BaseClient")
|
|
33
48
|
|
|
34
49
|
|
|
35
50
|
class Dispatcher(Generic[ClientT]):
|
|
36
|
-
def __init__(
|
|
37
|
-
self, app: App, root_router: Router[ClientT] | None = None
|
|
38
|
-
) -> None:
|
|
51
|
+
def __init__(self, app: App, root_router: Router[ClientT] | None = None) -> None:
|
|
39
52
|
self.root_router: Router[ClientT] = root_router or Router()
|
|
40
53
|
self.internal_router: Router[ClientT] = Router()
|
|
41
54
|
self.resolver = EventResolver()
|
|
@@ -71,11 +84,16 @@ class Dispatcher(Generic[ClientT]):
|
|
|
71
84
|
event: EventType,
|
|
72
85
|
*filters: FilterCallback[Any],
|
|
73
86
|
) -> HandlerDecorator[Any, ClientT]:
|
|
74
|
-
logger.debug(
|
|
75
|
-
"registering handler event=%s filters=%s", event, len(filters)
|
|
76
|
-
)
|
|
87
|
+
logger.debug("registering handler event=%s filters=%s", event, len(filters))
|
|
77
88
|
return self.root_router.on(event, *filters)
|
|
78
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
|
+
|
|
79
97
|
def on_message(
|
|
80
98
|
self,
|
|
81
99
|
*filters: FilterCallback[Message],
|
|
@@ -87,9 +105,7 @@ class Dispatcher(Generic[ClientT]):
|
|
|
87
105
|
self,
|
|
88
106
|
*filters: FilterCallback[Message],
|
|
89
107
|
) -> HandlerDecorator[Message, ClientT]:
|
|
90
|
-
logger.debug(
|
|
91
|
-
"registering message edit handler filters=%s", len(filters)
|
|
92
|
-
)
|
|
108
|
+
logger.debug("registering message edit handler filters=%s", len(filters))
|
|
93
109
|
return self.root_router.on_message_edit(*filters)
|
|
94
110
|
|
|
95
111
|
def on_message_delete(
|
|
@@ -98,6 +114,30 @@ class Dispatcher(Generic[ClientT]):
|
|
|
98
114
|
) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
|
|
99
115
|
return self.root_router.on_message_delete(*filters)
|
|
100
116
|
|
|
117
|
+
def on_message_read(
|
|
118
|
+
self,
|
|
119
|
+
*filters: FilterCallback[MessageReadEvent],
|
|
120
|
+
) -> HandlerDecorator[MessageReadEvent, ClientT]:
|
|
121
|
+
return self.root_router.on_message_read(*filters)
|
|
122
|
+
|
|
123
|
+
def on_typing(
|
|
124
|
+
self,
|
|
125
|
+
*filters: FilterCallback[TypingEvent],
|
|
126
|
+
) -> HandlerDecorator[TypingEvent, ClientT]:
|
|
127
|
+
return self.root_router.on_typing(*filters)
|
|
128
|
+
|
|
129
|
+
def on_presence(
|
|
130
|
+
self,
|
|
131
|
+
*filters: FilterCallback[PresenceEvent],
|
|
132
|
+
) -> HandlerDecorator[PresenceEvent, ClientT]:
|
|
133
|
+
return self.root_router.on_presence(*filters)
|
|
134
|
+
|
|
135
|
+
def on_reaction_update(
|
|
136
|
+
self,
|
|
137
|
+
*filters: FilterCallback[ReactionUpdateEvent],
|
|
138
|
+
) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
|
|
139
|
+
return self.root_router.on_reaction_update(*filters)
|
|
140
|
+
|
|
101
141
|
def on_chat_update(
|
|
102
142
|
self,
|
|
103
143
|
*filters: FilterCallback[Chat],
|
|
@@ -116,30 +156,68 @@ class Dispatcher(Generic[ClientT]):
|
|
|
116
156
|
def iter_routers(self) -> Generator[Router[ClientT], Any, None]:
|
|
117
157
|
yield from self._iter_router(self.root_router)
|
|
118
158
|
|
|
119
|
-
def _iter_router(
|
|
120
|
-
self, router: Router[ClientT]
|
|
121
|
-
) -> Generator[Router[ClientT], Any, None]:
|
|
159
|
+
def _iter_router(self, router: Router[ClientT]) -> Generator[Router[ClientT], Any, None]:
|
|
122
160
|
yield router
|
|
123
161
|
|
|
124
162
|
for child in router.children:
|
|
125
163
|
yield from self._iter_router(child)
|
|
126
164
|
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
129
171
|
|
|
172
|
+
def iter_disconnect_handlers(self) -> Generator[DisconnectCallback, Any, None]:
|
|
173
|
+
"""Итерирует обработчики disconnect по root router и его детям."""
|
|
130
174
|
for router in self.iter_routers():
|
|
131
|
-
|
|
132
|
-
|
|
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:
|
|
133
183
|
continue
|
|
134
184
|
|
|
135
|
-
|
|
185
|
+
yield entry
|
|
136
186
|
|
|
137
|
-
|
|
138
|
-
|
|
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))
|
|
139
193
|
task.add_done_callback(_log_task_error)
|
|
140
194
|
tasks.append(task)
|
|
141
195
|
|
|
142
|
-
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
|
|
143
221
|
|
|
144
222
|
async def stop_startup_tasks(self) -> None:
|
|
145
223
|
if not self.startup_tasks:
|
|
@@ -161,9 +239,7 @@ class Dispatcher(Generic[ClientT]):
|
|
|
161
239
|
if event_type is not None:
|
|
162
240
|
logger.debug("dispatching event type=%s", event_type)
|
|
163
241
|
event = self.mapper.map(event_type, frame)
|
|
164
|
-
await self._dispatch_to_router(
|
|
165
|
-
self.internal_router, event_type, event
|
|
166
|
-
)
|
|
242
|
+
await self._dispatch_to_router(self.internal_router, event_type, event)
|
|
167
243
|
await self._dispatch_to_router(self.root_router, event_type, event)
|
|
168
244
|
else:
|
|
169
245
|
logger.debug(
|
|
@@ -181,13 +257,18 @@ class Dispatcher(Generic[ClientT]):
|
|
|
181
257
|
event: Any,
|
|
182
258
|
) -> None:
|
|
183
259
|
for entry in router.handlers.get(event_type, []):
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
191
272
|
|
|
192
273
|
for child in router.children:
|
|
193
274
|
await self._dispatch_to_router(child, event_type, event)
|
|
@@ -209,9 +290,7 @@ class Dispatcher(Generic[ClientT]):
|
|
|
209
290
|
return False
|
|
210
291
|
return True
|
|
211
292
|
|
|
212
|
-
async def _call(
|
|
213
|
-
self, callback: HandlerCallback[Any, ClientT], event: Any
|
|
214
|
-
) -> Any:
|
|
293
|
+
async def _call(self, callback: HandlerCallback[Any, ClientT], event: Any) -> Any:
|
|
215
294
|
if self.client is None:
|
|
216
295
|
raise RuntimeError("client is not bound")
|
|
217
296
|
|
|
@@ -222,6 +301,63 @@ class Dispatcher(Generic[ClientT]):
|
|
|
222
301
|
|
|
223
302
|
return result
|
|
224
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
|
+
|
|
225
361
|
|
|
226
362
|
def _callback_name(callback: Any) -> str:
|
|
227
363
|
return getattr(
|
pymax/dispatch/enums.py
CHANGED
|
@@ -5,8 +5,13 @@ 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"
|
|
11
15
|
FILE_READY = "file_ready"
|
|
12
16
|
RAW = "raw"
|
|
17
|
+
ON_START = "on_start"
|
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
|
|
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
|
|
62
|
-
self.app
|
|
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
|
|
66
|
-
self.app
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
self.app.api.chats,
|
|
87
|
+
return bind_api_model(
|
|
88
|
+
self.app,
|
|
89
|
+
MessageDeleteEvent.model_validate(frame.payload),
|
|
74
90
|
)
|
|
75
|
-
|
|
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:
|
pymax/dispatch/resolvers.py
CHANGED
|
@@ -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:
|