maxapi-sdk 0.12.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/__init__.py +49 -0
- maxapi/bot.py +674 -0
- maxapi/builders/__init__.py +23 -0
- maxapi/builders/keyboards.py +133 -0
- maxapi/builders/media.py +81 -0
- maxapi/callback_schema.py +140 -0
- maxapi/client/__init__.py +3 -0
- maxapi/client/default.py +89 -0
- maxapi/compat/__init__.py +15 -0
- maxapi/connection/__init__.py +3 -0
- maxapi/connection/base.py +136 -0
- maxapi/dispatcher.py +487 -0
- maxapi/exceptions/__init__.py +3 -0
- maxapi/exceptions/max.py +45 -0
- maxapi/filters/__init__.py +25 -0
- maxapi/filters/base.py +73 -0
- maxapi/filters/command.py +28 -0
- maxapi/filters/common.py +78 -0
- maxapi/filters/text.py +71 -0
- maxapi/fsm/__init__.py +17 -0
- maxapi/fsm/context.py +41 -0
- maxapi/fsm/filters.py +30 -0
- maxapi/fsm/middleware.py +42 -0
- maxapi/fsm/state.py +33 -0
- maxapi/fsm/storage/__init__.py +4 -0
- maxapi/fsm/storage/base.py +30 -0
- maxapi/fsm/storage/memory.py +38 -0
- maxapi/middlewares/__init__.py +3 -0
- maxapi/middlewares/base.py +34 -0
- maxapi/plugins/__init__.py +3 -0
- maxapi/plugins/base.py +19 -0
- maxapi/py.typed +1 -0
- maxapi/runners/__init__.py +4 -0
- maxapi/runners/polling.py +55 -0
- maxapi/runners/webhook.py +72 -0
- maxapi/transport/__init__.py +13 -0
- maxapi/transport/client.py +239 -0
- maxapi/transport/config.py +81 -0
- maxapi/transport/errors.py +45 -0
- maxapi/types/__init__.py +73 -0
- maxapi/types/base.py +9 -0
- maxapi/types/bot_mixin.py +14 -0
- maxapi/types/models.py +308 -0
- maxapi_sdk-0.12.0.dist-info/METADATA +270 -0
- maxapi_sdk-0.12.0.dist-info/RECORD +48 -0
- maxapi_sdk-0.12.0.dist-info/WHEEL +5 -0
- maxapi_sdk-0.12.0.dist-info/licenses/LICENSE +21 -0
- maxapi_sdk-0.12.0.dist-info/top_level.txt +1 -0
maxapi/dispatcher.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Awaitable, Callable, Iterable, Sequence
|
|
6
|
+
|
|
7
|
+
from .callback_schema import extract_callback_mapping, extract_callback_value
|
|
8
|
+
from .filters import ensure_filter
|
|
9
|
+
from .fsm import FSMMiddleware, MemoryStorage
|
|
10
|
+
from .middlewares import BaseMiddleware, FunctionMiddleware, MiddlewareHandler
|
|
11
|
+
from .runners import PollingRunner, WebhookRunner
|
|
12
|
+
from .types import Message, Update, UpdateType
|
|
13
|
+
|
|
14
|
+
HandlerCallable = Callable[[Any], Awaitable[Any]]
|
|
15
|
+
FilterCallable = Callable[[Any], Awaitable[bool] | bool]
|
|
16
|
+
MiddlewareCallable = Callable[[MiddlewareHandler, Any, dict[str, Any]], Awaitable[Any] | Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class HandlerSpec:
|
|
21
|
+
update_type: UpdateType | str
|
|
22
|
+
callback: HandlerCallable
|
|
23
|
+
filters: list[FilterCallable] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseEvent:
|
|
27
|
+
def __init__(self, *, bot, update: Update, raw_data: dict[str, Any] | None = None) -> None:
|
|
28
|
+
self.bot = bot
|
|
29
|
+
self.update = update
|
|
30
|
+
self.raw_data = raw_data or {}
|
|
31
|
+
self.update_type = update.update_type
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def chat_id(self) -> int | None:
|
|
35
|
+
if self.update.chat_id is not None:
|
|
36
|
+
return self.update.chat_id
|
|
37
|
+
message = getattr(self.update, "message", None)
|
|
38
|
+
if message is not None:
|
|
39
|
+
if message.chat_id is not None:
|
|
40
|
+
return message.chat_id
|
|
41
|
+
if message.recipient is not None:
|
|
42
|
+
return message.recipient.chat_id
|
|
43
|
+
callback = getattr(self.update, "callback", None)
|
|
44
|
+
if callback is not None and callback.message is not None:
|
|
45
|
+
callback_message = callback.message
|
|
46
|
+
if callback_message.chat_id is not None:
|
|
47
|
+
return callback_message.chat_id
|
|
48
|
+
if callback_message.recipient is not None:
|
|
49
|
+
return callback_message.recipient.chat_id
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def user_id(self) -> int | None:
|
|
54
|
+
if self.update.user_id is not None:
|
|
55
|
+
return self.update.user_id
|
|
56
|
+
message = getattr(self.update, "message", None)
|
|
57
|
+
if message is not None and message.sender is not None:
|
|
58
|
+
return message.sender.user_id
|
|
59
|
+
callback = getattr(self.update, "callback", None)
|
|
60
|
+
if callback is not None and callback.user is not None:
|
|
61
|
+
return callback.user.user_id
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MessageEvent(BaseEvent):
|
|
66
|
+
@property
|
|
67
|
+
def message(self) -> Message:
|
|
68
|
+
if self.update.message is None:
|
|
69
|
+
raise RuntimeError("В событии отсутствует message.")
|
|
70
|
+
return self.update.message
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CallbackEvent(BaseEvent):
|
|
74
|
+
@property
|
|
75
|
+
def callback_id(self) -> str | None:
|
|
76
|
+
if self.update.callback is not None and self.update.callback.callback_id is not None:
|
|
77
|
+
return self.update.callback.callback_id
|
|
78
|
+
return self.update.callback_id
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def callback(self):
|
|
82
|
+
return self.update.callback
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def message(self) -> Message | None:
|
|
86
|
+
if self.update.callback is not None:
|
|
87
|
+
return self.update.callback.message
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def payload(self) -> Any:
|
|
92
|
+
if self.update.callback is None:
|
|
93
|
+
return None
|
|
94
|
+
return self.update.callback.payload
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def payload_text(self) -> str | None:
|
|
98
|
+
return extract_callback_value(self.payload)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def payload_dict(self) -> dict[str, Any] | None:
|
|
102
|
+
return extract_callback_mapping(self.payload)
|
|
103
|
+
|
|
104
|
+
def unpack(self, schema):
|
|
105
|
+
return schema.unpack(self.payload)
|
|
106
|
+
|
|
107
|
+
async def answer(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
notification: str | None = None,
|
|
111
|
+
message: dict[str, Any] | None = None,
|
|
112
|
+
keyboard: Any | None = None,
|
|
113
|
+
):
|
|
114
|
+
callback_id = self.callback_id
|
|
115
|
+
if callback_id is None:
|
|
116
|
+
raise RuntimeError("В callback-событии отсутствует callback_id.")
|
|
117
|
+
from .types import MessageBody
|
|
118
|
+
|
|
119
|
+
message_body = MessageBody.model_validate(message) if message is not None else None
|
|
120
|
+
return await self.bot.answer_callback(
|
|
121
|
+
callback_id,
|
|
122
|
+
notification=notification,
|
|
123
|
+
message=message_body,
|
|
124
|
+
keyboard=keyboard,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Router:
|
|
129
|
+
def __init__(self) -> None:
|
|
130
|
+
self.handlers: dict[str, list[HandlerSpec]] = {}
|
|
131
|
+
self.routers: list[Router] = []
|
|
132
|
+
self.middlewares: list[BaseMiddleware] = []
|
|
133
|
+
self.plugins: list[Any] = []
|
|
134
|
+
self.bot = None
|
|
135
|
+
|
|
136
|
+
def include_router(self, router: "Router") -> None:
|
|
137
|
+
self.routers.append(router)
|
|
138
|
+
|
|
139
|
+
def include_plugin(self, plugin) -> Any:
|
|
140
|
+
plugin.setup(self)
|
|
141
|
+
self.plugins.append(plugin)
|
|
142
|
+
return plugin
|
|
143
|
+
|
|
144
|
+
def include_plugins(self, *plugins) -> tuple[Any, ...]:
|
|
145
|
+
return tuple(self.include_plugin(plugin) for plugin in plugins)
|
|
146
|
+
|
|
147
|
+
def use(self, middleware: BaseMiddleware | MiddlewareCallable) -> BaseMiddleware:
|
|
148
|
+
prepared = middleware if isinstance(middleware, BaseMiddleware) else FunctionMiddleware(middleware)
|
|
149
|
+
self.middlewares.append(prepared)
|
|
150
|
+
return prepared
|
|
151
|
+
|
|
152
|
+
def add_middleware(self, middleware: BaseMiddleware | MiddlewareCallable) -> BaseMiddleware:
|
|
153
|
+
return self.use(middleware)
|
|
154
|
+
|
|
155
|
+
def _register_handler(
|
|
156
|
+
self,
|
|
157
|
+
update_type: UpdateType | str,
|
|
158
|
+
callback: HandlerCallable,
|
|
159
|
+
*filters: FilterCallable,
|
|
160
|
+
) -> HandlerCallable:
|
|
161
|
+
normalized_type = update_type.value if hasattr(update_type, "value") else str(update_type)
|
|
162
|
+
self.handlers.setdefault(normalized_type, []).append(
|
|
163
|
+
HandlerSpec(
|
|
164
|
+
update_type=normalized_type,
|
|
165
|
+
callback=callback,
|
|
166
|
+
filters=list(filters),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
return callback
|
|
170
|
+
|
|
171
|
+
def on(self, update_type: UpdateType | str, *filters: FilterCallable):
|
|
172
|
+
normalized_type = update_type.value if hasattr(update_type, "value") else str(update_type)
|
|
173
|
+
|
|
174
|
+
def decorator(func: HandlerCallable) -> HandlerCallable:
|
|
175
|
+
self.handlers.setdefault(normalized_type, []).append(
|
|
176
|
+
HandlerSpec(update_type=normalized_type, callback=func, filters=list(filters))
|
|
177
|
+
)
|
|
178
|
+
return func
|
|
179
|
+
|
|
180
|
+
return decorator
|
|
181
|
+
|
|
182
|
+
def message_created(self, *filters: FilterCallable):
|
|
183
|
+
return self.on(UpdateType.MESSAGE_CREATED, *filters)
|
|
184
|
+
|
|
185
|
+
def message_callback(self, *filters: FilterCallable):
|
|
186
|
+
return self.on(UpdateType.MESSAGE_CALLBACK, *filters)
|
|
187
|
+
|
|
188
|
+
def message_edited(self, *filters: FilterCallable):
|
|
189
|
+
return self.on(UpdateType.MESSAGE_EDITED, *filters)
|
|
190
|
+
|
|
191
|
+
def message_removed(self, *filters: FilterCallable):
|
|
192
|
+
return self.on(UpdateType.MESSAGE_REMOVED, *filters)
|
|
193
|
+
|
|
194
|
+
def bot_started(self, *filters: FilterCallable):
|
|
195
|
+
return self.on(UpdateType.BOT_STARTED, *filters)
|
|
196
|
+
|
|
197
|
+
def bot_added(self, *filters: FilterCallable):
|
|
198
|
+
return self.on(UpdateType.BOT_ADDED, *filters)
|
|
199
|
+
|
|
200
|
+
def bot_removed(self, *filters: FilterCallable):
|
|
201
|
+
return self.on(UpdateType.BOT_REMOVED, *filters)
|
|
202
|
+
|
|
203
|
+
def bot_stopped(self, *filters: FilterCallable):
|
|
204
|
+
return self.on(UpdateType.BOT_STOPPED, *filters)
|
|
205
|
+
|
|
206
|
+
def user_added(self, *filters: FilterCallable):
|
|
207
|
+
return self.on(UpdateType.USER_ADDED, *filters)
|
|
208
|
+
|
|
209
|
+
def user_removed(self, *filters: FilterCallable):
|
|
210
|
+
return self.on(UpdateType.USER_REMOVED, *filters)
|
|
211
|
+
|
|
212
|
+
def message_handler(self, *filters: FilterCallable):
|
|
213
|
+
return self.message_created(*filters)
|
|
214
|
+
|
|
215
|
+
def callback_handler(self, *filters: FilterCallable):
|
|
216
|
+
return self.message_callback(*filters)
|
|
217
|
+
|
|
218
|
+
def callback_query_handler(self, *filters: FilterCallable):
|
|
219
|
+
return self.message_callback(*filters)
|
|
220
|
+
|
|
221
|
+
def edited_message_handler(self, *filters: FilterCallable):
|
|
222
|
+
return self.message_edited(*filters)
|
|
223
|
+
|
|
224
|
+
def removed_message_handler(self, *filters: FilterCallable):
|
|
225
|
+
return self.message_removed(*filters)
|
|
226
|
+
|
|
227
|
+
def register_message_handler(self, callback: HandlerCallable, *filters: FilterCallable) -> HandlerCallable:
|
|
228
|
+
return self._register_handler(UpdateType.MESSAGE_CREATED, callback, *filters)
|
|
229
|
+
|
|
230
|
+
def register_callback_handler(self, callback: HandlerCallable, *filters: FilterCallable) -> HandlerCallable:
|
|
231
|
+
return self._register_handler(UpdateType.MESSAGE_CALLBACK, callback, *filters)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Dispatcher(Router):
|
|
235
|
+
def __init__(self, *, storage=None) -> None:
|
|
236
|
+
super().__init__()
|
|
237
|
+
self.fsm_storage = storage
|
|
238
|
+
if self.fsm_storage is not None:
|
|
239
|
+
self.use(FSMMiddleware(self.fsm_storage))
|
|
240
|
+
|
|
241
|
+
def setup_fsm(self, storage=None):
|
|
242
|
+
self.fsm_storage = storage or MemoryStorage()
|
|
243
|
+
self.middlewares = [
|
|
244
|
+
middleware for middleware in self.middlewares if not isinstance(middleware, FSMMiddleware)
|
|
245
|
+
]
|
|
246
|
+
self.middlewares.insert(0, FSMMiddleware(self.fsm_storage))
|
|
247
|
+
return self.fsm_storage
|
|
248
|
+
|
|
249
|
+
async def process_update(self, payload: Update | dict[str, Any], *, bot) -> None:
|
|
250
|
+
self.bot = bot
|
|
251
|
+
bot.dispatcher = self
|
|
252
|
+
update = payload if isinstance(payload, Update) else Update.model_validate(payload)
|
|
253
|
+
if update.message is not None:
|
|
254
|
+
update.message.bind_bot(bot)
|
|
255
|
+
if update.callback is not None and update.callback.message is not None:
|
|
256
|
+
update.callback.message.bind_bot(bot)
|
|
257
|
+
event = self._build_event(bot=bot, update=update, raw_data=payload if isinstance(payload, dict) else None)
|
|
258
|
+
await self._dispatch_router(self, event, inherited_middlewares=[])
|
|
259
|
+
|
|
260
|
+
async def _dispatch_router(
|
|
261
|
+
self,
|
|
262
|
+
router: Router,
|
|
263
|
+
event: BaseEvent,
|
|
264
|
+
*,
|
|
265
|
+
inherited_middlewares: Sequence[BaseMiddleware],
|
|
266
|
+
) -> None:
|
|
267
|
+
update_type = event.update.update_type
|
|
268
|
+
normalized_type = update_type.value if hasattr(update_type, "value") else str(update_type)
|
|
269
|
+
middleware_chain = [*inherited_middlewares, *router.middlewares]
|
|
270
|
+
for handler in router.handlers.get(normalized_type, []):
|
|
271
|
+
if await self._filters_pass(handler.filters, event):
|
|
272
|
+
data = self._build_handler_data(event=event, router=router)
|
|
273
|
+
await self._call_with_middlewares(
|
|
274
|
+
callback=handler.callback,
|
|
275
|
+
event=event,
|
|
276
|
+
data=data,
|
|
277
|
+
middlewares=middleware_chain,
|
|
278
|
+
)
|
|
279
|
+
for child in router.routers:
|
|
280
|
+
child.bot = self.bot
|
|
281
|
+
await self._dispatch_router(
|
|
282
|
+
child,
|
|
283
|
+
event,
|
|
284
|
+
inherited_middlewares=middleware_chain,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
async def _filters_pass(self, filters: Iterable[FilterCallable], event: BaseEvent) -> bool:
|
|
288
|
+
for item in filters:
|
|
289
|
+
prepared = ensure_filter(item)
|
|
290
|
+
result = await prepared(event)
|
|
291
|
+
if not result:
|
|
292
|
+
return False
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
async def _call_with_middlewares(
|
|
296
|
+
self,
|
|
297
|
+
*,
|
|
298
|
+
callback: HandlerCallable,
|
|
299
|
+
event: BaseEvent,
|
|
300
|
+
data: dict[str, Any],
|
|
301
|
+
middlewares: Sequence[BaseMiddleware],
|
|
302
|
+
) -> Any:
|
|
303
|
+
async def terminal(current_event: BaseEvent, current_data: dict[str, Any]) -> Any:
|
|
304
|
+
return await self._call_handler(callback, current_event, current_data)
|
|
305
|
+
|
|
306
|
+
handler = terminal
|
|
307
|
+
for middleware in reversed(middlewares):
|
|
308
|
+
next_handler = handler
|
|
309
|
+
|
|
310
|
+
async def wrapped(
|
|
311
|
+
current_event: BaseEvent,
|
|
312
|
+
current_data: dict[str, Any],
|
|
313
|
+
*,
|
|
314
|
+
current_middleware: BaseMiddleware = middleware,
|
|
315
|
+
current_next: MiddlewareHandler = next_handler,
|
|
316
|
+
) -> Any:
|
|
317
|
+
return await current_middleware(current_next, current_event, current_data)
|
|
318
|
+
|
|
319
|
+
handler = wrapped
|
|
320
|
+
return await handler(event, data)
|
|
321
|
+
|
|
322
|
+
async def _call_handler(
|
|
323
|
+
self,
|
|
324
|
+
callback: HandlerCallable,
|
|
325
|
+
event: BaseEvent,
|
|
326
|
+
data: dict[str, Any],
|
|
327
|
+
) -> Any:
|
|
328
|
+
signature = inspect.signature(callback)
|
|
329
|
+
positional_args: list[Any] = []
|
|
330
|
+
keyword_args: dict[str, Any] = {}
|
|
331
|
+
used_event_fallback = False
|
|
332
|
+
|
|
333
|
+
for parameter in signature.parameters.values():
|
|
334
|
+
if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
335
|
+
continue
|
|
336
|
+
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
|
337
|
+
keyword_args.update(data)
|
|
338
|
+
continue
|
|
339
|
+
value_provided = False
|
|
340
|
+
value: Any = None
|
|
341
|
+
if parameter.name in data:
|
|
342
|
+
value = data[parameter.name]
|
|
343
|
+
value_provided = True
|
|
344
|
+
elif parameter.name in {"event", "message_event", "callback_event"}:
|
|
345
|
+
value = event
|
|
346
|
+
value_provided = True
|
|
347
|
+
elif not used_event_fallback:
|
|
348
|
+
value = event
|
|
349
|
+
value_provided = True
|
|
350
|
+
used_event_fallback = True
|
|
351
|
+
elif parameter.default is inspect.Parameter.empty:
|
|
352
|
+
raise TypeError(
|
|
353
|
+
f"Не удалось внедрить обязательный аргумент '{parameter.name}' "
|
|
354
|
+
f"для handler {callback.__name__}."
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if not value_provided:
|
|
358
|
+
continue
|
|
359
|
+
if parameter.kind in (
|
|
360
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
361
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
362
|
+
):
|
|
363
|
+
positional_args.append(value)
|
|
364
|
+
else:
|
|
365
|
+
keyword_args[parameter.name] = value
|
|
366
|
+
|
|
367
|
+
result = callback(*positional_args, **keyword_args)
|
|
368
|
+
if inspect.isawaitable(result):
|
|
369
|
+
return await result
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
def _build_handler_data(self, *, event: BaseEvent, router: Router) -> dict[str, Any]:
|
|
373
|
+
message = getattr(event, "message", None)
|
|
374
|
+
callback = getattr(event.update, "callback", None)
|
|
375
|
+
callback_event = event if isinstance(event, CallbackEvent) else None
|
|
376
|
+
return {
|
|
377
|
+
"bot": event.bot,
|
|
378
|
+
"callback": callback,
|
|
379
|
+
"callback_event": callback_event,
|
|
380
|
+
"callback_payload": callback_event.payload if callback_event is not None else None,
|
|
381
|
+
"callback_payload_dict": callback_event.payload_dict if callback_event is not None else None,
|
|
382
|
+
"callback_payload_text": callback_event.payload_text if callback_event is not None else None,
|
|
383
|
+
"chat_id": event.chat_id,
|
|
384
|
+
"data": {},
|
|
385
|
+
"dispatcher": self,
|
|
386
|
+
"event": event,
|
|
387
|
+
"message": message,
|
|
388
|
+
"message_event": event if isinstance(event, MessageEvent) else None,
|
|
389
|
+
"raw_data": event.raw_data,
|
|
390
|
+
"router": router,
|
|
391
|
+
"update": event.update,
|
|
392
|
+
"user_id": event.user_id,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
def _build_event(self, *, bot, update: Update, raw_data: dict[str, Any] | None) -> BaseEvent:
|
|
396
|
+
update_type = update.update_type.value if hasattr(update.update_type, "value") else str(update.update_type)
|
|
397
|
+
if update_type in {
|
|
398
|
+
UpdateType.MESSAGE_CREATED.value,
|
|
399
|
+
UpdateType.MESSAGE_EDITED.value,
|
|
400
|
+
UpdateType.MESSAGE_REMOVED.value,
|
|
401
|
+
UpdateType.BOT_STARTED.value,
|
|
402
|
+
UpdateType.BOT_ADDED.value,
|
|
403
|
+
UpdateType.BOT_REMOVED.value,
|
|
404
|
+
UpdateType.BOT_STOPPED.value,
|
|
405
|
+
UpdateType.USER_ADDED.value,
|
|
406
|
+
UpdateType.USER_REMOVED.value,
|
|
407
|
+
}:
|
|
408
|
+
return MessageEvent(bot=bot, update=update, raw_data=raw_data)
|
|
409
|
+
if update_type == UpdateType.MESSAGE_CALLBACK.value:
|
|
410
|
+
return CallbackEvent(bot=bot, update=update, raw_data=raw_data)
|
|
411
|
+
return BaseEvent(bot=bot, update=update, raw_data=raw_data)
|
|
412
|
+
|
|
413
|
+
async def start_polling(
|
|
414
|
+
self,
|
|
415
|
+
bot,
|
|
416
|
+
*,
|
|
417
|
+
limit: int = 100,
|
|
418
|
+
timeout: int = 30,
|
|
419
|
+
allowed_updates: Iterable[UpdateType | str] | None = None,
|
|
420
|
+
retry_delay: float = 2.0,
|
|
421
|
+
) -> None:
|
|
422
|
+
bot.dispatcher = self
|
|
423
|
+
if bot.auto_check_subscriptions:
|
|
424
|
+
subscriptions = await bot.get_subscriptions()
|
|
425
|
+
if subscriptions.subscriptions:
|
|
426
|
+
raise RuntimeError(
|
|
427
|
+
"Обнаружены активные webhook-подписки. Удалите их через bot.delete_webhook()."
|
|
428
|
+
)
|
|
429
|
+
runner = PollingRunner(
|
|
430
|
+
bot=bot,
|
|
431
|
+
dispatcher=self,
|
|
432
|
+
limit=limit,
|
|
433
|
+
timeout=timeout,
|
|
434
|
+
allowed_updates=allowed_updates,
|
|
435
|
+
retry_delay=retry_delay,
|
|
436
|
+
)
|
|
437
|
+
await runner.start()
|
|
438
|
+
|
|
439
|
+
async def run_polling(
|
|
440
|
+
self,
|
|
441
|
+
bot,
|
|
442
|
+
*,
|
|
443
|
+
limit: int = 100,
|
|
444
|
+
timeout: int = 30,
|
|
445
|
+
allowed_updates: Iterable[UpdateType | str] | None = None,
|
|
446
|
+
retry_delay: float = 2.0,
|
|
447
|
+
) -> None:
|
|
448
|
+
await self.start_polling(
|
|
449
|
+
bot,
|
|
450
|
+
limit=limit,
|
|
451
|
+
timeout=timeout,
|
|
452
|
+
allowed_updates=allowed_updates,
|
|
453
|
+
retry_delay=retry_delay,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def create_webhook_app(
|
|
457
|
+
self,
|
|
458
|
+
*,
|
|
459
|
+
bot,
|
|
460
|
+
path: str = "/webhook",
|
|
461
|
+
secret: str | None = None,
|
|
462
|
+
):
|
|
463
|
+
bot.dispatcher = self
|
|
464
|
+
runner = WebhookRunner(bot=bot, dispatcher=self, path=path, secret=secret)
|
|
465
|
+
return runner.create_app()
|
|
466
|
+
|
|
467
|
+
async def handle_webhook(
|
|
468
|
+
self,
|
|
469
|
+
*,
|
|
470
|
+
bot,
|
|
471
|
+
host: str = "127.0.0.1",
|
|
472
|
+
port: int = 8080,
|
|
473
|
+
path: str = "/webhook",
|
|
474
|
+
secret: str | None = None,
|
|
475
|
+
log_level: str = "info",
|
|
476
|
+
) -> None:
|
|
477
|
+
bot.dispatcher = self
|
|
478
|
+
runner = WebhookRunner(
|
|
479
|
+
bot=bot,
|
|
480
|
+
dispatcher=self,
|
|
481
|
+
host=host,
|
|
482
|
+
port=port,
|
|
483
|
+
path=path,
|
|
484
|
+
secret=secret,
|
|
485
|
+
log_level=log_level,
|
|
486
|
+
)
|
|
487
|
+
await runner.start()
|
maxapi/exceptions/max.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MaxError(Exception):
|
|
8
|
+
"""Базовая ошибка SDK."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MaxConnection(MaxError):
|
|
12
|
+
"""Ошибка сетевого уровня."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class MaxApiError(MaxError):
|
|
17
|
+
"""Ошибка ответа MAX API."""
|
|
18
|
+
|
|
19
|
+
code: int
|
|
20
|
+
raw: Any
|
|
21
|
+
message: str | None = None
|
|
22
|
+
headers: dict[str, str] | None = None
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
if self.message:
|
|
26
|
+
return self.message
|
|
27
|
+
return f"MAX API returned status={self.code}: {self.raw!r}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InvalidToken(MaxApiError):
|
|
31
|
+
"""Ошибка авторизации API MAX."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
message: str = "Неверный токен!",
|
|
36
|
+
*,
|
|
37
|
+
raw: Any | None = None,
|
|
38
|
+
headers: dict[str, str] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(
|
|
41
|
+
code=401,
|
|
42
|
+
raw=raw if raw is not None else {"message": message},
|
|
43
|
+
message=message,
|
|
44
|
+
headers=headers,
|
|
45
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .base import AndFilter, BaseFilter, CallableFilter, NotFilter, OrFilter, ensure_filter
|
|
2
|
+
from .command import Command
|
|
3
|
+
from .common import CallbackData, ChatId, ChatType, HasAttachments, UserId
|
|
4
|
+
from .text import Regex, Text, TextContains, TextStartsWith
|
|
5
|
+
from ..fsm.filters import StateFilter
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AndFilter",
|
|
9
|
+
"BaseFilter",
|
|
10
|
+
"CallableFilter",
|
|
11
|
+
"CallbackData",
|
|
12
|
+
"ChatId",
|
|
13
|
+
"ChatType",
|
|
14
|
+
"Command",
|
|
15
|
+
"HasAttachments",
|
|
16
|
+
"NotFilter",
|
|
17
|
+
"OrFilter",
|
|
18
|
+
"Regex",
|
|
19
|
+
"StateFilter",
|
|
20
|
+
"Text",
|
|
21
|
+
"TextContains",
|
|
22
|
+
"TextStartsWith",
|
|
23
|
+
"UserId",
|
|
24
|
+
"ensure_filter",
|
|
25
|
+
]
|
maxapi/filters/base.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, Awaitable, Callable, Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FilterLike(Protocol):
|
|
8
|
+
async def __call__(self, event: Any) -> bool: # pragma: no cover - typing protocol
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseFilter:
|
|
13
|
+
"""Базовый фильтр с поддержкой композиции."""
|
|
14
|
+
|
|
15
|
+
async def __call__(self, event: Any) -> bool:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
def __and__(self, other: Any) -> "AndFilter":
|
|
19
|
+
return AndFilter(self, ensure_filter(other))
|
|
20
|
+
|
|
21
|
+
def __or__(self, other: Any) -> "OrFilter":
|
|
22
|
+
return OrFilter(self, ensure_filter(other))
|
|
23
|
+
|
|
24
|
+
def __invert__(self) -> "NotFilter":
|
|
25
|
+
return NotFilter(self)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CallableFilter(BaseFilter):
|
|
29
|
+
def __init__(self, callback: Callable[[Any], Awaitable[bool] | bool]) -> None:
|
|
30
|
+
self.callback = callback
|
|
31
|
+
|
|
32
|
+
async def __call__(self, event: Any) -> bool:
|
|
33
|
+
result = self.callback(event)
|
|
34
|
+
if inspect.isawaitable(result):
|
|
35
|
+
result = await result
|
|
36
|
+
return bool(result)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MultiFilter(BaseFilter):
|
|
40
|
+
def __init__(self, *filters: BaseFilter) -> None:
|
|
41
|
+
self.filters = list(filters)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AndFilter(MultiFilter):
|
|
45
|
+
async def __call__(self, event: Any) -> bool:
|
|
46
|
+
for item in self.filters:
|
|
47
|
+
if not await item(event):
|
|
48
|
+
return False
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OrFilter(MultiFilter):
|
|
53
|
+
async def __call__(self, event: Any) -> bool:
|
|
54
|
+
for item in self.filters:
|
|
55
|
+
if await item(event):
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NotFilter(BaseFilter):
|
|
61
|
+
def __init__(self, inner: BaseFilter) -> None:
|
|
62
|
+
self.inner = inner
|
|
63
|
+
|
|
64
|
+
async def __call__(self, event: Any) -> bool:
|
|
65
|
+
return not await self.inner(event)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_filter(candidate: Any) -> BaseFilter:
|
|
69
|
+
if isinstance(candidate, BaseFilter):
|
|
70
|
+
return candidate
|
|
71
|
+
if callable(candidate):
|
|
72
|
+
return CallableFilter(candidate)
|
|
73
|
+
raise TypeError(f"Неподдерживаемый фильтр: {candidate!r}")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import BaseFilter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class Command(BaseFilter):
|
|
11
|
+
"""Фильтр команды по тексту сообщения."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
prefix: str = "/"
|
|
15
|
+
case_sensitive: bool = False
|
|
16
|
+
|
|
17
|
+
async def __call__(self, event: Any) -> bool:
|
|
18
|
+
message = getattr(event, "message", None)
|
|
19
|
+
if message is None or message.body is None or message.body.text is None:
|
|
20
|
+
return False
|
|
21
|
+
text = message.body.text.strip()
|
|
22
|
+
if not text:
|
|
23
|
+
return False
|
|
24
|
+
first_token = text.split(maxsplit=1)[0]
|
|
25
|
+
command_value = f"{self.prefix}{self.name}"
|
|
26
|
+
if self.case_sensitive:
|
|
27
|
+
return first_token == command_value
|
|
28
|
+
return first_token.lower() == command_value.lower()
|