diff --git a/src/gui b/src/gui index b781f7f9..ba9ace6c 160000 --- a/src/gui +++ b/src/gui @@ -1 +1 @@ -Subproject commit b781f7f9f21bd094601905eedd0c6463140e818f +Subproject commit ba9ace6ced300123c53287ff1ba4dbd106d1cc46 diff --git a/src/meta/LionBot.py b/src/meta/LionBot.py index 9fb9e5e5..f316c24e 100644 --- a/src/meta/LionBot.py +++ b/src/meta/LionBot.py @@ -12,6 +12,7 @@ from aiohttp import ClientSession from data import Database from utils.lib import tabulate +from gui.errors import RenderingException from .config import Conf from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context @@ -204,13 +205,23 @@ class LionBot(Bot): pass except asyncio.TimeoutError: pass + except RenderingException as e: + logger.info(f"Command failed due to RenderingException: {repr(e)}") + embed = self.tree.rendersplat(e) + try: + await ctx.error_reply(embed=embed) + except discord.HTTPException: + pass except Exception as e: logger.exception( f"Caught an unknown CommandInvokeError while executing: {cmd_str}", extra={'action': 'BotError', 'with_ctx': True} ) - error_embed = discord.Embed(title="Something went wrong!") + error_embed = discord.Embed( + title="Something went wrong!", + colour=discord.Colour.dark_red() + ) error_embed.description = ( "An unexpected error occurred while processing your command!\n" "Our development team has been notified, and the issue will be addressed soon.\n" @@ -246,7 +257,7 @@ class LionBot(Bot): try: await ctx.error_reply(embed=error_embed) - except Exception: + except discord.HTTPException: pass finally: exception.original = HandledException(exception.original) diff --git a/src/meta/LionTree.py b/src/meta/LionTree.py index e5f5624b..4461ef2e 100644 --- a/src/meta/LionTree.py +++ b/src/meta/LionTree.py @@ -8,9 +8,11 @@ from discord.enums import InteractionType from discord.app_commands.namespace import Namespace from utils.lib import tabulate +from gui.errors import RenderingException from .logger import logging_context, set_logging_context, log_wrap, log_action_stack from .errors import SafeCancellation +from .config import conf logger = logging.getLogger(__name__) @@ -29,17 +31,36 @@ class LionTree(CommandTree): except SafeCancellation: # Assume this has already been handled pass + except RenderingException as e: + logger.info(f"Tree interaction failed due to rendering exception: {repr(e)}") + embed = self.rendersplat(e) + await self.error_reply(interaction, embed) 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 + embed = self.bugsplat(interaction, error) + await self.error_reply(interaction, embed) + + async def error_reply(self, interaction, embed): + if not interaction.is_expired(): + try: + if interaction.response.is_done(): + await interaction.followup.send(embed=embed, ephemeral=True) + else: + await interaction.response.send_message(embed=embed, ephemeral=True) + except discord.HTTPException: + pass + + def rendersplat(self, e: RenderingException): + embed = discord.Embed( + title="Resource Currently Unavailable!", + description=( + "Sorry, the graphics service is currently unavailable!\n" + "Please try again in a few minutes.\n" + "If the error persists, please contact our [support team]({link})" + ).format(link=conf.bot.support_guild), + colour=discord.Colour.dark_red() + ) + return embed def bugsplat(self, interaction, e): error_embed = discord.Embed(title="Something went wrong!", colour=discord.Colour.red()) diff --git a/src/modules/pomodoro/cog.py b/src/modules/pomodoro/cog.py index 799a3822..f2a5cc41 100644 --- a/src/modules/pomodoro/cog.py +++ b/src/modules/pomodoro/cog.py @@ -386,8 +386,9 @@ class TimerCog(LionCog): ) else: # Display the timer status ephemerally + await ctx.interaction.response.defer(thinking=True, ephemeral=True) status = await timer.current_status(with_notify=False, with_warnings=False) - await ctx.reply(**status.send_args, ephemeral=True) + await ctx.interaction.edit_original_response(**status.edit_args) if error is not None: await ctx.reply(embed=error, ephemeral=True) diff --git a/src/modules/pomodoro/timer.py b/src/modules/pomodoro/timer.py index f169d4f9..9da3b288 100644 --- a/src/modules/pomodoro/timer.py +++ b/src/modules/pomodoro/timer.py @@ -12,6 +12,7 @@ from utils.lib import MessageArgs, utc_now, replace_multiple from core.lion_guild import LionGuild from core.data import CoreData from babel.translator import ctx_locale +from gui.errors import RenderingException from . import babel, logger from .data import TimerData @@ -83,6 +84,7 @@ class Timer: self.destroyed = False def __repr__(self): + # TODO: Add lock status and current state and stage return ( "") - card = await get_timer_card(self.bot, self, stage) - await card.render() - if (ui := self.status_view) is None: ui = self.status_view = TimerStatusUI(self.bot, self, self.channel) await ui.refresh() - return MessageArgs( - content=content, - file=card.as_file(f"pomodoro_{self.data.channelid}.png"), - view=ui - ) + card = await get_timer_card(self.bot, self, stage) + try: + await card.render() + file = card.as_file(f"pomodoro_{self.data.channelid}.png") + args = MessageArgs(content=content, file=file, view=ui) + except RenderingException: + args = MessageArgs(content=content, view=ui) + + return args @log_wrap(action='Send Timer Status') async def send_status(self, delete_last=True, **kwargs): @@ -785,8 +788,8 @@ class Timer: to_next_stage = (current.end - utc_now()).total_seconds() # TODO: Consider request rate and load - if to_next_stage > 1 * 60 - drift: - time_to_sleep = 1 * 60 + if to_next_stage > 5 * 60 - drift: + time_to_sleep = 5 * 60 else: time_to_sleep = to_next_stage diff --git a/src/modules/statistics/ui/leaderboard.py b/src/modules/statistics/ui/leaderboard.py index cc82c2ff..4dadbde8 100644 --- a/src/modules/statistics/ui/leaderboard.py +++ b/src/modules/statistics/ui/leaderboard.py @@ -299,42 +299,42 @@ class LeaderboardUI(StatsUI): @button(label="This Season", style=ButtonStyle.grey) async def season_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.SEASON self.focused = True await self.refresh(thinking=press) @button(label="Today", style=ButtonStyle.grey) async def day_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.DAY self.focused = True await self.refresh(thinking=press) @button(label="This Week", style=ButtonStyle.grey) async def week_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.WEEK self.focused = True await self.refresh(thinking=press) @button(label="This Month", style=ButtonStyle.grey) async def month_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.MONTH self.focused = True await self.refresh(thinking=press) @button(label="All Time", style=ButtonStyle.grey) async def alltime_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.current_period = LBPeriod.ALLTIME self.focused = True await self.refresh(thinking=press) @button(emoji=conf.emojis.backward, style=ButtonStyle.grey) async def prev_button(self, press: discord.Interaction, pressed: Button): - await press.response.defer(thinking=True) + await press.response.defer(thinking=True, ephemeral=True) self.pagen -= 1 self.focused = False await self.refresh(thinking=press) diff --git a/src/utils/ui/leo.py b/src/utils/ui/leo.py index 07979a2f..7459f0f8 100644 --- a/src/utils/ui/leo.py +++ b/src/utils/ui/leo.py @@ -10,6 +10,8 @@ from discord.ui import Modal, View, Item from meta.logger import log_action_stack, logging_context from meta.errors import SafeCancellation +from gui.errors import RenderingException + from . import logger from ..lib import MessageArgs, error_embed @@ -228,6 +230,12 @@ class LeoUI(View): f"Caught a safe cancellation from LeoUI: {e.details}", extra={'action': 'Cancel'} ) + except RenderingException as e: + logger.info( + f"UI interaction failed due to rendering exception: {repr(e)}" + ) + embed = interaction.client.tree.rendersplat(e) + await interaction.client.tree.error_reply(interaction, embed) except Exception: logger.exception( f"Unhandled interaction exception occurred in item {item!r} of LeoUI {self!r} from interaction: " @@ -235,15 +243,8 @@ class LeoUI(View): 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 + splat = interaction.client.tree.bugsplat(interaction, error) + await interaction.client.tree.error_reply(interaction, splat) class MessageUI(LeoUI): @@ -475,21 +476,20 @@ class LeoModal(Modal): """ try: raise error + except RenderingException as e: + logger.info( + f"Modal submit failed due to rendering exception: {repr(e)}" + ) + embed = interaction.client.tree.rendersplat(e) + await interaction.client.tree.error_reply(interaction, embed) except Exception: logger.exception( 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 + splat = interaction.client.tree.bugsplat(interaction, error) + await interaction.client.tree.error_reply(interaction, splat) def error_handler_for(exc):