mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
Add user param to menu() (#4913)
Co-authored-by: jack1142 <6032823+jack1142@users.noreply.github.com> Co-authored-by: Jakub Kuczys <me@jacken.men>
This commit is contained in:
parent
4c7a691ec9
commit
8c2976504a
@ -878,14 +878,10 @@ class RedHelpFormatter(HelpFormatterABC):
|
|||||||
m = await (destination.send(embed=pages[0]) if embed else destination.send(pages[0]))
|
m = await (destination.send(embed=pages[0]) if embed else destination.send(pages[0]))
|
||||||
c = menus.DEFAULT_CONTROLS if len(pages) > 1 else {"\N{CROSS MARK}": menus.close_menu}
|
c = menus.DEFAULT_CONTROLS if len(pages) > 1 else {"\N{CROSS MARK}": menus.close_menu}
|
||||||
# Allow other things to happen during menu timeout/interaction.
|
# Allow other things to happen during menu timeout/interaction.
|
||||||
if use_DMs:
|
|
||||||
menu_ctx = await ctx.bot.get_context(m)
|
|
||||||
# Monkeypatch so help listens for reactions from the original author, not the bot
|
|
||||||
menu_ctx.author = ctx.author
|
|
||||||
else:
|
|
||||||
menu_ctx = ctx
|
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
menus.menu(menu_ctx, pages, c, message=m, timeout=help_settings.react_timeout)
|
menus.menu(
|
||||||
|
ctx, pages, c, user=ctx.author, message=m, timeout=help_settings.react_timeout
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# menu needs reactions added manually since we fed it a message
|
# menu needs reactions added manually since we fed it a message
|
||||||
menus.start_adding_reactions(m, c.keys())
|
menus.start_adding_reactions(m, c.keys())
|
||||||
|
|||||||
@ -49,48 +49,126 @@ class _GenericButton(discord.ui.Button):
|
|||||||
if self.emoji.is_unicode_emoji()
|
if self.emoji.is_unicode_emoji()
|
||||||
else (ctx.bot.get_emoji(self.emoji.id) or self.emoji)
|
else (ctx.bot.get_emoji(self.emoji.id) or self.emoji)
|
||||||
)
|
)
|
||||||
await self.func(ctx, pages, controls, message, page, timeout, emoji)
|
user = self.view.author if not self.view._fallback_author_to_ctx else None
|
||||||
|
if user is not None:
|
||||||
|
await self.func(ctx, pages, controls, message, page, timeout, emoji, user=user)
|
||||||
|
else:
|
||||||
|
await self.func(ctx, pages, controls, message, page, timeout, emoji)
|
||||||
|
|
||||||
|
|
||||||
async def menu(
|
async def menu(
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
pages: _PageList,
|
pages: _PageList,
|
||||||
controls: Optional[Mapping[str, _ControlCallable]] = None,
|
controls: Optional[Mapping[str, _ControlCallable]] = None,
|
||||||
message: discord.Message = None,
|
message: Optional[discord.Message] = None,
|
||||||
page: int = 0,
|
page: int = 0,
|
||||||
timeout: float = 30.0,
|
timeout: float = 30.0,
|
||||||
|
*,
|
||||||
|
user: Optional[discord.User] = None,
|
||||||
) -> _T:
|
) -> _T:
|
||||||
"""
|
"""
|
||||||
An emoji-based menu
|
An emoji-based menu
|
||||||
|
|
||||||
.. note:: All pages should be of the same type
|
All functions for handling what a particular emoji does
|
||||||
|
should be coroutines (i.e. :code:`async def`). Additionally,
|
||||||
|
they must take all of the parameters of this function, in
|
||||||
|
addition to a string representing the emoji reacted with.
|
||||||
|
This parameter should be the 7th one, and none of the
|
||||||
|
parameters in the handling functions are optional.
|
||||||
|
|
||||||
.. note:: All functions for handling what a particular emoji does
|
.. warning::
|
||||||
should be coroutines (i.e. :code:`async def`). Additionally,
|
|
||||||
they must take all of the parameters of this function, in
|
The ``user`` parameter is considered `provisional <developer-guarantees-exclusions>`.
|
||||||
addition to a string representing the emoji reacted with.
|
If no issues arise, we plan on including it under developer guarantees
|
||||||
This parameter should be the last one, and none of the
|
in the first release made after 2024-05-18.
|
||||||
parameters in the handling functions are optional
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
If you're using the ``user`` param, you need to pass it
|
||||||
|
as a keyword-only argument, and set :obj:`None` as the
|
||||||
|
default in your function.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
Simple menu using default controls::
|
||||||
|
|
||||||
|
from redbot.core.utils.menus import menu
|
||||||
|
|
||||||
|
pages = ["Hello", "Hi", "Bonjour", "Salut"]
|
||||||
|
await menu(ctx, pages)
|
||||||
|
|
||||||
|
Menu with a custom control performing an action (deleting an item from pages list)::
|
||||||
|
|
||||||
|
from redbot.core.utils import menus
|
||||||
|
|
||||||
|
items = ["Apple", "Banana", "Cucumber", "Dragonfruit"]
|
||||||
|
|
||||||
|
def generate_pages():
|
||||||
|
return [f"{fruit} is an awesome fruit!" for fruit in items]
|
||||||
|
|
||||||
|
async def delete_item_action(ctx, pages, controls, message, page, timeout, emoji):
|
||||||
|
fruit = items.pop(page) # lookup and remove corresponding fruit name
|
||||||
|
await ctx.send(f"I guess you don't like {fruit}, huh? Deleting...")
|
||||||
|
pages = generate_pages()
|
||||||
|
if not pages:
|
||||||
|
return await menus.close_menu(ctx, pages, controls, message, page, timeout)
|
||||||
|
page = min(page, len(pages) - 1)
|
||||||
|
return await menus.menu(ctx, pages, controls, message, page, timeout)
|
||||||
|
|
||||||
|
pages = generate_pages()
|
||||||
|
controls = {**menus.DEFAULT_CONTROLS, "\\N{NO ENTRY SIGN}": delete_item_action}
|
||||||
|
await menus.menu(ctx, pages, controls)
|
||||||
|
|
||||||
|
Menu with custom controls that output a result (confirmation prompt)::
|
||||||
|
|
||||||
|
from redbot.core.utils.menus import menu
|
||||||
|
|
||||||
|
async def control_yes(*args, **kwargs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def control_no(*args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
msg = "Do you wish to continue?"
|
||||||
|
controls = {
|
||||||
|
"\\N{WHITE HEAVY CHECK MARK}": control_yes,
|
||||||
|
"\\N{CROSS MARK}": control_no,
|
||||||
|
}
|
||||||
|
reply = await menu(ctx, [msg], controls)
|
||||||
|
if reply:
|
||||||
|
await ctx.send("Continuing...")
|
||||||
|
else:
|
||||||
|
await ctx.send("Okay, I'm not going to perform the requested action.")
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
ctx: commands.Context
|
ctx: commands.Context
|
||||||
The command context
|
The command context
|
||||||
pages: `list` of `str` or `discord.Embed`
|
pages: Union[List[str], List[discord.Embed]]
|
||||||
The pages of the menu.
|
The pages of the menu.
|
||||||
|
All pages need to be of the same type (either `str` or `discord.Embed`).
|
||||||
controls: Optional[Mapping[str, Callable]]
|
controls: Optional[Mapping[str, Callable]]
|
||||||
A mapping of emoji to the function which handles the action for the
|
A mapping of emoji to the function which handles the action for the
|
||||||
emoji. The signature of the function should be the same as of this function
|
emoji. The signature of the function should be the same as of this function
|
||||||
and should additionally accept an ``emoji`` parameter of type `str`.
|
and should additionally accept an ``emoji`` parameter of type `str`.
|
||||||
If not passed, `DEFAULT_CONTROLS` is used *or*
|
If not passed, `DEFAULT_CONTROLS` is used *or*
|
||||||
only a close menu control is shown when ``pages`` is of length 1.
|
only a close menu control is shown when ``pages`` is of length 1.
|
||||||
message: discord.Message
|
message: Optional[discord.Message]
|
||||||
The message representing the menu. Usually :code:`None` when first opening
|
The message representing the menu. Usually :code:`None` when first opening
|
||||||
the menu
|
the menu
|
||||||
page: int
|
page: int
|
||||||
The current page number of the menu
|
The current page number of the menu
|
||||||
timeout: float
|
timeout: float
|
||||||
The time (in seconds) to wait for a reaction
|
The time (in seconds) to wait for a reaction
|
||||||
|
user: Optional[discord.User]
|
||||||
|
The user allowed to interact with the menu. Defaults to ``ctx.author``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
This parameter is `provisional <developer-guarantees-exclusions>`.
|
||||||
|
If no issues arise, we plan on including it under developer guarantees
|
||||||
|
in the first release made after 2024-05-18.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@ -136,7 +214,7 @@ async def menu(
|
|||||||
# internally we already include the emojis we expect.
|
# internally we already include the emojis we expect.
|
||||||
if controls == DEFAULT_CONTROLS:
|
if controls == DEFAULT_CONTROLS:
|
||||||
view = SimpleMenu(pages, timeout=timeout)
|
view = SimpleMenu(pages, timeout=timeout)
|
||||||
await view.start(ctx)
|
await view.start(ctx, user=user)
|
||||||
await view.wait()
|
await view.wait()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@ -169,7 +247,7 @@ async def menu(
|
|||||||
view.remove_item(view.stop_button)
|
view.remove_item(view.stop_button)
|
||||||
for emoji, func in to_add.items():
|
for emoji, func in to_add.items():
|
||||||
view.add_item(_GenericButton(emoji, func))
|
view.add_item(_GenericButton(emoji, func))
|
||||||
await view.start(ctx)
|
await view.start(ctx, user=user)
|
||||||
_active_menus[view.message.id] = view
|
_active_menus[view.message.id] = view
|
||||||
await view.wait()
|
await view.wait()
|
||||||
del _active_menus[view.message.id]
|
del _active_menus[view.message.id]
|
||||||
@ -194,7 +272,9 @@ async def menu(
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
predicates = ReactionPredicate.with_emojis(tuple(controls.keys()), message, ctx.author)
|
predicates = ReactionPredicate.with_emojis(
|
||||||
|
tuple(controls.keys()), message, user or ctx.author
|
||||||
|
)
|
||||||
tasks = [
|
tasks = [
|
||||||
asyncio.create_task(ctx.bot.wait_for("reaction_add", check=predicates)),
|
asyncio.create_task(ctx.bot.wait_for("reaction_add", check=predicates)),
|
||||||
asyncio.create_task(ctx.bot.wait_for("reaction_remove", check=predicates)),
|
asyncio.create_task(ctx.bot.wait_for("reaction_remove", check=predicates)),
|
||||||
@ -230,9 +310,14 @@ async def menu(
|
|||||||
except discord.NotFound:
|
except discord.NotFound:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return await controls[react.emoji](
|
if user is not None:
|
||||||
ctx, pages, controls, message, page, timeout, react.emoji
|
return await controls[react.emoji](
|
||||||
)
|
ctx, pages, controls, message, page, timeout, react.emoji, user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await controls[react.emoji](
|
||||||
|
ctx, pages, controls, message, page, timeout, react.emoji
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def next_page(
|
async def next_page(
|
||||||
@ -243,6 +328,8 @@ async def next_page(
|
|||||||
page: int,
|
page: int,
|
||||||
timeout: float,
|
timeout: float,
|
||||||
emoji: str,
|
emoji: str,
|
||||||
|
*,
|
||||||
|
user: Optional[discord.User] = None,
|
||||||
) -> _T:
|
) -> _T:
|
||||||
"""
|
"""
|
||||||
Function for showing next page which is suitable
|
Function for showing next page which is suitable
|
||||||
@ -252,7 +339,12 @@ async def next_page(
|
|||||||
page = 0 # Loop around to the first item
|
page = 0 # Loop around to the first item
|
||||||
else:
|
else:
|
||||||
page = page + 1
|
page = page + 1
|
||||||
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
|
if user is not None:
|
||||||
|
return await menu(
|
||||||
|
ctx, pages, controls, message=message, page=page, timeout=timeout, user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
async def prev_page(
|
async def prev_page(
|
||||||
@ -263,6 +355,8 @@ async def prev_page(
|
|||||||
page: int,
|
page: int,
|
||||||
timeout: float,
|
timeout: float,
|
||||||
emoji: str,
|
emoji: str,
|
||||||
|
*,
|
||||||
|
user: Optional[discord.User] = None,
|
||||||
) -> _T:
|
) -> _T:
|
||||||
"""
|
"""
|
||||||
Function for showing previous page which is suitable
|
Function for showing previous page which is suitable
|
||||||
@ -272,7 +366,12 @@ async def prev_page(
|
|||||||
page = len(pages) - 1 # Loop around to the last item
|
page = len(pages) - 1 # Loop around to the last item
|
||||||
else:
|
else:
|
||||||
page = page - 1
|
page = page - 1
|
||||||
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
|
if user is not None:
|
||||||
|
return await menu(
|
||||||
|
ctx, pages, controls, message=message, page=page, timeout=timeout, user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
async def close_menu(
|
async def close_menu(
|
||||||
@ -283,6 +382,8 @@ async def close_menu(
|
|||||||
page: int,
|
page: int,
|
||||||
timeout: float,
|
timeout: float,
|
||||||
emoji: str,
|
emoji: str,
|
||||||
|
*,
|
||||||
|
user: Optional[discord.User] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Function for closing (deleting) menu which is suitable
|
Function for closing (deleting) menu which is suitable
|
||||||
|
|||||||
@ -136,6 +136,7 @@ class SimpleMenu(discord.ui.View):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
self._fallback_author_to_ctx = True
|
||||||
self.author: Optional[discord.abc.User] = None
|
self.author: Optional[discord.abc.User] = None
|
||||||
self.message: Optional[discord.Message] = None
|
self.message: Optional[discord.Message] = None
|
||||||
self._source = _SimplePageSource(items=pages)
|
self._source = _SimplePageSource(items=pages)
|
||||||
@ -192,6 +193,19 @@ class SimpleMenu(discord.ui.View):
|
|||||||
def source(self):
|
def source(self):
|
||||||
return self._source
|
return self._source
|
||||||
|
|
||||||
|
@property
|
||||||
|
def author(self) -> Optional[discord.abc.User]:
|
||||||
|
if self._author is not None:
|
||||||
|
return self._author
|
||||||
|
if self._fallback_author_to_ctx:
|
||||||
|
return getattr(self.ctx, "author", None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@author.setter
|
||||||
|
def author(self, value: Optional[discord.abc.User]) -> None:
|
||||||
|
self._fallback_author_to_ctx = False
|
||||||
|
self._author = value
|
||||||
|
|
||||||
async def on_timeout(self):
|
async def on_timeout(self):
|
||||||
try:
|
try:
|
||||||
if self.delete_after_timeout and not self.message.flags.ephemeral:
|
if self.delete_after_timeout and not self.message.flags.ephemeral:
|
||||||
@ -225,19 +239,38 @@ class SimpleMenu(discord.ui.View):
|
|||||||
options = self.select_options[:25]
|
options = self.select_options[:25]
|
||||||
return _SelectMenu(options)
|
return _SelectMenu(options)
|
||||||
|
|
||||||
async def start(self, ctx: Context, *, ephemeral: bool = False):
|
async def start(
|
||||||
|
self, ctx: Context, *, user: Optional[discord.abc.User] = None, ephemeral: bool = False
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Used to start the menu displaying the first page requested.
|
Used to start the menu displaying the first page requested.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
The ``user`` parameter is considered `provisional <developer-guarantees-exclusions>`.
|
||||||
|
If no issues arise, we plan on including it under developer guarantees
|
||||||
|
in the first release made after 2024-05-18.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
ctx: `commands.Context`
|
ctx: `commands.Context`
|
||||||
The context to start the menu in.
|
The context to start the menu in.
|
||||||
|
user: discord.User
|
||||||
|
The user allowed to interact with the menu.
|
||||||
|
If this is ``None``, ``ctx.author`` will be able to interact with the menu.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
This parameter is `provisional <developer-guarantees-exclusions>`.
|
||||||
|
If no issues arise, we plan on including it under developer guarantees
|
||||||
|
in the first release made after 2024-05-18.
|
||||||
ephemeral: `bool`
|
ephemeral: `bool`
|
||||||
Send the message ephemerally. This only works
|
Send the message ephemerally. This only works
|
||||||
if the context is from a slash command interaction.
|
if the context is from a slash command interaction.
|
||||||
"""
|
"""
|
||||||
self.author = ctx.author
|
self._fallback_author_to_ctx = True
|
||||||
|
if user is not None:
|
||||||
|
self.author = user
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
kwargs = await self.get_page(self.current_page)
|
kwargs = await self.get_page(self.current_page)
|
||||||
self.message = await ctx.send(**kwargs, ephemeral=ephemeral)
|
self.message = await ctx.send(**kwargs, ephemeral=ephemeral)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user