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.
Files changed (215) hide show
  1. telegrinder/__init__.py +258 -0
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +15 -0
  4. telegrinder/api/api.py +175 -0
  5. telegrinder/api/error.py +50 -0
  6. telegrinder/api/response.py +23 -0
  7. telegrinder/api/token.py +30 -0
  8. telegrinder/api/validators.py +30 -0
  9. telegrinder/bot/__init__.py +144 -0
  10. telegrinder/bot/bot.py +70 -0
  11. telegrinder/bot/cute_types/__init__.py +41 -0
  12. telegrinder/bot/cute_types/base.py +228 -0
  13. telegrinder/bot/cute_types/base.pyi +49 -0
  14. telegrinder/bot/cute_types/business_connection.py +9 -0
  15. telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
  16. telegrinder/bot/cute_types/callback_query.py +248 -0
  17. telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
  18. telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
  19. telegrinder/bot/cute_types/chat_join_request.py +59 -0
  20. telegrinder/bot/cute_types/chat_member_updated.py +158 -0
  21. telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
  22. telegrinder/bot/cute_types/inline_query.py +41 -0
  23. telegrinder/bot/cute_types/message.py +2809 -0
  24. telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
  25. telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
  26. telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
  27. telegrinder/bot/cute_types/poll.py +9 -0
  28. telegrinder/bot/cute_types/poll_answer.py +9 -0
  29. telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
  30. telegrinder/bot/cute_types/shipping_query.py +11 -0
  31. telegrinder/bot/cute_types/update.py +209 -0
  32. telegrinder/bot/cute_types/utils.py +141 -0
  33. telegrinder/bot/dispatch/__init__.py +99 -0
  34. telegrinder/bot/dispatch/abc.py +74 -0
  35. telegrinder/bot/dispatch/action.py +99 -0
  36. telegrinder/bot/dispatch/context.py +162 -0
  37. telegrinder/bot/dispatch/dispatch.py +362 -0
  38. telegrinder/bot/dispatch/handler/__init__.py +23 -0
  39. telegrinder/bot/dispatch/handler/abc.py +25 -0
  40. telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
  41. telegrinder/bot/dispatch/handler/base.py +34 -0
  42. telegrinder/bot/dispatch/handler/document_reply.py +43 -0
  43. telegrinder/bot/dispatch/handler/func.py +73 -0
  44. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  45. telegrinder/bot/dispatch/handler/message_reply.py +35 -0
  46. telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
  47. telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
  48. telegrinder/bot/dispatch/handler/video_reply.py +43 -0
  49. telegrinder/bot/dispatch/middleware/__init__.py +13 -0
  50. telegrinder/bot/dispatch/middleware/abc.py +112 -0
  51. telegrinder/bot/dispatch/middleware/box.py +32 -0
  52. telegrinder/bot/dispatch/middleware/filter.py +88 -0
  53. telegrinder/bot/dispatch/middleware/media_group.py +69 -0
  54. telegrinder/bot/dispatch/process.py +93 -0
  55. telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
  56. telegrinder/bot/dispatch/return_manager/abc.py +107 -0
  57. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  58. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  59. telegrinder/bot/dispatch/return_manager/message.py +34 -0
  60. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
  61. telegrinder/bot/dispatch/return_manager/utils.py +20 -0
  62. telegrinder/bot/dispatch/router/__init__.py +4 -0
  63. telegrinder/bot/dispatch/router/abc.py +15 -0
  64. telegrinder/bot/dispatch/router/base.py +154 -0
  65. telegrinder/bot/dispatch/view/__init__.py +15 -0
  66. telegrinder/bot/dispatch/view/abc.py +15 -0
  67. telegrinder/bot/dispatch/view/base.py +226 -0
  68. telegrinder/bot/dispatch/view/box.py +207 -0
  69. telegrinder/bot/dispatch/view/media_group.py +25 -0
  70. telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
  71. telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
  72. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
  73. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
  74. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
  75. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
  76. telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
  77. telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
  78. telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
  79. telegrinder/bot/polling/__init__.py +4 -0
  80. telegrinder/bot/polling/abc.py +25 -0
  81. telegrinder/bot/polling/error_handler.py +93 -0
  82. telegrinder/bot/polling/polling.py +167 -0
  83. telegrinder/bot/polling/utils.py +12 -0
  84. telegrinder/bot/rules/__init__.py +166 -0
  85. telegrinder/bot/rules/abc.py +150 -0
  86. telegrinder/bot/rules/button.py +20 -0
  87. telegrinder/bot/rules/callback_data.py +109 -0
  88. telegrinder/bot/rules/chat_join.py +28 -0
  89. telegrinder/bot/rules/chat_member_updated.py +145 -0
  90. telegrinder/bot/rules/command.py +137 -0
  91. telegrinder/bot/rules/enum_text.py +29 -0
  92. telegrinder/bot/rules/func.py +21 -0
  93. telegrinder/bot/rules/fuzzy.py +21 -0
  94. telegrinder/bot/rules/inline.py +45 -0
  95. telegrinder/bot/rules/integer.py +19 -0
  96. telegrinder/bot/rules/is_from.py +213 -0
  97. telegrinder/bot/rules/logic.py +22 -0
  98. telegrinder/bot/rules/magic.py +60 -0
  99. telegrinder/bot/rules/markup.py +51 -0
  100. telegrinder/bot/rules/media.py +13 -0
  101. telegrinder/bot/rules/mention.py +15 -0
  102. telegrinder/bot/rules/message_entities.py +37 -0
  103. telegrinder/bot/rules/node.py +43 -0
  104. telegrinder/bot/rules/payload.py +89 -0
  105. telegrinder/bot/rules/payment_invoice.py +14 -0
  106. telegrinder/bot/rules/regex.py +34 -0
  107. telegrinder/bot/rules/rule_enum.py +71 -0
  108. telegrinder/bot/rules/start.py +73 -0
  109. telegrinder/bot/rules/state.py +35 -0
  110. telegrinder/bot/rules/text.py +27 -0
  111. telegrinder/bot/rules/update.py +14 -0
  112. telegrinder/bot/scenario/__init__.py +5 -0
  113. telegrinder/bot/scenario/abc.py +16 -0
  114. telegrinder/bot/scenario/checkbox.py +183 -0
  115. telegrinder/bot/scenario/choice.py +44 -0
  116. telegrinder/client/__init__.py +11 -0
  117. telegrinder/client/abc.py +136 -0
  118. telegrinder/client/form_data.py +34 -0
  119. telegrinder/client/rnet.py +198 -0
  120. telegrinder/model.py +133 -0
  121. telegrinder/model.pyi +57 -0
  122. telegrinder/modules.py +1081 -0
  123. telegrinder/msgspec_utils/__init__.py +42 -0
  124. telegrinder/msgspec_utils/abc.py +16 -0
  125. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  126. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  127. telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
  128. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  129. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  130. telegrinder/msgspec_utils/decoder.py +388 -0
  131. telegrinder/msgspec_utils/encoder.py +204 -0
  132. telegrinder/msgspec_utils/json.py +15 -0
  133. telegrinder/msgspec_utils/tools.py +80 -0
  134. telegrinder/node/__init__.py +80 -0
  135. telegrinder/node/compose.py +193 -0
  136. telegrinder/node/nodes/__init__.py +96 -0
  137. telegrinder/node/nodes/attachment.py +169 -0
  138. telegrinder/node/nodes/callback_query.py +25 -0
  139. telegrinder/node/nodes/channel.py +97 -0
  140. telegrinder/node/nodes/command.py +33 -0
  141. telegrinder/node/nodes/error.py +43 -0
  142. telegrinder/node/nodes/event.py +70 -0
  143. telegrinder/node/nodes/file.py +39 -0
  144. telegrinder/node/nodes/global_node.py +66 -0
  145. telegrinder/node/nodes/i18n.py +110 -0
  146. telegrinder/node/nodes/me.py +26 -0
  147. telegrinder/node/nodes/message_entities.py +15 -0
  148. telegrinder/node/nodes/payload.py +84 -0
  149. telegrinder/node/nodes/reply_message.py +14 -0
  150. telegrinder/node/nodes/source.py +172 -0
  151. telegrinder/node/nodes/state_mutator.py +71 -0
  152. telegrinder/node/nodes/text.py +62 -0
  153. telegrinder/node/scope.py +88 -0
  154. telegrinder/node/utils.py +38 -0
  155. telegrinder/py.typed +0 -0
  156. telegrinder/rules.py +1 -0
  157. telegrinder/tools/__init__.py +183 -0
  158. telegrinder/tools/aio.py +147 -0
  159. telegrinder/tools/final.py +21 -0
  160. telegrinder/tools/formatting/__init__.py +85 -0
  161. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  162. telegrinder/tools/formatting/deep_links/links.py +468 -0
  163. telegrinder/tools/formatting/deep_links/parsing.py +88 -0
  164. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  165. telegrinder/tools/formatting/html.py +241 -0
  166. telegrinder/tools/fullname.py +82 -0
  167. telegrinder/tools/global_context/__init__.py +13 -0
  168. telegrinder/tools/global_context/abc.py +63 -0
  169. telegrinder/tools/global_context/builtin_context.py +45 -0
  170. telegrinder/tools/global_context/global_context.py +614 -0
  171. telegrinder/tools/input_file_directory.py +30 -0
  172. telegrinder/tools/keyboard/__init__.py +6 -0
  173. telegrinder/tools/keyboard/abc.py +84 -0
  174. telegrinder/tools/keyboard/base.py +108 -0
  175. telegrinder/tools/keyboard/button.py +181 -0
  176. telegrinder/tools/keyboard/data.py +31 -0
  177. telegrinder/tools/keyboard/keyboard.py +160 -0
  178. telegrinder/tools/keyboard/utils.py +95 -0
  179. telegrinder/tools/lifespan.py +188 -0
  180. telegrinder/tools/limited_dict.py +35 -0
  181. telegrinder/tools/loop_wrapper.py +271 -0
  182. telegrinder/tools/magic/__init__.py +29 -0
  183. telegrinder/tools/magic/annotations.py +172 -0
  184. telegrinder/tools/magic/descriptors.py +57 -0
  185. telegrinder/tools/magic/function.py +254 -0
  186. telegrinder/tools/magic/inspect.py +16 -0
  187. telegrinder/tools/magic/shortcut.py +107 -0
  188. telegrinder/tools/member_descriptor_proxy.py +95 -0
  189. telegrinder/tools/parse_mode.py +12 -0
  190. telegrinder/tools/serialization/__init__.py +5 -0
  191. telegrinder/tools/serialization/abc.py +34 -0
  192. telegrinder/tools/serialization/json_ser.py +60 -0
  193. telegrinder/tools/serialization/msgpack_ser.py +197 -0
  194. telegrinder/tools/serialization/utils.py +18 -0
  195. telegrinder/tools/singleton/__init__.py +4 -0
  196. telegrinder/tools/singleton/abc.py +14 -0
  197. telegrinder/tools/singleton/singleton.py +18 -0
  198. telegrinder/tools/state_mutator/__init__.py +4 -0
  199. telegrinder/tools/state_mutator/mutation.py +85 -0
  200. telegrinder/tools/state_storage/__init__.py +4 -0
  201. telegrinder/tools/state_storage/abc.py +38 -0
  202. telegrinder/tools/state_storage/memory.py +27 -0
  203. telegrinder/tools/strings.py +22 -0
  204. telegrinder/types/__init__.py +323 -0
  205. telegrinder/types/enums.py +754 -0
  206. telegrinder/types/input_file.py +51 -0
  207. telegrinder/types/methods.py +6143 -0
  208. telegrinder/types/methods_utils.py +66 -0
  209. telegrinder/types/objects.py +8184 -0
  210. telegrinder/types/webapp.py +129 -0
  211. telegrinder/verification_utils.py +35 -0
  212. telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
  213. telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
  214. telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
  215. 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")