We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Command suggester wrongly invoked for commands under cooldown
When a command is under cooldown and is invoked again, the error handler will wrongly try to suggest an alternative as if the command does not exist, when it should not do anything at all.

![image](https://user-images.githubusercontent.com/18114431/105258400-5dae7800-5b92-11eb-9841-c07b06766644.png)


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bot/exts/info/tags.py`
Content:
```
1 import logging
2 import re
3 import time
4 from pathlib import Path
5 from typing import Callable, Dict, Iterable, List, Optional
6 
7 from discord import Colour, Embed, Member
8 from discord.ext.commands import Cog, Context, group
9 
10 from bot import constants
11 from bot.bot import Bot
12 from bot.converters import TagNameConverter
13 from bot.pagination import LinePaginator
14 from bot.utils.messages import wait_for_deletion
15 
16 log = logging.getLogger(__name__)
17 
18 TEST_CHANNELS = (
19     constants.Channels.bot_commands,
20     constants.Channels.helpers
21 )
22 
23 REGEX_NON_ALPHABET = re.compile(r"[^a-z]", re.MULTILINE & re.IGNORECASE)
24 FOOTER_TEXT = f"To show a tag, type {constants.Bot.prefix}tags <tagname>."
25 
26 
27 class Tags(Cog):
28     """Save new tags and fetch existing tags."""
29 
30     def __init__(self, bot: Bot):
31         self.bot = bot
32         self.tag_cooldowns = {}
33         self._cache = self.get_tags()
34 
35     @staticmethod
36     def get_tags() -> dict:
37         """Get all tags."""
38         cache = {}
39 
40         base_path = Path("bot", "resources", "tags")
41         for file in base_path.glob("**/*"):
42             if file.is_file():
43                 tag_title = file.stem
44                 tag = {
45                     "title": tag_title,
46                     "embed": {
47                         "description": file.read_text(encoding="utf8"),
48                     },
49                     "restricted_to": None,
50                     "location": f"/bot/{file}"
51                 }
52 
53                 # Convert to a list to allow negative indexing.
54                 parents = list(file.relative_to(base_path).parents)
55                 if len(parents) > 1:
56                     # -1 would be '.' hence -2 is used as the index.
57                     tag["restricted_to"] = parents[-2].name
58 
59                 cache[tag_title] = tag
60 
61         return cache
62 
63     @staticmethod
64     def check_accessibility(user: Member, tag: dict) -> bool:
65         """Check if user can access a tag."""
66         return not tag["restricted_to"] or tag["restricted_to"].lower() in [role.name.lower() for role in user.roles]
67 
68     @staticmethod
69     def _fuzzy_search(search: str, target: str) -> float:
70         """A simple scoring algorithm based on how many letters are found / total, with order in mind."""
71         current, index = 0, 0
72         _search = REGEX_NON_ALPHABET.sub('', search.lower())
73         _targets = iter(REGEX_NON_ALPHABET.split(target.lower()))
74         _target = next(_targets)
75         try:
76             while True:
77                 while index < len(_target) and _search[current] == _target[index]:
78                     current += 1
79                     index += 1
80                 index, _target = 0, next(_targets)
81         except (StopIteration, IndexError):
82             pass
83         return current / len(_search) * 100
84 
85     def _get_suggestions(self, tag_name: str, thresholds: Optional[List[int]] = None) -> List[str]:
86         """Return a list of suggested tags."""
87         scores: Dict[str, int] = {
88             tag_title: Tags._fuzzy_search(tag_name, tag['title'])
89             for tag_title, tag in self._cache.items()
90         }
91 
92         thresholds = thresholds or [100, 90, 80, 70, 60]
93 
94         for threshold in thresholds:
95             suggestions = [
96                 self._cache[tag_title]
97                 for tag_title, matching_score in scores.items()
98                 if matching_score >= threshold
99             ]
100             if suggestions:
101                 return suggestions
102 
103         return []
104 
105     def _get_tag(self, tag_name: str) -> list:
106         """Get a specific tag."""
107         found = [self._cache.get(tag_name.lower(), None)]
108         if not found[0]:
109             return self._get_suggestions(tag_name)
110         return found
111 
112     def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str, user: Member) -> list:
113         """
114         Search for tags via contents.
115 
116         `predicate` will be the built-in any, all, or a custom callable. Must return a bool.
117         """
118         keywords_processed: List[str] = []
119         for keyword in keywords.split(','):
120             keyword_sanitized = keyword.strip().casefold()
121             if not keyword_sanitized:
122                 # this happens when there are leading / trailing / consecutive comma.
123                 continue
124             keywords_processed.append(keyword_sanitized)
125 
126         if not keywords_processed:
127             # after sanitizing, we can end up with an empty list, for example when keywords is ','
128             # in that case, we simply want to search for such keywords directly instead.
129             keywords_processed = [keywords]
130 
131         matching_tags = []
132         for tag in self._cache.values():
133             matches = (query in tag['embed']['description'].casefold() for query in keywords_processed)
134             if self.check_accessibility(user, tag) and check(matches):
135                 matching_tags.append(tag)
136 
137         return matching_tags
138 
139     async def _send_matching_tags(self, ctx: Context, keywords: str, matching_tags: list) -> None:
140         """Send the result of matching tags to user."""
141         if not matching_tags:
142             pass
143         elif len(matching_tags) == 1:
144             await ctx.send(embed=Embed().from_dict(matching_tags[0]['embed']))
145         else:
146             is_plural = keywords.strip().count(' ') > 0 or keywords.strip().count(',') > 0
147             embed = Embed(
148                 title=f"Here are the tags containing the given keyword{'s' * is_plural}:",
149                 description='\n'.join(tag['title'] for tag in matching_tags[:10])
150             )
151             await LinePaginator.paginate(
152                 sorted(f"**»**   {tag['title']}" for tag in matching_tags),
153                 ctx,
154                 embed,
155                 footer_text=FOOTER_TEXT,
156                 empty=False,
157                 max_lines=15
158             )
159 
160     @group(name='tags', aliases=('tag', 't'), invoke_without_command=True)
161     async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:
162         """Show all known tags, a single tag, or run a subcommand."""
163         await self.get_command(ctx, tag_name=tag_name)
164 
165     @tags_group.group(name='search', invoke_without_command=True)
166     async def search_tag_content(self, ctx: Context, *, keywords: str) -> None:
167         """
168         Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma.
169 
170         Only search for tags that has ALL the keywords.
171         """
172         matching_tags = self._get_tags_via_content(all, keywords, ctx.author)
173         await self._send_matching_tags(ctx, keywords, matching_tags)
174 
175     @search_tag_content.command(name='any')
176     async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = 'any') -> None:
177         """
178         Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma.
179 
180         Search for tags that has ANY of the keywords.
181         """
182         matching_tags = self._get_tags_via_content(any, keywords or 'any', ctx.author)
183         await self._send_matching_tags(ctx, keywords, matching_tags)
184 
185     async def display_tag(self, ctx: Context, tag_name: str = None) -> bool:
186         """
187         If a tag is not found, display similar tag names as suggestions.
188 
189         If a tag is not specified, display a paginated embed of all tags.
190 
191         Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display
192         nothing and return False.
193         """
194         def _command_on_cooldown(tag_name: str) -> bool:
195             """
196             Check if the command is currently on cooldown, on a per-tag, per-channel basis.
197 
198             The cooldown duration is set in constants.py.
199             """
200             now = time.time()
201 
202             cooldown_conditions = (
203                 tag_name
204                 and tag_name in self.tag_cooldowns
205                 and (now - self.tag_cooldowns[tag_name]["time"]) < constants.Cooldowns.tags
206                 and self.tag_cooldowns[tag_name]["channel"] == ctx.channel.id
207             )
208 
209             if cooldown_conditions:
210                 return True
211             return False
212 
213         if _command_on_cooldown(tag_name):
214             time_elapsed = time.time() - self.tag_cooldowns[tag_name]["time"]
215             time_left = constants.Cooldowns.tags - time_elapsed
216             log.info(
217                 f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. "
218                 f"Cooldown ends in {time_left:.1f} seconds."
219             )
220             return False
221 
222         if tag_name is not None:
223             temp_founds = self._get_tag(tag_name)
224 
225             founds = []
226 
227             for found_tag in temp_founds:
228                 if self.check_accessibility(ctx.author, found_tag):
229                     founds.append(found_tag)
230 
231             if len(founds) == 1:
232                 tag = founds[0]
233                 if ctx.channel.id not in TEST_CHANNELS:
234                     self.tag_cooldowns[tag_name] = {
235                         "time": time.time(),
236                         "channel": ctx.channel.id
237                     }
238 
239                 self.bot.stats.incr(f"tags.usages.{tag['title'].replace('-', '_')}")
240 
241                 await wait_for_deletion(
242                     await ctx.send(embed=Embed.from_dict(tag['embed'])),
243                     [ctx.author.id],
244                 )
245                 return True
246             elif founds and len(tag_name) >= 3:
247                 await wait_for_deletion(
248                     await ctx.send(
249                         embed=Embed(
250                             title='Did you mean ...',
251                             description='\n'.join(tag['title'] for tag in founds[:10])
252                         )
253                     ),
254                     [ctx.author.id],
255                 )
256                 return True
257 
258         else:
259             tags = self._cache.values()
260             if not tags:
261                 await ctx.send(embed=Embed(
262                     description="**There are no tags in the database!**",
263                     colour=Colour.red()
264                 ))
265                 return True
266             else:
267                 embed: Embed = Embed(title="**Current tags**")
268                 await LinePaginator.paginate(
269                     sorted(
270                         f"**»**   {tag['title']}" for tag in tags
271                         if self.check_accessibility(ctx.author, tag)
272                     ),
273                     ctx,
274                     embed,
275                     footer_text=FOOTER_TEXT,
276                     empty=False,
277                     max_lines=15
278                 )
279                 return True
280 
281         return False
282 
283     @tags_group.command(name='get', aliases=('show', 'g'))
284     async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> bool:
285         """
286         Get a specified tag, or a list of all tags if no tag is specified.
287 
288         Returns False if a tag is on cooldown, or if no matches are found.
289         """
290         return await self.display_tag(ctx, tag_name)
291 
292 
293 def setup(bot: Bot) -> None:
294     """Load the Tags cog."""
295     bot.add_cog(Tags(bot))
296
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py
--- a/bot/exts/info/tags.py
+++ b/bot/exts/info/tags.py
@@ -189,7 +189,7 @@
         If a tag is not specified, display a paginated embed of all tags.
 
         Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display
