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.
Files changed (48) hide show
  1. maxapi/__init__.py +49 -0
  2. maxapi/bot.py +674 -0
  3. maxapi/builders/__init__.py +23 -0
  4. maxapi/builders/keyboards.py +133 -0
  5. maxapi/builders/media.py +81 -0
  6. maxapi/callback_schema.py +140 -0
  7. maxapi/client/__init__.py +3 -0
  8. maxapi/client/default.py +89 -0
  9. maxapi/compat/__init__.py +15 -0
  10. maxapi/connection/__init__.py +3 -0
  11. maxapi/connection/base.py +136 -0
  12. maxapi/dispatcher.py +487 -0
  13. maxapi/exceptions/__init__.py +3 -0
  14. maxapi/exceptions/max.py +45 -0
  15. maxapi/filters/__init__.py +25 -0
  16. maxapi/filters/base.py +73 -0
  17. maxapi/filters/command.py +28 -0
  18. maxapi/filters/common.py +78 -0
  19. maxapi/filters/text.py +71 -0
  20. maxapi/fsm/__init__.py +17 -0
  21. maxapi/fsm/context.py +41 -0
  22. maxapi/fsm/filters.py +30 -0
  23. maxapi/fsm/middleware.py +42 -0
  24. maxapi/fsm/state.py +33 -0
  25. maxapi/fsm/storage/__init__.py +4 -0
  26. maxapi/fsm/storage/base.py +30 -0
  27. maxapi/fsm/storage/memory.py +38 -0
  28. maxapi/middlewares/__init__.py +3 -0
  29. maxapi/middlewares/base.py +34 -0
  30. maxapi/plugins/__init__.py +3 -0
  31. maxapi/plugins/base.py +19 -0
  32. maxapi/py.typed +1 -0
  33. maxapi/runners/__init__.py +4 -0
  34. maxapi/runners/polling.py +55 -0
  35. maxapi/runners/webhook.py +72 -0
  36. maxapi/transport/__init__.py +13 -0
  37. maxapi/transport/client.py +239 -0
  38. maxapi/transport/config.py +81 -0
  39. maxapi/transport/errors.py +45 -0
  40. maxapi/types/__init__.py +73 -0
  41. maxapi/types/base.py +9 -0
  42. maxapi/types/bot_mixin.py +14 -0
  43. maxapi/types/models.py +308 -0
  44. maxapi_sdk-0.12.0.dist-info/METADATA +270 -0
  45. maxapi_sdk-0.12.0.dist-info/RECORD +48 -0
  46. maxapi_sdk-0.12.0.dist-info/WHEEL +5 -0
  47. maxapi_sdk-0.12.0.dist-info/licenses/LICENSE +21 -0
  48. 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()
@@ -0,0 +1,3 @@
1
+ from .max import InvalidToken, MaxApiError, MaxConnection, MaxError
2
+
3
+ __all__ = ["InvalidToken", "MaxApiError", "MaxConnection", "MaxError"]
@@ -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()