diff --git a/docs/framework_utils.rst b/docs/framework_utils.rst index dc7668716..6d0805181 100644 --- a/docs/framework_utils.rst +++ b/docs/framework_utils.rst @@ -84,6 +84,65 @@ Utility UI .. automodule:: redbot.core.utils.views :members: + :exclude-members: ConfirmView + + .. autoclass:: ConfirmView + :members: + :exclude-members: confirm_button, dismiss_button + + .. autoattribute:: confirm_button + :no-value: + + A `discord.ui.Button` to confirm the message. + + The button's callback will set `result` to ``True``, defer the response, + and call `on_timeout()` to clean up the view. + + .. rubric:: Example + + Changing the style and label of this `discord.ui.Button`:: + + view = ConfirmView(ctx.author) + view.confirm_button.style = discord.ButtonStyle.red + view.confirm_button.label = "Delete" + view.dismiss_button.label = "Cancel" + view.message = await ctx.send( + "Are you sure you want to remove #very-important-channel?", view=view + ) + await view.wait() + if view.result: + await ctx.send("Channel #very-important-channel deleted.") + else: + await ctx.send("Canceled.") + + :type: discord.ui.Button + + .. autoattribute:: dismiss_button + :no-value: + + A `discord.ui.Button` to dismiss the message. + + The button's callback will set `result` to ``False``, defer the response, + and call `on_timeout()` to clean up the view. + + .. rubric:: Example + + Changing the style and label of this `discord.ui.Button`:: + + view = ConfirmView(ctx.author) + view.confirm_button.style = discord.ButtonStyle.red + view.confirm_button.label = "Delete" + view.dismiss_button.label = "Cancel" + view.message = await ctx.send( + "Are you sure you want to remove #very-important-channel?", view=view + ) + await view.wait() + if view.result: + await ctx.send("Channel #very-important-channel deleted.") + else: + await ctx.send("Canceled.") + + :type: discord.ui.Button AntiSpam ======== diff --git a/redbot/core/utils/views.py b/redbot/core/utils/views.py index 960131b99..ee20fbfb3 100644 --- a/redbot/core/utils/views.py +++ b/redbot/core/utils/views.py @@ -11,7 +11,7 @@ from redbot.core.commands.converter import get_dict_converter if TYPE_CHECKING: from redbot.core.commands import Context -__all__ = ("SimpleMenu", "SetApiModal", "SetApiView") +__all__ = ("SimpleMenu", "SetApiModal", "SetApiView", "ConfirmView") _ = Translator("UtilsViews", __file__) @@ -431,3 +431,190 @@ class SetApiView(discord.ui.View): return await interaction.response.send_modal( SetApiModal(self.default_service, self.default_keys) ) + + +class ConfirmView(discord.ui.View): + """ + A simple `discord.ui.View` used for confirming something. + + Parameters + ---------- + author: Optional[discord.abc.User] + The user who you want to be interacting with the confirmation. + If this is omitted anyone can click yes or no. + timeout: float + The timeout of the view in seconds. Defaults to ``180`` seconds. + disable_buttons: bool + Whether to disable the buttons instead of removing them from the message after the timeout. + Defaults to ``False``. + + Examples + -------- + Using the view:: + + view = ConfirmView(ctx.author) + # attach the message to the view after sending it. + # This way, the view will be automatically removed + # from the message after the timeout. + view.message = await ctx.send("Are you sure you about that?", view=view) + await view.wait() + if view.result: + await ctx.send("Okay I will do that.") + else: + await ctx.send("I will not be doing that then.") + + Auto-disable the buttons after timeout if nothing is pressed:: + + view = ConfirmView(ctx.author, disable_buttons=True) + view.message = await ctx.send("Are you sure you about that?", view=view) + await view.wait() + if view.result: + await ctx.send("Okay I will do that.") + else: + await ctx.send("I will not be doing that then.") + + Attributes + ---------- + result: Optional[bool] + The result of the confirm view. + author: Optional[discord.abc.User] + The author of the message who is allowed to press the buttons. + message: Optional[discord.Message] + The message the confirm view is sent on. This can be set while + sending the message. This can also be left as ``None`` in which case + nothing will happen in `on_timeout()`, if the view is never interacted with. + disable_buttons: bool + Whether to disable the buttons isntead of removing them on timeout + (if the `message` attribute has been set on the view). + """ + + def __init__( + self, + author: Optional[discord.abc.User] = None, + *, + timeout: float = 180.0, + disable_buttons: bool = False, + ): + if timeout is None: + raise TypeError("This view should not be used as a persistent view.") + super().__init__(timeout=timeout) + self.result: Optional[bool] = None + self.author: Optional[discord.abc.User] = author + self.message: Optional[discord.Message] = None + self.disable_buttons = disable_buttons + + async def on_timeout(self): + """ + A callback that is called by the provided (default) callbacks for `confirm_button` + and `dismiss_button` as well as when a view’s timeout elapses without being + explicitly stopped. + + The default implementation will either disable the buttons + when `disable_buttons` is ``True``, or remove the view from the message otherwise. + + .. note:: + + This will not do anything if `message` is ``None``. + """ + if self.message is None: + # we can't do anything here if message is none + return + + if self.disable_buttons: + self.confirm_button.disabled = True + self.dismiss_button.disabled = True + await self.message.edit(view=self) + else: + await self.message.edit(view=None) + + @discord.ui.button(label=_("Yes"), style=discord.ButtonStyle.green) + async def confirm_button(self, interaction: discord.Interaction, button: discord.ui.Button): + # Warning: The Sphinx documentation for this method/attribute does not use this docstring. + """ + A `discord.ui.Button` to confirm the message. + + The button's callback will set `result` to ``True``, defer the response, + and call `on_timeout()` to clean up the view. + + Example + ------- + Changing the style and label of this `discord.ui.Button`:: + + view = ConfirmView(ctx.author) + view.confirm_button.style = discord.ButtonStyle.red + view.confirm_button.label = "Delete" + view.dismiss_button.label = "Cancel" + view.message = await ctx.send( + "Are you sure you want to remove #very-important-channel?", view=view + ) + await view.wait() + if view.result: + await ctx.send("Channel #very-important-channel deleted.") + else: + await ctx.send("Canceled.") + """ + self.result = True + self.stop() + # respond to the interaction so the user does not see "interaction failed". + await interaction.response.defer() + # call `on_timeout` explicitly here since it's not called when `stop()` is called. + await self.on_timeout() + + @discord.ui.button(label=_("No"), style=discord.ButtonStyle.secondary) + async def dismiss_button(self, interaction: discord.Interaction, button: discord.ui.Button): + # Warning: The Sphinx documentation for this method/attribute does not use this docstring. + """ + A `discord.ui.Button` to dismiss the message. + + The button's callback will set `result` to ``False``, defer the response, + and call `on_timeout()` to clean up the view. + + Example + ------- + Changing the style and label of this `discord.ui.Button`:: + + view = ConfirmView(ctx.author) + view.confirm_button.style = discord.ButtonStyle.red + view.confirm_button.label = "Delete" + view.dismiss_button.label = "Cancel" + view.message = await ctx.send( + "Are you sure you want to remove #very-important-channel?", view=view + ) + await view.wait() + if view.result: + await ctx.send("Channel #very-important-channel deleted.") + else: + await ctx.send("Canceled.") + """ + self.result = False + self.stop() + # respond to the interaction so the user does not see "interaction failed". + await interaction.response.defer() + # call `on_timeout` explicitly here since it's not called when `stop()` is called. + await self.on_timeout() + + async def interaction_check(self, interaction: discord.Interaction): + """ + A callback that is called when an interaction happens within the view + that checks whether the view should process item callbacks for the interaction. + + The default implementation of this will assign value of `discord.Interaction.message` + to the `message` attribute and either: + + - send an ephemeral failure message and return ``False``, + if `author` is set and isn't the same as the interaction user, or + - return ``True`` + + .. seealso:: + + The documentation of the callback in the base class: + :meth:`discord.ui.View.interaction_check()` + """ + if self.message is None: + self.message = interaction.message + if self.author and interaction.user.id != self.author.id: + await interaction.response.send_message( + content=_("You are not authorized to interact with this."), ephemeral=True + ) + return False + return True