Repository: Cog-Creators/Red-DiscordBot

Issue:
[CustomCommands] Cooldown for custom commands
# Feature request

#### Select the type of feature you are requesting:

- [ ] Cog
- [x] Command
- [ ] API functionality

#### Describe your requested feature

In customcog cog add the possibility to mention a cooldown for the custom command 

`[p]customcom add commandyouwant Hello World -t 3600`

3600 = time in seconds


Discussion:
Hi! I think we could probably include this in 3.1. I see in your example you've included unix-style arg parsing, unfortunately we don't really support that in core at the moment. I would instead suggest a new command:
```
[p]customcom setcooldown <command> <seconds>
```
This prevents polluting `[p]customcom add`'s arguments but is still straightforward to use.
Nice thank you :)
Sounds fun and I'm bored. I'll look into it.

Resolving patch:
diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py
--- a/redbot/cogs/customcom/customcom.py
+++ b/redbot/cogs/customcom/customcom.py
@@ -1,7 +1,7 @@
 import os
 import re
 import random
-from datetime import datetime
+from datetime import datetime, timedelta
 from inspect import Parameter
 from collections import OrderedDict
 from typing import Mapping
@@ -19,15 +19,19 @@ class CCError(Exception):
     pass
 
 
-class NotFound(CCError):
+class AlreadyExists(CCError):
     pass
 
 
-class AlreadyExists(CCError):
+class ArgParseError(CCError):
     pass
 
 
-class ArgParseError(CCError):
+class NotFound(CCError):
+    pass
+
+
+class OnCooldown(CCError):
     pass
 
 
@@ -88,9 +92,9 @@ async def get(self, message: discord.Message, command: str) -> str:
         if not ccinfo:
             raise NotFound()
         else:
-            return ccinfo["response"]
+            return ccinfo["response"], ccinfo.get("cooldowns", {})
 
-    async def create(self, ctx: commands.Context, command: str, response):
+    async def create(self, ctx: commands.Context, command: str, *, response):
         """Create a custom command"""
         # Check if this command is already registered as a customcommand
         if await self.db(ctx.guild).commands.get_raw(command, default=None):
@@ -101,25 +105,35 @@ async def create(self, ctx: commands.Context, command: str, response):
         ccinfo = {
             "author": {"id": author.id, "name": author.name},
             "command": command,
+            "cooldowns": {},
             "created_at": self.get_now(),
             "editors": [],
             "response": response,
         }
         await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
 
-    async def edit(self, ctx: commands.Context, command: str, response: None):
+    async def edit(
+        self,
+        ctx: commands.Context,
+        command: str,
+        *,
+        response=None,
+        cooldowns: Mapping[str, int] = None,
+        ask_for: bool = True
+    ):
         """Edit an already existing custom command"""
+        ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
+
         # Check if this command is registered
-        if not await self.db(ctx.guild).commands.get_raw(command, default=None):
+        if not ccinfo:
             raise NotFound()
 
         author = ctx.message.author
-        ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None)
 
         def check(m):
-            return m.channel == ctx.channel and m.author == ctx.message.author
+            return m.channel == ctx.channel and m.author == ctx.author
 
-        if not response:
+        if ask_for and not response:
             await ctx.send(_("Do you want to create a 'randomized' cc? {}").format("y/n"))
 
             msg = await self.bot.wait_for("message", check=check)
@@ -129,17 +143,24 @@ def check(m):
                 await ctx.send(_("What response do you want?"))
                 response = (await self.bot.wait_for("message", check=check)).content
 
-        # test to raise
-        ctx.cog.prepare_args(response if isinstance(response, str) else response[0])
+        if response:
+            # test to raise
+            ctx.cog.prepare_args(response if isinstance(response, str) else response[0])
+            ccinfo["response"] = response
 
-        ccinfo["response"] = response
-        ccinfo["edited_at"] = self.get_now()
+        if cooldowns:
+            ccinfo.setdefault("cooldowns", {}).update(cooldowns)
+            for key, value in ccinfo["cooldowns"].copy().items():
+                if value <= 0:
+                    del ccinfo["cooldowns"][key]
 
         if author.id not in ccinfo["editors"]:
             # Add the person who invoked the `edit` coroutine to the list of
             # editors, if the person is not yet in there
             ccinfo["editors"].append(author.id)
 
+        ccinfo["edited_at"] = self.get_now()
+
         await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
 
     async def delete(self, ctx: commands.Context, command: str):
@@ -162,6 +183,7 @@ def __init__(self, bot):
         self.config = Config.get_conf(self, self.key)
         self.config.register_guild(commands={})
         self.commandobj = CommandObj(config=self.config, bot=self.bot)
+        self.cooldowns = {}
 
     @commands.group(aliases=["cc"])
     @commands.guild_only()
@@ -182,7 +204,7 @@ async def cc_add(self, ctx: commands.Context):
 
     @cc_add.command(name="random")
     @checks.mod_or_permissions(administrator=True)
-    async def cc_add_random(self, ctx: commands.Context, command: str):
+    async def cc_add_random(self, ctx: commands.Context, command: str.lower):
         """
         Create a CC where it will randomly choose a response!
 
@@ -205,13 +227,12 @@ async def cc_add_random(self, ctx: commands.Context, command: str):
 
     @cc_add.command(name="simple")
     @checks.mod_or_permissions(administrator=True)
-    async def cc_add_simple(self, ctx, command: str, *, text):
+    async def cc_add_simple(self, ctx, command: str.lower, *, text: str):
         """Adds a simple custom command
 
         Example:
         [p]customcom add simple yourcommand Text you want
         """
-        command = command.lower()
         if command in self.bot.all_commands:
             await ctx.send(_("That command is already a standard command."))
             return
@@ -227,41 +248,81 @@ async def cc_add_simple(self, ctx, command: str, *, text):
         except ArgParseError as e:
             await ctx.send(e.args[0])
 
-    @customcom.command(name="edit")
+    @customcom.command(name="cooldown")
     @checks.mod_or_permissions(administrator=True)
-    async def cc_edit(self, ctx, command: str, *, text=None):
-        """Edits a custom command
+    async def cc_cooldown(
+        self, ctx, command: str.lower, cooldown: int = None, *, per: str.lower = "member"
+    ):
+        """
+        Sets, edits, or views cooldowns for a custom command
 
+        You may set cooldowns per member, channel, or guild.
+        Multiple cooldowns may be set. All cooldowns must be cooled to call the custom command.
         Example:
-        [p]customcom edit yourcommand Text you want
+        [p]customcom cooldown yourcommand 30
         """
-        command = command.lower()
-
+        if cooldown is None:
+            try:
+                cooldowns = (await self.commandobj.get(ctx.message, command))[1]
+            except NotFound:
+                return await ctx.send(_("That command doesn't exist."))
+            if cooldowns:
+                cooldown = []
+                for per, rate in cooldowns.items():
+                    cooldown.append(
+                        _("A {} may call this command every {} seconds").format(per, rate)
+                    )
+                return await ctx.send("\n".join(cooldown))
+            else:
+                return await ctx.send(_("This command has no cooldown."))
+        per = {"server": "guild", "user": "member"}.get(per, per)
+        allowed = ("guild", "member", "channel")
+        if per not in allowed:
+            return await ctx.send(_("{} must be one of {}").format("per", ", ".join(allowed)))
+        cooldown = {per: cooldown}
         try:
-            await self.commandobj.edit(ctx=ctx, command=command, response=text)
-            await ctx.send(_("Custom command successfully edited."))
+            await self.commandobj.edit(ctx=ctx, command=command, cooldowns=cooldown, ask_for=False)
+            await ctx.send(_("Custom command cooldown successfully edited."))
         except NotFound:
             await ctx.send(
                 _("That command doesn't exist. Use `{}` to add it.").format(
                     "{}customcom add".format(ctx.prefix)
                 )
             )
-        except ArgParseError as e:
-            await ctx.send(e.args[0])
 
     @customcom.command(name="delete")
     @checks.mod_or_permissions(administrator=True)
-    async def cc_delete(self, ctx, command: str):
+    async def cc_delete(self, ctx, command: str.lower):
         """Deletes a custom command
+
         Example:
         [p]customcom delete yourcommand"""
-        command = command.lower()
         try:
             await self.commandobj.delete(ctx=ctx, command=command)
             await ctx.send(_("Custom command successfully deleted."))
         except NotFound:
             await ctx.send(_("That command doesn't exist."))
 
+    @customcom.command(name="edit")
+    @checks.mod_or_permissions(administrator=True)
+    async def cc_edit(self, ctx, command: str.lower, *, text: str = None):
+        """Edits a custom command's response
+
+        Example:
+        [p]customcom edit yourcommand Text you want
+        """
+        try:
+            await self.commandobj.edit(ctx=ctx, command=command, response=text)
+            await ctx.send(_("Custom command successfully edited."))
+        except NotFound:
+            await ctx.send(
+                _("That command doesn't exist. Use `{}` to add it.").format(
+                    "{}customcom add".format(ctx.prefix)
+                )
+            )
+        except ArgParseError as e:
+            await ctx.send(e.args[0])
+
     @customcom.command(name="list")
     async def cc_list(self, ctx):
         """Shows custom commands list"""
@@ -302,8 +363,8 @@ async def on_message(self, message):
 
         # user_allowed check, will be replaced with self.bot.user_allowed or
         # something similar once it's added
-
         user_allowed = True
+
         if len(message.content) < 2 or is_private or not user_allowed or message.author.bot:
             return
 
@@ -313,22 +374,25 @@ async def on_message(self, message):
             return
 
         try:
-            raw_response = await self.commandobj.get(message=message, command=ctx.invoked_with)
+            raw_response, cooldowns = await self.commandobj.get(
+                message=message, command=ctx.invoked_with
+            )
             if isinstance(raw_response, list):
                 raw_response = random.choice(raw_response)
             elif isinstance(raw_response, str):
                 pass
             else:
                 raise NotFound()
-        except NotFound:
+            if cooldowns:
+                self.test_cooldowns(ctx, ctx.invoked_with, cooldowns)
+        except CCError:
             return
-        await self.call_cc_command(ctx, raw_response, message)
 
-    async def call_cc_command(self, ctx, raw_response, message) -> None:
         # wrap the command here so it won't register with the bot
         fake_cc = commands.Command(ctx.invoked_with, self.cc_callback)
         fake_cc.params = self.prepare_args(raw_response)
         ctx.command = fake_cc
+
         await self.bot.invoke(ctx)
         if not ctx.command_failed:
             await self.cc_command(*ctx.args, **ctx.kwargs, raw_response=raw_response)
@@ -429,6 +493,25 @@ def prepare_args(self, raw_response) -> Mapping[str, Parameter]:
         fin = default + [(p.name, p) for p in fin]
         return OrderedDict(fin)
 
+    def test_cooldowns(self, ctx, command, cooldowns):
+        now = datetime.utcnow()
+        new_cooldowns = {}
+        for per, rate in cooldowns.items():
+            if per == "guild":
+                key = (command, ctx.guild)
+            elif per == "channel":
+                key = (command, ctx.guild, ctx.channel)
+            elif per == "member":
+                key = (command, ctx.guild, ctx.author)
+            cooldown = self.cooldowns.get(key)
+            if cooldown:
+                cooldown += timedelta(seconds=rate)
+                if cooldown > now:
+                    raise OnCooldown()
+            new_cooldowns[key] = now
+        # only update cooldowns if the command isn't on cooldown
+        self.cooldowns.update(new_cooldowns)
+
     def transform_arg(self, result, attr, obj) -> str:
         attr = attr[1:]  # strip initial dot
         if not attr:


Test patch: