diff --git a/classes/pycord_bot.py b/classes/pycord_bot.py index 067c9be..3c01dbb 100644 --- a/classes/pycord_bot.py +++ b/classes/pycord_bot.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional from zoneinfo import ZoneInfo from bson import ObjectId +from bson.errors import InvalidId from discord import Attachment, File, Guild, TextChannel, User from libbot.cache.classes import CacheMemcached, CacheRedis from libbot.cache.manager import create_cache_client @@ -12,7 +13,12 @@ from libbot.pycord.classes import PycordBot as LibPycordBot from typing_extensions import override from classes import PycordEvent, PycordEventStage, PycordGuild, PycordUser -from classes.errors import EventStageMissingSequenceError +from classes.errors import ( + EventStageMissingSequenceError, + GuildNotFoundError, + EventStageNotFoundError, + EventNotFoundError, +) from modules.database import col_events, col_users from modules.utils import get_logger @@ -82,7 +88,12 @@ class PycordBot(LibPycordBot): # Process each event for event in events: guild: Guild = self.get_guild(event.guild_id) - pycord_guild: PycordGuild = await self.find_guild(guild) + + try: + pycord_guild: PycordGuild = await self.find_guild(guild) + except (InvalidId, GuildNotFoundError) as exc: + logger.error("Could not find guild %s (%s) due to: %s.", guild, guild.id, exc_info=exc) + continue if len(event.stage_ids) == 0: # TODO Make a nice message for management @@ -172,8 +183,23 @@ class PycordBot(LibPycordBot): # Process each event for event in events: guild: Guild = self.get_guild(event.guild_id) - pycord_guild: PycordGuild = await self.find_guild(guild) - stages: List[PycordEventStage] = await self.get_event_stages(event) + + try: + pycord_guild: PycordGuild = await self.find_guild(guild) + except (InvalidId, GuildNotFoundError) as exc: + logger.error("Could not find guild %s (%s) due to: %s.", guild, guild.id, exc_info=exc) + continue + + try: + stages: List[PycordEventStage] = await self.get_event_stages(event) + except (InvalidId, EventNotFoundError, EventStageNotFoundError) as exc: + logger.error( + "Could not event stages of the event %s (%s) due to: %s.", + event, + event._id, + exc_info=exc, + ) + continue # TODO Make a nice message stages_string: str = "\n\n".join( diff --git a/cogs/cog_config.py b/cogs/cog_config.py index 31e2bf5..c45d000 100644 --- a/cogs/cog_config.py +++ b/cogs/cog_config.py @@ -1,5 +1,6 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +from bson.errors import InvalidId from discord import ( ApplicationContext, CategoryChannel, @@ -12,6 +13,7 @@ from discord.utils import basic_autocomplete from libbot.i18n import _, in_every_locale from classes import PycordGuild +from classes.errors import GuildNotFoundError from classes.pycord_bot import PycordBot from modules.utils import autocomplete_timezones, is_operation_confirmed @@ -58,7 +60,11 @@ class CogConfig(Cog): channel: TextChannel, timezone: str, ) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return try: timezone_parsed: ZoneInfo = ZoneInfo(timezone) @@ -94,7 +100,11 @@ class CogConfig(Cog): if not (await is_operation_confirmed(ctx, confirm)): return - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return await guild.purge(self.bot.cache) @@ -106,7 +116,11 @@ class CogConfig(Cog): description_localizations=in_every_locale("description", "commands", "config_show"), ) async def command_config_show(self, ctx: ApplicationContext) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return if not guild.is_configured(): await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale)) diff --git a/cogs/cog_event.py b/cogs/cog_event.py index 8fda7d6..40e15b5 100644 --- a/cogs/cog_event.py +++ b/cogs/cog_event.py @@ -14,6 +14,7 @@ from discord.utils import basic_autocomplete from libbot.i18n import _, in_every_locale from classes import PycordEvent, PycordEventStage, PycordGuild +from classes.errors import GuildNotFoundError, EventNotFoundError from classes.pycord_bot import PycordBot from modules.utils import ( autocomplete_active_events, @@ -80,7 +81,11 @@ class CogEvent(Cog): end: str, thumbnail: Attachment = None, ) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return if not guild.is_configured(): await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale)) @@ -177,11 +182,15 @@ class CogEvent(Cog): end: str = None, thumbnail: Attachment = None, ) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) - except (InvalidId, RuntimeError): + except (InvalidId, EventNotFoundError): # TODO Make a nice message await ctx.respond("Event was not found.") return @@ -269,11 +278,15 @@ class CogEvent(Cog): if not (await is_operation_confirmed(ctx, confirm)): return - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) - except (InvalidId, RuntimeError): + except (InvalidId, EventNotFoundError): # TODO Make a nice message await ctx.respond("Event was not found.") return @@ -317,11 +330,9 @@ class CogEvent(Cog): required=True, ) async def command_event_show(self, ctx: ApplicationContext, event: str) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) - try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) - except (InvalidId, RuntimeError): + except (InvalidId, EventNotFoundError): # TODO Make a nice message await ctx.respond("Event was not found.") return diff --git a/cogs/cog_guess.py b/cogs/cog_guess.py index 7b8edf0..0925eb4 100644 --- a/cogs/cog_guess.py +++ b/cogs/cog_guess.py @@ -6,6 +6,7 @@ from discord import ApplicationContext, Cog, File, option, slash_command from libbot.i18n import _, in_every_locale from classes import PycordEvent, PycordEventStage, PycordGuild, PycordUser +from classes.errors import EventNotFoundError, EventStageNotFoundError, GuildNotFoundError from classes.pycord_bot import PycordBot @@ -26,7 +27,11 @@ class CogGuess(Cog): description_localizations=in_every_locale("description", "commands", "guess", "options", "answer"), ) async def command_guess(self, ctx: ApplicationContext, answer: str) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return if not guild.is_configured(): await ctx.respond(self.bot._("guild_unconfigured", "messages", locale=ctx.locale)) @@ -35,30 +40,24 @@ class CogGuess(Cog): user: PycordUser = await self.bot.find_user(ctx.author, ctx.guild) if user.is_jailed: - # TODO Make a nice message - await ctx.respond( - "You are jailed and cannot interact with events. Please, contact the administrator." - ) + await ctx.respond(self.bot._("jailed_error", "messages", locale=ctx.locale)) return if user.current_event_id is None or user.current_stage_id is None: - # TODO Make a nice message - await ctx.respond( - "You have no ongoing events. You can register for events using the `/register` command." - ) + await ctx.respond(self.bot._("guess_unregistered", "messages", locale=ctx.locale)) return try: event: PycordEvent = await self.bot.find_event(event_id=user.current_event_id) stage: PycordEventStage = await self.bot.find_event_stage(user.current_stage_id) - except (InvalidId, RuntimeError): - # TODO Make a nice message - await ctx.respond("Your event could not be found. Please, contact the administrator.") + except (InvalidId, EventNotFoundError, EventStageNotFoundError): + await ctx.respond(self.bot._("guess_incorrect_event", "messages", locale=ctx.locale)) return if ctx.channel_id != user.event_channels[str(event._id)]: - # TODO Make a nice message - await ctx.respond("Usage outside own event channel is not allowed.", ephemeral=True) + await ctx.respond( + self.bot._("guess_incorrect_channel", "messages", locale=ctx.locale), ephemeral=True + ) return if answer.lower() != stage.answer.lower(): @@ -73,7 +72,6 @@ class CogGuess(Cog): ) if next_stage_id is None: - # TODO Make a nice message user.completed_event_ids.append(event._id) await user._set( @@ -83,12 +81,14 @@ class CogGuess(Cog): completed_event_ids=user.completed_event_ids, ) - await ctx.respond("Congratulations! You have completed the event!") + await ctx.respond(self.bot._("guess_completed_event", "messages", locale=ctx.locale)) await self.bot.notify_admins( ctx.guild, guild, - f"User **{ctx.author.display_name}** ({ctx.author.mention}) has completed the event", + self.bot._("admin_user_completed_event", "messages", locale=ctx.locale).format( + display_name=ctx.author.display_name, mention=ctx.author.mention + ), ) return @@ -108,7 +108,12 @@ class CogGuess(Cog): await self.bot.notify_admins( ctx.guild, guild, - f"User **{ctx.author.display_name}** ({ctx.author.mention}) has completed the stage {stage.sequence+1} of the event **{event.name}**.", + self.bot._("admin_user_completed_stage", "messages", locale=ctx.locale).format( + display_name=ctx.author.display_name, + mention=ctx.author.mention, + stage_sequence=stage.sequence + 1, + event_name=event.name, + ), ) diff --git a/cogs/cog_register.py b/cogs/cog_register.py index a90d4e3..34308a6 100644 --- a/cogs/cog_register.py +++ b/cogs/cog_register.py @@ -10,6 +10,7 @@ from discord.utils import basic_autocomplete from libbot.i18n import _, in_every_locale from classes import PycordEvent, PycordEventStage, PycordGuild, PycordUser +from classes.errors import EventNotFoundError, GuildNotFoundError from classes.pycord_bot import PycordBot from modules.utils import autocomplete_active_events, get_logger, get_unix_timestamp @@ -22,7 +23,6 @@ class CogRegister(Cog): def __init__(self, bot: PycordBot): self.bot: PycordBot = bot - # TODO Introduce i18n @slash_command( name="register", description=_("description", "commands", "register"), @@ -37,13 +37,16 @@ class CogRegister(Cog): autocomplete=basic_autocomplete(autocomplete_active_events), ) async def command_register(self, ctx: ApplicationContext, event: str) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) - except (InvalidId, RuntimeError): - # TODO Make a nice message - await ctx.respond("Event was not found.") + except (InvalidId, EventNotFoundError): + await ctx.respond(self.bot._("event_not_found", "messages", locale=ctx.locale)) return if not guild.is_configured(): @@ -53,25 +56,33 @@ class CogRegister(Cog): user: PycordUser = await self.bot.find_user(ctx.author, ctx.guild) if user.is_jailed: - # TODO Make a nice message - await ctx.respond( - "You are jailed and cannot interact with events. Please, contact the administrator." - ) + await ctx.respond(self.bot._("jailed_error", "messages", locale=ctx.locale)) return if pycord_event._id in user.registered_event_ids: - # TODO Make a nice message - await ctx.respond("You are already registered for this event.") + await ctx.respond(self.bot._("register_already_registered", "messages", locale=ctx.locale)) return await user.event_register(pycord_event._id, cache=self.bot.cache) - # TODO Make a nice message - await ctx.respond( - f"You are now registered for the event **{pycord_event.name}**.\n\nNew channel will be created for you and further instructions will be provided as soon as the event starts . Good luck!" + event_ongoing: bool = pycord_event.starts.replace(tzinfo=ZoneInfo("UTC")) < datetime.now( + tz=ZoneInfo("UTC") ) - if pycord_event.starts.replace(tzinfo=ZoneInfo("UTC")) < datetime.now(tz=ZoneInfo("UTC")): + registered_message: str = ( + self.bot._("register_success_ongoing", "messages", locale=ctx.locale).format( + event_name=pycord_event.name + ) + if event_ongoing + else self.bot._("register_success_scheduled", "messages", locale=ctx.locale).format( + event_name=pycord_event.name, + event_starts=get_unix_timestamp(pycord_event.starts, to_utc=True), + ) + ) + + await ctx.respond(registered_message) + + if event_ongoing: await user.set_event(pycord_event._id, cache=self.bot.cache) user_channel: TextChannel = await user.setup_event_channel( @@ -89,7 +100,11 @@ class CogRegister(Cog): await self.bot.notify_admins( ctx.guild, guild, - f"Event channel could not be created for user **{ctx.author.display_name}** ({ctx.author.mention}) and event **{pycord_event.name}**.", + self.bot._("admin_user_channel_creation_failed", "messages", locale=ctx.locale).format( + display_name=ctx.author.display_name, + mention=ctx.author.mention, + event_name=pycord_event.name, + ), ) return @@ -100,9 +115,10 @@ class CogRegister(Cog): else File(Path(f"data/{pycord_event.thumbnail['id']}"), pycord_event.thumbnail["filename"]) ) - # TODO Make a nice message await user_channel.send( - f"Event **{pycord_event.name}** has already started!\n\nUse slash command `/guess` to suggest your answers to each event stage.", + self.bot._("register_already_started", "messages", locale=ctx.locale).format( + event_name=pycord_event.name + ), file=thumbnail, ) diff --git a/cogs/cog_stage.py b/cogs/cog_stage.py index 67d03b9..44a3449 100644 --- a/cogs/cog_stage.py +++ b/cogs/cog_stage.py @@ -7,6 +7,7 @@ from discord.utils import basic_autocomplete from libbot.i18n import _, in_every_locale from classes import PycordEvent, PycordEventStage, PycordGuild +from classes.errors import GuildNotFoundError, EventStageNotFoundError, EventNotFoundError from classes.pycord_bot import PycordBot from modules.utils import ( autocomplete_active_events, @@ -76,7 +77,11 @@ class CogStage(Cog): answer: str, media: Attachment = None, ) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return if not guild.is_configured(): await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale)) @@ -84,7 +89,7 @@ class CogStage(Cog): try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) - except (InvalidId, RuntimeError): + except (InvalidId, EventStageNotFoundError): # TODO Make a nice message await ctx.respond("Event was not found.") return @@ -186,7 +191,11 @@ class CogStage(Cog): media: Attachment = None, remove_media: bool = False, ) -> None: - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return if not guild.is_configured(): await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale)) @@ -194,7 +203,7 @@ class CogStage(Cog): try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) - except (InvalidId, RuntimeError): + except (InvalidId, EventNotFoundError): # TODO Make a nice message await ctx.respond("Event was not found.") return @@ -204,7 +213,7 @@ class CogStage(Cog): try: event_stage: PycordEventStage = await self.bot.find_event_stage(stage) - except (InvalidId, RuntimeError): + except (InvalidId, EventStageNotFoundError): # TODO Make a nice message await ctx.respond("Event stage was not found.") return @@ -267,7 +276,11 @@ class CogStage(Cog): if not (await is_operation_confirmed(ctx, confirm)): return - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return if not guild.is_configured(): await ctx.respond(self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale)) @@ -275,7 +288,7 @@ class CogStage(Cog): try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) - except (InvalidId, RuntimeError): + except (InvalidId, EventNotFoundError): # TODO Make a nice message await ctx.respond("Event was not found.") return @@ -285,7 +298,7 @@ class CogStage(Cog): try: event_stage: PycordEventStage = await self.bot.find_event_stage(stage) - except (InvalidId, RuntimeError): + except (InvalidId, EventStageNotFoundError): # TODO Make a nice message await ctx.respond("Event stage was not found.") return diff --git a/cogs/cog_unregister.py b/cogs/cog_unregister.py index d188fbd..6def4b7 100644 --- a/cogs/cog_unregister.py +++ b/cogs/cog_unregister.py @@ -4,6 +4,7 @@ from discord.utils import basic_autocomplete from libbot.i18n import _, in_every_locale from classes import PycordEvent, PycordGuild, PycordUser +from classes.errors import EventNotFoundError, GuildNotFoundError from classes.pycord_bot import PycordBot from modules.utils import autocomplete_user_registered_events, is_operation_confirmed @@ -39,13 +40,16 @@ class CogUnregister(Cog): if not (await is_operation_confirmed(ctx, confirm)): return - guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + try: + guild: PycordGuild = await self.bot.find_guild(ctx.guild.id) + except (InvalidId, GuildNotFoundError): + await ctx.respond(self.bot._("unexpected_error", "messages", locale=ctx.locale)) + return try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) - except (InvalidId, RuntimeError): - # TODO Make a nice message - await ctx.respond("Event was not found.") + except (InvalidId, EventNotFoundError): + await ctx.respond(self.bot._("event_not_found", "messages", locale=ctx.locale)) return if not guild.is_configured(): @@ -55,22 +59,18 @@ class CogUnregister(Cog): user: PycordUser = await self.bot.find_user(ctx.author, ctx.guild) if user.is_jailed: - # TODO Make a nice message - await ctx.respond( - "You are jailed and cannot interact with events. Please, contact the administrator." - ) + await ctx.respond(self.bot._("jailed_error", "messages", locale=ctx.locale)) return if pycord_event._id not in user.registered_event_ids: - # TODO Make a nice message - await ctx.respond("You are not registered for this event.") + await ctx.respond(self.bot._("unregister_not_registered", "messages", locale=ctx.locale)) return await user.event_unregister(pycord_event._id, cache=self.bot.cache) # TODO Text channel must be locked and updated - await ctx.respond("You are no longer registered for this event.") + await ctx.respond(self.bot._("unregister_unregistered", "messages", locale=ctx.locale)) def setup(bot: PycordBot) -> None: diff --git a/locale/en-US.json b/locale/en-US.json index 59731df..592ffe9 100644 --- a/locale/en-US.json +++ b/locale/en-US.json @@ -1,14 +1,30 @@ { "messages": { - "operation_unconfirmed": "Operation not confirmed.", + "admin_user_channel_creation_failed": "Event channel could not be created for user **{display_name}** ({mention}) and event **{event_name}**.", + "admin_user_completed_event": "User **{display_name}** ({mention}) has completed the event", + "admin_user_completed_stage": "User **{display_name}** ({mention}) has completed the stage {stage_sequence} of the event **{event_name}**.", + "config_reset": "Configuration has been reset. You can update it using `/config set`, otherwise no events can be held.", + "config_set": "Configuration has been updated. You can review it anytime using `/config show`.", + "config_show": "**Guild config**\n\nChannel: <#{channel_id}>\nCategory: <#{category_id}>\nTimezone: `{timezone}`", + "event_not_found": "Event was not found.", + "guess_completed_event": "Congratulations! You have completed the event!", + "guess_incorrect_channel": "Usage outside own event channel is not allowed.", + "guess_incorrect_event": "Your event could not be found. Please, contact the administrator.", + "guess_unregistered": "You have no ongoing events. You can register for events using the `/register` command.", "guild_unconfigured": "Guild is not configured. Please, report this to the administrator.", "guild_unconfigured_admin": "Guild is not configured. Please, configure it using `/config set`.", - "timezone_invalid": "Timezone **{timezone}** was not found. Please, select one of the timezones provided by the autocompletion.", - "config_set": "Configuration has been updated. You can review it anytime using `/config show`.", - "config_reset": "Configuration has been reset. You can update it using `/config set`, otherwise no events can be held.", - "config_show": "**Guild config**\n\nChannel: <#{channel_id}>\nCategory: <#{category_id}>\nTimezone: `{timezone}`", + "jailed_error": "You are jailed and cannot interact with events. Please, contact the administrator.", + "operation_unconfirmed": "Operation not confirmed.", + "register_already_registered": "You are already registered for this event.", + "register_already_started": "Event **{event_name}** has already started!\n\nUse slash command `/guess` to suggest your answers to each event stage.", + "register_success_ongoing": "You are now registered for the event **{event_name}**.\n\nNew channel has been created for you and further instructions will are provided in it. Good luck!", + "register_success_scheduled": "You are now registered for the event **{event_name}**.\n\nNew channel will be created for you and further instructions will be provided as soon as the event starts . Good luck!", "status": "**QuizBot** v{version}\n\nUptime: since ", - "status_git": "**QuizBot** v{version} (`{commit}`)\n\nUptime: up since " + "status_git": "**QuizBot** v{version} (`{commit}`)\n\nUptime: up since ", + "timezone_invalid": "Timezone **{timezone}** was not found. Please, select one of the timezones provided by the autocompletion.", + "unexpected_error": "An unexpected error has occurred. Please, contact the administrator.", + "unregister_not_registered": "You are not registered for this event.", + "unregister_unregistered": "You are no longer registered for this event." }, "commands": { "config": {