We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add ability to remove bot tag responses.
With the new fuzzy tags, there's a chance an user may think something which is not a tag is one; resulting in a similar tag being fetched 
(`!t def` as an example resulting in a tag through the whole page)

Adding the ability to remove them (perhaps when it was fuzzy matched) would solve this and perhaps some others when a tag's purpose is misremembered.

--- 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/cogs/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
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 
15 log = logging.getLogger(__name__)
16 
17 TEST_CHANNELS = (
18     constants.Channels.bot_commands,
19     constants.Channels.helpers
20 )
21 
22 REGEX_NON_ALPHABET = re.compile(r"[^a-z]", re.MULTILINE & re.IGNORECASE)
23 FOOTER_TEXT = f"To show a tag, type {constants.Bot.prefix}tags <tagname>."
24 
25 
26 class Tags(Cog):
27     """Save new tags and fetch existing tags."""
28 
29     def __init__(self, bot: Bot):
30         self.bot = bot
31         self.tag_cooldowns = {}
32         self._cache = self.get_tags()
33 
34     @staticmethod
35     def get_tags() -> dict:
36         """Get all tags."""
37         # Save all tags in memory.
38         cache = {}
39         tag_files = Path("bot", "resources", "tags").iterdir()
40         for file in tag_files:
41             tag_title = file.stem
42             tag = {
43                 "title": tag_title,
44                 "embed": {
45                     "description": file.read_text()
46                 }
47             }
48             cache[tag_title] = tag
49         return cache
50 
51     @staticmethod
52     def _fuzzy_search(search: str, target: str) -> float:
53         """A simple scoring algorithm based on how many letters are found / total, with order in mind."""
54         current, index = 0, 0
55         _search = REGEX_NON_ALPHABET.sub('', search.lower())
56         _targets = iter(REGEX_NON_ALPHABET.split(target.lower()))
57         _target = next(_targets)
58         try:
59             while True:
60                 while index < len(_target) and _search[current] == _target[index]:
61                     current += 1
62                     index += 1
63                 index, _target = 0, next(_targets)
64         except (StopIteration, IndexError):
65             pass
66         return current / len(_search) * 100
67 
68     def _get_suggestions(self, tag_name: str, thresholds: Optional[List[int]] = None) -> List[str]:
69         """Return a list of suggested tags."""
70         scores: Dict[str, int] = {
71             tag_title: Tags._fuzzy_search(tag_name, tag['title'])
72             for tag_title, tag in self._cache.items()
73         }
74 
75         thresholds = thresholds or [100, 90, 80, 70, 60]
76 
77         for threshold in thresholds:
78             suggestions = [
79                 self._cache[tag_title]
80                 for tag_title, matching_score in scores.items()
81                 if matching_score >= threshold
82             ]
83             if suggestions:
84                 return suggestions
85 
86         return []
87 
88     def _get_tag(self, tag_name: str) -> list:
89         """Get a specific tag."""
90         found = [self._cache.get(tag_name.lower(), None)]
91         if not found[0]:
92             return self._get_suggestions(tag_name)
93         return found
94 
95     def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str) -> list:
96         """
97         Search for tags via contents.
98 
99         `predicate` will be the built-in any, all, or a custom callable. Must return a bool.
100         """
101         keywords_processed: List[str] = []
102         for keyword in keywords.split(','):
103             keyword_sanitized = keyword.strip().casefold()
104             if not keyword_sanitized:
105                 # this happens when there are leading / trailing / consecutive comma.
106                 continue
107             keywords_processed.append(keyword_sanitized)
108 
109         if not keywords_processed:
110             # after sanitizing, we can end up with an empty list, for example when keywords is ','
111             # in that case, we simply want to search for such keywords directly instead.
112             keywords_processed = [keywords]
113 
114         matching_tags = []
115         for tag in self._cache.values():
116             if check(query in tag['embed']['description'].casefold() for query in keywords_processed):
117                 matching_tags.append(tag)
118 
119         return matching_tags
120 
121     async def _send_matching_tags(self, ctx: Context, keywords: str, matching_tags: list) -> None:
122         """Send the result of matching tags to user."""
123         if not matching_tags:
124             pass
125         elif len(matching_tags) == 1:
126             await ctx.send(embed=Embed().from_dict(matching_tags[0]['embed']))
127         else:
128             is_plural = keywords.strip().count(' ') > 0 or keywords.strip().count(',') > 0
129             embed = Embed(
130                 title=f"Here are the tags containing the given keyword{'s' * is_plural}:",
131                 description='\n'.join(tag['title'] for tag in matching_tags[:10])
132             )
133             await LinePaginator.paginate(
134                 sorted(f"**»**   {tag['title']}" for tag in matching_tags),
135                 ctx,
136                 embed,
137                 footer_text=FOOTER_TEXT,
138                 empty=False,
139                 max_lines=15
140             )
141 
142     @group(name='tags', aliases=('tag', 't'), invoke_without_command=True)
143     async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:
144         """Show all known tags, a single tag, or run a subcommand."""
145         await ctx.invoke(self.get_command, tag_name=tag_name)
146 
147     @tags_group.group(name='search', invoke_without_command=True)
148     async def search_tag_content(self, ctx: Context, *, keywords: str) -> None:
149         """
150         Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma.
151 
152         Only search for tags that has ALL the keywords.
153         """
154         matching_tags = self._get_tags_via_content(all, keywords)
155         await self._send_matching_tags(ctx, keywords, matching_tags)
156 
157     @search_tag_content.command(name='any')
158     async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = 'any') -> None:
159         """
160         Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma.
161 
162         Search for tags that has ANY of the keywords.
163         """
164         matching_tags = self._get_tags_via_content(any, keywords or 'any')
165         await self._send_matching_tags(ctx, keywords, matching_tags)
166 
167     @tags_group.command(name='get', aliases=('show', 'g'))
168     async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:
169         """Get a specified tag, or a list of all tags if no tag is specified."""
170         def _command_on_cooldown(tag_name: str) -> bool:
171             """
172             Check if the command is currently on cooldown, on a per-tag, per-channel basis.
173 
174             The cooldown duration is set in constants.py.
175             """
176             now = time.time()
177 
178             cooldown_conditions = (
179                 tag_name
180                 and tag_name in self.tag_cooldowns
181                 and (now - self.tag_cooldowns[tag_name]["time"]) < constants.Cooldowns.tags
182                 and self.tag_cooldowns[tag_name]["channel"] == ctx.channel.id
183             )
184 
185             if cooldown_conditions:
186                 return True
187             return False
188 
189         if _command_on_cooldown(tag_name):
190             time_elapsed = time.time() - self.tag_cooldowns[tag_name]["time"]
191             time_left = constants.Cooldowns.tags - time_elapsed
192             log.info(
193                 f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. "
194                 f"Cooldown ends in {time_left:.1f} seconds."
195             )
196             return
197 
198         if tag_name is not None:
199             founds = self._get_tag(tag_name)
200 
201             if len(founds) == 1:
202                 tag = founds[0]
203                 if ctx.channel.id not in TEST_CHANNELS:
204                     self.tag_cooldowns[tag_name] = {
205                         "time": time.time(),
206                         "channel": ctx.channel.id
207                     }
208                 await ctx.send(embed=Embed.from_dict(tag['embed']))
209             elif founds and len(tag_name) >= 3:
210                 await ctx.send(embed=Embed(
211                     title='Did you mean ...',
212                     description='\n'.join(tag['title'] for tag in founds[:10])
213                 ))
214 
215         else:
216             tags = self._cache.values()
217             if not tags:
218                 await ctx.send(embed=Embed(
219                     description="**There are no tags in the database!**",
220                     colour=Colour.red()
221                 ))
222             else:
223                 embed: Embed = Embed(title="**Current tags**")
224                 await LinePaginator.paginate(
225                     sorted(f"**»**   {tag['title']}" for tag in tags),
226                     ctx,
227                     embed,
228                     footer_text=FOOTER_TEXT,
229                     empty=False,
230                     max_lines=15
231                 )
232 
233 
234 def setup(bot: Bot) -> None:
235     """Load the Tags cog."""
236     bot.add_cog(Tags(bot))
237
```
--- 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/cogs/tags.py b/bot/cogs/tags.py
--- a/bot/cogs/tags.py
+++ b/bot/cogs/tags.py
@@ -11,6 +11,7 @@
 from bot.bot import Bot
 from bot.converters import TagNameConverter
 from bot.pagination import LinePaginator
+from bot.utils.messages import wait_for_deletion
 
 log = logging.getLogger(__name__)
 
@@ -167,6 +168,7 @@
     @tags_group.command(name='get', aliases=('show', 'g'))
     async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:
         """Get a specified tag, or a list of all tags if no tag is specified."""
+
         def _command_on_cooldown(tag_name: str) -> bool:
             """
             Check if the command is currently on cooldown, on a per-tag, per-channel basis.
@@ -205,12 +207,22 @@
                         "time": time.time(),
                         "channel": ctx.channel.id
                     }
-                await ctx.send(embed=Embed.from_dict(tag['embed']))
+                await wait_for_deletion(
+                    await ctx.send(embed=Embed.from_dict(tag['embed'])),
+                    [ctx.author.id],
+                    client=self.bot
+                )
             elif founds and len(tag_name) >= 3:
-                await ctx.send(embed=Embed(
-                    title='Did you mean ...',
-                    description='\n'.join(tag['title'] for tag in founds[:10])
-                ))
+                await wait_for_deletion(
+                    await ctx.send(
+                        embed=Embed(
+                            title='Did you mean ...',
+                            description='\n'.join(tag['title'] for tag in founds[:10])
+                        )
+                    ),
+                    [ctx.author.id],
+                    client=self.bot
+                )
 
         else:
             tags = self._cache.values()

{"golden_diff": "diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py\n--- a/bot/cogs/tags.py\n+++ b/bot/cogs/tags.py\n@@ -11,6 +11,7 @@\n from bot.bot import Bot\n from bot.converters import TagNameConverter\n from bot.pagination import LinePaginator\n+from bot.utils.messages import wait_for_deletion\n \n log = logging.getLogger(__name__)\n \n@@ -167,6 +168,7 @@\n     @tags_group.command(name='get', aliases=('show', 'g'))\n     async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:\n         \"\"\"Get a specified tag, or a list of all tags if no tag is specified.\"\"\"\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@@ -205,12 +207,22 @@\n                         \"time\": time.time(),\n                         \"channel\": ctx.channel.id\n                     }\n-                await ctx.send(embed=Embed.from_dict(tag['embed']))\n+                await wait_for_deletion(\n+                    await ctx.send(embed=Embed.from_dict(tag['embed'])),\n+                    [ctx.author.id],\n+                    client=self.bot\n+                )\n             elif founds and len(tag_name) >= 3:\n-                await ctx.send(embed=Embed(\n-                    title='Did you mean ...',\n-                    description='\\n'.join(tag['title'] for tag in founds[:10])\n-                ))\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+                    client=self.bot\n+                )\n \n         else:\n             tags = self._cache.values()\n", "issue": "Add ability to remove bot tag responses.\nWith the new fuzzy tags, there's a chance an user may think something which is not a tag is one; resulting in a similar tag being fetched \r\n(`!t def` as an example resulting in a tag through the whole page)\r\n\r\nAdding the ability to remove them (perhaps when it was fuzzy matched) would solve this and perhaps some others when a tag's purpose is misremembered.\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\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\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        # Save all tags in memory.\n        cache = {}\n        tag_files = Path(\"bot\", \"resources\", \"tags\").iterdir()\n        for file in tag_files:\n            tag_title = file.stem\n            tag = {\n                \"title\": tag_title,\n                \"embed\": {\n                    \"description\": file.read_text()\n                }\n            }\n            cache[tag_title] = tag\n        return cache\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) -> 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            if check(query in tag['embed']['description'].casefold() for query in keywords_processed):\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 ctx.invoke(self.get_command, 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)\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')\n        await self._send_matching_tags(ctx, keywords, matching_tags)\n\n    @tags_group.command(name='get', aliases=('show', 'g'))\n    async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:\n        \"\"\"Get a specified tag, or a list of all tags if no tag is specified.\"\"\"\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\n\n        if tag_name is not None:\n            founds = self._get_tag(tag_name)\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                await ctx.send(embed=Embed.from_dict(tag['embed']))\n            elif founds and len(tag_name) >= 3:\n                await ctx.send(embed=Embed(\n                    title='Did you mean ...',\n                    description='\\n'.join(tag['title'] for tag in founds[:10])\n                ))\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            else:\n                embed: Embed = Embed(title=\"**Current tags**\")\n                await LinePaginator.paginate(\n                    sorted(f\"**\u00bb**   {tag['title']}\" for tag in tags),\n                    ctx,\n                    embed,\n                    footer_text=FOOTER_TEXT,\n                    empty=False,\n                    max_lines=15\n                )\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Tags cog.\"\"\"\n    bot.add_cog(Tags(bot))\n", "path": "bot/cogs/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\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        # Save all tags in memory.\n        cache = {}\n        tag_files = Path(\"bot\", \"resources\", \"tags\").iterdir()\n        for file in tag_files:\n            tag_title = file.stem\n            tag = {\n                \"title\": tag_title,\n                \"embed\": {\n                    \"description\": file.read_text()\n                }\n            }\n            cache[tag_title] = tag\n        return cache\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) -> 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            if check(query in tag['embed']['description'].casefold() for query in keywords_processed):\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 ctx.invoke(self.get_command, 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)\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')\n        await self._send_matching_tags(ctx, keywords, matching_tags)\n\n    @tags_group.command(name='get', aliases=('show', 'g'))\n    async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None:\n        \"\"\"Get a specified tag, or a list of all tags if no tag is specified.\"\"\"\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\n\n        if tag_name is not None:\n            founds = self._get_tag(tag_name)\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                await wait_for_deletion(\n                    await ctx.send(embed=Embed.from_dict(tag['embed'])),\n                    [ctx.author.id],\n                    client=self.bot\n                )\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                    client=self.bot\n                )\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            else:\n                embed: Embed = Embed(title=\"**Current tags**\")\n                await LinePaginator.paginate(\n                    sorted(f\"**\u00bb**   {tag['title']}\" for tag in tags),\n                    ctx,\n                    embed,\n                    footer_text=FOOTER_TEXT,\n                    empty=False,\n                    max_lines=15\n                )\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Tags cog.\"\"\"\n    bot.add_cog(Tags(bot))\n", "path": "bot/cogs/tags.py"}]}