telegrinder 0.1.dev168__py3-none-any.whl → 0.1.dev170__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.

Files changed (100) hide show
  1. telegrinder/__init__.py +9 -3
  2. telegrinder/bot/__init__.py +7 -5
  3. telegrinder/bot/cute_types/base.py +12 -14
  4. telegrinder/bot/cute_types/callback_query.py +54 -43
  5. telegrinder/bot/cute_types/chat_join_request.py +8 -7
  6. telegrinder/bot/cute_types/chat_member_updated.py +23 -17
  7. telegrinder/bot/cute_types/inline_query.py +1 -1
  8. telegrinder/bot/cute_types/message.py +331 -183
  9. telegrinder/bot/cute_types/update.py +4 -8
  10. telegrinder/bot/cute_types/utils.py +1 -5
  11. telegrinder/bot/dispatch/__init__.py +2 -3
  12. telegrinder/bot/dispatch/abc.py +4 -0
  13. telegrinder/bot/dispatch/context.py +9 -4
  14. telegrinder/bot/dispatch/dispatch.py +35 -33
  15. telegrinder/bot/dispatch/handler/func.py +34 -13
  16. telegrinder/bot/dispatch/handler/message_reply.py +6 -3
  17. telegrinder/bot/dispatch/middleware/abc.py +4 -4
  18. telegrinder/bot/dispatch/process.py +40 -13
  19. telegrinder/bot/dispatch/return_manager/abc.py +12 -12
  20. telegrinder/bot/dispatch/return_manager/callback_query.py +1 -3
  21. telegrinder/bot/dispatch/return_manager/inline_query.py +1 -3
  22. telegrinder/bot/dispatch/view/abc.py +37 -45
  23. telegrinder/bot/dispatch/view/box.py +66 -50
  24. telegrinder/bot/dispatch/view/message.py +1 -5
  25. telegrinder/bot/dispatch/view/raw.py +6 -6
  26. telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -1
  27. telegrinder/bot/dispatch/waiter_machine/machine.py +77 -35
  28. telegrinder/bot/dispatch/waiter_machine/middleware.py +31 -7
  29. telegrinder/bot/dispatch/waiter_machine/short_state.py +17 -8
  30. telegrinder/bot/polling/polling.py +4 -4
  31. telegrinder/bot/rules/__init__.py +9 -6
  32. telegrinder/bot/rules/abc.py +99 -22
  33. telegrinder/bot/rules/adapter/__init__.py +4 -1
  34. telegrinder/bot/rules/adapter/abc.py +11 -6
  35. telegrinder/bot/rules/adapter/errors.py +1 -2
  36. telegrinder/bot/rules/adapter/event.py +14 -9
  37. telegrinder/bot/rules/adapter/node.py +42 -0
  38. telegrinder/bot/rules/callback_data.py +4 -4
  39. telegrinder/bot/rules/chat_join.py +3 -2
  40. telegrinder/bot/rules/command.py +26 -14
  41. telegrinder/bot/rules/enum_text.py +5 -5
  42. telegrinder/bot/rules/func.py +6 -6
  43. telegrinder/bot/rules/fuzzy.py +5 -7
  44. telegrinder/bot/rules/inline.py +4 -5
  45. telegrinder/bot/rules/integer.py +10 -8
  46. telegrinder/bot/rules/is_from.py +63 -91
  47. telegrinder/bot/rules/markup.py +5 -5
  48. telegrinder/bot/rules/mention.py +4 -4
  49. telegrinder/bot/rules/message.py +1 -1
  50. telegrinder/bot/rules/node.py +27 -0
  51. telegrinder/bot/rules/regex.py +5 -5
  52. telegrinder/bot/rules/rule_enum.py +4 -4
  53. telegrinder/bot/rules/start.py +5 -5
  54. telegrinder/bot/rules/text.py +9 -13
  55. telegrinder/bot/rules/update.py +4 -4
  56. telegrinder/bot/scenario/__init__.py +3 -3
  57. telegrinder/bot/scenario/choice.py +2 -3
  58. telegrinder/model.py +51 -16
  59. telegrinder/modules.py +14 -6
  60. telegrinder/msgspec_utils.py +67 -23
  61. telegrinder/node/__init__.py +26 -8
  62. telegrinder/node/attachment.py +13 -9
  63. telegrinder/node/base.py +27 -14
  64. telegrinder/node/callback_query.py +18 -0
  65. telegrinder/node/command.py +29 -0
  66. telegrinder/node/composer.py +119 -30
  67. telegrinder/node/me.py +14 -0
  68. telegrinder/node/message.py +2 -4
  69. telegrinder/node/polymorphic.py +44 -0
  70. telegrinder/node/rule.py +26 -22
  71. telegrinder/node/scope.py +36 -0
  72. telegrinder/node/source.py +37 -10
  73. telegrinder/node/text.py +11 -5
  74. telegrinder/node/tools/__init__.py +2 -2
  75. telegrinder/node/tools/generator.py +6 -6
  76. telegrinder/tools/__init__.py +8 -13
  77. telegrinder/tools/buttons.py +23 -17
  78. telegrinder/tools/error_handler/error_handler.py +11 -14
  79. telegrinder/tools/formatting/__init__.py +0 -6
  80. telegrinder/tools/formatting/html.py +10 -12
  81. telegrinder/tools/formatting/links.py +0 -5
  82. telegrinder/tools/formatting/spec_html_formats.py +0 -11
  83. telegrinder/tools/global_context/abc.py +1 -3
  84. telegrinder/tools/global_context/global_context.py +6 -16
  85. telegrinder/tools/i18n/simple.py +1 -3
  86. telegrinder/tools/kb_set/yaml.py +1 -2
  87. telegrinder/tools/keyboard.py +7 -8
  88. telegrinder/tools/limited_dict.py +1 -1
  89. telegrinder/tools/loop_wrapper/loop_wrapper.py +6 -5
  90. telegrinder/tools/magic.py +27 -5
  91. telegrinder/types/__init__.py +20 -0
  92. telegrinder/types/enums.py +37 -31
  93. telegrinder/types/methods.py +608 -327
  94. telegrinder/types/objects.py +1139 -716
  95. {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/LICENSE +1 -1
  96. {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/METADATA +6 -5
  97. telegrinder-0.1.dev170.dist-info/RECORD +143 -0
  98. telegrinder/bot/dispatch/composition.py +0 -88
  99. telegrinder-0.1.dev168.dist-info/RECORD +0 -137
  100. {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/WHEEL +0 -0
@@ -2,24 +2,24 @@ import dataclasses
2
2
  import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
+ from telegrinder.node import Source
6
+ from telegrinder.node.command import CommandInfo, single_split
7
+ from telegrinder.node.me import Me
5
8
 
6
- from .abc import Message
7
- from .text import TextMessageRule
9
+ from ...types import ChatType
10
+ from .abc import ABCRule
8
11
 
9
12
  Validator = typing.Callable[[str], typing.Any | None]
10
13
 
11
14
 
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
15
  @dataclasses.dataclass(frozen=True)
18
16
  class Argument:
19
17
  name: str
20
18
  validators: list[Validator] = dataclasses.field(default_factory=lambda: [])
21
19
  optional: bool = dataclasses.field(default=False, kw_only=True)
22
20
 
21
+ # NOTE: add optional param `description`
22
+
23
23
  def check(self, data: str) -> typing.Any | None:
24
24
  for validator in self.validators:
25
25
  data = validator(data) # type: ignore
@@ -28,7 +28,7 @@ class Argument:
28
28
  return data
29
29
 
30
30
 
31
- class Command(TextMessageRule):
31
+ class Command(ABCRule):
32
32
  def __init__(
33
33
  self,
34
34
  names: str | typing.Iterable[str],
@@ -36,12 +36,18 @@ class Command(TextMessageRule):
36
36
  prefixes: tuple[str, ...] = ("/",),
37
37
  separator: str = " ",
38
38
  lazy: bool = False,
39
+ validate_mention: bool = True,
40
+ mention_needed_in_chat: bool = False,
39
41
  ) -> None:
40
42
  self.names = [names] if isinstance(names, str) else names
41
43
  self.arguments = arguments
42
44
  self.prefixes = prefixes
43
45
  self.separator = separator
44
46
  self.lazy = lazy
47
+ self.validate_mention = validate_mention
48
+
49
+ # if true then we'll check for mention when message is from a group
50
+ self.mention_needed_in_chat = mention_needed_in_chat
45
51
 
46
52
  def remove_prefix(self, text: str) -> str | None:
47
53
  for prefix in self.prefixes:
@@ -93,19 +99,25 @@ class Command(TextMessageRule):
93
99
 
94
100
  return None
95
101
 
96
- async def check(self, message: Message, ctx: Context) -> bool:
97
- text = self.remove_prefix(message.text.unwrap())
98
- if text is None:
102
+ async def check(self, command: CommandInfo, me: Me, src: Source, ctx: Context) -> bool:
103
+ name = self.remove_prefix(command.name)
104
+ if name is None:
99
105
  return False
100
106
 
101
- name, arguments = single_split(text, self.separator)
102
107
  if name not in self.names:
103
108
  return False
104
109
 
110
+ if not command.mention and self.mention_needed_in_chat and src.chat.type is not ChatType.PRIVATE:
111
+ return False
112
+
113
+ if command.mention and self.validate_mention: # noqa
114
+ if command.mention.unwrap().lower() != me.username.unwrap().lower():
115
+ return False
116
+
105
117
  if not self.arguments:
106
- return not arguments
118
+ return not command.arguments
107
119
 
108
- result = self.parse_arguments(list(self.arguments), arguments)
120
+ result = self.parse_arguments(list(self.arguments), command.arguments)
109
121
  if result is None:
110
122
  return False
111
123
 
@@ -2,14 +2,14 @@ import enum
2
2
  import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
+ from telegrinder.node.text import Text
5
6
 
6
- from .abc import Message
7
- from .text import TextMessageRule
7
+ from .abc import ABCRule
8
8
 
9
9
  T = typing.TypeVar("T", bound=enum.Enum)
10
10
 
11
11
 
12
- class EnumTextRule(TextMessageRule, typing.Generic[T]):
12
+ class EnumTextRule(ABCRule, typing.Generic[T]):
13
13
  def __init__(self, enum_t: type[T], *, lower_case: bool = True) -> None:
14
14
  self.enum_t = enum_t
15
15
  self.texts = list(
@@ -25,8 +25,8 @@ class EnumTextRule(TextMessageRule, typing.Generic[T]):
25
25
  return enumeration
26
26
  raise KeyError("Enumeration is undefined.")
27
27
 
28
- async def check(self, message: Message, ctx: Context) -> bool:
29
- text = message.text.unwrap().lower()
28
+ async def check(self, text: Text, ctx: Context) -> bool:
29
+ text = text.lower() # type: ignore
30
30
  if text not in self.texts:
31
31
  return False
32
32
  ctx.enum_text = self.find(text)
@@ -4,19 +4,19 @@ import typing
4
4
  from telegrinder.bot.dispatch.context import Context
5
5
  from telegrinder.types import Update
6
6
 
7
- from .abc import ABCAdapter, ABCRule, RawUpdateAdapter, T
7
+ from .abc import ABCAdapter, ABCRule, AdaptTo, RawUpdateAdapter
8
8
 
9
9
 
10
- class FuncRule(ABCRule, typing.Generic[T]):
10
+ class FuncRule(ABCRule, typing.Generic[AdaptTo]):
11
11
  def __init__(
12
12
  self,
13
- func: typing.Callable[[T, Context], typing.Awaitable[bool] | bool],
14
- adapter: ABCAdapter[Update, T] | None = None,
13
+ func: typing.Callable[[AdaptTo, Context], typing.Awaitable[bool] | bool],
14
+ adapter: ABCAdapter[Update, AdaptTo] | None = None,
15
15
  ):
16
16
  self.func = func
17
- self.adapter = adapter or RawUpdateAdapter()
17
+ self.adapter = adapter or RawUpdateAdapter() # type: ignore
18
18
 
19
- async def check(self, event: T, ctx: Context) -> bool:
19
+ async def check(self, event: AdaptTo, ctx: Context) -> bool:
20
20
  result = self.func(event, ctx)
21
21
  if inspect.isawaitable(result):
22
22
  return await result
@@ -1,22 +1,20 @@
1
1
  import difflib
2
2
 
3
3
  from telegrinder.bot.dispatch.context import Context
4
+ from telegrinder.node.text import Text
4
5
 
5
- from .abc import Message
6
- from .text import TextMessageRule
6
+ from .abc import ABCRule
7
7
 
8
8
 
9
- class FuzzyText(TextMessageRule):
9
+ class FuzzyText(ABCRule):
10
10
  def __init__(self, texts: str | list[str], min_ratio: float = 0.7):
11
11
  if isinstance(texts, str):
12
12
  texts = [texts]
13
13
  self.texts = texts
14
14
  self.min_ratio = min_ratio
15
15
 
16
- async def check(self, message: Message, ctx: Context) -> bool:
17
- match = max(
18
- difflib.SequenceMatcher(a=message.text.unwrap(), b=text).ratio() for text in self.texts
19
- )
16
+ async def check(self, message_text: Text, ctx: Context) -> bool:
17
+ match = max(difflib.SequenceMatcher(a=message_text, b=text).ratio() for text in self.texts)
20
18
  if match < self.min_ratio:
21
19
  return False
22
20
  ctx.fuzzy_ratio = match
@@ -5,7 +5,7 @@ from telegrinder.bot.cute_types import InlineQueryCute
5
5
  from telegrinder.bot.dispatch.context import Context
6
6
  from telegrinder.bot.rules.abc import ABCRule
7
7
  from telegrinder.bot.rules.adapter import EventAdapter
8
- from telegrinder.types.enums import ChatType
8
+ from telegrinder.types.enums import ChatType, UpdateType
9
9
 
10
10
  from .markup import Markup, PatternLike, check_string
11
11
 
@@ -13,11 +13,11 @@ InlineQuery: typing.TypeAlias = InlineQueryCute
13
13
 
14
14
 
15
15
  class InlineQueryRule(ABCRule[InlineQuery], abc.ABC):
16
- adapter = EventAdapter("inline_query", InlineQuery)
16
+ adapter: EventAdapter[InlineQuery] = EventAdapter(UpdateType.INLINE_QUERY, InlineQuery)
17
17
 
18
18
  @abc.abstractmethod
19
19
  async def check(self, query: InlineQuery, ctx: Context) -> bool:
20
- pass
20
+ ...
21
21
 
22
22
 
23
23
  class HasLocation(InlineQueryRule):
@@ -36,8 +36,7 @@ class InlineQueryChatType(InlineQueryRule):
36
36
  class InlineQueryText(InlineQueryRule):
37
37
  def __init__(self, texts: str | list[str], *, lower_case: bool = False) -> None:
38
38
  self.texts = [
39
- text.lower() if lower_case else text
40
- for text in ([texts] if isinstance(texts, str) else texts)
39
+ text.lower() if lower_case else text for text in ([texts] if isinstance(texts, str) else texts)
41
40
  ]
42
41
  self.lower_case = lower_case
43
42
 
@@ -1,19 +1,21 @@
1
1
  from telegrinder.bot.dispatch.context import Context
2
+ from telegrinder.node.text import TextInteger
2
3
 
3
- from .text import Message, TextMessageRule
4
+ from .abc import ABCRule
5
+ from .node import NodeRule
4
6
 
5
7
 
6
- class Integer(TextMessageRule):
7
- async def check(self, message: Message, ctx: Context) -> bool:
8
- return message.text.unwrap().isdigit()
8
+ class IsInteger(NodeRule):
9
+ def __init__(self) -> None:
10
+ super().__init__(TextInteger)
9
11
 
10
12
 
11
- class IntegerInRange(TextMessageRule, requires=[Integer()]):
13
+ class IntegerInRange(ABCRule):
12
14
  def __init__(self, rng: range):
13
15
  self.rng = rng
14
16
 
15
- async def check(self, message: Message, ctx: Context) -> bool:
16
- return int(message.text.unwrap()) in self.rng
17
+ async def check(self, integer: TextInteger) -> bool:
18
+ return integer in self.rng
17
19
 
18
20
 
19
- __all__ = ("Integer", "IntegerInRange")
21
+ __all__ = ("IsInteger", "IntegerInRange")
@@ -1,145 +1,117 @@
1
1
  import typing
2
2
 
3
- from fntypes.co import Nothing, Some
4
-
5
- from telegrinder.bot.cute_types.base import BaseCute
6
- from telegrinder.bot.cute_types.update import UpdateCute
7
- from telegrinder.bot.dispatch.context import Context
8
- from telegrinder.msgspec_utils import Option
3
+ from telegrinder.node.source import ChatSource, UserSource
9
4
  from telegrinder.types.enums import ChatType, DiceEmoji
10
- from telegrinder.types.objects import User
11
5
 
12
6
  from .abc import ABCRule, Message
13
7
  from .message import MessageRule
14
8
 
15
- T = typing.TypeVar("T", bound=BaseCute)
16
-
17
-
18
- def get_from_user(obj: typing.Any) -> User:
19
- assert isinstance(obj, FromUserProto)
20
- return obj.from_.unwrap() if isinstance(obj.from_, Some | Nothing) else obj.from_
21
-
22
-
23
- @typing.runtime_checkable
24
- class FromUserProto(typing.Protocol):
25
- from_: User | Option[User]
26
-
27
9
 
28
- class HasFrom(ABCRule[T]):
29
- async def check(self, event: UpdateCute, ctx: Context) -> bool:
30
- event_model = event.incoming_update.unwrap()
31
- return isinstance(event_model, FromUserProto) and bool(event_model.from_)
10
+ class IsBot(ABCRule):
11
+ async def check(self, user: UserSource) -> bool:
12
+ return user.is_bot
32
13
 
33
14
 
34
- class HasDice(MessageRule):
35
- async def check(self, message: Message, ctx: Context) -> bool:
36
- return bool(message.dice)
37
-
15
+ class IsUser(ABCRule):
16
+ async def check(self, user: UserSource) -> bool:
17
+ return not user.is_bot
38
18
 
39
- class IsForward(MessageRule):
40
- async def check(self, message: Message, ctx: Context) -> bool:
41
- return bool(message.forward_origin)
42
19
 
20
+ class IsPremium(ABCRule):
21
+ async def check(self, user: UserSource) -> bool:
22
+ return user.is_premium.unwrap_or(False)
43
23
 
44
- class IsForwardType(MessageRule, requires=[IsForward()]):
45
- def __init__(
46
- self, fwd_type: typing.Literal["user", "hidden_user", "chat", "channel"], /
47
- ) -> None:
48
- self.fwd_type = fwd_type
49
-
50
- async def check(self, message: Message, ctx: Context) -> bool:
51
- return message.forward_origin.unwrap().v.type == self.fwd_type
52
24
 
25
+ class IsLanguageCode(ABCRule):
26
+ def __init__(self, lang_codes: str | list[str], /) -> None:
27
+ self.lang_codes = [lang_codes] if isinstance(lang_codes, str) else lang_codes
53
28
 
54
- class IsReply(MessageRule):
55
- async def check(self, message: Message, ctx: Context) -> bool:
56
- return bool(message.reply_to_message)
29
+ async def check(self, user: UserSource) -> bool:
30
+ return user.language_code.unwrap_or_none() in self.lang_codes
57
31
 
58
32
 
59
- class IsSticker(MessageRule):
60
- async def check(self, message: Message, ctx: Context) -> bool:
61
- return bool(message.sticker)
33
+ class IsUserId(ABCRule):
34
+ def __init__(self, user_ids: int | list[int], /) -> None:
35
+ self.user_ids = [user_ids] if isinstance(user_ids, int) else user_ids
62
36
 
37
+ async def check(self, user: UserSource) -> bool:
38
+ return user.id in self.user_ids
63
39
 
64
- class IsBot(ABCRule[T], requires=[HasFrom()]):
65
- async def check(self, event: UpdateCute, ctx: Context) -> bool:
66
- return get_from_user(event.incoming_update.unwrap()).is_bot
67
40
 
41
+ class IsForum(ABCRule):
42
+ async def check(self, chat: ChatSource) -> bool:
43
+ return chat.is_forum.unwrap_or(False)
68
44
 
69
- class IsUser(ABCRule[T], requires=[HasFrom()]):
70
- async def check(self, event: UpdateCute, ctx: Context) -> bool:
71
- return not get_from_user(event.incoming_update.unwrap()).is_bot
72
45
 
46
+ class IsChatId(ABCRule):
47
+ def __init__(self, chat_ids: int | list[int], /) -> None:
48
+ self.chat_ids = [chat_ids] if isinstance(chat_ids, int) else chat_ids
73
49
 
74
- class IsPremium(ABCRule[T], requires=[HasFrom()]):
75
- async def check(self, event: UpdateCute, ctx: Context) -> bool:
76
- return get_from_user(event.incoming_update.unwrap()).is_premium.unwrap_or(False)
50
+ async def check(self, chat: ChatSource) -> bool:
51
+ return chat.id in self.chat_ids
77
52
 
78
53
 
79
- class IsLanguageCode(ABCRule[T], requires=[HasFrom()]):
80
- def __init__(self, lang_codes: str | list[str], /) -> None:
81
- self.lang_codes = [lang_codes] if isinstance(lang_codes, str) else lang_codes
54
+ class IsPrivate(ABCRule):
55
+ async def check(self, chat: ChatSource) -> bool:
56
+ return chat.type == ChatType.PRIVATE
82
57
 
83
- async def check(self, event: UpdateCute, ctx: Context) -> bool:
84
- return (
85
- get_from_user(event.incoming_update.unwrap()).language_code.unwrap_or_none()
86
- in self.lang_codes
87
- )
88
58
 
59
+ class IsGroup(ABCRule):
60
+ async def check(self, chat: ChatSource) -> bool:
61
+ return chat.type == ChatType.GROUP
89
62
 
90
- class IsForum(MessageRule):
91
- async def check(self, message: Message, ctx: Context) -> bool:
92
- return message.chat.is_forum.unwrap_or(False)
93
63
 
64
+ class IsSuperGroup(ABCRule):
65
+ async def check(self, chat: ChatSource) -> bool:
66
+ return chat.type == ChatType.SUPERGROUP
94
67
 
95
- class IsUserId(ABCRule[T], requires=[HasFrom()]):
96
- def __init__(self, user_ids: int | list[int], /) -> None:
97
- self.user_ids = [user_ids] if isinstance(user_ids, int) else user_ids
98
68
 
99
- async def check(self, event: UpdateCute, ctx: Context) -> bool:
100
- return get_from_user(event.incoming_update.unwrap()).id in self.user_ids
69
+ class IsChat(ABCRule):
70
+ async def check(self, chat: ChatSource) -> bool:
71
+ return chat.type in (ChatType.GROUP, ChatType.SUPERGROUP)
101
72
 
102
73
 
103
- class IsChatId(MessageRule):
104
- def __init__(self, chat_ids: int | list[int], /) -> None:
105
- self.chat_ids = [chat_ids] if isinstance(chat_ids, int) else chat_ids
74
+ class IsDice(MessageRule):
75
+ async def check(self, message: Message) -> bool:
76
+ return bool(message.dice)
106
77
 
107
- async def check(self, message: Message, ctx: Context) -> bool:
108
- return message.chat.id in self.chat_ids
109
78
 
79
+ class IsDiceEmoji(MessageRule, requires=[IsDice()]):
80
+ def __init__(self, dice_emoji: DiceEmoji, /) -> None:
81
+ self.dice_emoji = dice_emoji
110
82
 
111
- class IsPrivate(MessageRule):
112
- async def check(self, message: Message, ctx: Context) -> bool:
113
- return message.chat.type == ChatType.PRIVATE
83
+ async def check(self, message: Message) -> bool:
84
+ return message.dice.unwrap().emoji == self.dice_emoji
114
85
 
115
86
 
116
- class IsGroup(MessageRule):
117
- async def check(self, message: Message, ctx: Context) -> bool:
118
- return message.chat.type == ChatType.GROUP
87
+ class IsForward(MessageRule):
88
+ async def check(self, message: Message) -> bool:
89
+ return bool(message.forward_origin)
119
90
 
120
91
 
121
- class IsSuperGroup(MessageRule):
122
- async def check(self, message: Message, ctx: Context) -> bool:
123
- return message.chat.type == ChatType.SUPERGROUP
92
+ class IsForwardType(MessageRule, requires=[IsForward()]):
93
+ def __init__(self, fwd_type: typing.Literal["user", "hidden_user", "chat", "channel"], /) -> None:
94
+ self.fwd_type = fwd_type
124
95
 
96
+ async def check(self, message: Message) -> bool:
97
+ return message.forward_origin.unwrap().v.type == self.fwd_type
125
98
 
126
- class IsChat(MessageRule):
127
- async def check(self, message: Message, ctx: Context) -> bool:
128
- return message.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP)
129
99
 
100
+ class IsReply(MessageRule):
101
+ async def check(self, message: Message) -> bool:
102
+ return bool(message.reply_to_message)
130
103
 
131
- class IsDiceEmoji(MessageRule, requires=[HasDice()]):
132
- def __init__(self, dice_emoji: DiceEmoji, /) -> None:
133
- self.dice_emoji = dice_emoji
134
104
 
135
- async def check(self, message: Message, ctx: Context) -> bool:
136
- return message.dice.unwrap().emoji == self.dice_emoji
105
+ class IsSticker(MessageRule):
106
+ async def check(self, message: Message) -> bool:
107
+ return bool(message.sticker)
137
108
 
138
109
 
139
110
  __all__ = (
140
111
  "IsBot",
141
112
  "IsChat",
142
113
  "IsChatId",
114
+ "IsDice",
143
115
  "IsDiceEmoji",
144
116
  "IsForum",
145
117
  "IsForward",
@@ -3,10 +3,10 @@ import typing
3
3
  import vbml
4
4
 
5
5
  from telegrinder.bot.dispatch.context import Context
6
+ from telegrinder.node.text import Text
6
7
  from telegrinder.tools.global_context import TelegrinderCtx
7
8
 
8
- from .abc import Message
9
- from .text import TextMessageRule
9
+ from .abc import ABCRule
10
10
 
11
11
  PatternLike: typing.TypeAlias = str | vbml.Pattern
12
12
  global_ctx = TelegrinderCtx()
@@ -23,7 +23,7 @@ def check_string(patterns: list[vbml.Pattern], s: str, ctx: Context) -> bool:
23
23
  return False
24
24
 
25
25
 
26
- class Markup(TextMessageRule):
26
+ class Markup(ABCRule):
27
27
  def __init__(self, patterns: PatternLike | list[PatternLike], /):
28
28
  if not isinstance(patterns, list):
29
29
  patterns = [patterns]
@@ -31,8 +31,8 @@ class Markup(TextMessageRule):
31
31
  vbml.Pattern(pattern) if isinstance(pattern, str) else pattern for pattern in patterns
32
32
  ]
33
33
 
34
- async def check(self, message: Message, ctx: Context) -> bool:
35
- return check_string(self.patterns, message.text.unwrap(), ctx)
34
+ async def check(self, text: Text, ctx: Context) -> bool:
35
+ return check_string(self.patterns, text, ctx)
36
36
 
37
37
 
38
38
  __all__ = ("Markup", "check_string")
@@ -1,11 +1,11 @@
1
- from telegrinder.bot.dispatch.context import Context
2
1
  from telegrinder.types.enums import MessageEntityType
3
2
 
4
- from .text import Message, TextMessageRule
3
+ from .message import Message, MessageRule
4
+ from .text import HasText
5
5
 
6
6
 
7
- class HasMention(TextMessageRule):
8
- async def check(self, message: Message, ctx: Context) -> bool:
7
+ class HasMention(MessageRule, requires=[HasText()]):
8
+ async def check(self, message: Message) -> bool:
9
9
  if not message.entities.unwrap_or_none():
10
10
  return False
11
11
  return any(entity.type == MessageEntityType.MENTION for entity in message.entities.unwrap())
@@ -8,7 +8,7 @@ from .adapter import EventAdapter
8
8
 
9
9
 
10
10
  class MessageRule(ABCRule[Message], abc.ABC):
11
- adapter = EventAdapter(MessageEvent, Message)
11
+ adapter: EventAdapter[Message] = EventAdapter(MessageEvent, Message)
12
12
 
13
13
  @abc.abstractmethod
14
14
  async def check(self, message: Message, ctx: Context) -> bool: ...
@@ -0,0 +1,27 @@
1
+ import typing
2
+
3
+ from telegrinder.bot.dispatch.context import Context
4
+ from telegrinder.node import Node
5
+
6
+ from .abc import ABCRule
7
+ from .adapter.node import NodeAdapter
8
+
9
+
10
+ class NodeRule(ABCRule[tuple[Node, ...]]):
11
+ def __init__(self, *nodes: type[Node] | tuple[str, type[Node]]) -> None:
12
+ bindings = [binding if isinstance(binding, tuple) else (None, binding) for binding in nodes]
13
+ self.nodes = [binding[1] for binding in bindings]
14
+ self.node_keys = [binding[0] for binding in bindings]
15
+
16
+ @property
17
+ def adapter(self) -> NodeAdapter:
18
+ return NodeAdapter(*self.nodes)
19
+
20
+ async def check(self, resolved_nodes: tuple[Node, ...], ctx: Context) -> typing.Literal[True]:
21
+ for i, node in enumerate(resolved_nodes):
22
+ if key := self.node_keys[i]:
23
+ ctx[key] = node
24
+ return True
25
+
26
+
27
+ __all__ = ("NodeRule",)
@@ -2,14 +2,14 @@ import re
2
2
  import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
+ from telegrinder.node.text import Text
5
6
 
6
- from .abc import Message
7
- from .text import TextMessageRule
7
+ from .abc import ABCRule
8
8
 
9
9
  PatternLike: typing.TypeAlias = str | typing.Pattern[str]
10
10
 
11
11
 
12
- class Regex(TextMessageRule):
12
+ class Regex(ABCRule):
13
13
  def __init__(self, regexp: PatternLike | list[PatternLike]):
14
14
  self.regexp: list[re.Pattern[str]] = []
15
15
  match regexp:
@@ -22,9 +22,9 @@ class Regex(TextMessageRule):
22
22
  re.compile(regexp) if isinstance(regexp, str) else regexp for regexp in regexp
23
23
  )
24
24
 
25
- async def check(self, message: Message, ctx: Context) -> bool:
25
+ async def check(self, text: Text, ctx: Context) -> bool:
26
26
  for regexp in self.regexp:
27
- response = re.match(regexp, message.text.unwrap())
27
+ response = re.match(regexp, text)
28
28
  if response is not None:
29
29
  if matches := response.groupdict():
30
30
  ctx |= matches
@@ -3,7 +3,7 @@ import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
5
 
6
- from .abc import ABCRule, T, Update, check_rule
6
+ from .abc import ABCRule, Update, check_rule
7
7
  from .func import FuncRule
8
8
 
9
9
 
@@ -17,10 +17,10 @@ class RuleEnumState:
17
17
  return self.cls == other.cls and self.name == other.name
18
18
 
19
19
 
20
- class RuleEnum(ABCRule[T]):
20
+ class RuleEnum(ABCRule):
21
21
  __enum__: list[RuleEnumState]
22
22
 
23
- def __init_subclass__(cls, *args, **kwargs):
23
+ def __init_subclass__(cls, *args: typing.Any, **kwargs: typing.Any) -> None:
24
24
  new_attributes = set(cls.__dict__) - set(RuleEnum.__dict__) - {"__enum__", "__init__"}
25
25
  enum_lst: list[RuleEnumState] = []
26
26
 
@@ -34,7 +34,7 @@ class RuleEnum(ABCRule[T]):
34
34
  setattr(
35
35
  self,
36
36
  attribute.name,
37
- self & FuncRule(lambda _, ctx: self.must_be_state(ctx, attribute)),
37
+ self & FuncRule(lambda _, ctx: self.must_be_state(ctx, attribute)), # type: ignore
38
38
  )
39
39
  enum_lst.append(attribute)
40
40
 
@@ -4,7 +4,7 @@ from telegrinder.bot.dispatch.context import Context
4
4
  from telegrinder.types.enums import MessageEntityType
5
5
 
6
6
  from .is_from import IsPrivate
7
- from .markup import Markup, Message
7
+ from .markup import Markup
8
8
  from .message import MessageRule
9
9
  from .message_entities import MessageEntities
10
10
 
@@ -12,9 +12,9 @@ from .message_entities import MessageEntities
12
12
  class StartCommand(
13
13
  MessageRule,
14
14
  requires=[
15
- IsPrivate()
16
- & MessageEntities(MessageEntityType.BOT_COMMAND)
17
- & Markup(["/start <param>", "/start"]),
15
+ IsPrivate(),
16
+ MessageEntities(MessageEntityType.BOT_COMMAND),
17
+ Markup(["/start <param>", "/start"]),
18
18
  ],
19
19
  ):
20
20
  def __init__(
@@ -28,7 +28,7 @@ class StartCommand(
28
28
  self.validator = validator
29
29
  self.alias = alias
30
30
 
31
- async def check(self, _: Message, ctx: Context) -> bool:
31
+ async def check(self, ctx: Context) -> bool:
32
32
  param: str | None = ctx.pop("param", None)
33
33
  validated_param = self.validator(param) if self.validator and param is not None else param
34
34