Repository: Cog-Creators/Red-DiscordBot

Issue:
Make `traceback` prompt if you want it to print all pages, if there's more than say 5
### What component of Red (cog, command, API) would you like to see improvements on?

^traceback

### Describe the enhancement you're suggesting.

Occasionally an error will have *egregiously* long text, and using `^traceback` currently is not gated in the number of messages it sends. Adding a confirmation if you want it to print, in the case that an error message is *truly* gigantic would be nice. (Recently I restarted my bot because it was printing the entire source code of a wiki page that had failed to save; I don't even know how many pages it would have been, but it just would not stop.)

### Anything else?

As a bonus, the page count could be configurable, but really I'm just looking for something covering truly outlandish edge cases here, so a quick fix hard-coding even like 15 or 20 would be fine by me.


Discussion:
To solve this issue, I would suggest using `send_interactive` rather than `send` in the `[p]traceback` command.

Resolving patch:
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -52,9 +52,11 @@
     DisabledCogCache,
     I18nManager,
 )
+from .utils.predicates import MessagePredicate
 from .rpc import RPCMixin
 from .tree import RedTree
 from .utils import can_user_send_messages_in, common_filters, AsyncIter
+from .utils.chat_formatting import box, text_to_file
 from .utils._internal_utils import send_to_owners_with_prefix_replaced
 
 if TYPE_CHECKING:
@@ -81,6 +83,8 @@
 T_BIC = TypeVar("T_BIC", bound=PreInvokeCoroutine)
 UserOrRole = Union[int, discord.Role, discord.Member, discord.User]
 
+_ = i18n.Translator("Core", __file__)
+
 
 def _is_submodule(parent, child):
     return parent == child or child.startswith(parent + ".")
@@ -2289,3 +2293,102 @@ async def wrapper(func, stype, sname):
             failed_cogs=failures["cog"],
             unhandled=failures["unhandled"],
         )
+
+    async def send_interactive(
+        self,
+        channel: discord.abc.Messageable,
+        messages: Iterable[str],
+        *,
+        user: Optional[discord.User] = None,
+        box_lang: Optional[str] = None,
+        timeout: int = 15,
+        join_character: str = "",
+    ) -> List[discord.Message]:
+        """
+        Send multiple messages interactively.
+
+        The user will be prompted for whether or not they would like to view
+        the next message, one at a time. They will also be notified of how
+        many messages are remaining on each prompt.
+
+        Parameters
+        ----------
+        channel : discord.abc.Messageable
+            The channel to send the messages to.
+        messages : `iterable` of `str`
+            The messages to send.
+        user : discord.User
+            The user that can respond to the prompt.
+            When this is ``None``, any user can respond.
+        box_lang : Optional[str]
+            If specified, each message will be contained within a code block of
+            this language.
+        timeout : int
+            How long the user has to respond to the prompt before it times out.
+            After timing out, the bot deletes its prompt message.
+        join_character : str
+            The character used to join all the messages when the file output
+            is selected.
+
+        Returns
+        -------
+        List[discord.Message]
+            A list of sent messages.
+        """
+        messages = tuple(messages)
+        ret = []
+        # using dpy_commands.Context to keep the Messageable contract in full
+        if isinstance(channel, dpy_commands.Context):
+            # this is only necessary to ensure that `channel.delete_messages()` works
+            # when `ctx.channel` has that method
+            channel = channel.channel
+
+        for idx, page in enumerate(messages, 1):
+            if box_lang is None:
+                msg = await channel.send(page)
+            else:
+                msg = await channel.send(box(page, lang=box_lang))
+            ret.append(msg)
+            n_remaining = len(messages) - idx
+            if n_remaining > 0:
+                if n_remaining == 1:
+                    prompt_text = _(
+                        "There is still one message remaining. Type {command_1} to continue"
+                        " or {command_2} to upload all contents as a file."
+                    )
+                else:
+                    prompt_text = _(
+                        "There are still {count} messages remaining. Type {command_1} to continue"
+                        " or {command_2} to upload all contents as a file."
+                    )
+                query = await channel.send(
+                    prompt_text.format(count=n_remaining, command_1="`more`", command_2="`file`")
+                )
+                pred = MessagePredicate.lower_contained_in(
+                    ("more", "file"), channel=channel, user=user
+                )
+                try:
+                    resp = await self.wait_for(
+                        "message",
+                        check=pred,
+                        timeout=timeout,
+                    )
+                except asyncio.TimeoutError:
+                    with contextlib.suppress(discord.HTTPException):
+                        await query.delete()
+                    break
+                else:
+                    try:
+                        await channel.delete_messages((query, resp))
+                    except (discord.HTTPException, AttributeError):
+                        # In case the bot can't delete other users' messages,
+                        # or is not a bot account
+                        # or channel is a DM
+                        with contextlib.suppress(discord.HTTPException):
+                            await query.delete()
+                    if pred.result == 1:
+                        ret.append(
+                            await channel.send(file=text_to_file(join_character.join(messages)))
+                        )
+                        break
+        return ret
diff --git a/redbot/core/commands/context.py b/redbot/core/commands/context.py
--- a/redbot/core/commands/context.py
+++ b/redbot/core/commands/context.py
@@ -9,8 +9,6 @@
 from discord.ext.commands import Context as DPYContext
 
 from .requires import PermState
