Add Traceback prompt (#5851)

Co-authored-by: Jakub Kuczys <me@jacken.men>
This commit is contained in:
yuansheng1549 2023-04-15 00:50:39 +08:00 committed by GitHub
parent 896d5e9200
commit 030607fb04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 133 additions and 64 deletions

View File

@ -52,9 +52,11 @@ from .settings_caches import (
DisabledCogCache, DisabledCogCache,
I18nManager, I18nManager,
) )
from .utils.predicates import MessagePredicate
from .rpc import RPCMixin from .rpc import RPCMixin
from .tree import RedTree from .tree import RedTree
from .utils import can_user_send_messages_in, common_filters, AsyncIter 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 from .utils._internal_utils import send_to_owners_with_prefix_replaced
if TYPE_CHECKING: if TYPE_CHECKING:
@ -81,6 +83,8 @@ PreInvokeCoroutine = Callable[[commands.Context], Awaitable[Any]]
T_BIC = TypeVar("T_BIC", bound=PreInvokeCoroutine) T_BIC = TypeVar("T_BIC", bound=PreInvokeCoroutine)
UserOrRole = Union[int, discord.Role, discord.Member, discord.User] UserOrRole = Union[int, discord.Role, discord.Member, discord.User]
_ = i18n.Translator("Core", __file__)
def _is_submodule(parent, child): def _is_submodule(parent, child):
return parent == child or child.startswith(parent + ".") return parent == child or child.startswith(parent + ".")
@ -2289,3 +2293,102 @@ class Red(
failed_cogs=failures["cog"], failed_cogs=failures["cog"],
unhandled=failures["unhandled"], 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

View File

@ -9,8 +9,6 @@ import discord
from discord.ext.commands import Context as DPYContext from discord.ext.commands import Context as DPYContext
from .requires import PermState 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 from ..utils import can_user_react_in
if TYPE_CHECKING: if TYPE_CHECKING:
@ -152,11 +150,12 @@ class Context(DPYContext):
async def send_interactive( async def send_interactive(
self, self,
messages: Iterable[str], messages: Iterable[str],
box_lang: str = None, box_lang: Optional[str] = None,
timeout: int = 15, timeout: int = 15,
join_character: str = "", join_character: str = "",
) -> List[discord.Message]: ) -> 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 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 the next message, one at a time. They will also be notified of how
@ -176,53 +175,19 @@ class Context(DPYContext):
The character used to join all the messages when the file output The character used to join all the messages when the file output
is selected. is selected.
Returns
-------
List[discord.Message]
A list of sent messages.
""" """
messages = tuple(messages) return await self.bot.send_interactive(
ret = [] channel=self.channel,
messages=messages,
for idx, page in enumerate(messages, 1): user=self.author,
if box_lang is None: box_lang=box_lang,
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, timeout=timeout,
join_character=join_character,
) )
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
async def embed_colour(self): async def embed_colour(self):
""" """

View File

@ -1446,15 +1446,16 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
**Arguments:** **Arguments:**
- `[public]` - Whether to send the traceback to the current context. Leave blank to send to your DMs. - `[public]` - Whether to send the traceback to the current context. Leave blank to send to your DMs.
""" """
if not public: channel = ctx.channel if public else ctx.author
destination = ctx.author
else:
destination = ctx.channel
if self.bot._last_exception: if self.bot._last_exception:
for page in pagify(self.bot._last_exception, shorten_by=10):
try: try:
await destination.send(box(page, lang="py")) await self.bot.send_interactive(
channel,
pagify(self.bot._last_exception, shorten_by=10),
user=ctx.author,
box_lang="py",
)
except discord.HTTPException: except discord.HTTPException:
await ctx.channel.send( await ctx.channel.send(
"I couldn't send the traceback message to you in DM. " "I couldn't send the traceback message to you in DM. "