From 62b7def76b783bf65074819806f422a523ab43d8 Mon Sep 17 00:00:00 2001 From: SylteA Date: Mon, 17 Jul 2023 17:04:27 +0200 Subject: [PATCH 1/2] Defer the response and send replies as followups in case we take too long to respond. --- bot/extensions/challenges/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/extensions/challenges/commands.py b/bot/extensions/challenges/commands.py index d8b16e5b..af170bd7 100644 --- a/bot/extensions/challenges/commands.py +++ b/bot/extensions/challenges/commands.py @@ -213,6 +213,8 @@ async def submit(self, interaction: core.InteractionType, attachment: discord.At "File extension must be between 1 and 4 characters.", ephemeral=True ) + await interaction.response.defer(ephemeral=True, thinking=True) + code = (await attachment.read()).decode("u8") log.info(len(code) == attachment.size) @@ -222,7 +224,7 @@ async def submit(self, interaction: core.InteractionType, attachment: discord.At max_user_length = 4096 - len(filetype) - 7 msg = f"Your submission can't be __more than {max_user_length} characters__." - return await interaction.response.send_message(msg, ephemeral=True) + return await interaction.followup.send(msg, ephemeral=True) await interaction.user.add_roles(self.submitted_role) @@ -233,4 +235,4 @@ async def submit(self, interaction: core.InteractionType, attachment: discord.At embed.set_author(name="Your submission", icon_url=interaction.user.display_avatar.url) embed.set_footer(text=f"{len(code)} chars • Language: {filetype}") - return await interaction.response.send_message(embed=embed, ephemeral=True) + return await interaction.followup.send(embed=embed, ephemeral=True) From 81bbe4d2c3d18a3992bed4c6ce830b6dc14bb156 Mon Sep 17 00:00:00 2001 From: SylteA Date: Mon, 17 Jul 2023 18:07:53 +0200 Subject: [PATCH 2/2] Add some basic error handling --- bot/config.py | 10 ++++++++++ bot/core.py | 30 +++++++++++++++++++++++++++--- bot/services/paste.py | 21 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 bot/services/paste.py diff --git a/bot/config.py b/bot/config.py index 615b958b..f06a98e8 100644 --- a/bot/config.py +++ b/bot/config.py @@ -89,6 +89,14 @@ class Timathon(BaseModel): participant_role_id: int +class Hastebin(BaseModel): + base_url: str + + +class ErrorHandling(BaseModel): + webhook_url: str + + class Settings(BaseSettings): aoc: AoC bot: Bot @@ -101,6 +109,8 @@ class Settings(BaseSettings): reaction_roles: ReactionRoles tags: Tags timathon: Timathon + hastebin: Hastebin + errors: ErrorHandling class Config: env_file = ".env" diff --git a/bot/core.py b/bot/core.py index 537e3cd9..d18339b0 100644 --- a/bot/core.py +++ b/bot/core.py @@ -1,12 +1,14 @@ import datetime import logging import os +import traceback import discord from discord import app_commands from discord.ext import commands, tasks from bot.config import settings +from bot.services import http, paste from utils.time import human_timedelta log = logging.getLogger(__name__) @@ -23,6 +25,8 @@ def __init__(self, prefixes: tuple[str, ...], extensions: tuple[str, ...], inten self.start_time = datetime.datetime.utcnow() self.initial_extensions = extensions + self.error_webhook: discord.Webhook | None = None + async def resolve_user(self, user_id: int) -> discord.User | None: """Resolve a user from their ID.""" user = self.get_user(user_id) @@ -45,6 +49,8 @@ async def setup_hook(self) -> None: self.tree.on_error = self.on_app_command_error + self.error_webhook = discord.Webhook.from_url(url=settings.errors.webhook_url, session=http.session) + async def when_online(self): log.info("Waiting until bot is ready to load extensions and app commands.") await self.wait_until_ready() @@ -92,15 +98,33 @@ async def on_app_command_error(self, interaction: "InteractionType", error: app_ if interaction.command is None: return log.error("Ignoring exception in command tree.", exc_info=error) - if interaction.command._has_any_error_handlers(): - return - if isinstance(error, app_commands.CheckFailure): log.info(f"{interaction.user} failed to use the command {interaction.command.qualified_name}") return + await self.publish_error(interaction=interaction, error=error) log.error("Ignoring unhandled exception", exc_info=error) + async def publish_error(self, interaction: "InteractionType", error: app_commands.AppCommandError) -> None: + """Publishes the error to our error webhook.""" + content = "\n".join(traceback.format_exception(type(error), error, error.__traceback__)) + header = f"Ignored exception in command **{interaction.command.qualified_name}**" + + def wrap(code: str) -> str: + code = code.replace("`", "\u200b`") + return f"```py\n{code}\n```" + + if len(content) > 1024: # Keeping it short for readability. + document = await paste.create(content) + content = wrap(content[:1024]) + f"\n\n [Full traceback]({document.url})" + else: + content = wrap(content) + + embed = discord.Embed( + title=header, description=content, color=discord.Color.red(), timestamp=discord.utils.utcnow() + ) + await self.error_webhook.send(embed=embed) + @tasks.loop(hours=24) async def presence(self): await self.wait_until_ready() diff --git a/bot/services/paste.py b/bot/services/paste.py new file mode 100644 index 00000000..dc59cb81 --- /dev/null +++ b/bot/services/paste.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel + +from bot.config import settings +from bot.services import http + + +class Document(BaseModel): + key: str + + @property + def url(self) -> str: + return settings.hastebin.base_url + "/" + self.key + + +async def create(content: str) -> Document: + """Creates a hastebin Document with the provided content.""" + async with http.session.post(settings.hastebin.base_url + "/documents", data=content) as response: + response.raise_for_status() + + data = await response.json() + return Document(**data)