From 4c1a762ff040d4e21c9c4fa0f51fccb505ed56a9 Mon Sep 17 00:00:00 2001 From: Cindy Yang Date: Thu, 5 Feb 2026 21:30:47 -0500 Subject: [PATCH 1/3] Updated purge to include single line command for amount and duration --- capy_discord/exts/tools/purge.py | 276 +++++++++++++++++++++++++++++++ capy_discord/ui/modal.py | 2 +- 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 capy_discord/exts/tools/purge.py diff --git a/capy_discord/exts/tools/purge.py b/capy_discord/exts/tools/purge.py new file mode 100644 index 0000000..dec5be4 --- /dev/null +++ b/capy_discord/exts/tools/purge.py @@ -0,0 +1,276 @@ +import logging +import re +from datetime import UTC, datetime, timedelta +from typing import Any, TYPE_CHECKING, cast + +import discord +from discord import app_commands +from discord.ext import commands + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + + +class DateTimeModal(discord.ui.Modal): + """Modal for date and time input.""" + + def __init__(self) -> None: + """Initialize the date time modal.""" + super().__init__(title="Enter Date and Time") + self.add_item( + discord.ui.TextInput( + label="Date (YYYY-MM-DD)", + placeholder="2024-02-08", + required=True, + ) + ) + self.add_item( + discord.ui.TextInput( + label="Time (HH:MM)", + placeholder="14:30", + required=True, + ) + ) + + +class PurgeModeView(discord.ui.View): + """Modal for Menu View.""" + + def __init__(self) -> None: + """Initialize the Menu View.""" + super().__init__() + self.mode: str | None = None + self.value: int | str | datetime | None = None + self.mode_select: discord.ui.Select[discord.ui.View] = discord.ui.Select( + placeholder="Choose purge mode", + options=[ + discord.SelectOption( + label="Message Count", + value="count", + description="Delete specific number of messages", + ), + discord.SelectOption( + label="Time Duration", + value="duration", + description="Delete messages from last X time", + ), + discord.SelectOption( + label="Specific Date", + value="date", + description="Delete messages since specific date/time", + ), + ], + ) + + self.mode_select.callback = self.on_mode_selected # type: ignore[method-assign] + self.add_item(self.mode_select) + + async def _prompt_count(self, interaction: discord.Interaction) -> None: + modal = discord.ui.Modal(title="Enter Count") + text_input: Any = discord.ui.TextInput(label="Number of messages", placeholder="10") + modal.add_item(text_input) + + async def _on_submit(_: discord.Interaction) -> None: + try: + self.value = int(text_input.value) + await _.response.defer() + self.stop() + except ValueError: + await _.response.send_message("Please enter a valid integer.", ephemeral=True) + + cast("Any", modal).on_submit = _on_submit + await interaction.response.send_modal(modal) + + async def _prompt_duration(self, interaction: discord.Interaction) -> None: + modal = discord.ui.Modal(title="Enter Duration") + text_input: Any = discord.ui.TextInput( + label="Duration (1d2h3m)", + placeholder="1d = 1 day, 2h = 2 hours, 3m = 3 minutes", + ) + modal.add_item(text_input) + + async def _on_submit(_: discord.Interaction) -> None: + self.value = text_input.value + await _.response.defer() + self.stop() + + cast("Any", modal).on_submit = _on_submit + await interaction.response.send_modal(modal) + + async def _prompt_date(self, interaction: discord.Interaction) -> None: + modal = DateTimeModal() + + async def _on_submit(_: discord.Interaction) -> None: + try: + date_input = modal.children[0] + time_input = modal.children[1] + if isinstance(date_input, discord.ui.TextInput) and isinstance(time_input, discord.ui.TextInput): + y, m, d = map(int, date_input.value.split("-")) + hh, mm = map(int, time_input.value.split(":")) + self.value = datetime(y, m, d, hh, mm, tzinfo=UTC) + await _.response.defer() + self.stop() + except ValueError: + await _.response.send_message("Invalid date/time format", ephemeral=True) + + cast("Any", modal).on_submit = _on_submit # type: ignore[method-assign] + await interaction.response.send_modal(modal) + + async def on_mode_selected(self, interaction: discord.Interaction) -> None: + """Handle the user's selected purge mode and prompt for parameters.""" + if not self.mode_select.values: + await interaction.response.send_message("No mode selected.", ephemeral=True) + return + + mode = self.mode_select.values[0] + self.mode = mode + + handlers: dict[str, Callable[[discord.Interaction], Awaitable[None]]] = { + "count": self._prompt_count, + "duration": self._prompt_duration, + "date": self._prompt_date, + } + handler = handlers.get(mode) + if handler: + await handler(interaction) + else: + await interaction.response.send_message("Invalid mode selected.", ephemeral=True) + + +class PurgeCog(commands.Cog): + """Cog for delete messages permanently based on mode.""" + + def __init__(self, bot: commands.Bot) -> None: + """Initialize the purge cog.""" + self.bot = bot + self.logger = logging.getLogger(f"discord.cog.{self.__class__.__name__.lower()}") + + def parse_duration(self, duration: str) -> timedelta | None: + """Parse duration string into timedelta. Format: 1d2h3m.""" + 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 + + days = int(match.group(1) or 0) + hours = int(match.group(2) or 0) + minutes = int(match.group(3) or 0) + + return timedelta(days=days, hours=hours, minutes=minutes) + + async def _handle_purge_count(self, amount: int, channel: discord.TextChannel) -> tuple[bool, str]: + if amount <= 0: + return False, "Please specify a number greater than 0" + deleted = await channel.purge(limit=amount) + return True, f"✨ Successfully deleted {len(deleted)} messages!" + + async def _handle_purge_duration(self, duration: str, channel: discord.TextChannel) -> tuple[bool, str]: + time_delta = self.parse_duration(duration) + if not time_delta: + return ( + False, + "Invalid duration format. Use format: 1d2h3m (e.g., 1d = 1 day,2h = 2 hours, 3m = 3 minutes)", + ) + + after_time = datetime.now(UTC) - time_delta + deleted = await channel.purge(after=after_time) + return ( + True, + f"✨ Successfully deleted {len(deleted)} messages from the last {duration}!", + ) + + async def _handle_purge_date(self, date: datetime, channel: discord.TextChannel) -> tuple[bool, str]: + if date > datetime.now(UTC): + return False, "Cannot purge future messages" + deleted = await channel.purge(after=date) + date_str = date.strftime("%Y-%m-%d %H:%M") + return ( + True, + f"✨ Successfully deleted {len(deleted)} messages since {date_str}!", + ) + + @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) + async def purge( + self, interaction: discord.Interaction, amount: int | None = None, duration: str | None = None + ) -> None: + """Purge method with view and execution.""" + if amount is not None and duration is not None: + await interaction.response.send_message( + "❌ Please provide **either** an amount **or** a duration, not both.", ephemeral=True + ) + return + if amount is not None: + channel = interaction.channel + if not isinstance(channel, discord.TextChannel): + await interaction.response.send_message( + "This command can only be used in text channels.", ephemeral=True + ) + return + + await interaction.response.defer(ephemeral=True) + success, message = await self._handle_purge_count(amount, channel) + await interaction.followup.send(message, ephemeral=True) + return + if duration is not None: + channel = interaction.channel + if not isinstance(channel, discord.TextChannel): + await interaction.response.send_message( + "This command can only be used in text channels.", ephemeral=True + ) + return + + await interaction.response.defer(ephemeral=True) + success, message = await self._handle_purge_duration(duration, channel) + await interaction.followup.send(message, ephemeral=True) + return + + view = PurgeModeView() + await interaction.response.send_message("Select purge mode:", view=view, ephemeral=True) + + await view.wait() + if not view.mode or not view.value: + await interaction.followup.send("Purge cancelled or timed out.", ephemeral=True) + 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") + except discord.Forbidden: + await interaction.followup.send("Error, I don't have permission to delete messages", ephemeral=True) + except Exception: + await interaction.followup.send("Error, An error occurred: ", ephemeral=True) + + async def _execute_purge( + self, + view: PurgeModeView, + channel: discord.abc.GuildChannel | discord.abc.PrivateChannel | discord.Thread | None, + ) -> tuple[bool, str]: + """Execute purge action based on selected mode.""" + if channel is None: + return False, "This command must be used in a channel." + if not isinstance(channel, discord.TextChannel): + return False, "This command can only be used in text channels." + + if view.mode == "count" and isinstance(view.value, int): + return await self._handle_purge_count(view.value, channel) + if view.mode == "duration" and isinstance(view.value, str): + return await self._handle_purge_duration(view.value, channel) + if view.mode == "date" and isinstance(view.value, datetime): + return await self._handle_purge_date(view.value, channel) + + return False, "Invalid mode/value combination. Please try again." + + +async def setup(bot: commands.Bot) -> None: + """Set up the Sync cog.""" + await bot.add_cog(PurgeCog(bot)) diff --git a/capy_discord/ui/modal.py b/capy_discord/ui/modal.py index b5c0497..6f0a431 100644 --- a/capy_discord/ui/modal.py +++ b/capy_discord/ui/modal.py @@ -22,7 +22,7 @@ def __init__(self, *, title: str, timeout: float | None = None) -> None: T = TypeVar("T", bound="CallbackModal") -class CallbackModal[T](BaseModal): +class CallbackModal[T: "CallbackModal"](BaseModal): """A modal that delegates submission logic to a callback function. This is useful for decoupling the UI from the business logic. From e2dfc301804bfa9112660023c3ec46592d440caf Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Mon, 9 Feb 2026 23:04:44 -0500 Subject: [PATCH 2/3] feat(purge): simplified purge cog and improved overall standard --- capy_discord/exts/tools/purge.py | 251 ++++++------------------------- 1 file changed, 45 insertions(+), 206 deletions(-) diff --git a/capy_discord/exts/tools/purge.py b/capy_discord/exts/tools/purge.py index dec5be4..3c90e73 100644 --- a/capy_discord/exts/tools/purge.py +++ b/capy_discord/exts/tools/purge.py @@ -1,149 +1,27 @@ +"""Purge command cog. + +This module provides a purge command to delete messages from channels +based on count or time duration. +""" + import logging import re from datetime import UTC, datetime, timedelta -from typing import Any, TYPE_CHECKING, cast import discord from discord import app_commands from discord.ext import commands -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - - -class DateTimeModal(discord.ui.Modal): - """Modal for date and time input.""" - - def __init__(self) -> None: - """Initialize the date time modal.""" - super().__init__(title="Enter Date and Time") - self.add_item( - discord.ui.TextInput( - label="Date (YYYY-MM-DD)", - placeholder="2024-02-08", - required=True, - ) - ) - self.add_item( - discord.ui.TextInput( - label="Time (HH:MM)", - placeholder="14:30", - required=True, - ) - ) - - -class PurgeModeView(discord.ui.View): - """Modal for Menu View.""" - - def __init__(self) -> None: - """Initialize the Menu View.""" - super().__init__() - self.mode: str | None = None - self.value: int | str | datetime | None = None - self.mode_select: discord.ui.Select[discord.ui.View] = discord.ui.Select( - placeholder="Choose purge mode", - options=[ - discord.SelectOption( - label="Message Count", - value="count", - description="Delete specific number of messages", - ), - discord.SelectOption( - label="Time Duration", - value="duration", - description="Delete messages from last X time", - ), - discord.SelectOption( - label="Specific Date", - value="date", - description="Delete messages since specific date/time", - ), - ], - ) - - self.mode_select.callback = self.on_mode_selected # type: ignore[method-assign] - self.add_item(self.mode_select) - - async def _prompt_count(self, interaction: discord.Interaction) -> None: - modal = discord.ui.Modal(title="Enter Count") - text_input: Any = discord.ui.TextInput(label="Number of messages", placeholder="10") - modal.add_item(text_input) - - async def _on_submit(_: discord.Interaction) -> None: - try: - self.value = int(text_input.value) - await _.response.defer() - self.stop() - except ValueError: - await _.response.send_message("Please enter a valid integer.", ephemeral=True) - - cast("Any", modal).on_submit = _on_submit - await interaction.response.send_modal(modal) - - async def _prompt_duration(self, interaction: discord.Interaction) -> None: - modal = discord.ui.Modal(title="Enter Duration") - text_input: Any = discord.ui.TextInput( - label="Duration (1d2h3m)", - placeholder="1d = 1 day, 2h = 2 hours, 3m = 3 minutes", - ) - modal.add_item(text_input) - - async def _on_submit(_: discord.Interaction) -> None: - self.value = text_input.value - await _.response.defer() - self.stop() - - cast("Any", modal).on_submit = _on_submit - await interaction.response.send_modal(modal) - - async def _prompt_date(self, interaction: discord.Interaction) -> None: - modal = DateTimeModal() - - async def _on_submit(_: discord.Interaction) -> None: - try: - date_input = modal.children[0] - time_input = modal.children[1] - if isinstance(date_input, discord.ui.TextInput) and isinstance(time_input, discord.ui.TextInput): - y, m, d = map(int, date_input.value.split("-")) - hh, mm = map(int, time_input.value.split(":")) - self.value = datetime(y, m, d, hh, mm, tzinfo=UTC) - await _.response.defer() - self.stop() - except ValueError: - await _.response.send_message("Invalid date/time format", ephemeral=True) - - cast("Any", modal).on_submit = _on_submit # type: ignore[method-assign] - await interaction.response.send_modal(modal) - - async def on_mode_selected(self, interaction: discord.Interaction) -> None: - """Handle the user's selected purge mode and prompt for parameters.""" - if not self.mode_select.values: - await interaction.response.send_message("No mode selected.", ephemeral=True) - return - - mode = self.mode_select.values[0] - self.mode = mode - - handlers: dict[str, Callable[[discord.Interaction], Awaitable[None]]] = { - "count": self._prompt_count, - "duration": self._prompt_duration, - "date": self._prompt_date, - } - handler = handlers.get(mode) - if handler: - await handler(interaction) - else: - await interaction.response.send_message("Invalid mode selected.", ephemeral=True) +from capy_discord.ui.embeds import error_embed, success_embed class PurgeCog(commands.Cog): - """Cog for delete messages permanently based on mode.""" + """Cog for deleting messages permanently based on mode.""" def __init__(self, bot: commands.Bot) -> None: - """Initialize the purge cog.""" + """Initialize the Purge cog.""" self.bot = bot - self.logger = logging.getLogger(f"discord.cog.{self.__class__.__name__.lower()}") + self.log = logging.getLogger(__name__) def parse_duration(self, duration: str) -> timedelta | None: """Parse duration string into timedelta. Format: 1d2h3m.""" @@ -161,35 +39,30 @@ def parse_duration(self, duration: str) -> timedelta | None: return timedelta(days=days, hours=hours, minutes=minutes) - async def _handle_purge_count(self, amount: int, channel: discord.TextChannel) -> tuple[bool, str]: + async def _handle_purge_count(self, amount: int, channel: discord.TextChannel) -> tuple[bool, discord.Embed]: if amount <= 0: - return False, "Please specify a number greater than 0" + return False, error_embed(description="Please specify a number greater than 0.") deleted = await channel.purge(limit=amount) - return True, f"✨ Successfully deleted {len(deleted)} messages!" + return True, success_embed("Purge Complete", f"Successfully deleted {len(deleted)} messages.") - async def _handle_purge_duration(self, duration: str, channel: discord.TextChannel) -> tuple[bool, str]: + async def _handle_purge_duration(self, duration: str, channel: discord.TextChannel) -> tuple[bool, discord.Embed]: time_delta = self.parse_duration(duration) if not time_delta: return ( False, - "Invalid duration format. Use format: 1d2h3m (e.g., 1d = 1 day,2h = 2 hours, 3m = 3 minutes)", + error_embed( + description=( + "Invalid duration format.\n" + "Use format: `1d2h3m` (e.g., 1d = 1 day, 2h = 2 hours, 3m = 3 minutes)" + ), + ), ) after_time = datetime.now(UTC) - time_delta deleted = await channel.purge(after=after_time) return ( True, - f"✨ Successfully deleted {len(deleted)} messages from the last {duration}!", - ) - - async def _handle_purge_date(self, date: datetime, channel: discord.TextChannel) -> tuple[bool, str]: - if date > datetime.now(UTC): - return False, "Cannot purge future messages" - deleted = await channel.purge(after=date) - date_str = date.strftime("%Y-%m-%d %H:%M") - return ( - True, - f"✨ Successfully deleted {len(deleted)} messages since {date_str}!", + success_embed("Purge Complete", f"Successfully deleted {len(deleted)} messages from the last {duration}."), ) @app_commands.command(name="purge", description="Delete messages") @@ -197,80 +70,46 @@ async def _handle_purge_date(self, date: datetime, channel: discord.TextChannel) 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) + @app_commands.checks.has_permissions(manage_messages=True) async def purge( self, interaction: discord.Interaction, amount: int | None = None, duration: str | None = None ) -> None: - """Purge method with view and execution.""" + """Purge messages with optional direct args.""" if amount is not None and duration is not None: await interaction.response.send_message( - "❌ Please provide **either** an amount **or** a duration, not both.", ephemeral=True + embed=error_embed(description="Please provide **either** an amount **or** a duration, not both."), + ephemeral=True, ) return - if amount is not None: - channel = interaction.channel - if not isinstance(channel, discord.TextChannel): - await interaction.response.send_message( - "This command can only be used in text channels.", ephemeral=True - ) - return - await interaction.response.defer(ephemeral=True) - success, message = await self._handle_purge_count(amount, channel) - await interaction.followup.send(message, ephemeral=True) + if amount is None and duration is None: + await interaction.response.send_message( + embed=error_embed(description="Please provide either an `amount` or a `duration`."), + ephemeral=True, + ) return - if duration is not None: - channel = interaction.channel - if not isinstance(channel, discord.TextChannel): - await interaction.response.send_message( - "This command can only be used in text channels.", ephemeral=True - ) - return - await interaction.response.defer(ephemeral=True) - success, message = await self._handle_purge_duration(duration, channel) - await interaction.followup.send(message, ephemeral=True) + channel = interaction.channel + if not isinstance(channel, discord.TextChannel): + await interaction.response.send_message( + embed=error_embed(description="This command can only be used in text channels."), + ephemeral=True, + ) return - view = PurgeModeView() - await interaction.response.send_message("Select purge mode:", view=view, ephemeral=True) + await interaction.response.defer(ephemeral=True) - await view.wait() - if not view.mode or not view.value: - await interaction.followup.send("Purge cancelled or timed out.", ephemeral=True) + if amount is not None: + _, embed = await self._handle_purge_count(amount, channel) + await interaction.followup.send(embed=embed, ephemeral=True) 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") - except discord.Forbidden: - await interaction.followup.send("Error, I don't have permission to delete messages", ephemeral=True) - except Exception: - await interaction.followup.send("Error, An error occurred: ", ephemeral=True) - - async def _execute_purge( - self, - view: PurgeModeView, - channel: discord.abc.GuildChannel | discord.abc.PrivateChannel | discord.Thread | None, - ) -> tuple[bool, str]: - """Execute purge action based on selected mode.""" - if channel is None: - return False, "This command must be used in a channel." - if not isinstance(channel, discord.TextChannel): - return False, "This command can only be used in text channels." - - if view.mode == "count" and isinstance(view.value, int): - return await self._handle_purge_count(view.value, channel) - if view.mode == "duration" and isinstance(view.value, str): - return await self._handle_purge_duration(view.value, channel) - if view.mode == "date" and isinstance(view.value, datetime): - return await self._handle_purge_date(view.value, channel) - - return False, "Invalid mode/value combination. Please try again." + if duration is not None: + _, embed = await self._handle_purge_duration(duration, channel) + await interaction.followup.send(embed=embed, ephemeral=True) + return async def setup(bot: commands.Bot) -> None: - """Set up the Sync cog.""" + """Set up the Purge cog.""" await bot.add_cog(PurgeCog(bot)) From 9c5034756af71e2aa2574a0b02ee80707af68def Mon Sep 17 00:00:00 2001 From: Shamik Karkhanis Date: Tue, 10 Feb 2026 00:02:16 -0500 Subject: [PATCH 3/3] fix(purge): regex update and unused attribute fix --- capy_discord/exts/tools/purge.py | 35 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/capy_discord/exts/tools/purge.py b/capy_discord/exts/tools/purge.py index 3c90e73..dcd744e 100644 --- a/capy_discord/exts/tools/purge.py +++ b/capy_discord/exts/tools/purge.py @@ -24,12 +24,12 @@ def __init__(self, bot: commands.Bot) -> None: self.log = logging.getLogger(__name__) def parse_duration(self, duration: str) -> timedelta | None: - """Parse duration string into timedelta. Format: 1d2h3m.""" + """Parse duration string into timedelta. Format: 1d 2h 3m (spaces optional).""" if not duration: return None - pattern = r"(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?" - match = re.match(pattern, duration) + pattern = r"(?:(\d+)d)?\s*(?:(\d+)h)?\s*(?:(\d+)m)?" + match = re.match(pattern, duration.strip()) if not match or not any(match.groups()): return None @@ -39,36 +39,31 @@ def parse_duration(self, duration: str) -> timedelta | None: return timedelta(days=days, hours=hours, minutes=minutes) - async def _handle_purge_count(self, amount: int, channel: discord.TextChannel) -> tuple[bool, discord.Embed]: + async def _handle_purge_count(self, amount: int, channel: discord.TextChannel) -> discord.Embed: if amount <= 0: - return False, error_embed(description="Please specify a number greater than 0.") + return error_embed(description="Please specify a number greater than 0.") deleted = await channel.purge(limit=amount) - return True, success_embed("Purge Complete", f"Successfully deleted {len(deleted)} messages.") + return success_embed("Purge Complete", f"Successfully deleted {len(deleted)} messages.") - async def _handle_purge_duration(self, duration: str, channel: discord.TextChannel) -> tuple[bool, discord.Embed]: + async def _handle_purge_duration(self, duration: str, channel: discord.TextChannel) -> discord.Embed: time_delta = self.parse_duration(duration) if not time_delta: - return ( - False, - error_embed( - description=( - "Invalid duration format.\n" - "Use format: `1d2h3m` (e.g., 1d = 1 day, 2h = 2 hours, 3m = 3 minutes)" - ), + return error_embed( + description=( + "Invalid duration format.\nUse format: `1d 2h 3m` (e.g., 1d = 1 day, 2h = 2 hours, 3m = 3 minutes)" ), ) after_time = datetime.now(UTC) - time_delta deleted = await channel.purge(after=after_time) - return ( - True, - success_embed("Purge Complete", f"Successfully deleted {len(deleted)} messages from the last {duration}."), + return success_embed( + "Purge Complete", f"Successfully deleted {len(deleted)} messages from the last {duration}." ) @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)", + duration="The timeframe to delete messages from (e.g. 1h30m, 1h 30m)", ) @app_commands.checks.has_permissions(manage_messages=True) async def purge( @@ -100,12 +95,12 @@ async def purge( await interaction.response.defer(ephemeral=True) if amount is not None: - _, embed = await self._handle_purge_count(amount, channel) + embed = await self._handle_purge_count(amount, channel) await interaction.followup.send(embed=embed, ephemeral=True) return if duration is not None: - _, embed = await self._handle_purge_duration(duration, channel) + embed = await self._handle_purge_duration(duration, channel) await interaction.followup.send(embed=embed, ephemeral=True) return