-from ..utils.chat_formatting import box, text_to_file
-from ..utils.predicates import MessagePredicate
 from ..utils import can_user_react_in
 
 if TYPE_CHECKING:
@@ -152,11 +150,12 @@ async def react_quietly(
     async def send_interactive(
         self,
         messages: Iterable[str],
-        box_lang: str = None,
+        box_lang: Optional[str] = None,
         timeout: int = 15,
         join_character: str = "",
     ) -> List[discord.Message]:
-        """Send multiple messages interactively.
+        """
+        Send multiple messages interactively.
 
         The user will be prompted for whether or not they would like to view
         the next message, one at a time. They will also be notified of how
@@ -167,7 +166,7 @@ async def send_interactive(
         messages : `iterable` of `str`
             The messages to send.
         box_lang : str
-            If specified, each message will be contained within a codeblock of
+            If specified, each message will be contained within a code block of
             this language.
         timeout : int
             How long the user has to respond to the prompt before it times out.
@@ -176,53 +175,19 @@ async def send_interactive(
             The character used to join all the messages when the file output
             is selected.
 
+        Returns
+        -------
+        List[discord.Message]
+            A list of sent messages.
         """
-        messages = tuple(messages)
-        ret = []
-
-        for idx, page in enumerate(messages, 1):
-            if box_lang is None:
-                msg = await self.send(page)
-            else:
-                msg = await self.send(box(page, lang=box_lang))
-            ret.append(msg)
-            n_remaining = len(messages) - idx
-            if n_remaining > 0:
-                if n_remaining == 1:
-                    plural = ""
-                    is_are = "is"
-                else:
-                    plural = "s"
-                    is_are = "are"
-                query = await self.send(
-                    "There {} still {} message{} remaining. "
-                    "Type `more` to continue or `file` to upload all contents as a file."
-                    "".format(is_are, n_remaining, plural)
-                )
-                pred = MessagePredicate.lower_contained_in(("more", "file"), self)
-                try:
-                    resp = await self.bot.wait_for(
-                        "message",
-                        check=pred,
-                        timeout=timeout,
-                    )
-                except asyncio.TimeoutError:
-                    with contextlib.suppress(discord.HTTPException):
-                        await query.delete()
-                    break
-                else:
-                    try:
-                        await self.channel.delete_messages((query, resp))
-                    except (discord.HTTPException, AttributeError):
-                        # In case the bot can't delete other users' messages,
-                        # or is not a bot account
-                        # or channel is a DM
-                        with contextlib.suppress(discord.HTTPException):
-                            await query.delete()
-                    if pred.result == 1:
-                        await self.send(file=text_to_file(join_character.join(messages)))
-                        break
-        return ret
+        return await self.bot.send_interactive(
+            channel=self.channel,
+            messages=messages,
+            user=self.author,
+            box_lang=box_lang,
+            timeout=timeout,
+            join_character=join_character,
+        )
 
     async def embed_colour(self):
         """
diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py
--- a/redbot/core/core_commands.py
+++ b/redbot/core/core_commands.py
@@ -1446,21 +1446,22 @@ async def traceback(self, ctx: commands.Context, public: bool = False):
         **Arguments:**
             - `[public]` - Whether to send the traceback to the current context. Leave blank to send to your DMs.
         """
-        if not public:
-            destination = ctx.author
-        else:
-            destination = ctx.channel
+        channel = ctx.channel if public else ctx.author
 
         if self.bot._last_exception:
-            for page in pagify(self.bot._last_exception, shorten_by=10):
-                try:
-                    await destination.send(box(page, lang="py"))
-                except discord.HTTPException:
-                    await ctx.channel.send(
-                        "I couldn't send the traceback message to you in DM. "
-                        "Either you blocked me or you disabled DMs in this server."
-                    )
-                    return
+            try:
+                await self.bot.send_interactive(
+                    channel,
+                    pagify(self.bot._last_exception, shorten_by=10),
+                    user=ctx.author,
+                    box_lang="py",
+                )
+            except discord.HTTPException:
+                await ctx.channel.send(
+                    "I couldn't send the traceback message to you in DM. "
+                    "Either you blocked me or you disabled DMs in this server."
+                )
+                return
             if not public:
                 await ctx.tick()
         else:


Test patch: