[Utils] Finish and Refactor Predicate Utility (#2169)

* Uses classmethods to create predicates
* Classmethods allow using a combination of different parameters to describe context
* Some predicates assign a captured `result` to the predicate object on success
* Added `ReactionPredicate` equivalent to `MessagePredicate`
* Added `utils.menus.start_adding_reactions`, a non-blocking method for adding reactions asynchronously
* Added documentation
* Uses these new utils throughout the core bot
Happened to also find some bugs in places, and places where we were waiting for events without catching `asyncio.TimeoutError`

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
This commit is contained in:
Toby Harradine 2018-10-06 08:07:09 +10:00 committed by GitHub
parent 5d44bfabed
commit dea9dde637
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1229 additions and 320 deletions

View File

@ -22,12 +22,18 @@ Embed Helpers
.. automodule:: redbot.core.utils.embed
:members:
Menu Helpers
============
Reaction Menus
==============
.. automodule:: redbot.core.utils.menus
:members:
Event Predicates
================
.. automodule:: redbot.core.utils.predicates
:members:
Mod Helpers
===========

View File

@ -13,8 +13,16 @@ import time
import redbot.core
from redbot.core import Config, commands, checks, bank
from redbot.core.data_manager import cog_data_path
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, prev_page, next_page, close_menu
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.menus import (
menu,
DEFAULT_CONTROLS,
prev_page,
next_page,
close_menu,
start_adding_reactions,
)
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
from urllib.parse import urlparse
from .manager import shutdown_lavalink_server
@ -225,22 +233,17 @@ class Audio(commands.Cog):
async def dj(self, ctx):
"""Toggle DJ mode (users need a role to use audio commands)."""
dj_role_id = await self.config.guild(ctx.guild).dj_role()
if dj_role_id is None:
if dj_role_id is None and ctx.guild.get_role(dj_role_id):
await self._embed_msg(
ctx, "Please set a role to use with DJ mode. Enter the role name now."
ctx, "Please set a role to use with DJ mode. Enter the role name or ID now."
)
def check(m):
return m.author == ctx.author
try:
dj_role = await ctx.bot.wait_for("message", timeout=15.0, check=check)
dj_role_obj = discord.utils.get(ctx.guild.roles, name=dj_role.content)
if dj_role_obj is None:
return await self._embed_msg(ctx, "No role with that name.")
await ctx.invoke(self.role, dj_role_obj)
pred = MessagePredicate.valid_role(ctx)
await ctx.bot.wait_for("message", timeout=15.0, check=pred)
await ctx.invoke(self.role, pred.result)
except asyncio.TimeoutError:
return await self._embed_msg(ctx, "No role entered, try again later.")
return await self._embed_msg(ctx, "Response timed out, try again later.")
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
@ -710,20 +713,21 @@ class Audio(commands.Cog):
return
if player.current:
for i in range(4):
await message.add_reaction(expected[i])
def check(r, u):
return (
r.message.id == message.id
and u == ctx.message.author
and any(e in str(r.emoji) for e in expected)
)
task = start_adding_reactions(message, expected[:4], ctx.bot.loop)
else:
task = None
try:
(r, u) = await self.bot.wait_for("reaction_add", check=check, timeout=10.0)
(r, u) = await self.bot.wait_for(
"reaction_add",
check=ReactionPredicate.with_emojis(expected, message, ctx.author),
timeout=10.0,
)
except asyncio.TimeoutError:
return await self._clear_react(message)
else:
if task is not None:
task.cancel()
reacts = {v: k for k, v in emoji.items()}
react = reacts[r.emoji]
if react == "prev":
@ -1125,11 +1129,12 @@ class Audio(commands.Cog):
if not playlist_name:
await self._embed_msg(ctx, "Please enter a name for this playlist.")
def check(m):
return m.author == ctx.author and not m.content.startswith(ctx.prefix)
try:
playlist_name_msg = await ctx.bot.wait_for("message", timeout=15.0, check=check)
playlist_name_msg = await ctx.bot.wait_for(
"message",
timeout=15.0,
check=MessagePredicate.regex(fr"^(?!{ctx.prefix})", ctx),
)
playlist_name = playlist_name_msg.content.split(" ")[0].strip('"')
if len(playlist_name) > 20:
return await self._embed_msg(ctx, "Try the command again with a shorter name.")
@ -1238,11 +1243,10 @@ class Audio(commands.Cog):
ctx, "Please upload the playlist file. Any other message will cancel this operation."
)
def check(m):
return m.author == ctx.author
try:
file_message = await ctx.bot.wait_for("message", timeout=30.0, check=check)
file_message = await ctx.bot.wait_for(
"message", timeout=30.0, check=MessagePredicate.same_context(ctx)
)
except asyncio.TimeoutError:
return await self._embed_msg(ctx, "No file detected, try again later.")
try:

