Skip to content

Feature/capr 23 scaffold event cog from deprecated repo - #70

Merged
shamikkarkhanis merged 23 commits into
developfrom
feature/capr-23-scaffold-event_cog-from-deprecated-repo
Feb 10, 2026
Merged

Feature/capr 23 scaffold event cog from deprecated repo#70
shamikkarkhanis merged 23 commits into
developfrom
feature/capr-23-scaffold-event_cog-from-deprecated-repo

Conversation

@simtiaz5

@simtiaz5 simtiaz5 commented Feb 7, 2026

Copy link
Copy Markdown
Contributor

Summary by Sourcery

Add a new Discord cog for event management, including schema validation and interaction flows for creating, viewing, editing, deleting, listing, announcing, and tracking user registrations for events.

New Features:

  • Introduce an Event cog providing a unified /event command with sub-actions for full event lifecycle management in Discord servers.
  • Add an EventSchema Pydantic model to validate and parse event input data used by modals and commands.
  • Implement interactive Discord UI components (dropdowns, confirmation view, and modals) for selecting, managing, and confirming operations on events.
  • Enable event announcement messages with RSVP via reactions and a command to show a user's registered upcoming events.

…o feature/capr-23-scaffold-event_cog-from-deprecated-repo
…o feature/capr-23-scaffold-event_cog-from-deprecated-repo
…o feature/capr-23-scaffold-event_cog-from-deprecated-repo
Copilot AI review requested due to automatic review settings February 7, 2026 23:08
@simtiaz5 simtiaz5 self-assigned this Feb 7, 2026
@sourcery-ai

sourcery-ai Bot commented Feb 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a new Discord "Event" cog with full event lifecycle management (create, edit, list, show, delete, announce, and "my events") backed by a temporary in-memory store, plus a Pydantic EventSchema for modal-driven form validation and parsing of event date/time text inputs.

Sequence diagram for /event command lifecycle handling

sequenceDiagram
    actor User
    participant DiscordClient
    participant EventCog
    participant ModelModal
    participant EventDropdownView
    participant ConfirmDeleteView

    User->>DiscordClient: /event action=create
    DiscordClient->>EventCog: event(interaction, action=create)
    EventCog->>EventCog: handle_create_action(interaction)
    EventCog->>ModelModal: create ModelModal(EventSchema, _handle_event_submit)
    EventCog->>DiscordClient: interaction.response.send_modal(modal)

    User->>DiscordClient: submits event modal
    DiscordClient->>ModelModal: validate fields via EventSchema
    ModelModal->>EventCog: _handle_event_submit(interaction, event)
    EventCog->>EventCog: events[guild_id].append(event)
    EventCog->>DiscordClient: interaction.response.send_message(embeds=[success, event_embed])

    User->>DiscordClient: /event action=edit
    DiscordClient->>EventCog: event(interaction, action=edit)
    EventCog->>EventCog: handle_edit_action(interaction)
    EventCog->>EventCog: events = self.events[guild_id]
    EventCog->>DiscordClient: interaction.response.defer(ephemeral=True)
    EventCog->>EventDropdownView: create EventDropdownView(events, cog, placeholder, _on_edit_select)
    EventCog->>DiscordClient: followup.send(view=EventDropdownView)

    User->>EventDropdownView: select event
    EventDropdownView->>EventCog: _on_edit_select(interaction, selected_event)
    EventCog->>ModelModal: create ModelModal(EventSchema, _handle_event_update)
    EventCog->>DiscordClient: interaction.response.send_modal(modal)

    User->>DiscordClient: submits updated modal
    DiscordClient->>ModelModal: validate via EventSchema
    ModelModal->>EventCog: _handle_event_update(interaction, updated_event, original_event)
    EventCog->>EventCog: replace original_event with updated_event in events[guild_id]
    EventCog->>DiscordClient: interaction.response.send_message(embeds=[success, event_embed])

    User->>DiscordClient: /event action=delete
    DiscordClient->>EventCog: event(interaction, action=delete)
    EventCog->>EventCog: handle_delete_action(interaction)
    EventCog->>EventDropdownView: create EventDropdownView(events, cog, placeholder, _on_delete_select)
    EventCog->>DiscordClient: followup.send(view=EventDropdownView)

    User->>EventDropdownView: select event
    EventDropdownView->>EventCog: _on_delete_select(interaction, selected_event)
    EventCog->>ConfirmDeleteView: create ConfirmDeleteView()
    EventCog->>DiscordClient: interaction.response.send_message(embed=confirm, view=ConfirmDeleteView)

    User->>ConfirmDeleteView: clicks Delete
    ConfirmDeleteView->>ConfirmDeleteView: value = True, disable_all_items()
    ConfirmDeleteView->>DiscordClient: interaction.response.edit_message(view=self)
    EventCog->>EventCog: remove selected_event from events[guild_id]
    EventCog->>DiscordClient: interaction.followup.send(success_embed)
