dshellInterpreter 0.2.21.7__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.
- Dshell/DISCORD_COMMANDS/__init__.py +7 -0
- Dshell/DISCORD_COMMANDS/dshell_channel.py +612 -0
- Dshell/DISCORD_COMMANDS/dshell_interaction.py +89 -0
- Dshell/DISCORD_COMMANDS/dshell_member.py +274 -0
- Dshell/DISCORD_COMMANDS/dshell_message.py +444 -0
- Dshell/DISCORD_COMMANDS/dshell_pastbin.py +28 -0
- Dshell/DISCORD_COMMANDS/dshell_role.py +128 -0
- Dshell/DISCORD_COMMANDS/utils/__init__.py +8 -0
- Dshell/DISCORD_COMMANDS/utils/utils_global.py +155 -0
- Dshell/DISCORD_COMMANDS/utils/utils_list.py +109 -0
- Dshell/DISCORD_COMMANDS/utils/utils_member.py +30 -0
- Dshell/DISCORD_COMMANDS/utils/utils_message.py +78 -0
- Dshell/DISCORD_COMMANDS/utils/utils_permissions.py +94 -0
- Dshell/DISCORD_COMMANDS/utils/utils_string.py +157 -0
- Dshell/DISCORD_COMMANDS/utils/utils_thread.py +35 -0
- Dshell/_DshellInterpreteur/__init__.py +4 -0
- Dshell/_DshellInterpreteur/cached_messages.py +4 -0
- Dshell/_DshellInterpreteur/dshell_arguments.py +74 -0
- Dshell/_DshellInterpreteur/dshell_interpreter.py +671 -0
- Dshell/_DshellInterpreteur/errors.py +8 -0
- Dshell/_DshellParser/__init__.py +2 -0
- Dshell/_DshellParser/ast_nodes.py +675 -0
- Dshell/_DshellParser/dshell_parser.py +408 -0
- Dshell/_DshellTokenizer/__init__.py +4 -0
- Dshell/_DshellTokenizer/dshell_keywords.py +193 -0
- Dshell/_DshellTokenizer/dshell_token_type.py +58 -0
- Dshell/_DshellTokenizer/dshell_tokenizer.py +146 -0
- Dshell/__init__.py +3 -0
- Dshell/_utils.py +1 -0
- dshellinterpreter-0.2.21.7.dist-info/METADATA +37 -0
- dshellinterpreter-0.2.21.7.dist-info/RECORD +34 -0
- dshellinterpreter-0.2.21.7.dist-info/WHEEL +5 -0
- dshellinterpreter-0.2.21.7.dist-info/licenses/LICENSE +21 -0
- dshellinterpreter-0.2.21.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
from asyncio import sleep
|
|
2
|
+
from re import findall, sub, escape
|
|
3
|
+
from typing import TypeVar, Union, Any, Optional, Callable
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from pycordViews import EasyModifiedViews
|
|
6
|
+
from pycordViews.views.errors import CustomIDNotFound
|
|
7
|
+
|
|
8
|
+
from discord import AutoShardedBot, Embed, Colour, PermissionOverwrite, Permissions, Guild, Member, Role, Message, Interaction, ButtonStyle
|
|
9
|
+
from discord.ui import Button
|
|
10
|
+
from discord.abc import PrivateChannel
|
|
11
|
+
|
|
12
|
+
from .errors import *
|
|
13
|
+
from .._DshellParser.ast_nodes import *
|
|
14
|
+
from ..DISCORD_COMMANDS.utils.utils_permissions import DshellPermissions
|
|
15
|
+
from .._DshellParser.dshell_parser import parse
|
|
16
|
+
from .._DshellParser.dshell_parser import to_postfix, print_ast
|
|
17
|
+
from .._DshellTokenizer.dshell_keywords import *
|
|
18
|
+
from .._DshellTokenizer.dshell_token_type import DshellTokenType as DTT
|
|
19
|
+
from .._DshellTokenizer.dshell_token_type import Token
|
|
20
|
+
from .._DshellTokenizer.dshell_tokenizer import DshellTokenizer
|
|
21
|
+
from .cached_messages import dshell_cached_messages
|
|
22
|
+
from .dshell_arguments import DshellArguments
|
|
23
|
+
|
|
24
|
+
All_nodes = TypeVar('All_nodes', IfNode, LoopNode, ElseNode, ElifNode, ArgsCommandNode, VarNode)
|
|
25
|
+
context = TypeVar('context', AutoShardedBot, Message, PrivateChannel, Interaction)
|
|
26
|
+
ButtonStyleValues: tuple = tuple(i.name for i in ButtonStyle)
|
|
27
|
+
|
|
28
|
+
class DshellInterpreteur:
|
|
29
|
+
"""
|
|
30
|
+
Discord Dshell interpreter.
|
|
31
|
+
Make what you want with Dshell code to interact with Discord !
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, code: str, ctx: context,
|
|
35
|
+
debug: bool = False,
|
|
36
|
+
vars: Optional[str] = None,
|
|
37
|
+
vars_env: Optional[dict[str, Any]] = None):
|
|
38
|
+
"""
|
|
39
|
+
Interpreter Dshell code
|
|
40
|
+
:param code: The code to interpret. Each line must end with a newline character, except SEPARATOR and SUB_SEPARATOR tokens.
|
|
41
|
+
:param ctx: The context in which the code is executed. It can be a Discord bot, a message, or a channel.
|
|
42
|
+
:param debug: If True, prints the AST of the code and put the ctx to None.
|
|
43
|
+
:param vars: Optional dictionary of variables to initialize in the interpreter's environment.
|
|
44
|
+
:param vars_env: Optional dictionary of additional environment variables to add to the interpreter's environment.
|
|
45
|
+
|
|
46
|
+
Note: __message_before__ (message content before edit) can be overwritten by vars_env parameter.
|
|
47
|
+
"""
|
|
48
|
+
self.ast: list[ASTNode] = parse(DshellTokenizer(code).start(), StartNode([]))[0]
|
|
49
|
+
message = ctx.message if isinstance(ctx, Interaction) else ctx
|
|
50
|
+
self.env: dict[str, Any] = {
|
|
51
|
+
'__ret__': None, # environment variables, '__ret__' is used to store the return value of commands
|
|
52
|
+
|
|
53
|
+
'__author__': message.author.id,
|
|
54
|
+
'__author_name__': message.author.name,
|
|
55
|
+
'__author_display_name__': message.author.display_name,
|
|
56
|
+
'__author_avatar__': message.author.display_avatar.url if message.author.display_avatar else None,
|
|
57
|
+
'__author_discriminator__': message.author.discriminator,
|
|
58
|
+
'__author_bot__': message.author.bot,
|
|
59
|
+
'__author_nick__': message.author.nick if hasattr(message.author, 'nick') else None,
|
|
60
|
+
'__author_id__': message.author.id,
|
|
61
|
+
'__author_add_reaction__': None, # Can be overwritten by add vars_env parameter to get the author on message add event reaction
|
|
62
|
+
'__author_remove_reaction__': None, # Can be overwritten by add vars_env parameter to get the author on message remove event reaction
|
|
63
|
+
|
|
64
|
+
'__message__': message.content,
|
|
65
|
+
'__message_content__': message.content,
|
|
66
|
+
'__message_id__': message.id,
|
|
67
|
+
'__message_author__': message.author.id,
|
|
68
|
+
'__message_before__': message.content, # same as __message__, but before edit. Can be overwritten by add vars_env parameter
|
|
69
|
+
'__message_created_at__': str(message.created_at),
|
|
70
|
+
'__message_edited_at__': str(message.edited_at),
|
|
71
|
+
'__message_reactions__': ListNode([str(reaction.emoji) for reaction in message.reactions]),
|
|
72
|
+
'__message_add_reaction__': None, # Can be overwritten by add vars_env parameter to get the reaction added on message add event reaction
|
|
73
|
+
'__message_remove_reaction__': None, # Can be overwritten by add vars_env parameter to get the reaction removed on message remove event reaction
|
|
74
|
+
'__message_url__': message.jump_url if hasattr(message, 'jump_url') else None,
|
|
75
|
+
'__last_message__': message.channel.last_message_id,
|
|
76
|
+
|
|
77
|
+
'__channel__': message.channel.id,
|
|
78
|
+
'__channel_name__': message.channel.name,
|
|
79
|
+
'__channel_type__': message.channel.type.name if hasattr(message.channel, 'type') else None,
|
|
80
|
+
'__channel_id__': message.channel.id,
|
|
81
|
+
'__private_channel__': isinstance(message.channel, PrivateChannel),
|
|
82
|
+
|
|
83
|
+
'__guild__': message.channel.guild.id,
|
|
84
|
+
'__guild_name__': message.channel.guild.name,
|
|
85
|
+
'__guild_id__': message.channel.guild.id,
|
|
86
|
+
'__guild_members__': ListNode([member.id for member in message.channel.guild.members]),
|
|
87
|
+
'__guild_member_count__': message.channel.guild.member_count,
|
|
88
|
+
'__guild_icon__': message.channel.guild.icon.url if message.channel.guild.icon else None,
|
|
89
|
+
'__guild_owner_id__': message.channel.guild.owner_id,
|
|
90
|
+
'__guild_description__': message.channel.guild.description,
|
|
91
|
+
'__guild_roles__': ListNode([role.id for role in message.channel.guild.roles]),
|
|
92
|
+
'__guild_roles_count__': len(message.channel.guild.roles),
|
|
93
|
+
'__guild_emojis__': ListNode([emoji.id for emoji in message.channel.guild.emojis]),
|
|
94
|
+
'__guild_emojis_count__': len(message.channel.guild.emojis),
|
|
95
|
+
'__guild_channels__': ListNode([channel.id for channel in message.channel.guild.channels]),
|
|
96
|
+
'__guild_text_channels__': ListNode([channel.id for channel in message.channel.guild.text_channels]),
|
|
97
|
+
'__guild_voice_channels__': ListNode([channel.id for channel in message.channel.guild.voice_channels]),
|
|
98
|
+
'__guild_categories__': ListNode([channel.id for channel in message.channel.guild.categories]),
|
|
99
|
+
'__guild_stage_channels__': ListNode([channel.id for channel in message.channel.guild.stage_channels]),
|
|
100
|
+
'__guild_forum_channels__': ListNode([channel.id for channel in message.channel.guild.forum_channels]),
|
|
101
|
+
'__guild_channels_count__': len(message.channel.guild.channels),
|
|
102
|
+
|
|
103
|
+
} if message is not None and not debug else {'__ret__': None} # {} is used in debug mode, when ctx is None
|
|
104
|
+
if vars_env is not None: # add the variables to the environment
|
|
105
|
+
self.env.update(vars_env)
|
|
106
|
+
self.vars = vars or ''
|
|
107
|
+
self.ctx: context = ctx
|
|
108
|
+
dshell_cached_messages.set(dict()) # save all messages view in the current scoop
|
|
109
|
+
if debug:
|
|
110
|
+
print_ast(self.ast)
|
|
111
|
+
|
|
112
|
+
async def execute(self, ast: Optional[list[All_nodes]] = None):
|
|
113
|
+
"""
|
|
114
|
+
Executes the abstract syntax tree (AST) generated from the Dshell code.
|
|
115
|
+
|
|
116
|
+
This asynchronous method traverses and interprets each node in the AST, executing commands,
|
|
117
|
+
handling control flow structures (such as if, elif, else, and loops), managing variables,
|
|
118
|
+
and interacting with Discord through the provided context. It supports command execution,
|
|
119
|
+
variable assignment, sleep operations, and permission handling, among other features.
|
|
120
|
+
|
|
121
|
+
:param ast: Optional list of AST nodes to execute. If None, uses the interpreter's main AST.
|
|
122
|
+
:raises RuntimeError: If an EndNode is encountered, indicating execution should be stopped.
|
|
123
|
+
:raises Exception: If sleep duration is out of allowed bounds.
|
|
124
|
+
"""
|
|
125
|
+
if ast is None:
|
|
126
|
+
ast = self.ast
|
|
127
|
+
|
|
128
|
+
for node in ast:
|
|
129
|
+
|
|
130
|
+
if isinstance(node, StartNode):
|
|
131
|
+
await self.execute(node.body)
|
|
132
|
+
|
|
133
|
+
if isinstance(node, CommandNode):
|
|
134
|
+
result = await call_function(dshell_commands[node.name], node.body, self)
|
|
135
|
+
self.env[f'__{node.name}__'] = result # return value of the command
|
|
136
|
+
self.env['__ret__'] = result # global return variable for all commands
|
|
137
|
+
|
|
138
|
+
elif isinstance(node, ParamNode):
|
|
139
|
+
params = await get_params(node, self)
|
|
140
|
+
self.env.update(params) # update the environment
|
|
141
|
+
|
|
142
|
+
elif isinstance(node, IfNode):
|
|
143
|
+
elif_valid = False
|
|
144
|
+
if await eval_expression(node.condition, self):
|
|
145
|
+
await self.execute(node.body)
|
|
146
|
+
continue
|
|
147
|
+
elif node.elif_nodes:
|
|
148
|
+
|
|
149
|
+
for i in node.elif_nodes:
|
|
150
|
+
if await eval_expression(i.condition, self):
|
|
151
|
+
await self.execute(i.body)
|
|
152
|
+
elif_valid = True
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
if not elif_valid and node.else_body is not None:
|
|
156
|
+
await self.execute(node.else_body.body)
|
|
157
|
+
|
|
158
|
+
elif isinstance(node, LoopNode):
|
|
159
|
+
self.env[node.variable.name.value] = 0
|
|
160
|
+
for i in DshellIterator(await eval_expression(node.variable.body, self)):
|
|
161
|
+
self.env[node.variable.name.value] = i
|
|
162
|
+
c = deepcopy(node.body)
|
|
163
|
+
await self.execute(c)
|
|
164
|
+
del c
|
|
165
|
+
|
|
166
|
+
elif isinstance(node, VarNode):
|
|
167
|
+
|
|
168
|
+
first_node = node.body[0]
|
|
169
|
+
if isinstance(first_node, IfNode):
|
|
170
|
+
self.env[node.name.value] = await eval_expression_inline(first_node, self)
|
|
171
|
+
|
|
172
|
+
elif isinstance(first_node, EmbedNode):
|
|
173
|
+
# rebuild the embed if it already exists
|
|
174
|
+
if node.name.value in self.env and isinstance(self.env[node.name.value], Embed):
|
|
175
|
+
self.env[node.name.value] = await rebuild_embed(self.env[node.name.value], first_node.body, first_node.fields, self)
|
|
176
|
+
else:
|
|
177
|
+
self.env[node.name.value] = await build_embed(first_node.body, first_node.fields, self)
|
|
178
|
+
|
|
179
|
+
elif isinstance(first_node, PermissionNode):
|
|
180
|
+
# rebuild the permissions if it already exists
|
|
181
|
+
if node.name.value in self.env and isinstance(self.env[node.name.value], dict):
|
|
182
|
+
self.env[node.name.value].update(await build_permission(first_node.body, self))
|
|
183
|
+
else:
|
|
184
|
+
self.env[node.name.value] = await build_permission(first_node.body, self)
|
|
185
|
+
|
|
186
|
+
elif isinstance(first_node, UiNode):
|
|
187
|
+
# rebuild the UI if it already exists
|
|
188
|
+
if node.name.value in self.env and isinstance(self.env[node.name.value], EasyModifiedViews):
|
|
189
|
+
self.env[node.name.value] = await rebuild_ui(first_node, self.env[node.name.value], self)
|
|
190
|
+
else:
|
|
191
|
+
self.env[node.name.value] = await build_ui(first_node, self)
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
self.env[node.name.value] = await eval_expression(node.body, self)
|
|
195
|
+
|
|
196
|
+
elif isinstance(node, SleepNode):
|
|
197
|
+
sleep_time = await eval_expression(node.body, self)
|
|
198
|
+
if sleep_time > 3600:
|
|
199
|
+
raise Exception(f"Sleep time is too long! ({sleep_time} seconds) - maximum is 3600 seconds)")
|
|
200
|
+
elif sleep_time < 1:
|
|
201
|
+
raise Exception(f"Sleep time is too short! ({sleep_time} seconds) - minimum is 1 second)")
|
|
202
|
+
|
|
203
|
+
await sleep(sleep_time)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
elif isinstance(node, EndNode):
|
|
207
|
+
if await self.eval_data_token(node.error_message):
|
|
208
|
+
raise RuntimeError("Execution stopped - EndNode encountered")
|
|
209
|
+
else:
|
|
210
|
+
raise DshellInterpreterStopExecution()
|
|
211
|
+
|
|
212
|
+
async def eval_data_token(self, token: Token):
|
|
213
|
+
"""
|
|
214
|
+
Eval a data token and returns its value in Python.
|
|
215
|
+
:param token: The token to evaluate.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
if not hasattr(token, 'type'):
|
|
219
|
+
return token
|
|
220
|
+
|
|
221
|
+
if token.type in (DTT.INT, DTT.MENTION):
|
|
222
|
+
return int(token.value)
|
|
223
|
+
elif token.type == DTT.FLOAT:
|
|
224
|
+
return float(token.value)
|
|
225
|
+
elif token.type == DTT.BOOL:
|
|
226
|
+
return token.value.lower() == "true"
|
|
227
|
+
elif token.type == DTT.NONE:
|
|
228
|
+
return None
|
|
229
|
+
elif token.type == DTT.LIST:
|
|
230
|
+
return ListNode(
|
|
231
|
+
[await self.eval_data_token(tok) for tok in token.value]) # token.value contient déjà une liste de Token
|
|
232
|
+
elif token.type == DTT.IDENT:
|
|
233
|
+
if token.value in self.env.keys():
|
|
234
|
+
return self.env[token.value]
|
|
235
|
+
return token.value
|
|
236
|
+
elif token.type == DTT.EVAL_GROUP:
|
|
237
|
+
await self.execute(parse([token.value], StartNode([]))[0]) # obliger de parser car ce il n'est pas dejà un AST
|
|
238
|
+
return self.env['__ret__']
|
|
239
|
+
elif token.type == DTT.STR:
|
|
240
|
+
for match in findall(rf"\$({'|'.join(self.env.keys())})", token.value):
|
|
241
|
+
token.value = token.value.replace('$' + match, str(self.env[match]))
|
|
242
|
+
return token.value
|
|
243
|
+
else:
|
|
244
|
+
return token.value # fallback
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def get_params(node: ParamNode, interpreter: DshellInterpreteur) -> dict[str, Any]:
|
|
248
|
+
"""
|
|
249
|
+
Get the parameters from a ParamNode.
|
|
250
|
+
:param node: The ParamNode to get the parameters from.
|
|
251
|
+
:param interpreter: The Dshell interpreter instance.
|
|
252
|
+
:return: A dictionary of parameters.
|
|
253
|
+
"""
|
|
254
|
+
def remplacer(match) -> str:
|
|
255
|
+
spacial_char = match.group(1)
|
|
256
|
+
if spacial_char:
|
|
257
|
+
return ''
|
|
258
|
+
return match.group(4)
|
|
259
|
+
|
|
260
|
+
variables = interpreter.vars
|
|
261
|
+
regrouped_parameters: DshellArguments = await regroupe_commandes(node.body, interpreter)
|
|
262
|
+
|
|
263
|
+
_ = DshellTokenizer(variables).start()
|
|
264
|
+
regrouped_variables = await regroupe_commandes(_[0] if _ else tuple(), interpreter)
|
|
265
|
+
|
|
266
|
+
already_modified = set()
|
|
267
|
+
variables_non_specified_parameters = regrouped_variables.parameters.pop('*', None).value # remove non-specified parameters
|
|
268
|
+
|
|
269
|
+
for param_name, param_data in regrouped_variables.parameters.items():
|
|
270
|
+
regrouped_parameters.update_parameter(param_name, param_data)
|
|
271
|
+
variables = sub(rf"--([*']?)({escape(param_name)})\s+(.*)\s*?(.*)$", remplacer, variables, count=1)
|
|
272
|
+
already_modified.add(param_name)
|
|
273
|
+
|
|
274
|
+
index_variable = 0
|
|
275
|
+
for var in regrouped_parameters.parameters.keys():
|
|
276
|
+
if var not in already_modified:
|
|
277
|
+
|
|
278
|
+
parameter_type = regrouped_parameters.get_parameter(var).type
|
|
279
|
+
|
|
280
|
+
if parameter_type == DTT.PARAMETER and index_variable < len(variables_non_specified_parameters):
|
|
281
|
+
regrouped_parameters.set_parameter(var, variables_non_specified_parameters[index_variable], parameter_type) # variables_post_regrouped[index_variable] n'est pas un token donc impossible de l'évaluer ! pose problème dans les commandes qui requière autre chose que des str
|
|
282
|
+
index_variable += 1
|
|
283
|
+
|
|
284
|
+
elif parameter_type == DTT.STR_PARAMETER:
|
|
285
|
+
variables_post_regrouped: list[str] = variables.strip().split(' ') if variables else [] # set uniquement pour les paramètres full str
|
|
286
|
+
regrouped_parameters.set_parameter(var, ' '.join(variables_post_regrouped[index_variable:]), parameter_type)
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
elif parameter_type == DTT.PARAMETERS:
|
|
290
|
+
regrouped_parameters.set_parameter(var, ListNode(variables_non_specified_parameters[index_variable:]), parameter_type)
|
|
291
|
+
break
|
|
292
|
+
|
|
293
|
+
for param_name, param_data in regrouped_parameters.parameters.items():
|
|
294
|
+
if param_data.obligatory and param_data.value == '*':
|
|
295
|
+
raise Exception(f"Parameter '{param_name}' is obligatory but not specified!")
|
|
296
|
+
|
|
297
|
+
x = regrouped_parameters.get_dict_parameters()
|
|
298
|
+
x.pop('*', None)
|
|
299
|
+
print(x)
|
|
300
|
+
return x
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
async def eval_expression_inline(if_node: IfNode, interpreter: DshellInterpreteur) -> Token:
|
|
304
|
+
"""
|
|
305
|
+
Eval a conditional expression inline.
|
|
306
|
+
:param if_node: The IfNode to evaluate.
|
|
307
|
+
:param interpreter: The Dshell interpreter instance.
|
|
308
|
+
"""
|
|
309
|
+
if await eval_expression(if_node.condition, interpreter):
|
|
310
|
+
return await eval_expression(if_node.body, interpreter)
|
|
311
|
+
else:
|
|
312
|
+
return await eval_expression(if_node.else_body.body, interpreter)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
async def eval_expression(tokens: list[Token], interpreter: DshellInterpreteur) -> Any:
|
|
316
|
+
"""
|
|
317
|
+
Evaluates an arithmetic and logical expression.
|
|
318
|
+
:param tokens: A list of tokens representing the expression.
|
|
319
|
+
:param interpreter: The Dshell interpreter instance.
|
|
320
|
+
"""
|
|
321
|
+
postfix = to_postfix(tokens, interpreter)
|
|
322
|
+
stack = []
|
|
323
|
+
|
|
324
|
+
for token in postfix:
|
|
325
|
+
|
|
326
|
+
if token.type in {DTT.INT, DTT.FLOAT, DTT.BOOL, DTT.STR, DTT.LIST, DTT.IDENT, DTT.EVAL_GROUP}:
|
|
327
|
+
stack.append(await interpreter.eval_data_token(token))
|
|
328
|
+
|
|
329
|
+
elif token.type in (DTT.MATHS_OPERATOR, DTT.LOGIC_OPERATOR, DTT.LOGIC_WORD_OPERATOR):
|
|
330
|
+
op = token.value
|
|
331
|
+
|
|
332
|
+
if op == "not":
|
|
333
|
+
a = stack.pop()
|
|
334
|
+
result = dshell_operators[op][0](a)
|
|
335
|
+
|
|
336
|
+
else:
|
|
337
|
+
b = stack.pop()
|
|
338
|
+
try:
|
|
339
|
+
a = stack.pop()
|
|
340
|
+
except IndexError:
|
|
341
|
+
if op == "-":
|
|
342
|
+
a = 0
|
|
343
|
+
else:
|
|
344
|
+
raise SyntaxError(f"Invalid expression: {op} operator requires two operands, but only one was found.")
|
|
345
|
+
|
|
346
|
+
result = dshell_operators[op][0](a, b)
|
|
347
|
+
|
|
348
|
+
stack.append(result)
|
|
349
|
+
|
|
350
|
+
else:
|
|
351
|
+
raise SyntaxError(f"Unexpected token type: {token.type} - {token.value}")
|
|
352
|
+
|
|
353
|
+
if len(stack) != 1:
|
|
354
|
+
raise SyntaxError("Invalid expression: stack should contain exactly one element after evaluation.")
|
|
355
|
+
|
|
356
|
+
return stack[0]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async def call_function(function: Callable, args: ArgsCommandNode, interpreter: DshellInterpreteur):
|
|
360
|
+
"""
|
|
361
|
+
Call the function with the given arguments.
|
|
362
|
+
It can be an async function !
|
|
363
|
+
:param function: The function to call.
|
|
364
|
+
:param args: The arguments to pass to the function.
|
|
365
|
+
:param interpreter: The Dshell interpreter instance.
|
|
366
|
+
"""
|
|
367
|
+
reformatted = await regroupe_commandes(args.body, interpreter)
|
|
368
|
+
|
|
369
|
+
args = reformatted.get_non_specified_parameters() # remove non-specified parameters from dict parameters
|
|
370
|
+
kwargs = reformatted.get_dict_parameters()
|
|
371
|
+
kwargs.pop('*', None)
|
|
372
|
+
|
|
373
|
+
args.insert(0, interpreter.ctx) # add the context as first argument
|
|
374
|
+
|
|
375
|
+
return await function(*args, **kwargs)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
async def regroupe_commandes(body: list[Token], interpreter: DshellInterpreteur, normalise: bool = False) -> DshellArguments:
|
|
379
|
+
"""
|
|
380
|
+
Groups the command arguments in the form of a python dictionary.
|
|
381
|
+
Note that you can specify the parameter you wish to pass via -- followed by the parameter name. But this is not mandatory!
|
|
382
|
+
Non-mandatory parameters will be stored in a list in the form of tokens with the key \`*\`.
|
|
383
|
+
The others, having been specified via a separator, will be in the form of a list of tokens with the IDENT token as key, following the separator for each argument.
|
|
384
|
+
If two parameters have the same name, the last one will overwrite the previous one.
|
|
385
|
+
To accept duplicates, use the SUB_SEPARATOR (~~) to create a sub-dictionary for parameters with the same name (sub-dictionary is added to the list returned).
|
|
386
|
+
|
|
387
|
+
:param body: The list of tokens to group.
|
|
388
|
+
:param interpreter: The Dshell interpreter instance.
|
|
389
|
+
:param normalise: If True, normalises the arguments (make value lowercase).
|
|
390
|
+
"""
|
|
391
|
+
# tokens to return
|
|
392
|
+
|
|
393
|
+
instance_dhsell_arguments = DshellArguments()
|
|
394
|
+
index = 0
|
|
395
|
+
n = len(body)
|
|
396
|
+
|
|
397
|
+
while index < n:
|
|
398
|
+
|
|
399
|
+
if normalise and hasattr(body[index], 'value') and isinstance(body[index].value, str):
|
|
400
|
+
body[index].value = body[index].value.lower()
|
|
401
|
+
|
|
402
|
+
# If the current token is the last one and is a parameter marker, add it with empty value
|
|
403
|
+
if index == n - 1 and body[index].type in (DTT.PARAMETER, DTT.STR_PARAMETER, DTT.PARAMETERS):
|
|
404
|
+
if body[index].type == DTT.PARAMETER:
|
|
405
|
+
instance_dhsell_arguments.set_parameter(body[index].value, '', DTT.PARAMETER)
|
|
406
|
+
elif body[index].type == DTT.STR_PARAMETER:
|
|
407
|
+
instance_dhsell_arguments.set_parameter(body[index].value, '', DTT.STR_PARAMETER)
|
|
408
|
+
else: # DTT.PARAMETERS
|
|
409
|
+
instance_dhsell_arguments.set_parameter(body[index].value, ListNode([]), DTT.PARAMETERS)
|
|
410
|
+
index += 1
|
|
411
|
+
continue
|
|
412
|
+
|
|
413
|
+
if body[index].type == DTT.PARAMETER:
|
|
414
|
+
|
|
415
|
+
value = ''
|
|
416
|
+
current_index = index
|
|
417
|
+
while (index + 1) < n and body[index + 1].type not in (DTT.PARAMETER, DTT.STR_PARAMETER, DTT.PARAMETERS):
|
|
418
|
+
|
|
419
|
+
value = await interpreter.eval_data_token(body[index + 1])
|
|
420
|
+
index += 1
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
instance_dhsell_arguments.set_parameter(body[current_index].value, value, DTT.PARAMETER, obligatory=value == '*')
|
|
424
|
+
index += 1
|
|
425
|
+
|
|
426
|
+
elif body[index].type == DTT.STR_PARAMETER:
|
|
427
|
+
|
|
428
|
+
final_argument = ''
|
|
429
|
+
current_index = index
|
|
430
|
+
|
|
431
|
+
while (index + 1) < n and body[index + 1].type not in (DTT.PARAMETER, DTT.STR_PARAMETER, DTT.PARAMETERS):
|
|
432
|
+
|
|
433
|
+
final_argument += body[index + 1].value + ' '
|
|
434
|
+
index += 1
|
|
435
|
+
instance_dhsell_arguments.set_parameter(body[current_index].value, final_argument, type_=DTT.STR_PARAMETER)
|
|
436
|
+
|
|
437
|
+
index += 1
|
|
438
|
+
|
|
439
|
+
elif body[index].type == DTT.PARAMETERS:
|
|
440
|
+
|
|
441
|
+
list_parameters = []
|
|
442
|
+
current_index = index
|
|
443
|
+
while (index + 1) < n and body[index + 1].type not in (DTT.PARAMETER, DTT.STR_PARAMETER, DTT.PARAMETERS):
|
|
444
|
+
|
|
445
|
+
list_parameters.append(await interpreter.eval_data_token(body[index + 1]))
|
|
446
|
+
index += 1
|
|
447
|
+
instance_dhsell_arguments.set_parameter(body[current_index].value, ListNode(list_parameters), type_=DTT.PARAMETERS)
|
|
448
|
+
|
|
449
|
+
index += 1
|
|
450
|
+
|
|
451
|
+
else:
|
|
452
|
+
instance_dhsell_arguments.add_non_specified_parameters(await interpreter.eval_data_token(body[index]))
|
|
453
|
+
index += 1
|
|
454
|
+
|
|
455
|
+
return instance_dhsell_arguments
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
async def build_embed_args(body: list[Token], fields: list[FieldEmbedNode], interpreter: DshellInterpreteur) -> tuple[dict, list[dict]]:
|
|
459
|
+
"""
|
|
460
|
+
Builds the arguments for an embed from the command information.
|
|
461
|
+
"""
|
|
462
|
+
regrouped_parameters = await regroupe_commandes(body, interpreter)
|
|
463
|
+
args_main_embed: dict[str, list[Any]] = regrouped_parameters.get_dict_parameters()
|
|
464
|
+
args_main_embed.pop('*') # remove unspecified parameters for the embed
|
|
465
|
+
args_main_embed: dict[str, Token] # specify what it contains from now on
|
|
466
|
+
|
|
467
|
+
args_fields: list[dict[str, Token]] = []
|
|
468
|
+
for field in fields: # do the same for the fields
|
|
469
|
+
y = await regroupe_commandes(field.body, interpreter)
|
|
470
|
+
args_field = y.get_dict_parameters()
|
|
471
|
+
args_field.pop('*')
|
|
472
|
+
args_field: dict[str, Token]
|
|
473
|
+
args_fields.append(args_field)
|
|
474
|
+
|
|
475
|
+
if 'color' in args_main_embed:
|
|
476
|
+
args_main_embed['color'] = build_colour(args_main_embed['color']) # convert color to Colour object or int
|
|
477
|
+
|
|
478
|
+
return args_main_embed, args_fields
|
|
479
|
+
|
|
480
|
+
async def build_embed(body: list[Token], fields: list[FieldEmbedNode], interpreter: DshellInterpreteur) -> Embed:
|
|
481
|
+
"""
|
|
482
|
+
Builds an embed from the command information.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
args_main_embed, args_fields = await build_embed_args(body, fields, interpreter)
|
|
486
|
+
embed = Embed(**args_main_embed) # build the main embed
|
|
487
|
+
for field in args_fields:
|
|
488
|
+
embed.add_field(**field) # add all fields
|
|
489
|
+
|
|
490
|
+
return embed
|
|
491
|
+
|
|
492
|
+
async def rebuild_embed(embed: Embed, body: list[Token], fields: list[FieldEmbedNode], interpreter: DshellInterpreteur) -> Embed:
|
|
493
|
+
"""
|
|
494
|
+
Rebuilds an embed from an existing embed and the command information.
|
|
495
|
+
"""
|
|
496
|
+
args_main_embed, args_fields = await build_embed_args(body, fields, interpreter)
|
|
497
|
+
|
|
498
|
+
for key, value in args_main_embed.items():
|
|
499
|
+
if key == 'color':
|
|
500
|
+
embed.colour = value
|
|
501
|
+
else:
|
|
502
|
+
setattr(embed, key, value)
|
|
503
|
+
|
|
504
|
+
if args_fields:
|
|
505
|
+
embed.clear_fields()
|
|
506
|
+
for field in args_fields:
|
|
507
|
+
embed.add_field(**field)
|
|
508
|
+
|
|
509
|
+
return embed
|
|
510
|
+
|
|
511
|
+
def build_colour(color: Union[int, ListNode]) -> Union[Colour, int]:
|
|
512
|
+
"""
|
|
513
|
+
Builds a Colour object from an integer or a ListNode.
|
|
514
|
+
:param color: The color to build.
|
|
515
|
+
:return: A Colour object.
|
|
516
|
+
"""
|
|
517
|
+
if isinstance(color, int):
|
|
518
|
+
return color
|
|
519
|
+
elif isinstance(color, (ListNode, list)):
|
|
520
|
+
if not len(color) == 3:
|
|
521
|
+
raise ValueError(f"Color must be a list of 3 integers, not {len(color)} elements !")
|
|
522
|
+
return Colour.from_rgb(*color)
|
|
523
|
+
else:
|
|
524
|
+
raise TypeError(f"Color must be an integer or a ListNode, not {type(color)} !")
|
|
525
|
+
|
|
526
|
+
async def build_ui_parameters(ui_node: UiNode, interpreter: DshellInterpreteur):
|
|
527
|
+
"""
|
|
528
|
+
Builds the parameters for a UI component from the UiNode.
|
|
529
|
+
Can accept buttons and select menus.
|
|
530
|
+
:param ui_node:
|
|
531
|
+
:param interpreter:
|
|
532
|
+
:return:
|
|
533
|
+
"""
|
|
534
|
+
for ident_component in range(len(ui_node.buttons)):
|
|
535
|
+
regrouped_parameters = await regroupe_commandes(ui_node.buttons[ident_component].body, interpreter, normalise=True)
|
|
536
|
+
args_button: dict[str, list[Any]] = regrouped_parameters.get_dict_parameters()
|
|
537
|
+
|
|
538
|
+
code = args_button.pop('code', None)
|
|
539
|
+
style = args_button.pop('style', 'primary').lower()
|
|
540
|
+
custom_id = args_button.pop('custom_id', str(ident_component))
|
|
541
|
+
|
|
542
|
+
if not isinstance(custom_id, str):
|
|
543
|
+
raise TypeError(f"Button custom_id must be a string, not {type(custom_id)} !")
|
|
544
|
+
|
|
545
|
+
if style not in ButtonStyleValues:
|
|
546
|
+
raise ValueError(f"Button style must be one of {', '.join(ButtonStyleValues)}, not '{style}' !")
|
|
547
|
+
|
|
548
|
+
args_button['custom_id'] = custom_id
|
|
549
|
+
args_button['style'] = ButtonStyle[style]
|
|
550
|
+
args = args_button.pop('*', ())
|
|
551
|
+
yield args, args_button, code
|
|
552
|
+
|
|
553
|
+
async def build_ui(ui_node: UiNode, interpreter: DshellInterpreteur) -> EasyModifiedViews:
|
|
554
|
+
"""
|
|
555
|
+
Builds a UI component from the UiNode.
|
|
556
|
+
Can accept buttons and select menus.
|
|
557
|
+
:param ui_node:
|
|
558
|
+
:param interpreter:
|
|
559
|
+
:return:
|
|
560
|
+
"""
|
|
561
|
+
view = EasyModifiedViews()
|
|
562
|
+
|
|
563
|
+
async for args, args_button, code in build_ui_parameters(ui_node, interpreter):
|
|
564
|
+
b = Button(**args_button)
|
|
565
|
+
|
|
566
|
+
view.add_items(b)
|
|
567
|
+
view.set_callable(b.custom_id, _callable=ui_button_callback, data={'code': code})
|
|
568
|
+
|
|
569
|
+
return view
|
|
570
|
+
|
|
571
|
+
async def rebuild_ui(ui_node : UiNode, view: EasyModifiedViews, interpreter: DshellInterpreteur) -> EasyModifiedViews:
|
|
572
|
+
"""
|
|
573
|
+
Rebuilds a UI component from an existing EasyModifiedViews.
|
|
574
|
+
:param view:
|
|
575
|
+
:param interpreter:
|
|
576
|
+
:return:
|
|
577
|
+
"""
|
|
578
|
+
async for args, args_button, code in build_ui_parameters(ui_node, interpreter):
|
|
579
|
+
try:
|
|
580
|
+
ui = view.get_ui(args_button['custom_id'])
|
|
581
|
+
except CustomIDNotFound:
|
|
582
|
+
raise ValueError(f"Button with custom_id '{args_button['custom_id']}' not found in the view !")
|
|
583
|
+
|
|
584
|
+
ui.label = args_button.get('label', ui.label)
|
|
585
|
+
ui.style = args_button.get('style', ui.style)
|
|
586
|
+
ui.emoji = args_button.get('emoji', ui.emoji)
|
|
587
|
+
ui.disabled = args_button.get('disabled', ui.disabled)
|
|
588
|
+
ui.url = args_button.get('url', ui.url)
|
|
589
|
+
ui.row = args_button.get('row', ui.row)
|
|
590
|
+
new_code = code if code is not None else view.get_callable_data(args_button['custom_id'])['code']
|
|
591
|
+
view.set_callable(args_button['custom_id'], _callable=ui_button_callback, data={'code': args_button.get('code', code)})
|
|
592
|
+
|
|
593
|
+
return view
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
async def ui_button_callback(button: Button, interaction: Interaction, data: dict[str, Any]):
|
|
597
|
+
"""
|
|
598
|
+
Callback for UI buttons.
|
|
599
|
+
Executes the code associated with the button.
|
|
600
|
+
:param button:
|
|
601
|
+
:param interaction:
|
|
602
|
+
:param data:
|
|
603
|
+
:return:
|
|
604
|
+
"""
|
|
605
|
+
code = data.pop('code', None)
|
|
606
|
+
if code is not None:
|
|
607
|
+
local_env = {
|
|
608
|
+
'__ret__': None,
|
|
609
|
+
'__guild__': interaction.guild.name if interaction.guild else None,
|
|
610
|
+
'__channel__': interaction.channel.name if interaction.channel else None,
|
|
611
|
+
'__author__': interaction.user.name,
|
|
612
|
+
'__author_display_name__': interaction.user.display_name,
|
|
613
|
+
'__author_avatar__': interaction.user.display_avatar.url if interaction.user.display_avatar else None,
|
|
614
|
+
'__author_discriminator__': interaction.user.discriminator,
|
|
615
|
+
'__author_bot__': interaction.user.bot,
|
|
616
|
+
'__author_nick__': interaction.user.nick if hasattr(interaction.user, 'nick') else None,
|
|
617
|
+
'__author_id__': interaction.user.id,
|
|
618
|
+
'__message__': interaction.message.content if hasattr(interaction.message, 'content') else None,
|
|
619
|
+
'__message_id__': interaction.message.id if hasattr(interaction.message, 'id') else None,
|
|
620
|
+
'__channel_name__': interaction.channel.name if interaction.channel else None,
|
|
621
|
+
'__channel_type__': interaction.channel.type.name if hasattr(interaction.channel, 'type') else None,
|
|
622
|
+
'__channel_id__': interaction.channel.id if interaction.channel else None,
|
|
623
|
+
'__private_channel__': isinstance(interaction.channel, PrivateChannel),
|
|
624
|
+
}
|
|
625
|
+
local_env.update(data)
|
|
626
|
+
x = DshellInterpreteur(code, interaction, debug=False)
|
|
627
|
+
x.env.update(local_env)
|
|
628
|
+
await x.execute()
|
|
629
|
+
else:
|
|
630
|
+
await interaction.response.defer(invisible=True)
|
|
631
|
+
|
|
632
|
+
data.update({'code': code})
|
|
633
|
+
|
|
634
|
+
async def build_permission(body: list[Token], interpreter: DshellInterpreteur) -> dict[
|
|
635
|
+
Union[Member, Role], PermissionOverwrite]:
|
|
636
|
+
"""
|
|
637
|
+
Builds a dictionary of PermissionOverwrite objects from the command information.
|
|
638
|
+
"""
|
|
639
|
+
args_permissions: DshellArguments = await regroupe_commandes(body, interpreter, normalise=True)
|
|
640
|
+
|
|
641
|
+
print(args_permissions)
|
|
642
|
+
|
|
643
|
+
x = args_permissions.get_dict_parameters()
|
|
644
|
+
x.pop('*', None)
|
|
645
|
+
|
|
646
|
+
return DshellPermissions(x).get_permission_overwrite(interpreter.ctx.channel.guild)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class DshellIterator:
|
|
651
|
+
"""
|
|
652
|
+
Used to transform anything into an iterable
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
def __init__(self, data):
|
|
656
|
+
self.data = data if isinstance(data, (str, list, ListNode)) else range(int(data))
|
|
657
|
+
self.current = 0
|
|
658
|
+
|
|
659
|
+
def __iter__(self):
|
|
660
|
+
return self
|
|
661
|
+
|
|
662
|
+
def __next__(self):
|
|
663
|
+
if self.current >= len(self.data):
|
|
664
|
+
self.current = 0
|
|
665
|
+
raise StopIteration
|
|
666
|
+
|
|
667
|
+
value = self.data[self.current]
|
|
668
|
+
self.current += 1
|
|
669
|
+
return value
|
|
670
|
+
|
|
671
|
+
|