View File

@ -9,6 +9,7 @@ from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.mod import slow_deletion, mass_purge
from redbot.cogs.mod.log import log
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("Cleanup", __file__)
@ -31,13 +32,10 @@ class Cleanup(commands.Cog):
Tries its best to cleanup after itself if the response is positive.
"""
def author_check(message):
return message.author == ctx.author
prompt = await ctx.send(
_("Are you sure you want to delete {} messages? (y/n)").format(number)
)
response = await ctx.bot.wait_for("message", check=author_check)
response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
if response.content.lower().startswith("y"):
await prompt.delete()

View File

@ -11,6 +11,7 @@ import discord
from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("CustomCommands", __file__)
@ -58,14 +59,11 @@ class CommandObj:
).format("customcommand", "customcommand", "exit()")
await ctx.send(intro)
def check(m):
return m.channel == ctx.channel and m.author == ctx.message.author
responses = []
args = None
while True:
await ctx.send(_("Add a random response:"))
msg = await self.bot.wait_for("message", check=check)
msg = await self.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
if msg.content.lower() == "exit()":
break
@ -130,18 +128,27 @@ class CommandObj:
author = ctx.message.author
def check(m):
return m.channel == ctx.channel and m.author == ctx.author
if ask_for and not response:
await ctx.send(_("Do you want to create a 'randomized' cc? {}").format("y/n"))
await ctx.send(_("Do you want to create a 'randomized' custom command? (y/n)"))
msg = await self.bot.wait_for("message", check=check)
if msg.content.lower() == "y":
pred = MessagePredicate.yes_or_no(ctx)
try:
await self.bot.wait_for("message", check=pred, timeout=30)
except TimeoutError:
await ctx.send(_("Response timed out, please try again later."))
return
if pred.result is True:
response = await self.get_responses(ctx=ctx)
else:
await ctx.send(_("What response do you want?"))
response = (await self.bot.wait_for("message", check=check)).content
try:
resp = await self.bot.wait_for(
"message", check=MessagePredicate.same_context(ctx), timeout=180
)
except TimeoutError:
await ctx.send(_("Response timed out, please try again later."))
return
response = resp.content
if response:
# test to raise

View File

@ -6,6 +6,7 @@ from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.cogs.dataconverter.core_specs import SpecResolver
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("DataConverter", __file__)
@ -48,11 +49,10 @@ class DataConverter(commands.Cog):
menu_message = await ctx.send(box(menu))
def pred(m):
return m.channel == ctx.channel and m.author == ctx.author
try:
message = await self.bot.wait_for("message", check=pred, timeout=60)
message = await self.bot.wait_for(
"message", check=MessagePredicate.same_context(ctx), timeout=60
)
except asyncio.TimeoutError:
return await ctx.send(_("Try this again when you are more ready"))
else:

View File

@ -1,7 +1,7 @@
import asyncio
import discord
from redbot.core import commands
from redbot.core.utils.predicates import MessagePredicate
__all__ = ["do_install_agreement"]
@ -21,13 +21,12 @@ async def do_install_agreement(ctx: commands.Context):
if downloader is None or downloader.already_agreed:
return True
def does_agree(msg: discord.Message):
return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
await ctx.send(REPO_INSTALL_MSG)
try:
await ctx.bot.wait_for("message", check=does_agree, timeout=30)
await ctx.bot.wait_for(
"message", check=MessagePredicate.lower_equal_to("i agree", ctx), timeout=30
)
except asyncio.TimeoutError:
await ctx.send("Your response has timed out, please try again.")
return False

View File

@ -11,6 +11,8 @@ from redbot.core import checks, commands, config
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate, MessagePredicate
from .converters import CogOrCommand, RuleType, ClearableRuleType
@ -20,9 +22,6 @@ COG = "COG"
COMMAND = "COMMAND"
GLOBAL = 0
# noinspection PyDictDuplicateKeys
REACTS = {"\N{WHITE HEAVY CHECK MARK}": True, "\N{NEGATIVE SQUARED CROSS MARK}": False}
Y_OR_N = {"y": True, "yes": True, "n": False, "no": False}
# The strings in the schema are constants and should get extracted, but not translated until
# runtime.
translate = _
@ -566,35 +565,29 @@ class Permissions(commands.Cog):
"""Ask "Are you sure?" and get the response as a bool."""
if ctx.guild is None or ctx.guild.me.permissions_in(ctx.channel).add_reactions:
msg = await ctx.send(_("Are you sure?"))
for emoji in REACTS.keys():
await msg.add_reaction(emoji)
# noinspection PyAsyncCall
task = start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS, ctx.bot.loop)
pred = ReactionPredicate.yes_or_no(msg, ctx.author)
try:
reaction, user = await ctx.bot.wait_for(
"reaction_add",
check=lambda r, u: (
r.message.id == msg.id and u == ctx.author and r.emoji in REACTS
),
timeout=30,
)
await ctx.bot.wait_for("reaction_add", check=pred, timeout=30)
except asyncio.TimeoutError:
agreed = False
await ctx.send(_("Response timed out."))
return False
else:
agreed = REACTS.get(reaction.emoji)
task.cancel()
agreed = pred.result
finally:
await msg.delete()
else:
await ctx.send(_("Are you sure? (y/n)"))
pred = MessagePredicate.yes_or_no(ctx)
try:
message = await ctx.bot.wait_for(
"message",
check=lambda m: m.author == ctx.author
and m.channel == ctx.channel
and m.content in Y_OR_N,
timeout=30,
)
await ctx.bot.wait_for("message", check=pred, timeout=30)
except asyncio.TimeoutError:
agreed = False
await ctx.send(_("Response timed out."))
return False
else:
agreed = Y_OR_N.get(message.content.lower())
agreed = pred.result
if agreed is False:
await ctx.send(_("Action cancelled."))

View File

@ -11,6 +11,7 @@ from redbot.core.utils.chat_formatting import pagify, box
from redbot.core.utils.antispam import AntiSpam
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.predicates import MessagePredicate
from redbot.core.utils.tunnel import Tunnel
@ -136,13 +137,14 @@ class Reports(commands.Cog):
output += "\n{}".format(prompt)
for page in pagify(output, delims=["\n"]):
dm = await author.send(box(page))
def pred(m):
return m.author == author and m.channel == dm.channel
await author.send(box(page))
try:
message = await self.bot.wait_for("message", check=pred, timeout=45)
message = await self.bot.wait_for(
"message",
check=MessagePredicate.same_context(channel=author.dm_channel, user=author),
timeout=45,
)
except asyncio.TimeoutError:
await author.send(_("You took too long to select. Try again later."))
return None
@ -247,7 +249,7 @@ class Reports(commands.Cog):
val = await self.send_report(_m, guild)
else:
try:
dm = await author.send(
await author.send(
_(
"Please respond to this message with your Report."
"\nYour report should be a single message"
@ -256,11 +258,12 @@ class Reports(commands.Cog):
except discord.Forbidden:
return await ctx.send(_("This requires DMs enabled."))
def pred(m):
return m.author == author and m.channel == dm.channel
try:
message = await self.bot.wait_for("message", check=pred, timeout=180)
message = await self.bot.wait_for(
"message",
check=MessagePredicate.same_context(ctx, channel=author.dm_channel),
timeout=180,
)
except asyncio.TimeoutError:
return await author.send(_("You took too long. Try again later."))
else:

View File

@ -5,6 +5,7 @@ import discord
from redbot.core import Config, checks, commands
from redbot.core.i18n import Translator
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("Warnings", __file__)
@ -95,11 +96,10 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
await ctx.send(_("You may enter your response now."))
def same_author_check(m):
return m.author == ctx.author
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
msg = await ctx.bot.wait_for(
"message", check=MessagePredicate.same_context(ctx), timeout=30
)
except asyncio.TimeoutError:
return None
else:
@ -140,11 +140,10 @@ async def get_command_for_dropping_points(ctx: commands.Context):
await ctx.send(_("You may enter your response now."))
def same_author_check(m):
return m.author == ctx.author
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
msg = await ctx.bot.wait_for(
"message", check=MessagePredicate.same_context(ctx), timeout=30
)
except asyncio.TimeoutError:
return None
else:

View File

@ -15,6 +15,7 @@ from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.mod import is_admin_or_superior
from redbot.core.utils.chat_formatting import warning, pagify
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("Warnings", __file__)
@ -363,12 +364,11 @@ class Warnings(commands.Cog):
"""Handles getting description and points for custom reasons"""
to_add = {"points": 0, "description": ""}
def same_author_check(m):
return m.author == ctx.author
await ctx.send(_("How many points should be given for this reason?"))
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
msg = await ctx.bot.wait_for(
"message", check=MessagePredicate.same_context(ctx), timeout=30
)
except asyncio.TimeoutError:
await ctx.send(_("Ok then."))
return
@ -385,7 +385,9 @@ class Warnings(commands.Cog):
await ctx.send(_("Enter a description for this reason."))
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
msg = await ctx.bot.wait_for(
"message", check=MessagePredicate.same_context(ctx), timeout=30
)
except asyncio.TimeoutError:
await ctx.send(_("Ok then."))
return

View File

@ -6,6 +6,7 @@ from discord.ext import commands
from .requires import PermState
from ..utils.chat_formatting import box
from ..utils.predicates import MessagePredicate
from ..utils import common_filters
TICK = "\N{WHITE HEAVY CHECK MARK}"
@ -141,10 +142,6 @@ class Context(commands.Context):
messages = tuple(messages)
ret = []
more_check = lambda m: (
m.author == self.author and m.channel == self.channel and m.content.lower() == "more"
)
for idx, page in enumerate(messages, 1):
if box_lang is None:
msg = await self.send(page)
@ -165,7 +162,11 @@ class Context(commands.Context):
"".format(is_are, n_remaining, plural)
)
try:
resp = await self.bot.wait_for("message", check=more_check, timeout=timeout)
resp = await self.bot.wait_for(
"message",
check=MessagePredicate.lower_equal_to("more", self),
timeout=timeout,
)
except asyncio.TimeoutError:
await query.delete()
break
@ -175,7 +176,7 @@ class Context(commands.Context):
except (discord.HTTPException, AttributeError):
# In case the bot can't delete other users' messages,
# or is not a bot account
# or chanel is a DM
# or channel is a DM
await query.delete()
return ret

View File

@ -24,6 +24,7 @@ from redbot.core import __version__
from redbot.core import checks
from redbot.core import i18n
from redbot.core import commands
from .utils.predicates import MessagePredicate
from .utils.chat_formatting import pagify, box, inline
if TYPE_CHECKING:
@ -438,73 +439,63 @@ class Core(commands.Cog, CoreLogic):
@checks.is_owner()
async def leave(self, ctx):
"""Leaves server"""
author = ctx.author
guild = ctx.guild
await ctx.send("Are you sure you want me to leave this server? (y/n)")
await ctx.send("Are you sure you want me to leave this server? Type yes to confirm.")
def conf_check(m):
return m.author == author
response = await self.bot.wait_for("message", check=conf_check)
if response.content.lower().strip() == "yes":
pred = MessagePredicate.yes_or_no(ctx)
try:
await self.bot.wait_for("message", check=MessagePredicate.yes_or_no(ctx))
except asyncio.TimeoutError:
await ctx.send("Response timed out.")
return
else:
if pred.result is True:
await ctx.send("Alright. Bye :wave:")
log.debug("Leaving '{}'".format(guild.name))
await guild.leave()
log.debug("Leaving guild '{}'".format(ctx.guild.name))
await ctx.guild.leave()
else:
await ctx.send("Alright, I'll stay then :)")
@commands.command()
@checks.is_owner()
async def servers(self, ctx):
"""Lists and allows to leave servers"""
owner = ctx.author
guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower())
msg = ""
responses = []
for i, server in enumerate(guilds, 1):
msg += "{}: {}\n".format(i, server.name)
msg += "\nTo leave a server, just type its number."
responses.append(str(i))
for page in pagify(msg, ["\n"]):
await ctx.send(page)
def msg_check(m):
return m.author == owner
query = await ctx.send("To leave a server, just type its number.")
while msg is not None:
pred = MessagePredicate.contained_in(responses, ctx)
try:
msg = await self.bot.wait_for("message", check=msg_check, timeout=15)
await self.bot.wait_for("message", check=pred, timeout=15)
except asyncio.TimeoutError:
await ctx.send("I guess not.")
break
try:
msg = int(msg.content) - 1
if msg < 0:
break
await self.leave_confirmation(guilds[msg], owner, ctx)
break
except (IndexError, ValueError, AttributeError):
pass
await query.delete()
else:
await self.leave_confirmation(guilds[pred.result], ctx)
async def leave_confirmation(self, server, owner, ctx):
await ctx.send("Are you sure you want me to leave {}? (yes/no)".format(server.name))
def conf_check(m):
return m.author == owner
try:
msg = await self.bot.wait_for("message", check=conf_check, timeout=15)
if msg.content.lower().strip() in ("yes", "y"):
if server.owner == ctx.bot.user:
async def leave_confirmation(self, guild, ctx):
if guild.owner.id == ctx.bot.user.id:
await ctx.send("I cannot leave a guild I am the owner of.")
return
await server.leave()
if server != ctx.guild:
await ctx.send("Are you sure you want me to leave {}? (yes/no)".format(guild.name))
pred = MessagePredicate.yes_or_no(ctx)
try:
await self.bot.wait_for("message", check=pred, timeout=15)
if pred.result is True:
await guild.leave()
if guild != ctx.guild:
await ctx.send("Done.")
else:
await ctx.send("Alright then.")
except asyncio.TimeoutError:
await ctx.send("I guess not.")
await ctx.send("Response timed out.")
@commands.command()
@checks.is_owner()
@ -892,10 +883,6 @@ class Core(commands.Cog, CoreLogic):
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
async def owner(self, ctx):
"""Sets Red's main owner"""
def check(m):
return m.author == ctx.author and m.channel == ctx.channel
# According to the Python docs this is suitable for cryptographic use
random = SystemRandom()
length = random.randint(25, 35)
@ -919,10 +906,14 @@ class Core(commands.Cog, CoreLogic):
)
try:
message = await ctx.bot.wait_for("message", check=check, timeout=60)
message = await ctx.bot.wait_for(
"message", check=MessagePredicate.same_context(ctx), timeout=60
)
except asyncio.TimeoutError:
self.owner.reset_cooldown(ctx)
await ctx.send(_("The set owner request has timed out."))
await ctx.send(
_("The `{prefix}set owner` request has timed out.").format(prefix=ctx.prefix)
)
else:
if message.content.strip() == token:
self.owner.reset_cooldown(ctx)
@ -1146,18 +1137,20 @@ class Core(commands.Cog, CoreLogic):
)
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
def same_author_check(m):
return m.author == ctx.author and m.channel == ctx.channel
pred = MessagePredicate.yes_or_no(ctx)
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=60)
await ctx.bot.wait_for("message", check=pred, timeout=60)
except asyncio.TimeoutError:
await ctx.send(_("Ok then."))
await ctx.send(_("Response timed out."))
else:
if msg.content.lower().strip() == "y":
if pred.result is True:
await ctx.send(_("OK, it's on its way!"))
async with ctx.author.typing():
await ctx.author.send(
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
)
else:
await ctx.send(_("OK then."))
else:
await ctx.send(_("That directory doesn't seem to exist..."))

