[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 .. automodule:: redbot.core.utils.embed
:members: :members:
Menu Helpers Reaction Menus
============ ==============
.. automodule:: redbot.core.utils.menus .. automodule:: redbot.core.utils.menus
:members: :members:
Event Predicates
================
.. automodule:: redbot.core.utils.predicates
:members:
Mod Helpers Mod Helpers
=========== ===========

View File

@ -13,8 +13,16 @@ import time
import redbot.core import redbot.core
from redbot.core import Config, commands, checks, bank from redbot.core import Config, commands, checks, bank
from redbot.core.data_manager import cog_data_path 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.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 urllib.parse import urlparse
from .manager import shutdown_lavalink_server from .manager import shutdown_lavalink_server
@ -225,22 +233,17 @@ class Audio(commands.Cog):
async def dj(self, ctx): async def dj(self, ctx):
"""Toggle DJ mode (users need a role to use audio commands).""" """Toggle DJ mode (users need a role to use audio commands)."""
dj_role_id = await self.config.guild(ctx.guild).dj_role() 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( 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: try:
dj_role = await ctx.bot.wait_for("message", timeout=15.0, check=check) pred = MessagePredicate.valid_role(ctx)
dj_role_obj = discord.utils.get(ctx.guild.roles, name=dj_role.content) await ctx.bot.wait_for("message", timeout=15.0, check=pred)
if dj_role_obj is None: await ctx.invoke(self.role, pred.result)
return await self._embed_msg(ctx, "No role with that name.")
await ctx.invoke(self.role, dj_role_obj)
except asyncio.TimeoutError: 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() dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled) await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
@ -710,20 +713,21 @@ class Audio(commands.Cog):
return return
if player.current: if player.current:
for i in range(4): task = start_adding_reactions(message, expected[:4], ctx.bot.loop)
await message.add_reaction(expected[i]) else:
task = None
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)
)
try: 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: except asyncio.TimeoutError:
return await self._clear_react(message) return await self._clear_react(message)
else:
if task is not None:
task.cancel()
reacts = {v: k for k, v in emoji.items()} reacts = {v: k for k, v in emoji.items()}
react = reacts[r.emoji] react = reacts[r.emoji]
if react == "prev": if react == "prev":
@ -1125,11 +1129,12 @@ class Audio(commands.Cog):
if not playlist_name: if not playlist_name:
await self._embed_msg(ctx, "Please enter a name for this playlist.") 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: 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('"') playlist_name = playlist_name_msg.content.split(" ")[0].strip('"')
if len(playlist_name) > 20: if len(playlist_name) > 20:
return await self._embed_msg(ctx, "Try the command again with a shorter name.") 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." ctx, "Please upload the playlist file. Any other message will cancel this operation."
) )
def check(m):
return m.author == ctx.author
try: 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: except asyncio.TimeoutError:
return await self._embed_msg(ctx, "No file detected, try again later.") return await self._embed_msg(ctx, "No file detected, try again later.")
try: try:

View File

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

View File

@ -11,6 +11,7 @@ import discord
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import box, pagify from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("CustomCommands", __file__) _ = Translator("CustomCommands", __file__)
@ -58,14 +59,11 @@ class CommandObj:
).format("customcommand", "customcommand", "exit()") ).format("customcommand", "customcommand", "exit()")
await ctx.send(intro) await ctx.send(intro)
def check(m):
return m.channel == ctx.channel and m.author == ctx.message.author
responses = [] responses = []
args = None args = None
while True: while True:
await ctx.send(_("Add a random response:")) 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()": if msg.content.lower() == "exit()":
break break
@ -130,18 +128,27 @@ class CommandObj:
author = ctx.message.author author = ctx.message.author
def check(m):
return m.channel == ctx.channel and m.author == ctx.author
if ask_for and not response: 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) pred = MessagePredicate.yes_or_no(ctx)
if msg.content.lower() == "y": 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) response = await self.get_responses(ctx=ctx)
else: else:
await ctx.send(_("What response do you want?")) 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: if response:
# test to raise # 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.core.i18n import Translator, cog_i18n
from redbot.cogs.dataconverter.core_specs import SpecResolver from redbot.cogs.dataconverter.core_specs import SpecResolver
from redbot.core.utils.chat_formatting import box from redbot.core.utils.chat_formatting import box
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("DataConverter", __file__) _ = Translator("DataConverter", __file__)
@ -48,11 +49,10 @@ class DataConverter(commands.Cog):
menu_message = await ctx.send(box(menu)) menu_message = await ctx.send(box(menu))
def pred(m):
return m.channel == ctx.channel and m.author == ctx.author
try: 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: except asyncio.TimeoutError:
return await ctx.send(_("Try this again when you are more ready")) return await ctx.send(_("Try this again when you are more ready"))
else: else:

