telegrinder 0.1.dev20__py3-none-any.whl → 0.1.dev159__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.
Potentially problematic release.
This version of telegrinder might be problematic. Click here for more details.
- telegrinder/__init__.py +129 -22
- telegrinder/api/__init__.py +11 -2
- telegrinder/api/abc.py +25 -9
- telegrinder/api/api.py +47 -28
- telegrinder/api/error.py +14 -4
- telegrinder/api/response.py +11 -7
- telegrinder/bot/__init__.py +68 -7
- telegrinder/bot/bot.py +30 -24
- telegrinder/bot/cute_types/__init__.py +11 -1
- telegrinder/bot/cute_types/base.py +138 -0
- telegrinder/bot/cute_types/callback_query.py +458 -15
- telegrinder/bot/cute_types/inline_query.py +30 -24
- telegrinder/bot/cute_types/message.py +2982 -78
- telegrinder/bot/cute_types/update.py +30 -0
- telegrinder/bot/cute_types/utils.py +794 -0
- telegrinder/bot/dispatch/__init__.py +56 -3
- telegrinder/bot/dispatch/abc.py +9 -7
- telegrinder/bot/dispatch/composition.py +74 -0
- telegrinder/bot/dispatch/context.py +71 -0
- telegrinder/bot/dispatch/dispatch.py +86 -49
- telegrinder/bot/dispatch/handler/__init__.py +3 -0
- telegrinder/bot/dispatch/handler/abc.py +11 -5
- telegrinder/bot/dispatch/handler/func.py +41 -32
- telegrinder/bot/dispatch/handler/message_reply.py +46 -0
- telegrinder/bot/dispatch/middleware/__init__.py +2 -0
- telegrinder/bot/dispatch/middleware/abc.py +10 -4
- telegrinder/bot/dispatch/process.py +53 -49
- telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
- telegrinder/bot/dispatch/return_manager/abc.py +95 -0
- telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
- telegrinder/bot/dispatch/return_manager/message.py +25 -0
- telegrinder/bot/dispatch/view/__init__.py +14 -2
- telegrinder/bot/dispatch/view/abc.py +128 -2
- telegrinder/bot/dispatch/view/box.py +38 -0
- telegrinder/bot/dispatch/view/callback_query.py +13 -39
- telegrinder/bot/dispatch/view/inline_query.py +11 -39
- telegrinder/bot/dispatch/view/message.py +11 -47
- telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
- telegrinder/bot/polling/__init__.py +2 -0
- telegrinder/bot/polling/abc.py +11 -4
- telegrinder/bot/polling/polling.py +89 -40
- telegrinder/bot/rules/__init__.py +91 -5
- telegrinder/bot/rules/abc.py +81 -63
- telegrinder/bot/rules/adapter/__init__.py +11 -0
- telegrinder/bot/rules/adapter/abc.py +21 -0
- telegrinder/bot/rules/adapter/errors.py +5 -0
- telegrinder/bot/rules/adapter/event.py +49 -0
- telegrinder/bot/rules/adapter/raw_update.py +24 -0
- telegrinder/bot/rules/callback_data.py +159 -38
- telegrinder/bot/rules/command.py +116 -0
- telegrinder/bot/rules/enum_text.py +28 -0
- telegrinder/bot/rules/func.py +17 -17
- telegrinder/bot/rules/fuzzy.py +13 -10
- telegrinder/bot/rules/inline.py +61 -0
- telegrinder/bot/rules/integer.py +12 -7
- telegrinder/bot/rules/is_from.py +148 -7
- telegrinder/bot/rules/markup.py +21 -18
- telegrinder/bot/rules/mention.py +17 -0
- telegrinder/bot/rules/message_entities.py +33 -0
- telegrinder/bot/rules/regex.py +27 -19
- telegrinder/bot/rules/rule_enum.py +74 -0
- telegrinder/bot/rules/start.py +25 -13
- telegrinder/bot/rules/text.py +23 -14
- telegrinder/bot/scenario/__init__.py +2 -0
- telegrinder/bot/scenario/abc.py +12 -5
- telegrinder/bot/scenario/checkbox.py +48 -30
- telegrinder/bot/scenario/choice.py +16 -10
- telegrinder/client/__init__.py +3 -1
- telegrinder/client/abc.py +26 -16
- telegrinder/client/aiohttp.py +54 -32
- telegrinder/model.py +119 -40
- telegrinder/modules.py +189 -21
- telegrinder/msgspec_json.py +14 -0
- telegrinder/msgspec_utils.py +227 -0
- telegrinder/node/__init__.py +31 -0
- telegrinder/node/attachment.py +71 -0
- telegrinder/node/base.py +93 -0
- telegrinder/node/composer.py +71 -0
- telegrinder/node/container.py +22 -0
- telegrinder/node/message.py +18 -0
- telegrinder/node/rule.py +56 -0
- telegrinder/node/source.py +31 -0
- telegrinder/node/text.py +13 -0
- telegrinder/node/tools/__init__.py +3 -0
- telegrinder/node/tools/generator.py +40 -0
- telegrinder/node/update.py +12 -0
- telegrinder/rules.py +1 -1
- telegrinder/tools/__init__.py +138 -4
- telegrinder/tools/buttons.py +89 -51
- telegrinder/tools/error_handler/__init__.py +8 -0
- telegrinder/tools/error_handler/abc.py +30 -0
- telegrinder/tools/error_handler/error_handler.py +156 -0
- telegrinder/tools/formatting/__init__.py +81 -3
- telegrinder/tools/formatting/html.py +283 -37
- telegrinder/tools/formatting/links.py +32 -0
- telegrinder/tools/formatting/spec_html_formats.py +121 -0
- telegrinder/tools/global_context/__init__.py +12 -0
- telegrinder/tools/global_context/abc.py +66 -0
- telegrinder/tools/global_context/global_context.py +451 -0
- telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
- telegrinder/tools/i18n/__init__.py +12 -0
- telegrinder/tools/i18n/base.py +31 -0
- telegrinder/tools/i18n/middleware/__init__.py +3 -0
- telegrinder/tools/i18n/middleware/base.py +26 -0
- telegrinder/tools/i18n/simple.py +48 -0
- telegrinder/tools/kb_set/__init__.py +2 -0
- telegrinder/tools/kb_set/base.py +3 -0
- telegrinder/tools/kb_set/yaml.py +28 -17
- telegrinder/tools/keyboard.py +84 -62
- telegrinder/tools/loop_wrapper/__init__.py +4 -0
- telegrinder/tools/loop_wrapper/abc.py +18 -0
- telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
- telegrinder/tools/magic.py +48 -23
- telegrinder/tools/parse_mode.py +1 -2
- telegrinder/types/__init__.py +1 -0
- telegrinder/types/enums.py +653 -0
- telegrinder/types/methods.py +4107 -1279
- telegrinder/types/objects.py +4771 -1745
- {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/LICENSE +2 -1
- telegrinder-0.1.dev159.dist-info/METADATA +109 -0
- telegrinder-0.1.dev159.dist-info/RECORD +126 -0
- {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/WHEEL +1 -1
- telegrinder/bot/dispatch/waiter.py +0 -38
- telegrinder/result.py +0 -38
- telegrinder/tools/formatting/abc.py +0 -52
- telegrinder/tools/formatting/markdown.py +0 -57
- telegrinder-0.1.dev20.dist-info/METADATA +0 -22
- telegrinder-0.1.dev20.dist-info/RECORD +0 -71
telegrinder/bot/rules/abc.py
CHANGED
|
@@ -1,102 +1,120 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from telegrinder.bot.cute_types import MessageCute
|
|
3
|
-
from telegrinder.types import Update
|
|
4
|
-
import typing
|
|
5
|
-
import collections
|
|
6
1
|
import inspect
|
|
7
|
-
import
|
|
2
|
+
import typing
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
from telegrinder.bot.cute_types import BaseCute, MessageCute, UpdateCute
|
|
6
|
+
from telegrinder.bot.dispatch.context import Context
|
|
7
|
+
from telegrinder.bot.dispatch.process import check_rule
|
|
8
|
+
from telegrinder.bot.rules.adapter import ABCAdapter, EventAdapter, RawUpdateAdapter
|
|
9
|
+
from telegrinder.tools.i18n.base import ABCTranslator
|
|
10
|
+
from telegrinder.tools.magic import cache_translation, get_cached_translation
|
|
11
|
+
from telegrinder.types.objects import Update as UpdateObject
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
EventScheme = collections.namedtuple("EventScheme", ["name", "dataclass"])
|
|
13
|
+
T = typing.TypeVar("T", bound=BaseCute)
|
|
14
14
|
|
|
15
|
+
Message: typing.TypeAlias = MessageCute
|
|
16
|
+
Update: typing.TypeAlias = UpdateCute
|
|
15
17
|
|
|
16
|
-
class ABCRule(ABC, typing.Generic[T]):
|
|
17
|
-
__event__: typing.Optional[EventScheme] = None
|
|
18
|
-
require: typing.List["ABCRule[T]"] = []
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
def with_caching_translations(func):
|
|
20
|
+
"""Should be used as decorator for .translate method. Caches rule translations."""
|
|
21
|
+
|
|
22
|
+
async def wrapper(self: "ABCRule", translator: ABCTranslator):
|
|
23
|
+
if translation := get_cached_translation(self, translator.locale):
|
|
24
|
+
return translation
|
|
25
|
+
translation = await func(self, translator)
|
|
26
|
+
cache_translation(self, translator.locale, translation)
|
|
27
|
+
return translation
|
|
28
|
+
|
|
29
|
+
return wrapper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ABCRule(ABC, typing.Generic[T]):
|
|
33
|
+
adapter: ABCAdapter[UpdateObject, T] = RawUpdateAdapter() # type: ignore
|
|
34
|
+
requires: list["ABCRule[T]"] = []
|
|
27
35
|
|
|
28
36
|
@abstractmethod
|
|
29
|
-
async def check(self, event: T, ctx:
|
|
37
|
+
async def check(self, event: T, ctx: Context) -> bool:
|
|
30
38
|
pass
|
|
31
39
|
|
|
32
|
-
def __init_subclass__(cls,
|
|
33
|
-
"""Merges requirements from inherited classes and rule-specific requirements"""
|
|
40
|
+
def __init_subclass__(cls, requires: list["ABCRule[T]"] | None = None):
|
|
41
|
+
"""Merges requirements from inherited classes and rule-specific requirements."""
|
|
42
|
+
|
|
34
43
|
requirements = []
|
|
35
44
|
for base in inspect.getmro(cls):
|
|
36
45
|
if issubclass(base, ABCRule) and base != cls:
|
|
37
|
-
requirements.extend(base.
|
|
46
|
+
requirements.extend(base.requires or ()) # type: ignore
|
|
38
47
|
|
|
39
|
-
requirements.extend(
|
|
40
|
-
cls.
|
|
48
|
+
requirements.extend(requires or ())
|
|
49
|
+
cls.requires = list(dict.fromkeys(requirements))
|
|
41
50
|
|
|
42
|
-
def __and__(self, other: "ABCRule"):
|
|
51
|
+
def __and__(self, other: "ABCRule[T]"):
|
|
43
52
|
return AndRule(self, other)
|
|
44
53
|
|
|
45
|
-
def __or__(self, other: "ABCRule"):
|
|
54
|
+
def __or__(self, other: "ABCRule[T]"):
|
|
46
55
|
return OrRule(self, other)
|
|
47
56
|
|
|
57
|
+
def __neg__(self) -> "ABCRule[T]":
|
|
58
|
+
return NotRule(self)
|
|
59
|
+
|
|
48
60
|
def __repr__(self) -> str:
|
|
49
|
-
return
|
|
61
|
+
return "<rule: {!r}, adapter: {!r}>".format(
|
|
62
|
+
self.__class__.__name__,
|
|
63
|
+
self.adapter,
|
|
64
|
+
)
|
|
50
65
|
|
|
66
|
+
async def translate(self, translator: ABCTranslator) -> typing.Self:
|
|
67
|
+
return self
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
|
|
70
|
+
class AndRule(ABCRule[T]):
|
|
71
|
+
def __init__(self, *rules: ABCRule[T]):
|
|
54
72
|
self.rules = rules
|
|
55
73
|
|
|
56
|
-
async def check(self, event: Update, ctx:
|
|
74
|
+
async def check(self, event: Update, ctx: Context) -> bool:
|
|
57
75
|
ctx_copy = ctx.copy()
|
|
58
76
|
for rule in self.rules:
|
|
59
|
-
|
|
60
|
-
if rule.__event__:
|
|
61
|
-
event_dict = event.to_dict()
|
|
62
|
-
if rule.__event__.name not in event:
|
|
63
|
-
return False
|
|
64
|
-
e = rule.__event__.dataclass(
|
|
65
|
-
**event_dict[rule.__event__.name].to_dict()
|
|
66
|
-
)
|
|
67
|
-
if not await rule.run_check(e, ctx_copy):
|
|
77
|
+
if not await check_rule(event.ctx_api, rule, event, ctx_copy):
|
|
68
78
|
return False
|
|
69
|
-
ctx
|
|
70
|
-
ctx.update(ctx_copy)
|
|
79
|
+
ctx |= ctx_copy
|
|
71
80
|
return True
|
|
72
81
|
|
|
73
82
|
|
|
74
|
-
class OrRule(ABCRule):
|
|
75
|
-
def __init__(self, *rules: ABCRule):
|
|
83
|
+
class OrRule(ABCRule[T]):
|
|
84
|
+
def __init__(self, *rules: ABCRule[T]):
|
|
76
85
|
self.rules = rules
|
|
77
86
|
|
|
78
|
-
async def check(self, event:
|
|
79
|
-
ctx_copy = ctx.copy()
|
|
80
|
-
found = False
|
|
81
|
-
|
|
87
|
+
async def check(self, event: Update, ctx: Context) -> bool:
|
|
82
88
|
for rule in self.rules:
|
|
83
|
-
|
|
84
|
-
if rule
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if await rule.run_check(e, ctx_copy):
|
|
89
|
-
found = True
|
|
90
|
-
break
|
|
89
|
+
ctx_copy = ctx.copy()
|
|
90
|
+
if await check_rule(event.ctx_api, rule, event, ctx_copy):
|
|
91
|
+
ctx |= ctx_copy
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
91
94
|
|
|
92
|
-
ctx.clear()
|
|
93
|
-
ctx.update(ctx_copy)
|
|
94
|
-
return found
|
|
95
95
|
|
|
96
|
+
class NotRule(ABCRule[T]):
|
|
97
|
+
def __init__(self, rule: ABCRule[T]):
|
|
98
|
+
self.rule = rule
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
async def check(self, event: Update, ctx: Context) -> bool:
|
|
101
|
+
ctx_copy = ctx.copy()
|
|
102
|
+
return not await check_rule(event.ctx_api, self.rule, event, ctx_copy)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MessageRule(ABCRule[Message], ABC, requires=[]):
|
|
106
|
+
adapter = EventAdapter("message", Message)
|
|
99
107
|
|
|
100
108
|
@abstractmethod
|
|
101
|
-
async def check(self, message: Message, ctx:
|
|
109
|
+
async def check(self, message: Message, ctx: Context) -> bool:
|
|
102
110
|
...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = (
|
|
114
|
+
"ABCRule",
|
|
115
|
+
"AndRule",
|
|
116
|
+
"MessageRule",
|
|
117
|
+
"NotRule",
|
|
118
|
+
"OrRule",
|
|
119
|
+
"with_caching_translations",
|
|
120
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from fntypes.result import Result
|
|
5
|
+
|
|
6
|
+
from telegrinder.api.abc import ABCAPI
|
|
7
|
+
from telegrinder.bot.cute_types import BaseCute
|
|
8
|
+
from telegrinder.bot.rules.adapter.errors import AdapterError
|
|
9
|
+
from telegrinder.model import Model
|
|
10
|
+
|
|
11
|
+
UpdateT = typing.TypeVar("UpdateT", bound=Model)
|
|
12
|
+
CuteT = typing.TypeVar("CuteT", bound=BaseCute)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ABCAdapter(abc.ABC, typing.Generic[UpdateT, CuteT]):
|
|
16
|
+
@abc.abstractmethod
|
|
17
|
+
async def adapt(self, api: ABCAPI, update: UpdateT) -> Result[CuteT, AdapterError]:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ("ABCAdapter",)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from fntypes.result import Error, Ok, Result
|
|
4
|
+
|
|
5
|
+
from telegrinder.api.abc import ABCAPI
|
|
6
|
+
from telegrinder.bot.cute_types import BaseCute
|
|
7
|
+
from telegrinder.bot.rules.adapter.abc import ABCAdapter
|
|
8
|
+
from telegrinder.bot.rules.adapter.errors import AdapterError
|
|
9
|
+
from telegrinder.msgspec_utils import Nothing
|
|
10
|
+
from telegrinder.types.objects import Model, Update
|
|
11
|
+
|
|
12
|
+
EventT = typing.TypeVar("EventT", bound=Model)
|
|
13
|
+
CuteT = typing.TypeVar("CuteT", bound=BaseCute)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EventAdapter(ABCAdapter[Update, CuteT]):
|
|
17
|
+
def __init__(self, event_name: str, model: type[CuteT]) -> None:
|
|
18
|
+
self.event_name = event_name
|
|
19
|
+
self.model = model
|
|
20
|
+
|
|
21
|
+
def __repr__(self) -> str:
|
|
22
|
+
raw_update_type = Update.__annotations__.get(self.event_name, "Unknown")
|
|
23
|
+
raw_update_type = (
|
|
24
|
+
typing.get_args(raw_update_type)[0].__forward_arg__
|
|
25
|
+
if typing.get_args(raw_update_type)
|
|
26
|
+
else raw_update_type
|
|
27
|
+
)
|
|
28
|
+
return "<{}: adapt {} -> {}>".format(
|
|
29
|
+
self.__class__.__name__,
|
|
30
|
+
raw_update_type,
|
|
31
|
+
self.model.__name__,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async def adapt(self, api: ABCAPI, update: Update) -> Result[CuteT, AdapterError]:
|
|
35
|
+
update_dct = update.to_dict()
|
|
36
|
+
if self.event_name not in update_dct:
|
|
37
|
+
return Error(
|
|
38
|
+
AdapterError(f"Update is not of event type {self.event_name!r}."),
|
|
39
|
+
)
|
|
40
|
+
if update_dct[self.event_name] is Nothing:
|
|
41
|
+
return Error(
|
|
42
|
+
AdapterError(f"Update is not an {self.event_name!r}."),
|
|
43
|
+
)
|
|
44
|
+
return Ok(
|
|
45
|
+
self.model.from_update(update_dct[self.event_name].unwrap(), bound_api=api),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ("EventAdapter",)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from fntypes.result import Ok, Result
|
|
2
|
+
|
|
3
|
+
from telegrinder.api.abc import ABCAPI
|
|
4
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
5
|
+
from telegrinder.bot.rules.adapter.abc import ABCAdapter
|
|
6
|
+
from telegrinder.bot.rules.adapter.errors import AdapterError
|
|
7
|
+
from telegrinder.types.objects import Update
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RawUpdateAdapter(ABCAdapter[Update, UpdateCute]):
|
|
11
|
+
def __repr__(self) -> str:
|
|
12
|
+
return f"<{self.__class__.__name__}: adapt Update -> UpdateCute>"
|
|
13
|
+
|
|
14
|
+
async def adapt(
|
|
15
|
+
self,
|
|
16
|
+
api: ABCAPI,
|
|
17
|
+
update: Update,
|
|
18
|
+
) -> Result[UpdateCute, AdapterError]:
|
|
19
|
+
if not isinstance(update, UpdateCute):
|
|
20
|
+
return Ok(UpdateCute.from_update(update, api))
|
|
21
|
+
return Ok(update)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ("RawUpdateAdapter",)
|
|
@@ -1,57 +1,178 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from telegrinder.types import Update
|
|
4
|
-
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
5
|
-
from .markup import Markup, check_string
|
|
6
|
-
import msgspec
|
|
7
|
-
import vbml
|
|
1
|
+
import abc
|
|
2
|
+
import inspect
|
|
8
3
|
import typing
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
|
|
6
|
+
import msgspec
|
|
7
|
+
|
|
8
|
+
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
9
|
+
from telegrinder.bot.dispatch.context import Context
|
|
10
|
+
from telegrinder.bot.rules.adapter import EventAdapter
|
|
11
|
+
from telegrinder.model import decoder
|
|
12
|
+
from telegrinder.tools.buttons import DataclassInstance
|
|
13
|
+
|
|
14
|
+
from .abc import ABCRule
|
|
15
|
+
from .markup import Markup, PatternLike, check_string
|
|
16
|
+
|
|
17
|
+
if typing.TYPE_CHECKING:
|
|
18
|
+
|
|
19
|
+
T = typing.TypeVar("T")
|
|
20
|
+
Ref: typing.TypeAlias = typing.Annotated[T, ...]
|
|
21
|
+
else:
|
|
22
|
+
|
|
23
|
+
class Ref:
|
|
24
|
+
def __class_getitem__(cls, code: str) -> typing.ForwardRef:
|
|
25
|
+
return typing.ForwardRef(code)
|
|
26
|
+
|
|
9
27
|
|
|
10
28
|
CallbackQuery = CallbackQueryCute
|
|
11
|
-
|
|
29
|
+
Validator: typing.TypeAlias = typing.Callable[[typing.Any], bool | typing.Awaitable[bool]]
|
|
30
|
+
MapDict: typing.TypeAlias = dict[
|
|
31
|
+
str, typing.Any | type[typing.Any] | Validator | list[Ref["MapDict"]] | Ref["MapDict"]
|
|
32
|
+
]
|
|
33
|
+
CallbackMap: typing.TypeAlias = list[tuple[str, typing.Any | type | Validator | Ref["CallbackMap"]]]
|
|
34
|
+
CallbackMapStrict: typing.TypeAlias = list[tuple[str, Validator | Ref["CallbackMapStrict"]]]
|
|
12
35
|
|
|
13
36
|
|
|
14
|
-
class
|
|
15
|
-
|
|
16
|
-
self.value = value
|
|
37
|
+
class CallbackQueryRule(ABCRule[CallbackQuery], abc.ABC):
|
|
38
|
+
adapter = EventAdapter("callback_query", CallbackQuery)
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
|
|
40
|
+
@abc.abstractmethod
|
|
41
|
+
async def check(self, event: CallbackQuery, ctx: Context) -> bool:
|
|
42
|
+
pass
|
|
20
43
|
|
|
21
44
|
|
|
22
|
-
class
|
|
23
|
-
def
|
|
24
|
-
|
|
45
|
+
class HasData(CallbackQueryRule):
|
|
46
|
+
async def check(self, event: CallbackQuery, ctx: Context) -> bool:
|
|
47
|
+
return bool(event.data or event.data.unwrap())
|
|
25
48
|
|
|
26
|
-
async def check(self, event: Update, ctx: dict) -> bool:
|
|
27
|
-
if not event.callback_query.data:
|
|
28
|
-
return False
|
|
29
|
-
try:
|
|
30
|
-
# todo: use msgspec
|
|
31
|
-
return json.loads(event.callback_query.data) == self.d
|
|
32
|
-
except:
|
|
33
|
-
return False
|
|
34
49
|
|
|
50
|
+
class CallbackQueryDataRule(CallbackQueryRule, abc.ABC, requires=[HasData()]):
|
|
51
|
+
pass
|
|
35
52
|
|
|
36
|
-
class CallbackDataJsonModel(ABCRule[CallbackQuery]):
|
|
37
|
-
__event__ = EventScheme("callback_query", CallbackQuery)
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
class CallbackDataMap(CallbackQueryDataRule):
|
|
55
|
+
def __init__(self, mapping: MapDict) -> None:
|
|
56
|
+
self.mapping = self.transform_to_callbacks(
|
|
57
|
+
self.transform_to_map(mapping),
|
|
58
|
+
)
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
@classmethod
|
|
61
|
+
def transform_to_map(cls, mapping: MapDict) -> CallbackMap:
|
|
62
|
+
"""Transforms MapDict to CallbackMap."""
|
|
63
|
+
|
|
64
|
+
callback_map = []
|
|
65
|
+
|
|
66
|
+
for k, v in mapping.items():
|
|
67
|
+
if isinstance(v, dict):
|
|
68
|
+
v = cls.transform_to_map(v)
|
|
69
|
+
callback_map.append((k, v))
|
|
70
|
+
|
|
71
|
+
return callback_map
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def transform_to_callbacks(cls, callback_map: CallbackMap) -> CallbackMapStrict:
|
|
75
|
+
"""Transforms `CallbackMap` to `CallbackMapStrict`."""
|
|
76
|
+
|
|
77
|
+
callback_map_result = []
|
|
78
|
+
|
|
79
|
+
for key, value in callback_map:
|
|
80
|
+
if isinstance(value, type):
|
|
81
|
+
validator = (lambda tp: lambda v: isinstance(v, tp))(value)
|
|
82
|
+
elif isinstance(value, list):
|
|
83
|
+
validator = cls.transform_to_callbacks(value)
|
|
84
|
+
elif not callable(value):
|
|
85
|
+
validator = (lambda val: lambda v: val == v)(value)
|
|
86
|
+
else:
|
|
87
|
+
validator = value
|
|
88
|
+
callback_map_result.append((key, validator))
|
|
89
|
+
|
|
90
|
+
return callback_map_result
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
async def run_validator(value: typing.Any, validator: Validator) -> bool:
|
|
94
|
+
"""Run async or sync validator."""
|
|
95
|
+
|
|
96
|
+
with suppress(BaseException):
|
|
97
|
+
result = validator(value)
|
|
98
|
+
if inspect.isawaitable(result):
|
|
99
|
+
result = await result
|
|
100
|
+
return result # type: ignore
|
|
101
|
+
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
async def match(cls, callback_data: dict, callback_map: CallbackMapStrict) -> bool:
|
|
106
|
+
"""Matches callback_data with callback_map recursively."""
|
|
107
|
+
|
|
108
|
+
for key, validator in callback_map:
|
|
109
|
+
if key not in callback_data:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
if isinstance(validator, list):
|
|
113
|
+
if not (
|
|
114
|
+
isinstance(callback_data[key], dict)
|
|
115
|
+
and await cls.match(callback_data[key], validator)
|
|
116
|
+
):
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
elif not await cls.run_validator(callback_data[key], validator):
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
async def check(self, event: CallbackQuery, ctx: Context) -> bool:
|
|
125
|
+
callback_data = event.decode_callback_data().unwrap_or_none()
|
|
126
|
+
if callback_data is None:
|
|
47
127
|
return False
|
|
128
|
+
if await self.match(callback_data, self.mapping):
|
|
129
|
+
ctx.update(callback_data)
|
|
130
|
+
return True
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class CallbackDataEq(CallbackQueryDataRule):
|
|
135
|
+
def __init__(self, value: str):
|
|
136
|
+
self.value = value
|
|
48
137
|
|
|
138
|
+
async def check(self, event: CallbackQuery, ctx: Context) -> bool:
|
|
139
|
+
return event.data.unwrap() == self.value
|
|
49
140
|
|
|
50
|
-
class CallbackDataMarkup(ABCRule[CallbackQuery]):
|
|
51
|
-
__event__ = EventScheme("callback_query", CallbackQuery)
|
|
52
141
|
|
|
53
|
-
|
|
142
|
+
class CallbackDataJsonEq(CallbackQueryDataRule):
|
|
143
|
+
def __init__(self, d: dict[str, typing.Any]):
|
|
144
|
+
self.d = d
|
|
145
|
+
|
|
146
|
+
async def check(self, event: CallbackQuery, ctx: Context) -> bool:
|
|
147
|
+
return event.decode_callback_data().unwrap_or_none() == self.d
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CallbackDataJsonModel(CallbackQueryDataRule):
|
|
151
|
+
def __init__(self, model: type[msgspec.Struct] | type[DataclassInstance]):
|
|
152
|
+
self.model = model
|
|
153
|
+
|
|
154
|
+
async def check(self, event: CallbackQuery, ctx: Context) -> bool:
|
|
155
|
+
with suppress(BaseException):
|
|
156
|
+
ctx.data = decoder.decode(event.data.unwrap().encode(), type=self.model)
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class CallbackDataMarkup(CallbackQueryDataRule):
|
|
162
|
+
def __init__(self, patterns: PatternLike | list[PatternLike]):
|
|
54
163
|
self.patterns = Markup(patterns).patterns
|
|
55
164
|
|
|
56
|
-
async def check(self, event: CallbackQuery, ctx:
|
|
57
|
-
return check_string(self.patterns, event.data, ctx)
|
|
165
|
+
async def check(self, event: CallbackQuery, ctx: Context) -> bool:
|
|
166
|
+
return check_string(self.patterns, event.data.unwrap(), ctx)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
__all__ = (
|
|
170
|
+
"CallbackDataEq",
|
|
171
|
+
"CallbackDataJsonEq",
|
|
172
|
+
"CallbackDataJsonModel",
|
|
173
|
+
"CallbackDataMap",
|
|
174
|
+
"CallbackDataMarkup",
|
|
175
|
+
"CallbackQueryDataRule",
|
|
176
|
+
"CallbackQueryRule",
|
|
177
|
+
"HasData",
|
|
178
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from telegrinder.bot.dispatch.context import Context
|
|
5
|
+
|
|
6
|
+
from .abc import Message
|
|
7
|
+
from .text import TextMessageRule
|
|
8
|
+
|
|
9
|
+
Validator = typing.Callable[[str], typing.Any | None]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def single_split(s: str, separator: str) -> tuple[str, str]:
|
|
13
|
+
left, *right = s.split(separator, 1)
|
|
14
|
+
return left, (right[0] if right else "")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass(frozen=True)
|
|
18
|
+
class Argument:
|
|
19
|
+
name: str
|
|
20
|
+
validators: list[Validator] = dataclasses.field(default_factory=lambda: [])
|
|
21
|
+
optional: bool = dataclasses.field(default=False, kw_only=True)
|
|
22
|
+
|
|
23
|
+
def check(self, data: str) -> typing.Any | None:
|
|
24
|
+
for validator in self.validators:
|
|
25
|
+
data = validator(data) # type: ignore
|
|
26
|
+
if data is None:
|
|
27
|
+
return None
|
|
28
|
+
return data
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Command(TextMessageRule):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
names: str | typing.Iterable[str],
|
|
35
|
+
*arguments: Argument,
|
|
36
|
+
prefixes: tuple[str, ...] = ("/",),
|
|
37
|
+
separator: str = " ",
|
|
38
|
+
lazy: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.names = [names] if isinstance(names, str) else names
|
|
41
|
+
self.arguments = arguments
|
|
42
|
+
self.prefixes = prefixes
|
|
43
|
+
self.separator = separator
|
|
44
|
+
self.lazy = lazy
|
|
45
|
+
|
|
46
|
+
def remove_prefix(self, text: str) -> str | None:
|
|
47
|
+
for prefix in self.prefixes:
|
|
48
|
+
if text.startswith(prefix):
|
|
49
|
+
return text.removeprefix(prefix)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def parse_argument(
|
|
53
|
+
self,
|
|
54
|
+
arguments: list[Argument],
|
|
55
|
+
data_s: str,
|
|
56
|
+
new_s: str,
|
|
57
|
+
s: str,
|
|
58
|
+
) -> dict | None:
|
|
59
|
+
argument = arguments[0]
|
|
60
|
+
data = argument.check(data_s)
|
|
61
|
+
if data is None and not argument.optional:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
if data is None:
|
|
65
|
+
return self.parse_arguments(arguments[1:], s)
|
|
66
|
+
|
|
67
|
+
with_argument = self.parse_arguments(arguments[1:], new_s)
|
|
68
|
+
if with_argument is not None:
|
|
69
|
+
return {argument.name: data, **with_argument}
|
|
70
|
+
|
|
71
|
+
if not argument.optional:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
return self.parse_arguments(arguments[1:], s)
|
|
75
|
+
|
|
76
|
+
def parse_arguments(self, arguments: list[Argument], s: str) -> dict | None:
|
|
77
|
+
if not arguments:
|
|
78
|
+
return {} if not s else None
|
|
79
|
+
|
|
80
|
+
if self.lazy:
|
|
81
|
+
return self.parse_argument(arguments, *single_split(s, self.separator), s)
|
|
82
|
+
|
|
83
|
+
all_split = s.split(self.separator)
|
|
84
|
+
for i in range(1, len(all_split) + 1):
|
|
85
|
+
ctx = self.parse_argument(
|
|
86
|
+
arguments,
|
|
87
|
+
self.separator.join(all_split[:i]),
|
|
88
|
+
self.separator.join(all_split[i:]),
|
|
89
|
+
s,
|
|
90
|
+
)
|
|
91
|
+
if ctx is not None:
|
|
92
|
+
return ctx
|
|
93
|
+
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
async def check(self, message: Message, ctx: Context) -> bool:
|
|
97
|
+
text = self.remove_prefix(message.text.unwrap())
|
|
98
|
+
if text is None:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
name, arguments = single_split(text, self.separator)
|
|
102
|
+
if name not in self.names:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
if not self.arguments:
|
|
106
|
+
return not arguments
|
|
107
|
+
|
|
108
|
+
result = self.parse_arguments(list(self.arguments), arguments)
|
|
109
|
+
if result is None:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
ctx.update(result)
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
__all__ = ("Argument", "Command", "single_split")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from telegrinder.bot.dispatch.context import Context
|
|
5
|
+
|
|
6
|
+
from .abc import Message
|
|
7
|
+
from .text import TextMessageRule
|
|
8
|
+
|
|
9
|
+
T = typing.TypeVar("T", bound=enum.Enum, covariant=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EnumTextRule(TextMessageRule, typing.Generic[T]):
|
|
13
|
+
def __init__(self, enum_t: type[T], *, lower_case: bool = True) -> None:
|
|
14
|
+
self.enum_t = enum_t
|
|
15
|
+
self.texts = list(map(lambda x: x.value.lower() if lower_case else x.value, self.enum_t))
|
|
16
|
+
|
|
17
|
+
def find(self, s: str) -> T:
|
|
18
|
+
for enumeration in self.enum_t:
|
|
19
|
+
if enumeration.value.lower() == s:
|
|
20
|
+
return enumeration
|
|
21
|
+
raise KeyError("Enumeration is undefined.")
|
|
22
|
+
|
|
23
|
+
async def check(self, message: Message, ctx: Context) -> bool:
|
|
24
|
+
text = message.text.unwrap().lower()
|
|
25
|
+
if text not in self.texts:
|
|
26
|
+
return False
|
|
27
|
+
ctx.enum_text = self.find(text)
|
|
28
|
+
return True
|