mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -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]))
|
||||
c = menus.DEFAULT_CONTROLS if len(pages) > 1 else {"\N{CROSS MARK}": menus.close_menu}
|
||||
# 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(
|
||||
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
|
||||
menus.start_adding_reactions(m, c.keys())
|
||||
|
||||
@ -49,48 +49,126 @@ class _GenericButton(discord.ui.Button):
|
||||
if self.emoji.is_unicode_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(
|
||||
ctx: commands.Context,
|
||||
pages: _PageList,
|
||||
controls: Optional[Mapping[str, _ControlCallable]] = None,
|
||||
message: discord.Message = None,
|
||||
message: Optional[discord.Message] = None,
|
||||
page: int = 0,
|
||||
timeout: float = 30.0,
|
||||
*,
|
||||
user: Optional[discord.User] = None,
|
||||
) -> _T:
|
||||
"""
|
||||
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
|
||||
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 last one, and none of the
|
||||
parameters in the handling functions are optional
|
||||
.. 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.
|
||||
|
||||
.. 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
|
||||
----------
|
||||
ctx: commands.Context
|
||||
The command context
|
||||
pages: `list` of `str` or `discord.Embed`
|
||||
pages: Union[List[str], List[discord.Embed]]
|
||||
The pages of the menu.
|
||||
All pages need to be of the same type (either `str` or `discord.Embed`).
|
||||
controls: Optional[Mapping[str, Callable]]
|
||||
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
|
||||
and should additionally accept an ``emoji`` parameter of type `str`.
|
||||
If not passed, `DEFAULT_CONTROLS` is used *or*
|
||||
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 menu
|
||||
page: int
|
||||
The current page number of the menu
|
||||
timeout: float
|
||||
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
|
||||
------
|
||||
@ -136,7 +214,7 @@ async def menu(
|
||||
# internally we already include the emojis we expect.
|
||||
if controls == DEFAULT_CONTROLS:
|
||||
view = SimpleMenu(pages, timeout=timeout)
|
||||
await view.start(ctx)
|
||||
await view.start(ctx, user=user)
|
||||
await view.wait()
|
||||
return
|
||||
else:
|
||||
@ -169,7 +247,7 @@ async def menu(
|
||||
view.remove_item(view.stop_button)
|
||||
for emoji, func in to_add.items():
|
||||
view.add_item(_GenericButton(emoji, func))
|
||||
await view.start(ctx)
|
||||
await view.start(ctx, user=user)
|
||||
_active_menus[view.message.id] = view
|
||||
await view.wait()
|
||||
del _active_menus[view.message.id]
|
||||
@ -194,7 +272,9 @@ async def menu(
|
||||
return
|
||||
|
||||
try:
|
||||
predicates = ReactionPredicate.with_emojis(tuple(controls.keys()), message, ctx.author)
|
||||
predicates = ReactionPredicate.with_emojis(
|
||||
tuple(controls.keys()), message, user or ctx.author
|
||||
)
|
||||
tasks = [
|
||||
asyncio.create_task(ctx.bot.wait_for("reaction_add", check=predicates)),
|
||||
asyncio.create_task(ctx.bot.wait_for("reaction_remove", check=predicates)),
|
||||
@ -230,9 +310,14 @@ async def menu(
|
||||
except discord.NotFound:
|
||||
return
|
||||
else:
|
||||
return await controls[react.emoji](
|
||||
ctx, pages, controls, message, page, timeout, react.emoji
|
||||
)
|
||||
if user is not None:
|
||||
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(
|
||||
@ -243,6 +328,8 @@ async def next_page(
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
*,
|
||||
user: Optional[discord.User] = None,
|
||||
) -> _T:
|
||||
"""
|
||||
Function for showing next page which is suitable
|
||||
@ -252,7 +339,12 @@ async def next_page(
|
||||
page = 0 # Loop around to the first item
|
||||
else:
|
||||
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(
|
||||
@ -263,6 +355,8 @@ async def prev_page(
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
*,
|
||||
user: Optional[discord.User] = None,
|
||||
) -> _T:
|
||||
"""
|
||||
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
|
||||
else:
|
||||
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(
|
||||
@ -283,6 +382,8 @@ async def close_menu(
|
||||
page: int,
|
||||
timeout: float,
|
||||
emoji: str,
|
||||
*,
|
||||
user: Optional[discord.User] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Function for closing (deleting) menu which is suitable
|
||||
|
||||
@ -136,6 +136,7 @@ class SimpleMenu(discord.ui.View):
|
||||
super().__init__(
|
||||
timeout=timeout,
|
||||
)
|
||||
self._fallback_author_to_ctx = True
|
||||
self.author: Optional[discord.abc.User] = None
|
||||
self.message: Optional[discord.Message] = None
|
||||
self._source = _SimplePageSource(items=pages)
|
||||
@ -192,6 +193,19 @@ class SimpleMenu(discord.ui.View):
|
||||
def source(self):
|
||||
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):
|
||||
try:
|
||||
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]
|
||||
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.
|
||||
|
||||
.. 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
|
||||
----------
|
||||
ctx: `commands.Context`
|
||||
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`
|
||||
Send the message ephemerally. This only works
|
||||
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
|
||||
kwargs = await self.get_page(self.current_page)
|
||||
self.message = await ctx.send(**kwargs, ephemeral=ephemeral)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user