Skip to content

CAPR-21: Port purge_cog from deprecated repo - #68

Closed
YC-5002 wants to merge 2 commits into
developfrom
feature/purge-branch-update
Closed

CAPR-21: Port purge_cog from deprecated repo#68
YC-5002 wants to merge 2 commits into
developfrom
feature/purge-branch-update

Conversation

@YC-5002

@YC-5002 YC-5002 commented Feb 6, 2026

Copy link
Copy Markdown

Implemented single line command for deleting messages for both amount and duration mode

Summary by Sourcery

Introduce a purge cog providing flexible message deletion options via a slash command and update a modal generic bound.

New Features:

  • Add a /purge slash command that can delete messages by count, duration, or from a specific date using either direct arguments or an interactive UI.
  • Introduce interactive views and modals to collect purge parameters such as message count, duration, or date/time from the user.

Enhancements:

  • Tighten the generic type bound on CallbackModal to reference itself for improved type safety.

@sourcery-ai

sourcery-ai Bot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Ports a new Discord purge cog that supports deleting messages by count, duration, or specific date via both slash-command parameters and an interactive UI, and makes a small type-bound adjustment to CallbackModal in the UI module.

Sequence diagram for the new purge command interactions

sequenceDiagram
    actor User
    participant DiscordClient
    participant PurgeCog
    participant PurgeModeView
    participant DateTimeModal
    participant TextChannel

    User->>DiscordClient: invoke slash command purge(amount, duration)
    DiscordClient->>PurgeCog: purge(interaction, amount, duration)

    alt amount provided
        PurgeCog->>TextChannel: purge(limit=amount)
        TextChannel-->>PurgeCog: deleted_messages
        PurgeCog-->>DiscordClient: followup.send(success message)
        DiscordClient-->>User: ephemeral success message
    else duration provided
        PurgeCog->>PurgeCog: parse_duration(duration)
        PurgeCog->>TextChannel: purge(after=now-td)
        TextChannel-->>PurgeCog: deleted_messages
        PurgeCog-->>DiscordClient: followup.send(success message)
        DiscordClient-->>User: ephemeral success message
    else interactive mode (no params)
        PurgeCog->>PurgeModeView: create view
        PurgeCog->>DiscordClient: response.send_message(Select purge mode, view)
        DiscordClient-->>User: ephemeral view with mode_select

        User->>PurgeModeView: select mode (count | duration | date)
        PurgeModeView->>PurgeModeView: on_mode_selected(interaction)

        alt mode is count
            PurgeModeView->>DiscordClient: response.send_modal(Enter Count)
            User->>DiscordClient: submit modal
            DiscordClient->>PurgeModeView: _on_submit
            PurgeModeView->>PurgeModeView: set value(int)
        else mode is duration
            PurgeModeView->>DiscordClient: response.send_modal(Enter Duration)
            User->>DiscordClient: submit modal
            DiscordClient->>PurgeModeView: _on_submit
            PurgeModeView->>PurgeModeView: set value(str)
        else mode is date
            PurgeModeView->>DateTimeModal: create modal
            PurgeModeView->>DiscordClient: response.send_modal(DateTimeModal)
            User->>DiscordClient: submit modal
            DiscordClient->>DateTimeModal: on_submit
            DateTimeModal->>PurgeModeView: set value(datetime)
        end

        PurgeModeView->>PurgeModeView: stop view
        PurgeCog->>PurgeCog: _execute_purge(view, channel)
        alt view.mode is count
            PurgeCog->>TextChannel: purge(limit=value)
        else view.mode is duration
            PurgeCog->>PurgeCog: parse_duration(value)
            PurgeCog->>TextChannel: purge(after=now-td)
        else view.mode is date
            PurgeCog->>TextChannel: purge(after=date)
        end
        TextChannel-->>PurgeCog: deleted_messages
        PurgeCog-->>DiscordClient: followup.send(result message)
        DiscordClient-->>User: ephemeral result
    end
Loading

Updated class diagram for purge cog and related UI components

classDiagram
    class Modal
    class View
    class Cog
    class BaseModal

    class DateTimeModal {
        +DateTimeModal()
    }

    class PurgeModeView {
        +str mode
        +int_or_str_or_datetime value
        +mode_select
        +PurgeModeView()
        +on_mode_selected(interaction)
        +_prompt_count(interaction)
        +_prompt_duration(interaction)
        +_prompt_date(interaction)
    }

    class PurgeCog {
        +bot
        +logger
        +PurgeCog(bot)
        +parse_duration(duration)
        +_handle_purge_count(amount, channel)
        +_handle_purge_duration(duration, channel)
        +_handle_purge_date(date, channel)
        +purge(interaction, amount, duration)
        +_execute_purge(view, channel)
    }

    class CallbackModal~T~ {
        +CallbackModal(title, timeout)
    }

    Modal <|-- DateTimeModal
    View <|-- PurgeModeView
    Cog  <|-- PurgeCog
    BaseModal <|-- CallbackModal

    class T {
    }

    CallbackModal ..|> T
Loading

File-Level Changes

