diff --git a/didier/cogs/discord.py b/didier/cogs/discord.py index a73ead3..db9ae7d 100644 --- a/didier/cogs/discord.py +++ b/didier/cogs/discord.py @@ -49,6 +49,12 @@ class Discord(commands.Cog): await birthdays.add_birthday(session, ctx.author.id, date) await self.client.confirm_message(ctx.message) + @commands.command(name="Join", usage="[Thread]") + async def join(self, ctx: commands.Context, thread: discord.Thread): + """Make Didier join a thread""" + if thread.me is not None: + return await ctx.reply() + async def setup(client: Didier): """Load the cog""" diff --git a/didier/cogs/owner.py b/didier/cogs/owner.py index c251b73..30090df 100644 --- a/didier/cogs/owner.py +++ b/didier/cogs/owner.py @@ -43,7 +43,7 @@ class Owner(commands.Cog): return await self.client.is_owner(ctx.author) @commands.command(name="Error", aliases=["Raise"]) - async def _error(self, ctx: commands.Context, message: str = "Debug"): + async def _error(self, ctx: commands.Context, *, message: str = "Debug"): """Raise an exception for debugging purposes""" raise Exception(message) diff --git a/didier/data/embeds/error_embed.py b/didier/data/embeds/error_embed.py new file mode 100644 index 0000000..0dc1b80 --- /dev/null +++ b/didier/data/embeds/error_embed.py @@ -0,0 +1,47 @@ +import traceback + +import discord +from discord.ext import commands + +from didier.utils.discord.constants import Limits +from didier.utils.types.string import abbreviate + +__all__ = ["create_error_embed"] + + +def _get_traceback(exception: Exception) -> str: + """Get a proper representation of the exception""" + tb = traceback.format_exception(type(exception), exception, exception.__traceback__) + error_string = "" + for line in tb: + # Don't add endless tracebacks + if line.strip().startswith("The above exception was the direct cause of"): + break + + # Escape Discord markdown formatting + error_string += line.replace(r"*", r"\*").replace(r"_", r"\_") + if line.strip(): + error_string += "\n" + + return abbreviate(error_string, Limits.EMBED_FIELD_VALUE_LENGTH) + + +def create_error_embed(ctx: commands.Context, exception: Exception) -> discord.Embed: + """Create an embed for the traceback of an exception""" + description = _get_traceback(exception) + + if ctx.guild is None: + origin = "DM" + else: + origin = f"{ctx.channel.mention} ({ctx.guild.name})" + + invocation = f"{ctx.author.display_name} in {origin}" + + embed = discord.Embed(colour=discord.Colour.red()) + embed.set_author(name="Error") + embed.add_field(name="Command", value=f"{ctx.message.content}", inline=True) + embed.add_field(name="Context", value=invocation, inline=True) + embed.add_field(name="Exception", value=abbreviate(str(exception), Limits.EMBED_FIELD_VALUE_LENGTH), inline=False) + embed.add_field(name="Traceback", value=description, inline=False) + + return embed diff --git a/didier/didier.py b/didier/didier.py index 2f07372..d5745c2 100644 --- a/didier/didier.py +++ b/didier/didier.py @@ -10,6 +10,7 @@ import settings from database.crud import custom_commands from database.engine import DBSession from database.utils.caches import CacheManager +from didier.data.embeds.error_embed import create_error_embed from didier.utils.discord.prefix import get_prefix __all__ = ["Didier"] @@ -139,6 +140,9 @@ class Didier(commands.Bot): await self.process_commands(message) + # TODO easter eggs + # TODO stats + async def _try_invoke_custom_command(self, message: discord.Message) -> bool: """Check if the message tries to invoke a custom command @@ -162,11 +166,50 @@ class Didier(commands.Bot): # Nothing found return False - async def on_command_error(self, context: commands.Context, exception: commands.CommandError, /) -> None: - """Event triggered when a regular command errors""" - # Print everything to the logs/stderr - await super().on_command_error(context, exception) + async def on_thread_create(self, thread: discord.Thread): + """Event triggered when a new thread is created""" + await thread.join() - # If developing, do nothing special + async def on_command_error(self, ctx: commands.Context, exception: commands.CommandError, /): + """Event triggered when a regular command errors""" + # If working locally, print everything to your console if settings.SANDBOX: + await super().on_command_error(ctx, exception) return + + # If commands have their own error handler, let it handle the error instead + if hasattr(ctx.command, "on_error"): + return + + # Ignore exceptions that aren't important + if isinstance( + exception, + ( + commands.CommandNotFound, + commands.CheckFailure, + commands.TooManyArguments, + ), + ): + return + + # Print everything that we care about to the logs/stderr + await super().on_command_error(ctx, exception) + + if isinstance(exception, commands.MessageNotFound): + return await ctx.reply("This message could not be found.", ephemeral=True, delete_after=10) + + if isinstance( + exception, + ( + commands.BadArgument, + commands.MissingRequiredArgument, + commands.UnexpectedQuoteError, + commands.ExpectedClosingQuoteError, + ), + ): + return await ctx.reply("Invalid arguments.", ephemeral=True, delete_after=10) + + if settings.ERRORS_CHANNEL is not None: + embed = create_error_embed(ctx, exception) + channel = self.get_channel(settings.ERRORS_CHANNEL) + await channel.send(embed=embed) diff --git a/readme.md b/readme.md index dae8347..1060271 100644 --- a/readme.md +++ b/readme.md @@ -61,3 +61,5 @@ black flake8 mypy ``` + +It's also convenient to have code-formatting happen automatically on-save. The [`Black documentation`](https://black.readthedocs.io/en/stable/integrations/editors.html) explains how to set this up for different types of editors.