telegrinder 1.0.0rc1__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.
- telegrinder/__init__.py +258 -0
- telegrinder/__meta__.py +1 -0
- telegrinder/api/__init__.py +15 -0
- telegrinder/api/api.py +175 -0
- telegrinder/api/error.py +50 -0
- telegrinder/api/response.py +23 -0
- telegrinder/api/token.py +30 -0
- telegrinder/api/validators.py +30 -0
- telegrinder/bot/__init__.py +144 -0
- telegrinder/bot/bot.py +70 -0
- telegrinder/bot/cute_types/__init__.py +41 -0
- telegrinder/bot/cute_types/base.py +228 -0
- telegrinder/bot/cute_types/base.pyi +49 -0
- telegrinder/bot/cute_types/business_connection.py +9 -0
- telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
- telegrinder/bot/cute_types/callback_query.py +248 -0
- telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
- telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
- telegrinder/bot/cute_types/chat_join_request.py +59 -0
- telegrinder/bot/cute_types/chat_member_updated.py +158 -0
- telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
- telegrinder/bot/cute_types/inline_query.py +41 -0
- telegrinder/bot/cute_types/message.py +2809 -0
- telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
- telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
- telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
- telegrinder/bot/cute_types/poll.py +9 -0
- telegrinder/bot/cute_types/poll_answer.py +9 -0
- telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
- telegrinder/bot/cute_types/shipping_query.py +11 -0
- telegrinder/bot/cute_types/update.py +209 -0
- telegrinder/bot/cute_types/utils.py +141 -0
- telegrinder/bot/dispatch/__init__.py +99 -0
- telegrinder/bot/dispatch/abc.py +74 -0
- telegrinder/bot/dispatch/action.py +99 -0
- telegrinder/bot/dispatch/context.py +162 -0
- telegrinder/bot/dispatch/dispatch.py +362 -0
- telegrinder/bot/dispatch/handler/__init__.py +23 -0
- telegrinder/bot/dispatch/handler/abc.py +25 -0
- telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
- telegrinder/bot/dispatch/handler/base.py +34 -0
- telegrinder/bot/dispatch/handler/document_reply.py +43 -0
- telegrinder/bot/dispatch/handler/func.py +73 -0
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
- telegrinder/bot/dispatch/handler/message_reply.py +35 -0
- telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
- telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
- telegrinder/bot/dispatch/handler/video_reply.py +43 -0
- telegrinder/bot/dispatch/middleware/__init__.py +13 -0
- telegrinder/bot/dispatch/middleware/abc.py +112 -0
- telegrinder/bot/dispatch/middleware/box.py +32 -0
- telegrinder/bot/dispatch/middleware/filter.py +88 -0
- telegrinder/bot/dispatch/middleware/media_group.py +69 -0
- telegrinder/bot/dispatch/process.py +93 -0
- telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
- telegrinder/bot/dispatch/return_manager/abc.py +107 -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 +34 -0
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/utils.py +20 -0
- telegrinder/bot/dispatch/router/__init__.py +4 -0
- telegrinder/bot/dispatch/router/abc.py +15 -0
- telegrinder/bot/dispatch/router/base.py +154 -0
- telegrinder/bot/dispatch/view/__init__.py +15 -0
- telegrinder/bot/dispatch/view/abc.py +15 -0
- telegrinder/bot/dispatch/view/base.py +226 -0
- telegrinder/bot/dispatch/view/box.py +207 -0
- telegrinder/bot/dispatch/view/media_group.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
- telegrinder/bot/polling/__init__.py +4 -0
- telegrinder/bot/polling/abc.py +25 -0
- telegrinder/bot/polling/error_handler.py +93 -0
- telegrinder/bot/polling/polling.py +167 -0
- telegrinder/bot/polling/utils.py +12 -0
- telegrinder/bot/rules/__init__.py +166 -0
- telegrinder/bot/rules/abc.py +150 -0
- telegrinder/bot/rules/button.py +20 -0
- telegrinder/bot/rules/callback_data.py +109 -0
- telegrinder/bot/rules/chat_join.py +28 -0
- telegrinder/bot/rules/chat_member_updated.py +145 -0
- telegrinder/bot/rules/command.py +137 -0
- telegrinder/bot/rules/enum_text.py +29 -0
- telegrinder/bot/rules/func.py +21 -0
- telegrinder/bot/rules/fuzzy.py +21 -0
- telegrinder/bot/rules/inline.py +45 -0
- telegrinder/bot/rules/integer.py +19 -0
- telegrinder/bot/rules/is_from.py +213 -0
- telegrinder/bot/rules/logic.py +22 -0
- telegrinder/bot/rules/magic.py +60 -0
- telegrinder/bot/rules/markup.py +51 -0
- telegrinder/bot/rules/media.py +13 -0
- telegrinder/bot/rules/mention.py +15 -0
- telegrinder/bot/rules/message_entities.py +37 -0
- telegrinder/bot/rules/node.py +43 -0
- telegrinder/bot/rules/payload.py +89 -0
- telegrinder/bot/rules/payment_invoice.py +14 -0
- telegrinder/bot/rules/regex.py +34 -0
- telegrinder/bot/rules/rule_enum.py +71 -0
- telegrinder/bot/rules/start.py +73 -0
- telegrinder/bot/rules/state.py +35 -0
- telegrinder/bot/rules/text.py +27 -0
- telegrinder/bot/rules/update.py +14 -0
- telegrinder/bot/scenario/__init__.py +5 -0
- telegrinder/bot/scenario/abc.py +16 -0
- telegrinder/bot/scenario/checkbox.py +183 -0
- telegrinder/bot/scenario/choice.py +44 -0
- telegrinder/client/__init__.py +11 -0
- telegrinder/client/abc.py +136 -0
- telegrinder/client/form_data.py +34 -0
- telegrinder/client/rnet.py +198 -0
- telegrinder/model.py +133 -0
- telegrinder/model.pyi +57 -0
- telegrinder/modules.py +1081 -0
- telegrinder/msgspec_utils/__init__.py +42 -0
- telegrinder/msgspec_utils/abc.py +16 -0
- telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
- telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
- telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
- telegrinder/msgspec_utils/custom_types/literal.py +25 -0
- telegrinder/msgspec_utils/custom_types/option.py +17 -0
- telegrinder/msgspec_utils/decoder.py +388 -0
- telegrinder/msgspec_utils/encoder.py +204 -0
- telegrinder/msgspec_utils/json.py +15 -0
- telegrinder/msgspec_utils/tools.py +80 -0
- telegrinder/node/__init__.py +80 -0
- telegrinder/node/compose.py +193 -0
- telegrinder/node/nodes/__init__.py +96 -0
- telegrinder/node/nodes/attachment.py +169 -0
- telegrinder/node/nodes/callback_query.py +25 -0
- telegrinder/node/nodes/channel.py +97 -0
- telegrinder/node/nodes/command.py +33 -0
- telegrinder/node/nodes/error.py +43 -0
- telegrinder/node/nodes/event.py +70 -0
- telegrinder/node/nodes/file.py +39 -0
- telegrinder/node/nodes/global_node.py +66 -0
- telegrinder/node/nodes/i18n.py +110 -0
- telegrinder/node/nodes/me.py +26 -0
- telegrinder/node/nodes/message_entities.py +15 -0
- telegrinder/node/nodes/payload.py +84 -0
- telegrinder/node/nodes/reply_message.py +14 -0
- telegrinder/node/nodes/source.py +172 -0
- telegrinder/node/nodes/state_mutator.py +71 -0
- telegrinder/node/nodes/text.py +62 -0
- telegrinder/node/scope.py +88 -0
- telegrinder/node/utils.py +38 -0
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +1 -0
- telegrinder/tools/__init__.py +183 -0
- telegrinder/tools/aio.py +147 -0
- telegrinder/tools/final.py +21 -0
- telegrinder/tools/formatting/__init__.py +85 -0
- telegrinder/tools/formatting/deep_links/__init__.py +39 -0
- telegrinder/tools/formatting/deep_links/links.py +468 -0
- telegrinder/tools/formatting/deep_links/parsing.py +88 -0
- telegrinder/tools/formatting/deep_links/validators.py +8 -0
- telegrinder/tools/formatting/html.py +241 -0
- telegrinder/tools/fullname.py +82 -0
- telegrinder/tools/global_context/__init__.py +13 -0
- telegrinder/tools/global_context/abc.py +63 -0
- telegrinder/tools/global_context/builtin_context.py +45 -0
- telegrinder/tools/global_context/global_context.py +614 -0
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard/__init__.py +6 -0
- telegrinder/tools/keyboard/abc.py +84 -0
- telegrinder/tools/keyboard/base.py +108 -0
- telegrinder/tools/keyboard/button.py +181 -0
- telegrinder/tools/keyboard/data.py +31 -0
- telegrinder/tools/keyboard/keyboard.py +160 -0
- telegrinder/tools/keyboard/utils.py +95 -0
- telegrinder/tools/lifespan.py +188 -0
- telegrinder/tools/limited_dict.py +35 -0
- telegrinder/tools/loop_wrapper.py +271 -0
- telegrinder/tools/magic/__init__.py +29 -0
- telegrinder/tools/magic/annotations.py +172 -0
- telegrinder/tools/magic/descriptors.py +57 -0
- telegrinder/tools/magic/function.py +254 -0
- telegrinder/tools/magic/inspect.py +16 -0
- telegrinder/tools/magic/shortcut.py +107 -0
- telegrinder/tools/member_descriptor_proxy.py +95 -0
- telegrinder/tools/parse_mode.py +12 -0
- telegrinder/tools/serialization/__init__.py +5 -0
- telegrinder/tools/serialization/abc.py +34 -0
- telegrinder/tools/serialization/json_ser.py +60 -0
- telegrinder/tools/serialization/msgpack_ser.py +197 -0
- telegrinder/tools/serialization/utils.py +18 -0
- telegrinder/tools/singleton/__init__.py +4 -0
- telegrinder/tools/singleton/abc.py +14 -0
- telegrinder/tools/singleton/singleton.py +18 -0
- telegrinder/tools/state_mutator/__init__.py +4 -0
- telegrinder/tools/state_mutator/mutation.py +85 -0
- telegrinder/tools/state_storage/__init__.py +4 -0
- telegrinder/tools/state_storage/abc.py +38 -0
- telegrinder/tools/state_storage/memory.py +27 -0
- telegrinder/tools/strings.py +22 -0
- telegrinder/types/__init__.py +323 -0
- telegrinder/types/enums.py +754 -0
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +6143 -0
- telegrinder/types/methods_utils.py +66 -0
- telegrinder/types/objects.py +8184 -0
- telegrinder/types/webapp.py +129 -0
- telegrinder/verification_utils.py +35 -0
- telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
- telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
- telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
- telegrinder-1.0.0rc1.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import typing
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
|
|
5
|
+
import msgspec
|
|
6
|
+
from kungfu.library.misc import is_ok
|
|
7
|
+
|
|
8
|
+
from telegrinder.api.api import API
|
|
9
|
+
from telegrinder.api.error import APIServerError, InvalidTokenError
|
|
10
|
+
from telegrinder.bot.polling.abc import ABCPolling
|
|
11
|
+
from telegrinder.bot.polling.error_handler import ErrorHandler
|
|
12
|
+
from telegrinder.bot.polling.utils import compute_number
|
|
13
|
+
from telegrinder.modules import logger
|
|
14
|
+
from telegrinder.msgspec_utils import decoder
|
|
15
|
+
from telegrinder.types.objects import Update, UpdateType
|
|
16
|
+
|
|
17
|
+
DEFAULT_OFFSET: typing.Final = 0
|
|
18
|
+
DEFAULT_RECONNECT_AFTER: typing.Final = 5.0
|
|
19
|
+
DEFAULT_MAX_RECONNECTS: typing.Final = 15
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Polling(ABCPolling):
|
|
23
|
+
__slots__ = (
|
|
24
|
+
"api",
|
|
25
|
+
"timeout",
|
|
26
|
+
"limit",
|
|
27
|
+
"allowed_updates",
|
|
28
|
+
"reconnect_after",
|
|
29
|
+
"max_reconnects",
|
|
30
|
+
"offset",
|
|
31
|
+
"_running",
|
|
32
|
+
"_reconnects_counter",
|
|
33
|
+
"_error_handler",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
api: API,
|
|
39
|
+
*,
|
|
40
|
+
timeout: int | float | datetime.timedelta | None = None,
|
|
41
|
+
limit: int | None = None,
|
|
42
|
+
offset: int = DEFAULT_OFFSET,
|
|
43
|
+
reconnect_after: float = DEFAULT_RECONNECT_AFTER,
|
|
44
|
+
max_reconnects: int = DEFAULT_MAX_RECONNECTS,
|
|
45
|
+
include_updates: set[UpdateType] | None = None,
|
|
46
|
+
exclude_updates: set[UpdateType] | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.api = api
|
|
49
|
+
self.timeout = timeout if isinstance(timeout, datetime.timedelta) else datetime.timedelta(seconds=timeout or 0)
|
|
50
|
+
self.timeout_seconds = int(self.timeout.total_seconds())
|
|
51
|
+
self.limit = limit
|
|
52
|
+
self.offset = max(DEFAULT_OFFSET, offset)
|
|
53
|
+
self.allowed_updates = self.get_allowed_updates(
|
|
54
|
+
include_updates=include_updates,
|
|
55
|
+
exclude_updates=exclude_updates,
|
|
56
|
+
)
|
|
57
|
+
self.reconnect_after = compute_number(DEFAULT_RECONNECT_AFTER, reconnect_after, 0.0)
|
|
58
|
+
self.max_reconnects = compute_number(DEFAULT_MAX_RECONNECTS, max_reconnects, 0)
|
|
59
|
+
self._running = False
|
|
60
|
+
self._reconnects_counter = 0
|
|
61
|
+
self._error_handler = ErrorHandler(self)
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return (
|
|
65
|
+
"<{}: api={!r}, running={}, offset={}, timeout={}, limit={}, "
|
|
66
|
+
"allowed_updates={!r}, max_reconnects={}, reconnect_after={}>"
|
|
67
|
+
).format(
|
|
68
|
+
type(self).__name__,
|
|
69
|
+
self.api,
|
|
70
|
+
self._running,
|
|
71
|
+
self.offset,
|
|
72
|
+
self.timeout,
|
|
73
|
+
self.limit,
|
|
74
|
+
self.allowed_updates,
|
|
75
|
+
self.max_reconnects,
|
|
76
|
+
self.reconnect_after,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def get_allowed_updates(
|
|
81
|
+
*,
|
|
82
|
+
include_updates: set[UpdateType] | None = None,
|
|
83
|
+
exclude_updates: set[UpdateType] | None = None,
|
|
84
|
+
) -> list[UpdateType]:
|
|
85
|
+
allowed_updates = list(UpdateType)
|
|
86
|
+
|
|
87
|
+
if include_updates and exclude_updates:
|
|
88
|
+
return [x for x in allowed_updates if x in include_updates and x not in exclude_updates]
|
|
89
|
+
|
|
90
|
+
if exclude_updates:
|
|
91
|
+
return [x for x in allowed_updates if x not in exclude_updates]
|
|
92
|
+
|
|
93
|
+
if include_updates:
|
|
94
|
+
return [x for x in allowed_updates if x in include_updates]
|
|
95
|
+
|
|
96
|
+
return allowed_updates
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def reconnects_counter(self) -> int:
|
|
100
|
+
return self._reconnects_counter
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def running(self) -> bool:
|
|
104
|
+
return self._running
|
|
105
|
+
|
|
106
|
+
def _reset_reconnects_counter(self) -> None:
|
|
107
|
+
self._reconnects_counter = 0
|
|
108
|
+
|
|
109
|
+
async def get_updates(self) -> msgspec.Raw:
|
|
110
|
+
try:
|
|
111
|
+
raw_updates = await self.api.request_raw(
|
|
112
|
+
method="getUpdates",
|
|
113
|
+
data=dict(
|
|
114
|
+
offset=self.offset,
|
|
115
|
+
limit=self.limit,
|
|
116
|
+
timeout=self.timeout_seconds,
|
|
117
|
+
allowed_updates=self.allowed_updates,
|
|
118
|
+
),
|
|
119
|
+
timeout=self.timeout + self.api.http.timeout,
|
|
120
|
+
)
|
|
121
|
+
except TimeoutError:
|
|
122
|
+
return msgspec.Raw(b"")
|
|
123
|
+
|
|
124
|
+
if is_ok(raw_updates):
|
|
125
|
+
return raw_updates.value
|
|
126
|
+
|
|
127
|
+
match (error := raw_updates.error).status_code:
|
|
128
|
+
case HTTPStatus.CONFLICT:
|
|
129
|
+
error = APIServerError("Cannot run polling, because another bot instance is already running.")
|
|
130
|
+
case HTTPStatus.UNAUTHORIZED | HTTPStatus.NOT_FOUND as status:
|
|
131
|
+
error = InvalidTokenError(
|
|
132
|
+
"Token seems to be invalid" if status == HTTPStatus.NOT_FOUND else "Invalid token (unauthorized)",
|
|
133
|
+
)
|
|
134
|
+
case HTTPStatus.BAD_GATEWAY | HTTPStatus.GATEWAY_TIMEOUT as status:
|
|
135
|
+
error = APIServerError(
|
|
136
|
+
message="Telegram API server responded a {}".format(status.name.replace("_", " ")),
|
|
137
|
+
retry_after=int(self.reconnect_after),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
raise error from None
|
|
141
|
+
|
|
142
|
+
async def listen(self) -> typing.AsyncGenerator[list[Update], None]:
|
|
143
|
+
await logger.adebug("Listening polling")
|
|
144
|
+
self._running = True
|
|
145
|
+
|
|
146
|
+
with decoder(list[Update]) as updates_decoder:
|
|
147
|
+
while self._running:
|
|
148
|
+
try:
|
|
149
|
+
if (raw := await self.get_updates()) and (updates := updates_decoder.decode(raw)):
|
|
150
|
+
yield updates
|
|
151
|
+
self.offset = updates[-1].update_id + 1
|
|
152
|
+
|
|
153
|
+
if self._reconnects_counter != 0:
|
|
154
|
+
self._reset_reconnects_counter()
|
|
155
|
+
except BaseException as error:
|
|
156
|
+
if not await self._error_handler.handle(error):
|
|
157
|
+
await logger.aexception("Traceback message below:")
|
|
158
|
+
|
|
159
|
+
if isinstance(error, self.api.http.CONNECTION_TIMEOUT_ERRORS):
|
|
160
|
+
self._reconnects_counter += 1
|
|
161
|
+
|
|
162
|
+
def stop(self) -> None:
|
|
163
|
+
self._running = False
|
|
164
|
+
self._reset_reconnects_counter()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ("Polling",)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
def compute_number(
|
|
2
|
+
default: int | float,
|
|
3
|
+
input_value: int | float,
|
|
4
|
+
conditional_value: int | float,
|
|
5
|
+
/,
|
|
6
|
+
) -> int | float:
|
|
7
|
+
return max(default, input_value) * (input_value <= conditional_value) + input_value * (
|
|
8
|
+
input_value >= conditional_value
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ("compute_number",)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from telegrinder.bot.rules.abc import ABCRule, AndRule, NotRule, OrRule, check_rule
|
|
2
|
+
from telegrinder.bot.rules.button import ButtonRule
|
|
3
|
+
from telegrinder.bot.rules.callback_data import (
|
|
4
|
+
CallbackDataEq,
|
|
5
|
+
CallbackDataJsonEq,
|
|
6
|
+
CallbackDataJsonModel,
|
|
7
|
+
CallbackDataMap,
|
|
8
|
+
CallbackDataMarkup,
|
|
9
|
+
CallbackQueryDataRule,
|
|
10
|
+
HasData,
|
|
11
|
+
)
|
|
12
|
+
from telegrinder.bot.rules.chat_join import (
|
|
13
|
+
HasInviteLink,
|
|
14
|
+
InviteLinkByCreator,
|
|
15
|
+
InviteLinkName,
|
|
16
|
+
)
|
|
17
|
+
from telegrinder.bot.rules.chat_member_updated import ChatMemberUpdatedRule, MemberStatus
|
|
18
|
+
from telegrinder.bot.rules.command import Argument, Command
|
|
19
|
+
from telegrinder.bot.rules.enum_text import EnumTextRule
|
|
20
|
+
from telegrinder.bot.rules.func import FuncRule
|
|
21
|
+
from telegrinder.bot.rules.fuzzy import FuzzyText
|
|
22
|
+
from telegrinder.bot.rules.inline import (
|
|
23
|
+
HasLocation,
|
|
24
|
+
InlineQueryChatType,
|
|
25
|
+
InlineQueryMarkup,
|
|
26
|
+
InlineQueryText,
|
|
27
|
+
)
|
|
28
|
+
from telegrinder.bot.rules.integer import IntegerInRange, IsInteger
|
|
29
|
+
from telegrinder.bot.rules.is_from import (
|
|
30
|
+
IsBot,
|
|
31
|
+
IsChat,
|
|
32
|
+
IsChatId,
|
|
33
|
+
IsChecklist,
|
|
34
|
+
IsContact,
|
|
35
|
+
IsDice,
|
|
36
|
+
IsDiceEmoji,
|
|
37
|
+
IsDocument,
|
|
38
|
+
IsForum,
|
|
39
|
+
IsForward,
|
|
40
|
+
IsForwardType,
|
|
41
|
+
IsGame,
|
|
42
|
+
IsGroup,
|
|
43
|
+
IsLanguageCode,
|
|
44
|
+
IsLeftChatMember,
|
|
45
|
+
IsLocation,
|
|
46
|
+
IsNewChatMembers,
|
|
47
|
+
IsNewChatPhoto,
|
|
48
|
+
IsNewChatTitle,
|
|
49
|
+
IsPhoto,
|
|
50
|
+
IsPoll,
|
|
51
|
+
IsPremium,
|
|
52
|
+
IsPrivate,
|
|
53
|
+
IsReply,
|
|
54
|
+
IsSticker,
|
|
55
|
+
IsSuperGroup,
|
|
56
|
+
IsTelegram,
|
|
57
|
+
IsUser,
|
|
58
|
+
IsUserId,
|
|
59
|
+
IsVenue,
|
|
60
|
+
IsVideoNote,
|
|
61
|
+
)
|
|
62
|
+
from telegrinder.bot.rules.logic import If
|
|
63
|
+
from telegrinder.bot.rules.magic import Magic
|
|
64
|
+
from telegrinder.bot.rules.markup import Markup
|
|
65
|
+
from telegrinder.bot.rules.mention import HasMention
|
|
66
|
+
from telegrinder.bot.rules.message_entities import HasEntities, MessageEntities
|
|
67
|
+
from telegrinder.bot.rules.node import NodeRule
|
|
68
|
+
from telegrinder.bot.rules.payload import (
|
|
69
|
+
PayloadEqRule,
|
|
70
|
+
PayloadJsonEqRule,
|
|
71
|
+
PayloadMarkupRule,
|
|
72
|
+
PayloadModelRule,
|
|
73
|
+
PayloadRule,
|
|
74
|
+
)
|
|
75
|
+
from telegrinder.bot.rules.payment_invoice import PaymentInvoiceCurrency
|
|
76
|
+
from telegrinder.bot.rules.regex import Regex
|
|
77
|
+
from telegrinder.bot.rules.rule_enum import RuleEnum
|
|
78
|
+
from telegrinder.bot.rules.start import StartCommand
|
|
79
|
+
from telegrinder.bot.rules.state import State, StateMeta
|
|
80
|
+
from telegrinder.bot.rules.text import HasCaption, HasText, Text
|
|
81
|
+
from telegrinder.bot.rules.update import IsUpdateType
|
|
82
|
+
|
|
83
|
+
__all__ = (
|
|
84
|
+
"ABCRule",
|
|
85
|
+
"AndRule",
|
|
86
|
+
"Argument",
|
|
87
|
+
"ButtonRule",
|
|
88
|
+
"CallbackDataEq",
|
|
89
|
+
"CallbackDataJsonEq",
|
|
90
|
+
"CallbackDataJsonModel",
|
|
91
|
+
"CallbackDataMap",
|
|
92
|
+
"CallbackDataMarkup",
|
|
93
|
+
"CallbackQueryDataRule",
|
|
94
|
+
"ChatMemberUpdatedRule",
|
|
95
|
+
"Command",
|
|
96
|
+
"EnumTextRule",
|
|
97
|
+
"FuncRule",
|
|
98
|
+
"FuzzyText",
|
|
99
|
+
"HasCaption",
|
|
100
|
+
"HasData",
|
|
101
|
+
"HasEntities",
|
|
102
|
+
"HasInviteLink",
|
|
103
|
+
"HasLocation",
|
|
104
|
+
"HasMention",
|
|
105
|
+
"HasText",
|
|
106
|
+
"If",
|
|
107
|
+
"InlineQueryChatType",
|
|
108
|
+
"InlineQueryMarkup",
|
|
109
|
+
"InlineQueryText",
|
|
110
|
+
"IntegerInRange",
|
|
111
|
+
"InviteLinkByCreator",
|
|
112
|
+
"InviteLinkName",
|
|
113
|
+
"IsBot",
|
|
114
|
+
"IsChat",
|
|
115
|
+
"IsChatId",
|
|
116
|
+
"IsChecklist",
|
|
117
|
+
"IsContact",
|
|
118
|
+
"IsDice",
|
|
119
|
+
"IsDiceEmoji",
|
|
120
|
+
"IsDocument",
|
|
121
|
+
"IsForum",
|
|
122
|
+
"IsForward",
|
|
123
|
+
"IsForwardType",
|
|
124
|
+
"IsGame",
|
|
125
|
+
"IsGroup",
|
|
126
|
+
"IsInteger",
|
|
127
|
+
"IsLanguageCode",
|
|
128
|
+
"IsLeftChatMember",
|
|
129
|
+
"IsLocation",
|
|
130
|
+
"IsNewChatMembers",
|
|
131
|
+
"IsNewChatPhoto",
|
|
132
|
+
"IsNewChatTitle",
|
|
133
|
+
"IsPhoto",
|
|
134
|
+
"IsPoll",
|
|
135
|
+
"IsPremium",
|
|
136
|
+
"IsPrivate",
|
|
137
|
+
"IsReply",
|
|
138
|
+
"IsSticker",
|
|
139
|
+
"IsSuperGroup",
|
|
140
|
+
"IsTelegram",
|
|
141
|
+
"IsUpdateType",
|
|
142
|
+
"IsUser",
|
|
143
|
+
"IsUserId",
|
|
144
|
+
"IsVenue",
|
|
145
|
+
"IsVideoNote",
|
|
146
|
+
"Magic",
|
|
147
|
+
"Markup",
|
|
148
|
+
"MemberStatus",
|
|
149
|
+
"MessageEntities",
|
|
150
|
+
"NodeRule",
|
|
151
|
+
"NotRule",
|
|
152
|
+
"OrRule",
|
|
153
|
+
"PayloadEqRule",
|
|
154
|
+
"PayloadJsonEqRule",
|
|
155
|
+
"PayloadMarkupRule",
|
|
156
|
+
"PayloadModelRule",
|
|
157
|
+
"PayloadRule",
|
|
158
|
+
"PaymentInvoiceCurrency",
|
|
159
|
+
"Regex",
|
|
160
|
+
"RuleEnum",
|
|
161
|
+
"StartCommand",
|
|
162
|
+
"State",
|
|
163
|
+
"StateMeta",
|
|
164
|
+
"Text",
|
|
165
|
+
"check_rule",
|
|
166
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections import deque
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
|
|
6
|
+
from nodnod.agent.event_loop.agent import EventLoopAgent
|
|
7
|
+
from nodnod.interface.node_from_function import create_node_from_function
|
|
8
|
+
|
|
9
|
+
from telegrinder.bot.dispatch.context import Context
|
|
10
|
+
from telegrinder.bot.dispatch.process import check_rule
|
|
11
|
+
from telegrinder.node.compose import create_composable
|
|
12
|
+
from telegrinder.node.utils import get_globals_from_function, get_locals_from_function
|
|
13
|
+
from telegrinder.tools.fullname import fullname
|
|
14
|
+
|
|
15
|
+
if typing.TYPE_CHECKING:
|
|
16
|
+
from nodnod.agent.base import Agent
|
|
17
|
+
|
|
18
|
+
from telegrinder.node.compose import Composable
|
|
19
|
+
|
|
20
|
+
type CheckResult = bool | typing.Awaitable[bool]
|
|
21
|
+
type Node = typing.Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ABCRule(ABC):
|
|
25
|
+
required_nodes: typing.Mapping[str, Node] | None = None
|
|
26
|
+
agent_cls: type[Agent] = EventLoopAgent
|
|
27
|
+
requires: deque[ABCRule] = deque()
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def check(self, *args: typing.Any, **kwargs: typing.Any) -> CheckResult:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def __init_subclass__(
|
|
34
|
+
cls,
|
|
35
|
+
*,
|
|
36
|
+
requires: typing.Iterable[ABCRule] | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
requirements: list[ABCRule] = []
|
|
39
|
+
|
|
40
|
+
for base in cls.__mro__:
|
|
41
|
+
if issubclass(base, ABCRule) and base != cls:
|
|
42
|
+
requirements.extend(base.requires or ())
|
|
43
|
+
|
|
44
|
+
requirements.extend(requires or ())
|
|
45
|
+
cls.requires = deque(dict.fromkeys(requirements))
|
|
46
|
+
|
|
47
|
+
def __and__(self, other: object, /) -> AndRule:
|
|
48
|
+
if not isinstance(other, ABCRule):
|
|
49
|
+
return NotImplemented
|
|
50
|
+
return AndRule(self, other)
|
|
51
|
+
|
|
52
|
+
def __iadd__(self, other: object, /) -> AndRule:
|
|
53
|
+
return self.__and__(other)
|
|
54
|
+
|
|
55
|
+
def __or__(self, other: object, /) -> OrRule:
|
|
56
|
+
if not isinstance(other, ABCRule):
|
|
57
|
+
return NotImplemented
|
|
58
|
+
return OrRule(self, other)
|
|
59
|
+
|
|
60
|
+
def __ior__(self, other: object, /) -> OrRule:
|
|
61
|
+
return self.__or__(other)
|
|
62
|
+
|
|
63
|
+
def __invert__(self) -> NotRule:
|
|
64
|
+
return NotRule(self)
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return "<{}{}>".format(
|
|
68
|
+
fullname(self),
|
|
69
|
+
"" if not self.requires else ", requires={!r}".format(self.requires),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def as_optional(self) -> ABCRule:
|
|
73
|
+
return self | Always()
|
|
74
|
+
|
|
75
|
+
def should_fail(self) -> ABCRule:
|
|
76
|
+
return self & Never()
|
|
77
|
+
|
|
78
|
+
@cached_property
|
|
79
|
+
def composable(self) -> Composable:
|
|
80
|
+
node = create_node_from_function(
|
|
81
|
+
self.check,
|
|
82
|
+
dependencies=self.required_nodes,
|
|
83
|
+
forward_refs=get_globals_from_function(self.check),
|
|
84
|
+
namespace=get_locals_from_function(self.check),
|
|
85
|
+
)
|
|
86
|
+
return create_composable(node, agent_cls=self.agent_cls)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AndRule(ABCRule):
|
|
90
|
+
def __init__(self, *rules: ABCRule) -> None:
|
|
91
|
+
self.rules = rules
|
|
92
|
+
|
|
93
|
+
async def check(self, context: Context) -> bool:
|
|
94
|
+
ctx_copy = context.copy()
|
|
95
|
+
|
|
96
|
+
for rule in self.rules:
|
|
97
|
+
if not await check_rule(rule, ctx_copy):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
context |= ctx_copy
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class OrRule(ABCRule):
|
|
105
|
+
def __init__(self, *rules: ABCRule) -> None:
|
|
106
|
+
self.rules = rules
|
|
107
|
+
|
|
108
|
+
async def check(self, context: Context) -> bool:
|
|
109
|
+
for rule in self.rules:
|
|
110
|
+
ctx_copy = context.copy()
|
|
111
|
+
|
|
112
|
+
if await check_rule(rule, ctx_copy):
|
|
113
|
+
context |= ctx_copy
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class NotRule(ABCRule):
|
|
120
|
+
def __init__(self, rule: ABCRule) -> None:
|
|
121
|
+
self.rule = rule
|
|
122
|
+
|
|
123
|
+
async def check(self, context: Context) -> bool:
|
|
124
|
+
return not await check_rule(self.rule, context.copy())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Never(ABCRule):
|
|
128
|
+
"""Neutral element for `|` (OrRule)."""
|
|
129
|
+
|
|
130
|
+
def check(self) -> typing.Literal[False]:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Always(ABCRule):
|
|
135
|
+
"""Neutral element for `&` (AndRule)."""
|
|
136
|
+
|
|
137
|
+
def check(self) -> typing.Literal[True]:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
__all__ = (
|
|
142
|
+
"ABCRule",
|
|
143
|
+
"Always",
|
|
144
|
+
"AndRule",
|
|
145
|
+
"CheckResult",
|
|
146
|
+
"Never",
|
|
147
|
+
"NotRule",
|
|
148
|
+
"OrRule",
|
|
149
|
+
"check_rule",
|
|
150
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from telegrinder.bot.rules.abc import ABCRule, CheckResult
|
|
4
|
+
|
|
5
|
+
if typing.TYPE_CHECKING:
|
|
6
|
+
from telegrinder.tools.keyboard.button import BaseButton
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ButtonRule[KeyboardButton: BaseButton[typing.Any]](ABCRule):
|
|
10
|
+
def __init__(self, button: KeyboardButton, rule: ABCRule) -> None:
|
|
11
|
+
self.button = button
|
|
12
|
+
self.rule = rule
|
|
13
|
+
self.composable = rule.composable
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def check(self) -> typing.Callable[..., CheckResult]:
|
|
17
|
+
return self.rule.check
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ("ButtonRule",)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
|
|
5
|
+
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
6
|
+
from telegrinder.bot.dispatch.context import Context
|
|
7
|
+
from telegrinder.bot.rules.abc import ABCRule
|
|
8
|
+
from telegrinder.bot.rules.payload import (
|
|
9
|
+
PayloadEqRule,
|
|
10
|
+
PayloadJsonEqRule,
|
|
11
|
+
PayloadMarkupRule,
|
|
12
|
+
PayloadModelRule,
|
|
13
|
+
)
|
|
14
|
+
from telegrinder.tools.aio import maybe_awaitable
|
|
15
|
+
|
|
16
|
+
type Validator = typing.Callable[[typing.Any], bool | typing.Awaitable[bool]]
|
|
17
|
+
type CallbackMap = dict[str, typing.Any | type[typing.Any] | Validator | CallbackMap]
|
|
18
|
+
type CallbackMapStrict = dict[str, Validator | CallbackMapStrict]
|
|
19
|
+
|
|
20
|
+
CallbackQuery: typing.TypeAlias = CallbackQueryCute
|
|
21
|
+
CallbackDataEq: typing.TypeAlias = PayloadEqRule
|
|
22
|
+
CallbackDataJsonEq: typing.TypeAlias = PayloadJsonEqRule
|
|
23
|
+
CallbackDataMarkup: typing.TypeAlias = PayloadMarkupRule
|
|
24
|
+
CallbackDataJsonModel: typing.TypeAlias = PayloadModelRule
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HasData(ABCRule):
|
|
28
|
+
def check(self, event: CallbackQuery) -> bool:
|
|
29
|
+
return bool(event.data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CallbackQueryDataRule(ABCRule, abc.ABC, requires=[HasData()]):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CallbackDataMap(CallbackQueryDataRule):
|
|
37
|
+
def __init__(self, mapping: CallbackMap, /, *, allow_extra_fields: bool = False) -> None:
|
|
38
|
+
"""Callback data map validation.
|
|
39
|
+
:param mapping: A callback data mapping with validators.
|
|
40
|
+
:param allow_extra_fields: Allows extra fields in a callback query data.
|
|
41
|
+
"""
|
|
42
|
+
self.mapping = self.transform_to_callbacks(mapping)
|
|
43
|
+
self.allow_extra_fields = allow_extra_fields
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def transform_to_callbacks(cls, callback_map: CallbackMap) -> CallbackMapStrict:
|
|
47
|
+
"""Transforms `CallbackMap` to `CallbackMapStrict`."""
|
|
48
|
+
callback_map_result = []
|
|
49
|
+
|
|
50
|
+
for key, value in callback_map.items():
|
|
51
|
+
if isinstance(value, type):
|
|
52
|
+
validator = (lambda tp: lambda v: isinstance(v, tp))(value)
|
|
53
|
+
elif isinstance(value, dict):
|
|
54
|
+
validator = cls.transform_to_callbacks(value)
|
|
55
|
+
elif not callable(value):
|
|
56
|
+
validator = (lambda val: lambda v: val == v)(value)
|
|
57
|
+
else:
|
|
58
|
+
validator = value
|
|
59
|
+
callback_map_result.append((key, validator))
|
|
60
|
+
|
|
61
|
+
return dict(callback_map_result)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
async def run_validator(value: typing.Any, validator: Validator) -> bool:
|
|
65
|
+
"""Runs sync/async validator."""
|
|
66
|
+
with suppress(Exception):
|
|
67
|
+
return await maybe_awaitable(validator(value))
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
async def match(cls, callback_data: dict[str, typing.Any], callback_map: CallbackMapStrict) -> bool:
|
|
72
|
+
"""Matches `callback_data` with `callback_map` recursively."""
|
|
73
|
+
for key, validator in callback_map.items():
|
|
74
|
+
if key not in callback_data:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
if isinstance(validator, dict):
|
|
78
|
+
if not (isinstance(callback_data[key], dict) and await cls.match(callback_data[key], validator)):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
elif not await cls.run_validator(callback_data[key], validator):
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
async def check(self, event: CallbackQuery, ctx: Context) -> bool:
|
|
87
|
+
callback_data = event.decode_data().unwrap_or_none()
|
|
88
|
+
if callback_data is None:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
if not self.allow_extra_fields and self.mapping.keys() != callback_data.keys():
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
if await self.match(callback_data, self.mapping):
|
|
95
|
+
ctx |= {k: callback_data[k] for k in self.mapping}
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
__all__ = (
|
|
102
|
+
"CallbackDataEq",
|
|
103
|
+
"CallbackDataJsonEq",
|
|
104
|
+
"CallbackDataJsonModel",
|
|
105
|
+
"CallbackDataMap",
|
|
106
|
+
"CallbackDataMarkup",
|
|
107
|
+
"CallbackQueryDataRule",
|
|
108
|
+
"HasData",
|
|
109
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from telegrinder.bot.cute_types.chat_join_request import ChatJoinRequestCute
|
|
2
|
+
from telegrinder.bot.rules.abc import ABCRule
|
|
3
|
+
|
|
4
|
+
type ChatJoinRequest = ChatJoinRequestCute
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HasInviteLink(ABCRule):
|
|
8
|
+
def check(self, event: ChatJoinRequest) -> bool:
|
|
9
|
+
return bool(event.invite_link)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InviteLinkName(ABCRule, requires=[HasInviteLink()]):
|
|
13
|
+
def __init__(self, name: str, /) -> None:
|
|
14
|
+
self.name = name
|
|
15
|
+
|
|
16
|
+
def check(self, event: ChatJoinRequest) -> bool:
|
|
17
|
+
return event.invite_link.unwrap().name.unwrap_or_none() == self.name
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InviteLinkByCreator(ABCRule, requires=[HasInviteLink()]):
|
|
21
|
+
def __init__(self, creator_id: int, /) -> None:
|
|
22
|
+
self.creator_id = creator_id
|
|
23
|
+
|
|
24
|
+
def check(self, event: ChatJoinRequest) -> bool:
|
|
25
|
+
return event.invite_link.unwrap().creator.id == self.creator_id
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ("HasInviteLink", "InviteLinkByCreator", "InviteLinkName")
|