Skip to content

Conversation

@woweiseng
Copy link

@woweiseng woweiseng commented Nov 18, 2025

Summary by Sourcery

Introduce guild linking capability on profile creation and enhance profile deletion workflow with robust confirmation handling and cleanup

New Features:

  • Automatically link user profiles to Discord guilds upon creation, handling both server and DM contexts

Enhancements:

  • Add guards and correct save logic when deleting profiles from events and guilds
  • Refactor profile deletion command to include confirmation timeout, cancellation feedback, and pre-deletion cleanup
  • Store button interactions in the view to enable proper response handling

Chores:

  • Update import path for the Statistics module

@sourcery-ai
Copy link

sourcery-ai bot commented Nov 18, 2025

Reviewer's Guide

This PR adds automatic guild linkage during profile creation, tightens safety checks and response handling in the profile deletion flow, streamlines button confirmation views by storing interactions and refining timeouts, and fixes a misnamed import.

Sequence diagram for profile creation and automatic guild linkage

sequenceDiagram
    participant U as actor User
    participant D as Discord
    participant B as Bot
    participant PC as ProfileCog
    participant DB as Database
    participant G as Guild

    U->>D: Initiates profile creation (in guild or DM)
    D->>B: Sends interaction
    B->>PC: Handles _save_profile()
    PC->>DB: Checks for existing user
    alt New user
        PC->>DB: Adds new user document
        PC->>PC: Calls _link_user_to_guilds(interaction)
        PC->>PC: _get_target_guild_ids(interaction)
        alt In guild
            PC->>PC: Returns current guild ID
        else In DM
            PC->>B: Gets mutual guilds
            PC->>PC: Returns all mutual guild IDs
        end
        loop For each guild
            PC->>DB: bulk_update_attr(User, [user_id], "guilds", guild_id)
            PC->>PC: _add_user_to_guild(guild_id, user_id)
            PC->>DB: Ensure Guild document exists
            PC->>DB: Update Guild.users
        end
    end
    PC->>B: Logs success
Loading

Sequence diagram for improved profile deletion flow with confirmation and cleanup

sequenceDiagram
    participant U as actor User
    participant D as Discord
    participant B as Bot
    participant PC as ProfileCog
    participant V as ConfirmDeleteView
    participant DB as Database
    participant E as Event
    participant G as Guild

    U->>D: Requests profile deletion
    D->>B: Sends interaction
    B->>PC: Handles delete_profile()
    PC->>V: Shows confirmation view
    U->>V: Presses Accept/Cancel or times out
    V->>PC: Returns value and interaction
    alt Accept pressed
        PC->>V: Responds with "Deleting your profile..."
        PC->>PC: Calls delete_profile_from_events(user)
        loop For each event
            PC->>E: Removes user from event
            E->>DB: Saves event
        end
        PC->>PC: Calls delete_profile_from_guilds(user)
        loop For each guild
            PC->>G: Removes user from guild
            G->>DB: Saves guild
        end
        PC->>DB: Deletes user document
        PC->>V: Responds with success message
    else Cancel pressed
        PC->>V: Responds with cancellation message
    else Timeout
        PC->>V: Responds with timeout message
    end
Loading

Entity relationship diagram for User and Guild linkage

