From 3b11a675c273442935c07f7a95ff6c0a747ebb6a Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 26 Oct 2021 15:44:16 +0530 Subject: [PATCH 1/6] Add the mandatory_close_duration plugin --- .../mandatory_close_duration.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 mandatory_close_duration/mandatory_close_duration.py diff --git a/mandatory_close_duration/mandatory_close_duration.py b/mandatory_close_duration/mandatory_close_duration.py new file mode 100644 index 0000000..09221c4 --- /dev/null +++ b/mandatory_close_duration/mandatory_close_duration.py @@ -0,0 +1,67 @@ +from discord.ext import commands + +from bot import ModmailBot +from core import time + + +class StrictUserFriendlyDuration(time.UserFriendlyTime): + """ + A converter which parses user-friendly time durations. + + Since this converter is meant for parsing close messages while + closing threads, both custom close messages and time durations are + parsed. + + Unlike the parent class, a time duration must be provided when + a custom close message is provided. + """ + + MODIFIERS = {'silently', 'silent', 'cancel'} + + async def convert(self, ctx: commands.Context, argument: str) -> "StrictUserFriendlyDuration": + """ + Parse the provided time duration along with any close message. + + Fail if a custom close message is provided without a time + duration. + """ + await super().convert(ctx, argument) + + argument_passed = bool(argument) + not_a_modifier = argument not in self.MODIFIERS + if argument_passed and not_a_modifier and self.arg == argument: + # Fail since only a close message was provided. + raise commands.BadArgument("A time duration must be provided when closing with a custom message.") + + return self + + +ADDED_HELP_TEXT = '\n\n*Note: Providing a time duration is necessary when closing with a custom message.*' + + +def setup(bot: ModmailBot) -> None: + """ + Monkey patch the close command's callback. + + This makes it use the StrictUserFriendlyTime converter and updates + the help text to reflect the new behaviour. + """ + global previous_converter + + command = bot.get_command('close') + + previous_converter = command.callback.__annotations__['after'] + command.callback.__annotations__['after'] = StrictUserFriendlyDuration + command.callback = command.callback + + command.help += ADDED_HELP_TEXT + + +def teardown(bot: ModmailBot) -> None: + """Undo changes to the close command.""" + command = bot.get_command('close') + + command.callback.__annotations__['after'] = previous_converter + command.callback = command.callback + + command.help = command.help.remove_suffix(ADDED_HELP_TEXT) From 4ee3d51e3612ffb1b3a9cd8c32a879da5b5318e1 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 26 Oct 2021 15:45:26 +0530 Subject: [PATCH 2/6] Add the plugin's info in the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4a32866..a09eb16 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Modmail plugins for our Modmail bot, located at https://github.com/kyb3r/modmail ## List of plugins - **Ban appeals**: Threads created by users in a defined "appeal" guild get moved to a configured appeal category. This also both kicks users from the appeal guild when they rejoin the main guild, and kicks users from the appeal guild if they're not banned in the main guild. - **Close message:** Add a `?closemessage` command that will close the thread after 15 minutes with a default message. +- **Mandatory Close Duration** Makes the `?close` command require a time duration when closing with a custom close message. - **MDLink**: Generate a ready to paste link to the thread logs. - **Ping manager**: Delay pings by a configurable time period, cancel the ping task if a message is sent in the thread (Other than an author message). - **Reply cooldown**: Forbid you from sending the same message twice in ten seconds. From 9cb0aa6d6087c17ac8a8ebdef1ac0506465af11f Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 2 Nov 2021 12:31:35 +0530 Subject: [PATCH 3/6] Ask for confirmation if a duration isn't provided --- .../mandatory_close_duration.py | 103 ++++++++++-------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/mandatory_close_duration/mandatory_close_duration.py b/mandatory_close_duration/mandatory_close_duration.py index 09221c4..c53ec36 100644 --- a/mandatory_close_duration/mandatory_close_duration.py +++ b/mandatory_close_duration/mandatory_close_duration.py @@ -1,67 +1,84 @@ +import asyncio + +import discord from discord.ext import commands from bot import ModmailBot from core import time -class StrictUserFriendlyDuration(time.UserFriendlyTime): +async def close_after_confirmation(ctx: commands.Context, converted_arg: time.UserFriendlyTime) -> None: """ - A converter which parses user-friendly time durations. - - Since this converter is meant for parsing close messages while - closing threads, both custom close messages and time durations are - parsed. + Send a message and allow users to react to it to close the thread. - Unlike the parent class, a time duration must be provided when - a custom close message is provided. + The reaction times out after 5 minutes. """ + unicode_reaction = '\N{WHITE HEAVY CHECK MARK}' + warning_message = ("\N{WARNING SIGN} A time duration wasn't provided, reacting to this message will close" + " this thread instantly with the provided custom close message.") + + message = await ctx.send(warning_message) + await message.add_reaction(unicode_reaction) + + def checkmark_press_check(reaction: discord.Reaction, user: discord.User) -> bool: + is_right_reaction = ( + user != ctx.bot.user + and reaction.message.id == message.id + and str(reaction.emoji) == unicode_reaction + ) + + return is_right_reaction + + try: + await ctx.bot.wait_for('reaction_add', check=checkmark_press_check, timeout=5 * 60) + except asyncio.TimeoutError: + await message.edit(content=message.content+'\n\n**Timed out.**') + await message.clear_reactions() + else: + await original_close_command(ctx, after=converted_arg) + + +async def safe_close( + self: time.UserFriendlyTime, + ctx: commands.Context, + *, + after: time.UserFriendlyTime = None +) -> None: + """ + Close the current thread. - MODIFIERS = {'silently', 'silent', 'cancel'} - - async def convert(self, ctx: commands.Context, argument: str) -> "StrictUserFriendlyDuration": - """ - Parse the provided time duration along with any close message. - - Fail if a custom close message is provided without a time - duration. - """ - await super().convert(ctx, argument) - - argument_passed = bool(argument) - not_a_modifier = argument not in self.MODIFIERS - if argument_passed and not_a_modifier and self.arg == argument: - # Fail since only a close message was provided. - raise commands.BadArgument("A time duration must be provided when closing with a custom message.") - - return self + Unlike the original close command, confirmation is awaited when + a time duration isn't provided but a custom close message is. + """ + modifiers = {'silently', 'silent', 'cancel'} + argument_passed = bool(after) + not_a_modifier = after.arg not in modifiers -ADDED_HELP_TEXT = '\n\n*Note: Providing a time duration is necessary when closing with a custom message.*' + if argument_passed and not_a_modifier and after.arg == after.raw: + # Ask for confirmation since only a close message was provided. + await close_after_confirmation(ctx, after) + else: + await original_close_command(ctx, after=after) def setup(bot: ModmailBot) -> None: """ - Monkey patch the close command's callback. + Monkey patch the close command's callback to safe_close. - This makes it use the StrictUserFriendlyTime converter and updates - the help text to reflect the new behaviour. + The help text is also updated to reflect the new behaviour. """ - global previous_converter + global original_close_command command = bot.get_command('close') + original_close_command = command.copy() + original_close_command.cog = command.cog - previous_converter = command.callback.__annotations__['after'] - command.callback.__annotations__['after'] = StrictUserFriendlyDuration - command.callback = command.callback - - command.help += ADDED_HELP_TEXT + command.callback = safe_close + command.help += '\n\n*Note: A time duration should be provided when closing with a custom message.*' def teardown(bot: ModmailBot) -> None: - """Undo changes to the close command.""" - command = bot.get_command('close') - - command.callback.__annotations__['after'] = previous_converter - command.callback = command.callback - - command.help = command.help.remove_suffix(ADDED_HELP_TEXT) + """Restore the original close command.""" + bot.remove_command('close') + bot.add_command(original_close_command) From d63e70912d89644511f2a05f9d11b96a49346678 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Thu, 4 Nov 2021 09:36:04 +0530 Subject: [PATCH 4/6] Remove certain phrases before comparing arguments --- .../mandatory_close_duration.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/mandatory_close_duration/mandatory_close_duration.py b/mandatory_close_duration/mandatory_close_duration.py index c53ec36..30b8064 100644 --- a/mandatory_close_duration/mandatory_close_duration.py +++ b/mandatory_close_duration/mandatory_close_duration.py @@ -52,10 +52,20 @@ async def safe_close( """ modifiers = {'silently', 'silent', 'cancel'} - argument_passed = bool(after) - not_a_modifier = after.arg not in modifiers + argument_passed = after is not None - if argument_passed and not_a_modifier and after.arg == after.raw: + if argument_passed: + not_a_modifier = after.arg not in modifiers + + # These changes are always made to the argument by the super + # class so they need to be replicated before the raw argument + # is compared with the parsed message. + stripped_argument = after.raw.strip() + argument_without_phrases = stripped_argument.removeprefix('in ').removesuffix(' from now') + + no_duration = after.arg == argument_without_phrases + + if argument_passed and not_a_modifier and no_duration: # Ask for confirmation since only a close message was provided. await close_after_confirmation(ctx, after) else: From f3868c4c8528a6b8078e65858ce14866752624a8 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Thu, 4 Nov 2021 09:45:09 +0530 Subject: [PATCH 5/6] Handle thread deletion while awaiting confirmation --- mandatory_close_duration/mandatory_close_duration.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mandatory_close_duration/mandatory_close_duration.py b/mandatory_close_duration/mandatory_close_duration.py index 30b8064..4768d36 100644 --- a/mandatory_close_duration/mandatory_close_duration.py +++ b/mandatory_close_duration/mandatory_close_duration.py @@ -32,8 +32,12 @@ def checkmark_press_check(reaction: discord.Reaction, user: discord.User) -> boo try: await ctx.bot.wait_for('reaction_add', check=checkmark_press_check, timeout=5 * 60) except asyncio.TimeoutError: - await message.edit(content=message.content+'\n\n**Timed out.**') - await message.clear_reactions() + try: + await message.edit(content=message.content+'\n\n**Timed out.**') + await message.clear_reactions() + except discord.NotFound: + # The thread might have been closed by now. + pass else: await original_close_command(ctx, after=converted_arg) From c5f07d437d88de56018c357ad7b3e4604b1c1b08 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Thu, 4 Nov 2021 09:58:10 +0530 Subject: [PATCH 6/6] Update the plugin's description. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a09eb16..d70d260 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Modmail plugins for our Modmail bot, located at https://github.com/kyb3r/modmail ## List of plugins - **Ban appeals**: Threads created by users in a defined "appeal" guild get moved to a configured appeal category. This also both kicks users from the appeal guild when they rejoin the main guild, and kicks users from the appeal guild if they're not banned in the main guild. - **Close message:** Add a `?closemessage` command that will close the thread after 15 minutes with a default message. -- **Mandatory Close Duration** Makes the `?close` command require a time duration when closing with a custom close message. +- **Mandatory Close Duration** Makes the `?close` command require confirmation if a time duration isn't provided when closing with a custom close message. - **MDLink**: Generate a ready to paste link to the thread logs. - **Ping manager**: Delay pings by a configurable time period, cancel the ping task if a message is sent in the thread (Other than an author message). - **Reply cooldown**: Forbid you from sending the same message twice in ten seconds.