Repository: python-discord/bot

Issue:
!mention command
Currently, all the roles in the community are unmentionable. This is in order to prevent spam. Before we made this change, people would occasionally come into the community and just mentionspam all the role mentions.

The problem with this is that we sometimes need to mention a role. For example, we mention the `@Announcements` role every time we make an announcement. We all desperately wish Discord would allow us to set mention permissions so that only certain roles were allowed to mention other roles, but this is not yet possible.

Currently the manual process looks like this:
1. We set the role to be mentionable by everyone
2. We ping the role.
3. We wait 5-10 seconds
4. We set the role to be unmentionable.

The reason we need to do step 3 is that if you set the role to unmentionable too soon after you do the ping, Discord has a tendency to mess up the mention. Sometimes it will not ping everyone, other times there will be a ghost ping.

We'd like to automate the above process, so we're looking for someone to make a `!mention` command. Here's what we'd like to see:
* Only Moderators, Admins and Owners should be able to use it.
* It takes a role ID or a string as input argument
* The string should not need to be in quotes, even if it has spaces.
* When called, the bot should unlock the role for mention and let the caller know that the role has been unlocked.
* When a role mention for this role is detected in any channel, wait 5 seconds (make this duration configurable), lock the role again and let the caller know that it has been locked in the channel where the unlock was invoked.
* After 5 minutes (this should be configurable), if no role mention has been detected, the unlock should time out and the role should be locked again. Remember here also to let the caller know that it has been locked in the channel where the unlock was invoked.
* If invoked on a role that's already mentionable (like `@Admins`), return an error message
* If invoked on a special role (like `@everyone`), return an error.


Discussion:
Sure, I can work on this. Which cog would be appropriate? I'm not seeing one jump out except maybe cogs.moderation
Sure thing, I'll assign it to you @mathsman5133. Utils cog is fine I think, it should suit.
Thanks. When you say " (make this duration configurable)" are we talking can be changed via command or just in constants.py - does `MentionCommandTimeout` suit? Also, should @Moderators be able to enable @Owners mention?
`config.yml` would be where that would need to be set, and the config name style and structure should match the style of the other configuration options around it.
Regarding the invoker role vs specific role mentions, I think it's best to keep that simple and not worry about it. Just have the command itself limited to Mods, Admins and Owners, and have the command check that the role isn't already mentionable. 

Resolving patch:
diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -1,15 +1,18 @@
 import logging
 import re
 import unicodedata
+from asyncio import TimeoutError, sleep
 from email.parser import HeaderParser
 from io import StringIO
 from typing import Tuple
 
-from discord import Colour, Embed
+from dateutil import relativedelta
+from discord import Colour, Embed, Message, Role
 from discord.ext.commands import Bot, Cog, Context, command
 
-from bot.constants import Channels, STAFF_ROLES
-from bot.decorators import in_channel
+from bot.constants import Channels, MODERATION_ROLES, Mention, STAFF_ROLES
+from bot.decorators import in_channel, with_role
+from bot.utils.time import humanize_delta
 
 log = logging.getLogger(__name__)
 
@@ -130,6 +133,47 @@ def get_info(char: str) -> Tuple[str, str]:
 
         await ctx.send(embed=embed)
 
+    @command()
+    @with_role(*MODERATION_ROLES)
+    async def mention(self, ctx: Context, *, role: Role) -> None:
+        """Set a role to be mentionable for a limited time."""
+        if role.mentionable:
+            await ctx.send(f"{role} is already mentionable!")
+            return
+
+        await role.edit(reason=f"Role unlocked by {ctx.author}", mentionable=True)
+
+        human_time = humanize_delta(relativedelta.relativedelta(seconds=Mention.message_timeout))
+        await ctx.send(
+            f"{role} has been made mentionable. I will reset it in {human_time}, or when someone mentions this role."
+        )
+
+        def check(m: Message) -> bool:
+            """Checks that the message contains the role mention."""
+            return role in m.role_mentions
+
+        try:
+            msg = await self.bot.wait_for("message", check=check, timeout=Mention.message_timeout)
+        except TimeoutError:
+            await role.edit(mentionable=False, reason="Automatic role lock - timeout.")
+            await ctx.send(f"{ctx.author.mention}, you took too long. I have reset {role} to be unmentionable.")
+            return
+
+        if any(r.id in MODERATION_ROLES for r in msg.author.roles):
+            await sleep(Mention.reset_delay)
+            await role.edit(mentionable=False, reason=f"Automatic role lock by {msg.author}")
+            await ctx.send(
+                f"{ctx.author.mention}, I have reset {role} to be unmentionable as "
+                f"{msg.author if msg.author != ctx.author else 'you'} sent a message mentioning it."
+            )
+            return
+
+        await role.edit(mentionable=False, reason=f"Automatic role lock - unauthorised use by {msg.author}")
+        await ctx.send(
+            f"{ctx.author.mention}, I have reset {role} to be unmentionable "
+            f"as I detected unauthorised use by {msg.author} (ID: {msg.author.id})."
+        )
+
 
 def setup(bot: Bot) -> None:
     """Utils cog load."""
diff --git a/bot/constants.py b/bot/constants.py
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -475,6 +475,13 @@ class Free(metaclass=YAMLGetter):
     cooldown_per: float
 
 
+class Mention(metaclass=YAMLGetter):
+    section = 'mention'
+
+    message_timeout: int
+    reset_delay: int
+
+
 class RedirectOutput(metaclass=YAMLGetter):
     section = 'redirect_output'
 


Test patch: