diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f81906..ad49049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Flag emojis to commands with countries. - Image attribution and description to "image" commands. +- Better user-facing command error handling. - New key in options.py: pika. diff --git a/common.py b/common.py index 2822302..bb46f3f 100644 --- a/common.py +++ b/common.py @@ -42,8 +42,12 @@ cat = SimpleNamespace(lookup='Information Lookup', study='Exam Study', weather='Land and Space Weather') -emojis = SimpleNamespace(good='✅', - bad='❌') +emojis = SimpleNamespace(check_mark='✅', + x='❌', + warning='⚠️', + question='❓', + no_entry='⛔', + bangbang='‼️') paths = SimpleNamespace(data=Path("./data/"), resources=Path("./resources/"), @@ -106,7 +110,7 @@ def error_embed_factory(ctx: commands.Context, exception: Exception, debug_mode: else: fmtd_ex = traceback.format_exception_only(exception.__class__, exception) embed = embed_factory(ctx) - embed.title = "Error" + embed.title = "⚠️ Error" embed.description = "```\n" + '\n'.join(fmtd_ex) + "```" embed.colour = colours.bad return embed @@ -124,5 +128,4 @@ async def add_react(msg: discord.Message, react: str): async def check_if_owner(ctx: commands.Context): if ctx.author.id in opt.owners_uids: return True - await add_react(ctx.message, emojis.bad) - return False + raise commands.NotOwner diff --git a/exts/base.py b/exts/base.py index 390c598..57e2713 100644 --- a/exts/base.py +++ b/exts/base.py @@ -106,14 +106,18 @@ class BaseCog(commands.Cog): embed.set_thumbnail(url=str(self.bot.user.avatar_url)) await ctx.send(embed=embed) - @commands.command(name="ping") + @commands.command(name="ping", aliases=['beep']) async def _ping(self, ctx: commands.Context): """Show the current latency to the discord endpoint.""" - content = ctx.message.author.mention if random.random() < 0.05 else '' embed = cmn.embed_factory(ctx) - embed.title = "**Pong!**" + content = '' + if ctx.invoked_with == "beep": + embed.title = "**Boop!**" + else: + content = ctx.message.author.mention if random.random() < 0.05 else '' + embed.title = "🏓 **Pong!**" embed.description = f'Current ping is {self.bot.latency*1000:.1f} ms' - await ctx.send(content=content, embed=embed) + await ctx.send(content, embed=embed) @commands.command(name="changelog", aliases=["clog"]) async def _changelog(self, ctx: commands.Context): @@ -138,6 +142,20 @@ class BaseCog(commands.Cog): await ctx.send(embed=embed) + @commands.command(name="issue") + async def _issue(self, ctx: commands.Context): + """Shows how to create an issue for the bot.""" + embed = cmn.embed_factory(ctx) + embed.title = "Found a bug? Have a feature request?" + embed.description = ("Submit an issue on the [issue tracker]" + "(https://github.com/classabbyamp/discord-qrm2/issues)!") + await ctx.send(embed=embed) + + @commands.command(name="bruce", hidden=True) + async def _b_issue(self, ctx: commands.Context): + """Shows how to create an issue for the bot.""" + await ctx.invoke(self._issue) + @commands.command(name="echo", aliases=["e"], hidden=True) @commands.check(cmn.check_if_owner) async def _echo(self, ctx: commands.Context, channel: commands.TextChannelConverter, *, msg: str): diff --git a/main.py b/main.py index a658e27..c3b4ae4 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,9 @@ This file is part of discord-qrm2 and is released under the terms of the GNU General Public License, version 2. """ + +import sys +import traceback from datetime import time, datetime import random from types import SimpleNamespace @@ -42,6 +45,8 @@ bot = commands.Bot(command_prefix=opt.prefix, bot.qrm = SimpleNamespace() bot.qrm.session = aiohttp.ClientSession(headers={'User-Agent': f'discord-qrm2/{info.release}'}) +bot.qrm.debug_mode = debug_mode + # --- Commands --- @@ -51,7 +56,7 @@ async def _restart_bot(ctx: commands.Context): """Restarts the bot.""" await bot.qrm.session.close() global exit_code - await cmn.add_react(ctx.message, cmn.emojis.good) + await cmn.add_react(ctx.message, cmn.emojis.check_mark) print(f"[**] Restarting! Requested by {ctx.author}.") exit_code = 42 # Signals to the wrapper script that the bot needs to be restarted. await bot.logout() @@ -63,7 +68,7 @@ async def _shutdown_bot(ctx: commands.Context): """Shuts down the bot.""" await bot.qrm.session.close() global exit_code - await cmn.add_react(ctx.message, cmn.emojis.good) + await cmn.add_react(ctx.message, cmn.emojis.check_mark) print(f"[**] Shutting down! Requested by {ctx.author}.") exit_code = 0 # Signals to the wrapper script that the bot should not be restarted. await bot.logout() @@ -92,9 +97,9 @@ async def _extctl_list(ctx: commands.Context): async def _extctl_load(ctx: commands.Context, extension: str): try: bot.load_extension(ext_dir + "." + extension) - await cmn.add_react(ctx.message, cmn.emojis.good) + await cmn.add_react(ctx.message, cmn.emojis.check_mark) except commands.ExtensionError as ex: - embed = cmn.error_embed_factory(ctx, ex, debug_mode) + embed = cmn.error_embed_factory(ctx, ex, bot.qrm.debug_mode) await ctx.send(embed=embed) @@ -106,9 +111,9 @@ async def _extctl_reload(ctx: commands.Context, extension: str): await cmn.add_react(ctx.message, pika) try: bot.reload_extension(ext_dir + "." + extension) - await cmn.add_react(ctx.message, cmn.emojis.good) + await cmn.add_react(ctx.message, cmn.emojis.check_mark) except commands.ExtensionError as ex: - embed = cmn.error_embed_factory(ctx, ex, debug_mode) + embed = cmn.error_embed_factory(ctx, ex, bot.qrm.debug_mode) await ctx.send(embed=embed) @@ -116,9 +121,9 @@ async def _extctl_reload(ctx: commands.Context, extension: str): async def _extctl_unload(ctx: commands.Context, extension: str): try: bot.unload_extension(ext_dir + "." + extension) - await cmn.add_react(ctx.message, cmn.emojis.good) + await cmn.add_react(ctx.message, cmn.emojis.check_mark) except commands.ExtensionError as ex: - embed = cmn.error_embed_factory(ctx, ex, debug_mode) + embed = cmn.error_embed_factory(ctx, ex, bot.qrm.debug_mode) await ctx.send(embed=embed) @@ -146,6 +151,37 @@ async def on_message(message): await bot.process_commands(message) +@bot.event +async def on_command_error(ctx: commands.Context, err: commands.CommandError): + if isinstance(err, commands.UserInputError): + await cmn.add_react(ctx.message, cmn.emojis.warning) + await ctx.send_help(ctx.command) + elif isinstance(err, commands.CommandNotFound) and not ctx.invoked_with.startswith("?"): + await cmn.add_react(ctx.message, cmn.emojis.question) + elif isinstance(err, commands.CheckFailure): + # Add handling of other subclasses of CheckFailure as needed. + if isinstance(err, commands.NotOwner): + await cmn.add_react(ctx.message, cmn.emojis.no_entry) + else: + await cmn.add_react(ctx.message, cmn.emojis.x) + elif isinstance(err, commands.DisabledCommand): + await cmn.add_react(ctx.message, cmn.emojis.bangbang) + elif isinstance(err, (commands.CommandInvokeError, commands.ConversionError)): + # Emulating discord.py's default beaviour. + print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr) + traceback.print_exception(type(err), err, err.__traceback__, file=sys.stderr) + + embed = cmn.error_embed_factory(ctx, err.original, bot.qrm.debug_mode) + embed.description += f"\n`{type(err).__name__}`" + await cmn.add_react(ctx.message, cmn.emojis.warning) + await ctx.send(embed=embed) + else: + # Emulating discord.py's default beaviour. (safest bet) + print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr) + traceback.print_exception(type(err), err, err.__traceback__, file=sys.stderr) + await cmn.add_react(ctx.message, cmn.emojis.warning) + + # --- Tasks --- @tasks.loop(minutes=5) @@ -194,19 +230,19 @@ try: except discord.LoginFailure as ex: # Miscellaneous authentications errors: borked token and co - if debug_mode: + if bot.qrm.debug_mode: raise raise SystemExit("Error: Failed to authenticate: {}".format(ex)) except discord.ConnectionClosed as ex: # When the connection to the gateway (websocket) is closed - if debug_mode: + if bot.qrm.debug_mode: raise raise SystemExit("Error: Discord gateway connection closed: [Code {}] {}".format(ex.code, ex.reason)) except ConnectionResetError as ex: # More generic connection reset error - if debug_mode: + if bot.qrm.debug_mode: raise raise SystemExit("ConnectionResetError: {}".format(ex))