We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Enhance off-topic name search feature
We've recently added a search feature for off-topic names. It's a really cool feature, but I think it can be enhanced in a couple of ways. (Use the informal definition of "couple": ["an indefinite small number."](https://www.merriam-webster.com/dictionary/couple))

For all the examples, this is the list of off-topic-names in the database:

![2019-09-17_18-07](https://user-images.githubusercontent.com/33516116/65059284-07b23a80-d976-11e9-9df9-3d5630004202.png)

**1. Use `OffTopicName` converter for the `query` argument**
We don't use the characters as-is in off-topic names, but we use a translation table on the string input. However, we don't do that yet for the search query of the search feature. This leads to interesting search results:

![2019-09-17_18-07_1](https://user-images.githubusercontent.com/33516116/65059311-1567c000-d976-11e9-8081-a0218ed7dc00.png)

If we change the annotation to `query: OffTopicName`, we utilize the same conversion for the query as we use for the actual off-topic-names, leading to much better results:

![2019-09-17_18-03](https://user-images.githubusercontent.com/33516116/65058948-6e832400-d975-11e9-9eaf-3ef9d56712ba.png)

**2. Add a simple membership test of `query in offtopicname`**
Currently, we utilize `difflib.get_close_match` to get results that closely match the query. However, this function will match based on the whole off-topic name, so it doesn't do well if we search for a substring of the off-topic name. (I suspect this is part of the reason we use relatively low cutoff.)

While there's clearly a "pool" in volcyy's underground, we don't find it:

![2019-09-17_18-11](https://user-images.githubusercontent.com/33516116/65059630-a2127e00-d976-11e9-8766-e14fb2142d05.png)

If we add a simple membership test to the existing `get_close_match`, we do get the results we expect:

![2019-09-17_18-13](https://user-images.githubusercontent.com/33516116/65059723-cd956880-d976-11e9-9c99-8fa03b1fd6eb.png)

**3. Given the membership test of suggestion 2, up the cutoff**
The low cutoff does allow us some leeway when searching for partial off-topic names, but it also causes interesting results. Using the current search feature, without the membership testing suggested in 2, we don't find "eevee-is-cooler-than-python" when we search for "eevee", but "eivl's-evil-eval":
![2019-09-17_18-14](https://user-images.githubusercontent.com/33516116/65059873-0d5c5000-d977-11e9-8097-0e4ff750356c.png)

However, if we up the cutoff value and combine it with an additional membership test, we can search for "sounds-like" off-topic names and find off-topic names that contain exact matches:

![2019-09-17_18-19](https://user-images.githubusercontent.com/33516116/65060191-b60aaf80-d977-11e9-9bf1-8c8e0bb443a8.png)

![2019-09-17_18-19_1](https://user-images.githubusercontent.com/33516116/65060199-ba36cd00-d977-11e9-9c14-3645a3dc9f6e.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/cogs/off_topic_names.py`
Content:
```
1 import asyncio
2 import difflib
3 import logging
4 from datetime import datetime, timedelta
5 
6 from discord import Colour, Embed
7 from discord.ext.commands import BadArgument, Bot, Cog, Context, Converter, group
8 
9 from bot.constants import Channels, MODERATION_ROLES
10 from bot.decorators import with_role
11 from bot.pagination import LinePaginator
12 
13 
14 CHANNELS = (Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2)
15 log = logging.getLogger(__name__)
16 
17 
18 class OffTopicName(Converter):
19     """A converter that ensures an added off-topic name is valid."""
20 
21     @staticmethod
22     async def convert(ctx: Context, argument: str):
23         allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-"
24 
25         if not (2 <= len(argument) <= 96):
26             raise BadArgument("Channel name must be between 2 and 96 chars long")
27 
28         elif not all(c.isalnum() or c in allowed_characters for c in argument):
29             raise BadArgument(
30                 "Channel name must only consist of "
31                 "alphanumeric characters, minus signs or apostrophes."
32             )
33 
34         # Replace invalid characters with unicode alternatives.
35         table = str.maketrans(
36             allowed_characters, '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹ǃ？’’-'
37         )
38         return argument.translate(table)
39 
40 
41 async def update_names(bot: Bot):
42     """
43     The background updater task that performs a channel name update daily.
44 
45     Args:
46         bot (Bot):
47             The running bot instance, used for fetching data from the
48             website via the bot's `api_client`.
49     """
50 
51     while True:
52         # Since we truncate the compute timedelta to seconds, we add one second to ensure
53         # we go past midnight in the `seconds_to_sleep` set below.
54         today_at_midnight = datetime.utcnow().replace(microsecond=0, second=0, minute=0, hour=0)
55         next_midnight = today_at_midnight + timedelta(days=1)
56         seconds_to_sleep = (next_midnight - datetime.utcnow()).seconds + 1
57         await asyncio.sleep(seconds_to_sleep)
58 
59         channel_0_name, channel_1_name, channel_2_name = await bot.api_client.get(
60             'bot/off-topic-channel-names', params={'random_items': 3}
61         )
62         channel_0, channel_1, channel_2 = (bot.get_channel(channel_id) for channel_id in CHANNELS)
63 
64         await channel_0.edit(name=f'ot0-{channel_0_name}')
65         await channel_1.edit(name=f'ot1-{channel_1_name}')
66         await channel_2.edit(name=f'ot2-{channel_2_name}')
67         log.debug(
68             "Updated off-topic channel names to"
69             f" {channel_0_name}, {channel_1_name} and {channel_2_name}"
70         )
71 
72 
73 class OffTopicNames(Cog):
74     """Commands related to managing the off-topic category channel names."""
75 
76     def __init__(self, bot: Bot):
77         self.bot = bot
78         self.updater_task = None
79 
80     def cog_unload(self):
81         if self.updater_task is not None:
82             self.updater_task.cancel()
83 
84     @Cog.listener()
85     async def on_ready(self):
86         if self.updater_task is None:
87             coro = update_names(self.bot)
88             self.updater_task = self.bot.loop.create_task(coro)
89 
90     @group(name='otname', aliases=('otnames', 'otn'), invoke_without_command=True)
91     @with_role(*MODERATION_ROLES)
92     async def otname_group(self, ctx):
93         """Add or list items from the off-topic channel name rotation."""
94 
95         await ctx.invoke(self.bot.get_command("help"), "otname")
96 
97     @otname_group.command(name='add', aliases=('a',))
98     @with_role(*MODERATION_ROLES)
99     async def add_command(self, ctx, *names: OffTopicName):
100         """Adds a new off-topic name to the rotation."""
101         # Chain multiple words to a single one
102         name = "-".join(names)
103 
104         await self.bot.api_client.post(f'bot/off-topic-channel-names', params={'name': name})
105         log.info(
106             f"{ctx.author.name}#{ctx.author.discriminator}"
107             f" added the off-topic channel name '{name}"
108         )
109         await ctx.send(f":ok_hand: Added `{name}` to the names list.")
110 
111     @otname_group.command(name='delete', aliases=('remove', 'rm', 'del', 'd'))
112     @with_role(*MODERATION_ROLES)
113     async def delete_command(self, ctx, *names: OffTopicName):
114         """Removes a off-topic name from the rotation."""
115         # Chain multiple words to a single one
116         name = "-".join(names)
117 
118         await self.bot.api_client.delete(f'bot/off-topic-channel-names/{name}')
119         log.info(
120             f"{ctx.author.name}#{ctx.author.discriminator}"
121             f" deleted the off-topic channel name '{name}"
122         )
123         await ctx.send(f":ok_hand: Removed `{name}` from the names list.")
124 
125     @otname_group.command(name='list', aliases=('l',))
126     @with_role(*MODERATION_ROLES)
127     async def list_command(self, ctx):
128         """
129         Lists all currently known off-topic channel names in a paginator.
130         Restricted to Moderator and above to not spoil the surprise.
131         """
132 
133         result = await self.bot.api_client.get('bot/off-topic-channel-names')
134         lines = sorted(f"• {name}" for name in result)
135         embed = Embed(
136             title=f"Known off-topic names (`{len(result)}` total)",
137             colour=Colour.blue()
138         )
139         if result:
140             await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)
141         else:
142             embed.description = "Hmmm, seems like there's nothing here yet."
143             await ctx.send(embed=embed)
144 
145     @otname_group.command(name='search', aliases=('s',))
146     @with_role(*MODERATION_ROLES)
147     async def search_command(self, ctx, *, query: str):
148         """
149         Search for an off-topic name.
150         """
151 
152         result = await self.bot.api_client.get('bot/off-topic-channel-names')
153         matches = difflib.get_close_matches(query, result, n=10, cutoff=0.35)
154         lines = sorted(f"• {name}" for name in matches)
155         embed = Embed(
156             title=f"Query results",
157             colour=Colour.blue()
158         )
159 
160         if matches:
161             await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)
162         else:
163             embed.description = "Nothing found."
164             await ctx.send(embed=embed)
165 
166 
167 def setup(bot: Bot):
168     bot.add_cog(OffTopicNames(bot))
169     log.info("Cog loaded: OffTopicNames")
170
```
--- 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/off_topic_names.py b/bot/cogs/off_topic_names.py
--- a/bot/cogs/off_topic_names.py
+++ b/bot/cogs/off_topic_names.py
@@ -144,20 +144,21 @@
 
     @otname_group.command(name='search', aliases=('s',))
     @with_role(*MODERATION_ROLES)
-    async def search_command(self, ctx, *, query: str):
+    async def search_command(self, ctx, *, query: OffTopicName):
         """
         Search for an off-topic name.
         """
 
         result = await self.bot.api_client.get('bot/off-topic-channel-names')
-        matches = difflib.get_close_matches(query, result, n=10, cutoff=0.35)
-        lines = sorted(f"• {name}" for name in matches)
+        in_matches = {name for name in result if query in name}
+        close_matches = difflib.get_close_matches(query, result, n=10, cutoff=0.70)
+        lines = sorted(f"• {name}" for name in in_matches.union(close_matches))
         embed = Embed(
             title=f"Query results",
             colour=Colour.blue()
         )
 
-        if matches:
+        if lines:
             await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)
         else:
             embed.description = "Nothing found."

{"golden_diff": "diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py\n--- a/bot/cogs/off_topic_names.py\n+++ b/bot/cogs/off_topic_names.py\n@@ -144,20 +144,21 @@\n \n     @otname_group.command(name='search', aliases=('s',))\n     @with_role(*MODERATION_ROLES)\n-    async def search_command(self, ctx, *, query: str):\n+    async def search_command(self, ctx, *, query: OffTopicName):\n         \"\"\"\n         Search for an off-topic name.\n         \"\"\"\n \n         result = await self.bot.api_client.get('bot/off-topic-channel-names')\n-        matches = difflib.get_close_matches(query, result, n=10, cutoff=0.35)\n-        lines = sorted(f\"\u2022 {name}\" for name in matches)\n+        in_matches = {name for name in result if query in name}\n+        close_matches = difflib.get_close_matches(query, result, n=10, cutoff=0.70)\n+        lines = sorted(f\"\u2022 {name}\" for name in in_matches.union(close_matches))\n         embed = Embed(\n             title=f\"Query results\",\n             colour=Colour.blue()\n         )\n \n-        if matches:\n+        if lines:\n             await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)\n         else:\n             embed.description = \"Nothing found.\"\n", "issue": "Enhance off-topic name search feature\nWe've recently added a search feature for off-topic names. It's a really cool feature, but I think it can be enhanced in a couple of ways. (Use the informal definition of \"couple\": [\"an indefinite small number.\"](https://www.merriam-webster.com/dictionary/couple))\r\n\r\nFor all the examples, this is the list of off-topic-names in the database:\r\n\r\n![2019-09-17_18-07](https://user-images.githubusercontent.com/33516116/65059284-07b23a80-d976-11e9-9df9-3d5630004202.png)\r\n\r\n**1. Use `OffTopicName` converter for the `query` argument**\r\nWe don't use the characters as-is in off-topic names, but we use a translation table on the string input. However, we don't do that yet for the search query of the search feature. This leads to interesting search results:\r\n\r\n![2019-09-17_18-07_1](https://user-images.githubusercontent.com/33516116/65059311-1567c000-d976-11e9-8081-a0218ed7dc00.png)\r\n\r\nIf we change the annotation to `query: OffTopicName`, we utilize the same conversion for the query as we use for the actual off-topic-names, leading to much better results:\r\n\r\n![2019-09-17_18-03](https://user-images.githubusercontent.com/33516116/65058948-6e832400-d975-11e9-9eaf-3ef9d56712ba.png)\r\n\r\n**2. Add a simple membership test of `query in offtopicname`**\r\nCurrently, we utilize `difflib.get_close_match` to get results that closely match the query. However, this function will match based on the whole off-topic name, so it doesn't do well if we search for a substring of the off-topic name. (I suspect this is part of the reason we use relatively low cutoff.)\r\n\r\nWhile there's clearly a \"pool\" in volcyy's underground, we don't find it:\r\n\r\n![2019-09-17_18-11](https://user-images.githubusercontent.com/33516116/65059630-a2127e00-d976-11e9-8766-e14fb2142d05.png)\r\n\r\nIf we add a simple membership test to the existing `get_close_match`, we do get the results we expect:\r\n\r\n![2019-09-17_18-13](https://user-images.githubusercontent.com/33516116/65059723-cd956880-d976-11e9-9c99-8fa03b1fd6eb.png)\r\n\r\n**3. Given the membership test of suggestion 2, up the cutoff**\r\nThe low cutoff does allow us some leeway when searching for partial off-topic names, but it also causes interesting results. Using the current search feature, without the membership testing suggested in 2, we don't find \"eevee-is-cooler-than-python\" when we search for \"eevee\", but \"eivl's-evil-eval\":\r\n![2019-09-17_18-14](https://user-images.githubusercontent.com/33516116/65059873-0d5c5000-d977-11e9-8097-0e4ff750356c.png)\r\n\r\nHowever, if we up the cutoff value and combine it with an additional membership test, we can search for \"sounds-like\" off-topic names and find off-topic names that contain exact matches:\r\n\r\n![2019-09-17_18-19](https://user-images.githubusercontent.com/33516116/65060191-b60aaf80-d977-11e9-9bf1-8c8e0bb443a8.png)\r\n\r\n![2019-09-17_18-19_1](https://user-images.githubusercontent.com/33516116/65060199-ba36cd00-d977-11e9-9c14-3645a3dc9f6e.png)\n", "before_files": [{"content": "import asyncio\nimport difflib\nimport logging\nfrom datetime import datetime, timedelta\n\nfrom discord import Colour, Embed\nfrom discord.ext.commands import BadArgument, Bot, Cog, Context, Converter, group\n\nfrom bot.constants import Channels, MODERATION_ROLES\nfrom bot.decorators import with_role\nfrom bot.pagination import LinePaginator\n\n\nCHANNELS = (Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2)\nlog = logging.getLogger(__name__)\n\n\nclass OffTopicName(Converter):\n    \"\"\"A converter that ensures an added off-topic name is valid.\"\"\"\n\n    @staticmethod\n    async def convert(ctx: Context, argument: str):\n        allowed_characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-\"\n\n        if not (2 <= len(argument) <= 96):\n            raise BadArgument(\"Channel name must be between 2 and 96 chars long\")\n\n        elif not all(c.isalnum() or c in allowed_characters for c in argument):\n            raise BadArgument(\n                \"Channel name must only consist of \"\n                \"alphanumeric characters, minus signs or apostrophes.\"\n            )\n\n        # Replace invalid characters with unicode alternatives.\n        table = str.maketrans(\n            allowed_characters, '\ud835\udda0\ud835\udda1\ud835\udda2\ud835\udda3\ud835\udda4\ud835\udda5\ud835\udda6\ud835\udda7\ud835\udda8\ud835\udda9\ud835\uddaa\ud835\uddab\ud835\uddac\ud835\uddad\ud835\uddae\ud835\uddaf\ud835\uddb0\ud835\uddb1\ud835\uddb2\ud835\uddb3\ud835\uddb4\ud835\uddb5\ud835\uddb6\ud835\uddb7\ud835\uddb8\ud835\uddb9\u01c3\uff1f\u2019\u2019-'\n        )\n        return argument.translate(table)\n\n\nasync def update_names(bot: Bot):\n    \"\"\"\n    The background updater task that performs a channel name update daily.\n\n    Args:\n        bot (Bot):\n            The running bot instance, used for fetching data from the\n            website via the bot's `api_client`.\n    \"\"\"\n\n    while True:\n        # Since we truncate the compute timedelta to seconds, we add one second to ensure\n        # we go past midnight in the `seconds_to_sleep` set below.\n        today_at_midnight = datetime.utcnow().replace(microsecond=0, second=0, minute=0, hour=0)\n        next_midnight = today_at_midnight + timedelta(days=1)\n        seconds_to_sleep = (next_midnight - datetime.utcnow()).seconds + 1\n        await asyncio.sleep(seconds_to_sleep)\n\n        channel_0_name, channel_1_name, channel_2_name = await bot.api_client.get(\n            'bot/off-topic-channel-names', params={'random_items': 3}\n        )\n        channel_0, channel_1, channel_2 = (bot.get_channel(channel_id) for channel_id in CHANNELS)\n\n        await channel_0.edit(name=f'ot0-{channel_0_name}')\n        await channel_1.edit(name=f'ot1-{channel_1_name}')\n        await channel_2.edit(name=f'ot2-{channel_2_name}')\n        log.debug(\n            \"Updated off-topic channel names to\"\n            f\" {channel_0_name}, {channel_1_name} and {channel_2_name}\"\n        )\n\n\nclass OffTopicNames(Cog):\n    \"\"\"Commands related to managing the off-topic category channel names.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.updater_task = None\n\n    def cog_unload(self):\n        if self.updater_task is not None:\n            self.updater_task.cancel()\n\n    @Cog.listener()\n    async def on_ready(self):\n        if self.updater_task is None:\n            coro = update_names(self.bot)\n            self.updater_task = self.bot.loop.create_task(coro)\n\n    @group(name='otname', aliases=('otnames', 'otn'), invoke_without_command=True)\n    @with_role(*MODERATION_ROLES)\n    async def otname_group(self, ctx):\n        \"\"\"Add or list items from the off-topic channel name rotation.\"\"\"\n\n        await ctx.invoke(self.bot.get_command(\"help\"), \"otname\")\n\n    @otname_group.command(name='add', aliases=('a',))\n    @with_role(*MODERATION_ROLES)\n    async def add_command(self, ctx, *names: OffTopicName):\n        \"\"\"Adds a new off-topic name to the rotation.\"\"\"\n        # Chain multiple words to a single one\n        name = \"-\".join(names)\n\n        await self.bot.api_client.post(f'bot/off-topic-channel-names', params={'name': name})\n        log.info(\n            f\"{ctx.author.name}#{ctx.author.discriminator}\"\n            f\" added the off-topic channel name '{name}\"\n        )\n        await ctx.send(f\":ok_hand: Added `{name}` to the names list.\")\n\n    @otname_group.command(name='delete', aliases=('remove', 'rm', 'del', 'd'))\n    @with_role(*MODERATION_ROLES)\n    async def delete_command(self, ctx, *names: OffTopicName):\n        \"\"\"Removes a off-topic name from the rotation.\"\"\"\n        # Chain multiple words to a single one\n        name = \"-\".join(names)\n\n        await self.bot.api_client.delete(f'bot/off-topic-channel-names/{name}')\n        log.info(\n            f\"{ctx.author.name}#{ctx.author.discriminator}\"\n            f\" deleted the off-topic channel name '{name}\"\n        )\n        await ctx.send(f\":ok_hand: Removed `{name}` from the names list.\")\n\n    @otname_group.command(name='list', aliases=('l',))\n    @with_role(*MODERATION_ROLES)\n    async def list_command(self, ctx):\n        \"\"\"\n        Lists all currently known off-topic channel names in a paginator.\n        Restricted to Moderator and above to not spoil the surprise.\n        \"\"\"\n\n        result = await self.bot.api_client.get('bot/off-topic-channel-names')\n        lines = sorted(f\"\u2022 {name}\" for name in result)\n        embed = Embed(\n            title=f\"Known off-topic names (`{len(result)}` total)\",\n            colour=Colour.blue()\n        )\n        if result:\n            await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)\n        else:\n            embed.description = \"Hmmm, seems like there's nothing here yet.\"\n            await ctx.send(embed=embed)\n\n    @otname_group.command(name='search', aliases=('s',))\n    @with_role(*MODERATION_ROLES)\n    async def search_command(self, ctx, *, query: str):\n        \"\"\"\n        Search for an off-topic name.\n        \"\"\"\n\n        result = await self.bot.api_client.get('bot/off-topic-channel-names')\n        matches = difflib.get_close_matches(query, result, n=10, cutoff=0.35)\n        lines = sorted(f\"\u2022 {name}\" for name in matches)\n        embed = Embed(\n            title=f\"Query results\",\n            colour=Colour.blue()\n        )\n\n        if matches:\n            await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)\n        else:\n            embed.description = \"Nothing found.\"\n            await ctx.send(embed=embed)\n\n\ndef setup(bot: Bot):\n    bot.add_cog(OffTopicNames(bot))\n    log.info(\"Cog loaded: OffTopicNames\")\n", "path": "bot/cogs/off_topic_names.py"}], "after_files": [{"content": "import asyncio\nimport difflib\nimport logging\nfrom datetime import datetime, timedelta\n\nfrom discord import Colour, Embed\nfrom discord.ext.commands import BadArgument, Bot, Cog, Context, Converter, group\n\nfrom bot.constants import Channels, MODERATION_ROLES\nfrom bot.decorators import with_role\nfrom bot.pagination import LinePaginator\n\n\nCHANNELS = (Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2)\nlog = logging.getLogger(__name__)\n\n\nclass OffTopicName(Converter):\n    \"\"\"A converter that ensures an added off-topic name is valid.\"\"\"\n\n    @staticmethod\n    async def convert(ctx: Context, argument: str):\n        allowed_characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-\"\n\n        if not (2 <= len(argument) <= 96):\n            raise BadArgument(\"Channel name must be between 2 and 96 chars long\")\n\n        elif not all(c.isalnum() or c in allowed_characters for c in argument):\n            raise BadArgument(\n                \"Channel name must only consist of \"\n                \"alphanumeric characters, minus signs or apostrophes.\"\n            )\n\n        # Replace invalid characters with unicode alternatives.\n        table = str.maketrans(\n            allowed_characters, '\ud835\udda0\ud835\udda1\ud835\udda2\ud835\udda3\ud835\udda4\ud835\udda5\ud835\udda6\ud835\udda7\ud835\udda8\ud835\udda9\ud835\uddaa\ud835\uddab\ud835\uddac\ud835\uddad\ud835\uddae\ud835\uddaf\ud835\uddb0\ud835\uddb1\ud835\uddb2\ud835\uddb3\ud835\uddb4\ud835\uddb5\ud835\uddb6\ud835\uddb7\ud835\uddb8\ud835\uddb9\u01c3\uff1f\u2019\u2019-'\n        )\n        return argument.translate(table)\n\n\nasync def update_names(bot: Bot):\n    \"\"\"\n    The background updater task that performs a channel name update daily.\n\n    Args:\n        bot (Bot):\n            The running bot instance, used for fetching data from the\n            website via the bot's `api_client`.\n    \"\"\"\n\n    while True:\n        # Since we truncate the compute timedelta to seconds, we add one second to ensure\n        # we go past midnight in the `seconds_to_sleep` set below.\n        today_at_midnight = datetime.utcnow().replace(microsecond=0, second=0, minute=0, hour=0)\n        next_midnight = today_at_midnight + timedelta(days=1)\n        seconds_to_sleep = (next_midnight - datetime.utcnow()).seconds + 1\n        await asyncio.sleep(seconds_to_sleep)\n\n        channel_0_name, channel_1_name, channel_2_name = await bot.api_client.get(\n            'bot/off-topic-channel-names', params={'random_items': 3}\n        )\n        channel_0, channel_1, channel_2 = (bot.get_channel(channel_id) for channel_id in CHANNELS)\n\n        await channel_0.edit(name=f'ot0-{channel_0_name}')\n        await channel_1.edit(name=f'ot1-{channel_1_name}')\n        await channel_2.edit(name=f'ot2-{channel_2_name}')\n        log.debug(\n            \"Updated off-topic channel names to\"\n            f\" {channel_0_name}, {channel_1_name} and {channel_2_name}\"\n        )\n\n\nclass OffTopicNames(Cog):\n    \"\"\"Commands related to managing the off-topic category channel names.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.updater_task = None\n\n    def cog_unload(self):\n        if self.updater_task is not None:\n            self.updater_task.cancel()\n\n    @Cog.listener()\n    async def on_ready(self):\n        if self.updater_task is None:\n            coro = update_names(self.bot)\n            self.updater_task = self.bot.loop.create_task(coro)\n\n    @group(name='otname', aliases=('otnames', 'otn'), invoke_without_command=True)\n    @with_role(*MODERATION_ROLES)\n    async def otname_group(self, ctx):\n        \"\"\"Add or list items from the off-topic channel name rotation.\"\"\"\n\n        await ctx.invoke(self.bot.get_command(\"help\"), \"otname\")\n\n    @otname_group.command(name='add', aliases=('a',))\n    @with_role(*MODERATION_ROLES)\n    async def add_command(self, ctx, *names: OffTopicName):\n        \"\"\"Adds a new off-topic name to the rotation.\"\"\"\n        # Chain multiple words to a single one\n        name = \"-\".join(names)\n\n        await self.bot.api_client.post(f'bot/off-topic-channel-names', params={'name': name})\n        log.info(\n            f\"{ctx.author.name}#{ctx.author.discriminator}\"\n            f\" added the off-topic channel name '{name}\"\n        )\n        await ctx.send(f\":ok_hand: Added `{name}` to the names list.\")\n\n    @otname_group.command(name='delete', aliases=('remove', 'rm', 'del', 'd'))\n    @with_role(*MODERATION_ROLES)\n    async def delete_command(self, ctx, *names: OffTopicName):\n        \"\"\"Removes a off-topic name from the rotation.\"\"\"\n        # Chain multiple words to a single one\n        name = \"-\".join(names)\n\n        await self.bot.api_client.delete(f'bot/off-topic-channel-names/{name}')\n        log.info(\n            f\"{ctx.author.name}#{ctx.author.discriminator}\"\n            f\" deleted the off-topic channel name '{name}\"\n        )\n        await ctx.send(f\":ok_hand: Removed `{name}` from the names list.\")\n\n    @otname_group.command(name='list', aliases=('l',))\n    @with_role(*MODERATION_ROLES)\n    async def list_command(self, ctx):\n        \"\"\"\n        Lists all currently known off-topic channel names in a paginator.\n        Restricted to Moderator and above to not spoil the surprise.\n        \"\"\"\n\n        result = await self.bot.api_client.get('bot/off-topic-channel-names')\n        lines = sorted(f\"\u2022 {name}\" for name in result)\n        embed = Embed(\n            title=f\"Known off-topic names (`{len(result)}` total)\",\n            colour=Colour.blue()\n        )\n        if result:\n            await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)\n        else:\n            embed.description = \"Hmmm, seems like there's nothing here yet.\"\n            await ctx.send(embed=embed)\n\n    @otname_group.command(name='search', aliases=('s',))\n    @with_role(*MODERATION_ROLES)\n    async def search_command(self, ctx, *, query: OffTopicName):\n        \"\"\"\n        Search for an off-topic name.\n        \"\"\"\n\n        result = await self.bot.api_client.get('bot/off-topic-channel-names')\n        in_matches = {name for name in result if query in name}\n        close_matches = difflib.get_close_matches(query, result, n=10, cutoff=0.70)\n        lines = sorted(f\"\u2022 {name}\" for name in in_matches.union(close_matches))\n        embed = Embed(\n            title=f\"Query results\",\n            colour=Colour.blue()\n        )\n\n        if lines:\n            await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)\n        else:\n            embed.description = \"Nothing found.\"\n            await ctx.send(embed=embed)\n\n\ndef setup(bot: Bot):\n    bot.add_cog(OffTopicNames(bot))\n    log.info(\"Cog loaded: OffTopicNames\")\n", "path": "bot/cogs/off_topic_names.py"}]}