-        nothing and return False.
+        nothing and return True.
         """
         def _command_on_cooldown(tag_name: str) -> bool:
             """
@@ -217,7 +217,7 @@
                 f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. "
                 f"Cooldown ends in {time_left:.1f} seconds."
             )
-            return False
+            return True
 
         if tag_name is not None:
             temp_founds = self._get_tag(tag_name)
@@ -285,7 +285,8 @@
         """
         Get a specified tag, or a list of all tags if no tag is specified.
 
-        Returns False if a tag is on cooldown, or if no matches are found.
+        Returns True if something can be sent, or if the tag is on cooldown.
+        Returns False if no matches are found.
         """
         return await self.display_tag(ctx, tag_name)

{"golden_diff": "diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py\n--- a/bot/exts/info/tags.py\n+++ b/bot/exts/info/tags.py\n@@ -189,7 +189,7 @@\n         If a tag is not specified, display a paginated embed of all tags.\n \n         Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display\n-        nothing and return False.\n+        nothing and return True.\n         \"\"\"\n         def _command_on_cooldown(tag_name: str) -> bool:\n             \"\"\"\n@@ -217,7 +217,7 @@\n                 f\"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. \"\n                 f\"Cooldown ends in {time_left:.1f} seconds.\"\n             )\n-            return False\n+            return True\n \n         if tag_name is not None:\n             temp_founds = self._get_tag(tag_name)\n@@ -285,7 +285,8 @@\n         \"\"\"\n         Get a specified tag, or a list of all tags if no tag is specified.\n \n-        Returns False if a tag is on cooldown, or if no matches are found.\n+        Returns True if something can be sent, or if the tag is on cooldown.\n+        Returns False if no matches are found.\n         \"\"\"\n         return await self.display_tag(ctx, tag_name)\n", "issue": "Command suggester wrongly invoked for commands under cooldown\nWhen a command is under cooldown and is invoked again, the error handler will wrongly try to suggest an alternative as if the command does not exist, when it should not do anything at all.\r\n\r\n![image](https://user-images.githubusercontent.com/18114431/105258400-5dae7800-5b92-11eb-9841-c07b06766644.png)\r\n\n", "before_files": [{"content": "import logging\nimport re\nimport time\nfrom pathlib import Path\nfrom typing import Callable, Dict, Iterable, List, Optional\n\nfrom discord import Colour, Embed, Member\nfrom discord.ext.commands import Cog, Context, group\n\nfrom bot import constants\nfrom bot.bot import Bot\nfrom bot.converters import TagNameConverter\nfrom bot.pagination import LinePaginator\nfrom bot.utils.messages import wait_for_deletion\n\nlog = logging.getLogger(__name__)\n\nTEST_CHANNELS = (\n    constants.Channels.bot_commands,\n    constants.Channels.helpers\n)\n\nREGEX_NON_ALPHABET = re.compile(r\"[^a-z]\", re.MULTILINE & re.IGNORECASE)\nFOOTER_TEXT = f\"To show a tag, type {constants.Bot.prefix}tags <tagname>.\"\n\n\nclass Tags(Cog):\n    \"\"\"Save new tags and fetch existing tags.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.tag_cooldowns = {}\n        self._cache = self.get_tags()\n\n    @staticmethod\n    def get_tags() -> dict:\n        \"\"\"Get all tags.\"\"\"\n        cache = {}\n\n        base_path = Path(\"bot\", \"resources\", \"tags\")\n        for file in base_path.glob(\"**/*\"):\n            if file.is_file():\n                tag_title = file.stem\n                tag = {\n                    \"title\": tag_title,\n                    \"embed\": {\n                        \"description\": file.read_text(encoding=\"utf8\"),\n                    },\n                    \"restricted_to\": None,\n                    \"location\": f\"/bot/{file}\"\n                }\n\n                # Convert to a list to allow negative indexing.\n                parents = list(file.relative_to(base_path).parents)\n                if len(parents) > 1:\n                    # -1 would be '.' hence -2 is used as the index.\n                    tag[\"restricted_to\"] = parents[-2].name\n\n                cache[tag_title] = tag\n\n        return cache\n\n    @staticmethod\n    def check_accessibility(user: Member, tag: dict) -> bool:\n        \"\"\"Check if user can access a tag.\"\"\"\n        return not tag[\"restricted_to\"] or tag[\"restricted_to\"].lower() in [role.name.lower() for role in user.roles]\n\n    @staticmethod\n    def _fuzzy_search(search: str, target: str) -> float:\n        \"\"\"A simple scoring algorithm based on how many letters are found / total, with order in mind.\"\"\"\n        current, index = 0, 0\n        _search = REGEX_NON_ALPHABET.sub('', search.lower())\n        _targets = iter(REGEX_NON_ALPHABET.split(target.lower()))\n        _target = next(_targets)\n        try:\n            while True:\n                while index < len(_target) and _search[current] == _target[index]:\n                    current += 1\n                    index += 1\n                index, _target = 0, next(_targets)\n        except (StopIteration, IndexError):\n            pass\n        return current / len(_search) * 100\n\n    def _get_suggestions(self, tag_name: str, thresholds: Optional[List[int]] = None) -> List[str]:\n        \"\"\"Return a list of suggested tags.\"\"\"\n        scores: Dict[str, int] = {\n            tag_title: Tags._fuzzy_search(tag_name, tag['title'])\n            for tag_title, tag in self._cache.items()\n        }\n\n        thresholds = thresholds or [100, 90, 80, 70, 60]\n\n        for threshold in thresholds:\n            suggestions = [\n                self._cache[tag_title]\n                for tag_title, matching_score in scores.items()\n                if matching_score >= threshold\n            ]\n            if suggestions:\n                return suggestions\n\n        return []\n\n    def _get_tag(self, tag_name: str) -> list:\n        \"\"\"Get a specific tag.\"\"\"\n        found = [self._cache.get(tag_name.lower(), None)]\n        if not found[0]:\n            return self._get_suggestions(tag_name)\n        return found\n\n    def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str, user: Member) -> list:\n        \"\"\"\n        Search for tags via contents.\n\n        `predicate` will be the built-in any, all, or a custom callable. Must return a bool.\n        \"\"\"\n        keywords_processed: List[str] = []\n        for keyword in keywords.split(','):\n            keyword_sanitized = keyword.strip().casefold()\n            if not keyword_sanitized:\n                # this happens when there are leading / trailing / consecutive comma.\n                continue\n            keywords_processed.append(keyword_sanitized)\n\n        if not keywords_processed:\n            # after sanitizing, we can end up with an empty list, for example when keywords is ','\n            # in that case, we simply want to search for such keywords directly instead.\n            keywords_processed = [keywords]\n\n        matching_tags = []\n        for tag in self._cache.values():\n            matches = (query in tag['embed']['description'].casefold() for query in keywords_processed)\n            if self.check_accessibility(user, tag) and check(matches):\n                matching_tags.append(tag)\n\n        return matching_tags\n\n    async def _send_matching_tags(self, ctx: Context, keywords: str, matching_tags: list) -> None:\n        \"\"\"Send the result of matching tags to user.\"\"\"\n        if not matching_tags:\n            pass\n        elif len(matching_tags) == 1:\n            await ctx.send(embed=Embed().from_dict(matching_tags[0]['embed']))\n        else:\n            is_plural = keywords.strip().count(' ') > 0 or keywords.strip().count(',') > 0\n            embed = Embed(\n                title=f\"Here are the tags containing the given keyword{'s' * is_plural}:\",\n                description='\\n'.join(tag['title'] for tag in matching_tags[:10])\n            )\n            await LinePaginator.paginate(\n                sorted(f\"**\u00bb**   {tag['title']}\" for tag in matching_tags),\n                ctx,\n                embed,\n                footer_text=FOOTER_TEXT,\n                empty=False,\n                max_lines=15\n            )\n\n    @group(name='tags', aliases=('tag', 't'), invoke_without_command=True)\n    async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:\n        \"\"\"Show all known tags, a single tag, or run a subcommand.\"\"\"\n        await self.get_command(ctx, tag_name=tag_name)\n\n    @tags_group.group(name='search', invoke_without_command=True)\n    async def search_tag_content(self, ctx: Context, *, keywords: str) -> None:\n        \"\"\"\n        Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma.\n\n        Only search for tags that has ALL the keywords.\n        \"\"\"\n        matching_tags = self._get_tags_via_content(all, keywords, ctx.author)\n        await self._send_matching_tags(ctx, keywords, matching_tags)\n\n    @search_tag_content.command(name='any')\n    async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = 'any') -> None:\n        \"\"\"\n        Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma.\n\n        Search for tags that has ANY of the keywords.\n        \"\"\"\n        matching_tags = self._get_tags_via_content(any, keywords or 'any', ctx.author)\n        await self._send_matching_tags(ctx, keywords, matching_tags)\n\n    async def display_tag(self, ctx: Context, tag_name: str = None) -> bool:\n        \"\"\"\n        If a tag is not found, display similar tag names as suggestions.\n\n        If a tag is not specified, display a paginated embed of all tags.\n\n        Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display\n        nothing and return False.\n        \"\"\"\n        def _command_on_cooldown(tag_name: str) -> bool:\n            \"\"\"\n            Check if the command is currently on cooldown, on a per-tag, per-channel basis.\n\n            The cooldown duration is set in constants.py.\n            \"\"\"\n            now = time.time()\n\n            cooldown_conditions = (\n                tag_name\n                and tag_name in self.tag_cooldowns\n                and (now - self.tag_cooldowns[tag_name][\"time\"]) < constants.Cooldowns.tags\n                and self.tag_cooldowns[tag_name][\"channel\"] == ctx.channel.id\n            )\n\n            if cooldown_conditions:\n                return True\n            return False\n\n        if _command_on_cooldown(tag_name):\n            time_elapsed = time.time() - self.tag_cooldowns[tag_name][\"time\"]\n            time_left = constants.Cooldowns.tags - time_elapsed\n            log.info(\n                f\"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. \"\n                f\"Cooldown ends in {time_left:.1f} seconds.\"\n            )\n            return False\n\n        if tag_name is not None:\n            temp_founds = self._get_tag(tag_name)\n\n            founds = []\n\n            for found_tag in temp_founds:\n                if self.check_accessibility(ctx.author, found_tag):\n                    founds.append(found_tag)\n\n            if len(founds) == 1:\n                tag = founds[0]\n                if ctx.channel.id not in TEST_CHANNELS:\n                    self.tag_cooldowns[tag_name] = {\n                        \"time\": time.time(),\n                        \"channel\": ctx.channel.id\n                    }\n\n                self.bot.stats.incr(f\"tags.usages.{tag['title'].replace('-', '_')}\")\n\n                await wait_for_deletion(\n                    await ctx.send(embed=Embed.from_dict(tag['embed'])),\n                    [ctx.author.id],\n                )\n                return True\n            elif founds and len(tag_name) >= 3:\n                await wait_for_deletion(\n                    await ctx.send(\n                        embed=Embed(\n                            title='Did you mean ...',\n                            description='\\n'.join(tag['title'] for tag in founds[:10])\n                        )\n                    ),\n                    [ctx.author.id],\n                )\n                return True\n\n        else:\n            tags = self._cache.values()\n            if not tags:\n                await ctx.send(embed=Embed(\n                    description=\"**There are no tags in the database!**\",\n                    colour=Colour.red()\n                ))\n                return True\n            else:\n                embed: Embed = Embed(title=\"**Current tags**\")\n                await LinePaginator.paginate(\n                    sorted(\n                        f\"**\u00bb**   {tag['title']}\" for tag in tags\n                        if self.check_accessibility(ctx.author, tag)\n                    ),\n                    ctx,\n                    embed,\n                    footer_text=FOOTER_TEXT,\n                    empty=False,\n                    max_lines=15\n                )\n                return True\n\n        return False\n\n    @tags_group.command(name='get', aliases=('show', 'g'))\n    async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> bool:\n        \"\"\"\n        Get a specified tag, or a list of all tags if no tag is specified.\n\n        Returns False if a tag is on cooldown, or if no matches are found.\n        \"\"\"\n        return await self.display_tag(ctx, tag_name)\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Tags cog.\"\"\"\n    bot.add_cog(Tags(bot))\n", "path": "bot/exts/info/tags.py"}], "after_files": [{"content": "import logging\nimport re\nimport time\nfrom pathlib import Path\nfrom typing import Callable, Dict, Iterable, List, Optional\n\nfrom discord import Colour, Embed, Member\nfrom discord.ext.commands import Cog, Context, group\n\nfrom bot import constants\nfrom bot.bot import Bot\nfrom bot.converters import TagNameConverter\nfrom bot.pagination import LinePaginator\nfrom bot.utils.messages import wait_for_deletion\n\nlog = logging.getLogger(__name__)\n\nTEST_CHANNELS = (\n    constants.Channels.bot_commands,\n    constants.Channels.helpers\n)\n\nREGEX_NON_ALPHABET = re.compile(r\"[^a-z]\", re.MULTILINE & re.IGNORECASE)\nFOOTER_TEXT = f\"To show a tag, type {constants.Bot.prefix}tags <tagname>.\"\n\n\nclass Tags(Cog):\n    \"\"\"Save new tags and fetch existing tags.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.tag_cooldowns = {}\n        self._cache = self.get_tags()\n\n    @staticmethod\n    def get_tags() -> dict:\n        \"\"\"Get all tags.\"\"\"\n        cache = {}\n\n        base_path = Path(\"bot\", \"resources\", \"tags\")\n        for file in base_path.glob(\"**/*\"):\n            if file.is_file():\n                tag_title = file.stem\n                tag = {\n                    \"title\": tag_title,\n                    \"embed\": {\n                        \"description\": file.read_text(encoding=\"utf8\"),\n                    },\n                    \"restricted_to\": None,\n                    \"location\": f\"/bot/{file}\"\n                }\n\n                # Convert to a list to allow negative indexing.\n                parents = list(file.relative_to(base_path).parents)\n                if len(parents) > 1:\n                    # -1 would be '.' hence -2 is used as the index.\n                    tag[\"restricted_to\"] = parents[-2].name\n\n                cache[tag_title] = tag\n\n        return cache\n\n    @staticmethod\n    def check_accessibility(user: Member, tag: dict) -> bool:\n        \"\"\"Check if user can access a tag.\"\"\"\n        return not tag[\"restricted_to\"] or tag[\"restricted_to\"].lower() in [role.name.lower() for role in user.roles]\n\n    @staticmethod\n    def _fuzzy_search(search: str, target: str) -> float:\n        \"\"\"A simple scoring algorithm based on how many letters are found / total, with order in mind.\"\"\"\n        current, index = 0, 0\n        _search = REGEX_NON_ALPHABET.sub('', search.lower())\n        _targets = iter(REGEX_NON_ALPHABET.split(target.lower()))\n        _target = next(_targets)\n        try:\n            while True:\n                while index < len(_target) and _search[current] == _target[index]:\n                    current += 1\n                    index += 1\n                index, _target = 0, next(_targets)\n        except (StopIteration, IndexError):\n            pass\n        return current / len(_search) * 100\n\n    def _get_suggestions(self, tag_name: str, thresholds: Optional[List[int]] = None) -> List[str]:\n        \"\"\"Return a list of suggested tags.\"\"\"\n        scores: Dict[str, int] = {\n            tag_title: Tags._fuzzy_search(tag_name, tag['title'])\n            for tag_title, tag in self._cache.items()\n        }\n\n        thresholds = thresholds or [100, 90, 80, 70, 60]\n\n        for threshold in thresholds:\n            suggestions = [\n                self._cache[tag_title]\n                for tag_title, matching_score in scores.items()\n                if matching_score >= threshold\n            ]\n            if suggestions:\n                return suggestions\n\n        return []\n\n    def _get_tag(self, tag_name: str) -> list:\n        \"\"\"Get a specific tag.\"\"\"\n        found = [self._cache.get(tag_name.lower(), None)]\n        if not found[0]:\n            return self._get_suggestions(tag_name)\n        return found\n\n    def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str, user: Member) -> list:\n        \"\"\"\n        Search for tags via contents.\n\n        `predicate` will be the built-in any, all, or a custom callable. Must return a bool.\n        \"\"\"\n        keywords_processed: List[str] = []\n        for keyword in keywords.split(','):\n            keyword_sanitized = keyword.strip().casefold()\n            if not keyword_sanitized:\n                # this happens when there are leading / trailing / consecutive comma.\n                continue\n            keywords_processed.append(keyword_sanitized)\n\n        if not keywords_processed:\n            # after sanitizing, we can end up with an empty list, for example when keywords is ','\n            # in that case, we simply want to search for such keywords directly instead.\n            keywords_processed = [keywords]\n\n        matching_tags = []\n        for tag in self._cache.values():\n            matches = (query in tag['embed']['description'].casefold() for query in keywords_processed)\n            if self.check_accessibility(user, tag) and check(matches):\n                matching_tags.append(tag)\n\n        return matching_tags\n\n    async def _send_matching_tags(self, ctx: Context, keywords: str, matching_tags: list) -> None:\n        \"\"\"Send the result of matching tags to user.\"\"\"\n        if not matching_tags:\n            pass\n        elif len(matching_tags) == 1:\n            await ctx.send(embed=Embed().from_dict(matching_tags[0]['embed']))\n        else:\n            is_plural = keywords.strip().count(' ') > 0 or keywords.strip().count(',') > 0\n            embed = Embed(\n                title=f\"Here are the tags containing the given keyword{'s' * is_plural}:\",\n                description='\\n'.join(tag['title'] for tag in matching_tags[:10])\n            )\n            await LinePaginator.paginate(\n                sorted(f\"**\u00bb**   {tag['title']}\" for tag in matching_tags),\n                ctx,\n                embed,\n                footer_text=FOOTER_TEXT,\n                empty=False,\n                max_lines=15\n            )\n\n    @group(name='tags', aliases=('tag', 't'), invoke_without_command=True)\n    async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:\n        \"\"\"Show all known tags, a single tag, or run a subcommand.\"\"\"\n        await self.get_command(ctx, tag_name=tag_name)\n\n    @tags_group.group(name='search', invoke_without_command=True)\n    async def search_tag_content(self, ctx: Context, *, keywords: str) -> None:\n        \"\"\"\n        Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma.\n\n        Only search for tags that has ALL the keywords.\n        \"\"\"\n        matching_tags = self._get_tags_via_content(all, keywords, ctx.author)\n        await self._send_matching_tags(ctx, keywords, matching_tags)\n\n    @search_tag_content.command(name='any')\n    async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = 'any') -> None:\n        \"\"\"\n        Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma.\n\n        Search for tags that has ANY of the keywords.\n        \"\"\"\n        matching_tags = self._get_tags_via_content(any, keywords or 'any', ctx.author)\n        await self._send_matching_tags(ctx, keywords, matching_tags)\n\n    async def display_tag(self, ctx: Context, tag_name: str = None) -> bool:\n        \"\"\"\n        If a tag is not found, display similar tag names as suggestions.\n\n        If a tag is not specified, display a paginated embed of all tags.\n\n        Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display\n        nothing and return True.\n        \"\"\"\n        def _command_on_cooldown(tag_name: str) -> bool:\n            \"\"\"\n            Check if the command is currently on cooldown, on a per-tag, per-channel basis.\n\n            The cooldown duration is set in constants.py.\n            \"\"\"\n            now = time.time()\n\n            cooldown_conditions = (\n                tag_name\n                and tag_name in self.tag_cooldowns\n                and (now - self.tag_cooldowns[tag_name][\"time\"]) < constants.Cooldowns.tags\n                and self.tag_cooldowns[tag_name][\"channel\"] == ctx.channel.id\n            )\n\n            if cooldown_conditions:\n                return True\n            return False\n\n        if _command_on_cooldown(tag_name):\n            time_elapsed = time.time() - self.tag_cooldowns[tag_name][\"time\"]\n            time_left = constants.Cooldowns.tags - time_elapsed\n            log.info(\n                f\"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. \"\n                f\"Cooldown ends in {time_left:.1f} seconds.\"\n            )\n            return True\n\n        if tag_name is not None:\n            temp_founds = self._get_tag(tag_name)\n\n            founds = []\n\n            for found_tag in temp_founds:\n                if self.check_accessibility(ctx.author, found_tag):\n                    founds.append(found_tag)\n\n            if len(founds) == 1:\n                tag = founds[0]\n                if ctx.channel.id not in TEST_CHANNELS:\n                    self.tag_cooldowns[tag_name] = {\n                        \"time\": time.time(),\n                        \"channel\": ctx.channel.id\n                    }\n\n                self.bot.stats.incr(f\"tags.usages.{tag['title'].replace('-', '_')}\")\n\n                await wait_for_deletion(\n                    await ctx.send(embed=Embed.from_dict(tag['embed'])),\n                    [ctx.author.id],\n                )\n                return True\n            elif founds and len(tag_name) >= 3:\n                await wait_for_deletion(\n                    await ctx.send(\n                        embed=Embed(\n                            title='Did you mean ...',\n                            description='\\n'.join(tag['title'] for tag in founds[:10])\n                        )\n                    ),\n                    [ctx.author.id],\n                )\n                return True\n\n        else:\n            tags = self._cache.values()\n            if not tags:\n                await ctx.send(embed=Embed(\n                    description=\"**There are no tags in the database!**\",\n                    colour=Colour.red()\n                ))\n                return True\n            else:\n                embed: Embed = Embed(title=\"**Current tags**\")\n                await LinePaginator.paginate(\n                    sorted(\n                        f\"**\u00bb**   {tag['title']}\" for tag in tags\n                        if self.check_accessibility(ctx.author, tag)\n                    ),\n                    ctx,\n                    embed,\n                    footer_text=FOOTER_TEXT,\n                    empty=False,\n                    max_lines=15\n                )\n                return True\n\n        return False\n\n    @tags_group.command(name='get', aliases=('show', 'g'))\n    async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> bool:\n        \"\"\"\n        Get a specified tag, or a list of all tags if no tag is specified.\n\n        Returns True if something can be sent, or if the tag is on cooldown.\n        Returns False if no matches are found.\n        \"\"\"\n        return await self.display_tag(ctx, tag_name)\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Tags cog.\"\"\"\n    bot.add_cog(Tags(bot))\n", "path": "bot/exts/info/tags.py"}]}