erDiagram
    USER {
        int id
        list guilds
        list events
    }
    GUILD {
        int id
        list users
    }
    USER ||--o{ GUILD : "belongs to"
    GUILD ||--o{ USER : "has users"
Loading

Class diagram for updated ProfileCog and confirmation view classes

classDiagram
    class ProfileCog {
        +_save_profile(interaction, profile_data_dict)
        +_get_target_guild_ids(interaction)
        +_ensure_guild_exists(guild_id)
        +_add_user_to_guild(guild_id, user_id)
        +_link_user_to_guilds(interaction)
        +delete_profile(interaction)
    }
    class ConfirmDeleteView {
        +timeout: int
        +value: bool | None
        +interaction: Interaction | None
        +wait()
    }
    class AcceptCancelView {
        +value: bool | None
        +interaction: Interaction | None
        +accept(interaction, button)
        +cancel(interaction, button)
    }
    ConfirmDeleteView --|> AcceptCancelView
Loading

File-Level Changes

Change Details Files
Safe cleanup in profile deletion utilities
  • Guard delete_profile_from_events/guilds on missing attributes
  • Early return if user.events or user.guilds is not present
  • Adjust save calls to only occur when modifications happen
src/capy_app/frontend/cogs/features/profile_cog.py
Automatic guild linking on profile creation
  • Invoke _link_user_to_guilds after creating a new profile
  • Implement _get_target_guild_ids to determine server vs DM context
  • Create _ensure_guild_exists and _add_user_to_guild to manage Guild docs
  • Develop _link_user_to_guilds for bulk updating User.guilds and Guild.users
src/capy_app/frontend/cogs/features/profile_cog.py
Enhanced profile deletion confirmation flow
  • Add timeout parameter to ConfirmDeleteView
  • Store the confirm button interaction on click
  • Handle success, cancellation, and timeout branches with appropriate edits
  • Remove user from events and guilds before deleting the document
src/capy_app/frontend/cogs/features/profile_cog.py
Streamlined Accept/Cancel button view
  • Remove defer calls and status messaging in accept/cancel handlers
  • Store interaction reference for downstream response
  • Remove redundant stop() invocation
src/capy_app/frontend/interactions/bases/button_base.py
Fix import path for Statistics module
  • Update import from capy_app.stats to stats
src/capy_app/frontend/bot.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

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/capy_app/frontend/cogs/features/profile_cog.py:922` </location>
<code_context>
             return

-        view = ConfirmDeleteView()
+        view = ConfirmDeleteView(timeout=60)
         await interaction.edit_original_response(
             content="⚠️ Are you sure you want to delete your profile? This action cannot be undone.",
</code_context>

<issue_to_address>
**suggestion:** Consider making the timeout value configurable.

Hardcoding the timeout to 60 seconds reduces flexibility. Making it configurable would better support different use cases.

Suggested implementation:

```python
    async def show_profile_embed(
        self,
        message_or_interaction: discord.Message | discord.Interaction,
        delete_profile_timeout: int = 60,

```

```python
        view = ConfirmDeleteView(timeout=delete_profile_timeout)

```

If `show_profile_embed` is called from other places, you may want to pass the `delete_profile_timeout` argument from those call sites, or rely on the default value.
You may also want to document the new parameter in the function's docstring.
</issue_to_address>

### Comment 2
<location> `src/capy_app/frontend/interactions/bases/button_base.py:85` </location>
<code_context>
         self.value = True
         self._completed = True
-        await self._send_status_message("Operation accepted")
         self.stop()

     @discord.ui.button(label="Cancel", style=ButtonStyle.danger)
</code_context>

<issue_to_address>
**nitpick:** Duplicate call to self.stop() in cancel handler.

Please remove the redundant 'self.stop()' call to simplify the method.
</issue_to_address>

### Comment 3
<location> `src/capy_app/frontend/cogs/features/profile_cog.py:800` </location>
<code_context>
+            # Non-fatal: log and continue
+            self.logger.error(f"Failed to add user {user_id} to Guild.users for {guild_id}: {e}")
+
+    async def _link_user_to_guilds(self, interaction: discord.Interaction) -> None:
+        """Link a user to their guilds after profile creation.
+
</code_context>

<issue_to_address>
**issue (complexity):** Consider merging duplicate loops and consolidating error handling in profile-guild linking and event RSVP removal functions to simplify the code.

Here are two small refactorings that keep all existing behavior but remove a lot of the duplicated loops, logging and try/catch scaffolding:

1) **Merge the two loops in `_link_user_to_guilds` into one**  
   This folds the “add to User.guilds” and “add to Guild.users” steps into a single pass:

```python
async def _link_user_to_guilds(self, interaction: discord.Interaction) -> None:
    user_id = interaction.user.id
    guild_ids = self._get_target_guild_ids(interaction)
    if not guild_ids:
        self.logger.warning(f"No guilds found for user {user_id}")
        return

    success = 0
    for gid in guild_ids:
        try:
            # 1) update the user side
            Database.bulk_update_attr(User, [user_id], "guilds", gid)

            # 2) update the guild side
            guild = self._ensure_guild_exists(gid)
            if user_id not in (guild.users or []):
                guild.users = sorted((guild.users or []) + [user_id])
                Database.update_document(guild, {"users": guild.users})

            success += 1
        except Exception as e:
            self.logger.error(f"Failed linking user {user_id} ↔ guild {gid}: {e}")

    self.logger.info(f"Linked user {user_id} to {success}/{len(guild_ids)} guild(s)")
```

2) **Collapse the three RSVP‐removals inside `delete_profile_from_events`**  
   Use a small map to drive all three cases and call `save()` once:

```python
async def delete_profile_from_events(user):
    if not getattr(user, "events", None):
        return

    for eid in user.events:
        event = Database.get_document(Event, eid)
        if not event:
            continue

        for attr, reaction in (
            ("yes_users",   "yes"),
            ("no_users",    "no"),
            ("maybe_users", "maybe"),
        ):
            users = getattr(event, attr)
            if user.id in users:
                users.remove(user.id)
                event.details.reactions.modify(reaction, -1)

        event.save()
```

These two changes

- Remove duplicate `for …` loops and nested `try/except`s  
- Consolidate logging to one place each  
- Keep exactly the same side‐effects (DB writes + reaction modifications)
</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.

return

view = ConfirmDeleteView()
view = ConfirmDeleteView(timeout=60)
Copy link

Choose a reason for hiding this comment

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

suggestion: Consider making the timeout value configurable.

Hardcoding the timeout to 60 seconds reduces flexibility. Making it configurable would better support different use cases.

Suggested implementation:

    async def show_profile_embed(
        self,
        message_or_interaction: discord.Message | discord.Interaction,
        delete_profile_timeout: int = 60,
        view = ConfirmDeleteView(timeout=delete_profile_timeout)

If show_profile_embed is called from other places, you may want to pass the delete_profile_timeout argument from those call sites, or rely on the default value.
You may also want to document the new parameter in the function's docstring.

self.value = True
self._completed = True
await self._send_status_message("Operation accepted")
self.stop()
Copy link

Choose a reason for hiding this comment

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

nitpick: Duplicate call to self.stop() in cancel handler.

Please remove the redundant 'self.stop()' call to simplify the method.

@shamikkarkhanis shamikkarkhanis merged commit 04c1114 into develop Dec 12, 2025
1 of 7 checks passed
@shamikkarkhanis shamikkarkhanis deleted the feature/cap-236-guild branch December 12, 2025 21:34
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.

4 participants