from datetime import datetime from typing import Any, Dict, List from zoneinfo import ZoneInfo from bson.errors import InvalidId from discord import ( ApplicationContext, Attachment, SlashCommandGroup, option, ) from discord.ext.commands import Cog from discord.utils import basic_autocomplete from libbot.i18n import _, in_every_locale from classes import PycordEvent, PycordEventStage, PycordGuild from classes.errors import EventNotFoundError, GuildNotFoundError from classes.pycord_bot import PycordBot from modules.utils import ( autocomplete_active_events, get_unix_timestamp, is_operation_confirmed, validate_event_validity, ) class CogEvent(Cog): """Cog with event management commands.""" def __init__(self, bot: PycordBot): self.bot: PycordBot = bot command_group: SlashCommandGroup = SlashCommandGroup( "event", description=_("description", "commands", "event"), description_localizations=in_every_locale("description", "commands", "event"), ) @command_group.command( name="create", description=_("description", "commands", "event_create"), description_localizations=in_every_locale("description", "commands", "event_create"), ) @option( "name", description=_("description", "commands", "event_create", "options", "name"), description_localizations=in_every_locale( "description", "commands", "event_create", "options", "name" ), required=True, ) @option( "start", description=_("description", "commands", "event_create", "options", "start"), description_localizations=in_every_locale( "description", "commands", "event_create", "options", "start" ), required=True, ) @option( "end", description=_("description", "commands", "event_create", "options", "end"), description_localizations=in_every_locale( "description", "commands", "event_create", "options", "end" ), required=True, ) @option( "thumbnail", description=_("description", "commands", "event_create", "options", "thumbnail"), description_localizations=in_every_locale( "description", "commands", "event_create", "options", "thumbnail" ), required=False, ) async def command_event_create( self, ctx: ApplicationContext, name: str, start: str, end: str, thumbnail: Attachment = None, ) -> None: 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), ephemeral=True) return if not guild.is_configured(): await ctx.respond( self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True ) return guild_timezone: ZoneInfo = ZoneInfo(guild.timezone) try: start_date: datetime = datetime.strptime(start, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone) end_date: datetime = datetime.strptime(end, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone) except ValueError: await ctx.respond( self.bot._("event_dates_parsing_failed", "messages", locale=ctx.locale), ephemeral=True ) return if not await validate_event_validity(ctx, name, start_date, end_date, to_utc=True): return processed_media: List[Dict[str, Any]] = ( [] if thumbnail is None else await self.bot.process_attachments([thumbnail]) ) event: PycordEvent = await self.bot.create_event( name=name, guild_id=guild.id, creator_id=ctx.author.id, starts=start_date.astimezone(ZoneInfo("UTC")), ends=end_date.astimezone(ZoneInfo("UTC")), thumbnail=processed_media[0] if thumbnail else None, ) await ctx.respond( self.bot._("event_created", "messages", locale=ctx.locale).format( event_name=event.name, start_time=get_unix_timestamp(event.starts, to_utc=True) ) ) @command_group.command( name="edit", description=_("description", "commands", "event_edit"), description_localizations=in_every_locale("description", "commands", "event_edit"), ) @option( "event", description=_("description", "commands", "event_edit", "options", "event"), description_localizations=in_every_locale( "description", "commands", "event_edit", "options", "event" ), autocomplete=basic_autocomplete(autocomplete_active_events), required=True, ) @option( "name", description=_("description", "commands", "event_edit", "options", "name"), description_localizations=in_every_locale( "description", "commands", "event_edit", "options", "name" ), required=False, ) @option( "start", description=_("description", "commands", "event_edit", "options", "start"), description_localizations=in_every_locale( "description", "commands", "event_edit", "options", "start" ), required=False, ) @option( "end", description=_("description", "commands", "event_edit", "options", "end"), description_localizations=in_every_locale( "description", "commands", "event_edit", "options", "end" ), required=False, ) @option( "thumbnail", description=_("description", "commands", "event_edit", "options", "thumbnail"), description_localizations=in_every_locale( "description", "commands", "event_edit", "options", "thumbnail" ), required=False, ) async def command_event_edit( self, ctx: ApplicationContext, event: str, name: str = None, start: str = None, end: str = None, thumbnail: Attachment = None, ) -> None: 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), ephemeral=True) return try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) except (InvalidId, EventNotFoundError): await ctx.respond(self.bot._("event_not_found", "messages", locale=ctx.locale), ephemeral=True) return if not guild.is_configured(): await ctx.respond( self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True ) return guild_timezone: ZoneInfo = ZoneInfo(guild.timezone) try: start_date: datetime = ( pycord_event.starts.replace(tzinfo=ZoneInfo("UTC")) if start is None else datetime.strptime(start, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone) ) except ValueError: await ctx.respond( self.bot._("event_start_date_parsing_failed", "messages", locale=ctx.locale), ephemeral=True ) return try: end_date: datetime = ( pycord_event.ends.replace(tzinfo=ZoneInfo("UTC")) if end is None else datetime.strptime(end, "%d.%m.%Y %H:%M").replace(tzinfo=guild_timezone) ) except ValueError: await ctx.respond( self.bot._("event_end_date_parsing_failed", "messages", locale=ctx.locale), ephemeral=True ) return if not await validate_event_validity( ctx, pycord_event.name if name is None else name, start_date, end_date, event_id=pycord_event._id, to_utc=True, ): return processed_media: List[Dict[str, Any]] = ( [] if thumbnail is None else await self.bot.process_attachments([thumbnail]) ) await pycord_event.update( self.bot.cache, starts=start_date.astimezone(ZoneInfo("UTC")), ends=end_date.astimezone(ZoneInfo("UTC")), name=pycord_event.name if name is None else name, thumbnail=pycord_event.thumbnail if thumbnail is None else processed_media[0], ) # TODO Notify participants about time changes await ctx.respond( self.bot._("event_updated", "messages", locale=ctx.locale).format( event_name=pycord_event.name, start_time=get_unix_timestamp(pycord_event.starts, to_utc=True), ) ) @command_group.command( name="cancel", description=_("description", "commands", "event_cancel"), description_localizations=in_every_locale("description", "commands", "event_cancel"), ) @option( "event", description=_("description", "commands", "event_cancel", "options", "event"), description_localizations=in_every_locale( "description", "commands", "event_cancel", "options", "event" ), autocomplete=basic_autocomplete(autocomplete_active_events), required=True, ) @option( "confirm", description=_("description", "commands", "event_cancel", "options", "confirm"), description_localizations=in_every_locale( "description", "commands", "event_cancel", "options", "confirm" ), required=False, ) async def command_event_cancel( self, ctx: ApplicationContext, event: str, confirm: bool = False, ) -> None: if not (await is_operation_confirmed(ctx, confirm)): return 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), ephemeral=True) return try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) except (InvalidId, EventNotFoundError): await ctx.respond(self.bot._("event_not_found", "messages", locale=ctx.locale), ephemeral=True) return if not guild.is_configured(): await ctx.respond( self.bot._("guild_unconfigured_admin", "messages", locale=ctx.locale), ephemeral=True ) return start_date: datetime = pycord_event.starts.replace(tzinfo=ZoneInfo("UTC")) end_date: datetime = pycord_event.ends.replace(tzinfo=ZoneInfo("UTC")) # TODO Make ongoing events cancellable if ( pycord_event.ended is not None or end_date <= datetime.now(tz=ZoneInfo("UTC")) or start_date <= datetime.now(tz=ZoneInfo("UTC")) ): await ctx.respond( self.bot._("event_not_editable", "messages", locale=ctx.locale).format( event_name=pycord_event.name ), ephemeral=True, ) return await pycord_event.cancel() # TODO Notify participants about cancellation await ctx.respond( self.bot._("event_cancelled", "messages", locale=ctx.locale).format( event_name=pycord_event.name ) ) @command_group.command( name="show", description=_("description", "commands", "event_show"), description_localizations=in_every_locale("description", "commands", "event_show"), ) @option( "event", description=_("description", "commands", "event_show", "options", "event"), description_localizations=in_every_locale( "description", "commands", "event_show", "options", "event" ), autocomplete=basic_autocomplete(autocomplete_active_events), required=True, ) async def command_event_show(self, ctx: ApplicationContext, event: str) -> None: try: pycord_event: PycordEvent = await self.bot.find_event(event_id=event) except (InvalidId, EventNotFoundError): await ctx.respond(self.bot._("event_not_found", "messages", locale=ctx.locale), ephemeral=True) return starts_date: datetime = pycord_event.get_start_date_utc() ends_date: datetime = pycord_event.get_end_date_utc() stages: List[PycordEventStage] = await self.bot.get_event_stages(pycord_event) stages_string: str = "\n\n".join( self.bot._("stage_entry", "messages", locale=ctx.locale).format( sequence=stage.sequence + 1, answer=stage.answer ) for stage in stages ) # TODO Show users registered for the event event_info_string: str = self.bot._("event_details", "messages", locale=ctx.locale).format( event_name=pycord_event.name, start_time=get_unix_timestamp(starts_date), end_time=get_unix_timestamp(ends_date), stages=stages_string, ) chunk_size: int = 2000 event_info_chunks: List[str] = [ event_info_string[i : i + chunk_size] for i in range(0, len(event_info_string), chunk_size) ] for chunk in event_info_chunks: await ctx.respond(chunk) def setup(bot: PycordBot) -> None: bot.add_cog(CogEvent(bot))