We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Truncate the charinfo embed to fit within 2048 characters
Sentry Issue: [BOT-3D](https://sentry.io/organizations/python-discord/issues/1621405078/?referrer=github_integration)

```
HTTPException: 400 Bad Request (error code: 50035): Invalid Form Body
In embed.description: Must be 2048 or fewer in length.
  File "discord/ext/commands/core.py", line 83, in wrapped
    ret = await coro(*args, **kwargs)
  File "bot/cogs/utils.py", line 162, in charinfo
    await ctx.send(embed=embed)
  File "discord/abc.py", line 856, in send
    data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce)
  File "discord/http.py", line 225, in request
    raise HTTPException(r, data)

Error executing command invoked by <REDACTED>: !charinfo ﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹﯹ
```

--- 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/utils.py`
Content:
```
1 import difflib
2 import logging
3 import re
4 import unicodedata
5 from email.parser import HeaderParser
6 from io import StringIO
7 from typing import Tuple, Union
8 
9 from discord import Colour, Embed, utils
10 from discord.ext.commands import BadArgument, Cog, Context, command
11 
12 from bot.bot import Bot
13 from bot.constants import Channels, MODERATION_ROLES, STAFF_ROLES
14 from bot.decorators import in_whitelist, with_role
15 
16 log = logging.getLogger(__name__)
17 
18 ZEN_OF_PYTHON = """\
19 Beautiful is better than ugly.
20 Explicit is better than implicit.
21 Simple is better than complex.
22 Complex is better than complicated.
23 Flat is better than nested.
24 Sparse is better than dense.
25 Readability counts.
26 Special cases aren't special enough to break the rules.
27 Although practicality beats purity.
28 Errors should never pass silently.
29 Unless explicitly silenced.
30 In the face of ambiguity, refuse the temptation to guess.
31 There should be one-- and preferably only one --obvious way to do it.
32 Although that way may not be obvious at first unless you're Dutch.
33 Now is better than never.
34 Although never is often better than *right* now.
35 If the implementation is hard to explain, it's a bad idea.
36 If the implementation is easy to explain, it may be a good idea.
37 Namespaces are one honking great idea -- let's do more of those!
38 """
39 
40 ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png"
41 
42 
43 class Utils(Cog):
44     """A selection of utilities which don't have a clear category."""
45 
46     def __init__(self, bot: Bot):
47         self.bot = bot
48 
49         self.base_pep_url = "http://www.python.org/dev/peps/pep-"
50         self.base_github_pep_url = "https://raw.githubusercontent.com/python/peps/master/pep-"
51 
52     @command(name='pep', aliases=('get_pep', 'p'))
53     async def pep_command(self, ctx: Context, pep_number: str) -> None:
54         """Fetches information about a PEP and sends it to the channel."""
55         if pep_number.isdigit():
56             pep_number = int(pep_number)
57         else:
58             await ctx.send_help(ctx.command)
59             return
60 
61         # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs.
62         if pep_number == 0:
63             return await self.send_pep_zero(ctx)
64 
65         possible_extensions = ['.txt', '.rst']
66         found_pep = False
67         for extension in possible_extensions:
68             # Attempt to fetch the PEP
69             pep_url = f"{self.base_github_pep_url}{pep_number:04}{extension}"
70             log.trace(f"Requesting PEP {pep_number} with {pep_url}")
71             response = await self.bot.http_session.get(pep_url)
72 
73             if response.status == 200:
74                 log.trace("PEP found")
75                 found_pep = True
76 
77                 pep_content = await response.text()
78 
79                 # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179
80                 pep_header = HeaderParser().parse(StringIO(pep_content))
81 
82                 # Assemble the embed
83                 pep_embed = Embed(
84                     title=f"**PEP {pep_number} - {pep_header['Title']}**",
85                     description=f"[Link]({self.base_pep_url}{pep_number:04})",
86                 )
87 
88                 pep_embed.set_thumbnail(url=ICON_URL)
89 
90                 # Add the interesting information
91                 fields_to_check = ("Status", "Python-Version", "Created", "Type")
92                 for field in fields_to_check:
93                     # Check for a PEP metadata field that is present but has an empty value
94                     # embed field values can't contain an empty string
95                     if pep_header.get(field, ""):
96                         pep_embed.add_field(name=field, value=pep_header[field])
97 
98             elif response.status != 404:
99                 # any response except 200 and 404 is expected
100                 found_pep = True  # actually not, but it's easier to display this way
101                 log.trace(f"The user requested PEP {pep_number}, but the response had an unexpected status code: "
102                           f"{response.status}.\n{response.text}")
103 
104                 error_message = "Unexpected HTTP error during PEP search. Please let us know."
105                 pep_embed = Embed(title="Unexpected error", description=error_message)
106                 pep_embed.colour = Colour.red()
107                 break
108 
109         if not found_pep:
110             log.trace("PEP was not found")
111             not_found = f"PEP {pep_number} does not exist."
112             pep_embed = Embed(title="PEP not found", description=not_found)
113             pep_embed.colour = Colour.red()
114 
115         await ctx.message.channel.send(embed=pep_embed)
116 
117     @command()
118     @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES)
119     async def charinfo(self, ctx: Context, *, characters: str) -> None:
120         """Shows you information on up to 25 unicode characters."""
121         match = re.match(r"<(a?):(\w+):(\d+)>", characters)
122         if match:
123             embed = Embed(
124                 title="Non-Character Detected",
125                 description=(
126                     "Only unicode characters can be processed, but a custom Discord emoji "
127                     "was found. Please remove it and try again."
128                 )
129             )
130             embed.colour = Colour.red()
131             await ctx.send(embed=embed)
132             return
133 
134         if len(characters) > 25:
135             embed = Embed(title=f"Too many characters ({len(characters)}/25)")
136             embed.colour = Colour.red()
137             await ctx.send(embed=embed)
138             return
139 
140         def get_info(char: str) -> Tuple[str, str]:
141             digit = f"{ord(char):x}"
142             if len(digit) <= 4:
143                 u_code = f"\\u{digit:>04}"
144             else:
145                 u_code = f"\\U{digit:>08}"
146             url = f"https://www.compart.com/en/unicode/U+{digit:>04}"
147             name = f"[{unicodedata.name(char, '')}]({url})"
148             info = f"`{u_code.ljust(10)}`: {name} - {utils.escape_markdown(char)}"
149             return info, u_code
150 
151         charlist, rawlist = zip(*(get_info(c) for c in characters))
152 
153         embed = Embed(description="\n".join(charlist))
154         embed.set_author(name="Character Info")
155 
156         if len(characters) > 1:
157             embed.add_field(name='Raw', value=f"`{''.join(rawlist)}`", inline=False)
158 
159         await ctx.send(embed=embed)
160 
161     @command()
162     async def zen(self, ctx: Context, *, search_value: Union[int, str, None] = None) -> None:
163         """
164         Show the Zen of Python.
165 
166         Without any arguments, the full Zen will be produced.
167         If an integer is provided, the line with that index will be produced.
168         If a string is provided, the line which matches best will be produced.
169         """
170         embed = Embed(
171             colour=Colour.blurple(),
172             title="The Zen of Python",
173             description=ZEN_OF_PYTHON
174         )
175 
176         if search_value is None:
177             embed.title += ", by Tim Peters"
178             await ctx.send(embed=embed)
179             return
180 
181         zen_lines = ZEN_OF_PYTHON.splitlines()
182 
183         # handle if it's an index int
184         if isinstance(search_value, int):
185             upper_bound = len(zen_lines) - 1
186             lower_bound = -1 * upper_bound
187             if not (lower_bound <= search_value <= upper_bound):
188                 raise BadArgument(f"Please provide an index between {lower_bound} and {upper_bound}.")
189 
190             embed.title += f" (line {search_value % len(zen_lines)}):"
191             embed.description = zen_lines[search_value]
192             await ctx.send(embed=embed)
193             return
194 
195         # Try to handle first exact word due difflib.SequenceMatched may use some other similar word instead
196         # exact word.
197         for i, line in enumerate(zen_lines):
198             for word in line.split():
199                 if word.lower() == search_value.lower():
200                     embed.title += f" (line {i}):"
201                     embed.description = line
202                     await ctx.send(embed=embed)
203                     return
204 
205         # handle if it's a search string and not exact word
206         matcher = difflib.SequenceMatcher(None, search_value.lower())
207 
208         best_match = ""
209         match_index = 0
210         best_ratio = 0
211 
212         for index, line in enumerate(zen_lines):
213             matcher.set_seq2(line.lower())
214 
215             # the match ratio needs to be adjusted because, naturally,
216             # longer lines will have worse ratios than shorter lines when
217             # fuzzy searching for keywords. this seems to work okay.
218             adjusted_ratio = (len(line) - 5) ** 0.5 * matcher.ratio()
219 
220             if adjusted_ratio > best_ratio:
221                 best_ratio = adjusted_ratio
222                 best_match = line
223                 match_index = index
224 
225         if not best_match:
226             raise BadArgument("I didn't get a match! Please try again with a different search term.")
227 
228         embed.title += f" (line {match_index}):"
229         embed.description = best_match
230         await ctx.send(embed=embed)
231 
232     @command(aliases=("poll",))
233     @with_role(*MODERATION_ROLES)
234     async def vote(self, ctx: Context, title: str, *options: str) -> None:
235         """
236         Build a quick voting poll with matching reactions with the provided options.
237 
238         A maximum of 20 options can be provided, as Discord supports a max of 20
239         reactions on a single message.
240         """
241         if len(options) < 2:
242             raise BadArgument("Please provide at least 2 options.")
243         if len(options) > 20:
244             raise BadArgument("I can only handle 20 options!")
245 
246         codepoint_start = 127462  # represents "regional_indicator_a" unicode value
247         options = {chr(i): f"{chr(i)} - {v}" for i, v in enumerate(options, start=codepoint_start)}
248         embed = Embed(title=title, description="\n".join(options.values()))
249         message = await ctx.send(embed=embed)
250         for reaction in options:
251             await message.add_reaction(reaction)
252 
253     async def send_pep_zero(self, ctx: Context) -> None:
254         """Send information about PEP 0."""
255         pep_embed = Embed(
256             title="**PEP 0 - Index of Python Enhancement Proposals (PEPs)**",
257             description="[Link](https://www.python.org/dev/peps/)"
258         )
259         pep_embed.set_thumbnail(url=ICON_URL)
260         pep_embed.add_field(name="Status", value="Active")
261         pep_embed.add_field(name="Created", value="13-Jul-2000")
262         pep_embed.add_field(name="Type", value="Informational")
263 
264         await ctx.send(embed=pep_embed)
265 
266 
267 def setup(bot: Bot) -> None:
268     """Load the Utils cog."""
269     bot.add_cog(Utils(bot))
270
```
--- 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/utils.py b/bot/cogs/utils.py
--- a/bot/cogs/utils.py
+++ b/bot/cogs/utils.py
@@ -12,6 +12,8 @@
 from bot.bot import Bot
 from bot.constants import Channels, MODERATION_ROLES, STAFF_ROLES
 from bot.decorators import in_whitelist, with_role
+from bot.pagination import LinePaginator
+from bot.utils import messages
 
 log = logging.getLogger(__name__)
 
@@ -117,25 +119,18 @@
     @command()
     @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES)
     async def charinfo(self, ctx: Context, *, characters: str) -> None:
-        """Shows you information on up to 25 unicode characters."""
+        """Shows you information on up to 50 unicode characters."""
         match = re.match(r"<(a?):(\w+):(\d+)>", characters)
         if match:
-            embed = Embed(
-                title="Non-Character Detected",
-                description=(
-                    "Only unicode characters can be processed, but a custom Discord emoji "
-                    "was found. Please remove it and try again."
-                )
+            return await messages.send_denial(
+                ctx,
+                "**Non-Character Detected**\n"
+                "Only unicode characters can be processed, but a custom Discord emoji "
+                "was found. Please remove it and try again."
             )
-            embed.colour = Colour.red()
-            await ctx.send(embed=embed)
-            return
 
-        if len(characters) > 25:
-            embed = Embed(title=f"Too many characters ({len(characters)}/25)")
-            embed.colour = Colour.red()
-            await ctx.send(embed=embed)
-            return
+        if len(characters) > 50:
+            return await messages.send_denial(ctx, f"Too many characters ({len(characters)}/50)")
 
         def get_info(char: str) -> Tuple[str, str]:
             digit = f"{ord(char):x}"
@@ -148,15 +143,14 @@
             info = f"`{u_code.ljust(10)}`: {name} - {utils.escape_markdown(char)}"
             return info, u_code
 
-        charlist, rawlist = zip(*(get_info(c) for c in characters))
-
-        embed = Embed(description="\n".join(charlist))
-        embed.set_author(name="Character Info")
+        char_list, raw_list = zip(*(get_info(c) for c in characters))
+        embed = Embed().set_author(name="Character Info")
 
         if len(characters) > 1:
-            embed.add_field(name='Raw', value=f"`{''.join(rawlist)}`", inline=False)
+            # Maximum length possible is 502 out of 1024, so there's no need to truncate.
+            embed.add_field(name='Full Raw Text', value=f"`{''.join(raw_list)}`", inline=False)
 
-        await ctx.send(embed=embed)
+        await LinePaginator.paginate(char_list, ctx, embed, max_lines=10, max_size=2000, empty=False)
 
     @command()
     async def zen(self, ctx: Context, *, search_value: Union[int, str, None] = None) -> None:

{"golden_diff": "diff --git a/bot/cogs/utils.py b/bot/cogs/utils.py\n--- a/bot/cogs/utils.py\n+++ b/bot/cogs/utils.py\n@@ -12,6 +12,8 @@\n from bot.bot import Bot\n from bot.constants import Channels, MODERATION_ROLES, STAFF_ROLES\n from bot.decorators import in_whitelist, with_role\n+from bot.pagination import LinePaginator\n+from bot.utils import messages\n \n log = logging.getLogger(__name__)\n \n@@ -117,25 +119,18 @@\n     @command()\n     @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES)\n     async def charinfo(self, ctx: Context, *, characters: str) -> None:\n-        \"\"\"Shows you information on up to 25 unicode characters.\"\"\"\n+        \"\"\"Shows you information on up to 50 unicode characters.\"\"\"\n         match = re.match(r\"<(a?):(\\w+):(\\d+)>\", characters)\n         if match:\n-            embed = Embed(\n-                title=\"Non-Character Detected\",\n-                description=(\n-                    \"Only unicode characters can be processed, but a custom Discord emoji \"\n-                    \"was found. Please remove it and try again.\"\n-                )\n+            return await messages.send_denial(\n+                ctx,\n+                \"**Non-Character Detected**\\n\"\n+                \"Only unicode characters can be processed, but a custom Discord emoji \"\n+                \"was found. Please remove it and try again.\"\n             )\n-            embed.colour = Colour.red()\n-            await ctx.send(embed=embed)\n-            return\n \n-        if len(characters) > 25:\n-            embed = Embed(title=f\"Too many characters ({len(characters)}/25)\")\n-            embed.colour = Colour.red()\n-            await ctx.send(embed=embed)\n-            return\n+        if len(characters) > 50:\n+            return await messages.send_denial(ctx, f\"Too many characters ({len(characters)}/50)\")\n \n         def get_info(char: str) -> Tuple[str, str]:\n             digit = f\"{ord(char):x}\"\n@@ -148,15 +143,14 @@\n             info = f\"`{u_code.ljust(10)}`: {name} - {utils.escape_markdown(char)}\"\n             return info, u_code\n \n-        charlist, rawlist = zip(*(get_info(c) for c in characters))\n-\n-        embed = Embed(description=\"\\n\".join(charlist))\n-        embed.set_author(name=\"Character Info\")\n+        char_list, raw_list = zip(*(get_info(c) for c in characters))\n+        embed = Embed().set_author(name=\"Character Info\")\n \n         if len(characters) > 1:\n-            embed.add_field(name='Raw', value=f\"`{''.join(rawlist)}`\", inline=False)\n+            # Maximum length possible is 502 out of 1024, so there's no need to truncate.\n+            embed.add_field(name='Full Raw Text', value=f\"`{''.join(raw_list)}`\", inline=False)\n \n-        await ctx.send(embed=embed)\n+        await LinePaginator.paginate(char_list, ctx, embed, max_lines=10, max_size=2000, empty=False)\n \n     @command()\n     async def zen(self, ctx: Context, *, search_value: Union[int, str, None] = None) -> None:\n", "issue": "Truncate the charinfo embed to fit within 2048 characters\nSentry Issue: [BOT-3D](https://sentry.io/organizations/python-discord/issues/1621405078/?referrer=github_integration)\n\n```\nHTTPException: 400 Bad Request (error code: 50035): Invalid Form Body\nIn embed.description: Must be 2048 or fewer in length.\n  File \"discord/ext/commands/core.py\", line 83, in wrapped\n    ret = await coro(*args, **kwargs)\n  File \"bot/cogs/utils.py\", line 162, in charinfo\n    await ctx.send(embed=embed)\n  File \"discord/abc.py\", line 856, in send\n    data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce)\n  File \"discord/http.py\", line 225, in request\n    raise HTTPException(r, data)\n\nError executing command invoked by <REDACTED>: !charinfo \ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\ufbf9\n```\n", "before_files": [{"content": "import difflib\nimport logging\nimport re\nimport unicodedata\nfrom email.parser import HeaderParser\nfrom io import StringIO\nfrom typing import Tuple, Union\n\nfrom discord import Colour, Embed, utils\nfrom discord.ext.commands import BadArgument, Cog, Context, command\n\nfrom bot.bot import Bot\nfrom bot.constants import Channels, MODERATION_ROLES, STAFF_ROLES\nfrom bot.decorators import in_whitelist, with_role\n\nlog = logging.getLogger(__name__)\n\nZEN_OF_PYTHON = \"\"\"\\\nBeautiful is better than ugly.\nExplicit is better than implicit.\nSimple is better than complex.\nComplex is better than complicated.\nFlat is better than nested.\nSparse is better than dense.\nReadability counts.\nSpecial cases aren't special enough to break the rules.\nAlthough practicality beats purity.\nErrors should never pass silently.\nUnless explicitly silenced.\nIn the face of ambiguity, refuse the temptation to guess.\nThere should be one-- and preferably only one --obvious way to do it.\nAlthough that way may not be obvious at first unless you're Dutch.\nNow is better than never.\nAlthough never is often better than *right* now.\nIf the implementation is hard to explain, it's a bad idea.\nIf the implementation is easy to explain, it may be a good idea.\nNamespaces are one honking great idea -- let's do more of those!\n\"\"\"\n\nICON_URL = \"https://www.python.org/static/opengraph-icon-200x200.png\"\n\n\nclass Utils(Cog):\n    \"\"\"A selection of utilities which don't have a clear category.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n\n        self.base_pep_url = \"http://www.python.org/dev/peps/pep-\"\n        self.base_github_pep_url = \"https://raw.githubusercontent.com/python/peps/master/pep-\"\n\n    @command(name='pep', aliases=('get_pep', 'p'))\n    async def pep_command(self, ctx: Context, pep_number: str) -> None:\n        \"\"\"Fetches information about a PEP and sends it to the channel.\"\"\"\n        if pep_number.isdigit():\n            pep_number = int(pep_number)\n        else:\n            await ctx.send_help(ctx.command)\n            return\n\n        # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs.\n        if pep_number == 0:\n            return await self.send_pep_zero(ctx)\n\n        possible_extensions = ['.txt', '.rst']\n        found_pep = False\n        for extension in possible_extensions:\n            # Attempt to fetch the PEP\n            pep_url = f\"{self.base_github_pep_url}{pep_number:04}{extension}\"\n            log.trace(f\"Requesting PEP {pep_number} with {pep_url}\")\n            response = await self.bot.http_session.get(pep_url)\n\n            if response.status == 200:\n                log.trace(\"PEP found\")\n                found_pep = True\n\n                pep_content = await response.text()\n\n                # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179\n                pep_header = HeaderParser().parse(StringIO(pep_content))\n\n                # Assemble the embed\n                pep_embed = Embed(\n                    title=f\"**PEP {pep_number} - {pep_header['Title']}**\",\n                    description=f\"[Link]({self.base_pep_url}{pep_number:04})\",\n                )\n\n                pep_embed.set_thumbnail(url=ICON_URL)\n\n                # Add the interesting information\n                fields_to_check = (\"Status\", \"Python-Version\", \"Created\", \"Type\")\n                for field in fields_to_check:\n                    # Check for a PEP metadata field that is present but has an empty value\n                    # embed field values can't contain an empty string\n                    if pep_header.get(field, \"\"):\n                        pep_embed.add_field(name=field, value=pep_header[field])\n\n            elif response.status != 404:\n                # any response except 200 and 404 is expected\n                found_pep = True  # actually not, but it's easier to display this way\n                log.trace(f\"The user requested PEP {pep_number}, but the response had an unexpected status code: \"\n                          f\"{response.status}.\\n{response.text}\")\n\n                error_message = \"Unexpected HTTP error during PEP search. Please let us know.\"\n                pep_embed = Embed(title=\"Unexpected error\", description=error_message)\n                pep_embed.colour = Colour.red()\n                break\n\n        if not found_pep:\n            log.trace(\"PEP was not found\")\n            not_found = f\"PEP {pep_number} does not exist.\"\n            pep_embed = Embed(title=\"PEP not found\", description=not_found)\n            pep_embed.colour = Colour.red()\n\n        await ctx.message.channel.send(embed=pep_embed)\n\n    @command()\n    @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES)\n    async def charinfo(self, ctx: Context, *, characters: str) -> None:\n        \"\"\"Shows you information on up to 25 unicode characters.\"\"\"\n        match = re.match(r\"<(a?):(\\w+):(\\d+)>\", characters)\n        if match:\n            embed = Embed(\n                title=\"Non-Character Detected\",\n                description=(\n                    \"Only unicode characters can be processed, but a custom Discord emoji \"\n                    \"was found. Please remove it and try again.\"\n                )\n            )\n            embed.colour = Colour.red()\n            await ctx.send(embed=embed)\n            return\n\n        if len(characters) > 25:\n            embed = Embed(title=f\"Too many characters ({len(characters)}/25)\")\n            embed.colour = Colour.red()\n            await ctx.send(embed=embed)\n            return\n\n        def get_info(char: str) -> Tuple[str, str]:\n            digit = f\"{ord(char):x}\"\n            if len(digit) <= 4:\n                u_code = f\"\\\\u{digit:>04}\"\n            else:\n                u_code = f\"\\\\U{digit:>08}\"\n            url = f\"https://www.compart.com/en/unicode/U+{digit:>04}\"\n            name = f\"[{unicodedata.name(char, '')}]({url})\"\n            info = f\"`{u_code.ljust(10)}`: {name} - {utils.escape_markdown(char)}\"\n            return info, u_code\n\n        charlist, rawlist = zip(*(get_info(c) for c in characters))\n\n        embed = Embed(description=\"\\n\".join(charlist))\n        embed.set_author(name=\"Character Info\")\n\n        if len(characters) > 1:\n            embed.add_field(name='Raw', value=f\"`{''.join(rawlist)}`\", inline=False)\n\n        await ctx.send(embed=embed)\n\n    @command()\n    async def zen(self, ctx: Context, *, search_value: Union[int, str, None] = None) -> None:\n        \"\"\"\n        Show the Zen of Python.\n\n        Without any arguments, the full Zen will be produced.\n        If an integer is provided, the line with that index will be produced.\n        If a string is provided, the line which matches best will be produced.\n        \"\"\"\n        embed = Embed(\n            colour=Colour.blurple(),\n            title=\"The Zen of Python\",\n            description=ZEN_OF_PYTHON\n        )\n\n        if search_value is None:\n            embed.title += \", by Tim Peters\"\n            await ctx.send(embed=embed)\n            return\n\n        zen_lines = ZEN_OF_PYTHON.splitlines()\n\n        # handle if it's an index int\n        if isinstance(search_value, int):\n            upper_bound = len(zen_lines) - 1\n            lower_bound = -1 * upper_bound\n            if not (lower_bound <= search_value <= upper_bound):\n                raise BadArgument(f\"Please provide an index between {lower_bound} and {upper_bound}.\")\n\n            embed.title += f\" (line {search_value % len(zen_lines)}):\"\n            embed.description = zen_lines[search_value]\n            await ctx.send(embed=embed)\n            return\n\n        # Try to handle first exact word due difflib.SequenceMatched may use some other similar word instead\n        # exact word.\n        for i, line in enumerate(zen_lines):\n            for word in line.split():\n                if word.lower() == search_value.lower():\n                    embed.title += f\" (line {i}):\"\n                    embed.description = line\n                    await ctx.send(embed=embed)\n                    return\n\n        # handle if it's a search string and not exact word\n        matcher = difflib.SequenceMatcher(None, search_value.lower())\n\n        best_match = \"\"\n        match_index = 0\n        best_ratio = 0\n\n        for index, line in enumerate(zen_lines):\n            matcher.set_seq2(line.lower())\n\n            # the match ratio needs to be adjusted because, naturally,\n            # longer lines will have worse ratios than shorter lines when\n            # fuzzy searching for keywords. this seems to work okay.\n            adjusted_ratio = (len(line) - 5) ** 0.5 * matcher.ratio()\n\n            if adjusted_ratio > best_ratio:\n                best_ratio = adjusted_ratio\n                best_match = line\n                match_index = index\n\n        if not best_match:\n            raise BadArgument(\"I didn't get a match! Please try again with a different search term.\")\n\n        embed.title += f\" (line {match_index}):\"\n        embed.description = best_match\n        await ctx.send(embed=embed)\n\n    @command(aliases=(\"poll\",))\n    @with_role(*MODERATION_ROLES)\n    async def vote(self, ctx: Context, title: str, *options: str) -> None:\n        \"\"\"\n        Build a quick voting poll with matching reactions with the provided options.\n\n        A maximum of 20 options can be provided, as Discord supports a max of 20\n        reactions on a single message.\n        \"\"\"\n        if len(options) < 2:\n            raise BadArgument(\"Please provide at least 2 options.\")\n        if len(options) > 20:\n            raise BadArgument(\"I can only handle 20 options!\")\n\n        codepoint_start = 127462  # represents \"regional_indicator_a\" unicode value\n        options = {chr(i): f\"{chr(i)} - {v}\" for i, v in enumerate(options, start=codepoint_start)}\n        embed = Embed(title=title, description=\"\\n\".join(options.values()))\n        message = await ctx.send(embed=embed)\n        for reaction in options:\n            await message.add_reaction(reaction)\n\n    async def send_pep_zero(self, ctx: Context) -> None:\n        \"\"\"Send information about PEP 0.\"\"\"\n        pep_embed = Embed(\n            title=\"**PEP 0 - Index of Python Enhancement Proposals (PEPs)**\",\n            description=\"[Link](https://www.python.org/dev/peps/)\"\n        )\n        pep_embed.set_thumbnail(url=ICON_URL)\n        pep_embed.add_field(name=\"Status\", value=\"Active\")\n        pep_embed.add_field(name=\"Created\", value=\"13-Jul-2000\")\n        pep_embed.add_field(name=\"Type\", value=\"Informational\")\n\n        await ctx.send(embed=pep_embed)\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Utils cog.\"\"\"\n    bot.add_cog(Utils(bot))\n", "path": "bot/cogs/utils.py"}], "after_files": [{"content": "import difflib\nimport logging\nimport re\nimport unicodedata\nfrom email.parser import HeaderParser\nfrom io import StringIO\nfrom typing import Tuple, Union\n\nfrom discord import Colour, Embed, utils\nfrom discord.ext.commands import BadArgument, Cog, Context, command\n\nfrom bot.bot import Bot\nfrom bot.constants import Channels, MODERATION_ROLES, STAFF_ROLES\nfrom bot.decorators import in_whitelist, with_role\nfrom bot.pagination import LinePaginator\nfrom bot.utils import messages\n\nlog = logging.getLogger(__name__)\n\nZEN_OF_PYTHON = \"\"\"\\\nBeautiful is better than ugly.\nExplicit is better than implicit.\nSimple is better than complex.\nComplex is better than complicated.\nFlat is better than nested.\nSparse is better than dense.\nReadability counts.\nSpecial cases aren't special enough to break the rules.\nAlthough practicality beats purity.\nErrors should never pass silently.\nUnless explicitly silenced.\nIn the face of ambiguity, refuse the temptation to guess.\nThere should be one-- and preferably only one --obvious way to do it.\nAlthough that way may not be obvious at first unless you're Dutch.\nNow is better than never.\nAlthough never is often better than *right* now.\nIf the implementation is hard to explain, it's a bad idea.\nIf the implementation is easy to explain, it may be a good idea.\nNamespaces are one honking great idea -- let's do more of those!\n\"\"\"\n\nICON_URL = \"https://www.python.org/static/opengraph-icon-200x200.png\"\n\n\nclass Utils(Cog):\n    \"\"\"A selection of utilities which don't have a clear category.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n\n        self.base_pep_url = \"http://www.python.org/dev/peps/pep-\"\n        self.base_github_pep_url = \"https://raw.githubusercontent.com/python/peps/master/pep-\"\n\n    @command(name='pep', aliases=('get_pep', 'p'))\n    async def pep_command(self, ctx: Context, pep_number: str) -> None:\n        \"\"\"Fetches information about a PEP and sends it to the channel.\"\"\"\n        if pep_number.isdigit():\n            pep_number = int(pep_number)\n        else:\n            await ctx.send_help(ctx.command)\n            return\n\n        # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs.\n        if pep_number == 0:\n            return await self.send_pep_zero(ctx)\n\n        possible_extensions = ['.txt', '.rst']\n        found_pep = False\n        for extension in possible_extensions:\n            # Attempt to fetch the PEP\n            pep_url = f\"{self.base_github_pep_url}{pep_number:04}{extension}\"\n            log.trace(f\"Requesting PEP {pep_number} with {pep_url}\")\n            response = await self.bot.http_session.get(pep_url)\n\n            if response.status == 200:\n                log.trace(\"PEP found\")\n                found_pep = True\n\n                pep_content = await response.text()\n\n                # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179\n                pep_header = HeaderParser().parse(StringIO(pep_content))\n\n                # Assemble the embed\n                pep_embed = Embed(\n                    title=f\"**PEP {pep_number} - {pep_header['Title']}**\",\n                    description=f\"[Link]({self.base_pep_url}{pep_number:04})\",\n                )\n\n                pep_embed.set_thumbnail(url=ICON_URL)\n\n                # Add the interesting information\n                fields_to_check = (\"Status\", \"Python-Version\", \"Created\", \"Type\")\n                for field in fields_to_check:\n                    # Check for a PEP metadata field that is present but has an empty value\n                    # embed field values can't contain an empty string\n                    if pep_header.get(field, \"\"):\n                        pep_embed.add_field(name=field, value=pep_header[field])\n\n            elif response.status != 404:\n                # any response except 200 and 404 is expected\n                found_pep = True  # actually not, but it's easier to display this way\n                log.trace(f\"The user requested PEP {pep_number}, but the response had an unexpected status code: \"\n                          f\"{response.status}.\\n{response.text}\")\n\n                error_message = \"Unexpected HTTP error during PEP search. Please let us know.\"\n                pep_embed = Embed(title=\"Unexpected error\", description=error_message)\n                pep_embed.colour = Colour.red()\n                break\n\n        if not found_pep:\n            log.trace(\"PEP was not found\")\n            not_found = f\"PEP {pep_number} does not exist.\"\n            pep_embed = Embed(title=\"PEP not found\", description=not_found)\n            pep_embed.colour = Colour.red()\n\n        await ctx.message.channel.send(embed=pep_embed)\n\n    @command()\n    @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES)\n    async def charinfo(self, ctx: Context, *, characters: str) -> None:\n        \"\"\"Shows you information on up to 50 unicode characters.\"\"\"\n        match = re.match(r\"<(a?):(\\w+):(\\d+)>\", characters)\n        if match:\n            return await messages.send_denial(\n                ctx,\n                \"**Non-Character Detected**\\n\"\n                \"Only unicode characters can be processed, but a custom Discord emoji \"\n                \"was found. Please remove it and try again.\"\n            )\n\n        if len(characters) > 50:\n            return await messages.send_denial(ctx, f\"Too many characters ({len(characters)}/50)\")\n\n        def get_info(char: str) -> Tuple[str, str]:\n            digit = f\"{ord(char):x}\"\n            if len(digit) <= 4:\n                u_code = f\"\\\\u{digit:>04}\"\n            else:\n                u_code = f\"\\\\U{digit:>08}\"\n            url = f\"https://www.compart.com/en/unicode/U+{digit:>04}\"\n            name = f\"[{unicodedata.name(char, '')}]({url})\"\n            info = f\"`{u_code.ljust(10)}`: {name} - {utils.escape_markdown(char)}\"\n            return info, u_code\n\n        char_list, raw_list = zip(*(get_info(c) for c in characters))\n        embed = Embed().set_author(name=\"Character Info\")\n\n        if len(characters) > 1:\n            # Maximum length possible is 502 out of 1024, so there's no need to truncate.\n            embed.add_field(name='Full Raw Text', value=f\"`{''.join(raw_list)}`\", inline=False)\n\n        await LinePaginator.paginate(char_list, ctx, embed, max_lines=10, max_size=2000, empty=False)\n\n    @command()\n    async def zen(self, ctx: Context, *, search_value: Union[int, str, None] = None) -> None:\n        \"\"\"\n        Show the Zen of Python.\n\n        Without any arguments, the full Zen will be produced.\n        If an integer is provided, the line with that index will be produced.\n        If a string is provided, the line which matches best will be produced.\n        \"\"\"\n        embed = Embed(\n            colour=Colour.blurple(),\n            title=\"The Zen of Python\",\n            description=ZEN_OF_PYTHON\n        )\n\n        if search_value is None:\n            embed.title += \", by Tim Peters\"\n            await ctx.send(embed=embed)\n            return\n\n        zen_lines = ZEN_OF_PYTHON.splitlines()\n\n        # handle if it's an index int\n        if isinstance(search_value, int):\n            upper_bound = len(zen_lines) - 1\n            lower_bound = -1 * upper_bound\n            if not (lower_bound <= search_value <= upper_bound):\n                raise BadArgument(f\"Please provide an index between {lower_bound} and {upper_bound}.\")\n\n            embed.title += f\" (line {search_value % len(zen_lines)}):\"\n            embed.description = zen_lines[search_value]\n            await ctx.send(embed=embed)\n            return\n\n        # Try to handle first exact word due difflib.SequenceMatched may use some other similar word instead\n        # exact word.\n        for i, line in enumerate(zen_lines):\n            for word in line.split():\n                if word.lower() == search_value.lower():\n                    embed.title += f\" (line {i}):\"\n                    embed.description = line\n                    await ctx.send(embed=embed)\n                    return\n\n        # handle if it's a search string and not exact word\n        matcher = difflib.SequenceMatcher(None, search_value.lower())\n\n        best_match = \"\"\n        match_index = 0\n        best_ratio = 0\n\n        for index, line in enumerate(zen_lines):\n            matcher.set_seq2(line.lower())\n\n            # the match ratio needs to be adjusted because, naturally,\n            # longer lines will have worse ratios than shorter lines when\n            # fuzzy searching for keywords. this seems to work okay.\n            adjusted_ratio = (len(line) - 5) ** 0.5 * matcher.ratio()\n\n            if adjusted_ratio > best_ratio:\n                best_ratio = adjusted_ratio\n                best_match = line\n                match_index = index\n\n        if not best_match:\n            raise BadArgument(\"I didn't get a match! Please try again with a different search term.\")\n\n        embed.title += f\" (line {match_index}):\"\n        embed.description = best_match\n        await ctx.send(embed=embed)\n\n    @command(aliases=(\"poll\",))\n    @with_role(*MODERATION_ROLES)\n    async def vote(self, ctx: Context, title: str, *options: str) -> None:\n        \"\"\"\n        Build a quick voting poll with matching reactions with the provided options.\n\n        A maximum of 20 options can be provided, as Discord supports a max of 20\n        reactions on a single message.\n        \"\"\"\n        if len(options) < 2:\n            raise BadArgument(\"Please provide at least 2 options.\")\n        if len(options) > 20:\n            raise BadArgument(\"I can only handle 20 options!\")\n\n        codepoint_start = 127462  # represents \"regional_indicator_a\" unicode value\n        options = {chr(i): f\"{chr(i)} - {v}\" for i, v in enumerate(options, start=codepoint_start)}\n        embed = Embed(title=title, description=\"\\n\".join(options.values()))\n        message = await ctx.send(embed=embed)\n        for reaction in options:\n            await message.add_reaction(reaction)\n\n    async def send_pep_zero(self, ctx: Context) -> None:\n        \"\"\"Send information about PEP 0.\"\"\"\n        pep_embed = Embed(\n            title=\"**PEP 0 - Index of Python Enhancement Proposals (PEPs)**\",\n            description=\"[Link](https://www.python.org/dev/peps/)\"\n        )\n        pep_embed.set_thumbnail(url=ICON_URL)\n        pep_embed.add_field(name=\"Status\", value=\"Active\")\n        pep_embed.add_field(name=\"Created\", value=\"13-Jul-2000\")\n        pep_embed.add_field(name=\"Type\", value=\"Informational\")\n\n        await ctx.send(embed=pep_embed)\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Load the Utils cog.\"\"\"\n    bot.add_cog(Utils(bot))\n", "path": "bot/cogs/utils.py"}]}