From 2c87e96262a558938459766697573b38644b552d Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 9 Sep 2022 12:05:35 +0100 Subject: [PATCH 1/5] Add `check_rerun_job` util to commands.py --- botcore/utils/commands.py | 62 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/botcore/utils/commands.py b/botcore/utils/commands.py index 7afd81376..57da19286 100644 --- a/botcore/utils/commands.py +++ b/botcore/utils/commands.py @@ -1,9 +1,15 @@ +from asyncio import TimeoutError +from contextlib import suppress from typing import Optional -from discord import Message +from discord import HTTPException, Message, NotFound from discord.ext.commands import BadArgument, Context, clean_content +REDO_EMOJI = '\U0001f501' # :repeat: +REDO_TIMEOUT = 30 + + async def clean_text_or_reply(ctx: Context, text: Optional[str] = None) -> str: """ Cleans a text argument or replied message's content. @@ -36,3 +42,57 @@ async def clean_text_or_reply(ctx: Context, text: Optional[str] = None) -> str: # No text provided, and either no message was referenced or we can't access the content raise BadArgument("Couldn't find text to clean. Provide a string or reply to a message to use its content.") + + +async def check_rerun_job(ctx: Context, response: Message) -> Optional[str]: + """ + Check if the job should be rerun. + + For a job to be rerun, the user must edit their message within `REDO_TIMEOUT` seconds, + and then react with the `REDO_EMOJI` within 10 seconds. + + Args: + ctx: The command's context + response: The job's response message + + Returns: + The content to be rerun, or `None`. + """ + # Correct message and content did actually change (i.e. wasn't a pin status udpate etc.) + _message_edit_predicate = lambda old, new: new.id == ctx.message.id and new.content != old.content + + _reaction_add_predicate = lambda reaction, user: all(( + user.id == ctx.author.id, # correct user + str(reaction) == REDO_EMOJI, # correct emoji + reaction.message.id == ctx.message.id # correct message + )) + + with suppress(NotFound): + try: + _, new_message = await ctx.bot.wait_for( + 'message_edit', + check=_message_edit_predicate, + timeout=REDO_TIMEOUT + ) + await ctx.message.add_reaction(REDO_EMOJI) + + await ctx.bot.wait_for( + 'reaction_add', + check=_reaction_add_predicate, + timeout=10 + ) + + await ctx.message.clear_reaction(REDO_EMOJI) + with suppress(HTTPException): + await response.delete() + + except TimeoutError: + # One of the `wait_for` timed out, so abort since user doesn't want to rerun + await ctx.message.clear_reaction(REDO_EMOJI) + return None + + else: + # Both `wait_for` triggered, so return the new content to be run since user wants to rerun + return new_message.content + + return None # triggered whenever a `NotFound` was raised From cdc9b30e9fc1e7b0974b2041e26b89336c269b8e Mon Sep 17 00:00:00 2001 From: Izan Date: Wed, 14 Sep 2022 21:17:08 +0100 Subject: [PATCH 2/5] Update changelog.rst --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a36bcc162..aa97d1ff7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ Changelog ========= +- :release:`8.3.0 <9th September 2922>` +- :feature:`137` Add a utility to check for a job rerun + + - :release:`8.2.0 <18th August 2022>` - :support:`125` Bump Discord.py to the stable :literal-url:`2.0 release `. From 4e008c2fe1aba670dc714ec6aa8d444eaa61198a Mon Sep 17 00:00:00 2001 From: Izan Date: Wed, 14 Sep 2022 21:27:04 +0100 Subject: [PATCH 3/5] Use double backquotes in docstring. --- botcore/utils/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/botcore/utils/commands.py b/botcore/utils/commands.py index 57da19286..973cbe69d 100644 --- a/botcore/utils/commands.py +++ b/botcore/utils/commands.py @@ -48,15 +48,15 @@ async def check_rerun_job(ctx: Context, response: Message) -> Optional[str]: """ Check if the job should be rerun. - For a job to be rerun, the user must edit their message within `REDO_TIMEOUT` seconds, - and then react with the `REDO_EMOJI` within 10 seconds. + For a job to be rerun, the user must edit their message within ``REDO_TIMEOUT`` seconds, + and then react with the ``REDO_EMOJI`` within 10 seconds. Args: ctx: The command's context response: The job's response message Returns: - The content to be rerun, or `None`. + The content to be rerun, or ``None``. """ # Correct message and content did actually change (i.e. wasn't a pin status udpate etc.) _message_edit_predicate = lambda old, new: new.id == ctx.message.id and new.content != old.content From 9461db3e05b44cdd4e3d1fe0527402b7b9617886 Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 23 Sep 2022 22:03:04 +0100 Subject: [PATCH 4/5] Hard rewrite `check_rerun_job` util to remove a bug, and rename to `check_rerun_command` to better reflect purpose. - The util now automatically re-invokes the command when applicable, and sends an error message otherwise. Rest of core functionality remains the same. --- botcore/utils/commands.py | 84 ++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/botcore/utils/commands.py b/botcore/utils/commands.py index 973cbe69d..a99f87e66 100644 --- a/botcore/utils/commands.py +++ b/botcore/utils/commands.py @@ -1,5 +1,6 @@ from asyncio import TimeoutError from contextlib import suppress +from itertools import zip_longest from typing import Optional from discord import HTTPException, Message, NotFound @@ -44,19 +45,16 @@ async def clean_text_or_reply(ctx: Context, text: Optional[str] = None) -> str: raise BadArgument("Couldn't find text to clean. Provide a string or reply to a message to use its content.") -async def check_rerun_job(ctx: Context, response: Message) -> Optional[str]: +async def check_rerun_command(ctx: Context, response: Message) -> None: """ - Check if the job should be rerun. + Check if the command should be rerun (and reruns if should be). - For a job to be rerun, the user must edit their message within ``REDO_TIMEOUT`` seconds, - and then react with the ``REDO_EMOJI`` within 10 seconds. + For a command to be rerun, the user must edit their invocation message within + ``REDO_TIMEOUT`` seconds, and then react with the ``REDO_EMOJI`` within 10 seconds. Args: ctx: The command's context response: The job's response message - - Returns: - The content to be rerun, or ``None``. """ # Correct message and content did actually change (i.e. wasn't a pin status udpate etc.) _message_edit_predicate = lambda old, new: new.id == ctx.message.id and new.content != old.content @@ -89,10 +87,76 @@ async def check_rerun_job(ctx: Context, response: Message) -> Optional[str]: except TimeoutError: # One of the `wait_for` timed out, so abort since user doesn't want to rerun await ctx.message.clear_reaction(REDO_EMOJI) - return None + return else: # Both `wait_for` triggered, so return the new content to be run since user wants to rerun - return new_message.content - return None # triggered whenever a `NotFound` was raised + # Determine if the edited message starts with a valid prefix, and if it does store it + prefix_or_prefixes = await ctx.bot.get_prefix(ctx.message) + active_prefix = None + if isinstance(prefix_or_prefixes, list): + # Bot is listening to multiple prefixes + for prefix in prefix_or_prefixes: + if ctx.message.content.startswith(prefix): + active_prefix = prefix + break + else: + await ctx.reply(":warning: Stopped listening because you removed the prefix.") + return False + else: + # Bot is only listening to one prefix + if not new_message.content.startswith(prefix_or_prefixes): + await ctx.reply(":warning: Stopped listening because you removed the prefix.") + return + active_prefix = prefix_or_prefixes + + # The edited content has a valid prefix, so remove it + content = new_message.content[len(active_prefix):] + + # Return whether the command of the new content is the same as `ctx.command`. + content_split = content.split() + accu = [] + matches = False + for cmd_or_arg, parent in zip_longest(content_split, ctx.command.parents + [ctx.command]): + if cmd_or_arg is None: + # `cmd_or_arg` will only ever be `None` due to `zip_longest` filling the value. + # This means that `content_split` is shorter than parents+command, and thus + # cannot be the same command (has to be missing at least one level of commands) + matches = False + break + + accu.append(cmd_or_arg) + curr_comm = ctx.bot.get_command(' '.join(accu)) + + if not curr_comm: + continue + + if parent is None: + # `parent` will only ever be `None` due to `zip_longest` filling the value. + + if curr_comm.qualified_name.endswith(cmd_or_arg): + # `cmd_or_arg` is a command (not an arg), which + # means it's a subcommand of `ctx.command` so not same + matches = False + else: + # `cmd_or_arg` is an arg (not a command), which + # means `curr_comm` is as deep as the command goes + matches = curr_comm.qualified_name == ctx.command.qualified_name + break + + if not curr_comm.qualified_name == parent.qualified_name: + # Command doesn't match, but there may be a valid subcommand + continue + + if curr_comm.qualified_name == ctx.command.qualified_name: + # Currently matches, but we need to ensure that `content_split` doesn't turn into a subcommand + matches = True + continue + + matches = False + + if matches: + await ctx.bot.invoke(await ctx.bot.get_context(new_message)) + else: + await ctx.reply(":warning: You changed the command, so no longer listening for edits.") From c7eb65eae31819f7ada6b57851896673729614c0 Mon Sep 17 00:00:00 2001 From: Izan Date: Sat, 24 Sep 2022 00:04:37 +0100 Subject: [PATCH 5/5] Fix date in changelog.rst --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index aa97d1ff7..9c9f3747f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog ========= -- :release:`8.3.0 <9th September 2922>` +- :release:`8.3.0 <9th September 2022>` - :feature:`137` Add a utility to check for a job rerun