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,169 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from kungfu.library.monad.option import NOTHING, Nothing, Option
|
|
5
|
+
from nodnod.error import NodeError
|
|
6
|
+
from nodnod.interface.scalar import scalar_node
|
|
7
|
+
from nodnod.node import Node
|
|
8
|
+
|
|
9
|
+
import telegrinder.types
|
|
10
|
+
from telegrinder.bot.cute_types.message import MessageCute
|
|
11
|
+
|
|
12
|
+
type AttachmentType = typing.Literal[
|
|
13
|
+
"animation",
|
|
14
|
+
"audio",
|
|
15
|
+
"document",
|
|
16
|
+
"photo",
|
|
17
|
+
"poll",
|
|
18
|
+
"video",
|
|
19
|
+
"video_note",
|
|
20
|
+
"voice",
|
|
21
|
+
"successful_payment",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
ATTACHMENT_TYPES: typing.Final[tuple[AttachmentType, ...]] = typing.get_args(AttachmentType.__value__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclasses.dataclass
|
|
28
|
+
class Attachment(Node):
|
|
29
|
+
attachment_type: AttachmentType
|
|
30
|
+
|
|
31
|
+
animation: Option[telegrinder.types.Animation] = dataclasses.field(
|
|
32
|
+
default_factory=Nothing,
|
|
33
|
+
kw_only=True,
|
|
34
|
+
)
|
|
35
|
+
audio: Option[telegrinder.types.Audio] = dataclasses.field(
|
|
36
|
+
default_factory=Nothing,
|
|
37
|
+
kw_only=True,
|
|
38
|
+
)
|
|
39
|
+
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
40
|
+
default_factory=Nothing,
|
|
41
|
+
kw_only=True,
|
|
42
|
+
)
|
|
43
|
+
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
|
|
44
|
+
default_factory=Nothing,
|
|
45
|
+
kw_only=True,
|
|
46
|
+
)
|
|
47
|
+
poll: Option[telegrinder.types.Poll] = dataclasses.field(
|
|
48
|
+
default_factory=Nothing,
|
|
49
|
+
kw_only=True,
|
|
50
|
+
)
|
|
51
|
+
voice: Option[telegrinder.types.Voice] = dataclasses.field(
|
|
52
|
+
default_factory=Nothing,
|
|
53
|
+
kw_only=True,
|
|
54
|
+
)
|
|
55
|
+
video: Option[telegrinder.types.Video] = dataclasses.field(
|
|
56
|
+
default_factory=Nothing,
|
|
57
|
+
kw_only=True,
|
|
58
|
+
)
|
|
59
|
+
video_note: Option[telegrinder.types.VideoNote] = dataclasses.field(
|
|
60
|
+
default_factory=Nothing,
|
|
61
|
+
kw_only=True,
|
|
62
|
+
)
|
|
63
|
+
successful_payment: Option[telegrinder.types.SuccessfulPayment] = dataclasses.field(
|
|
64
|
+
default_factory=Nothing,
|
|
65
|
+
kw_only=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def __compose__(cls, message: MessageCute) -> typing.Self:
|
|
70
|
+
for attachment_type in ATTACHMENT_TYPES:
|
|
71
|
+
attachment = getattr(message, attachment_type, NOTHING)
|
|
72
|
+
|
|
73
|
+
if attachment:
|
|
74
|
+
return cls(attachment_type, **{attachment_type: attachment})
|
|
75
|
+
|
|
76
|
+
raise NodeError("No attachment found in message.")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclasses.dataclass
|
|
80
|
+
class Photo(Node):
|
|
81
|
+
sizes: list[telegrinder.types.PhotoSize]
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def __compose__(cls, attachment: Attachment) -> typing.Self:
|
|
85
|
+
return cls(attachment.photo.expect(NodeError("Attachment is not a photo.")))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@scalar_node
|
|
89
|
+
class Video:
|
|
90
|
+
@classmethod
|
|
91
|
+
def __compose__(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
92
|
+
return attachment.video.expect(NodeError("Attachment is not a video."))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@scalar_node
|
|
96
|
+
class VideoNote:
|
|
97
|
+
@classmethod
|
|
98
|
+
def __compose__(cls, attachment: Attachment) -> telegrinder.types.VideoNote:
|
|
99
|
+
return attachment.video_note.expect(NodeError("Attachment is not a video note."))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@scalar_node
|
|
103
|
+
class Audio:
|
|
104
|
+
@classmethod
|
|
105
|
+
def __compose__(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
106
|
+
return attachment.audio.expect(NodeError("Attachment is not an audio."))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@scalar_node
|
|
110
|
+
class Animation:
|
|
111
|
+
@classmethod
|
|
112
|
+
def __compose__(cls, attachment: Attachment) -> telegrinder.types.Animation:
|
|
113
|
+
return attachment.animation.expect(NodeError("Attachment is not an animation."))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@scalar_node
|
|
117
|
+
class Voice:
|
|
118
|
+
@classmethod
|
|
119
|
+
def __compose__(cls, attachment: Attachment) -> telegrinder.types.Voice:
|
|
120
|
+
return attachment.voice.expect(NodeError("Attachment is not a voice."))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@scalar_node
|
|
124
|
+
class Document:
|
|
125
|
+
@classmethod
|
|
126
|
+
def __compose__(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
127
|
+
return attachment.document.expect(NodeError("Attachment is not a document."))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@scalar_node
|
|
131
|
+
class Poll:
|
|
132
|
+
@classmethod
|
|
133
|
+
def __compose__(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
134
|
+
return attachment.poll.expect(NodeError("Attachment is not a poll."))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@scalar_node
|
|
138
|
+
class SuccessfulPayment:
|
|
139
|
+
@classmethod
|
|
140
|
+
def __compose__(cls, attachment: Attachment) -> telegrinder.types.SuccessfulPayment:
|
|
141
|
+
return attachment.successful_payment.expect(NodeError("Attachment is not a successful payment."))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclasses.dataclass
|
|
145
|
+
class MediaGroup(Node):
|
|
146
|
+
id: str
|
|
147
|
+
items: list[MessageCute]
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def __compose__(cls, message: MessageCute) -> typing.Self:
|
|
151
|
+
return cls(
|
|
152
|
+
id=message.media_group_id.expect(NodeError("No media group id.")),
|
|
153
|
+
items=message.media_group_messages.expect(NodeError("No messages collected for media group.")),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = (
|
|
158
|
+
"Animation",
|
|
159
|
+
"Attachment",
|
|
160
|
+
"Audio",
|
|
161
|
+
"Document",
|
|
162
|
+
"MediaGroup",
|
|
163
|
+
"Photo",
|
|
164
|
+
"Poll",
|
|
165
|
+
"SuccessfulPayment",
|
|
166
|
+
"Video",
|
|
167
|
+
"VideoNote",
|
|
168
|
+
"Voice",
|
|
169
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from nodnod.error import NodeError
|
|
4
|
+
from nodnod.interface.scalar import scalar_node
|
|
5
|
+
|
|
6
|
+
from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@scalar_node
|
|
10
|
+
class CallbackQueryData:
|
|
11
|
+
@classmethod
|
|
12
|
+
def __compose__(cls, callback_query: CallbackQueryCute) -> str:
|
|
13
|
+
return callback_query.data.expect(NodeError("Cannot complete decode callback query data."))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@scalar_node
|
|
17
|
+
class CallbackQueryDataJson:
|
|
18
|
+
@classmethod
|
|
19
|
+
def __compose__(cls, callback_query: CallbackQueryCute) -> dict[str, typing.Any]:
|
|
20
|
+
return callback_query.decode_data().expect(
|
|
21
|
+
NodeError("Cannot complete decode callback query data."),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ("CallbackQueryData", "CallbackQueryDataJson")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from nodnod.error import NodeError
|
|
2
|
+
from nodnod.interface.scalar import scalar_node
|
|
3
|
+
from nodnod.node import Scalar
|
|
4
|
+
|
|
5
|
+
from telegrinder.bot.cute_types.message import MessageCute
|
|
6
|
+
from telegrinder.types.enums import ChatType
|
|
7
|
+
from telegrinder.types.objects import Chat, MessageOriginChannel
|
|
8
|
+
|
|
9
|
+
type MessageChannelPost = MessageOriginChannel
|
|
10
|
+
type Post = MessageCute
|
|
11
|
+
type PostId = int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@scalar_node
|
|
15
|
+
class ChatMessageChannelPost:
|
|
16
|
+
@classmethod
|
|
17
|
+
def __compose__(cls, message: MessageCute) -> MessageChannelPost:
|
|
18
|
+
forward_origin = message.forward_origin.expect(NodeError("Message has no forward origin."))
|
|
19
|
+
return forward_origin.only(MessageOriginChannel).expect(NodeError("Message forward origin is not a channel."))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@scalar_node
|
|
23
|
+
class ChatMessageChannelPostId:
|
|
24
|
+
@classmethod
|
|
25
|
+
def __compose__(cls, message_channel_post: ChatMessageChannelPost) -> PostId:
|
|
26
|
+
return message_channel_post.message_id
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@scalar_node
|
|
30
|
+
class ChatMessageChannelPostChannel:
|
|
31
|
+
@classmethod
|
|
32
|
+
def __compose__(cls, message_channel_post: ChatMessageChannelPost) -> Channel:
|
|
33
|
+
return message_channel_post.chat
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@scalar_node
|
|
37
|
+
class ChatMessageChannelPostChannelId:
|
|
38
|
+
@classmethod
|
|
39
|
+
def __compose__(cls, message_channel_post: ChatMessageChannelPost) -> ChannelId:
|
|
40
|
+
return message_channel_post.chat.id
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@scalar_node
|
|
44
|
+
class ChatMessageChannelPostAuthor:
|
|
45
|
+
@classmethod
|
|
46
|
+
def __compose__(cls, message_channel_post: ChatMessageChannelPost) -> str:
|
|
47
|
+
return message_channel_post.author_signature.expect(
|
|
48
|
+
NodeError("Discussion has no signature of the post author."),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@scalar_node
|
|
53
|
+
class ChannelPostNode:
|
|
54
|
+
@classmethod
|
|
55
|
+
def __compose__(cls, message: MessageCute) -> ChannelPost:
|
|
56
|
+
if message.chat.type != ChatType.CHANNEL:
|
|
57
|
+
raise NodeError("Message is not a channel post.")
|
|
58
|
+
return message
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@scalar_node
|
|
62
|
+
class ChannelPostId:
|
|
63
|
+
@classmethod
|
|
64
|
+
def __compose__(cls, channel_post: ChannelPostNode) -> PostId:
|
|
65
|
+
return channel_post.message_id
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@scalar_node
|
|
69
|
+
class ChannelNode:
|
|
70
|
+
@classmethod
|
|
71
|
+
def __compose__(cls, channel_post: ChannelPostNode) -> Channel:
|
|
72
|
+
return channel_post.chat
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@scalar_node
|
|
76
|
+
class ChannelIdNode:
|
|
77
|
+
@classmethod
|
|
78
|
+
def __compose__(cls, channel_post: ChannelPostNode) -> ChannelId:
|
|
79
|
+
return channel_post.chat.id
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
type Channel = Scalar[Chat, ChannelNode]
|
|
83
|
+
type ChannelId = Scalar[int, ChannelIdNode]
|
|
84
|
+
type ChannelPost = Scalar[MessageCute, ChannelPostNode]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = (
|
|
88
|
+
"Channel",
|
|
89
|
+
"ChannelId",
|
|
90
|
+
"ChannelPost",
|
|
91
|
+
"ChannelPostId",
|
|
92
|
+
"ChatMessageChannelPost",
|
|
93
|
+
"ChatMessageChannelPostAuthor",
|
|
94
|
+
"ChatMessageChannelPostChannel",
|
|
95
|
+
"ChatMessageChannelPostChannelId",
|
|
96
|
+
"ChatMessageChannelPostId",
|
|
97
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from kungfu.library.monad.option import NOTHING, Nothing, Option, Some
|
|
5
|
+
from nodnod.interface.data import Node
|
|
6
|
+
|
|
7
|
+
from telegrinder.node.nodes.text import Caption, Text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def single_split(s: str, separator: str) -> tuple[str, str]:
|
|
11
|
+
left, *right = s.split(separator, 1)
|
|
12
|
+
return left, (right[0] if right else "")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cut_mention(text: str) -> tuple[str, Option[str]]:
|
|
16
|
+
left, right = single_split(text, "@")
|
|
17
|
+
return left, Some(right) if right else NOTHING
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CommandInfo(Node):
|
|
22
|
+
name: str
|
|
23
|
+
arguments: str
|
|
24
|
+
mention: Option[str] = field(default_factory=Nothing)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def __compose__(cls, text: Text | Caption) -> typing.Self:
|
|
28
|
+
name, arguments = single_split(text, separator=" ")
|
|
29
|
+
name, mention = cut_mention(name)
|
|
30
|
+
return cls(name, arguments, mention)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ("CommandInfo", "cut_mention", "single_split")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from nodnod.error import NodeError
|
|
5
|
+
from nodnod.interface.generic import generic_node
|
|
6
|
+
from nodnod.node import Node
|
|
7
|
+
|
|
8
|
+
from telegrinder.bot.dispatch.context import Context
|
|
9
|
+
|
|
10
|
+
type ExceptionType = type[Exception]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def can_catch[ExceptionT: Exception](
|
|
14
|
+
exc: Exception | ExceptionType,
|
|
15
|
+
exc_types: type[ExceptionT] | tuple[type[ExceptionT], ...],
|
|
16
|
+
) -> typing.TypeGuard[ExceptionT]:
|
|
17
|
+
return issubclass(exc, exc_types) if isinstance(exc, type) else isinstance(exc, exc_types)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@generic_node
|
|
21
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
22
|
+
class Error[*Exceptions = *tuple[type[Exception], ...]](Node):
|
|
23
|
+
exception_update: Exception
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def exception[T: Exception = Exception](self: Error[*tuple[T, ...]]) -> T:
|
|
27
|
+
return self.exception_update # type: ignore[reportReturnType]
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def __compose__(
|
|
31
|
+
cls,
|
|
32
|
+
exceptions: tuple[typing.Unpack[Exceptions]],
|
|
33
|
+
context: Context,
|
|
34
|
+
) -> typing.Self:
|
|
35
|
+
exception_update = context.exception_update.expect(NodeError("No exception."))
|
|
36
|
+
|
|
37
|
+
if can_catch(exception_update, exceptions): # type: ignore
|
|
38
|
+
return cls(exception_update=exception_update)
|
|
39
|
+
|
|
40
|
+
raise NodeError("Foreign exception.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ("Error",)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import msgspec
|
|
5
|
+
from nodnod.error import NodeError
|
|
6
|
+
from nodnod.interface.generic import generic_node
|
|
7
|
+
|
|
8
|
+
from telegrinder.api.api import API
|
|
9
|
+
from telegrinder.bot.cute_types.base import BaseCute
|
|
10
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
11
|
+
from telegrinder.msgspec_utils import decoder
|
|
12
|
+
from telegrinder.tools.fullname import fullname
|
|
13
|
+
from telegrinder.types.objects import Model, Update
|
|
14
|
+
|
|
15
|
+
if typing.TYPE_CHECKING:
|
|
16
|
+
from _typeshed import DataclassInstance
|
|
17
|
+
|
|
18
|
+
type DataclassType = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@generic_node
|
|
22
|
+
class EventNode[Dataclass: DataclassType]: # type: ignore[reportRedeclaration]
|
|
23
|
+
@classmethod
|
|
24
|
+
def __compose__(
|
|
25
|
+
cls,
|
|
26
|
+
api: API,
|
|
27
|
+
raw_update: Update,
|
|
28
|
+
dataclass: type[Dataclass],
|
|
29
|
+
update_cute: UpdateCute,
|
|
30
|
+
) -> DataclassType:
|
|
31
|
+
orig_dataclass = typing.get_origin(dataclass) or dataclass
|
|
32
|
+
|
|
33
|
+
if orig_dataclass is UpdateCute:
|
|
34
|
+
return update_cute
|
|
35
|
+
|
|
36
|
+
if issubclass(orig_dataclass, BaseCute | Model):
|
|
37
|
+
incoming_update = (
|
|
38
|
+
update_cute.incoming_update if issubclass(orig_dataclass, BaseCute) else raw_update.incoming_update
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if type(incoming_update) is not orig_dataclass:
|
|
42
|
+
raise NodeError(f"Incoming update is not `{fullname(orig_dataclass)}`.")
|
|
43
|
+
|
|
44
|
+
return incoming_update
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
if issubclass(orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
|
|
48
|
+
orig_dataclass,
|
|
49
|
+
):
|
|
50
|
+
obj = decoder.convert(
|
|
51
|
+
obj=raw_update.incoming_update,
|
|
52
|
+
type=dataclass,
|
|
53
|
+
from_attributes=True,
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
obj = dataclass(**raw_update.incoming_update.to_full_dict())
|
|
57
|
+
|
|
58
|
+
return obj
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
raise NodeError(
|
|
61
|
+
f"Cannot parse an update object into `{fullname(dataclass)}`",
|
|
62
|
+
from_error=NodeError(exc),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if typing.TYPE_CHECKING:
|
|
67
|
+
type EventNode[Dataclass: DataclassType] = Dataclass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ("EventNode",)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from nodnod.error import NodeError
|
|
4
|
+
from nodnod.interface.node_constructor import NodeConstructor
|
|
5
|
+
|
|
6
|
+
import telegrinder.types as tg_types
|
|
7
|
+
from telegrinder.api.api import API
|
|
8
|
+
from telegrinder.node.nodes.attachment import Animation, Audio, Document, Photo, Video, VideoNote, Voice
|
|
9
|
+
|
|
10
|
+
type Attachment = Animation | Audio | Document | Photo | Video | VideoNote | Voice
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileIdNode(NodeConstructor):
|
|
14
|
+
def __init__(self, attachment_node: type[Attachment], /) -> None:
|
|
15
|
+
self.__map__ = {Attachment: attachment_node}
|
|
16
|
+
|
|
17
|
+
def __compose__(self, attach: Attachment) -> str:
|
|
18
|
+
if isinstance(attach, Photo):
|
|
19
|
+
return attach.sizes[-1].file_id
|
|
20
|
+
return attach.file_id
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FileNode(NodeConstructor):
|
|
24
|
+
def __init__(self, attachment_node: type[Attachment], /) -> None:
|
|
25
|
+
self.__map__ = {str: FileIdNode[attachment_node]}
|
|
26
|
+
|
|
27
|
+
async def __compose__(self, api: API, file_id: str) -> tg_types.File:
|
|
28
|
+
return (await api.get_file(file_id=file_id)).expect(NodeError("File can't be downloaded."))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if typing.TYPE_CHECKING:
|
|
32
|
+
type FileId[T: Attachment] = str
|
|
33
|
+
type File[T: Attachment] = tg_types.File
|
|
34
|
+
else:
|
|
35
|
+
FileId = FileIdNode
|
|
36
|
+
File = FileNode
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ("File", "FileId")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from nodnod.error import NodeError
|
|
4
|
+
from nodnod.node import Node
|
|
5
|
+
from nodnod.value import Value
|
|
6
|
+
|
|
7
|
+
from telegrinder.node.scope import TELEGRINDER_CONTEXT, global_node
|
|
8
|
+
from telegrinder.tools.fullname import fullname
|
|
9
|
+
|
|
10
|
+
NODEFAULT: typing.Final = object()
|
|
11
|
+
|
|
12
|
+
_Unspecialized = typing.NewType("_Unspecialized", type)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@global_node
|
|
16
|
+
class GlobalNode[T = _Unspecialized](Node, abstract=True):
|
|
17
|
+
@typing.overload
|
|
18
|
+
@classmethod
|
|
19
|
+
def set(cls: type[GlobalNode[_Unspecialized]], value: typing.Self, /) -> None: ... # type: ignore
|
|
20
|
+
|
|
21
|
+
@typing.overload
|
|
22
|
+
@classmethod
|
|
23
|
+
def set(cls, value: T, /) -> None: ...
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def set(cls, value: T | typing.Self, /) -> None:
|
|
27
|
+
TELEGRINDER_CONTEXT.node_global_scope.push(Value(cls, value))
|
|
28
|
+
|
|
29
|
+
@typing.overload
|
|
30
|
+
@classmethod
|
|
31
|
+
def get(cls: type[GlobalNode[_Unspecialized]], /) -> typing.Self: # type: ignore
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@typing.overload
|
|
35
|
+
@classmethod
|
|
36
|
+
def get(cls) -> T: ...
|
|
37
|
+
|
|
38
|
+
@typing.overload
|
|
39
|
+
@classmethod
|
|
40
|
+
def get[Default](cls: type[GlobalNode[_Unspecialized]], *, default: Default) -> typing.Self | Default: # type: ignore
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
@typing.overload
|
|
44
|
+
@classmethod
|
|
45
|
+
def get[Default](cls, *, default: Default) -> T | Default: ...
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get(cls, *, default: typing.Any = NODEFAULT) -> typing.Any:
|
|
49
|
+
if default is not NODEFAULT and cls not in TELEGRINDER_CONTEXT.node_global_scope:
|
|
50
|
+
return default
|
|
51
|
+
|
|
52
|
+
if (value := TELEGRINDER_CONTEXT.node_global_scope.get(cls)) is None and default is NODEFAULT:
|
|
53
|
+
raise NodeError(f"Node `{fullname(cls)}` has no global value.")
|
|
54
|
+
|
|
55
|
+
return value.value if value is not None else default
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def unset(cls) -> None:
|
|
59
|
+
TELEGRINDER_CONTEXT.node_global_scope.pop(cls, None)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def __compose__(cls) -> T:
|
|
63
|
+
return cls.get()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ("GlobalNode",)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import dataclasses
|
|
3
|
+
import gettext
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import typing
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
|
|
9
|
+
from nodnod import Node
|
|
10
|
+
|
|
11
|
+
from telegrinder.node.nodes.global_node import GlobalNode
|
|
12
|
+
from telegrinder.node.nodes.source import Locale
|
|
13
|
+
|
|
14
|
+
type Separator = KeySeparator
|
|
15
|
+
|
|
16
|
+
DEFAULT_SEPARATOR: typing.Final = "-"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclasses.dataclass(frozen=True)
|
|
20
|
+
class KeySeparator(GlobalNode[Separator]):
|
|
21
|
+
value: str
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def set(cls, value: str, /) -> None:
|
|
25
|
+
super().set(cls(value))
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def compose(cls) -> Separator:
|
|
29
|
+
return cls.get(default=cls(DEFAULT_SEPARATOR))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclasses.dataclass(kw_only=True)
|
|
33
|
+
class ABCTranslator(Node, abc.ABC):
|
|
34
|
+
locale: str
|
|
35
|
+
separator: str
|
|
36
|
+
_keys: list[str] = dataclasses.field(default_factory=list[str], init=False)
|
|
37
|
+
|
|
38
|
+
@typing.overload
|
|
39
|
+
def __call__(self, **context: typing.Any) -> str: ...
|
|
40
|
+
|
|
41
|
+
@typing.overload
|
|
42
|
+
def __call__(self, message_id: str, /, **context: typing.Any) -> str: ...
|
|
43
|
+
|
|
44
|
+
def __call__(self, message_id: str | None = None, **context: typing.Any) -> str:
|
|
45
|
+
result = self.translate(message_id or self.message_id, **context)
|
|
46
|
+
if not message_id:
|
|
47
|
+
self._keys.clear()
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
def __getattr__(self, __key: str) -> typing.Self:
|
|
51
|
+
self._keys.append(__key)
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def message_id(self) -> str:
|
|
56
|
+
return self.separator.join(self._keys)
|
|
57
|
+
|
|
58
|
+
@abc.abstractmethod
|
|
59
|
+
def translate(self, message_id: str, **context: typing.Any) -> str:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def compose(cls, locale: Locale, separator: KeySeparator) -> typing.Self:
|
|
64
|
+
return cls(locale=locale, separator=separator.value)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclasses.dataclass
|
|
68
|
+
class I18NConfig:
|
|
69
|
+
domain: str
|
|
70
|
+
folder: str | pathlib.Path
|
|
71
|
+
default_locale: str = dataclasses.field(default="en")
|
|
72
|
+
|
|
73
|
+
@cached_property
|
|
74
|
+
def translators(self) -> dict[str, gettext.GNUTranslations]:
|
|
75
|
+
result = {}
|
|
76
|
+
|
|
77
|
+
for name in os.listdir(self.folder):
|
|
78
|
+
if not os.path.isdir(os.path.join(self.folder, name)):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
mo_path = os.path.join(self.folder, name, "LC_MESSAGES", f"{self.domain}.mo")
|
|
82
|
+
|
|
83
|
+
if os.path.exists(mo_path):
|
|
84
|
+
with open(mo_path, "rb") as f:
|
|
85
|
+
result[name] = gettext.GNUTranslations(f)
|
|
86
|
+
elif os.path.exists(mo_path[:-2] + "po"):
|
|
87
|
+
raise FileNotFoundError(".po files should be compiled first")
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
def get_translator(self, locale: str, /) -> gettext.GNUTranslations:
|
|
92
|
+
locale = locale if locale in self.translators else self.default_locale
|
|
93
|
+
return self.translators[locale]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BaseTranslator(ABCTranslator):
|
|
97
|
+
config: typing.ClassVar[I18NConfig]
|
|
98
|
+
|
|
99
|
+
def __class_getitem__(cls, config: I18NConfig, /) -> typing.Any:
|
|
100
|
+
return type(cls.__name__, (cls,), dict(config=config, __module__=cls.__module__))
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def configure(cls, config: I18NConfig, /) -> None:
|
|
104
|
+
cls.config = config
|
|
105
|
+
|
|
106
|
+
def translate(self, message_id: str, **context: typing.Any) -> str:
|
|
107
|
+
return self.config.get_translator(self.locale).gettext(message_id).format(**context)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = ("ABCTranslator", "BaseTranslator", "I18NConfig", "KeySeparator")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from nodnod.error import NodeError
|
|
2
|
+
from nodnod.interface.scalar import scalar_node
|
|
3
|
+
|
|
4
|
+
from telegrinder.api.api import API
|
|
5
|
+
from telegrinder.node.scope import global_node
|
|
6
|
+
from telegrinder.types.objects import User
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@global_node
|
|
10
|
+
@scalar_node
|
|
11
|
+
class Me:
|
|
12
|
+
@classmethod
|
|
13
|
+
async def __compose__(cls, api: API) -> User:
|
|
14
|
+
me = await api.get_me()
|
|
15
|
+
return me.expect(NodeError("Can't complete api.get_me() request."))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@global_node
|
|
19
|
+
@scalar_node
|
|
20
|
+
class BotUsername:
|
|
21
|
+
@classmethod
|
|
22
|
+
def __compose__(cls, me: Me) -> str:
|
|
23
|
+
return me.username.unwrap()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ("BotUsername", "Me")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from nodnod.error import NodeError
|
|
2
|
+
from nodnod.interface.scalar import scalar_node
|
|
3
|
+
|
|
4
|
+
from telegrinder.bot.cute_types.message import MessageCute
|
|
5
|
+
from telegrinder.types.objects import MessageEntity
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@scalar_node
|
|
9
|
+
class MessageEntities:
|
|
10
|
+
@classmethod
|
|
11
|
+
def __compose__(cls, message: MessageCute) -> list[MessageEntity]:
|
|
12
|
+
return message.entities.expect(NodeError("Message has no entities."))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ("MessageEntities",)
|