mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-21 10:17:59 -05:00
[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:
@@ -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
|
||||
|
||||
|
||||
@@ -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":
|
||||
await ctx.send("Alright. Bye :wave:")
|
||||
log.debug("Leaving '{}'".format(guild.name))
|
||||
await guild.leave()
|
||||
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 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
|
||||
|
||||
while msg is not None:
|
||||
try:
|
||||
msg = await self.bot.wait_for("message", check=msg_check, 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
|
||||
|
||||
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
|
||||
query = await ctx.send("To leave a server, just type its number.")
|
||||
|
||||
pred = MessagePredicate.contained_in(responses, ctx)
|
||||
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.")
|
||||
return
|
||||
await server.leave()
|
||||
if server != ctx.guild:
|
||||
await self.bot.wait_for("message", check=pred, timeout=15)
|
||||
except asyncio.TimeoutError:
|
||||
await query.delete()
|
||||
else:
|
||||
await self.leave_confirmation(guilds[pred.result], ctx)
|
||||
|
||||
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 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":
|
||||
await ctx.author.send(
|
||||
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
|
||||
)
|
||||
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..."))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"""
|
||||
# The task should exit silently if the message is deleted
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
for emoji in emojis:
|
||||
await message.add_reaction(emoji)
|
||||
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
Reference in New Issue
Block a user