View File

@ -1,7 +1,7 @@
import asyncio import asyncio
import discord
from redbot.core import commands from redbot.core import commands
from redbot.core.utils.predicates import MessagePredicate
__all__ = ["do_install_agreement"] __all__ = ["do_install_agreement"]
@ -21,13 +21,12 @@ async def do_install_agreement(ctx: commands.Context):
if downloader is None or downloader.already_agreed: if downloader is None or downloader.already_agreed:
return True 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) await ctx.send(REPO_INSTALL_MSG)
try: 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: except asyncio.TimeoutError:
await ctx.send("Your response has timed out, please try again.") await ctx.send("Your response has timed out, please try again.")
return False return False

View File

@ -11,6 +11,8 @@ from redbot.core import checks, commands, config
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box 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 from .converters import CogOrCommand, RuleType, ClearableRuleType
@ -20,9 +22,6 @@ COG = "COG"
COMMAND = "COMMAND" COMMAND = "COMMAND"
GLOBAL = 0 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 # The strings in the schema are constants and should get extracted, but not translated until
# runtime. # runtime.
translate = _ translate = _
@ -566,35 +565,29 @@ class Permissions(commands.Cog):
"""Ask "Are you sure?" and get the response as a bool.""" """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: if ctx.guild is None or ctx.guild.me.permissions_in(ctx.channel).add_reactions:
msg = await ctx.send(_("Are you sure?")) msg = await ctx.send(_("Are you sure?"))
for emoji in REACTS.keys(): # noinspection PyAsyncCall
await msg.add_reaction(emoji) task = start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS, ctx.bot.loop)
pred = ReactionPredicate.yes_or_no(msg, ctx.author)
try: try:
reaction, user = await ctx.bot.wait_for( await ctx.bot.wait_for("reaction_add", check=pred, timeout=30)
"reaction_add",
check=lambda r, u: (
r.message.id == msg.id and u == ctx.author and r.emoji in REACTS
),
timeout=30,
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
agreed = False await ctx.send(_("Response timed out."))
return False
else: else:
agreed = REACTS.get(reaction.emoji) task.cancel()
agreed = pred.result
finally:
await msg.delete() await msg.delete()
else: else:
await ctx.send(_("Are you sure? (y/n)")) await ctx.send(_("Are you sure? (y/n)"))
pred = MessagePredicate.yes_or_no(ctx)
try: try:
message = await ctx.bot.wait_for( await ctx.bot.wait_for("message", check=pred, timeout=30)
"message",
check=lambda m: m.author == ctx.author
and m.channel == ctx.channel
and m.content in Y_OR_N,
timeout=30,
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
agreed = False await ctx.send(_("Response timed out."))
return False
else: else:
agreed = Y_OR_N.get(message.content.lower()) agreed = pred.result
if agreed is False: if agreed is False:
await ctx.send(_("Action cancelled.")) 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.utils.antispam import AntiSpam
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.predicates import MessagePredicate
from redbot.core.utils.tunnel import Tunnel from redbot.core.utils.tunnel import Tunnel
@ -136,13 +137,14 @@ class Reports(commands.Cog):
output += "\n{}".format(prompt) output += "\n{}".format(prompt)
for page in pagify(output, delims=["\n"]): for page in pagify(output, delims=["\n"]):
dm = await author.send(box(page)) await author.send(box(page))
def pred(m):
return m.author == author and m.channel == dm.channel
try: 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: except asyncio.TimeoutError:
await author.send(_("You took too long to select. Try again later.")) await author.send(_("You took too long to select. Try again later."))
return None return None
@ -247,7 +249,7 @@ class Reports(commands.Cog):
val = await self.send_report(_m, guild) val = await self.send_report(_m, guild)
else: else:
try: try:
dm = await author.send( await author.send(
_( _(
"Please respond to this message with your Report." "Please respond to this message with your Report."
"\nYour report should be a single message" "\nYour report should be a single message"
@ -256,11 +258,12 @@ class Reports(commands.Cog):
except discord.Forbidden: except discord.Forbidden:
return await ctx.send(_("This requires DMs enabled.")) return await ctx.send(_("This requires DMs enabled."))
def pred(m):
return m.author == author and m.channel == dm.channel
try: 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: except asyncio.TimeoutError:
return await author.send(_("You took too long. Try again later.")) return await author.send(_("You took too long. Try again later."))
else: else:

View File

@ -5,6 +5,7 @@ import discord
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("Warnings", __file__) _ = 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.")) await ctx.send(_("You may enter your response now."))
def same_author_check(m):
return m.author == ctx.author
try: 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: except asyncio.TimeoutError:
return None return None
else: else:
@ -140,11 +140,10 @@ async def get_command_for_dropping_points(ctx: commands.Context):
await ctx.send(_("You may enter your response now.")) await ctx.send(_("You may enter your response now."))
def same_author_check(m):
return m.author == ctx.author
try: 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: except asyncio.TimeoutError:
return None return None
else: 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.mod import is_admin_or_superior
from redbot.core.utils.chat_formatting import warning, pagify from redbot.core.utils.chat_formatting import warning, pagify
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.utils.predicates import MessagePredicate
_ = Translator("Warnings", __file__) _ = Translator("Warnings", __file__)
@ -363,12 +364,11 @@ class Warnings(commands.Cog):
"""Handles getting description and points for custom reasons""" """Handles getting description and points for custom reasons"""
to_add = {"points": 0, "description": ""} 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?")) await ctx.send(_("How many points should be given for this reason?"))
try: 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: except asyncio.TimeoutError:
await ctx.send(_("Ok then.")) await ctx.send(_("Ok then."))
return return
@ -385,7 +385,9 @@ class Warnings(commands.Cog):
await ctx.send(_("Enter a description for this reason.")) await ctx.send(_("Enter a description for this reason."))
try: 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: except asyncio.TimeoutError:
await ctx.send(_("Ok then.")) await ctx.send(_("Ok then."))
return return

View File

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

View File

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

View File

@ -1,15 +1,14 @@
""" # Original source of reaction-based menu idea from
Original source of reaction-based menu idea from # https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py #
# Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
Ported to Red V3 by Palm\_\_ (https://github.com/palmtree5)
"""
import asyncio import asyncio
import contextlib import contextlib
from typing import Union, Iterable from typing import Union, Iterable, Optional
import discord import discord
from redbot.core import commands from .. import commands
from .predicates import ReactionPredicate
_ReactableEmoji = Union[str, discord.Emoji] _ReactableEmoji = Union[str, discord.Emoji]
@ -71,18 +70,20 @@ async def menu(
else: else:
message = await ctx.send(current_page) message = await ctx.send(current_page)
# Don't wait for reactions to be added (GH-1797) # 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: else:
if isinstance(current_page, discord.Embed): if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page) await message.edit(embed=current_page)
else: else:
await message.edit(content=current_page) 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: 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: except asyncio.TimeoutError:
try: try:
await message.clear_reactions() await message.clear_reactions()
@ -152,12 +153,51 @@ async def close_menu(
return None return None
async def _add_menu_reactions(message: discord.Message, emojis: Iterable[_ReactableEmoji]): def start_adding_reactions(
"""Add the 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 # The task should exit silently if the message is deleted
with contextlib.suppress(discord.NotFound): with contextlib.suppress(discord.NotFound):
for emoji in emojis: for emoji in emojis:
await message.add_reaction(emoji) 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} DEFAULT_CONTROLS = {"": prev_page, "": close_menu, "": next_page}

File diff suppressed because it is too large Load Diff