Skip to content

Commit

Permalink
feat: Support for menu in interaction response (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
DenverCoder1 authored Jan 24, 2022
1 parent 792eb11 commit 261b732
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 16 deletions.
36 changes: 36 additions & 0 deletions docs/ext/menus/menu_examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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() <Menu.start>` instead of ``ctx``.

``interaction`` must be passed as a keyword argument.

Additionally, we will use :meth:`interaction.response.send_message() <nextcord.InteractionResponse.send_message>`
in the :meth:`send_initial_message() <Menu.send_initial_message>` method to send the initial message.

To make the response message ephemeral, we can pass ``ephemeral=True`` to :meth:`start() <Menu.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)
16 changes: 16 additions & 0 deletions docs/ext/menus/pagination_examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,22 @@ for an example on how to create a :class:`Select Menu <nextcord.ui.Select>`.
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() <Menu.start>` as a keyword argument instead of ``ctx``.

To make the response message ephemeral, we can pass ``ephemeral=True`` to :meth:`start() <Menu.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
--------------------------

Expand Down
2 changes: 1 addition & 1 deletion nextcord/ext/menus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
from .utils import *

# Needed for the setup.py script
__version__ = "1.3.4"
__version__ = "1.4.0"
25 changes: 21 additions & 4 deletions nextcord/ext/menus/menu_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
58 changes: 47 additions & 11 deletions nextcord/ext/menus/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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__(
Expand All @@ -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()
Expand Down Expand Up @@ -602,32 +610,47 @@ 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 <nextcord.ext.commands.Context>` or an :class:`Interaction <nextcord.Interaction>` object.
Parameters
-----------
ctx: :class:`Context`
ctx: :class:`Context <nextcord.ext.commands.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
-------
MenuError
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.
Expand All @@ -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)
Expand All @@ -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()
Expand Down

0 comments on commit 261b732

Please sign in to comment.