Loading

Sequence diagram for event announcement and RSVP-based myevents lookup

sequenceDiagram
    actor User
    participant DiscordClient
    participant EventCog
    participant EventDropdownView
    participant AnnouncementChannel

    User->>DiscordClient: /event action=announce
    DiscordClient->>EventCog: event(interaction, action=announce)
    EventCog->>EventCog: handle_announce_action(interaction)
    EventCog->>EventCog: events = self.events[guild_id]
    EventCog->>DiscordClient: interaction.response.defer(ephemeral=True)
    EventCog->>EventDropdownView: create EventDropdownView(events, cog, placeholder, _on_announce_select)
    EventCog->>DiscordClient: followup.send(view=EventDropdownView)

    User->>EventDropdownView: select event
    EventDropdownView->>EventCog: _on_announce_select(interaction, selected_event)
    EventCog->>EventCog: find announcement_channel in guild.text_channels
    EventCog->>EventCog: announce_embed = _create_announcement_embed(selected_event)
    EventCog->>AnnouncementChannel: send(embed=announce_embed)
    AnnouncementChannel-->>EventCog: message
    EventCog->>AnnouncementChannel: message.add_reaction("✅")
    EventCog->>AnnouncementChannel: message.add_reaction("❌")
    EventCog->>EventCog: event_announcements[guild_id][event_name] = message.id
    EventCog->>DiscordClient: interaction.response.send_message(success_embed, ephemeral=True)

    User->>AnnouncementChannel: reacts with ✅ on announcement

    User->>DiscordClient: /event action=myevents
    DiscordClient->>EventCog: event(interaction, action=myevents)
    EventCog->>EventCog: handle_myevents_action(interaction)
    EventCog->>EventCog: events = self.events[guild_id]
    EventCog->>DiscordClient: interaction.response.defer(ephemeral=True)

    loop for each upcoming event
        EventCog->>EventCog: _is_user_registered(event, guild, user)
        EventCog->>EventCog: message_id = event_announcements[guild.id][event.event_name]
        EventCog->>AnnouncementChannel: fetch_message(message_id)
        AnnouncementChannel-->>EventCog: message
        EventCog->>EventCog: inspect message.reactions for emoji ✅ containing user
        EventCog-->>EventCog: True/False
    end

    EventCog->>DiscordClient: interaction.followup.send(embed=registered_events_embed, ephemeral=True)
Loading

Class diagram for new Event cog and schemas

classDiagram
    class EventSchema {
        <<pydantic.BaseModel>>
        +str event_name
        +date event_date
        +time event_time
        +str location
        +str description
        +_parse_event_date(value)
        +_parse_event_time(value)
    }

    class EventDropdownSelect {
        <<discord.ui.Select>>
        +EventDropdownView view_ref
        +__init__(options, view, placeholder)
        +callback(interaction)
    }

    class EventDropdownView {
        <<BaseView>>
        +list~EventSchema~ event_list
        +Event cog
        +on_select(interaction, selected_event)
        +__init__(events, cog, placeholder, on_select_callback)
    }

    class ConfirmDeleteView {
        <<BaseView>>
        +bool value
        +__init__()
        +confirm(interaction, _button)
        +cancel(interaction, _button)
    }

    class Event {
        <<commands.Cog>>
        +commands.Bot bot
        +Logger log
        +dict~int, list~EventSchema~~ events
        +dict~int, dict~str, int~~ event_announcements
        +__init__(bot)
        +event(interaction, action)
        +handle_create_action(interaction)
        +handle_edit_action(interaction)
        +handle_show_action(interaction)
        +handle_delete_action(interaction)
        +handle_list_action(interaction)
        +handle_announce_action(interaction)
        +handle_myevents_action(interaction)
        +_is_user_registered(event, guild, user)
        +_on_edit_select(interaction, selected_event)
        +_on_announce_select(interaction, selected_event)
        +_create_announcement_embed(event)
        +_handle_event_submit(interaction, event)
        +_create_event_embed(event)
        +_handle_event_update(interaction, updated_event, original_event)
        +_on_show_select(interaction, selected_event)
        +_on_delete_select(interaction, selected_event)
    }

    class BaseView {
    }

    class commands_Cog {
    }

    class pydantic_BaseModel {
    }

    EventDropdownSelect --> EventDropdownView : references view_ref
    EventDropdownView --> EventSchema : uses EventSchema as event_list
    EventDropdownView --> Event : holds cog reference
    ConfirmDeleteView --> BaseView : inherits
    EventDropdownView --> BaseView : inherits
    Event --> commands_Cog : inherits
    EventSchema --> pydantic_BaseModel : inherits
Loading

File-Level Changes

