maxapi-python 1.2.4__py3-none-any.whl → 2.0.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.0.0.dist-info/METADATA +217 -0
- maxapi_python-2.0.0.dist-info/RECORD +140 -0
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +50 -105
- pymax/api/__init__.py +17 -0
- pymax/api/auth/__init__.py +1 -0
- pymax/api/auth/enums.py +17 -0
- pymax/api/auth/payloads.py +129 -0
- pymax/api/auth/service.py +313 -0
- pymax/api/auth/types.py +13 -0
- pymax/api/chats/__init__.py +8 -0
- pymax/api/chats/enums.py +27 -0
- pymax/api/chats/payloads.py +103 -0
- pymax/api/chats/service.py +277 -0
- pymax/api/facade.py +32 -0
- pymax/api/messages/__init__.py +1 -0
- pymax/api/messages/enums.py +17 -0
- pymax/api/messages/payloads.py +92 -0
- pymax/api/messages/service.py +337 -0
- pymax/api/models.py +13 -0
- pymax/api/response.py +123 -0
- pymax/api/self/__init__.py +2 -0
- pymax/api/self/enums.py +11 -0
- pymax/api/self/payloads.py +41 -0
- pymax/api/self/service.py +142 -0
- pymax/api/session/__init__.py +1 -0
- pymax/api/session/enums.py +10 -0
- pymax/api/session/payloads.py +76 -0
- pymax/api/session/service.py +72 -0
- pymax/api/uploads/__init__.py +1 -0
- pymax/api/uploads/models.py +49 -0
- pymax/api/uploads/payloads.py +25 -0
- pymax/api/uploads/service.py +458 -0
- pymax/api/users/__init__.py +2 -0
- pymax/api/users/enums.py +12 -0
- pymax/api/users/payloads.py +16 -0
- pymax/api/users/service.py +124 -0
- pymax/app.py +273 -0
- pymax/auth/__init__.py +25 -0
- pymax/auth/base.py +37 -0
- pymax/auth/email.py +0 -0
- pymax/auth/models.py +5 -0
- pymax/auth/providers.py +127 -0
- pymax/auth/qr.py +135 -0
- pymax/auth/service.py +25 -0
- pymax/auth/sms.py +122 -0
- pymax/base.py +204 -0
- pymax/client.py +106 -0
- pymax/client_web.py +83 -0
- pymax/config.py +215 -0
- pymax/connection/__init__.py +1 -0
- pymax/connection/connection.py +205 -0
- pymax/connection/pending.py +46 -0
- pymax/connection/readers/__init__.py +2 -0
- pymax/connection/readers/base.py +6 -0
- pymax/connection/readers/tcp.py +29 -0
- pymax/connection/readers/ws.py +14 -0
- pymax/dispatch/__init__.py +10 -0
- pymax/dispatch/dispatcher.py +222 -0
- pymax/dispatch/enums.py +12 -0
- pymax/dispatch/mapping.py +73 -0
- pymax/dispatch/resolvers.py +52 -0
- pymax/dispatch/router.py +216 -0
- pymax/exceptions.py +22 -89
- pymax/files/__init__.py +9 -0
- pymax/files/base.py +82 -0
- pymax/files/file.py +76 -0
- pymax/files/photo.py +108 -0
- pymax/files/static.py +10 -0
- pymax/files/video.py +74 -0
- pymax/formatting/__init__.py +0 -0
- pymax/formatting/markdown.py +217 -0
- pymax/infra/__init__.py +1 -0
- pymax/infra/auth.py +55 -0
- pymax/infra/base.py +15 -0
- pymax/infra/chat.py +240 -0
- pymax/infra/message.py +252 -0
- pymax/infra/protocol.py +9 -0
- pymax/infra/self.py +139 -0
- pymax/infra/user.py +107 -0
- pymax/logging.py +129 -0
- pymax/protocol/__init__.py +11 -0
- pymax/protocol/base.py +13 -0
- pymax/{static/enum.py → protocol/enums.py} +36 -79
- pymax/protocol/models.py +33 -0
- pymax/protocol/tcp/__init__.py +1 -0
- pymax/protocol/tcp/compression.py +97 -0
- pymax/protocol/tcp/framing.py +68 -0
- pymax/protocol/tcp/payload.py +127 -0
- pymax/protocol/tcp/protocol.py +68 -0
- pymax/protocol/ws/__init__.py +1 -0
- pymax/protocol/ws/protocol.py +27 -0
- pymax/py.typed +0 -0
- pymax/routers.py +8 -0
- pymax/session/__init__.py +3 -0
- pymax/session/models.py +11 -0
- pymax/session/protocol.py +14 -0
- pymax/session/store.py +232 -0
- pymax/telemetry/__init__.py +3 -0
- pymax/telemetry/navigation.py +181 -0
- pymax/telemetry/payloads.py +142 -0
- pymax/telemetry/service.py +225 -0
- pymax/transport/__init__.py +0 -0
- pymax/transport/base.py +14 -0
- pymax/transport/tcp.py +93 -0
- pymax/transport/websocket.py +50 -0
- pymax/types/__init__.py +2 -0
- pymax/types/domain/__init__.py +11 -0
- pymax/types/domain/attachments/__init__.py +11 -0
- pymax/types/domain/attachments/audio.py +35 -0
- pymax/types/domain/attachments/call.py +26 -0
- pymax/types/domain/attachments/contact.py +32 -0
- pymax/types/domain/attachments/control.py +20 -0
- pymax/types/domain/attachments/enums.py +27 -0
- pymax/types/domain/attachments/file.py +56 -0
- pymax/types/domain/attachments/keyboards/__init__.py +1 -0
- pymax/types/domain/attachments/keyboards/inline.py +19 -0
- pymax/types/domain/attachments/photo.py +45 -0
- pymax/types/domain/attachments/share.py +29 -0
- pymax/types/domain/attachments/sticker.py +50 -0
- pymax/types/domain/attachments/video.py +90 -0
- pymax/types/domain/auth.py +161 -0
- pymax/types/domain/base.py +17 -0
- pymax/types/domain/chat.py +426 -0
- pymax/types/domain/element.py +24 -0
- pymax/types/domain/enums.py +24 -0
- pymax/types/domain/error.py +20 -0
- pymax/types/domain/folder.py +74 -0
- pymax/types/domain/login.py +35 -0
- pymax/types/domain/message.py +378 -0
- pymax/types/domain/name.py +20 -0
- pymax/types/domain/profile.py +15 -0
- pymax/types/domain/session.py +52 -0
- pymax/types/domain/sync.py +80 -0
- pymax/types/domain/user.py +117 -0
- pymax/types/events/__init__.py +3 -0
- pymax/types/events/file.py +5 -0
- pymax/types/events/message.py +37 -0
- pymax/types/events/video.py +5 -0
- maxapi_python-1.2.4.dist-info/METADATA +0 -205
- maxapi_python-1.2.4.dist-info/RECORD +0 -33
- pymax/core.py +0 -390
- pymax/crud.py +0 -96
- pymax/files.py +0 -138
- pymax/filters.py +0 -164
- pymax/formatter.py +0 -31
- pymax/formatting.py +0 -74
- pymax/interfaces.py +0 -552
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -368
- pymax/mixins/channel.py +0 -130
- pymax/mixins/group.py +0 -458
- pymax/mixins/handler.py +0 -285
- pymax/mixins/message.py +0 -879
- pymax/mixins/scheduler.py +0 -28
- pymax/mixins/self.py +0 -259
- pymax/mixins/socket.py +0 -297
- pymax/mixins/telemetry.py +0 -112
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -142
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -367
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -89
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from pymax.logging import get_logger
|
|
9
|
+
from pymax.protocol import InboundFrame
|
|
10
|
+
from pymax.types import Chat, MessageDeleteEvent
|
|
11
|
+
from pymax.types.domain import Message
|
|
12
|
+
from pymax.types.events import FileUploadSignal, VideoUploadSignal
|
|
13
|
+
|
|
14
|
+
from .enums import EventType
|
|
15
|
+
from .mapping import EventMapper, EventResolver
|
|
16
|
+
from .router import (
|
|
17
|
+
FilterCallback,
|
|
18
|
+
HandlerCallback,
|
|
19
|
+
HandlerDecorator,
|
|
20
|
+
HandlerEntry,
|
|
21
|
+
Router,
|
|
22
|
+
StartDecorator,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Generator
|
|
27
|
+
|
|
28
|
+
from pymax.app import App
|
|
29
|
+
from pymax.client import Client
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
ClientT = TypeVar("ClientT")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Dispatcher(Generic[ClientT]):
|
|
38
|
+
def __init__(self, app: App, root_router: Router[ClientT] | None = None) -> None:
|
|
39
|
+
self.root_router: Router[ClientT] = root_router or Router()
|
|
40
|
+
self.internal_router: Router[ClientT] = Router()
|
|
41
|
+
self.resolver = EventResolver()
|
|
42
|
+
self.mapper = EventMapper(app)
|
|
43
|
+
self.startup_tasks: list[asyncio.Task[Any]] = []
|
|
44
|
+
self.client: ClientT | None = None
|
|
45
|
+
|
|
46
|
+
def bind_client(self, client: ClientT) -> None:
|
|
47
|
+
self.client = client
|
|
48
|
+
|
|
49
|
+
def include_router(self, router: Router[ClientT]) -> None:
|
|
50
|
+
self.root_router.include_router(router)
|
|
51
|
+
logger.debug(
|
|
52
|
+
"router included handlers=%s children=%s",
|
|
53
|
+
len(router.handlers),
|
|
54
|
+
len(router.children),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def on_internal(
|
|
58
|
+
self,
|
|
59
|
+
event: EventType,
|
|
60
|
+
*filters: FilterCallback[Any],
|
|
61
|
+
) -> HandlerDecorator[Any, ClientT]:
|
|
62
|
+
logger.debug("registering internal handler event=%s filters=%s", event, len(filters))
|
|
63
|
+
return self.internal_router.on(event, *filters)
|
|
64
|
+
|
|
65
|
+
def on(
|
|
66
|
+
self,
|
|
67
|
+
event: EventType,
|
|
68
|
+
*filters: FilterCallback[Any],
|
|
69
|
+
) -> HandlerDecorator[Any, ClientT]:
|
|
70
|
+
logger.debug("registering handler event=%s filters=%s", event, len(filters))
|
|
71
|
+
return self.root_router.on(event, *filters)
|
|
72
|
+
|
|
73
|
+
def on_message(
|
|
74
|
+
self,
|
|
75
|
+
*filters: FilterCallback[Message],
|
|
76
|
+
) -> HandlerDecorator[Message, ClientT]:
|
|
77
|
+
logger.debug("registering message handler filters=%s", len(filters))
|
|
78
|
+
return self.root_router.on_message(*filters)
|
|
79
|
+
|
|
80
|
+
def on_message_edit(
|
|
81
|
+
self,
|
|
82
|
+
*filters: FilterCallback[Message],
|
|
83
|
+
) -> HandlerDecorator[Message, ClientT]:
|
|
84
|
+
logger.debug("registering message edit handler filters=%s", len(filters))
|
|
85
|
+
return self.root_router.on_message_edit(*filters)
|
|
86
|
+
|
|
87
|
+
def on_message_delete(
|
|
88
|
+
self,
|
|
89
|
+
*filters: FilterCallback[MessageDeleteEvent],
|
|
90
|
+
) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
|
|
91
|
+
return self.root_router.on_message_delete(*filters)
|
|
92
|
+
|
|
93
|
+
def on_chat_update(
|
|
94
|
+
self,
|
|
95
|
+
*filters: FilterCallback[Chat],
|
|
96
|
+
) -> HandlerDecorator[Chat, ClientT]:
|
|
97
|
+
return self.root_router.on_chat_update(*filters)
|
|
98
|
+
|
|
99
|
+
def on_raw(
|
|
100
|
+
self,
|
|
101
|
+
*filters: FilterCallback[InboundFrame],
|
|
102
|
+
) -> HandlerDecorator[InboundFrame, ClientT]:
|
|
103
|
+
return self.root_router.on_raw(*filters)
|
|
104
|
+
|
|
105
|
+
def on_start(self) -> StartDecorator[ClientT]:
|
|
106
|
+
return self.root_router.on_start()
|
|
107
|
+
|
|
108
|
+
def iter_routers(self) -> Generator[Router[ClientT], Any, None]:
|
|
109
|
+
yield from self._iter_router(self.root_router)
|
|
110
|
+
|
|
111
|
+
def _iter_router(self, router: Router[ClientT]) -> Generator[Router[ClientT], Any, None]:
|
|
112
|
+
yield router
|
|
113
|
+
|
|
114
|
+
for child in router.children:
|
|
115
|
+
yield from self._iter_router(child)
|
|
116
|
+
|
|
117
|
+
async def emit_start(self, client: ClientT) -> None:
|
|
118
|
+
tasks: list[asyncio.Task[Any]] = []
|
|
119
|
+
|
|
120
|
+
for router in self.iter_routers():
|
|
121
|
+
handler = router.on_start_handler
|
|
122
|
+
if handler is None:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
result = handler(client)
|
|
126
|
+
|
|
127
|
+
if inspect.iscoroutine(result):
|
|
128
|
+
task = asyncio.create_task(result)
|
|
129
|
+
task.add_done_callback(_log_task_error)
|
|
130
|
+
tasks.append(task)
|
|
131
|
+
|
|
132
|
+
self.startup_tasks = tasks
|
|
133
|
+
|
|
134
|
+
async def stop_startup_tasks(self) -> None:
|
|
135
|
+
if not self.startup_tasks:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
for task in self.startup_tasks:
|
|
139
|
+
if not task.done():
|
|
140
|
+
task.cancel()
|
|
141
|
+
|
|
142
|
+
for task in self.startup_tasks:
|
|
143
|
+
with suppress(asyncio.CancelledError, Exception):
|
|
144
|
+
await task
|
|
145
|
+
|
|
146
|
+
self.startup_tasks = []
|
|
147
|
+
|
|
148
|
+
async def dispatch(self, frame: InboundFrame) -> None:
|
|
149
|
+
event_type = self.resolver.resolve(frame)
|
|
150
|
+
|
|
151
|
+
if event_type is not None:
|
|
152
|
+
logger.debug("dispatching event type=%s", event_type)
|
|
153
|
+
event = self.mapper.map(event_type, frame)
|
|
154
|
+
await self._dispatch_to_router(self.internal_router, event_type, event)
|
|
155
|
+
await self._dispatch_to_router(self.root_router, event_type, event)
|
|
156
|
+
else:
|
|
157
|
+
logger.debug("dispatching raw event only opcode=%s cmd=%s", frame.opcode, frame.cmd)
|
|
158
|
+
|
|
159
|
+
await self._dispatch_to_router(self.root_router, EventType.RAW, frame)
|
|
160
|
+
|
|
161
|
+
async def _dispatch_to_router(
|
|
162
|
+
self,
|
|
163
|
+
router: Router[ClientT],
|
|
164
|
+
event_type: EventType,
|
|
165
|
+
event: Any,
|
|
166
|
+
) -> None:
|
|
167
|
+
for entry in router.handlers.get(event_type, []):
|
|
168
|
+
if await self._matches(entry, event):
|
|
169
|
+
logger.debug(
|
|
170
|
+
"calling handler event=%s callback=%s",
|
|
171
|
+
event_type,
|
|
172
|
+
_callback_name(entry.callback),
|
|
173
|
+
)
|
|
174
|
+
await self._call(entry.callback, event)
|
|
175
|
+
|
|
176
|
+
for child in router.children:
|
|
177
|
+
await self._dispatch_to_router(child, event_type, event)
|
|
178
|
+
|
|
179
|
+
async def _matches(
|
|
180
|
+
self,
|
|
181
|
+
entry: HandlerEntry[Any, ClientT],
|
|
182
|
+
event: Any,
|
|
183
|
+
) -> bool:
|
|
184
|
+
for flt in entry.filters:
|
|
185
|
+
result = flt(event)
|
|
186
|
+
if inspect.isawaitable(result):
|
|
187
|
+
result = await result
|
|
188
|
+
if not result:
|
|
189
|
+
logger.debug(
|
|
190
|
+
"handler skipped by filter callback=%s",
|
|
191
|
+
_callback_name(entry.callback),
|
|
192
|
+
)
|
|
193
|
+
return False
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
async def _call(self, callback: HandlerCallback[Any, ClientT], event: Any) -> Any:
|
|
197
|
+
if self.client is None:
|
|
198
|
+
raise RuntimeError("client is not bound")
|
|
199
|
+
|
|
200
|
+
result = callback(event, self.client)
|
|
201
|
+
|
|
202
|
+
if inspect.isawaitable(result):
|
|
203
|
+
return await result
|
|
204
|
+
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _callback_name(callback: Any) -> str:
|
|
209
|
+
return getattr(
|
|
210
|
+
callback,
|
|
211
|
+
"__qualname__",
|
|
212
|
+
getattr(callback, "__name__", repr(callback)),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _log_task_error(task: asyncio.Task[Any]) -> None:
|
|
217
|
+
try:
|
|
218
|
+
task.result()
|
|
219
|
+
except asyncio.CancelledError:
|
|
220
|
+
pass
|
|
221
|
+
except Exception:
|
|
222
|
+
logger.exception("startup task failed")
|
pymax/dispatch/enums.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EventType(str, Enum):
|
|
5
|
+
MESSAGE_NEW = "message_new"
|
|
6
|
+
MESSAGE_EDIT = "message_edit"
|
|
7
|
+
MESSAGE_DELETE = "message_delete"
|
|
8
|
+
CHAT_UPDATE = "chat_update"
|
|
9
|
+
USER_UPDATE = "user_update"
|
|
10
|
+
VIDEO_READY = "video_ready"
|
|
11
|
+
FILE_READY = "file_ready"
|
|
12
|
+
RAW = "raw"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from pymax.protocol import InboundFrame, Opcode
|
|
7
|
+
from pymax.protocol.enums import Command
|
|
8
|
+
from pymax.types import Chat, MessageDeleteEvent
|
|
9
|
+
from pymax.types.domain import Message
|
|
10
|
+
from pymax.types.events import FileUploadSignal, VideoUploadSignal
|
|
11
|
+
|
|
12
|
+
from .enums import EventType
|
|
13
|
+
from .resolvers import resolve_attach, resolve_chat, resolve_message, resolve_message_delete
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pymax.app import App
|
|
17
|
+
|
|
18
|
+
Resolver = Callable[[InboundFrame], EventType | None]
|
|
19
|
+
|
|
20
|
+
EVENT_MAP: dict[Opcode, Resolver] = {
|
|
21
|
+
Opcode.NOTIF_MESSAGE: resolve_message,
|
|
22
|
+
Opcode.MSG_EDIT: resolve_message,
|
|
23
|
+
Opcode.NOTIF_CHAT: resolve_chat,
|
|
24
|
+
Opcode.NOTIF_MSG_DELETE: resolve_message_delete,
|
|
25
|
+
Opcode.NOTIF_ATTACH: resolve_attach,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EventResolver:
|
|
30
|
+
def resolve(self, frame: InboundFrame) -> EventType | None:
|
|
31
|
+
if frame.cmd != Command.EVENT:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
opcode = Opcode(frame.opcode)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
handler = EVENT_MAP.get(opcode)
|
|
40
|
+
|
|
41
|
+
if handler:
|
|
42
|
+
return handler(frame)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EventMapper:
|
|
47
|
+
def __init__(self, app: App) -> None:
|
|
48
|
+
self.app = app
|
|
49
|
+
|
|
50
|
+
def map(self, event_type: EventType, frame: InboundFrame):
|
|
51
|
+
if frame.cmd != Command.EVENT:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
if frame.payload:
|
|
55
|
+
if event_type in (EventType.MESSAGE_NEW, EventType.MESSAGE_EDIT):
|
|
56
|
+
return Message.model_validate(frame.payload).bind(self.app.api.messages)
|
|
57
|
+
elif event_type == EventType.CHAT_UPDATE:
|
|
58
|
+
return Chat.model_validate(frame.payload["chat"]).bind(
|
|
59
|
+
self.app.api.messages,
|
|
60
|
+
self.app.api.chats,
|
|
61
|
+
)
|
|
62
|
+
elif event_type == EventType.MESSAGE_DELETE:
|
|
63
|
+
model = MessageDeleteEvent.model_validate(frame.payload)
|
|
64
|
+
model.chat.bind(
|
|
65
|
+
self.app.api.messages,
|
|
66
|
+
self.app.api.chats,
|
|
67
|
+
)
|
|
68
|
+
return model
|
|
69
|
+
elif event_type == EventType.VIDEO_READY:
|
|
70
|
+
return VideoUploadSignal.model_validate(frame.payload)
|
|
71
|
+
elif event_type == EventType.FILE_READY:
|
|
72
|
+
return FileUploadSignal.model_validate(frame.payload)
|
|
73
|
+
return frame
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from pydantic import ValidationError
|
|
2
|
+
|
|
3
|
+
from pymax.logging import get_logger
|
|
4
|
+
from pymax.protocol import InboundFrame
|
|
5
|
+
from pymax.protocol.enums import Opcode
|
|
6
|
+
from pymax.types import Message
|
|
7
|
+
from pymax.types.domain.enums import MessageStatus
|
|
8
|
+
from pymax.types.events import FileUploadSignal, VideoUploadSignal
|
|
9
|
+
|
|
10
|
+
from .enums import EventType
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_chat(_: InboundFrame) -> EventType | None:
|
|
16
|
+
return EventType.CHAT_UPDATE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_message_delete(_: InboundFrame) -> EventType | None:
|
|
20
|
+
return EventType.MESSAGE_DELETE
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_attach(frame: InboundFrame) -> EventType | None:
|
|
24
|
+
try:
|
|
25
|
+
FileUploadSignal.model_validate(frame.payload)
|
|
26
|
+
return EventType.FILE_READY
|
|
27
|
+
except ValidationError:
|
|
28
|
+
logger.debug("attach event is not a file upload signal")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
VideoUploadSignal.model_validate(frame.payload)
|
|
32
|
+
return EventType.VIDEO_READY
|
|
33
|
+
except ValidationError:
|
|
34
|
+
logger.debug("attach event is not a video upload signal")
|
|
35
|
+
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_message(frame: InboundFrame) -> EventType | None:
|
|
40
|
+
if frame.opcode not in (Opcode.NOTIF_MESSAGE, Opcode.MSG_EDIT):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
model = Message.model_validate(frame.payload)
|
|
45
|
+
|
|
46
|
+
if model.status == MessageStatus.EDITED:
|
|
47
|
+
return EventType.MESSAGE_EDIT
|
|
48
|
+
else:
|
|
49
|
+
return EventType.MESSAGE_NEW
|
|
50
|
+
except ValidationError:
|
|
51
|
+
logger.debug("failed to resolve message event", exc_info=True)
|
|
52
|
+
return None
|
pymax/dispatch/router.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
|
|
7
|
+
|
|
8
|
+
from pymax.types import MessageDeleteEvent
|
|
9
|
+
|
|
10
|
+
from .enums import EventType
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pymax.client import Client
|
|
14
|
+
from pymax.protocol import InboundFrame
|
|
15
|
+
from pymax.types import Chat
|
|
16
|
+
from pymax.types.domain import Message
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_EventT = TypeVar("_EventT")
|
|
20
|
+
ClientT = TypeVar("ClientT")
|
|
21
|
+
|
|
22
|
+
HandlerCallback: TypeAlias = Callable[
|
|
23
|
+
[_EventT, ClientT],
|
|
24
|
+
Awaitable[Any] | Any,
|
|
25
|
+
]
|
|
26
|
+
HandlerDecorator: TypeAlias = Callable[
|
|
27
|
+
[HandlerCallback[_EventT, ClientT]],
|
|
28
|
+
HandlerCallback[_EventT, ClientT],
|
|
29
|
+
]
|
|
30
|
+
FilterCallback: TypeAlias = Callable[
|
|
31
|
+
[_EventT],
|
|
32
|
+
Awaitable[bool] | bool,
|
|
33
|
+
]
|
|
34
|
+
StartCallback: TypeAlias = Callable[
|
|
35
|
+
[ClientT],
|
|
36
|
+
Awaitable[Any] | Any,
|
|
37
|
+
]
|
|
38
|
+
StartDecorator: TypeAlias = Callable[
|
|
39
|
+
[StartCallback[ClientT]],
|
|
40
|
+
StartCallback[ClientT],
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(slots=True)
|
|
45
|
+
class HandlerEntry(Generic[_EventT, ClientT]):
|
|
46
|
+
callback: HandlerCallback[_EventT, ClientT]
|
|
47
|
+
filters: tuple[FilterCallback[_EventT], ...] = ()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Router(Generic[ClientT]):
|
|
51
|
+
"""Контейнер обработчиков событий PyMax.
|
|
52
|
+
|
|
53
|
+
Роутер хранит обработчики и фильтры. Когда приходит событие, dispatcher
|
|
54
|
+
проходит по root router и его дочерним роутерам, проверяет фильтры и
|
|
55
|
+
вызывает подходящие callbacks как ``handler(event, client)``.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
.. code-block:: python
|
|
59
|
+
|
|
60
|
+
from pymax import Client, Message, Router
|
|
61
|
+
|
|
62
|
+
router = Router[Client]()
|
|
63
|
+
|
|
64
|
+
def is_start(message: Message) -> bool:
|
|
65
|
+
return message.text == "/start"
|
|
66
|
+
|
|
67
|
+
@router.on_message(is_start)
|
|
68
|
+
async def start(message: Message, client: Client) -> None:
|
|
69
|
+
await message.answer("Привет")
|
|
70
|
+
|
|
71
|
+
client = Client(phone="+79990000000")
|
|
72
|
+
client.include_router(router)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self.handlers: dict[
|
|
77
|
+
EventType,
|
|
78
|
+
list[HandlerEntry[Any, ClientT]],
|
|
79
|
+
] = defaultdict(list)
|
|
80
|
+
|
|
81
|
+
self.children: list[Router[ClientT]] = []
|
|
82
|
+
self.on_start_handler: StartCallback[ClientT] | None = None
|
|
83
|
+
|
|
84
|
+
def on(
|
|
85
|
+
self,
|
|
86
|
+
event: EventType,
|
|
87
|
+
/,
|
|
88
|
+
*filters: FilterCallback[_EventT],
|
|
89
|
+
) -> HandlerDecorator[_EventT, ClientT]:
|
|
90
|
+
"""Регистрирует обработчик события по ``EventType``.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
event: Тип события.
|
|
94
|
+
*filters: Фильтры, которые получают событие и возвращают bool.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Декоратор для ``handler(event, client)``.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
.. code-block:: python
|
|
101
|
+
|
|
102
|
+
from pymax import Client
|
|
103
|
+
from pymax.protocol import InboundFrame
|
|
104
|
+
|
|
105
|
+
@router.on(EventType.RAW)
|
|
106
|
+
async def raw(frame: InboundFrame, client: Client) -> None:
|
|
107
|
+
print(frame.payload)
|
|
108
|
+
"""
|
|
109
|
+
def decorator(
|
|
110
|
+
handler: HandlerCallback[_EventT, ClientT],
|
|
111
|
+
) -> HandlerCallback[_EventT, ClientT]:
|
|
112
|
+
self.handlers[event].append(
|
|
113
|
+
HandlerEntry(
|
|
114
|
+
callback=handler,
|
|
115
|
+
filters=filters,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return handler
|
|
119
|
+
|
|
120
|
+
return decorator
|
|
121
|
+
|
|
122
|
+
def include_router(self, router: Router[ClientT]) -> None:
|
|
123
|
+
"""Подключает дочерний роутер.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
router: Router, обработчики которого нужно добавить в дерево.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
``None``.
|
|
130
|
+
"""
|
|
131
|
+
self.children.append(router)
|
|
132
|
+
|
|
133
|
+
def on_start(self) -> StartDecorator:
|
|
134
|
+
"""Регистрирует обработчик старта клиента.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Декоратор для ``handler(client)``.
|
|
138
|
+
"""
|
|
139
|
+
def decorator(handler: StartCallback) -> StartCallback:
|
|
140
|
+
self.on_start_handler = handler
|
|
141
|
+
return handler
|
|
142
|
+
|
|
143
|
+
return decorator
|
|
144
|
+
|
|
145
|
+
def on_message(
|
|
146
|
+
self,
|
|
147
|
+
*filters: FilterCallback[Message],
|
|
148
|
+
) -> HandlerDecorator[Message, ClientT]:
|
|
149
|
+
"""Регистрирует обработчик новых сообщений.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
*filters: Фильтры для ``Message``.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Декоратор для ``handler(message, client)``.
|
|
156
|
+
"""
|
|
157
|
+
return self.on(EventType.MESSAGE_NEW, *filters)
|
|
158
|
+
|
|
159
|
+
def on_message_edit(
|
|
160
|
+
self,
|
|
161
|
+
*filters: FilterCallback[Message],
|
|
162
|
+
) -> HandlerDecorator[Message, ClientT]:
|
|
163
|
+
"""Регистрирует обработчик редактирования сообщений.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
*filters: Фильтры для ``Message``.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Декоратор для ``handler(message, client)``.
|
|
170
|
+
"""
|
|
171
|
+
return self.on(EventType.MESSAGE_EDIT, *filters)
|
|
172
|
+
|
|
173
|
+
def on_message_delete(
|
|
174
|
+
self,
|
|
175
|
+
*filters: FilterCallback[MessageDeleteEvent],
|
|
176
|
+
) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
|
|
177
|
+
"""Регистрирует обработчик удаления сообщений.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
*filters: Фильтры для ``MessageDeleteEvent``.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Декоратор для ``handler(event, client)``.
|
|
184
|
+
"""
|
|
185
|
+
return self.on(EventType.MESSAGE_DELETE, *filters)
|
|
186
|
+
|
|
187
|
+
def on_chat_update(
|
|
188
|
+
self,
|
|
189
|
+
*filters: FilterCallback[Chat],
|
|
190
|
+
) -> HandlerDecorator[Chat, ClientT]:
|
|
191
|
+
"""Регистрирует обработчик обновления чата.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
*filters: Фильтры для ``Chat``.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Декоратор для ``handler(chat, client)``.
|
|
198
|
+
"""
|
|
199
|
+
return self.on(EventType.CHAT_UPDATE, *filters)
|
|
200
|
+
|
|
201
|
+
def on_raw(
|
|
202
|
+
self,
|
|
203
|
+
*filters: FilterCallback[InboundFrame],
|
|
204
|
+
) -> HandlerDecorator[InboundFrame, ClientT]:
|
|
205
|
+
"""Регистрирует обработчик исходных frame-ов.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
*filters: Фильтры для ``InboundFrame``.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Декоратор для ``handler(frame, client)``.
|
|
212
|
+
"""
|
|
213
|
+
return self.on(EventType.RAW, *filters)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
ClientRouter: TypeAlias = Router["Client"]
|
pymax/exceptions.py
CHANGED
|
@@ -1,108 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
"""
|
|
3
|
-
Исключение, вызываемое при неверном формате номера телефона.
|
|
1
|
+
from typing import Any
|
|
4
2
|
|
|
5
|
-
Args:
|
|
6
|
-
phone (str): Некорректный номер телефона.
|
|
7
|
-
"""
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
class PyMaxError(Exception):
|
|
5
|
+
pass
|
|
11
6
|
|
|
12
7
|
|
|
13
|
-
class
|
|
14
|
-
|
|
15
|
-
Исключение, вызываемое при попытке обращения к WebSocket,
|
|
16
|
-
если соединение не установлено.
|
|
17
|
-
"""
|
|
8
|
+
class UploadError(PyMaxError):
|
|
9
|
+
pass
|
|
18
10
|
|
|
19
|
-
def __init__(self) -> None:
|
|
20
|
-
super().__init__("WebSocket is not connected")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class SocketNotConnectedError(Exception):
|
|
24
|
-
"""
|
|
25
|
-
Исключение, вызываемое при попытке обращения к сокету,
|
|
26
|
-
если соединение не установлено.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(self) -> None:
|
|
30
|
-
super().__init__("Socket is not connected")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class SocketSendError(Exception):
|
|
34
|
-
"""
|
|
35
|
-
Исключение, вызываемое при ошибке отправки данных через сокет.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
def __init__(self) -> None:
|
|
39
|
-
super().__init__("Send and wait failed (socket)")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class ResponseError(Exception):
|
|
43
|
-
"""
|
|
44
|
-
Исключение, вызываемое при ошибке в ответе от сервера.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
def __init__(self, message: str) -> None:
|
|
48
|
-
super().__init__(f"Response error: {message}")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class ResponseStructureError(Exception):
|
|
52
|
-
"""
|
|
53
|
-
Исключение, вызываемое при неверной структуре ответа от сервера.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def __init__(self, message: str) -> None:
|
|
57
|
-
super().__init__(f"Response structure error: {message}")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class Error(Exception):
|
|
61
|
-
"""
|
|
62
|
-
Базовое исключение для ошибок PyMax.
|
|
63
|
-
"""
|
|
64
11
|
|
|
12
|
+
class ApiError(PyMaxError):
|
|
65
13
|
def __init__(
|
|
66
14
|
self,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
15
|
+
*,
|
|
16
|
+
opcode: int,
|
|
17
|
+
error: str | None = None,
|
|
18
|
+
message: str | None = None,
|
|
70
19
|
localized_message: str | None = None,
|
|
20
|
+
title: str | None = None,
|
|
21
|
+
payload: dict[str, Any] | None = None,
|
|
71
22
|
) -> None:
|
|
23
|
+
self.opcode = opcode
|
|
72
24
|
self.error = error
|
|
73
25
|
self.message = message
|
|
74
|
-
self.title = title
|
|
75
26
|
self.localized_message = localized_message
|
|
27
|
+
self.title = title
|
|
28
|
+
self.payload = payload or {}
|
|
76
29
|
|
|
77
30
|
parts = []
|
|
78
|
-
|
|
79
|
-
parts
|
|
80
|
-
|
|
81
|
-
parts.append(message)
|
|
31
|
+
for part in (localized_message, message):
|
|
32
|
+
if part and part not in parts:
|
|
33
|
+
parts.append(part)
|
|
82
34
|
if title:
|
|
83
35
|
parts.append(f"({title})")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
super().__init__("PyMax Error: " + " ".join(parts))
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class RateLimitError(Error):
|
|
90
|
-
"""
|
|
91
|
-
Исключение, вызываемое при превышении лимита запросов.
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
def __init__(
|
|
95
|
-
self, error: str, message: str, title: str, localized_message: str | None = None
|
|
96
|
-
) -> None:
|
|
97
|
-
super().__init__(error, message, title, localized_message)
|
|
36
|
+
if error:
|
|
37
|
+
parts.append(f"[{error}]")
|
|
98
38
|
|
|
39
|
+
text = " ".join(parts) or "API request failed"
|
|
99
40
|
|
|
100
|
-
|
|
101
|
-
"""
|
|
102
|
-
Исключение, вызываемое при ошибке авторизации.
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
def __init__(
|
|
106
|
-
self, error: str, message: str, title: str, localized_message: str | None = None
|
|
107
|
-
) -> None:
|
|
108
|
-
super().__init__(error, message, title, localized_message)
|
|
41
|
+
super().__init__(text)
|