Change Details Files
Tighten the generic type constraint on CallbackModal for better type checking.
  • Update the generic type parameter declaration on CallbackModal to use a bound type constraint syntax compatible with the current type checker.
capy_discord/ui/modal.py
Introduce a purge cog that deletes messages by amount, duration, or specific date, supporting both single-line slash usage and an interactive mode selector.
  • Add DateTimeModal to collect a date and time via two text inputs for date-based purging.
  • Add PurgeModeView with a select menu to choose between message count, time duration, or specific date, and prompt the user for the appropriate parameter via modals.
  • Implement duration parsing helper that converts strings like '1d2h3m' into timedeltas.
  • Implement purge handlers for count, duration, and date that call channel.purge with appropriate arguments and return user-facing status messages.
  • Implement the /purge app command that supports direct arguments (amount or duration) or, if omitted, opens the PurgeModeView for interactive selection, then executes the purge with error handling and logging.
  • Register the new PurgeCog in an async setup function for extension loading.
capy_discord/exts/tools/purge.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 5 issues, and left some high level feedback:

  • The change to CallbackModal's generic definition (class CallbackModal[T: "CallbackModal"]) looks incorrect/recursive compared to the original TypeVar("T", bound="CallbackModal") and may confuse or break type checking; consider reverting or explicitly defining the generic parameters in a PEP 695–compatible way.
  • The duration argument help text ("1h 30m") conflicts with parse_duration, which only accepts a compact 1d2h3m-style string without spaces; either adjust the parser to handle spaces or update the user-facing examples to match the actual accepted format.
  • In the interactive purge path you always prefix the followup message with "Success " even when success is False, and you ignore the success flag in the direct amount/duration branch; consider using the flag to choose different messaging for success vs. failure and avoid the misleading prefix.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The change to `CallbackModal`'s generic definition (`class CallbackModal[T: "CallbackModal"]`) looks incorrect/recursive compared to the original `TypeVar("T", bound="CallbackModal")` and may confuse or break type checking; consider reverting or explicitly defining the generic parameters in a PEP 695–compatible way.
- The `duration` argument help text (`"1h 30m"`) conflicts with `parse_duration`, which only accepts a compact `1d2h3m`-style string without spaces; either adjust the parser to handle spaces or update the user-facing examples to match the actual accepted format.
- In the interactive purge path you always prefix the followup message with `"Success "` even when `success` is `False`, and you ignore the `success` flag in the direct `amount`/`duration` branch; consider using the flag to choose different messaging for success vs. failure and avoid the misleading prefix.

## Individual Comments

### Comment 1
<location> `capy_discord/ui/modal.py:25` </location>
<code_context>