Change Details Files
Add Pydantic EventSchema model for validating and parsing event form inputs.
  • Define EventSchema with name, date, time, location, and description fields tailored for UI forms
  • Implement pre-validators to parse user-entered date strings in MM-DD-YYYY format into date objects
  • Implement pre-validators to parse 24-hour and 12-hour (AM/PM) time strings into naive time objects
capy_discord/exts/event/_schemas.py
Implement Event cog with a slash command and sub-actions for event CRUD, listing, announcement, and per-user RSVP views using embeds, modals, and views.
  • Register /event app command with an action choice that routes to dedicated handlers for create, edit, show, delete, list, announce, and myevents
  • Add in-memory guild-scoped event store and announcement message tracking for RSVP checks (with TODO-style [DB CALL] comments)
  • Use ModelModal bound to EventSchema to create and edit events, including pre-filled initial data when editing
  • Implement list/show/myevents flows that build rich embeds, split upcoming vs past events, and normalize timestamps with local or UTC timezones
  • Implement announcement flow that finds an 'announce' text channel, posts a structured announcement embed, adds RSVP reactions, and records the message ID
  • Implement RSVP membership check by fetching the stored announcement message and scanning ✅ reactions for a given user
  • Add UI views: a reusable dropdown-based EventDropdownView for selecting events by index and a ConfirmDeleteView with Delete/Cancel buttons, including state handling and ephemeral interactions
capy_discord/exts/event/event.py
Expose the new event extension as a Python module.
  • Add package init to mark the event extension module and provide a descriptive docstring
capy_discord/exts/event/__init__.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In _is_user_registered, the list comprehension users = [user async for user in reaction.users()] both shadows the user parameter and loads all users into memory; consider iterating the async iterator directly and comparing IDs (e.g., async for u in reaction.users(): if u.id == user.id: ...).
  • The logic to combine event_date and event_time and normalize timezone is duplicated in several places (handle_list_action, handle_myevents_action, _create_announcement_embed, _create_event_embed); extracting a small helper (e.g., _event_datetime(event: EventSchema) -> datetime) would reduce repetition and the risk of inconsistencies.
  • For self.event_announcements, using event.event_name as the key can lead to collisions (e.g., events with the same name or after renames); consider using a more stable identifier (such as an index, UUID, or a dedicated event ID) to track announcement messages.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_is_user_registered`, the list comprehension `users = [user async for user in reaction.users()]` both shadows the `user` parameter and loads all users into memory; consider iterating the async iterator directly and comparing IDs (e.g., `async for u in reaction.users(): if u.id == user.id: ...`).
- The logic to combine `event_date` and `event_time` and normalize timezone is duplicated in several places (`handle_list_action`, `handle_myevents_action`, `_create_announcement_embed`, `_create_event_embed`); extracting a small helper (e.g., `_event_datetime(event: EventSchema) -> datetime`) would reduce repetition and the risk of inconsistencies.
- For `self.event_announcements`, using `event.event_name` as the key can lead to collisions (e.g., events with the same name or after renames); consider using a more stable identifier (such as an index, UUID, or a dedicated event ID) to track announcement messages.

## Individual Comments

### Comment 1
<location> `capy_discord/exts/event/event.py:419-421` </location>
<code_context>
+            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
</code_context>

<issue_to_address>
**suggestion:** Factor out the repeated logic for locating the announcement channel.

This "first text channel with 'announce' in the name" lookup is duplicated in `_is_user_registered` and `_on_announce_select`. Please extract it into a shared helper like `_get_announcement_channel(guild)` so the selection rules remain consistent and easier to maintain.

Suggested implementation:

```python
        # 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

        def _get_announcement_channel(guild: discord.Guild) -> discord.TextChannel | None:
            """Return the first text channel whose name contains 'announce'."""
            for channel in guild.text_channels:
                if "announce" in channel.name.lower():
                    return channel
            return None

        # Try to find the announcement message and check reactions
        announcement_channel = _get_announcement_channel(guild)

        if not announcement_channel:
            return False