View File

@ -8,9 +8,11 @@ from contextlib import redirect_stdout
from copy import copy
import discord
from . import checks, commands
from .i18n import Translator
from .utils.chat_formatting import box, pagify
from .utils.predicates import MessagePredicate
"""
Notice:
@ -218,12 +220,8 @@ class Dev(commands.Cog):
self.sessions.add(ctx.channel.id)
await ctx.send(_("Enter code to execute or evaluate. `exit()` or `quit` to exit."))
msg_check = lambda m: (
m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")
)
while True:
response = await ctx.bot.wait_for("message", check=msg_check)
response = await ctx.bot.wait_for("message", check=MessagePredicate.regex(r"^`", ctx))
cleaned = self.cleanup_code(response.content)

View File

@ -1,15 +1,14 @@
"""
Original source of reaction-based menu idea from
https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
"""
# Original source of reaction-based menu idea from
# https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
#
# Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
import asyncio
import contextlib
from typing import Union, Iterable
from typing import Union, Iterable, Optional
import discord
from redbot.core import commands
from .. import commands
from .predicates import ReactionPredicate
_ReactableEmoji = Union[str, discord.Emoji]
@ -71,18 +70,20 @@ async def menu(
else:
message = await ctx.send(current_page)
# Don't wait for reactions to be added (GH-1797)
ctx.bot.loop.create_task(_add_menu_reactions(message, controls.keys()))
# noinspection PyAsyncCall
start_adding_reactions(message, controls.keys(), ctx.bot.loop)
else:
if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page)
else:
await message.edit(content=current_page)
def react_check(r, u):
return u == ctx.author and r.message.id == message.id and str(r.emoji) in controls.keys()
try:
react, user = await ctx.bot.wait_for("reaction_add", check=react_check, timeout=timeout)
react, user = await ctx.bot.wait_for(
"reaction_add",
check=ReactionPredicate.with_emojis(tuple(controls.keys()), message, ctx.author),
timeout=timeout,
)
except asyncio.TimeoutError:
try:
await message.clear_reactions()
@ -152,12 +153,51 @@ async def close_menu(
return None
async def _add_menu_reactions(message: discord.Message, emojis: Iterable[_ReactableEmoji]):
"""Add the reactions"""
def start_adding_reactions(
message: discord.Message,
emojis: Iterable[_ReactableEmoji],
loop: Optional[asyncio.AbstractEventLoop] = None,
) -> asyncio.Task:
"""Start adding reactions to a message.
This is a non-blocking operation - calling this will schedule the
reactions being added, but will the calling code will continue to
execute asynchronously. There is no need to await this function.
This is particularly useful if you wish to start waiting for a
reaction whilst the reactions are still being added - in fact,
this is exactly what `menu` uses to do that.
This spawns a `asyncio.Task` object and schedules it on ``loop``.
If ``loop`` omitted, the loop will be retreived with
`asyncio.get_event_loop`.
Parameters
----------
message: discord.Message
The message to add reactions to.
emojis : Iterable[Union[str, discord.Emoji]]
The emojis to react to the message with.
loop : Optional[asyncio.AbstractEventLoop]
The event loop.
Returns
-------
asyncio.Task
The task for the coroutine adding the reactions.
"""
async def task():
# The task should exit silently if the message is deleted
with contextlib.suppress(discord.NotFound):
for emoji in emojis:
await message.add_reaction(emoji)
if loop is None:
loop = asyncio.get_event_loop()
return loop.create_task(task())
DEFAULT_CONTROLS = {"": prev_page, "": close_menu, "": next_page}

File diff suppressed because it is too large Load Diff