From 57a4e641bacab0499d99d4371534f3f7c19e5b30 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sun, 10 Sep 2023 07:14:37 +0300 Subject: [PATCH] feat(meta): Improve bugsplat. --- src/meta/LionBot.py | 37 ++++++++++++++++++++++++++----- src/meta/LionTree.py | 52 ++++++++++++++++++++++++++++++++++++++++++-- src/utils/ui/leo.py | 25 +++++++++++++++++++-- 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/meta/LionBot.py b/src/meta/LionBot.py index 55b30345..d20349de 100644 --- a/src/meta/LionBot.py +++ b/src/meta/LionBot.py @@ -11,6 +11,7 @@ from discord.app_commands.errors import CommandInvokeError as appCommandInvokeEr from aiohttp import ClientSession from data import Database +from utils.lib import tabulate from .config import Conf from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context @@ -192,7 +193,7 @@ class LionBot(Bot): pass except asyncio.TimeoutError: pass - except Exception: + except Exception as e: logger.exception( f"Caught an unknown CommandInvokeError while executing: {cmd_str}", extra={'action': 'BotError', 'with_ctx': True} @@ -201,10 +202,36 @@ class LionBot(Bot): error_embed = discord.Embed(title="Something went wrong!") error_embed.description = ( "An unexpected error occurred while processing your command!\n" - "Our development team has been notified, and the issue should be fixed soon.\n" - "If the error persists, please contact our support team and give them the following number: " - f"`{ctx.interaction.id if ctx.interaction else ctx.message.id}`" - ) + "Our development team has been notified, and the issue will be addressed soon.\n" + "If the error persists, or you have any questions, please contact our [support team]({link}) " + "and give them the extra details below." + ).format(link=self.config.bot.support_guild) + details = {} + details['error'] = f"`{repr(e)}`" + if ctx.interaction: + details['interactionid'] = f"`{ctx.interaction.id}`" + if ctx.command: + details['cmd'] = f"`{ctx.command.qualified_name}`" + if ctx.author: + details['author'] = f"`{ctx.author.id}` -- `{ctx.author}`" + if ctx.guild: + details['guild'] = f"`{ctx.guild.id}` -- `{ctx.guild.name}`" + details['my_guild_perms'] = f"`{ctx.guild.me.guild_permissions.value}`" + if ctx.author: + ownerstr = ' (owner)' if ctx.author == ctx.guild.owner else '' + details['author_guild_perms'] = f"`{ctx.author.guild_permissions.value}{ownerstr}`" + if ctx.channel.type is discord.enums.ChannelType.private: + details['channel'] = "`Direct Message`" + elif ctx.channel: + details['channel'] = f"`{ctx.channel.id}` -- `{ctx.channel.name}`" + details['my_channel_perms'] = f"`{ctx.channel.permissions_for(ctx.guild.me).value}`" + if ctx.author: + details['author_channel_perms'] = f"`{ctx.channel.permissions_for(ctx.author).value}`" + details['shard'] = f"`{self.shardname}`" + details['log_stack'] = f"`{log_action_stack.get()}`" + + table = '\n'.join(tabulate(*details.items())) + error_embed.add_field(name='Details', value=table) try: await ctx.error_reply(embed=error_embed) diff --git a/src/meta/LionTree.py b/src/meta/LionTree.py index 9a2dd7ec..e5f5624b 100644 --- a/src/meta/LionTree.py +++ b/src/meta/LionTree.py @@ -1,12 +1,15 @@ import logging +import discord from discord import Interaction from discord.app_commands import CommandTree from discord.app_commands.errors import AppCommandError, CommandInvokeError from discord.enums import InteractionType from discord.app_commands.namespace import Namespace -from .logger import logging_context, set_logging_context, log_wrap +from utils.lib import tabulate + +from .logger import logging_context, set_logging_context, log_wrap, log_action_stack from .errors import SafeCancellation logger = logging.getLogger(__name__) @@ -17,7 +20,7 @@ class LionTree(CommandTree): super().__init__(*args, **kwargs) self._call_tasks = set() - async def on_error(self, interaction, error) -> None: + async def on_error(self, interaction: discord.Interaction, error) -> None: try: if isinstance(error, CommandInvokeError): raise error.original @@ -28,6 +31,51 @@ class LionTree(CommandTree): pass except Exception: logger.exception(f"Unhandled exception in interaction: {interaction}", extra={'action': 'TreeError'}) + if not interaction.is_expired(): + splat = self.bugsplat(interaction, error) + try: + if interaction.response.is_done(): + await interaction.followup.send(embed=splat, ephemeral=True) + else: + await interaction.response.send_message(embed=splat, ephemeral=True) + except discord.HTTPException: + pass + + def bugsplat(self, interaction, e): + error_embed = discord.Embed(title="Something went wrong!", colour=discord.Colour.red()) + error_embed.description = ( + "An unexpected error occurred during this interaction!\n" + "Our development team has been notified, and the issue will be addressed soon.\n" + "If the error persists, or you have any questions, please contact our [support team]({link}) " + "and give them the extra details below." + ).format(link=interaction.client.config.bot.support_guild) + details = {} + details['error'] = f"`{repr(e)}`" + details['interactionid'] = f"`{interaction.id}`" + details['interactiontype'] = f"`{interaction.type}`" + if interaction.command: + details['cmd'] = f"`{interaction.command.qualified_name}`" + if interaction.user: + details['user'] = f"`{interaction.user.id}` -- `{interaction.user}`" + if interaction.guild: + details['guild'] = f"`{interaction.guild.id}` -- `{interaction.guild.name}`" + details['my_guild_perms'] = f"`{interaction.guild.me.guild_permissions.value}`" + if interaction.user: + ownerstr = ' (owner)' if interaction.user == interaction.guild.owner else '' + details['user_guild_perms'] = f"`{interaction.user.guild_permissions.value}{ownerstr}`" + if interaction.channel.type is discord.enums.ChannelType.private: + details['channel'] = "`Direct Message`" + elif interaction.channel: + details['channel'] = f"`{interaction.channel.id}` -- `{interaction.channel.name}`" + details['my_channel_perms'] = f"`{interaction.channel.permissions_for(interaction.guild.me).value}`" + if interaction.user: + details['user_channel_perms'] = f"`{interaction.channel.permissions_for(interaction.user).value}`" + details['shard'] = f"`{interaction.client.shardname}`" + details['log_stack'] = f"`{log_action_stack.get()}`" + + table = '\n'.join(tabulate(*details.items())) + error_embed.add_field(name='Details', value=table) + return error_embed def _from_interaction(self, interaction: Interaction) -> None: @log_wrap(context=f"iid: {interaction.id}", isolate=False) diff --git a/src/utils/ui/leo.py b/src/utils/ui/leo.py index aba4118c..e2eedd0d 100644 --- a/src/utils/ui/leo.py +++ b/src/utils/ui/leo.py @@ -230,9 +230,20 @@ class LeoUI(View): ) except Exception: logger.exception( - f"Unhandled interaction exception occurred in item {item!r} of LeoUI {self!r}", + f"Unhandled interaction exception occurred in item {item!r} of LeoUI {self!r} from interaction: " + f"{interaction.data}", extra={'with_ctx': True, 'action': 'UIError'} ) + # Explicitly handle the bugsplat ourselves + if not interaction.is_expired(): + splat = interaction.client.tree.bugsplat(interaction, error) + try: + if interaction.response.is_done(): + await interaction.followup.send(embed=splat, ephemeral=True) + else: + await interaction.response.send_message(embed=splat, ephemeral=True) + except discord.HTTPException: + pass class MessageUI(LeoUI): @@ -461,9 +472,19 @@ class LeoModal(Modal): raise error except Exception: logger.exception( - f"Unhandled interaction exception occurred in {self!r}", + f"Unhandled interaction exception occurred in {self!r}. Interaction: {interaction.data}", extra={'with_ctx': True, 'action': 'ModalError'} ) + # Explicitly handle the bugsplat ourselves + if not interaction.is_expired(): + splat = interaction.client.tree.bugsplat(interaction, error) + try: + if interaction.response.is_done(): + await interaction.followup.send(embed=splat, ephemeral=True) + else: + await interaction.response.send_message(embed=splat, ephemeral=True) + except discord.HTTPException: + pass def error_handler_for(exc):