From e459d8a1533888441e78d7adde332f862ca2eb93 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Fri, 30 Jan 2026 17:29:41 -0500 Subject: [PATCH 01/18] feat(event): Initial event cog commit --- capy_discord/exts/event/event.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 capy_discord/exts/event/event.py diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py new file mode 100644 index 0000000..e69de29 From a906b044024670c5f7b8b91dbc0090d93e838a87 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Tue, 3 Feb 2026 12:48:00 -0500 Subject: [PATCH 02/18] feature(event): create __init__.py and _schemas.py files for event cog --- capy_discord/exts/event/__init__.py | 1 + capy_discord/exts/event/_schemas.py | 0 2 files changed, 1 insertion(+) create mode 100644 capy_discord/exts/event/__init__.py create mode 100644 capy_discord/exts/event/_schemas.py diff --git a/capy_discord/exts/event/__init__.py b/capy_discord/exts/event/__init__.py new file mode 100644 index 0000000..7a34749 --- /dev/null +++ b/capy_discord/exts/event/__init__.py @@ -0,0 +1 @@ +"""Event management module.""" diff --git a/capy_discord/exts/event/_schemas.py b/capy_discord/exts/event/_schemas.py new file mode 100644 index 0000000..e69de29 From a55929474688a115cec464444321698beef20225 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Tue, 3 Feb 2026 13:04:46 -0500 Subject: [PATCH 03/18] feature(event): created event class --- capy_discord/exts/event/event.py | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index e69de29..5e6265e 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -0,0 +1,34 @@ +import logging + +import discord +from discord import app_commands +from discord.ext import commands + + +class Event(commands.Cog): + """Cog for event-related commands.""" + + def __init__(self, bot: commands.Bot) -> None: + """Initialize the Event cog.""" + self.bot = bot + self.log = logging.getLogger(__name__) + self.log.info("Event cog initialized") + + @app_commands.command(name="event", description="Manage events") + @app_commands.describe(action="The action to perform with events") + @app_commands.choices( + action=[ + app_commands.Choice(name="create", value="create"), + app_commands.Choice(name="update", value="update"), + app_commands.Choice(name="show", value="show"), + app_commands.Choice(name="delete", value="delete"), + ] + ) + async def event(self, interaction: discord.Interaction, action: app_commands.Choice[str]) -> None: + """Manage events based on the action specified.""" + pass + + +async def setup(bot: commands.Bot) -> None: + """Set up the Event cog.""" + await bot.add_cog(Event(bot)) From 543239aaf3bf2281155a461231a2b06ceec4c519 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Tue, 3 Feb 2026 13:20:12 -0500 Subject: [PATCH 04/18] implement event schemas --- capy_discord/exts/event/_schemas.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/capy_discord/exts/event/_schemas.py b/capy_discord/exts/event/_schemas.py index e69de29..c5c61f4 100644 --- a/capy_discord/exts/event/_schemas.py +++ b/capy_discord/exts/event/_schemas.py @@ -0,0 +1,19 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + +from pydantic import BaseModel, Field + + +class EventSchema(BaseModel): + """Pydantic model defining the Event schema and validation rules.""" + + event_name: str = Field(title="Event Name", description="Name of the event", max_length=100) + event_date: datetime = Field( + title="Event Date", + description="Date and time of the event in UTC", + default_factory=lambda: datetime.now(tz=ZoneInfo("UTC")), + ) + location: str = Field(title="Location", description="Location of the event", max_length=200, default="") + description: str = Field( + title="Description", description="Detailed description of the event", max_length=1000, default="" + ) From d1aa4165ca1ee35a0a366050dd4c0bb3c7974435 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Tue, 3 Feb 2026 14:58:59 -0500 Subject: [PATCH 05/18] feature(event): add additonal actions and action handling. --- capy_discord/exts/event/event.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 5e6265e..083d995 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -22,11 +22,25 @@ def __init__(self, bot: commands.Bot) -> None: app_commands.Choice(name="update", value="update"), app_commands.Choice(name="show", value="show"), app_commands.Choice(name="delete", value="delete"), + app_commands.Choice(name="list", value="list"), + app_commands.Choice(name="announce", value="announce"), ] ) async def event(self, interaction: discord.Interaction, action: app_commands.Choice[str]) -> None: """Manage events based on the action specified.""" - pass + match action.value: + case "create": + await interaction.response.send_message("Create event action") + case "update": + await interaction.response.send_message("Update event action") + case "show": + await interaction.response.send_message("Show event action") + case "delete": + await interaction.response.send_message("Delete event action") + case "list": + await interaction.response.send_message("List events action") + case "announce": + await interaction.response.send_message("Announce event action") async def setup(bot: commands.Bot) -> None: From 03f6eefb8e83050630490990179d42c1faefd76c Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Tue, 3 Feb 2026 15:24:27 -0500 Subject: [PATCH 06/18] feature(event): created placeholder action handling functions --- capy_discord/exts/event/event.py | 36 ++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 083d995..2958898 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -30,17 +30,41 @@ async def event(self, interaction: discord.Interaction, action: app_commands.Cho """Manage events based on the action specified.""" match action.value: case "create": - await interaction.response.send_message("Create event action") + await self.create_event(interaction) case "update": - await interaction.response.send_message("Update event action") + await self.update_event(interaction) case "show": - await interaction.response.send_message("Show event action") + await self.show_event(interaction) case "delete": - await interaction.response.send_message("Delete event action") + await self.delete_event(interaction) case "list": - await interaction.response.send_message("List events action") + await self.list_events(interaction) case "announce": - await interaction.response.send_message("Announce event action") + await self.announce_event(interaction) + + async def create_event(self, interaction: discord.Interaction) -> None: + """Handle event creation.""" + await interaction.response.send_message("Event created successfully.") + + async def update_event(self, interaction: discord.Interaction) -> None: + """Handle event updating.""" + await interaction.response.send_message("Event updated successfully.") + + async def show_event(self, interaction: discord.Interaction) -> None: + """Handle showing event details.""" + await interaction.response.send_message("Event details displayed.") + + async def delete_event(self, interaction: discord.Interaction) -> None: + """Handle event deletion.""" + await interaction.response.send_message("Event deleted successfully.") + + async def list_events(self, interaction: discord.Interaction) -> None: + """Handle listing all events.""" + await interaction.response.send_message("List of events displayed.") + + async def announce_event(self, interaction: discord.Interaction) -> None: + """Handle announcing an event.""" + await interaction.response.send_message("Event announced successfully.") async def setup(bot: commands.Bot) -> None: From e1c4e9d3c6ac61e8e43d3de053b76f76c4df804a Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Tue, 3 Feb 2026 15:55:30 -0500 Subject: [PATCH 07/18] refactor(event): renamed action functions --- capy_discord/exts/event/event.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 2958898..60c1974 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -30,39 +30,39 @@ async def event(self, interaction: discord.Interaction, action: app_commands.Cho """Manage events based on the action specified.""" match action.value: case "create": - await self.create_event(interaction) + await self.handle_create_action(interaction) case "update": - await self.update_event(interaction) + await self.handle_update_action(interaction) case "show": - await self.show_event(interaction) + await self.handle_show_action(interaction) case "delete": - await self.delete_event(interaction) + await self.handle_delete_action(interaction) case "list": - await self.list_events(interaction) + await self.handle_list_action(interaction) case "announce": - await self.announce_event(interaction) + await self.handle_announce_action(interaction) - async def create_event(self, interaction: discord.Interaction) -> None: + async def handle_create_action(self, interaction: discord.Interaction) -> None: """Handle event creation.""" await interaction.response.send_message("Event created successfully.") - async def update_event(self, interaction: discord.Interaction) -> None: + async def handle_update_action(self, interaction: discord.Interaction) -> None: """Handle event updating.""" await interaction.response.send_message("Event updated successfully.") - async def show_event(self, interaction: discord.Interaction) -> None: + async def handle_show_action(self, interaction: discord.Interaction) -> None: """Handle showing event details.""" await interaction.response.send_message("Event details displayed.") - async def delete_event(self, interaction: discord.Interaction) -> None: + async def handle_delete_action(self, interaction: discord.Interaction) -> None: """Handle event deletion.""" await interaction.response.send_message("Event deleted successfully.") - async def list_events(self, interaction: discord.Interaction) -> None: + async def handle_list_action(self, interaction: discord.Interaction) -> None: """Handle listing all events.""" await interaction.response.send_message("List of events displayed.") - async def announce_event(self, interaction: discord.Interaction) -> None: + async def handle_announce_action(self, interaction: discord.Interaction) -> None: """Handle announcing an event.""" await interaction.response.send_message("Event announced successfully.") From b5a9db5cba788c4c10ed30b6da2ac24babe5b841 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Tue, 3 Feb 2026 16:45:55 -0500 Subject: [PATCH 08/18] feature(event): implemented logic for handling event creation --- capy_discord/exts/event/event.py | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 60c1974..bb25b8b 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -1,9 +1,16 @@ import logging +from datetime import datetime +from zoneinfo import ZoneInfo import discord from discord import app_commands from discord.ext import commands +from capy_discord.ui.embeds import error_embed, success_embed +from capy_discord.ui.forms import ModelModal + +from ._schemas import EventSchema + class Event(commands.Cog): """Cog for event-related commands.""" @@ -13,6 +20,8 @@ def __init__(self, bot: commands.Bot) -> None: self.bot = bot self.log = logging.getLogger(__name__) self.log.info("Event cog initialized") + # In-memory storage for demonstration. + self.events: dict[int, list[EventSchema]] = {} @app_commands.command(name="event", description="Manage events") @app_commands.describe(action="The action to perform with events") @@ -44,7 +53,14 @@ async def event(self, interaction: discord.Interaction, action: app_commands.Cho async def handle_create_action(self, interaction: discord.Interaction) -> None: """Handle event creation.""" - await interaction.response.send_message("Event created successfully.") + self.log.info("Opening event creation modal for %s", interaction.user) + + modal = ModelModal( + model_cls=EventSchema, + callback=self._handle_event_submit, + title="Create Event", + ) + await interaction.response.send_modal(modal) async def handle_update_action(self, interaction: discord.Interaction) -> None: """Handle event updating.""" @@ -66,6 +82,38 @@ async def handle_announce_action(self, interaction: discord.Interaction) -> None """Handle announcing an event.""" await interaction.response.send_message("Event announced successfully.") + async def _handle_event_submit(self, interaction: discord.Interaction, event: EventSchema) -> None: + """Process the valid event submission.""" + guild_id = interaction.guild_id + if not guild_id: + embed = error_embed("No Server", "Events must be created in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Save event + self.events.setdefault(guild_id, []).append(event) + + self.log.info("Created event '%s' for guild %s", event.event_name, guild_id) + + embed = self._create_event_embed(event) + success = success_embed("Event Created", "Your event has been created successfully!") + await interaction.response.send_message(embeds=[success, embed], ephemeral=True) + + def _create_event_embed(self, event: EventSchema) -> discord.Embed: + """Helper to build the event display embed.""" + embed = discord.Embed(title=event.event_name, description=event.description) + + event_time = event.event_date + if event_time.tzinfo is None: + event_time = event_time.replace(tzinfo=ZoneInfo("UTC")) + + embed.add_field(name="Date/Time", value=event_time.strftime("%Y-%m-%d %I:%M %p %Z"), inline=True) + embed.add_field(name="Location", value=event.location or "TBD", inline=True) + + now = datetime.now(ZoneInfo("UTC")).strftime("%Y-%m-%d %H:%M") + embed.set_footer(text=f"Created: {now}") + return embed + async def setup(bot: commands.Bot) -> None: """Set up the Event cog.""" From 2c43a58251db017a035fd696df85d50e35c7d8e3 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Tue, 3 Feb 2026 17:33:39 -0500 Subject: [PATCH 09/18] fix(event): Seperated event and date inputs. Implemented timestamps. Better timezone handling. --- capy_discord/exts/event/_schemas.py | 36 ++++++++++++++++++++++++----- capy_discord/exts/event/event.py | 20 ++++++++-------- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/capy_discord/exts/event/_schemas.py b/capy_discord/exts/event/_schemas.py index c5c61f4..4f021af 100644 --- a/capy_discord/exts/event/_schemas.py +++ b/capy_discord/exts/event/_schemas.py @@ -1,19 +1,43 @@ -from datetime import datetime -from zoneinfo import ZoneInfo +from datetime import date, datetime, time -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator class EventSchema(BaseModel): """Pydantic model defining the Event schema and validation rules.""" event_name: str = Field(title="Event Name", description="Name of the event", max_length=100) - event_date: datetime = Field( + event_date: date = Field( title="Event Date", - description="Date and time of the event in UTC", - default_factory=lambda: datetime.now(tz=ZoneInfo("UTC")), + description="Date of the event (MM-DD-YYYY)", + default_factory=date.today, + ) + event_time: time = Field( + title="Event Time", + description="Time of the event (HH:MM, 24-hour) or (HH:MM AM/PM)", + default_factory=lambda: datetime.now().astimezone().time(), ) location: str = Field(title="Location", description="Location of the event", max_length=200, default="") description: str = Field( title="Description", description="Detailed description of the event", max_length=1000, default="" ) + + @field_validator("event_date", mode="before") + @classmethod + def _parse_event_date(cls, value: object) -> date | object: + if isinstance(value, str): + value = value.strip() + return datetime.strptime(f"{value} +0000", "%m-%d-%Y %z").date() + return value + + @field_validator("event_time", mode="before") + @classmethod + def _parse_event_time(cls, value: object) -> time | object: + if isinstance(value, str): + value = value.strip() + if " " in value: + parsed = datetime.strptime(f"{value} +0000", "%I:%M %p %z") + else: + parsed = datetime.strptime(f"{value} +0000", "%H:%M %z") + return parsed.timetz().replace(tzinfo=None) + return value diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index bb25b8b..7741b81 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -28,7 +28,7 @@ def __init__(self, bot: commands.Bot) -> None: @app_commands.choices( action=[ app_commands.Choice(name="create", value="create"), - app_commands.Choice(name="update", value="update"), + app_commands.Choice(name="edit", value="edit"), app_commands.Choice(name="show", value="show"), app_commands.Choice(name="delete", value="delete"), app_commands.Choice(name="list", value="list"), @@ -40,8 +40,8 @@ async def event(self, interaction: discord.Interaction, action: app_commands.Cho match action.value: case "create": await self.handle_create_action(interaction) - case "update": - await self.handle_update_action(interaction) + case "edit": + await self.handle_edit_action(interaction) case "show": await self.handle_show_action(interaction) case "delete": @@ -62,9 +62,9 @@ async def handle_create_action(self, interaction: discord.Interaction) -> None: ) await interaction.response.send_modal(modal) - async def handle_update_action(self, interaction: discord.Interaction) -> None: - """Handle event updating.""" - await interaction.response.send_message("Event updated successfully.") + async def handle_edit_action(self, interaction: discord.Interaction) -> None: + """Handle event editing.""" + await interaction.response.send_message("Event edited successfully.") async def handle_show_action(self, interaction: discord.Interaction) -> None: """Handle showing event details.""" @@ -103,11 +103,13 @@ def _create_event_embed(self, event: EventSchema) -> discord.Embed: """Helper to build the event display embed.""" embed = discord.Embed(title=event.event_name, description=event.description) - event_time = event.event_date + event_time = datetime.combine(event.event_date, event.event_time) if event_time.tzinfo is None: - event_time = event_time.replace(tzinfo=ZoneInfo("UTC")) + local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") + event_time = event_time.replace(tzinfo=local_tz) - embed.add_field(name="Date/Time", value=event_time.strftime("%Y-%m-%d %I:%M %p %Z"), inline=True) + timestamp = int(event_time.timestamp()) + embed.add_field(name="Date/Time", value=f"", inline=True) embed.add_field(name="Location", value=event.location or "TBD", inline=True) now = datetime.now(ZoneInfo("UTC")).strftime("%Y-%m-%d %H:%M") From 85e7528bfea7bf11fdb1338c0c4afb35fdea7a92 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Fri, 6 Feb 2026 14:29:32 -0500 Subject: [PATCH 10/18] feature(event): add pattern check to event schemas --- capy_discord/exts/event/_schemas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/capy_discord/exts/event/_schemas.py b/capy_discord/exts/event/_schemas.py index 4f021af..e37c52d 100644 --- a/capy_discord/exts/event/_schemas.py +++ b/capy_discord/exts/event/_schemas.py @@ -10,11 +10,13 @@ class EventSchema(BaseModel): event_date: date = Field( title="Event Date", description="Date of the event (MM-DD-YYYY)", + pattern=r"^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])-\d{4}$", default_factory=date.today, ) event_time: time = Field( title="Event Time", description="Time of the event (HH:MM, 24-hour) or (HH:MM AM/PM)", + pattern=r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])(\s?(AM|PM))?$", default_factory=lambda: datetime.now().astimezone().time(), ) location: str = Field(title="Location", description="Location of the event", max_length=200, default="") From 09e2d3871007d38e780429b11d034ebfe748199f Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Fri, 6 Feb 2026 16:28:34 -0500 Subject: [PATCH 11/18] feature(event): Implemented edit event logic --- capy_discord/exts/event/_schemas.py | 2 - capy_discord/exts/event/event.py | 99 ++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/capy_discord/exts/event/_schemas.py b/capy_discord/exts/event/_schemas.py index e37c52d..4f021af 100644 --- a/capy_discord/exts/event/_schemas.py +++ b/capy_discord/exts/event/_schemas.py @@ -10,13 +10,11 @@ class EventSchema(BaseModel): event_date: date = Field( title="Event Date", description="Date of the event (MM-DD-YYYY)", - pattern=r"^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])-\d{4}$", default_factory=date.today, ) event_time: time = Field( title="Event Time", description="Time of the event (HH:MM, 24-hour) or (HH:MM AM/PM)", - pattern=r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])(\s?(AM|PM))?$", default_factory=lambda: datetime.now().astimezone().time(), ) location: str = Field(title="Location", description="Location of the event", max_length=200, default="") diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 7741b81..81fcd0c 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -3,15 +3,67 @@ from zoneinfo import ZoneInfo import discord -from discord import app_commands +from discord import app_commands, ui from discord.ext import commands from capy_discord.ui.embeds import error_embed, success_embed from capy_discord.ui.forms import ModelModal +from capy_discord.ui.views import BaseView from ._schemas import EventSchema +class EventSelectView(BaseView): + """View to select an event from a dropdown.""" + + def __init__(self, events: list[EventSchema], cog: "Event") -> None: + """Initialize the EventSelectView.""" + super().__init__(timeout=60) + self.event_list = events + self.cog = cog + + if not events: + return + + options = [discord.SelectOption(label=event.event_name[:100], value=str(i)) for i, event in enumerate(events)] + self.add_item(EventSelect(options=options, view=self)) + + +class EventSelect(ui.Select["EventSelectView"]): + """Select component for event selection.""" + + def __init__(self, options: list[discord.SelectOption], view: "EventSelectView") -> None: + """Initialize the select.""" + super().__init__(placeholder="Select an event to edit", options=options) + self.view_ref = view + + async def callback(self, interaction: discord.Interaction) -> None: + """Handle selection.""" + event_idx = int(self.values[0]) + selected_event = self.view_ref.event_list[event_idx] + + initial_data = { + "event_name": selected_event.event_name, + "event_date": selected_event.event_date.strftime("%m-%d-%Y"), + "event_time": selected_event.event_time.strftime("%H:%M"), + "location": selected_event.location, + "description": selected_event.description, + } + + self.view_ref.cog.log.info("Opening edit modal for event '%s'", selected_event.event_name) + + modal = ModelModal( + model_cls=EventSchema, + callback=lambda modal_interaction, event: self.view_ref.cog._handle_event_update( + modal_interaction, event, selected_event + ), + title="Edit Event", + initial_data=initial_data, + ) + await interaction.response.send_modal(modal) + self.view_ref.stop() + + class Event(commands.Cog): """Cog for event-related commands.""" @@ -64,7 +116,28 @@ async def handle_create_action(self, interaction: discord.Interaction) -> None: async def handle_edit_action(self, interaction: discord.Interaction) -> None: """Handle event editing.""" - await interaction.response.send_message("Event edited successfully.") + guild_id = interaction.guild_id + if not guild_id: + embed = error_embed("No Server", "Events must be edited in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Fetch guild events + events = self.events.get(guild_id, []) + + if not events: + embed = error_embed("No Events", "No events found in this server to edit.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + self.log.info("Opening event selection for editing in guild %s", guild_id) + + await interaction.response.defer(ephemeral=True) + + view = EventSelectView(events, self) + await interaction.followup.send(content="Select an event to edit:", view=view, ephemeral=True) + + await view.wait() async def handle_show_action(self, interaction: discord.Interaction) -> None: """Handle showing event details.""" @@ -116,6 +189,28 @@ def _create_event_embed(self, event: EventSchema) -> discord.Embed: embed.set_footer(text=f"Created: {now}") return embed + async def _handle_event_update( + self, interaction: discord.Interaction, updated_event: EventSchema, original_event: EventSchema + ) -> None: + """Process the event update submission.""" + guild_id = interaction.guild_id + if not guild_id: + embed = error_embed("No Server", "Events must be updated in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Update event + guild_events = self.events.setdefault(guild_id, []) + if original_event in guild_events: + idx = guild_events.index(original_event) + guild_events[idx] = updated_event + + self.log.info("Updated event '%s' for guild %s", updated_event.event_name, guild_id) + + embed = self._create_event_embed(updated_event) + success = success_embed("Event Updated", "Your event has been updated successfully!") + await interaction.response.send_message(embeds=[success, embed], ephemeral=True) + async def setup(bot: commands.Bot) -> None: """Set up the Event cog.""" From b7bf291033f8b3c91edca0952498d898a58c853c Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Fri, 6 Feb 2026 16:40:33 -0500 Subject: [PATCH 12/18] feature(event): Implemented show event action --- capy_discord/exts/event/event.py | 132 +++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 41 deletions(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 81fcd0c..8ec0822 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -1,5 +1,7 @@ import logging +from collections.abc import Callable, Coroutine from datetime import datetime +from typing import Any from zoneinfo import ZoneInfo import discord @@ -13,55 +15,55 @@ from ._schemas import EventSchema -class EventSelectView(BaseView): - """View to select an event from a dropdown.""" +class EventDropdownSelect(ui.Select["EventDropdownView"]): + """Generic select component for event selection with customizable callback.""" - def __init__(self, events: list[EventSchema], cog: "Event") -> None: - """Initialize the EventSelectView.""" - super().__init__(timeout=60) - self.event_list = events - self.cog = cog - - if not events: - return - - options = [discord.SelectOption(label=event.event_name[:100], value=str(i)) for i, event in enumerate(events)] - self.add_item(EventSelect(options=options, view=self)) - - -class EventSelect(ui.Select["EventSelectView"]): - """Select component for event selection.""" - - def __init__(self, options: list[discord.SelectOption], view: "EventSelectView") -> None: + def __init__( + self, + options: list[discord.SelectOption], + view: "EventDropdownView", + placeholder: str, + ) -> None: """Initialize the select.""" - super().__init__(placeholder="Select an event to edit", options=options) + super().__init__(placeholder=placeholder, options=options) self.view_ref = view async def callback(self, interaction: discord.Interaction) -> None: - """Handle selection.""" + """Handle selection by delegating to view's callback.""" event_idx = int(self.values[0]) selected_event = self.view_ref.event_list[event_idx] + await self.view_ref.on_select(interaction, selected_event) + self.view_ref.stop() - initial_data = { - "event_name": selected_event.event_name, - "event_date": selected_event.event_date.strftime("%m-%d-%Y"), - "event_time": selected_event.event_time.strftime("%H:%M"), - "location": selected_event.location, - "description": selected_event.description, - } - self.view_ref.cog.log.info("Opening edit modal for event '%s'", selected_event.event_name) +class EventDropdownView(BaseView): + """Generic view for event selection with customizable callback.""" - modal = ModelModal( - model_cls=EventSchema, - callback=lambda modal_interaction, event: self.view_ref.cog._handle_event_update( - modal_interaction, event, selected_event - ), - title="Edit Event", - initial_data=initial_data, - ) - await interaction.response.send_modal(modal) - self.view_ref.stop() + def __init__( + self, + events: list[EventSchema], + cog: "Event", + placeholder: str, + on_select_callback: Callable[[discord.Interaction, EventSchema], Coroutine[Any, Any, None]], + ) -> None: + """Initialize the EventDropdownView. + + Args: + events: List of events to select from. + cog: Reference to the Event cog. + placeholder: Placeholder text for the dropdown. + on_select_callback: Async callback to handle selection. + """ + super().__init__(timeout=60) + self.event_list = events + self.cog = cog + self.on_select = on_select_callback + + if not events: + return + + options = [discord.SelectOption(label=event.event_name[:100], value=str(i)) for i, event in enumerate(events)] + self.add_item(EventDropdownSelect(options=options, view=self, placeholder=placeholder)) class Event(commands.Cog): @@ -134,14 +136,62 @@ async def handle_edit_action(self, interaction: discord.Interaction) -> None: await interaction.response.defer(ephemeral=True) - view = EventSelectView(events, self) + view = EventDropdownView(events, self, "Select an event to edit", self._on_edit_select) await interaction.followup.send(content="Select an event to edit:", view=view, ephemeral=True) await view.wait() + async def _on_edit_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: + """Handle event selection for editing.""" + initial_data = { + "event_name": selected_event.event_name, + "event_date": selected_event.event_date.strftime("%m-%d-%Y"), + "event_time": selected_event.event_time.strftime("%H:%M"), + "location": selected_event.location, + "description": selected_event.description, + } + + self.log.info("Opening edit modal for event '%s'", selected_event.event_name) + + modal = ModelModal( + model_cls=EventSchema, + callback=lambda modal_interaction, event: self._handle_event_update( + modal_interaction, event, selected_event + ), + title="Edit Event", + initial_data=initial_data, + ) + await interaction.response.send_modal(modal) + async def handle_show_action(self, interaction: discord.Interaction) -> None: """Handle showing event details.""" - await interaction.response.send_message("Event details displayed.") + guild_id = interaction.guild_id + if not guild_id: + embed = error_embed("No Server", "Events must be viewed in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Fetch guild events + events = self.events.get(guild_id, []) + + if not events: + embed = error_embed("No Events", "No events found in this server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + self.log.info("Opening event selection for viewing in guild %s", guild_id) + + await interaction.response.defer(ephemeral=True) + + view = EventDropdownView(events, self, "Select an event to view", self._on_show_select) + await interaction.followup.send(content="Select an event to view:", view=view, ephemeral=True) + + await view.wait() + + async def _on_show_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: + """Handle event selection for showing details.""" + embed = self._create_event_embed(selected_event) + await interaction.response.send_message(embed=embed, ephemeral=True) async def handle_delete_action(self, interaction: discord.Interaction) -> None: """Handle event deletion.""" From a53b4318053507b5de366709ffa2f451252201a0 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Fri, 6 Feb 2026 16:49:32 -0500 Subject: [PATCH 13/18] feature(event): Implemented delete event logic --- capy_discord/exts/event/event.py | 82 +++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 8ec0822..9e027fd 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -66,6 +66,31 @@ def __init__( self.add_item(EventDropdownSelect(options=options, view=self, placeholder=placeholder)) +class ConfirmDeleteView(BaseView): + """View to confirm event deletion.""" + + def __init__(self) -> None: + """Initialize the ConfirmDeleteView.""" + super().__init__(timeout=60) + self.value: bool | None = None + + @ui.button(label="Delete", style=discord.ButtonStyle.danger) + async def confirm(self, interaction: discord.Interaction, _button: ui.Button) -> None: + """Button to confirm deletion.""" + self.value = True + self.disable_all_items() + await interaction.response.edit_message(view=self) + self.stop() + + @ui.button(label="Cancel", style=discord.ButtonStyle.secondary) + async def cancel(self, interaction: discord.Interaction, _button: ui.Button) -> None: + """Button to cancel deletion.""" + self.value = False + self.disable_all_items() + await interaction.response.edit_message(view=self) + self.stop() + + class Event(commands.Cog): """Cog for event-related commands.""" @@ -188,14 +213,30 @@ async def handle_show_action(self, interaction: discord.Interaction) -> None: await view.wait() - async def _on_show_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: - """Handle event selection for showing details.""" - embed = self._create_event_embed(selected_event) - await interaction.response.send_message(embed=embed, ephemeral=True) - async def handle_delete_action(self, interaction: discord.Interaction) -> None: """Handle event deletion.""" - await interaction.response.send_message("Event deleted successfully.") + guild_id = interaction.guild_id + if not guild_id: + embed = error_embed("No Server", "Events must be deleted in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Fetch guild events + events = self.events.get(guild_id, []) + + if not events: + embed = error_embed("No Events", "No events found in this server to delete.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + self.log.info("Opening event selection for deletion in guild %s", guild_id) + + await interaction.response.defer(ephemeral=True) + + view = EventDropdownView(events, self, "Select an event to delete", self._on_delete_select) + await interaction.followup.send(content="Select an event to delete:", view=view, ephemeral=True) + + await view.wait() async def handle_list_action(self, interaction: discord.Interaction) -> None: """Handle listing all events.""" @@ -261,6 +302,35 @@ async def _handle_event_update( success = success_embed("Event Updated", "Your event has been updated successfully!") await interaction.response.send_message(embeds=[success, embed], ephemeral=True) + async def _on_show_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: + """Handle event selection for showing details.""" + embed = self._create_event_embed(selected_event) + await interaction.response.send_message(embed=embed, ephemeral=True) + + async def _on_delete_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: + """Handle event selection for deletion.""" + view = ConfirmDeleteView() + embed = discord.Embed( + title="Confirm Deletion", + description=f"Are you sure you want to delete **{selected_event.event_name}**?", + color=discord.Color.red(), + ) + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + await view.wait() + + if view.value is True: + # [DB CALL]: Delete event from guild + guild_id = interaction.guild_id + if guild_id: + guild_events = self.events.setdefault(guild_id, []) + if selected_event in guild_events: + guild_events.remove(selected_event) + self.log.info("Deleted event '%s' from guild %s", selected_event.event_name, guild_id) + + success = success_embed("Event Deleted", "The event has been deleted successfully!") + await interaction.followup.send(embed=success, ephemeral=True) + async def setup(bot: commands.Bot) -> None: """Set up the Event cog.""" From 0bbf85b62743c3c0c12903514c3da9062be0d1d1 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Fri, 6 Feb 2026 16:57:59 -0500 Subject: [PATCH 14/18] feature(event): handle event listing --- capy_discord/exts/event/event.py | 76 +++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 9e027fd..b36ab12 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -240,7 +240,81 @@ async def handle_delete_action(self, interaction: discord.Interaction) -> None: async def handle_list_action(self, interaction: discord.Interaction) -> None: """Handle listing all events.""" - await interaction.response.send_message("List of events displayed.") + guild_id = interaction.guild_id + if not guild_id: + embed = error_embed("No Server", "Events must be listed in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Fetch guild events + events = self.events.get(guild_id, []) + + if not events: + embed = error_embed("No Events", "No events found in this server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + self.log.info("Listing events for guild %s", guild_id) + + await interaction.response.defer(ephemeral=True) + + # Separate into upcoming and past events + now = datetime.now().astimezone() + upcoming_events: list[EventSchema] = [] + past_events: list[EventSchema] = [] + + for event in events: + event_time = datetime.combine(event.event_date, event.event_time) + if event_time.tzinfo is None: + local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") + event_time = event_time.replace(tzinfo=local_tz) + + if event_time >= now: + upcoming_events.append(event) + else: + past_events.append(event) + + # Sort events + upcoming_events.sort(key=lambda e: datetime.combine(e.event_date, e.event_time)) + past_events.sort(key=lambda e: datetime.combine(e.event_date, e.event_time), reverse=True) + + # Build embed + total_count = len(upcoming_events) + len(past_events) + embed = discord.Embed( + title="Events", + description=(f"Found {total_count} events (Upcoming: {len(upcoming_events)}, Past: {len(past_events)})"), + color=discord.Color.blue(), + ) + + # Add upcoming events + for event in upcoming_events: + event_time = datetime.combine(event.event_date, event.event_time) + if event_time.tzinfo is None: + local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") + event_time = event_time.replace(tzinfo=local_tz) + + timestamp = int(event_time.timestamp()) + embed.add_field( + name=event.event_name, + value=f"**When:** \n**Where:** {event.location or 'TBD'}", + inline=False, + ) + + # Add past events with [OLD] prefix + for event in past_events: + event_time = datetime.combine(event.event_date, event.event_time) + if event_time.tzinfo is None: + local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") + event_time = event_time.replace(tzinfo=local_tz) + + timestamp = int(event_time.timestamp()) + embed.add_field( + name=f"[OLD] {event.event_name}", + value=f"**When:** \n**Where:** {event.location or 'TBD'}", + inline=False, + ) + + await interaction.followup.send(embed=embed, ephemeral=True) async def handle_announce_action(self, interaction: discord.Interaction) -> None: """Handle announcing an event.""" From 0488eb0e67954bf7cf1f03e03d1df03d73b92542 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Fri, 6 Feb 2026 17:09:10 -0500 Subject: [PATCH 15/18] event(feature): Implemetned event announcing. --- capy_discord/exts/event/event.py | 118 ++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 2 deletions(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index b36ab12..9726b8a 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -317,8 +317,122 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: await interaction.followup.send(embed=embed, ephemeral=True) async def handle_announce_action(self, interaction: discord.Interaction) -> None: - """Handle announcing an event.""" - await interaction.response.send_message("Event announced successfully.") + """Handle announcing an event and user registrations.""" + guild_id = interaction.guild_id + if not guild_id: + embed = error_embed("No Server", "Events must be announced in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Fetch guild events + events = self.events.get(guild_id, []) + + if not events: + embed = error_embed("No Events", "No events found in this server to announce.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + self.log.info("Opening event selection for announcement in guild %s", guild_id) + + await interaction.response.defer(ephemeral=True) + + view = EventDropdownView(events, self, "Select an event to announce", self._on_announce_select) + await interaction.followup.send(content="Select an event to announce:", view=view, ephemeral=True) + + await view.wait() + + async def _on_announce_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: + """Handle event selection for announcement.""" + guild = interaction.guild + if not guild: + embed = error_embed("No Server", "Cannot determine server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # Try to find an announcements channel + announcement_channel: discord.TextChannel | None = None + for channel in guild.text_channels: + if "announce" in channel.name.lower(): + announcement_channel = channel + break + + if not announcement_channel: + embed = error_embed( + "No Announcement Channel", + "Could not find a channel with 'announce' in the name. " + "Please rename or create an announcement channel.", + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # Check if bot has permission to post in the channel + if not announcement_channel.permissions_for(guild.me).send_messages: + embed = error_embed( + "No Permission", + "I don't have permission to send messages in the announcement channel.", + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + try: + # Create announcement embed + announce_embed = self._create_announcement_embed(selected_event) + + # Post to announcement channel + message = await announcement_channel.send(embed=announce_embed) + + # Add RSVP reactions + await message.add_reaction("βœ…") # Attending + await message.add_reaction("❌") # Not attending + + self.log.info( + "Announced event '%s' to guild %s in channel %s", + selected_event.event_name, + guild.id, + announcement_channel.name, + ) + + success = success_embed( + "Event Announced", + f"Event announced successfully in {announcement_channel.mention}!\n" + "Users can react with βœ… to attend or ❌ to decline.", + ) + await interaction.response.send_message(embed=success, ephemeral=True) + + except discord.Forbidden: + embed = error_embed("Permission Denied", "I don't have permission to send messages in that channel.") + await interaction.response.send_message(embed=embed, ephemeral=True) + except discord.HTTPException: + self.log.exception("Failed to announce event") + embed = error_embed("Announcement Failed", "Failed to announce the event. Please try again.") + await interaction.response.send_message(embed=embed, ephemeral=True) + + def _create_announcement_embed(self, event: EventSchema) -> discord.Embed: + """Create an announcement embed for an event.""" + embed = discord.Embed( + title=f"πŸ“… {event.event_name}", + description=event.description or "No description provided.", + color=discord.Color.gold(), + ) + + event_time = datetime.combine(event.event_date, event.event_time) + if event_time.tzinfo is None: + local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") + event_time = event_time.replace(tzinfo=local_tz) + + timestamp = int(event_time.timestamp()) + embed.add_field(name="πŸ“ When", value=f"", inline=False) + embed.add_field(name="πŸ—ΊοΈ Where", value=event.location or "TBD", inline=False) + + embed.add_field( + name="πŸ“‹ RSVP", + value="React with βœ… to attend or ❌ to decline.", + inline=False, + ) + + now = datetime.now(ZoneInfo("UTC")).strftime("%Y-%m-%d %H:%M") + embed.set_footer(text=f"Announced: {now}") + return embed async def _handle_event_submit(self, interaction: discord.Interaction, event: EventSchema) -> None: """Process the valid event submission.""" From d2d2b6edffc253b7f4dc77afe35309aa5ddab9a4 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Fri, 6 Feb 2026 18:42:52 -0500 Subject: [PATCH 16/18] feature(event): implemented myevents handling --- capy_discord/exts/event/event.py | 170 +++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 22 deletions(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 9726b8a..ca79293 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -101,6 +101,8 @@ def __init__(self, bot: commands.Bot) -> None: self.log.info("Event cog initialized") # In-memory storage for demonstration. self.events: dict[int, list[EventSchema]] = {} + # Track announcement messages: guild_id -> {event_name: message_id} + self.event_announcements: dict[int, dict[str, int]] = {} @app_commands.command(name="event", description="Manage events") @app_commands.describe(action="The action to perform with events") @@ -112,6 +114,7 @@ def __init__(self, bot: commands.Bot) -> None: app_commands.Choice(name="delete", value="delete"), app_commands.Choice(name="list", value="list"), app_commands.Choice(name="announce", value="announce"), + app_commands.Choice(name="myevents", value="myevents"), ] ) async def event(self, interaction: discord.Interaction, action: app_commands.Choice[str]) -> None: @@ -129,6 +132,8 @@ async def event(self, interaction: discord.Interaction, action: app_commands.Cho await self.handle_list_action(interaction) case "announce": await self.handle_announce_action(interaction) + case "myevents": + await self.handle_myevents_action(interaction) async def handle_create_action(self, interaction: discord.Interaction) -> None: """Handle event creation.""" @@ -166,28 +171,6 @@ async def handle_edit_action(self, interaction: discord.Interaction) -> None: await view.wait() - async def _on_edit_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: - """Handle event selection for editing.""" - initial_data = { - "event_name": selected_event.event_name, - "event_date": selected_event.event_date.strftime("%m-%d-%Y"), - "event_time": selected_event.event_time.strftime("%H:%M"), - "location": selected_event.location, - "description": selected_event.description, - } - - self.log.info("Opening edit modal for event '%s'", selected_event.event_name) - - modal = ModelModal( - model_cls=EventSchema, - callback=lambda modal_interaction, event: self._handle_event_update( - modal_interaction, event, selected_event - ), - title="Edit Event", - initial_data=initial_data, - ) - await interaction.response.send_modal(modal) - async def handle_show_action(self, interaction: discord.Interaction) -> None: """Handle showing event details.""" guild_id = interaction.guild_id @@ -341,6 +324,144 @@ async def handle_announce_action(self, interaction: discord.Interaction) -> None await view.wait() + async def handle_myevents_action(self, interaction: discord.Interaction) -> None: + """Handle showing events the user has registered for via RSVP.""" + guild_id = interaction.guild_id + guild = interaction.guild + if not guild_id or not guild: + embed = error_embed("No Server", "Events must be viewed in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Fetch guild events + events = self.events.get(guild_id, []) + + if not events: + embed = error_embed("No Events", "No events found in this server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + self.log.info("Listing registered events for user %s", interaction.user) + + await interaction.response.defer(ephemeral=True) + + # Get upcoming events the user has registered for + now = datetime.now().astimezone() + registered_events: list[EventSchema] = [] + + for event in events: + event_time = datetime.combine(event.event_date, event.event_time) + if event_time.tzinfo is None: + local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") + event_time = event_time.replace(tzinfo=local_tz) + + # Only include upcoming events + if event_time < now: + continue + + # Check if user has registered for this event + if await self._is_user_registered(event, guild, interaction.user): + registered_events.append(event) + + registered_events.sort(key=lambda e: datetime.combine(e.event_date, e.event_time)) + + # Build embed + embed = discord.Embed( + title="Your Registered Events", + description="Events you have registered for by reacting with βœ…", + color=discord.Color.purple(), + ) + + if not registered_events: + embed.description = ( + "You haven't registered for any upcoming events.\nReact to event announcements with βœ… to register!" + ) + await interaction.followup.send(embed=embed, ephemeral=True) + return + + # Add registered events + for event in registered_events: + event_time = datetime.combine(event.event_date, event.event_time) + if event_time.tzinfo is None: + local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") + event_time = event_time.replace(tzinfo=local_tz) + + timestamp = int(event_time.timestamp()) + embed.add_field( + name=event.event_name, + value=f"**When:** \n**Where:** {event.location or 'TBD'}", + inline=False, + ) + + await interaction.followup.send(embed=embed, ephemeral=True) + + async def _is_user_registered( + self, event: EventSchema, guild: discord.Guild, user: discord.User | discord.Member + ) -> bool: + """Check if a user has registered for an event via RSVP reaction. + + Args: + event: The event to check registration for. + guild: The guild where the event was announced. + user: The user to check registration for. + + Returns: + True if the user has reacted with βœ… to the event announcement, False otherwise. + """ + # Get announcement messages for this guild + guild_announcements = self.event_announcements.get(guild.id, {}) + message_id = guild_announcements.get(event.event_name) + + if not message_id: + return False + + # Try to find the announcement message and check reactions + announcement_channel: discord.TextChannel | None = None + for channel in guild.text_channels: + if "announce" in channel.name.lower(): + announcement_channel = channel + break + + if not announcement_channel: + return False + + try: + message = await announcement_channel.fetch_message(message_id) + # Check if user reacted with βœ… + for reaction in message.reactions: + if str(reaction.emoji) == "βœ…": + users = [user async for user in reaction.users()] + if user in users: + return True + except (discord.NotFound, discord.Forbidden, discord.HTTPException): + # Message not found or no permission - skip this event + self.log.warning("Could not fetch announcement message %s", message_id) + return False + + return False + + async def _on_edit_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: + """Handle event selection for editing.""" + initial_data = { + "event_name": selected_event.event_name, + "event_date": selected_event.event_date.strftime("%m-%d-%Y"), + "event_time": selected_event.event_time.strftime("%H:%M"), + "location": selected_event.location, + "description": selected_event.description, + } + + self.log.info("Opening edit modal for event '%s'", selected_event.event_name) + + modal = ModelModal( + model_cls=EventSchema, + callback=lambda modal_interaction, event: self._handle_event_update( + modal_interaction, event, selected_event + ), + title="Edit Event", + initial_data=initial_data, + ) + await interaction.response.send_modal(modal) + async def _on_announce_select(self, interaction: discord.Interaction, selected_event: EventSchema) -> None: """Handle event selection for announcement.""" guild = interaction.guild @@ -385,6 +506,11 @@ async def _on_announce_select(self, interaction: discord.Interaction, selected_e await message.add_reaction("βœ…") # Attending await message.add_reaction("❌") # Not attending + # [DB CALL]: Store announcement message ID for RSVP tracking + if guild.id not in self.event_announcements: + self.event_announcements[guild.id] = {} + self.event_announcements[guild.id][selected_event.event_name] = message.id + self.log.info( "Announced event '%s' to guild %s in channel %s", selected_event.event_name, From 9860422ecdab72dc98325284c87b1e4a12738cd4 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Mon, 9 Feb 2026 12:48:23 -0500 Subject: [PATCH 17/18] refactor(event): Import annoucement channel name from config file. Consolidated the timezone logic into helper methods --- capy_discord/config.py | 3 + capy_discord/exts/event/event.py | 98 +++++++++++++++----------------- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/capy_discord/config.py b/capy_discord/config.py index 53bdc12..532a1de 100644 --- a/capy_discord/config.py +++ b/capy_discord/config.py @@ -25,5 +25,8 @@ class Settings(EnvConfig): # Ticket System Configuration ticket_feedback_channel_id: int = 0 + # Event System Configuration + announcement_channel_name: str = "test-announcements" + settings = Settings() diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index ca79293..6416608 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -8,6 +8,7 @@ from discord import app_commands, ui from discord.ext import commands +from capy_discord.config import settings from capy_discord.ui.embeds import error_embed, success_embed from capy_discord.ui.forms import ModelModal from capy_discord.ui.views import BaseView @@ -247,10 +248,7 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: past_events: list[EventSchema] = [] for event in events: - event_time = datetime.combine(event.event_date, event.event_time) - if event_time.tzinfo is None: - local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") - event_time = event_time.replace(tzinfo=local_tz) + event_time = self._event_datetime(event) if event_time >= now: upcoming_events.append(event) @@ -258,8 +256,8 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: past_events.append(event) # Sort events - upcoming_events.sort(key=lambda e: datetime.combine(e.event_date, e.event_time)) - past_events.sort(key=lambda e: datetime.combine(e.event_date, e.event_time), reverse=True) + upcoming_events.sort(key=lambda e: self._event_datetime(e)) + past_events.sort(key=lambda e: self._event_datetime(e), reverse=True) # Build embed total_count = len(upcoming_events) + len(past_events) @@ -271,12 +269,7 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: # Add upcoming events for event in upcoming_events: - event_time = datetime.combine(event.event_date, event.event_time) - if event_time.tzinfo is None: - local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") - event_time = event_time.replace(tzinfo=local_tz) - - timestamp = int(event_time.timestamp()) + timestamp = int(self._event_datetime(event).timestamp()) embed.add_field( name=event.event_name, value=f"**When:** \n**Where:** {event.location or 'TBD'}", @@ -285,12 +278,7 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: # Add past events with [OLD] prefix for event in past_events: - event_time = datetime.combine(event.event_date, event.event_time) - if event_time.tzinfo is None: - local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") - event_time = event_time.replace(tzinfo=local_tz) - - timestamp = int(event_time.timestamp()) + timestamp = int(self._event_datetime(event).timestamp()) embed.add_field( name=f"[OLD] {event.event_name}", value=f"**When:** \n**Where:** {event.location or 'TBD'}", @@ -350,10 +338,7 @@ async def handle_myevents_action(self, interaction: discord.Interaction) -> None registered_events: list[EventSchema] = [] for event in events: - event_time = datetime.combine(event.event_date, event.event_time) - if event_time.tzinfo is None: - local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") - event_time = event_time.replace(tzinfo=local_tz) + event_time = self._event_datetime(event) # Only include upcoming events if event_time < now: @@ -363,7 +348,7 @@ async def handle_myevents_action(self, interaction: discord.Interaction) -> None if await self._is_user_registered(event, guild, interaction.user): registered_events.append(event) - registered_events.sort(key=lambda e: datetime.combine(e.event_date, e.event_time)) + registered_events.sort(key=lambda e: self._event_datetime(e)) # Build embed embed = discord.Embed( @@ -381,12 +366,7 @@ async def handle_myevents_action(self, interaction: discord.Interaction) -> None # Add registered events for event in registered_events: - event_time = datetime.combine(event.event_date, event.event_time) - if event_time.tzinfo is None: - local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") - event_time = event_time.replace(tzinfo=local_tz) - - timestamp = int(event_time.timestamp()) + timestamp = int(self._event_datetime(event).timestamp()) embed.add_field( name=event.event_name, value=f"**When:** \n**Where:** {event.location or 'TBD'}", @@ -395,6 +375,36 @@ async def handle_myevents_action(self, interaction: discord.Interaction) -> None await interaction.followup.send(embed=embed, ephemeral=True) + @staticmethod + def _event_datetime(event: EventSchema) -> datetime: + """Convert event date and time to a timezone-aware datetime. + + Args: + event: The event containing date and time information. + + Returns: + A timezone-aware datetime object. + """ + event_time = datetime.combine(event.event_date, event.event_time) + if event_time.tzinfo is None: + local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") + event_time = event_time.replace(tzinfo=local_tz) + return event_time + + def _get_announcement_channel(self, guild: discord.Guild) -> discord.TextChannel | None: + """Get the announcement channel from config name. + + Args: + guild: The guild to search for the announcement channel. + + Returns: + The announcement channel if found, None otherwise. + """ + for channel in guild.text_channels: + if channel.name.lower() == settings.announcement_channel_name.lower(): + return channel + return None + async def _is_user_registered( self, event: EventSchema, guild: discord.Guild, user: discord.User | discord.Member ) -> bool: @@ -416,11 +426,7 @@ async def _is_user_registered( return False # Try to find the announcement message and check reactions - announcement_channel: discord.TextChannel | None = None - for channel in guild.text_channels: - if "announce" in channel.name.lower(): - announcement_channel = channel - break + announcement_channel = self._get_announcement_channel(guild) if not announcement_channel: return False @@ -470,17 +476,13 @@ async def _on_announce_select(self, interaction: discord.Interaction, selected_e await interaction.response.send_message(embed=embed, ephemeral=True) return - # Try to find an announcements channel - announcement_channel: discord.TextChannel | None = None - for channel in guild.text_channels: - if "announce" in channel.name.lower(): - announcement_channel = channel - break + # Get the announcement channel + announcement_channel = self._get_announcement_channel(guild) if not announcement_channel: embed = error_embed( "No Announcement Channel", - "Could not find a channel with 'announce' in the name. " + f"Could not find a channel named '{settings.announcement_channel_name}'. " "Please rename or create an announcement channel.", ) await interaction.response.send_message(embed=embed, ephemeral=True) @@ -541,12 +543,7 @@ def _create_announcement_embed(self, event: EventSchema) -> discord.Embed: color=discord.Color.gold(), ) - event_time = datetime.combine(event.event_date, event.event_time) - if event_time.tzinfo is None: - local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") - event_time = event_time.replace(tzinfo=local_tz) - - timestamp = int(event_time.timestamp()) + timestamp = int(self._event_datetime(event).timestamp()) embed.add_field(name="πŸ“ When", value=f"", inline=False) embed.add_field(name="πŸ—ΊοΈ Where", value=event.location or "TBD", inline=False) @@ -581,12 +578,7 @@ def _create_event_embed(self, event: EventSchema) -> discord.Embed: """Helper to build the event display embed.""" embed = discord.Embed(title=event.event_name, description=event.description) - event_time = datetime.combine(event.event_date, event.event_time) - if event_time.tzinfo is None: - local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") - event_time = event_time.replace(tzinfo=local_tz) - - timestamp = int(event_time.timestamp()) + timestamp = int(self._event_datetime(event).timestamp()) embed.add_field(name="Date/Time", value=f"", inline=True) embed.add_field(name="Location", value=event.location or "TBD", inline=True) From 2e130fec33bb0d3933a034d96a69b5a374ae0965 Mon Sep 17 00:00:00 2001 From: Sayed Imtiazuddin Date: Mon, 9 Feb 2026 23:27:39 -0500 Subject: [PATCH 18/18] Refactor(event): Moved redundant code into helper methods. Simplified time handling from system timestamp to EST time. --- capy_discord/exts/event/event.py | 177 ++++++++++++------------------- 1 file changed, 69 insertions(+), 108 deletions(-) diff --git a/capy_discord/exts/event/event.py b/capy_discord/exts/event/event.py index 6416608..9beb2c4 100644 --- a/capy_discord/exts/event/event.py +++ b/capy_discord/exts/event/event.py @@ -149,78 +149,15 @@ async def handle_create_action(self, interaction: discord.Interaction) -> None: async def handle_edit_action(self, interaction: discord.Interaction) -> None: """Handle event editing.""" - guild_id = interaction.guild_id - if not guild_id: - embed = error_embed("No Server", "Events must be edited in a server.") - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - # [DB CALL]: Fetch guild events - events = self.events.get(guild_id, []) - - if not events: - embed = error_embed("No Events", "No events found in this server to edit.") - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - self.log.info("Opening event selection for editing in guild %s", guild_id) - - await interaction.response.defer(ephemeral=True) - - view = EventDropdownView(events, self, "Select an event to edit", self._on_edit_select) - await interaction.followup.send(content="Select an event to edit:", view=view, ephemeral=True) - - await view.wait() + await self._get_events_for_dropdown(interaction, "edit", self._on_edit_select) async def handle_show_action(self, interaction: discord.Interaction) -> None: """Handle showing event details.""" - guild_id = interaction.guild_id - if not guild_id: - embed = error_embed("No Server", "Events must be viewed in a server.") - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - # [DB CALL]: Fetch guild events - events = self.events.get(guild_id, []) - - if not events: - embed = error_embed("No Events", "No events found in this server.") - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - self.log.info("Opening event selection for viewing in guild %s", guild_id) - - await interaction.response.defer(ephemeral=True) - - view = EventDropdownView(events, self, "Select an event to view", self._on_show_select) - await interaction.followup.send(content="Select an event to view:", view=view, ephemeral=True) - - await view.wait() + await self._get_events_for_dropdown(interaction, "view", self._on_show_select) async def handle_delete_action(self, interaction: discord.Interaction) -> None: """Handle event deletion.""" - guild_id = interaction.guild_id - if not guild_id: - embed = error_embed("No Server", "Events must be deleted in a server.") - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - # [DB CALL]: Fetch guild events - events = self.events.get(guild_id, []) - - if not events: - embed = error_embed("No Events", "No events found in this server to delete.") - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - self.log.info("Opening event selection for deletion in guild %s", guild_id) - - await interaction.response.defer(ephemeral=True) - - view = EventDropdownView(events, self, "Select an event to delete", self._on_delete_select) - await interaction.followup.send(content="Select an event to delete:", view=view, ephemeral=True) - - await view.wait() + await self._get_events_for_dropdown(interaction, "delete", self._on_delete_select) async def handle_list_action(self, interaction: discord.Interaction) -> None: """Handle listing all events.""" @@ -243,7 +180,7 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: await interaction.response.defer(ephemeral=True) # Separate into upcoming and past events - now = datetime.now().astimezone() + now = datetime.now(ZoneInfo("UTC")) upcoming_events: list[EventSchema] = [] past_events: list[EventSchema] = [] @@ -256,8 +193,8 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: past_events.append(event) # Sort events - upcoming_events.sort(key=lambda e: self._event_datetime(e)) - past_events.sort(key=lambda e: self._event_datetime(e), reverse=True) + upcoming_events.sort(key=self._event_datetime) + past_events.sort(key=self._event_datetime, reverse=True) # Build embed total_count = len(upcoming_events) + len(past_events) @@ -269,19 +206,17 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: # Add upcoming events for event in upcoming_events: - timestamp = int(self._event_datetime(event).timestamp()) embed.add_field( name=event.event_name, - value=f"**When:** \n**Where:** {event.location or 'TBD'}", + value=self._format_when_where(event), inline=False, ) # Add past events with [OLD] prefix for event in past_events: - timestamp = int(self._event_datetime(event).timestamp()) embed.add_field( name=f"[OLD] {event.event_name}", - value=f"**When:** \n**Where:** {event.location or 'TBD'}", + value=self._format_when_where(event), inline=False, ) @@ -289,28 +224,7 @@ async def handle_list_action(self, interaction: discord.Interaction) -> None: async def handle_announce_action(self, interaction: discord.Interaction) -> None: """Handle announcing an event and user registrations.""" - guild_id = interaction.guild_id - if not guild_id: - embed = error_embed("No Server", "Events must be announced in a server.") - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - # [DB CALL]: Fetch guild events - events = self.events.get(guild_id, []) - - if not events: - embed = error_embed("No Events", "No events found in this server to announce.") - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - self.log.info("Opening event selection for announcement in guild %s", guild_id) - - await interaction.response.defer(ephemeral=True) - - view = EventDropdownView(events, self, "Select an event to announce", self._on_announce_select) - await interaction.followup.send(content="Select an event to announce:", view=view, ephemeral=True) - - await view.wait() + await self._get_events_for_dropdown(interaction, "announce", self._on_announce_select) async def handle_myevents_action(self, interaction: discord.Interaction) -> None: """Handle showing events the user has registered for via RSVP.""" @@ -334,7 +248,7 @@ async def handle_myevents_action(self, interaction: discord.Interaction) -> None await interaction.response.defer(ephemeral=True) # Get upcoming events the user has registered for - now = datetime.now().astimezone() + now = datetime.now(ZoneInfo("UTC")) registered_events: list[EventSchema] = [] for event in events: @@ -348,7 +262,7 @@ async def handle_myevents_action(self, interaction: discord.Interaction) -> None if await self._is_user_registered(event, guild, interaction.user): registered_events.append(event) - registered_events.sort(key=lambda e: self._event_datetime(e)) + registered_events.sort(key=self._event_datetime) # Build embed embed = discord.Embed( @@ -366,30 +280,79 @@ async def handle_myevents_action(self, interaction: discord.Interaction) -> None # Add registered events for event in registered_events: - timestamp = int(self._event_datetime(event).timestamp()) embed.add_field( name=event.event_name, - value=f"**When:** \n**Where:** {event.location or 'TBD'}", + value=self._format_when_where(event), inline=False, ) await interaction.followup.send(embed=embed, ephemeral=True) + async def _get_events_for_dropdown( + self, + interaction: discord.Interaction, + action_name: str, + callback: Callable[[discord.Interaction, EventSchema], Coroutine[Any, Any, None]], + ) -> None: + """Generic handler to get events and show dropdown for selection. + + Args: + interaction: The Discord interaction. + action_name: Name of the action (e.g., "edit", "view", "delete"). + callback: Async callback to handle the selected event. + """ + guild_id = interaction.guild_id + if not guild_id: + embed = error_embed("No Server", f"Events must be {action_name}ed in a server.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + # [DB CALL]: Fetch guild events + events = self.events.get(guild_id, []) + + if not events: + embed = error_embed("No Events", f"No events found in this server to {action_name}.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + self.log.info("Opening event selection for %s in guild %s", action_name, guild_id) + + await interaction.response.defer(ephemeral=True) + + view = EventDropdownView(events, self, f"Select an event to {action_name}", callback) + await interaction.followup.send(content=f"Select an event to {action_name}:", view=view, ephemeral=True) + + await view.wait() + @staticmethod def _event_datetime(event: EventSchema) -> datetime: - """Convert event date and time to a timezone-aware datetime. + """Convert event date and time to a timezone-aware datetime in UTC. + + User input is treated as EST, then converted to UTC for storage. Args: event: The event containing date and time information. Returns: - A timezone-aware datetime object. + A UTC timezone-aware datetime object. """ + est = ZoneInfo("America/New_York") event_time = datetime.combine(event.event_date, event.event_time) + # Treat user input as EST if event_time.tzinfo is None: - local_tz = datetime.now().astimezone().tzinfo or ZoneInfo("UTC") - event_time = event_time.replace(tzinfo=local_tz) - return event_time + event_time = event_time.replace(tzinfo=est) + # Convert to UTC for storage + return event_time.astimezone(ZoneInfo("UTC")) + + def _format_event_time_est(self, event: EventSchema) -> str: + """Format an event's date/time in EST for user-facing display.""" + event_dt_est = self._event_datetime(event).astimezone(ZoneInfo("America/New_York")) + return event_dt_est.strftime("%B %d, %Y at %I:%M %p EST") + + def _format_when_where(self, event: EventSchema) -> str: + """Format the when/where field for embeds.""" + time_str = self._format_event_time_est(event) + return f"**When:** {time_str}\n**Where:** {event.location or 'TBD'}" def _get_announcement_channel(self, guild: discord.Guild) -> discord.TextChannel | None: """Get the announcement channel from config name. @@ -543,8 +506,7 @@ def _create_announcement_embed(self, event: EventSchema) -> discord.Embed: color=discord.Color.gold(), ) - timestamp = int(self._event_datetime(event).timestamp()) - embed.add_field(name="πŸ“ When", value=f"", inline=False) + embed.add_field(name="πŸ“ When", value=self._format_event_time_est(event), inline=False) embed.add_field(name="πŸ—ΊοΈ Where", value=event.location or "TBD", inline=False) embed.add_field( @@ -578,8 +540,7 @@ def _create_event_embed(self, event: EventSchema) -> discord.Embed: """Helper to build the event display embed.""" embed = discord.Embed(title=event.event_name, description=event.description) - timestamp = int(self._event_datetime(event).timestamp()) - embed.add_field(name="Date/Time", value=f"", inline=True) + embed.add_field(name="Date/Time", value=self._format_event_time_est(event), inline=True) embed.add_field(name="Location", value=event.location or "TBD", inline=True) now = datetime.now(ZoneInfo("UTC")).strftime("%Y-%m-%d %H:%M")