```

To fully apply the review comment and avoid duplicating the lookup logic:
1. Extract `_get_announcement_channel(guild)` to a shared helper (e.g. a private method on the cog class or a module-level function) instead of a local function, so both `_is_user_registered` and `_on_announce_select` can use it.
2. In `_on_announce_select`, replace the duplicated loop that finds the "announce" text channel with a call to the shared `_get_announcement_channel(guild)` helper.
3. Remove any now-unused local implementations of the announcement channel lookup to ensure there is a single source of truth for the selection rules.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread capy_discord/exts/event/event.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new /event Discord cog to scaffold event creation/editing/listing/announcement flows, including a Pydantic-backed modal schema and supporting UI views.

Changes:

  • Introduces an Event cog with slash-command actions (create/edit/show/delete/list/announce/myevents).
  • Adds EventSchema (Pydantic) for modal-driven event data entry and parsing.
  • Adds the capy_discord.exts.event package initializer.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
capy_discord/exts/event/event.py New Event cog implementation with dropdown selection views, announcement posting, and RSVP lookup logic.
capy_discord/exts/event/_schemas.py New Pydantic schema for event fields, including custom parsing validators for date/time string inputs.
capy_discord/exts/event/__init__.py Adds module docstring for the new extension package.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +42
@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)

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New date/time parsing and validation logic in EventSchema is user-facing (it drives the modal input format) but currently has no unit tests. Adding tests for accepted formats (e.g., MM-DD-YYYY, HH:MM, HH:MM AM/PM) and failure/blank-input behavior would help prevent regressions, similar to how other cogs are covered under tests/capy_discord/exts/.

Copilot uses AI. Check for mistakes.
Comment thread capy_discord/exts/event/event.py Outdated
Comment on lines +245 to +263
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)

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle_list_action classifies events using timezone-normalized datetimes, but then sorts with datetime.combine(...) again without normalizing tzinfo. If some EventSchema.event_time values are tz-aware (e.g., from the default_factory) and others are naive (e.g., parsed input strips tzinfo), Python will raise TypeError: can't compare offset-naive and offset-aware datetimes during sorting or produce inconsistent ordering. Consider centralizing event datetime construction in a helper that always returns an aware datetime (or always naive) and use it consistently for both classification and sorting.

Copilot uses AI. Check for mistakes.
Comment on lines +362 to +367
# 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))

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registered_events.sort(key=lambda e: datetime.combine(e.event_date, e.event_time)) has the same naive/aware datetime mixing risk as the list command. If any events have tz-aware event_time while others are naive, sorting will throw a TypeError. Reuse the same timezone-normalizing helper used for comparisons so sorting is stable and error-free.

Copilot uses AI. Check for mistakes.
Comment on lines +489 to +496
# 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

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

announcement_channel.permissions_for(guild.me) can raise if guild.me is None (it’s a cached member and may be missing depending on intents/cache state). Use guild.membership/guild.get_member(self.bot.user.id)/interaction.guild.me with a fallback, or announcement_channel.permissions_for(guild.self_member) (if available in your discord.py version) to avoid an AttributeError and ensure permissions are checked against the bot member.

Copilot uses AI. Check for mistakes.
Comment on lines +433 to +435
users = [user async for user in reaction.users()]
if user in users:
return True

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

users = [user async for user in reaction.users()] materializes all users for the reaction just to check membership, which can be slow and memory-heavy on large reactions. Prefer iterating the async iterator and short-circuiting on the first matching user.id (and avoid fetching users for unrelated emojis).

Suggested change
users = [user async for user in reaction.users()]
if user in users:
return True
async for reacted_user in reaction.users():
if reacted_user.id == user.id:
return True

Copilot uses AI. Check for mistakes.
if original_event in guild_events:
idx = guild_events.index(original_event)
guild_events[idx] = updated_event

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event_announcements is keyed by event.event_name. If an event is edited and its name changes, RSVP tracking for that event will break because _is_user_registered will look up the new name while the announcement map still uses the old one. Consider adding a stable event_id field to EventSchema (hidden from the modal via json_schema_extra) and key event_announcements by that ID, or update the mapping when the name changes during _handle_event_update.

Suggested change
# Keep any announcement / RSVP mapping in sync if the event name changed.
if original_event.event_name != updated_event.event_name:
announcements = getattr(self, "event_announcements", None)
if isinstance(announcements, dict):
guild_announcements = announcements.get(guild_id)
if isinstance(guild_announcements, dict) and original_event.event_name in guild_announcements:
guild_announcements[updated_event.event_name] = guild_announcements.pop(
original_event.event_name
)

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +42
event_date: date = Field(
title="Event Date",
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)

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventSchema.event_date / event_time have defaults, and ModelModal marks them as not-required, but the modal will still pass empty strings ("") when users leave the inputs blank. The validators currently attempt to parse "" and will raise a validation error instead of falling back to the default. Treat empty/whitespace-only strings as missing (e.g., return PydanticUndefined/None and let defaults apply) or make these fields explicitly required in the UI to match the validation behavior.

Copilot uses AI. Check for mistakes.
"""
super().__init__(timeout=60)
self.event_list = events
self.cog = cog

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventDropdownView.__init__ stores self.cog = cog, but that attribute is never referenced anywhere in this module. If it’s not needed, remove it to avoid dead state; if it is intended for future use, consider using it in on_select (or adding a comment) so it’s clear why it’s retained.

Suggested change
self.cog = cog

Copilot uses AI. Check for mistakes.
@shamikkarkhanis

Copy link
Copy Markdown
Member

Few things:

  • import announcement channel id from config file. it's fine if we hardcode in config for now but we should be importing it regardless.

  • consolidate the repeated timezone logic:
    The timezone logic is repeated 7 times across the file. It manually combines date/time and attempts to infer the local timezone every single time.
    Locations in capy_discord/exts/event/event.py:

  1. Line 250-253 (handle_list_action - calculating upcoming vs past)
  2. Line 274-277 (handle_list_action - display loop for upcoming)
  3. Line 288-291 (handle_list_action - display loop for past)
  4. Line 353-356 (handle_myevents_action - filtering upcoming)
  5. Line 384-387 (handle_myevents_action - display loop)
  6. Line 544-547 (_create_announcement_embed)
  7. Line 584-587 (_create_event_embed)
    we can move this into a helper method either defined in an event helper file if we have many helpers, or just in event.py

…nsolidated the timezone logic into helper methods
@simtiaz5
simtiaz5 requested a review from Copilot February 9, 2026 17:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 16 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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)

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfirmDeleteView is a BaseView but it’s sent via interaction.response.send_message(...) without setting view.message, so BaseView.on_timeout() can’t disable/edit the message on timeout. Prefer view.reply(...) or store view.message = await interaction.original_response() after sending so the timeout handler can update the UI.

Suggested change
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
await view.reply(interaction, embed=embed, ephemeral=True)

Copilot uses AI. Check for mistakes.
"""
super().__init__(timeout=60)
self.event_list = events
self.cog = cog

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventDropdownView.__init__ stores cog on self.cog but the attribute is never used. Removing the parameter/attribute (or using it for something concrete like permission checks) will reduce dead state and make the view easier to reuse.

Suggested change
self.cog = cog

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +19
event_date: date = Field(
title="Event Date",
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(),
)

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event_date/event_time use default_factory, which makes them non-required in Pydantic, so ModelModal will render them as optional with no default text. If the user leaves them blank, Discord submits an empty string and the validators will raise a parsing error instead of using the default. Either (1) make these fields required (remove defaults) or (2) treat blank strings as “missing” in the validators and substitute the intended default, and/or pass initial_data in the create modal to prefill current date/time.

Copilot uses AI. Check for mistakes.
Comment on lines +270 to +286
# 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:** <t:{timestamp}:F>\n**Where:** {event.location or 'TBD'}",
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:** <t:{timestamp}:F>\n**Where:** {event.location or 'TBD'}",
inline=False,
)

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list output builds a single embed field per event. Discord embeds are limited to 25 fields; if a guild has more than 25 events this will raise an HTTPException and the command will fail. Paginate the results (multiple embeds / multiple messages) or truncate with a clear indicator when the limit is reached.

Suggested change
# 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:** <t:{timestamp}:F>\n**Where:** {event.location or 'TBD'}",
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:** <t:{timestamp}:F>\n**Where:** {event.location or 'TBD'}",
inline=False,
)
# Prepare all event fields (upcoming first, then past)
event_fields: list[tuple[str, str]] = []
# Upcoming events
for event in upcoming_events:
timestamp = int(self._event_datetime(event).timestamp())
name = event.event_name
value = f"**When:** <t:{timestamp}:F>\n**Where:** {event.location or 'TBD'}"
event_fields.append((name, value))
# Past events with [OLD] prefix
for event in past_events:
timestamp = int(self._event_datetime(event).timestamp())
name = f"[OLD] {event.event_name}"
value = f"**When:** <t:{timestamp}:F>\n**Where:** {event.location or 'TBD'}"
event_fields.append((name, value))
# Discord embeds can have at most 25 fields; truncate with an indicator if needed
max_fields = 25
if len(event_fields) > max_fields:
# Show as many events as possible while reserving one field for the truncation notice
visible_fields = event_fields[: max_fields - 1]
remaining = len(event_fields) - len(visible_fields)
for name, value in visible_fields:
embed.add_field(name=name, value=value, inline=False)
embed.add_field(
name="More events not shown",
value=f"{remaining} additional event(s) could not be displayed due to Discord's embed field limit.",
inline=False,
)
else:
for name, value in event_fields:
embed.add_field(name=name, value=value, inline=False)

Copilot uses AI. Check for mistakes.
Comment on lines +439 to +441
users = [user async for user in reaction.users()]
if user in users:
return True

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_is_user_registered materializes the entire reaction user list (users = [u async for u in reaction.users()]) just to check membership. For large announcements this can be slow and memory-heavy. Iterate the async generator and short-circuit on a matching user id instead of building a list.

Suggested change
users = [user async for user in reaction.users()]
if user in users:
return True
async for reaction_user in reaction.users():
if reaction_user.id == user.id:
return True

Copilot uses AI. Check for mistakes.
Comment on lines +491 to +498
# 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

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

announcement_channel.permissions_for(guild.me) can raise if guild.me is None (bot member not cached). Resolve the bot’s member safely (e.g., guild.get_member(bot.user.id) or fetch) and handle the None case before calling permissions_for.

Copilot uses AI. Check for mistakes.
# [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

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Announcement tracking uses event_name as the key (event_announcements[guild.id][selected_event.event_name] = message.id). This breaks RSVP tracking if an event is renamed via edit, and collides if two events share the same name. Use a stable unique identifier for events (e.g., generated id/uuid stored in the schema) and key announcements by that id; also ensure edit/delete update/remove the associated announcement mapping.

Suggested change
self.event_announcements[guild.id][selected_event.event_name] = message.id
self.event_announcements[guild.id][selected_event.id] = message.id

Copilot uses AI. Check for mistakes.
Comment thread capy_discord/exts/event/event.py Outdated
Comment on lines +259 to +260
upcoming_events.sort(key=lambda e: self._event_datetime(e))
past_events.sort(key=lambda e: self._event_datetime(e), reverse=True)

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment thread capy_discord/exts/event/event.py Outdated
Comment on lines +259 to +260
upcoming_events.sort(key=lambda e: self._event_datetime(e))
past_events.sort(key=lambda e: self._event_datetime(e), reverse=True)

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment thread capy_discord/exts/event/event.py Outdated
if await self._is_user_registered(event, guild, interaction.user):
registered_events.append(event)

registered_events.sort(key=lambda e: self._event_datetime(e))

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.

Suggested change
registered_events.sort(key=lambda e: self._event_datetime(e))
registered_events.sort(key=self._event_datetime)

Copilot uses AI. Check for mistakes.
… time handling from system timestamp to EST time.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 11 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread capy_discord/config.py
Comment on lines +28 to +30
# Event System Configuration
announcement_channel_name: str = "test-announcements"

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new setting is introduced but example.env doesn’t document it. Consider adding ANNOUNCEMENT_CHANNEL_NAME= (or whatever env var name is expected by pydantic-settings) to example.env/README so deployments know how to configure the announcement channel.

Copilot uses AI. Check for mistakes.
if not events:
return

options = [discord.SelectOption(label=event.event_name[:100], value=str(i)) for i, event in enumerate(events)]

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discord select menus support a maximum of 25 options. options = [...] for i, event in enumerate(events) will raise when a guild has >25 events. Consider limiting to 25 (as done in exts/tools/hotswap.py) and/or implementing pagination.

Suggested change
options = [discord.SelectOption(label=event.event_name[:100], value=str(i)) for i, event in enumerate(events)]
max_options = 25
if len(events) > max_options:
logging.warning(
"EventDropdownView received %d events, truncating to %d options for Discord select menu.",
len(events),
max_options,
)
options = [
discord.SelectOption(label=event.event_name[:100], value=str(i))
for i, event in enumerate(events[:max_options])
]

Copilot uses AI. Check for mistakes.
Comment thread capy_discord/exts/event/event.py Outdated
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)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same timeout/message-tracking issue: the delete dropdown view is sent with followup.send without setting view.message, so BaseView.on_timeout can’t disable the menu. Consider capturing the sent message and storing it on the view (or use view.reply).

Suggested change
await interaction.followup.send(content="Select an event to delete:", view=view, ephemeral=True)
message = await interaction.followup.send(content="Select an event to delete:", view=view, ephemeral=True)
view.message = message

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +138
@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="edit", value="edit"),
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"),
app_commands.Choice(name="myevents", value="myevents"),
]
)
async def event(self, interaction: discord.Interaction, action: app_commands.Choice[str]) -> None:
"""Manage events based on the action specified."""
match action.value:
case "create":
await self.handle_create_action(interaction)
case "edit":
await self.handle_edit_action(interaction)
case "show":
await self.handle_show_action(interaction)
case "delete":
await self.handle_delete_action(interaction)
case "list":
await self.handle_list_action(interaction)
case "announce":
await self.handle_announce_action(interaction)
case "myevents":
await self.handle_myevents_action(interaction)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No automated tests are added for the new /event command flows. There are existing pytest-based cog tests under tests/capy_discord/exts/; adding tests for at least the core command routing and event CRUD handlers would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +31
event_date: date = Field(
title="Event Date",
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

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event_date/event_time have default_factory, so ModelModal will mark them as not required. However, if the user leaves the inputs blank, Discord sends an empty string and the validators will attempt to strptime(''), causing validation errors instead of using defaults. Consider either making these fields required (no defaults) or updating the validators to treat empty/whitespace-only strings as “missing” and return the default value.

Copilot uses AI. Check for mistakes.
Comment on lines +353 to +377
# 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:
timestamp = int(self._event_datetime(event).timestamp())
embed.add_field(
name=event.event_name,
value=f"**When:** <t:{timestamp}:F>\n**Where:** {event.location or 'TBD'}",
inline=False,
)

await interaction.followup.send(embed=embed, ephemeral=True)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Embeds have a 25-field limit; myevents adds one field per registered event and can fail for users registered for many events. Consider pagination/splitting into multiple embeds or limiting output.

Suggested change
# 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:
timestamp = int(self._event_datetime(event).timestamp())
embed.add_field(
name=event.event_name,
value=f"**When:** <t:{timestamp}:F>\n**Where:** {event.location or 'TBD'}",
inline=False,
)
await interaction.followup.send(embed=embed, ephemeral=True)
# Build embed(s)
base_title = "Your Registered Events"
base_description = "Events you have registered for by reacting with ✅"
if not registered_events:
embed = discord.Embed(
title=base_title,
description=(
"You haven't registered for any upcoming events.\n"
"React to event announcements with ✅ to register!"
),
color=discord.Color.purple(),
)
await interaction.followup.send(embed=embed, ephemeral=True)
return
# Discord embeds support up to 25 fields. Paginate events in chunks of 25.
total_pages = (len(registered_events) + 24) // 25
for page_index, start in enumerate(range(0, len(registered_events), 25)):
end = start + 25
page_events = registered_events[start:end]
embed = discord.Embed(
title=base_title,
description=base_description,
color=discord.Color.purple(),
)
for event in page_events:
timestamp = int(self._event_datetime(event).timestamp())
embed.add_field(
name=event.event_name,
value=(
f"**When:** <t:{timestamp}:F>\n"
f"**Where:** {event.location or 'TBD'}"
),
inline=False,
)
if total_pages > 1:
embed.set_footer(text=f"Page {page_index + 1}/{total_pages}")
await interaction.followup.send(embed=embed, ephemeral=True)

Copilot uses AI. Check for mistakes.
Comment on lines +347 to +349
# Check if user has registered for this event
if await self._is_user_registered(event, guild, interaction.user):
registered_events.append(event)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/myevents calls _is_user_registered for every upcoming event, and _is_user_registered performs an HTTP fetch_message each time. This becomes N API calls per command and can hit rate limits in guilds with many events. Consider persisting RSVP data when reactions are added/removed (reaction listeners), caching announcement messages/reactions, or fetching messages in batches where possible.

Copilot uses AI. Check for mistakes.
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)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user clicks Cancel or the confirmation view times out (view.value is False/None), this handler sends no follow-up message. Consider explicitly handling those cases (e.g., info embed for cancel and a timeout notice) so the user gets feedback and the flow completes cleanly.

Suggested change
await interaction.followup.send(embed=success, ephemeral=True)
await interaction.followup.send(embed=success, ephemeral=True)
elif view.value is False:
cancel_embed = discord.Embed(
title="Deletion Cancelled",
description="The event was not deleted.",
color=discord.Color.orange(),
)
await interaction.followup.send(embed=cancel_embed, ephemeral=True)
else:
timeout_embed = discord.Embed(
title="Timed Out",
description="Event deletion was not confirmed in time, so no changes were made.",
color=discord.Color.orange(),
)
await interaction.followup.send(embed=timeout_embed, ephemeral=True)

Copilot uses AI. Check for mistakes.
Comment on lines +514 to +515
self.event_announcements[guild.id][selected_event.event_name] = message.id

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RSVP tracking keys announcements by event.event_name. If two events share a name, or if an event is renamed via edit, the mapping will collide/break and registrations may be attributed incorrectly. Consider storing a stable identifier for events (e.g., generated event_id) and key the announcement mapping by that instead; also consider cleaning up mappings on delete/edit.

Suggested change
self.event_announcements[guild.id][selected_event.event_name] = message.id
# Prefer a stable event identifier (e.g., database id) over the mutable name
event_key = (
getattr(selected_event, "id", None)
or getattr(selected_event, "event_id", None)
or selected_event.event_name
)
self.event_announcements[guild.id][event_key] = message.id

Copilot uses AI. Check for mistakes.
Comment thread capy_discord/exts/event/event.py Outdated
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)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same timeout/message-tracking issue for the announce dropdown: since the view is sent via followup.send without storing the message on the view, timeouts won’t disable the menu.

Suggested change
await interaction.followup.send(content="Select an event to announce:", view=view, ephemeral=True)
message = await interaction.followup.send(content="Select an event to announce:", view=view, ephemeral=True)
view.message = message

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +454 to +455
# Check if bot has permission to post in the channel
if not announcement_channel.permissions_for(guild.me).send_messages:

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guild.me can be None (e.g., member cache not available), which would cause permissions_for(guild.me) to raise at runtime. Resolve the bot member safely (e.g., guild.me or guild.get_member(self.bot.user.id) / fetch as needed) and handle the None case before checking permissions.

Suggested change
# Check if bot has permission to post in the channel
if not announcement_channel.permissions_for(guild.me).send_messages:
# Safely resolve the bot member before checking permissions
bot_member: discord.Member | None = guild.me
bot_user = getattr(interaction.client, "user", None)
if bot_member is None and bot_user is not None:
bot_member = guild.get_member(bot_user.id)
if bot_member is None and bot_user is not None:
try:
bot_member = await guild.fetch_member(bot_user.id)
except (discord.HTTPException, discord.Forbidden, discord.NotFound):
bot_member = None
if bot_member is None:
embed = error_embed(
"Permission Check Failed",
"I couldn't determine my permissions in the announcement channel. "
"Please check my role and try again.",
)
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(bot_member).send_messages:

Copilot uses AI. Check for mistakes.
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)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This view is sent via interaction.followup.send(...), but BaseView's timeout handling relies on self.message being set to edit/disable components. As written, EventDropdownView never records the message, so on-timeout it won't update the UI. Capture the sent message (e.g., use wait=True and assign to view.message, or add a helper on BaseView for followup sends).

Suggested change
await interaction.followup.send(content=f"Select an event to {action_name}:", view=view, ephemeral=True)
message = await interaction.followup.send(
content=f"Select an event to {action_name}:",
view=view,
ephemeral=True,
wait=True,
)
view.message = message

Copilot uses AI. Check for mistakes.
Comment on lines +386 to +387
message_id = guild_announcements.get(event.event_name)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RSVP tracking keys announcement messages by event.event_name, which is not stable (duplicates across events, and editing/renaming an event will break lookups). Use a stable identifier (e.g., add an event_id field) for event_announcements keys, or update the mapping when an event is renamed.

Suggested change
message_id = guild_announcements.get(event.event_name)
# Prefer a stable identifier (e.g., event.id or event.event_id) for lookups,
# but fall back to event_name to preserve existing behavior.
candidate_keys: list[Any] = []
if hasattr(event, "id"):
candidate_keys.append(getattr(event, "id"))
if hasattr(event, "event_id"):
candidate_keys.append(getattr(event, "event_id"))
candidate_keys.append(event.event_name)
message_id = None
for key in candidate_keys:
if key is None:
continue
message_id = guild_announcements.get(key)
if message_id:
break

Copilot uses AI. Check for mistakes.
Comment on lines +305 to +306
if not guild_id:
embed = error_embed("No Server", f"Events must be {action_name}ed in a server.")

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message builds past tense by appending "ed" to action_name, which produces incorrect strings like "deleteed" and "announceed". Use an explicit mapping for the user-facing verb/past tense (or pass the fully formatted verb into this helper) so messages read correctly for all actions.

Suggested change
if not guild_id:
embed = error_embed("No Server", f"Events must be {action_name}ed in a server.")
# Map action names to correctly formatted past tense/adjectival forms for messages.
action_past_map = {
"edit": "edited",
"view": "viewed",
"delete": "deleted",
"announce": "announced",
}
action_past = action_past_map.get(action_name, f"{action_name}ed")
if not guild_id:
embed = error_embed("No Server", f"Events must be {action_past} in a server.")

Copilot uses AI. Check for mistakes.
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)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfirmDeleteView is sent with interaction.response.send_message(...) but the view never sets BaseView.message, so on-timeout it can't edit the message to disable components / show timed-out state. Consider sending via view.reply(...) or setting view.message = await interaction.original_response() after sending.

Suggested change
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
view.message = await interaction.original_response()

Copilot uses AI. Check for mistakes.
Comment on lines +348 to +350
"""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")

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatter hardcodes the timezone label as "EST", but America/New_York observes DST, so in summer the correct label is "EDT". Use %Z in the format string (or a neutral label like "ET") so the displayed timezone matches the actual offset.

Suggested change
"""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")
"""Format an event's date/time in Eastern Time 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 %Z")

Copilot uses AI. Check for mistakes.
Comment on lines +402 to +404
users = [user async for user in reaction.users()]
if user in users:
return True

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This builds a full list of all users who reacted, which can be slow and memory-heavy for large events. Iterate the async users iterator and short-circuit on matching user.id (or otherwise avoid materializing the whole list).

Suggested change
users = [user async for user in reaction.users()]
if user in users:
return True
async for reacting_user in reaction.users():
if reacting_user.id == user.id:
return True

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +14
event_date: date = Field(
title="Event Date",
description="Date of the event (MM-DD-YYYY)",
default_factory=date.today,
)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event_date/event_time use default_factory, so ModelModal will mark these inputs as not required. If the user leaves an optional TextInput blank, Discord submits an empty string, and the current validators will try strptime on "", causing a validation error instead of falling back to the default. Consider making these fields required (remove defaults) and/or treating blank strings as missing in the validators so defaults apply.

Copilot uses AI. Check for mistakes.

@shamikkarkhanis shamikkarkhanis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fire !

@shamikkarkhanis
shamikkarkhanis merged commit d740fe5 into develop Feb 10, 2026
4 checks passed
@shamikkarkhanis
shamikkarkhanis deleted the feature/capr-23-scaffold-event_cog-from-deprecated-repo branch February 10, 2026 18:02
@shamikkarkhanis
shamikkarkhanis restored the feature/capr-23-scaffold-event_cog-from-deprecated-repo branch February 10, 2026 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants