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