From 261b732b892377007506aacbcb2b395914666ac2 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Sun, 23 Jan 2022 19:32:58 -0700 Subject: [PATCH] feat: Support for menu in interaction response (#27) --- docs/ext/menus/menu_examples.rst | 36 ++++++++++++++++ docs/ext/menus/pagination_examples.rst | 16 +++++++ nextcord/ext/menus/__init__.py | 2 +- nextcord/ext/menus/menu_pages.py | 25 +++++++++-- nextcord/ext/menus/menus.py | 58 +++++++++++++++++++++----- 5 files changed, 121 insertions(+), 16 deletions(-) diff --git a/docs/ext/menus/menu_examples.rst b/docs/ext/menus/menu_examples.rst index 56649f4..6889893 100644 --- a/docs/ext/menus/menu_examples.rst +++ b/docs/ext/menus/menu_examples.rst @@ -171,3 +171,39 @@ the :class:`ButtonMenu` in the same way as shown before. async def button_confirm(ctx): confirm = await ButtonConfirm("Confirm?").prompt(ctx) await ctx.send(f"You said: {confirm}") + + +Slash Commands +-------------- + +To use a menu in a slash command or component response, we need to pass ``interaction`` to :meth:`start() ` instead of ``ctx``. + +``interaction`` must be passed as a keyword argument. + +Additionally, we will use :meth:`interaction.response.send_message() ` +in the :meth:`send_initial_message() ` method to send the initial message. + +To make the response message ephemeral, we can pass ``ephemeral=True`` to :meth:`start() ` as well. + +.. code:: py + + class MySlashButtonMenu(menus.ButtonMenu): + async def send_initial_message(self, ctx, channel): + await self.interaction.response.send_message(f"Hello {self.interaction.user}", view=self) + return await self.interaction.original_message() + + @nextcord.ui.button(emoji="\N{THUMBS UP SIGN}") + async def on_thumbs_up(self, button, interaction): + await self.message.edit(content=f"Thanks {interaction.user}!") + + @nextcord.ui.button(emoji="\N{THUMBS DOWN SIGN}") + async def on_thumbs_down(self, button, interaction): + await self.message.edit(content=f"That's not nice {interaction.user}...") + + @nextcord.ui.button(emoji="\N{BLACK SQUARE FOR STOP}\ufe0f") + async def on_stop(self, button, interaction): + self.stop() + + @bot.slash_command(guild_ids=[TESTING_GUILD_ID], name="slashmenu") + async def slash_menu_example(interaction: nextcord.Interaction): + await MySlashButtonMenu().start(interaction=interaction) \ No newline at end of file diff --git a/docs/ext/menus/pagination_examples.rst b/docs/ext/menus/pagination_examples.rst index 91d88c6..8e37568 100644 --- a/docs/ext/menus/pagination_examples.rst +++ b/docs/ext/menus/pagination_examples.rst @@ -325,6 +325,22 @@ for an example on how to create a :class:`Select Menu `. pages = SelectButtonMenuPages(source=MySource(data)) await pages.start(ctx) +Menu in Slash Command Response +------------------------------ + +To use a menu in a slash command or component response, we need to pass ``interaction`` to +:meth:`start() ` as a keyword argument instead of ``ctx``. + +To make the response message ephemeral, we can pass ``ephemeral=True`` to :meth:`start() ` as well. + +.. code:: py + + @bot.slash_command(guild_ids=[TEST_GUILD_ID], name="slashpages") + async def slash_pages(interaction: nextcord.Interaction): + data = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] + pages = menus.ButtonMenuPages(source=MySource(data)) + await pages.start(interaction=interaction) + Paginated Help Command Cog -------------------------- diff --git a/nextcord/ext/menus/__init__.py b/nextcord/ext/menus/__init__.py index 0fc7b60..0e3ec60 100644 --- a/nextcord/ext/menus/__init__.py +++ b/nextcord/ext/menus/__init__.py @@ -6,4 +6,4 @@ from .utils import * # Needed for the setup.py script -__version__ = "1.3.4" +__version__ = "1.4.0" diff --git a/nextcord/ext/menus/menu_pages.py b/nextcord/ext/menus/menu_pages.py index e4456ff..d945dce 100644 --- a/nextcord/ext/menus/menu_pages.py +++ b/nextcord/ext/menus/menu_pages.py @@ -110,17 +110,34 @@ async def send_initial_message( """ page = await self._source.get_page(0) kwargs = await self._get_kwargs_from_page(page) + # filter out kwargs that are "None" + kwargs = {k: v for k, v in kwargs.items() if v is not None} + # if there is an interaction, send an interaction response + if self.interaction is not None: + await self.interaction.response.send_message( + ephemeral=self.ephemeral, **kwargs + ) + return await self.interaction.original_message() + # otherwise, send the message using the channel return await channel.send(**kwargs) async def start( self, - ctx: commands.Context, + ctx: Optional[commands.Context] = None, + interaction: Optional[nextcord.Interaction] = None, *, channel: Optional[nextcord.abc.Messageable] = None, - wait: Optional[bool] = False + wait: bool = False, + ephemeral: bool = False, ): await self._source._prepare_once() - await super().start(ctx, channel=channel, wait=wait) + await super().start( + ctx=ctx, + interaction=interaction, + channel=channel, + wait=wait, + ephemeral=ephemeral, + ) # If we're not paginating, we can remove the pagination buttons if not self._source.is_paginating(): await self.clear() @@ -267,7 +284,7 @@ def __init__( self, source: PageSource, style: nextcord.ButtonStyle = nextcord.ButtonStyle.secondary, - **kwargs + **kwargs, ): self.__button_menu_pages__ = True # make button pagination disable buttons on stop by default unless it's overridden diff --git a/nextcord/ext/menus/menus.py b/nextcord/ext/menus/menus.py index 299d775..d67bb8e 100644 --- a/nextcord/ext/menus/menus.py +++ b/nextcord/ext/menus/menus.py @@ -240,7 +240,10 @@ class Menu(metaclass=_MenuMeta): Whether to verify embed permissions as well. ctx: Optional[:class:`commands.Context`] The context that started this pagination session or ``None`` if it hasn't - been started yet. + been started yet or :class:`nextcord.Interaction` is used instead. + interaction: Optional[:class:`nextcord.Interaction`] + The interaction that started this pagination session or ``None`` if it hasn't + been started yet or :class:`commands.Context` is used instead. bot: Optional[:class:`commands.Bot`] The bot that is running this pagination session or ``None`` if it hasn't been started yet. @@ -249,6 +252,9 @@ class Menu(metaclass=_MenuMeta): message of :meth:`send_initial_message`. You can set it in order to avoid calling :meth:`send_initial_message`\, if for example you have a pre-existing message you want to attach a menu to. + ephemeral: :class:`bool` + Whether to make the response ephemeral when using an interaction response. + Note: Ephemeral messages do not support reactions. """ def __init__( @@ -270,6 +276,8 @@ def __init__( self._running = True self.message = message self.ctx = None + self.interaction = None + self.ephemeral = False self.bot = None self._author_id = None self._buttons = self.__class__.get_buttons() @@ -602,25 +610,38 @@ async def on_menu_button_error(self, exc: Exception): async def start( self, - ctx: commands.Context, + ctx: Optional[commands.Context] = None, + interaction: Optional[nextcord.Interaction] = None, *, channel: Optional[nextcord.abc.Messageable] = None, - wait: bool = False + wait: bool = False, + ephemeral: bool = False, ): """|coro| Starts the interactive menu session. + To start a menu session, you must provide either a + :class:`Context ` or an :class:`Interaction ` object. + Parameters ----------- - ctx: :class:`Context` + ctx: :class:`Context ` The invocation context to use. + interaction: :class:`nextcord.Interaction` + The interaction context to use for slash and + component responses. channel: :class:`nextcord.abc.Messageable` The messageable to send the message to. If not given - then it defaults to the channel in the context. + then it defaults to the channel in the context + or interaction. wait: :class:`bool` Whether to wait until the menu is completed before returning back to the caller. + ephemeral: :class:`bool` + Whether to make the response ephemeral when using an + interaction response. Note: ephemeral messages do not + support reactions. Raises ------- @@ -628,6 +649,8 @@ async def start( An error happened when verifying permissions. nextcord.HTTPException Adding a reaction failed. + ValueError + No context or interaction was given or both were given. """ # Clear the reaction buttons cache and re-compute if possible. @@ -636,11 +659,24 @@ async def start( except AttributeError: pass - self.bot = bot = ctx.bot + # ensure only one of ctx and interaction is set + if ctx is None and interaction is None: + raise ValueError("ctx or interaction must be set.") + if ctx is not None and interaction is not None: + raise ValueError("ctx and interaction cannot both be set.") + self.ctx = ctx - self._author_id = ctx.author.id - channel = channel or ctx.channel - me = channel.guild.me if hasattr(channel, "guild") else ctx.bot.user + self.interaction = interaction + self.ephemeral = ephemeral + if ctx is not None: + self.bot = ctx.bot + self._author_id = ctx.author.id + channel = channel or ctx.channel + else: + self.bot = getattr(interaction, "client", interaction._state._get_client()) + self._author_id = interaction.user.id + channel = channel or interaction.channel + me = channel.guild.me if hasattr(channel, "guild") else self.bot.user permissions = channel.permissions_for(me) self.__me = nextcord.Object(id=me.id) self._verify_permissions(ctx, channel, permissions) @@ -656,13 +692,13 @@ async def start( self.__tasks.clear() self._running = True - self.__tasks.append(bot.loop.create_task(self._internal_loop())) + self.__tasks.append(self.bot.loop.create_task(self._internal_loop())) async def add_reactions_task(): for emoji in self.buttons: await msg.add_reaction(emoji) - self.__tasks.append(bot.loop.create_task(add_reactions_task())) + self.__tasks.append(self.bot.loop.create_task(add_reactions_task())) if wait: await self._event.wait()