From 32b4f688eb452f7bb01419646ddc334a509b8b38 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Mon, 2 Nov 2020 09:51:03 +0530 Subject: [PATCH 01/26] Add UserEvents cog for managing/scheduling user events. --- bot/exts/fun/user_events.py | 492 ++++++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 bot/exts/fun/user_events.py diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py new file mode 100644 index 0000000000..9a3bc5d98d --- /dev/null +++ b/bot/exts/fun/user_events.py @@ -0,0 +1,492 @@ +import asyncio +import calendar +import logging +from datetime import datetime, timedelta +from typing import Optional, Tuple + +from dateutil.parser import isoparse, parse +from dateutil.relativedelta import relativedelta +from discord import Embed, Guild, Member, Message, Reaction, Role, TextChannel, VoiceChannel +from discord.ext.commands import Cog, Context, group, has_role + +from bot import constants +from bot.api import ResponseCodeError +from bot.bot import Bot +from bot.utils.scheduling import Scheduler +from bot.utils.time import humanize_delta + +log = logging.getLogger(__name__) + +EMOJIS = { + "check": "✅", + "cross": "❌" +} + +DATE_PREFIX = { + 1: 'st', 21: 'st', 31: 'st', + 2: 'nd', 22: 'nd', + 3: 'rd', 23: 'rd' +} + +USER_EVENTS_LIST_CHANNEL = 766316218944323594 +USER_EVENT_ANNOUNCEMENTS_CHANNEL = 756769546777395207 + +USER_EVENT_COORDINATORS_CHANNEL = 766318368500219976 +USER_EVENT_COORD_ROLE = 765955273138372618 + +USER_EVENT_ONGOING_ROLE = 766315117104726076 + +USER_EVENT_VOICE_CHANNEL = 766623039692734465 + +DEVELOPERS_ROLE = 756769543237533742 + + +class UserEvents(Cog): + """Manage user events with the provided commands.""" + + def __init__(self, bot: Bot) -> None: + self.bot = bot + self.scheduler = Scheduler(self.__class__.__name__) + + self.bot.loop.create_task(self.restart_event_reminders()) + + def cog_unload(self) -> None: + """Cancel scheduled tasks.""" + self.scheduler.cancel_all() + + async def restart_event_reminders(self) -> None: + """Restart scheduled event reminders when bot restarts.""" + await self.bot.wait_until_guild_available() + scheduled_events = await self.bot.api_client.get("bot/scheduled-events") + for event in scheduled_events: + # If event is live + if datetime.now() > parse(event["start_time"]).replace(tzinfo=None): + await self._schedule_event_end_reminder(event) + else: + await self._schedule_event_start_reminder(event) + + @property + def developers_role(self) -> Role: + """Return guild developers role.""" + return self.guild.get_role(DEVELOPERS_ROLE) + + @property + def guild(self) -> Guild: + """Return guild instance.""" + return self.bot.get_guild(constants.Guild.id) + + @property + def user_event_coord_channel(self) -> TextChannel: + """Return #user-events-coordinators channel.""" + return self.bot.get_channel(USER_EVENT_COORDINATORS_CHANNEL) + + @property + def user_event_announcement_channel(self) -> TextChannel: + """Return #user-events-announcement channel.""" + return self.bot.get_channel(USER_EVENT_ANNOUNCEMENTS_CHANNEL) + + @property + def user_events_list_channel(self) -> TextChannel: + """Return #user-events-list channel.""" + return self.bot.get_channel(USER_EVENTS_LIST_CHANNEL) + + @property + def user_event_voice_channel(self) -> VoiceChannel: + """Return #user-events-voice channel.""" + return self.bot.get_channel(USER_EVENT_VOICE_CHANNEL) + + @staticmethod + def user_event_embed( + event_name: str, event_description: str, organizer: Member, status: str = "Not scheduled" + ) -> Embed: + """Embed representing a user event.""" + embed = Embed( + title=f"{event_name}", + description=( + f"Organizer: {organizer.mention}\n\n" + f"{event_description}\n\n" + f"**Status:** {status}" + ) + ) + embed.set_footer(text="Confirm event creation.") + return embed + + async def fetch_subscribers(self, message_id: int) -> list: + """Fetch reacted users to event message as subscribers.""" + message = await self.user_events_list_channel.fetch_message(message_id) + reaction = message.reactions[0] + users = await reaction.users().flatten() + return users + + async def _sync_subscribers(self, event_name: str, sub_ids: list) -> None: + """Sync subscribers with the site.""" + data = { + "subscriptions": sub_ids + } + await self.bot.api_client.patch( + f"bot/user-events/{event_name}", + data=data + ) + + async def _update_user_event_status(self, status: str, user_event: dict) -> None: + """Update event status on #user-events-list channel.""" + message = await self.user_events_list_channel.fetch_message(user_event["message_id"]) + embed = self.user_event_embed( + user_event["name"], + user_event["description"], + self.guild.get_member(user_event["organizer"]), + status + ) + embed.set_footer(text="React to be notified.") + await message.edit(embed=embed) + + async def _event_preparation(self, scheduled_event: dict) -> None: + """Notify event organizer 30min before event and add User Event: Ongoing role.""" + organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) + + if USER_EVENT_ONGOING_ROLE not in [role.id for role in organizer.roles]: + role = self.guild.get_role(USER_EVENT_ONGOING_ROLE) + await organizer.add_roles(role) + + start_time = isoparse(scheduled_event["start_time"]).replace(tzinfo=None) + time_remaining = humanize_delta(relativedelta(start_time, datetime.now())) + await self.user_event_coord_channel.send( + f"{organizer.mention} Event to start in {time_remaining}. " + f"use `!userevent announce` " + f"command to start the event." + ) + + await self._schedule_event_end_reminder(scheduled_event) + + async def _event_end(self, scheduled_event: dict) -> None: + """End user event.""" + organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) + role = self.guild.get_role(USER_EVENT_ONGOING_ROLE) + + await organizer.remove_roles(role) + + status = "Not Scheduled" + await self._update_user_event_status(status, scheduled_event["user_event"]) + + await self.edit_events_vc(False) + + await self.user_event_coord_channel.send(f"{organizer.mention} event has ended! Voice channel is now closed.") + await self._cancel_scheduled_event(scheduled_event) + + async def _schedule_event_start_reminder(self, scheduled_event: dict) -> None: + """Schedule reminder to remind user 30min before event start.""" + start_datetime = isoparse(scheduled_event["start_time"]).replace(tzinfo=None) + + reminder = start_datetime - timedelta(minutes=30) + + # check if current time is already past reminder's time + if reminder < datetime.now(): + await self._event_preparation(scheduled_event) + return + + self.scheduler.schedule_at( + reminder, + scheduled_event["user_event"]["organizer"], + self._event_preparation(scheduled_event) + ) + + async def _send_confirmation_message( + self, + event_name: str, + event_description: str, + author: Member + ) -> Tuple[Message, Embed]: + """Send confirmation message for user event creation.""" + embed = self.user_event_embed(event_name, event_description, author) + message = await self.user_event_coord_channel.send(embed=embed) + + await message.add_reaction(EMOJIS["check"]) + await message.add_reaction(EMOJIS["cross"]) + + return message, embed + + async def _schedule_event_end_reminder(self, scheduled_event: dict) -> None: + """Schedule reminder to remind user about event end.""" + reminder = isoparse(scheduled_event["end_time"]).replace(tzinfo=None) + + self.scheduler.schedule_at( + reminder, + scheduled_event["user_event"]["organizer"], + self._event_end(scheduled_event) + ) + + async def list_new_event(self, embed: Embed) -> Message: + """List new event in the user-events-list channel.""" + embed.set_footer(text="React to be notified.") + + # send event embed in #User-events-list channel + event_message = await self.user_events_list_channel.send(embed=embed) + + # add reaction for subscribing + await event_message.add_reaction(EMOJIS["check"]) + + return event_message + + async def edit_events_vc(self, open_vc: bool) -> None: + """Open/Close events voice channel.""" + await self.user_event_voice_channel.set_permissions( + self.developers_role, + view_channel=open_vc, + connect=open_vc, + speak=open_vc + ) + + async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: + """Cancel a scheduled event.""" + # Remove scheduler related to the scheduled event + self.scheduler.cancel(scheduled_event["user_event"]["organizer"]) + + # DELETE scheduled event on site + await self.bot.api_client.delete(f"bot/scheduled-events/{scheduled_event['id']}") + + # Update user event status + status = "Not scheduled" + await self._update_user_event_status(status, scheduled_event["user_event"]) + + @has_role(USER_EVENT_COORD_ROLE) + @group(name="userevent", invoke_without_command=True) + async def user_event(self, ctx: Context) -> None: + """Commands to perform CRUD operations on user events and scheduled events.""" + if ctx.channel.id == USER_EVENT_COORDINATORS_CHANNEL: + await ctx.send_help(ctx.command) + + @has_role(USER_EVENT_COORD_ROLE) + @user_event.command(name="create") + async def create_user_event(self, ctx: Context, event_name: str, *, event_description: str) -> None: + """Create a new user event.""" + organizer = ctx.author.id + + # Ask user to confirm before event creation + confirmation_message, embed = await self._send_confirmation_message(event_name, event_description, ctx.author) + + def check(reaction: Reaction, user: Member) -> bool: + """Check for correct reaction and user.""" + return ( + user == ctx.author and str(reaction.emoji) in EMOJIS.values() + and reaction.message.id == confirmation_message.id + ) + + # Check if user is OK for event creation + try: + choice, _ = await self.bot.wait_for('reaction_add', timeout=60.0, check=check) + except asyncio.TimeoutError: + await self.user_event_coord_channel.send("User Event not created.") + return + + # User reacting to `cross` indicates cancellation of event creation process. + if str(choice) == EMOJIS["cross"]: + await ctx.send("User Event not created.") + return + + # POST new user event + post_data = { + "name": event_name, + "organizer": organizer, + "description": event_description, + "message_id": 0 # patch message_id after sending message in user_events channel + } + await self.bot.api_client.post("bot/user-events", data=post_data) + + # List new event in user-events-list channel + message = await self.list_new_event(embed) + + # PATCH message id of newly created user event + patch_data = { + "message_id": message.id + } + await self.bot.api_client.patch(f"bot/user-events/{event_name}", data=patch_data) + + await ctx.send(f"User Event **{event_name}** created.") + + @has_role(USER_EVENT_COORD_ROLE) + @user_event.command(name="delete") + async def delete_user_event(self, ctx: Context, event_name: str) -> None: + """Delete user event.""" + # Check if user is event organizer + # This will automatically raise error if event does not exist + user_event = await self.bot.api_client.get(f"bot/user-events/{event_name}") + + if user_event["organizer"] != ctx.author.id: + await ctx.send("You can only cancel your events!") + return + + # Check if the event is scheduled + query_params = { + "user_event__organizer": ctx.author.id + } + scheduled_event = await self.bot.api_client.get( + "bot/scheduled-events", + params=query_params + ) + + # If event is scheduled + if scheduled_event: + await ctx.send("Cancel the event before deleting!") + return + + # Delete user event on site + await self.bot.api_client.delete(f"bot/user-events/{event_name}") + + # Delete message in #user-events-list channel + channel = self.user_events_list_channel + message = await channel.fetch_message(user_event["message_id"]) + await message.delete() + + await ctx.send(f"User Event **{event_name}** deleted!") + + @has_role(USER_EVENT_COORD_ROLE) + @user_event.command(name="schedule") + async def schedule_user_event( + self, + ctx: Context, + event_name: str, + start_datetime: str, + duration: float = 3.0 + ) -> None: + """Schedule a user event at a particular date and time.""" + # Check if user event is registered. + query_params = { + "organizer": ctx.author.id + } + user_event = await self.bot.api_client.get( + f"bot/user-events/{event_name}", + params=query_params, + ) + # Parse and convert given timestamp to python datetime + start_datetime = parse(start_datetime) + + if start_datetime < datetime.now(): + await ctx.send("Invalid start datetime.") + return + + # Register scheduled event on site + post_data = { + "user_event_name": user_event["name"], + "start_time": start_datetime.isoformat(), + "end_time": (start_datetime + timedelta(hours=duration)).isoformat() + } + scheduled_event = await self.bot.api_client.post( + "bot/scheduled-events", + data=post_data + ) + + readable_datetime = ( + f"{start_datetime.day}{DATE_PREFIX.get(start_datetime.day, 'th')} " + f"{list(calendar.month_name)[start_datetime.month]}, {start_datetime.year}.\n" + f"{start_datetime.time().strftime('%H:%M:%S')} UTC (24-hour format)" + ) + status = f"Scheduled for\n{readable_datetime}" + + # Send message in #user-events-announcements regarding event schedule + embed = Embed( + title=f"{user_event['name']} Event Scheduled!", + description=readable_datetime + ) + + # Announce scheduled user event + event_message = await self.user_events_list_channel.fetch_message(scheduled_event["user_event"]["message_id"]) + embed.url = event_message.jump_url + "/discord" + embed.set_footer(text="Follow embed link and react to message to be notified.") + await self.user_event_announcement_channel.send(embed=embed) + + # Update status in #user-events-list channel + await self._update_user_event_status(status, user_event) + + # Set start reminders for scheduled event organizer. + await self._schedule_event_start_reminder(scheduled_event) + + @has_role(USER_EVENT_COORD_ROLE) + @user_event.command(name="cancel") + async def cancel_scheduled_event(self, ctx: Context) -> None: + """Cancel a scheduled event.""" + # Check if user has an event scheduled + query_params = { + "user_event__organizer": ctx.author.id + } + scheduled_event = await self.bot.api_client.get( + "bot/scheduled-events", + params=query_params + ) + + # If user has not scheduled any event + if not scheduled_event: + return + + await self._cancel_scheduled_event(scheduled_event[0]) + + await ctx.send(f"{scheduled_event['user_event']['name']} event is cancelled.") + + @has_role(USER_EVENT_COORD_ROLE) + @user_event.command(name="open") + async def open_voice_channel(self, ctx: Context) -> None: + """Open the events voice channel for developers.""" + await self.edit_events_vc(True) + await ctx.send("Channel is now open, have fun!") + + @has_role(USER_EVENT_COORD_ROLE) + @user_event.command(name="announce") + async def announce_event_start(self, ctx: Context, *, announcement_message: Optional[str]) -> None: + """Inform all event subscribers that the event is starting.""" + # Get scheduled event + query_params = { + "user_event__organizer": ctx.author.id + } + # This will error out if event is not scheduled + scheduled_event = await self.bot.api_client.get( + "bot/scheduled-events", + params=query_params + ) + message_id = scheduled_event[0]["user_event"]["message_id"] + + # Get subscribers + subscribers = await self.fetch_subscribers(message_id) + + # Remove organizer from subscribers list + subscribers = [ + sub for sub in subscribers + if sub.id != scheduled_event[0]["user_event"]["organizer"] + and not sub.bot + ] + + # Sync subscribers with the site + await self._sync_subscribers( + scheduled_event[0]["user_event"]["name"], + [sub.id for sub in subscribers] + ) + + # Send message in #user-event-announcements channel + subscribers = "".join(sub.mention for sub in subscribers) + message = subscribers + + if announcement_message: + message = announcement_message + subscribers + + # Update event status + status = "Live" + await self._update_user_event_status(status, scheduled_event[0]["user_event"]) + + await self.user_event_announcement_channel.send(message) + + @has_role(USER_EVENT_COORD_ROLE) + @user_event.command(name="close") + async def close_voice_channel(self, ctx: Context) -> None: + """Close the events voice channel for developers.""" + await self.edit_events_vc(False) + await ctx.send("Voice Channel is now closed.") + + async def cog_command_error(self, ctx: Context, error: Exception) -> None: + """Handle ResponseCodeError locally.""" + if isinstance(error, ResponseCodeError): + error_message = "\n".join("\n".join(value) for value in error.response_json.values()) + await ctx.send(error_message) + error.handled = True + + +def setup(bot: Bot) -> None: + """Load the UserEvents cog.""" + bot.add_cog(UserEvents(bot)) From b0e07539afe5010bf4ddb0131378618b39f8ffd7 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 4 Nov 2020 21:56:16 +0530 Subject: [PATCH 02/26] Add methods for various statuses --- bot/exts/fun/user_events.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 9a3bc5d98d..f3bff67170 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -65,6 +65,27 @@ async def restart_event_reminders(self) -> None: else: await self._schedule_event_start_reminder(event) + @staticmethod + def not_scheduled() -> str: + """To indicate user event is not scheduled.""" + return "Not scheduled" + + @staticmethod + def live() -> str: + """To indicate user event is live.""" + return "Live" + + @staticmethod + def scheduled(start_datetime: datetime) -> str: + """To indicate user event is scheduled.""" + readable_datetime = ( + f"{start_datetime.day}{DATE_PREFIX.get(start_datetime.day, 'th')} " + f"{list(calendar.month_name)[start_datetime.month]}, {start_datetime.year}.\n" + f"{start_datetime.time().strftime('%H:%M:%S')} UTC (24-hour format)" + ) + status = f"Scheduled for\n{readable_datetime}" + return status + @property def developers_role(self) -> Role: """Return guild developers role.""" From 6a409861df5893631961f671cfad1b2e5cf1fd0d Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 4 Nov 2020 21:59:18 +0530 Subject: [PATCH 03/26] Add method to check if user is event organizer. --- bot/exts/fun/user_events.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index f3bff67170..616243865b 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -269,6 +269,15 @@ async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: status = "Not scheduled" await self._update_user_event_status(status, scheduled_event["user_event"]) + async def check_if_user_is_organizer(self, user_id: int, event_name: str) -> Optional[dict]: + """Check if user is the organizer of an event.""" + # This will automatically raise error if event does not exist + user_event = await self.bot.api_client.get(f"bot/user-events/{event_name}") + + if user_event["organizer"] != user_id: + return None + return user_event + @has_role(USER_EVENT_COORD_ROLE) @group(name="userevent", invoke_without_command=True) async def user_event(self, ctx: Context) -> None: From a0515ffa7d6aad665679aa80ebd75d99408d3380 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 4 Nov 2020 21:59:51 +0530 Subject: [PATCH 04/26] Add method to modify event description. --- bot/exts/fun/user_events.py | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 616243865b..72c766abf4 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -333,6 +333,47 @@ def check(reaction: Reaction, user: Member) -> bool: await ctx.send(f"User Event **{event_name}** created.") + @has_role(USER_EVENT_COORD_ROLE) + @user_event.command(name="change_desc", aliases=["cd", "desc"]) + async def change_description(self, ctx: Context, event_name: str, *, event_description: str) -> None: + """Change user event description.""" + # Check if user is event organizer + if not self.check_if_user_is_organizer(ctx.author.id, event_name): + await ctx.send("You can only modify your events!") + return + data = { + "description": event_description + } + # Patch event description + # This is raise 404 error if event does not exist + user_event = await self.bot.api_client.patch( + f"bot/user-events/{event_name}", + data=data + ) + + # Update event message + # Check if event is scheduled + query_params = { + "user_event__organizer": ctx.author.id + } + scheduled_event = await self.bot.api_client.get( + "bot/scheduled-events", + params=query_params + ) + # If event is scheduled + if scheduled_event: + await self.update_user_event_message( + status=self.scheduled(isoparse(scheduled_event[0]["start_time"])), + user_event=scheduled_event[0]["user_event"] + ) + return + + # If event is not scheduled + await self.update_user_event_message( + status=self.not_scheduled(), + user_event=user_event + ) + @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="delete") async def delete_user_event(self, ctx: Context, event_name: str) -> None: From 5a6cd83903b13352c872c67451412fbd3dc6bb07 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 4 Nov 2020 22:00:28 +0530 Subject: [PATCH 05/26] Code cleanup, and renaming functions. --- bot/exts/fun/user_events.py | 73 ++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 72c766abf4..0b6eac716d 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -61,9 +61,9 @@ async def restart_event_reminders(self) -> None: for event in scheduled_events: # If event is live if datetime.now() > parse(event["start_time"]).replace(tzinfo=None): - await self._schedule_event_end_reminder(event) + self.schedule_event_end_reminder(event) else: - await self._schedule_event_start_reminder(event) + await self.schedule_event_start_reminder(event) @staticmethod def not_scheduled() -> str: @@ -117,9 +117,7 @@ def user_event_voice_channel(self) -> VoiceChannel: return self.bot.get_channel(USER_EVENT_VOICE_CHANNEL) @staticmethod - def user_event_embed( - event_name: str, event_description: str, organizer: Member, status: str = "Not scheduled" - ) -> Embed: + def user_event_embed(event_name: str, event_description: str, organizer: Member, status: str) -> Embed: """Embed representing a user event.""" embed = Embed( title=f"{event_name}", @@ -139,7 +137,7 @@ async def fetch_subscribers(self, message_id: int) -> list: users = await reaction.users().flatten() return users - async def _sync_subscribers(self, event_name: str, sub_ids: list) -> None: + async def sync_subscribers(self, event_name: str, sub_ids: list) -> None: """Sync subscribers with the site.""" data = { "subscriptions": sub_ids @@ -149,8 +147,8 @@ async def _sync_subscribers(self, event_name: str, sub_ids: list) -> None: data=data ) - async def _update_user_event_status(self, status: str, user_event: dict) -> None: - """Update event status on #user-events-list channel.""" + async def update_user_event_message(self, status: str, user_event: dict) -> None: + """Update event message on #user-events-list channel.""" message = await self.user_events_list_channel.fetch_message(user_event["message_id"]) embed = self.user_event_embed( user_event["name"], @@ -161,7 +159,7 @@ async def _update_user_event_status(self, status: str, user_event: dict) -> None embed.set_footer(text="React to be notified.") await message.edit(embed=embed) - async def _event_preparation(self, scheduled_event: dict) -> None: + async def event_preparation(self, scheduled_event: dict) -> None: """Notify event organizer 30min before event and add User Event: Ongoing role.""" organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) @@ -177,24 +175,24 @@ async def _event_preparation(self, scheduled_event: dict) -> None: f"command to start the event." ) - await self._schedule_event_end_reminder(scheduled_event) + self.schedule_event_end_reminder(scheduled_event) - async def _event_end(self, scheduled_event: dict) -> None: + async def event_end(self, scheduled_event: dict) -> None: """End user event.""" organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) role = self.guild.get_role(USER_EVENT_ONGOING_ROLE) await organizer.remove_roles(role) - status = "Not Scheduled" - await self._update_user_event_status(status, scheduled_event["user_event"]) + status = self.not_scheduled() + await self.update_user_event_message(status, scheduled_event["user_event"]) await self.edit_events_vc(False) await self.user_event_coord_channel.send(f"{organizer.mention} event has ended! Voice channel is now closed.") await self._cancel_scheduled_event(scheduled_event) - async def _schedule_event_start_reminder(self, scheduled_event: dict) -> None: + async def schedule_event_start_reminder(self, scheduled_event: dict) -> None: """Schedule reminder to remind user 30min before event start.""" start_datetime = isoparse(scheduled_event["start_time"]).replace(tzinfo=None) @@ -202,23 +200,23 @@ async def _schedule_event_start_reminder(self, scheduled_event: dict) -> None: # check if current time is already past reminder's time if reminder < datetime.now(): - await self._event_preparation(scheduled_event) + await self.event_preparation(scheduled_event) return self.scheduler.schedule_at( reminder, scheduled_event["user_event"]["organizer"], - self._event_preparation(scheduled_event) + self.event_preparation(scheduled_event) ) - async def _send_confirmation_message( + async def send_confirmation_message( self, event_name: str, event_description: str, author: Member ) -> Tuple[Message, Embed]: """Send confirmation message for user event creation.""" - embed = self.user_event_embed(event_name, event_description, author) + embed = self.user_event_embed(event_name, event_description, author, self.not_scheduled()) message = await self.user_event_coord_channel.send(embed=embed) await message.add_reaction(EMOJIS["check"]) @@ -226,14 +224,14 @@ async def _send_confirmation_message( return message, embed - async def _schedule_event_end_reminder(self, scheduled_event: dict) -> None: + def schedule_event_end_reminder(self, scheduled_event: dict) -> None: """Schedule reminder to remind user about event end.""" reminder = isoparse(scheduled_event["end_time"]).replace(tzinfo=None) self.scheduler.schedule_at( reminder, scheduled_event["user_event"]["organizer"], - self._event_end(scheduled_event) + self.event_end(scheduled_event) ) async def list_new_event(self, embed: Embed) -> Message: @@ -266,8 +264,8 @@ async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: await self.bot.api_client.delete(f"bot/scheduled-events/{scheduled_event['id']}") # Update user event status - status = "Not scheduled" - await self._update_user_event_status(status, scheduled_event["user_event"]) + status = self.not_scheduled() + await self.update_user_event_message(status, scheduled_event["user_event"]) async def check_if_user_is_organizer(self, user_id: int, event_name: str) -> Optional[dict]: """Check if user is the organizer of an event.""" @@ -292,7 +290,7 @@ async def create_user_event(self, ctx: Context, event_name: str, *, event_descri organizer = ctx.author.id # Ask user to confirm before event creation - confirmation_message, embed = await self._send_confirmation_message(event_name, event_description, ctx.author) + confirmation_message, embed = await self.send_confirmation_message(event_name, event_description, ctx.author) def check(reaction: Reaction, user: Member) -> bool: """Check for correct reaction and user.""" @@ -379,11 +377,9 @@ async def change_description(self, ctx: Context, event_name: str, *, event_descr async def delete_user_event(self, ctx: Context, event_name: str) -> None: """Delete user event.""" # Check if user is event organizer - # This will automatically raise error if event does not exist - user_event = await self.bot.api_client.get(f"bot/user-events/{event_name}") - - if user_event["organizer"] != ctx.author.id: - await ctx.send("You can only cancel your events!") + user_event = self.check_if_user_is_organizer(ctx.author.id, event_name) + if not user_event: + await ctx.send("You can only delete your events!") return # Check if the event is scheduled @@ -446,17 +442,12 @@ async def schedule_user_event( data=post_data ) - readable_datetime = ( - f"{start_datetime.day}{DATE_PREFIX.get(start_datetime.day, 'th')} " - f"{list(calendar.month_name)[start_datetime.month]}, {start_datetime.year}.\n" - f"{start_datetime.time().strftime('%H:%M:%S')} UTC (24-hour format)" - ) - status = f"Scheduled for\n{readable_datetime}" + status = self.scheduled(start_datetime) # Send message in #user-events-announcements regarding event schedule embed = Embed( title=f"{user_event['name']} Event Scheduled!", - description=readable_datetime + description=status ) # Announce scheduled user event @@ -466,10 +457,10 @@ async def schedule_user_event( await self.user_event_announcement_channel.send(embed=embed) # Update status in #user-events-list channel - await self._update_user_event_status(status, user_event) + await self.update_user_event_message(status, user_event) # Set start reminders for scheduled event organizer. - await self._schedule_event_start_reminder(scheduled_event) + await self.schedule_event_start_reminder(scheduled_event) @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="cancel") @@ -490,7 +481,7 @@ async def cancel_scheduled_event(self, ctx: Context) -> None: await self._cancel_scheduled_event(scheduled_event[0]) - await ctx.send(f"{scheduled_event['user_event']['name']} event is cancelled.") + await ctx.send(f"{scheduled_event[0]['user_event']['name']} event is cancelled.") @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="open") @@ -525,7 +516,7 @@ async def announce_event_start(self, ctx: Context, *, announcement_message: Opti ] # Sync subscribers with the site - await self._sync_subscribers( + await self.sync_subscribers( scheduled_event[0]["user_event"]["name"], [sub.id for sub in subscribers] ) @@ -538,8 +529,8 @@ async def announce_event_start(self, ctx: Context, *, announcement_message: Opti message = announcement_message + subscribers # Update event status - status = "Live" - await self._update_user_event_status(status, scheduled_event[0]["user_event"]) + status = self.live() + await self.update_user_event_message(status, scheduled_event[0]["user_event"]) await self.user_event_announcement_channel.send(message) From 09910432311c00ad4e6759101c67bedd9c35ac02 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 5 Nov 2020 01:34:42 +0530 Subject: [PATCH 06/26] fix bug: coroutine was never awaited. --- bot/exts/fun/user_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 0b6eac716d..f51502bab2 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -377,7 +377,7 @@ async def change_description(self, ctx: Context, event_name: str, *, event_descr async def delete_user_event(self, ctx: Context, event_name: str) -> None: """Delete user event.""" # Check if user is event organizer - user_event = self.check_if_user_is_organizer(ctx.author.id, event_name) + user_event = await self.check_if_user_is_organizer(ctx.author.id, event_name) if not user_event: await ctx.send("You can only delete your events!") return From 4591781d3bdd37c0d634138b48fc936fe5ae2a4e Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 00:02:26 +0530 Subject: [PATCH 07/26] More checks to error handler and fix bugs. --- bot/exts/fun/user_events.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index f51502bab2..0baa812108 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -7,7 +7,7 @@ from dateutil.parser import isoparse, parse from dateutil.relativedelta import relativedelta from discord import Embed, Guild, Member, Message, Reaction, Role, TextChannel, VoiceChannel -from discord.ext.commands import Cog, Context, group, has_role +from discord.ext.commands import Cog, CommandInvokeError, Context, group, has_role from bot import constants from bot.api import ResponseCodeError @@ -543,10 +543,23 @@ async def close_voice_channel(self, ctx: Context) -> None: async def cog_command_error(self, ctx: Context, error: Exception) -> None: """Handle ResponseCodeError locally.""" - if isinstance(error, ResponseCodeError): - error_message = "\n".join("\n".join(value) for value in error.response_json.values()) - await ctx.send(error_message) - error.handled = True + # Custom errors are raised via the CommandInvokeError + if isinstance(error, CommandInvokeError): + + if isinstance(error.original, ResponseCodeError): + + # Parse 400 error responses from site + if error.original.status == 400: + + # 400 error messages are usually of the + # format -> { field: [error message(s)] } + error_message = "\n".join( + "\n".join(value) + for value in error.original.response_json.values() + ) + + await ctx.send(error_message) + error.handled = True def setup(bot: Bot) -> None: From 4f576755d412d297ee8a0bb781c15ca330a85aa2 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 00:05:41 +0530 Subject: [PATCH 08/26] Replace command checks with a single cog check. --- bot/exts/fun/user_events.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 0baa812108..5fc99d5f51 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -283,7 +283,6 @@ async def user_event(self, ctx: Context) -> None: if ctx.channel.id == USER_EVENT_COORDINATORS_CHANNEL: await ctx.send_help(ctx.command) - @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="create") async def create_user_event(self, ctx: Context, event_name: str, *, event_description: str) -> None: """Create a new user event.""" @@ -331,7 +330,6 @@ def check(reaction: Reaction, user: Member) -> bool: await ctx.send(f"User Event **{event_name}** created.") - @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="change_desc", aliases=["cd", "desc"]) async def change_description(self, ctx: Context, event_name: str, *, event_description: str) -> None: """Change user event description.""" @@ -372,7 +370,6 @@ async def change_description(self, ctx: Context, event_name: str, *, event_descr user_event=user_event ) - @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="delete") async def delete_user_event(self, ctx: Context, event_name: str) -> None: """Delete user event.""" @@ -406,7 +403,6 @@ async def delete_user_event(self, ctx: Context, event_name: str) -> None: await ctx.send(f"User Event **{event_name}** deleted!") - @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="schedule") async def schedule_user_event( self, @@ -462,7 +458,6 @@ async def schedule_user_event( # Set start reminders for scheduled event organizer. await self.schedule_event_start_reminder(scheduled_event) - @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="cancel") async def cancel_scheduled_event(self, ctx: Context) -> None: """Cancel a scheduled event.""" @@ -483,14 +478,12 @@ async def cancel_scheduled_event(self, ctx: Context) -> None: await ctx.send(f"{scheduled_event[0]['user_event']['name']} event is cancelled.") - @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="open") async def open_voice_channel(self, ctx: Context) -> None: """Open the events voice channel for developers.""" await self.edit_events_vc(True) await ctx.send("Channel is now open, have fun!") - @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="announce") async def announce_event_start(self, ctx: Context, *, announcement_message: Optional[str]) -> None: """Inform all event subscribers that the event is starting.""" @@ -534,7 +527,6 @@ async def announce_event_start(self, ctx: Context, *, announcement_message: Opti await self.user_event_announcement_channel.send(message) - @has_role(USER_EVENT_COORD_ROLE) @user_event.command(name="close") async def close_voice_channel(self, ctx: Context) -> None: """Close the events voice channel for developers.""" @@ -561,6 +553,10 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: await ctx.send(error_message) error.handled = True + async def cog_check(self, ctx: Context) -> bool: + """Allow users with event coordinator role to exec cog commands.""" + return has_role(USER_EVENT_COORD_ROLE).predicate(ctx) + def setup(bot: Bot) -> None: """Load the UserEvents cog.""" From 87ed46386d1a3d5dd8c3c5ca48998f68a82f2288 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 00:06:29 +0530 Subject: [PATCH 09/26] Improve docs, bot responses and code. --- bot/exts/fun/user_events.py | 40 +++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 5fc99d5f51..1aa58d2458 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -187,7 +187,7 @@ async def event_end(self, scheduled_event: dict) -> None: status = self.not_scheduled() await self.update_user_event_message(status, scheduled_event["user_event"]) - await self.edit_events_vc(False) + await self.edit_events_vc(open_vc=False) await self.user_event_coord_channel.send(f"{organizer.mention} event has ended! Voice channel is now closed.") await self._cancel_scheduled_event(scheduled_event) @@ -255,6 +255,15 @@ async def edit_events_vc(self, open_vc: bool) -> None: speak=open_vc ) + async def check_if_user_is_organizer(self, user_id: int, event_name: str) -> Optional[dict]: + """Check if user is the organizer of an event.""" + # This will automatically raise error if event does not exist + user_event = await self.bot.api_client.get(f"bot/user-events/{event_name}") + + if user_event["organizer"] != user_id: + return None + return user_event + async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: """Cancel a scheduled event.""" # Remove scheduler related to the scheduled event @@ -267,16 +276,6 @@ async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: status = self.not_scheduled() await self.update_user_event_message(status, scheduled_event["user_event"]) - async def check_if_user_is_organizer(self, user_id: int, event_name: str) -> Optional[dict]: - """Check if user is the organizer of an event.""" - # This will automatically raise error if event does not exist - user_event = await self.bot.api_client.get(f"bot/user-events/{event_name}") - - if user_event["organizer"] != user_id: - return None - return user_event - - @has_role(USER_EVENT_COORD_ROLE) @group(name="userevent", invoke_without_command=True) async def user_event(self, ctx: Context) -> None: """Commands to perform CRUD operations on user events and scheduled events.""" @@ -411,8 +410,17 @@ async def schedule_user_event( start_datetime: str, duration: float = 3.0 ) -> None: - """Schedule a user event at a particular date and time.""" - # Check if user event is registered. + """ + Schedule a user event at a particular date and time. + + The time should be in UTC and 24hour format. + Default duration is 3 hours. + + Examples: + !userevent schedule minecraft "october 10th 2020 14:00:00" 1.5 + !userevent schedule "among us" "october 10th 2020 14:00:00" 2 + """ + # Check if author is event organizer. query_params = { "organizer": ctx.author.id } @@ -450,6 +458,7 @@ async def schedule_user_event( event_message = await self.user_events_list_channel.fetch_message(scheduled_event["user_event"]["message_id"]) embed.url = event_message.jump_url + "/discord" embed.set_footer(text="Follow embed link and react to message to be notified.") + await self.user_event_announcement_channel.send(embed=embed) # Update status in #user-events-list channel @@ -472,6 +481,7 @@ async def cancel_scheduled_event(self, ctx: Context) -> None: # If user has not scheduled any event if not scheduled_event: + await ctx.send("You do not have an event scheduled.") return await self._cancel_scheduled_event(scheduled_event[0]) @@ -481,7 +491,7 @@ async def cancel_scheduled_event(self, ctx: Context) -> None: @user_event.command(name="open") async def open_voice_channel(self, ctx: Context) -> None: """Open the events voice channel for developers.""" - await self.edit_events_vc(True) + await self.edit_events_vc(open_vc=True) await ctx.send("Channel is now open, have fun!") @user_event.command(name="announce") @@ -530,7 +540,7 @@ async def announce_event_start(self, ctx: Context, *, announcement_message: Opti @user_event.command(name="close") async def close_voice_channel(self, ctx: Context) -> None: """Close the events voice channel for developers.""" - await self.edit_events_vc(False) + await self.edit_events_vc(open_vc=False) await ctx.send("Voice Channel is now closed.") async def cog_command_error(self, ctx: Context, error: Exception) -> None: From a5efff0ea417f0e9bb2f304eab1abca40493b1fb Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 00:19:04 +0530 Subject: [PATCH 10/26] Move channel check to cog_check() method. --- bot/exts/fun/user_events.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 1aa58d2458..1e7a059c3f 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -279,8 +279,7 @@ async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: @group(name="userevent", invoke_without_command=True) async def user_event(self, ctx: Context) -> None: """Commands to perform CRUD operations on user events and scheduled events.""" - if ctx.channel.id == USER_EVENT_COORDINATORS_CHANNEL: - await ctx.send_help(ctx.command) + await ctx.send_help(ctx.command) @user_event.command(name="create") async def create_user_event(self, ctx: Context, event_name: str, *, event_description: str) -> None: @@ -565,7 +564,10 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: async def cog_check(self, ctx: Context) -> bool: """Allow users with event coordinator role to exec cog commands.""" - return has_role(USER_EVENT_COORD_ROLE).predicate(ctx) + return ( + has_role(USER_EVENT_COORD_ROLE).predicate(ctx) + and ctx.channel.id == USER_EVENT_COORDINATORS_CHANNEL + ) def setup(bot: Bot) -> None: From 4aaae7ed3e5bdb9f4d0d912cebf3ae43d9e6a746 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 00:28:28 +0530 Subject: [PATCH 11/26] Remove embed footer config done in user_event_embed() method and clean up. --- bot/exts/fun/user_events.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 1e7a059c3f..b364fbc4ab 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -58,10 +58,12 @@ async def restart_event_reminders(self) -> None: """Restart scheduled event reminders when bot restarts.""" await self.bot.wait_until_guild_available() scheduled_events = await self.bot.api_client.get("bot/scheduled-events") + for event in scheduled_events: # If event is live if datetime.now() > parse(event["start_time"]).replace(tzinfo=None): self.schedule_event_end_reminder(event) + else: await self.schedule_event_start_reminder(event) @@ -127,7 +129,6 @@ def user_event_embed(event_name: str, event_description: str, organizer: Member, f"**Status:** {status}" ) ) - embed.set_footer(text="Confirm event creation.") return embed async def fetch_subscribers(self, message_id: int) -> list: @@ -209,6 +210,16 @@ async def schedule_event_start_reminder(self, scheduled_event: dict) -> None: self.event_preparation(scheduled_event) ) + def schedule_event_end_reminder(self, scheduled_event: dict) -> None: + """Schedule reminder to remind user about event end.""" + reminder = isoparse(scheduled_event["end_time"]).replace(tzinfo=None) + + self.scheduler.schedule_at( + reminder, + scheduled_event["user_event"]["organizer"], + self.event_end(scheduled_event) + ) + async def send_confirmation_message( self, event_name: str, @@ -217,6 +228,8 @@ async def send_confirmation_message( ) -> Tuple[Message, Embed]: """Send confirmation message for user event creation.""" embed = self.user_event_embed(event_name, event_description, author, self.not_scheduled()) + embed.set_footer(text="Confirm event creation.") + message = await self.user_event_coord_channel.send(embed=embed) await message.add_reaction(EMOJIS["check"]) @@ -224,16 +237,6 @@ async def send_confirmation_message( return message, embed - def schedule_event_end_reminder(self, scheduled_event: dict) -> None: - """Schedule reminder to remind user about event end.""" - reminder = isoparse(scheduled_event["end_time"]).replace(tzinfo=None) - - self.scheduler.schedule_at( - reminder, - scheduled_event["user_event"]["organizer"], - self.event_end(scheduled_event) - ) - async def list_new_event(self, embed: Embed) -> Message: """List new event in the user-events-list channel.""" embed.set_footer(text="React to be notified.") From d8152152d8d90ad5379e9ccc57d6a0dca7ac53c5 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 17:35:07 +0530 Subject: [PATCH 12/26] Update config and constants to house required role and channel IDs. The user events cog file uses the channel and role IDs from the constants file --- bot/constants.py | 5 +++++ bot/exts/fun/user_events.py | 41 +++++++++++++++---------------------- config-default.yml | 15 +++++++++++++- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 4d41f4eb26..84a8f2ca26 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -423,7 +423,10 @@ class Channels(metaclass=YAMLGetter): reddit: int staff_announcements: int talent_pool: int + user_event_list: int user_event_announcements: int + user_event_coordinators: int + user_event_voice: int user_log: int verification: int voice_chat: int @@ -462,6 +465,8 @@ class Roles(metaclass=YAMLGetter): python_community: int sprinters: int team_leaders: int + user_event_coordinator: int + user_event_ongoing: int unverified: int verified: int # This is the Developers role on PyDis, here named verified for readability reasons. voice_verified: int diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index b364fbc4ab..ddb327e347 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -9,9 +9,9 @@ from discord import Embed, Guild, Member, Message, Reaction, Role, TextChannel, VoiceChannel from discord.ext.commands import Cog, CommandInvokeError, Context, group, has_role -from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot +from bot.constants import Channels, Guild as Server, Roles from bot.utils.scheduling import Scheduler from bot.utils.time import humanize_delta @@ -28,18 +28,6 @@ 3: 'rd', 23: 'rd' } -USER_EVENTS_LIST_CHANNEL = 766316218944323594 -USER_EVENT_ANNOUNCEMENTS_CHANNEL = 756769546777395207 - -USER_EVENT_COORDINATORS_CHANNEL = 766318368500219976 -USER_EVENT_COORD_ROLE = 765955273138372618 - -USER_EVENT_ONGOING_ROLE = 766315117104726076 - -USER_EVENT_VOICE_CHANNEL = 766623039692734465 - -DEVELOPERS_ROLE = 756769543237533742 - class UserEvents(Cog): """Manage user events with the provided commands.""" @@ -91,32 +79,37 @@ def scheduled(start_datetime: datetime) -> str: @property def developers_role(self) -> Role: """Return guild developers role.""" - return self.guild.get_role(DEVELOPERS_ROLE) + return self.guild.get_role(Roles.verified) + + @property + def user_event_ongoing_role(self) -> Role: + """Return guild user-event-ongoing role.""" + return self.guild.get_role(Roles.user_event_ongoing) @property def guild(self) -> Guild: """Return guild instance.""" - return self.bot.get_guild(constants.Guild.id) + return self.bot.get_guild(Server.id) @property def user_event_coord_channel(self) -> TextChannel: """Return #user-events-coordinators channel.""" - return self.bot.get_channel(USER_EVENT_COORDINATORS_CHANNEL) + return self.bot.get_channel(Channels.user_event_coordinators) @property def user_event_announcement_channel(self) -> TextChannel: """Return #user-events-announcement channel.""" - return self.bot.get_channel(USER_EVENT_ANNOUNCEMENTS_CHANNEL) + return self.bot.get_channel(Channels.user_event_announcements) @property def user_events_list_channel(self) -> TextChannel: """Return #user-events-list channel.""" - return self.bot.get_channel(USER_EVENTS_LIST_CHANNEL) + return self.bot.get_channel(Channels.user_event_list) @property def user_event_voice_channel(self) -> VoiceChannel: """Return #user-events-voice channel.""" - return self.bot.get_channel(USER_EVENT_VOICE_CHANNEL) + return self.bot.get_channel(Channels.user_event_voice) @staticmethod def user_event_embed(event_name: str, event_description: str, organizer: Member, status: str) -> Embed: @@ -164,9 +157,7 @@ async def event_preparation(self, scheduled_event: dict) -> None: """Notify event organizer 30min before event and add User Event: Ongoing role.""" organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) - if USER_EVENT_ONGOING_ROLE not in [role.id for role in organizer.roles]: - role = self.guild.get_role(USER_EVENT_ONGOING_ROLE) - await organizer.add_roles(role) + await organizer.add_roles(self.user_event_ongoing_role) start_time = isoparse(scheduled_event["start_time"]).replace(tzinfo=None) time_remaining = humanize_delta(relativedelta(start_time, datetime.now())) @@ -181,7 +172,7 @@ async def event_preparation(self, scheduled_event: dict) -> None: async def event_end(self, scheduled_event: dict) -> None: """End user event.""" organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) - role = self.guild.get_role(USER_EVENT_ONGOING_ROLE) + role = self.user_event_ongoing_role await organizer.remove_roles(role) @@ -568,8 +559,8 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: async def cog_check(self, ctx: Context) -> bool: """Allow users with event coordinator role to exec cog commands.""" return ( - has_role(USER_EVENT_COORD_ROLE).predicate(ctx) - and ctx.channel.id == USER_EVENT_COORDINATORS_CHANNEL + has_role(Roles.user_event_coordinator).predicate(ctx) + and ctx.channel.id == Channels.user_event_coordinators ) diff --git a/config-default.yml b/config-default.yml index 2afdcd5947..9405fac287 100644 --- a/config-default.yml +++ b/config-default.yml @@ -140,7 +140,6 @@ guild: python_events: &PYEVENTS_CHANNEL 729674110270963822 mailing_lists: &MAILING_LISTS 704372456592506880 reddit: &REDDIT_CHANNEL 458224812528238616 - user_event_announcements: &USER_EVENT_A 592000283102674944 # Development dev_contrib: &DEV_CONTRIB 635950537262759947 @@ -199,11 +198,17 @@ guild: voice_chat: 412357430186344448 admins_voice: &ADMINS_VOICE 500734494840717332 staff_voice: &STAFF_VOICE 412375055910043655 + user_event_voice: 751592185077563462 # Watch big_brother_logs: &BB_LOGS 468507907357409333 talent_pool: &TALENT_POOL 534321732593647616 + # User events + user_event_list: &USER_EVENT_L 0 + user_event_announcements: &USER_EVENT_A 592000283102674944 + user_event_coordinators: &USER_EVENT_C 1 + moderation_categories: - *MODMAIL - *LOGS @@ -251,6 +256,10 @@ guild: jammers: 737249140966162473 team_leaders: 737250302834638889 + # User Events + user_event_coordinator: 591999763478609927 + user_event_ongoing: 757666374607831221 + moderation_roles: - *OWNERS_ROLE - *ADMINS_ROLE @@ -303,6 +312,8 @@ filter: - *STAFF_LOUNGE - *TALENT_POOL - *USER_EVENT_A + - *USER_EVENT_L + - *USER_EVENT_C role_whitelist: - *ADMINS_ROLE @@ -488,6 +499,8 @@ duck_pond: - *MAILING_LISTS - *REDDIT_CHANNEL - *USER_EVENT_A + - *USER_EVENT_L + - *USER_EVENT_C - *DUCK_POND - *CHANGE_LOG - *STAFF_ANNOUNCEMENTS From 867c55496b7b38d5fd84597ca06b14503c72065b Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 18:51:36 +0530 Subject: [PATCH 13/26] Improve event scheduled message. --- bot/exts/fun/user_events.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index ddb327e347..eb0d5751a9 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -66,14 +66,17 @@ def live() -> str: return "Live" @staticmethod - def scheduled(start_datetime: datetime) -> str: + def scheduled(start_datetime: datetime, end_datetime: datetime) -> str: """To indicate user event is scheduled.""" - readable_datetime = ( + readable_date = ( f"{start_datetime.day}{DATE_PREFIX.get(start_datetime.day, 'th')} " - f"{list(calendar.month_name)[start_datetime.month]}, {start_datetime.year}.\n" - f"{start_datetime.time().strftime('%H:%M:%S')} UTC (24-hour format)" + f"{list(calendar.month_name)[start_datetime.month]} {start_datetime.year}" ) - status = f"Scheduled for\n{readable_datetime}" + readable_time = ( + f"from {start_datetime.time().strftime('%H:%M')} UTC " + f"To {end_datetime.time().strftime('%H:%M')} UTC.\n(24-hour format)" + ) + status = f"Scheduled on {readable_date},\n{readable_time}" return status @property @@ -230,7 +233,7 @@ async def send_confirmation_message( async def list_new_event(self, embed: Embed) -> Message: """List new event in the user-events-list channel.""" - embed.set_footer(text="React to be notified.") + embed.set_footer(text="React to be notified during event start.") # send event embed in #User-events-list channel event_message = await self.user_events_list_channel.send(embed=embed) @@ -350,8 +353,12 @@ async def change_description(self, ctx: Context, event_name: str, *, event_descr ) # If event is scheduled if scheduled_event: + status = self.scheduled( + isoparse(scheduled_event[0]["start_time"]), + isoparse(scheduled_event[0]["end_time"]) + ) await self.update_user_event_message( - status=self.scheduled(isoparse(scheduled_event[0]["start_time"])), + status=status, user_event=scheduled_event[0]["user_event"] ) return @@ -428,18 +435,20 @@ async def schedule_user_event( await ctx.send("Invalid start datetime.") return + end_datetime = start_datetime + timedelta(hours=duration) + # Register scheduled event on site post_data = { "user_event_name": user_event["name"], "start_time": start_datetime.isoformat(), - "end_time": (start_datetime + timedelta(hours=duration)).isoformat() + "end_time": end_datetime.isoformat() } scheduled_event = await self.bot.api_client.post( "bot/scheduled-events", data=post_data ) - status = self.scheduled(start_datetime) + status = self.scheduled(start_datetime, end_datetime) # Send message in #user-events-announcements regarding event schedule embed = Embed( From 375e8aed89de31c3efad1e6f37127742de81ec92 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 19:04:07 +0530 Subject: [PATCH 14/26] Add comments for better code clarity. --- bot/exts/fun/user_events.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index eb0d5751a9..a7ecf020a2 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -67,14 +67,22 @@ def live() -> str: @staticmethod def scheduled(start_datetime: datetime, end_datetime: datetime) -> str: - """To indicate user event is scheduled.""" + """ + To indicate user event is scheduled. + + The following code returns(an example): + + Scheduled on 7th November 2020, + from 17:30 UTC To 18:00 UTC + (24-hour format). + """ readable_date = ( f"{start_datetime.day}{DATE_PREFIX.get(start_datetime.day, 'th')} " f"{list(calendar.month_name)[start_datetime.month]} {start_datetime.year}" ) readable_time = ( f"from {start_datetime.time().strftime('%H:%M')} UTC " - f"To {end_datetime.time().strftime('%H:%M')} UTC.\n(24-hour format)" + f"To {end_datetime.time().strftime('%H:%M')} UTC\n(24-hour format)." ) status = f"Scheduled on {readable_date},\n{readable_time}" return status @@ -129,8 +137,12 @@ def user_event_embed(event_name: str, event_description: str, organizer: Member, async def fetch_subscribers(self, message_id: int) -> list: """Fetch reacted users to event message as subscribers.""" + # Fetch the event message message = await self.user_events_list_channel.fetch_message(message_id) + reaction = message.reactions[0] + + # Flatten into a list users = await reaction.users().flatten() return users @@ -146,7 +158,9 @@ async def sync_subscribers(self, event_name: str, sub_ids: list) -> None: async def update_user_event_message(self, status: str, user_event: dict) -> None: """Update event message on #user-events-list channel.""" + # Fetch user event message message = await self.user_events_list_channel.fetch_message(user_event["message_id"]) + embed = self.user_event_embed( user_event["name"], user_event["description"], @@ -154,16 +168,20 @@ async def update_user_event_message(self, status: str, user_event: dict) -> None status ) embed.set_footer(text="React to be notified.") + await message.edit(embed=embed) async def event_preparation(self, scheduled_event: dict) -> None: """Notify event organizer 30min before event and add User Event: Ongoing role.""" organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) + # Add the `user event: ongoing` role to the organizer await organizer.add_roles(self.user_event_ongoing_role) + # Calculate remaining time for event start start_time = isoparse(scheduled_event["start_time"]).replace(tzinfo=None) time_remaining = humanize_delta(relativedelta(start_time, datetime.now())) + await self.user_event_coord_channel.send( f"{organizer.mention} Event to start in {time_remaining}. " f"use `!userevent announce` " @@ -175,16 +193,19 @@ async def event_preparation(self, scheduled_event: dict) -> None: async def event_end(self, scheduled_event: dict) -> None: """End user event.""" organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) - role = self.user_event_ongoing_role - await organizer.remove_roles(role) + # Remove the `user event: ongoing` role to the organizer + await organizer.remove_roles(self.user_event_ongoing_role) status = self.not_scheduled() await self.update_user_event_message(status, scheduled_event["user_event"]) + # Close user events voice channel await self.edit_events_vc(open_vc=False) await self.user_event_coord_channel.send(f"{organizer.mention} event has ended! Voice channel is now closed.") + + # cancel the scheduler event and DELETE on site await self._cancel_scheduled_event(scheduled_event) async def schedule_event_start_reminder(self, scheduled_event: dict) -> None: @@ -486,6 +507,7 @@ async def cancel_scheduled_event(self, ctx: Context) -> None: await ctx.send("You do not have an event scheduled.") return + # cancel the scheduler and DELETE on site await self._cancel_scheduled_event(scheduled_event[0]) await ctx.send(f"{scheduled_event[0]['user_event']['name']} event is cancelled.") @@ -549,7 +571,6 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: """Handle ResponseCodeError locally.""" # Custom errors are raised via the CommandInvokeError if isinstance(error, CommandInvokeError): - if isinstance(error.original, ResponseCodeError): # Parse 400 error responses from site From 2158228aeba522e59ce0162a84ceabc298816dc0 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 19:52:51 +0530 Subject: [PATCH 15/26] Add bot responses after command execution. --- bot/exts/fun/user_events.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index a7ecf020a2..771ed1d37c 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -390,6 +390,8 @@ async def change_description(self, ctx: Context, event_name: str, *, event_descr user_event=user_event ) + await ctx.send("Event description updated.") + @user_event.command(name="delete") async def delete_user_event(self, ctx: Context, event_name: str) -> None: """Delete user event.""" @@ -490,6 +492,8 @@ async def schedule_user_event( # Set start reminders for scheduled event organizer. await self.schedule_event_start_reminder(scheduled_event) + await ctx.send("Event scheduled.") + @user_event.command(name="cancel") async def cancel_scheduled_event(self, ctx: Context) -> None: """Cancel a scheduled event.""" From 73174ad3787e425de6c63878a97ef56a47d3a99d Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 20:12:25 +0530 Subject: [PATCH 16/26] Simplify code. --- bot/exts/fun/user_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 771ed1d37c..9ab69d6f27 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -436,7 +436,7 @@ async def schedule_user_event( """ Schedule a user event at a particular date and time. - The time should be in UTC and 24hour format. + The time should be in UTC. Default duration is 3 hours. Examples: @@ -557,7 +557,7 @@ async def announce_event_start(self, ctx: Context, *, announcement_message: Opti message = subscribers if announcement_message: - message = announcement_message + subscribers + message += announcement_message # Update event status status = self.live() From 8b6b6a3b660187584273b9157448b63d8cbe4e3f Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 23:02:50 +0530 Subject: [PATCH 17/26] Add user_event_list and user_event_coordinators channel ID to config. --- config-default.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-default.yml b/config-default.yml index 9405fac287..df2a6b8daf 100644 --- a/config-default.yml +++ b/config-default.yml @@ -205,9 +205,9 @@ guild: talent_pool: &TALENT_POOL 534321732593647616 # User events - user_event_list: &USER_EVENT_L 0 + user_event_list: &USER_EVENT_L 774324592374317077 user_event_announcements: &USER_EVENT_A 592000283102674944 - user_event_coordinators: &USER_EVENT_C 1 + user_event_coordinators: &USER_EVENT_C 752260285951377429 moderation_categories: - *MODMAIL From 77e09c02701f654b6d9571051899c70eb07622b7 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 23:44:09 +0530 Subject: [PATCH 18/26] Remove subscription syncing functionality with the site. --- bot/exts/fun/user_events.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 9ab69d6f27..fce5aec316 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -146,16 +146,6 @@ async def fetch_subscribers(self, message_id: int) -> list: users = await reaction.users().flatten() return users - async def sync_subscribers(self, event_name: str, sub_ids: list) -> None: - """Sync subscribers with the site.""" - data = { - "subscriptions": sub_ids - } - await self.bot.api_client.patch( - f"bot/user-events/{event_name}", - data=data - ) - async def update_user_event_message(self, status: str, user_event: dict) -> None: """Update event message on #user-events-list channel.""" # Fetch user event message @@ -546,12 +536,6 @@ async def announce_event_start(self, ctx: Context, *, announcement_message: Opti and not sub.bot ] - # Sync subscribers with the site - await self.sync_subscribers( - scheduled_event[0]["user_event"]["name"], - [sub.id for sub in subscribers] - ) - # Send message in #user-event-announcements channel subscribers = "".join(sub.mention for sub in subscribers) message = subscribers From 008b615bc27b78de0c2602a8be4315282e774e54 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 6 Nov 2020 23:57:55 +0530 Subject: [PATCH 19/26] Remove `user event: ongoing` role when the !userevent cancel cmd is used. --- bot/exts/fun/user_events.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index fce5aec316..75e39bc7a1 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -184,7 +184,7 @@ async def event_end(self, scheduled_event: dict) -> None: """End user event.""" organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) - # Remove the `user event: ongoing` role to the organizer + # Remove the `user event: ongoing` role from the organizer await organizer.remove_roles(self.user_event_ongoing_role) status = self.not_scheduled() @@ -284,6 +284,16 @@ async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: status = self.not_scheduled() await self.update_user_event_message(status, scheduled_event["user_event"]) + # Remove the `user event: ongoing` role from the organizer incase + # the event is canceled using the cancel command when it is Live as + # the organizer decides to stop the event early + + # It is not required to use the cancel command to stop the event though, + # the ending timer, when done, will remove the role anyway + organizer = self.guild.get_member(scheduled_event["user_event"]["organizer"]) + + await organizer.remove_roles(self.user_event_ongoing_role) + @group(name="userevent", invoke_without_command=True) async def user_event(self, ctx: Context) -> None: """Commands to perform CRUD operations on user events and scheduled events.""" From 1e5e6ce8a4d22772254880658590072eab2cf2af Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sat, 7 Nov 2020 20:56:42 +0530 Subject: [PATCH 20/26] Look for specifically for check_mark reaction while fetching subscribers from event message. --- bot/exts/fun/user_events.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 75e39bc7a1..5d60335134 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -140,11 +140,16 @@ async def fetch_subscribers(self, message_id: int) -> list: # Fetch the event message message = await self.user_events_list_channel.fetch_message(message_id) - reaction = message.reactions[0] + for reaction in message.reactions: + # The `check` reaction will be added by the bot during event creation + # So the chances of it being removed or not present is negligible - # Flatten into a list - users = await reaction.users().flatten() - return users + if str(reaction) == EMOJIS["check"]: + # Flatten into a list + users = await reaction.users().flatten() + return users + + return [] async def update_user_event_message(self, status: str, user_event: dict) -> None: """Update event message on #user-events-list channel.""" From e837d7b32d42f5f63f062712ffe7e67282423c4e Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sat, 7 Nov 2020 21:03:31 +0530 Subject: [PATCH 21/26] Use constants for Not scheduled and Live statuses. --- bot/exts/fun/user_events.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 5d60335134..76e4fa85cb 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -28,6 +28,9 @@ 3: 'rd', 23: 'rd' } +NOT_SCHEDULED = "Not scheduled" +LIVE = "Live" + class UserEvents(Cog): """Manage user events with the provided commands.""" @@ -162,7 +165,7 @@ async def update_user_event_message(self, status: str, user_event: dict) -> None self.guild.get_member(user_event["organizer"]), status ) - embed.set_footer(text="React to be notified.") + embed.set_footer(text="React to get notified about event start.") await message.edit(embed=embed) @@ -192,7 +195,7 @@ async def event_end(self, scheduled_event: dict) -> None: # Remove the `user event: ongoing` role from the organizer await organizer.remove_roles(self.user_event_ongoing_role) - status = self.not_scheduled() + status = NOT_SCHEDULED await self.update_user_event_message(status, scheduled_event["user_event"]) # Close user events voice channel @@ -237,7 +240,7 @@ async def send_confirmation_message( author: Member ) -> Tuple[Message, Embed]: """Send confirmation message for user event creation.""" - embed = self.user_event_embed(event_name, event_description, author, self.not_scheduled()) + embed = self.user_event_embed(event_name, event_description, author, NOT_SCHEDULED) embed.set_footer(text="Confirm event creation.") message = await self.user_event_coord_channel.send(embed=embed) @@ -286,7 +289,7 @@ async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: await self.bot.api_client.delete(f"bot/scheduled-events/{scheduled_event['id']}") # Update user event status - status = self.not_scheduled() + status = NOT_SCHEDULED await self.update_user_event_message(status, scheduled_event["user_event"]) # Remove the `user event: ongoing` role from the organizer incase @@ -391,7 +394,7 @@ async def change_description(self, ctx: Context, event_name: str, *, event_descr # If event is not scheduled await self.update_user_event_message( - status=self.not_scheduled(), + status=NOT_SCHEDULED, user_event=user_event ) @@ -559,7 +562,7 @@ async def announce_event_start(self, ctx: Context, *, announcement_message: Opti message += announcement_message # Update event status - status = self.live() + status = LIVE await self.update_user_event_message(status, scheduled_event[0]["user_event"]) await self.user_event_announcement_channel.send(message) From 5cd39cce06caa9510723b28bb6a27a76ade4619a Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sat, 7 Nov 2020 21:05:55 +0530 Subject: [PATCH 22/26] Use decorator instead of function to check if user is event organizer. --- bot/exts/fun/user_events.py | 44 +++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 76e4fa85cb..7d856dcf52 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -2,7 +2,8 @@ import calendar import logging from datetime import datetime, timedelta -from typing import Optional, Tuple +from functools import wraps +from typing import Callable, Optional, Tuple from dateutil.parser import isoparse, parse from dateutil.relativedelta import relativedelta @@ -32,6 +33,21 @@ LIVE = "Live" +def is_event_organizer(func: Callable) -> Callable: + """Check if the user is event organizer.""" + @wraps(func) + async def wrapper(self: Cog, ctx: Context, event_name: str, *args, **kwargs) -> None: + user_event = await self.bot.api_client.get(f"bot/user-events/{event_name}") + + if user_event["organizer"] != ctx.author.id: + await ctx.send(f"You are not the organizer of the event **{event_name}**.") + return + + await func(self, ctx, event_name, *args, **kwargs) + + return wrapper + + class UserEvents(Cog): """Manage user events with the provided commands.""" @@ -271,15 +287,6 @@ async def edit_events_vc(self, open_vc: bool) -> None: speak=open_vc ) - async def check_if_user_is_organizer(self, user_id: int, event_name: str) -> Optional[dict]: - """Check if user is the organizer of an event.""" - # This will automatically raise error if event does not exist - user_event = await self.bot.api_client.get(f"bot/user-events/{event_name}") - - if user_event["organizer"] != user_id: - return None - return user_event - async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: """Cancel a scheduled event.""" # Remove scheduler related to the scheduled event @@ -355,12 +362,9 @@ def check(reaction: Reaction, user: Member) -> bool: await ctx.send(f"User Event **{event_name}** created.") @user_event.command(name="change_desc", aliases=["cd", "desc"]) + @is_event_organizer async def change_description(self, ctx: Context, event_name: str, *, event_description: str) -> None: """Change user event description.""" - # Check if user is event organizer - if not self.check_if_user_is_organizer(ctx.author.id, event_name): - await ctx.send("You can only modify your events!") - return data = { "description": event_description } @@ -401,16 +405,14 @@ async def change_description(self, ctx: Context, event_name: str, *, event_descr await ctx.send("Event description updated.") @user_event.command(name="delete") + @is_event_organizer async def delete_user_event(self, ctx: Context, event_name: str) -> None: """Delete user event.""" - # Check if user is event organizer - user_event = await self.check_if_user_is_organizer(ctx.author.id, event_name) - if not user_event: - await ctx.send("You can only delete your events!") - return - # Check if the event is scheduled + user_event = await self.bot.api_client.get(f"bot/user-events/{event_name}") + query_params = { + "user_event__name": user_event["name"], "user_event__organizer": ctx.author.id } scheduled_event = await self.bot.api_client.get( @@ -595,7 +597,7 @@ async def cog_command_error(self, ctx: Context, error: Exception) -> None: async def cog_check(self, ctx: Context) -> bool: """Allow users with event coordinator role to exec cog commands.""" return ( - has_role(Roles.user_event_coordinator).predicate(ctx) + await has_role(Roles.user_event_coordinator).predicate(ctx) and ctx.channel.id == Channels.user_event_coordinators ) From ec45fcf78346785709cf7c7aa71653a0cd8f9b7d Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sat, 21 Nov 2020 23:25:36 +0530 Subject: [PATCH 23/26] Remove redundant status methods. --- bot/exts/fun/user_events.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 7d856dcf52..ccfea52196 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -74,16 +74,6 @@ async def restart_event_reminders(self) -> None: else: await self.schedule_event_start_reminder(event) - @staticmethod - def not_scheduled() -> str: - """To indicate user event is not scheduled.""" - return "Not scheduled" - - @staticmethod - def live() -> str: - """To indicate user event is live.""" - return "Live" - @staticmethod def scheduled(start_datetime: datetime, end_datetime: datetime) -> str: """ From f06c52c61196fd8764eb24e5acd206b6646f3549 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sat, 21 Nov 2020 23:26:49 +0530 Subject: [PATCH 24/26] Normalize usage of update_user_event_message() method. --- bot/exts/fun/user_events.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index ccfea52196..815a1fd9ef 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -201,8 +201,7 @@ async def event_end(self, scheduled_event: dict) -> None: # Remove the `user event: ongoing` role from the organizer await organizer.remove_roles(self.user_event_ongoing_role) - status = NOT_SCHEDULED - await self.update_user_event_message(status, scheduled_event["user_event"]) + await self.update_user_event_message(NOT_SCHEDULED, scheduled_event["user_event"]) # Close user events voice channel await self.edit_events_vc(open_vc=False) @@ -286,8 +285,8 @@ async def _cancel_scheduled_event(self, scheduled_event: dict) -> None: await self.bot.api_client.delete(f"bot/scheduled-events/{scheduled_event['id']}") # Update user event status - status = NOT_SCHEDULED - await self.update_user_event_message(status, scheduled_event["user_event"]) + + await self.update_user_event_message(NOT_SCHEDULED, scheduled_event["user_event"]) # Remove the `user event: ongoing` role from the organizer incase # the event is canceled using the cancel command when it is Live as @@ -380,17 +379,11 @@ async def change_description(self, ctx: Context, event_name: str, *, event_descr isoparse(scheduled_event[0]["start_time"]), isoparse(scheduled_event[0]["end_time"]) ) - await self.update_user_event_message( - status=status, - user_event=scheduled_event[0]["user_event"] - ) + await self.update_user_event_message(status, scheduled_event[0]["user_event"]) return # If event is not scheduled - await self.update_user_event_message( - status=NOT_SCHEDULED, - user_event=user_event - ) + await self.update_user_event_message(NOT_SCHEDULED, user_event) await ctx.send("Event description updated.") @@ -554,8 +547,7 @@ async def announce_event_start(self, ctx: Context, *, announcement_message: Opti message += announcement_message # Update event status - status = LIVE - await self.update_user_event_message(status, scheduled_event[0]["user_event"]) + await self.update_user_event_message(LIVE, scheduled_event[0]["user_event"]) await self.user_event_announcement_channel.send(message) From 9829c877d384c2d076cfaed329154b22e2a903bd Mon Sep 17 00:00:00 2001 From: Rohan Date: Mon, 23 Nov 2020 18:41:50 +0530 Subject: [PATCH 25/26] Load discord guild, channel and role objects in __init__() method. --- bot/exts/fun/user_events.py | 70 +++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 815a1fd9ef..804d13a67a 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -7,12 +7,12 @@ from dateutil.parser import isoparse, parse from dateutil.relativedelta import relativedelta -from discord import Embed, Guild, Member, Message, Reaction, Role, TextChannel, VoiceChannel +from discord import Embed, Member, Message, Reaction, Role, TextChannel, VoiceChannel from discord.ext.commands import Cog, CommandInvokeError, Context, group, has_role from bot.api import ResponseCodeError from bot.bot import Bot -from bot.constants import Channels, Guild as Server, Roles +from bot.constants import Channels, Guild, Roles from bot.utils.scheduling import Scheduler from bot.utils.time import humanize_delta @@ -55,12 +55,42 @@ def __init__(self, bot: Bot) -> None: self.bot = bot self.scheduler = Scheduler(self.__class__.__name__) + self.guild = None + + # Load required channels. + self.user_event_coord_channel = None + self.user_event_announcement_channel = None + self.user_events_list_channel = None + self.user_event_voice_channel = None + + # Load required roles. + self.developers_role = None + self.user_event_ongoing_role = None + + self.bot.loop.create_task(self.load_required_assets()) self.bot.loop.create_task(self.restart_event_reminders()) def cog_unload(self) -> None: """Cancel scheduled tasks.""" self.scheduler.cancel_all() + async def load_required_assets(self): + """Load discord guild components required by this cog.""" + await self.bot.wait_until_guild_available() + + # Get guild + self.guild = self.bot.get_guild(Guild.id) + + # Load required channels. + self.user_event_coord_channel = self.bot.get_channel(Channels.user_event_coordinators) + self.user_event_announcement_channel = self.bot.get_channel(Channels.user_event_announcements) + self.user_events_list_channel = self.bot.get_channel(Channels.user_event_list) + self.user_event_voice_channel = self.bot.get_channel(Channels.user_event_voice) + + # Load required roles. + self.developers_role = self.guild.get_role(Roles.verified) + self.user_event_ongoing_role = self.guild.get_role(Roles.user_event_ongoing) + async def restart_event_reminders(self) -> None: """Restart scheduled event reminders when bot restarts.""" await self.bot.wait_until_guild_available() @@ -70,7 +100,6 @@ async def restart_event_reminders(self) -> None: # If event is live if datetime.now() > parse(event["start_time"]).replace(tzinfo=None): self.schedule_event_end_reminder(event) - else: await self.schedule_event_start_reminder(event) @@ -96,41 +125,6 @@ def scheduled(start_datetime: datetime, end_datetime: datetime) -> str: status = f"Scheduled on {readable_date},\n{readable_time}" return status - @property - def developers_role(self) -> Role: - """Return guild developers role.""" - return self.guild.get_role(Roles.verified) - - @property - def user_event_ongoing_role(self) -> Role: - """Return guild user-event-ongoing role.""" - return self.guild.get_role(Roles.user_event_ongoing) - - @property - def guild(self) -> Guild: - """Return guild instance.""" - return self.bot.get_guild(Server.id) - - @property - def user_event_coord_channel(self) -> TextChannel: - """Return #user-events-coordinators channel.""" - return self.bot.get_channel(Channels.user_event_coordinators) - - @property - def user_event_announcement_channel(self) -> TextChannel: - """Return #user-events-announcement channel.""" - return self.bot.get_channel(Channels.user_event_announcements) - - @property - def user_events_list_channel(self) -> TextChannel: - """Return #user-events-list channel.""" - return self.bot.get_channel(Channels.user_event_list) - - @property - def user_event_voice_channel(self) -> VoiceChannel: - """Return #user-events-voice channel.""" - return self.bot.get_channel(Channels.user_event_voice) - @staticmethod def user_event_embed(event_name: str, event_description: str, organizer: Member, status: str) -> Embed: """Embed representing a user event.""" From 4ba3fc10936320987fe928e35181404d11702864 Mon Sep 17 00:00:00 2001 From: Rohan Date: Mon, 23 Nov 2020 19:00:15 +0530 Subject: [PATCH 26/26] Fix lint errors. --- bot/exts/fun/user_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/fun/user_events.py b/bot/exts/fun/user_events.py index 804d13a67a..b3080676b8 100644 --- a/bot/exts/fun/user_events.py +++ b/bot/exts/fun/user_events.py @@ -7,7 +7,7 @@ from dateutil.parser import isoparse, parse from dateutil.relativedelta import relativedelta -from discord import Embed, Member, Message, Reaction, Role, TextChannel, VoiceChannel +from discord import Embed, Member, Message, Reaction from discord.ext.commands import Cog, CommandInvokeError, Context, group, has_role from bot.api import ResponseCodeError @@ -74,7 +74,7 @@ def cog_unload(self) -> None: """Cancel scheduled tasks.""" self.scheduler.cancel_all() - async def load_required_assets(self): + async def load_required_assets(self) -> None: """Load discord guild components required by this cog.""" await self.bot.wait_until_guild_available()