Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Catherine-Chan's error handlers #188

Merged
merged 5 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bot/libs/ui/pride_profiles/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncpg
import discord
from libs.utils import CatherineView
from libs.utils.embeds import ErrorEmbed, SuccessEmbed
from libs.utils.embeds import ErrorEmbed, SuccessEmbed, TimeoutEmbed

from .selects import SelectPrideCategory

Expand Down Expand Up @@ -43,7 +43,7 @@ def build_register_embed(self, status: str) -> discord.Embed:
async def on_timeout(self) -> None:
if self.original_response and not self.triggered.is_set():
await self.original_response.edit(
embed=self.build_timeout_embed(), view=None, delete_after=15.0
embed=TimeoutEmbed(), view=None, delete_after=15.0
)

@discord.ui.button(
Expand Down
4 changes: 2 additions & 2 deletions bot/libs/ui/tonetags/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import asyncpg
import discord
from libs.cog_utils.tonetags import parse_tonetag
from libs.utils import CatherineView, ErrorEmbed, SuccessEmbed
from libs.utils import CatherineView, ErrorEmbed, SuccessEmbed, TimeoutEmbed

NO_CONTROL_MSG = "This menu cannot be controlled by you, sorry!"

Expand All @@ -27,7 +27,7 @@ def __init__(
async def on_timeout(self) -> None:
if self.original_response and not self.triggered.is_set():
await self.original_response.edit(
embed=self.build_timeout_embed(), view=None, delete_after=15.0
embed=TimeoutEmbed(), view=None, delete_after=15.0
)

@discord.ui.button(
Expand Down
32 changes: 26 additions & 6 deletions bot/libs/utils/embeds.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import traceback

import discord


Expand Down Expand Up @@ -30,16 +32,25 @@ def __init__(self, **kwargs):
"Uh oh! It seems like the command ran into an issue! For support, please visit [Catherine-Chan's Support Server](https://discord.gg/ns3e74frqn) to get help!",
)
super().__init__(**kwargs)
self.set_footer(text="Happened At")
self.timestamp = discord.utils.utcnow()


class ConfirmEmbed(discord.Embed):
"""Kumiko's custom confirm embed"""

def __init__(self, **kwargs):
kwargs.setdefault("color", discord.Color.from_rgb(255, 191, 0))
kwargs.setdefault("title", "Are you sure?")
class FullErrorEmbed(ErrorEmbed):
def __init__(self, error: Exception, **kwargs):
kwargs.setdefault("description", self._format_description(error))
super().__init__(**kwargs)

def _format_description(self, error: Exception) -> str:
error_traceback = "\n".join(traceback.format_exception_only(type(error), error))
desc = f"""
Uh oh! It seems like the command ran into an issue! For support, please visit [Catherine-Chan's Support Server](https://discord.gg/ns3e74frqn) to get help!

**Error**:
```{error_traceback}```
"""
return desc


class TimeoutEmbed(discord.Embed):
"""Timed out embed"""
Expand All @@ -51,3 +62,12 @@ def __init__(self, **kwargs):
"description", "Timed out waiting for a response. Cancelling action."
)
super().__init__(**kwargs)


class ConfirmEmbed(discord.Embed):
"""Kumiko's custom confirm embed"""

def __init__(self, **kwargs):
kwargs.setdefault("color", discord.Color.from_rgb(255, 191, 0))
kwargs.setdefault("title", "Are you sure?")
super().__init__(**kwargs)
32 changes: 14 additions & 18 deletions bot/libs/utils/modal.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
import traceback
from __future__ import annotations

import discord
from discord.utils import utcnow
from typing import TYPE_CHECKING

from .embeds import ErrorEmbed
import discord

NO_CONTROL_MSG = "This modal cannot be controlled by you, sorry!"
from .embeds import FullErrorEmbed

if TYPE_CHECKING:
from catherinecore import Catherine

def produce_error_embed(error: Exception) -> ErrorEmbed:
error_traceback = "\n".join(traceback.format_exception_only(type(error), error))
embed = ErrorEmbed()
embed.description = f"""
Uh oh! It seems like the modal ran into an issue! For support, please visit [Catherine-Chan's Support Server](https://discord.gg/ns3e74frqn) to get help!

**Error**:
```{error_traceback}```
"""
embed.set_footer(text="Happened At")
embed.timestamp = utcnow()
return embed
NO_CONTROL_MSG = "This modal cannot be controlled by you, sorry!"


class CatherineModal(discord.ui.Modal):
def __init__(self, interaction: discord.Interaction, *args, **kwargs):
super().__init__(*args, **kwargs)
self.interaction = interaction
self.bot: Catherine = interaction.client # type: ignore

async def interaction_check(self, interaction: discord.Interaction, /) -> bool:
if interaction.user and interaction.user.id in (
Expand All @@ -39,7 +30,12 @@ async def interaction_check(self, interaction: discord.Interaction, /) -> bool:
async def on_error(
self, interaction: discord.Interaction, error: Exception, /
) -> None:
self.bot.logger.exception(
"Ignoring modal exception from %s: ",
self.__class__.__name__,
exc_info=error,
)
await interaction.response.send_message(
embed=produce_error_embed(error), ephemeral=True
embed=FullErrorEmbed(error), ephemeral=True
)
self.stop()
82 changes: 21 additions & 61 deletions bot/libs/utils/tree.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,21 @@
from __future__ import annotations

import traceback
from typing import TYPE_CHECKING, List, Union
from typing import TYPE_CHECKING

import discord
from discord.app_commands import (
AppCommandError,
BotMissingPermissions,
CommandTree,
MissingPermissions,
NoPrivateMessage,
)
from discord.utils import utcnow
from discord import app_commands

from .embeds import ErrorEmbed
from .embeds import FullErrorEmbed

if TYPE_CHECKING:
from bot.catherinecore import Catherine


def _build_error_embed(error: AppCommandError) -> ErrorEmbed:
error_traceback = "\n".join(traceback.format_exception_only(type(error), error))
embed = ErrorEmbed()
embed.description = f"""
Uh oh! It seems like the command ran into an issue! For support, please visit [Catherine-Chan's Support Server](https://discord.gg/ns3e74frqn) to get help!

**Error**:
```{error_traceback}```
"""
embed.set_footer(text="Happened At")
embed.timestamp = utcnow()
return embed


def _build_premade_embed(title: str, description: str) -> ErrorEmbed:
embed = ErrorEmbed()
embed.title = title
embed.description = description
return embed


def _build_missing_perm_embed(
missing_perms: List[str], user: Union[discord.User, discord.Member]
) -> ErrorEmbed:
str_user = "Catherine-Chan is" if user.bot is True else "You are"
perms = ",".join(missing_perms).rstrip(",")
desc = f"""
{str_user} missing the following permissions in order to run the command:

{perms}
"""
return _build_premade_embed("Missing Permissions", desc)


# This is needed for the blacklisting system
# Yes a custom CommandTree lol.
# At the very least better than Jade's on_interaction checks
# https://github.com/LilbabxJJ-1/PrideBot/blob/master/main.py#L19-L36
class CatherineCommandTree(CommandTree):
class CatherineCommandTree(app_commands.CommandTree):
async def interaction_check(self, interaction: discord.Interaction) -> bool:
bot: Catherine = interaction.client # type: ignore # Pretty much returns the subclass anyways. I checked - Noelle

Expand Down Expand Up @@ -93,22 +51,24 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool:
return True

async def on_error(
self, interaction: discord.Interaction, error: AppCommandError
self, interaction: discord.Interaction, error: app_commands.AppCommandError
) -> None:
if isinstance(error, MissingPermissions) or isinstance(
error, BotMissingPermissions
):
await interaction.response.send_message(
embed=_build_missing_perm_embed(
error.missing_permissions, interaction.user
)
bot: Catherine = interaction.client # type: ignore

if bot._dev_mode:
bot.logger.exception("Ignoring exception:", exc_info=error)
return

if isinstance(error, app_commands.NoPrivateMessage):
await interaction.user.send(
"This command cannot be used in private messages"
)
elif isinstance(error, NoPrivateMessage):
await interaction.response.send_message(
embed=_build_premade_embed(
"DMs don't work",
"The command you are trying to run does not work in DMs.",
elif isinstance(error, app_commands.CommandInvokeError):
original = error.original
if not isinstance(original, discord.HTTPException):
bot.logger.exception("In %s: ", interaction.command.qualified_name, exc_info=original) # type: ignore
await interaction.response.send_message(
embed=FullErrorEmbed(error), ephemeral=True
)
)
else:
await interaction.response.send_message(embed=_build_error_embed(error))
await interaction.response.send_message(embed=FullErrorEmbed(error))
41 changes: 13 additions & 28 deletions bot/libs/utils/view.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,16 @@
from __future__ import annotations

import asyncio
import traceback
from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional

import discord
from discord.utils import utcnow

from .embeds import ErrorEmbed

NO_CONTROL_MSG = "This view cannot be controlled by you, sorry!"
from .embeds import FullErrorEmbed, TimeoutEmbed

if TYPE_CHECKING:
from catherinecore import Catherine

def make_error_embed(error: Exception, item: discord.ui.Item[Any]) -> ErrorEmbed:
error_traceback = "\n".join(traceback.format_exception_only(type(error), error))
embed = ErrorEmbed()
embed.description = f"""
Uh oh! It seems like the view ran into an issue! For support, please visit [Catherine-Chan's Support Server](https://discord.gg/ns3e74frqn) to get help!

**Item**: `{item.__class__.__name__}`

**Error**:
```{error_traceback}```
"""
embed.set_footer(text="Happened At")
embed.timestamp = utcnow()
return embed
NO_CONTROL_MSG = "This view cannot be controlled by you, sorry!"


class CatherineView(discord.ui.View):
Expand All @@ -37,12 +24,7 @@ def __init__(
self.interaction = interaction
self.original_response: Optional[discord.InteractionMessage]
self.triggered = asyncio.Event()

def build_timeout_embed(self) -> ErrorEmbed:
embed = ErrorEmbed()
embed.title = "\U00002757 Timed Out"
embed.description = "Timed out waiting for a response. Cancelling action."
return embed
self.bot: Catherine = interaction.client # type: ignore

async def interaction_check(self, interaction: discord.Interaction, /) -> bool:
if interaction.user and interaction.user.id in (
Expand All @@ -56,7 +38,7 @@ async def interaction_check(self, interaction: discord.Interaction, /) -> bool:
async def on_timeout(self) -> None:
if self.original_response:
await self.original_response.edit(
embed=self.build_timeout_embed(), view=None, delete_after=15.0
embed=TimeoutEmbed(), view=None, delete_after=15.0
)

async def on_error(
Expand All @@ -66,7 +48,10 @@ async def on_error(
item: discord.ui.Item[Any],
/,
) -> None:
self.bot.logger.exception(
"Ignoring view exception from %s: ", self.__class__.__name__, exc_info=error
)
await interaction.response.send_message(
embed=make_error_embed(error, item), ephemeral=True
embed=FullErrorEmbed(error), ephemeral=True
)
self.stop()