1919from discord .ext import commands
2020from django .db import connection
2121
22- from ballsdex .core .utils .formatting import pagify
22+ from ballsdex .core .discord import LayoutView
23+ from ballsdex .core .utils .menus import Menu , TextFormatter , TextSource
2324from bd_models import models
2425from bd_models .enums import DonationPolicy , FriendPolicy , MentionPolicy , PrivacyPolicy
2526from bd_models .models import (
5253https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py
5354"""
5455
56+ ANSI_RED = "\x1b [31m"
57+ ANSI_BOLD = "\x1b [1m"
58+ ANSI_RESET = "\x1b [0m"
59+
5560
5661def format_duration (time_taken : float ) -> str :
5762 return f"{ round (time_taken * 1000 )} ms" if time_taken < 1 else f"{ round (time_taken , 3 )} s"
@@ -68,6 +73,28 @@ def text_to_file(
6873 return discord .File (file , filename , spoiler = spoiler )
6974
7075
76+ def format_exception (text : str ) -> str :
77+ """
78+ Formats the last line of an exception.
79+
80+ Parameters
81+ ----------
82+ text: str
83+ The text you want to format.
84+
85+ Returns
86+ -------
87+ str
88+ The formatted text.
89+ """
90+ lines = text .splitlines ()
91+
92+ if lines :
93+ lines [- 1 ] = f"{ ANSI_RED } { ANSI_BOLD } { lines [- 1 ]} { ANSI_RESET } "
94+
95+ return "\n " .join (lines )
96+
97+
7198async def send_interactive (
7299 ctx : commands .Context ["BallsDexBot" ],
73100 messages : Iterable [str ],
@@ -141,7 +168,7 @@ def predicate(m: discord.Message):
141168 prompt_text .format (count = n_remaining , command_1 = "`more`" , command_2 = "`file`" )
142169 )
143170 try :
144- resp = await ctx .bot .wait_for ("message" , check = predicate , timeout = 15 )
171+ resp = await ctx .bot .wait_for ("message" , check = predicate , timeout = timeout )
145172 except asyncio .TimeoutError :
146173 with contextlib .suppress (discord .HTTPException ):
147174 await query .delete ()
@@ -161,6 +188,72 @@ def predicate(m: discord.Message):
161188 return ret
162189
163190
191+ class FileRow (discord .ui .ActionRow ):
192+ """
193+ File row displayed in eval responses that uploads the specified content as a file.
194+
195+ Parameters
196+ ----------
197+ content: str
198+ The content that will be exported as a file.
199+ """
200+
201+ def __init__ (self , content : str ):
202+ super ().__init__ ()
203+ self .content = content
204+
205+ @discord .ui .button (label = "File" )
206+ async def file_button (self , interaction : discord .Interaction ["BallsDexBot" ], _ ):
207+ await interaction .response .send_message (file = text_to_file (self .content ))
208+
209+
210+ async def build_eval_response (
211+ ctx : commands .Context ["BallsDexBot" ], content : str , * , time_taken : float , error : bool = False
212+ ) -> LayoutView | None :
213+ """
214+ Creates a view with a paginator menu and a button to send the specified content as a file.
215+
216+ Parameters
217+ ----------
218+ content: str
219+ The content that will be displayed.
220+ time_taken: float
221+ The amount of time taken in milliseconds for the code to execute.
222+ error: bool
223+ Whether ansi formatting will be enabled.
224+
225+ Returns
226+ -------
227+ LayoutView | None
228+ The created view.
229+ """
230+ if content == "" :
231+ return
232+
233+ code_prefix = "ansi" if error else "py"
234+ formatted_content = format_exception (content ) if error else content
235+
236+ text_display = discord .ui .TextDisplay ("" )
237+
238+ view = LayoutView ()
239+ view .add_item (text_display )
240+ view .add_item (discord .ui .TextDisplay (f"-# Took { format_duration (time_taken )} " ))
241+ view .restrict_author (ctx .author .id )
242+
243+ menu = Menu (
244+ ctx .bot ,
245+ view ,
246+ TextSource (formatted_content , prefix = f"```{ code_prefix } \n " , suffix = "\n ```" ),
247+ TextFormatter (text_display ),
248+ )
249+ await menu .init ()
250+
251+ if menu .source .get_max_pages () > 1 :
252+ menu .view .add_item (FileRow (content ))
253+
254+ return view
255+
256+
164257START_CODE_BLOCK_RE = re .compile (r"^((```(py(thon)?|sql))(?=\s)|(```))" )
165258
166259
@@ -197,19 +290,15 @@ def cleanup_code(content):
197290 return content .strip ("` \n " )
198291
199292 @classmethod
200- def get_syntax_error (cls , e ):
293+ def get_syntax_error (cls , e ) -> str :
201294 """Format a syntax error to send to the user.
202295
203296 Returns a string representation of the error formatted as a codeblock.
204297 """
205298 if e .text is None :
206- return cls .get_pages ("{0.__class__.__name__}: {0}" .format (e ))
207- return cls .get_pages ("{0.text}\n {1:>{0.offset}}\n {2}: {0}" .format (e , "^" , type (e ).__name__ ))
299+ return "{0.__class__.__name__}: {0}" .format (e )
208300
209- @staticmethod
210- def get_pages (msg : str ):
211- """Pagify the given message for output to the user."""
212- return pagify (msg , delims = ["\n " , " " ], priority = True , shorten_by = 25 )
301+ return "{0.text}\n {1:>{0.offset}}\n {2}: {0}" .format (e , "^" , type (e ).__name__ )
213302
214303 @staticmethod
215304 def sanitize_output (ctx : commands .Context , input_ : str ) -> str :
@@ -292,19 +381,35 @@ async def debug(self, ctx: commands.Context, *, code):
292381 result = await self .maybe_await (eval (compiled , env ))
293382 except SyntaxError as e :
294383 t2 = time .time ()
295- await send_interactive (ctx , self .get_syntax_error (e ), time_taken = t2 - t1 )
384+
385+ view = await build_eval_response (ctx , self .get_syntax_error (e ), time_taken = t2 - t1 , error = True )
386+
387+ if view :
388+ await ctx .send (view = view )
389+
296390 return
297391 except Exception as e :
298392 t2 = time .time ()
299- await send_interactive (ctx , self .get_pages ("{}: {!s}" .format (type (e ).__name__ , e )), time_taken = t2 - t1 )
393+
394+ view = await build_eval_response (
395+ ctx , "{}: {!s}" .format (type (e ).__name__ , e ), time_taken = t2 - t1 , error = True
396+ )
397+
398+ if view :
399+ await ctx .send (view = view )
400+
300401 return
301402 t2 = time .time ()
302403
303404 self ._last_result = result
304405 result = self .sanitize_output (ctx , str (result ))
305406
306407 await ctx .message .add_reaction ("✅" )
307- await send_interactive (ctx , self .get_pages (result ), time_taken = t2 - t1 )
408+
409+ view = await build_eval_response (ctx , result , time_taken = t2 - t1 )
410+
411+ if view :
412+ await ctx .send (view = view )
308413
309414 @commands .command (name = "eval" )
310415 @commands .is_owner ()
@@ -340,20 +445,35 @@ async def _eval(self, ctx: commands.Context, *, body: str):
340445 exec (compiled , env )
341446 except SyntaxError as e :
342447 t2 = time .time ()
343- return await send_interactive (ctx , self .get_syntax_error (e ), time_taken = t2 - t1 )
448+
449+ view = await build_eval_response (ctx , self .get_syntax_error (e ), time_taken = t2 - t1 , error = True )
450+
451+ if view :
452+ await ctx .send (view = view )
453+
454+ return
344455 except Exception as e :
345456 t2 = time .time ()
346- await send_interactive (ctx , self .get_pages ("{}: {!s}" .format (type (e ).__name__ , e )), time_taken = t2 - t1 )
457+
458+ view = await build_eval_response (
459+ ctx , "{}: {!s}" .format (type (e ).__name__ , e ), time_taken = t2 - t1 , error = True
460+ )
461+
462+ if view :
463+ await ctx .send (view = view )
464+
347465 return
348466
349467 func = env ["func" ]
350468 result = None
469+ errored = False
351470 try :
352471 with redirect_stdout (stdout ):
353472 result = await func ()
354473 except Exception :
355474 t2 = time .time ()
356475 printed = "{}{}" .format (stdout .getvalue (), traceback .format_exc ())
476+ errored = True
357477 else :
358478 t2 = time .time ()
359479 printed = stdout .getvalue ()
@@ -366,7 +486,10 @@ async def _eval(self, ctx: commands.Context, *, body: str):
366486 msg = printed
367487 msg = self .sanitize_output (ctx , msg )
368488
369- await send_interactive (ctx , self .get_pages (msg ), time_taken = t2 - t1 )
489+ view = await build_eval_response (ctx , msg , time_taken = t2 - t1 , error = errored )
490+
491+ if view :
492+ await ctx .send (view = view )
370493
371494 @commands .command ()
372495 @commands .is_owner ()
0 commit comments