-class CallbackModal[T](BaseModal):
+class CallbackModal[T: "CallbackModal"](BaseModal):
     """A modal that delegates submission logic to a callback function.

</code_context>

<issue_to_address>
**issue (bug_risk):** The generic type parameter declaration here mixes two different typing styles and is likely invalid.

`T` is first defined as `TypeVar("T", bound="CallbackModal")`, then redefined as a PEP 695-style parameter in `class CallbackModal[T: "CallbackModal"](BaseModal):`. This will confuse both the runtime and type checkers. Use one approach consistently:

- Keep `T = TypeVar(...)` and write `class CallbackModal(Generic[T], BaseModal):`, or
- Remove the standalone `TypeVar` and rely solely on the PEP 695 form.

Avoid mixing the two in the same class definition.
</issue_to_address>

### Comment 2
<location> `capy_discord/exts/tools/purge.py:153` </location>
<code_context>
+        if not duration:
+            return None
+
+        pattern = r"(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?"
+        match = re.match(pattern, duration)
+        if not match or not any(match.groups()):
+            return None
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The duration regex is not anchored and may accept unintended partial matches.

Since the pattern lacks `^` and `$`, `re.match` will accept valid prefixes of invalid strings (e.g. `"1d2x"` is parsed as 1 day, 2 hours, ignoring the trailing `"x"`). To require the whole string to match the `1d2h3m` format, anchor the regex:

```python
pattern = r"^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$"
```

This ensures `parse_duration` returns `None` for inputs with extra characters or spacing.

```suggestion
        pattern = r"^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$"
```
</issue_to_address>

### Comment 3
<location> `capy_discord/exts/tools/purge.py:198` </location>
<code_context>
+    @app_commands.command(name="purge", description="Delete messages")
+    @app_commands.describe(
+        amount="The number of messages to delete (e.g. 10)",
+        duration="The timeframe to delete messages from (e.g. 1h 30m)",
+    )
+    # @app_commands.checks.has_permissions(manage_messages=True)
</code_context>

<issue_to_address>
**issue (bug_risk):** Duration help text suggests a format that `parse_duration` does not actually accept.

`parse_duration` only accepts compact values like `1d2h3m` (no spaces), but the description shows `"1h 30m"`, which will fail with "Invalid duration format". Please either update the description to reflect the actual format (e.g. `"e.g. 1h30m or 2d3h"`) or update `parse_duration` to support optional whitespace.
</issue_to_address>

### Comment 4
<location> `capy_discord/exts/tools/purge.py:244-245` </location>
<code_context>
+            return
+
+        try:
+            success, message = await self._execute_purge(view, interaction.channel)
+            await interaction.followup.send(f"Success {message}", ephemeral=True)
+            if success:
+                self.logger.info(f"{interaction.user} purged messages in {interaction.channel} using {view.mode} mode")
</code_context>

<issue_to_address>
**issue (bug_risk):** Interactive purge path always prefixes messages with "Success" even when the purge fails.

Here `success, message` is used, but the followup always formats the response as a success, even when `success` is False. That produces misleading output for invalid modes/durations where the message is actually an error. Instead, branch on `success` when building the prefix, e.g. using explicit success/failure markers, or send `message` directly and have the handlers include any needed status indicators.
</issue_to_address>

### Comment 5
<location> `capy_discord/exts/tools/purge.py:275` </location>
<code_context>
+
+
+async def setup(bot: commands.Bot) -> None:
+    """Set up the Sync cog."""
+    await bot.add_cog(PurgeCog(bot))
</code_context>

<issue_to_address>
**nitpick (typo):** Setup function docstring mentions "Sync cog" instead of the actual PurgeCog.

Please update the docstring to reference `PurgeCog` so it matches the cog being registered.
</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/ui/modal.py


class CallbackModal[T](BaseModal):
class CallbackModal[T: "CallbackModal"](BaseModal):

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.

issue (bug_risk): The generic type parameter declaration here mixes two different typing styles and is likely invalid.

T is first defined as TypeVar("T", bound="CallbackModal"), then redefined as a PEP 695-style parameter in class CallbackModal[T: "CallbackModal"](BaseModal):. This will confuse both the runtime and type checkers. Use one approach consistently:

  • Keep T = TypeVar(...) and write class CallbackModal(Generic[T], BaseModal):, or
  • Remove the standalone TypeVar and rely solely on the PEP 695 form.

Avoid mixing the two in the same class definition.

if not duration:
return None

pattern = r"(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?"

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.

suggestion (bug_risk): The duration regex is not anchored and may accept unintended partial matches.

Since the pattern lacks ^ and $, re.match will accept valid prefixes of invalid strings (e.g. "1d2x" is parsed as 1 day, 2 hours, ignoring the trailing "x"). To require the whole string to match the 1d2h3m format, anchor the regex:

pattern = r"^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$"

This ensures parse_duration returns None for inputs with extra characters or spacing.

Suggested change
pattern = r"(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?"
pattern = r"^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$"

@app_commands.command(name="purge", description="Delete messages")
@app_commands.describe(
amount="The number of messages to delete (e.g. 10)",
duration="The timeframe to delete messages from (e.g. 1h 30m)",

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.

issue (bug_risk): Duration help text suggests a format that parse_duration does not actually accept.

parse_duration only accepts compact values like 1d2h3m (no spaces), but the description shows "1h 30m", which will fail with "Invalid duration format". Please either update the description to reflect the actual format (e.g. "e.g. 1h30m or 2d3h") or update parse_duration to support optional whitespace.

Comment on lines +244 to +245
success, message = await self._execute_purge(view, interaction.channel)
await interaction.followup.send(f"Success {message}", ephemeral=True)

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.

issue (bug_risk): Interactive purge path always prefixes messages with "Success" even when the purge fails.

Here success, message is used, but the followup always formats the response as a success, even when success is False. That produces misleading output for invalid modes/durations where the message is actually an error. Instead, branch on success when building the prefix, e.g. using explicit success/failure markers, or send message directly and have the handlers include any needed status indicators.



async def setup(bot: commands.Bot) -> None:
"""Set up the Sync cog."""

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.

nitpick (typo): Setup function docstring mentions "Sync cog" instead of the actual PurgeCog.

Please update the docstring to reference PurgeCog so it matches the cog being registered.

@shamikkarkhanis

Copy link
Copy Markdown
Member

Critical Standards Violations

  1. UI Pattern Violations - Lines 14-137
    • DateTimeModal (14-33): Direct discord.ui.Modal subclass - should use CallbackModal from capy_discord.ui.modal
    • PurgeModeView (36-137): Direct discord.ui.View subclass - MUST inherit from BaseView per standards
    • Manual modal creation (lines 69-82, 85-98): Should use CallbackModal or ModelModal
  2. Missing Global UI Abstractions - Lines 68-117
    • _prompt_count, _prompt_duration, _prompt_date contain reusable input collection patterns that should be in capy_discord/ui/forms.py
    • Duration parsing (148-162) is a generic utility - should be in a global helper
  3. Logging Pattern - Line 146
    • Uses f"discord.cog.{self.class.name.lower()}" - should use logging.getLogger(name) per standards
  4. Missing Error Handling - Lines 248-251
    • Bare except Exception with generic message - violates global error handling pattern
  5. Commented Security Check - Line 200
    • @app_commands.checks.has_permissions(manage_messages=True) is commented out - security issue

@YC-5002 YC-5002 closed this Feb 6, 2026
@YC-5002
YC-5002 deleted the feature/purge-branch-update branch February 6, 2026 21:45
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.

2 participants