aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2022-06-07 22:41:37 +0100
committerGravatar GitHub <[email protected]>2022-06-07 22:41:37 +0100
commit01e53b870afc6177f8c65b00693fa92de704c0ed (patch)
tree9d908f3c997469d0f07de17776bffdf1bc2b8bde
parentMerge pull request #1936 from python-discord/feature/1903/fix-pin-inconsistency (diff)
parentMerge branch 'main' into improve-pastebin-error-handling (diff)
Merge pull request #2165 from python-discord/improve-pastebin-error-handling
-rw-r--r--bot/exts/events/code_jams/_cog.py13
-rw-r--r--bot/exts/moderation/dm_relay.py18
-rw-r--r--bot/exts/moderation/metabase.py13
-rw-r--r--bot/exts/utils/internal.py12
-rw-r--r--bot/exts/utils/snekbox.py11
-rw-r--r--bot/utils/__init__.py12
-rw-r--r--bot/utils/services.py34
-rw-r--r--tests/bot/exts/utils/test_snekbox.py6
-rw-r--r--tests/bot/utils/test_services.py27
9 files changed, 102 insertions, 44 deletions
diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py
index 452199f5f..86c357863 100644
--- a/bot/exts/events/code_jams/_cog.py
+++ b/bot/exts/events/code_jams/_cog.py
@@ -12,7 +12,7 @@ from bot.constants import Emojis, Roles
from bot.exts.events.code_jams import _channels
from bot.log import get_logger
from bot.utils.members import get_or_fetch_member
-from bot.utils.services import send_to_paste_service
+from bot.utils.services import PasteTooLongError, PasteUploadError, send_to_paste_service
log = get_logger(__name__)
@@ -139,11 +139,14 @@ class CodeJams(commands.Cog):
format_category_info(category, channels) for category, channels in categories.items()
)
- url = await send_to_paste_service(deletion_details)
- if url is None:
- url = "**Unable to send deletion details to the pasting service.**"
+ try:
+ message = await send_to_paste_service(deletion_details)
+ except PasteTooLongError:
+ message = "**Too long to upload to paste service.**"
+ except PasteUploadError:
+ message = "**Failed to upload to paste service.**"
- return f"Are you sure you want to delete all code jam channels?\n\nThe channels to be deleted: {url}"
+ return f"Are you sure you want to delete all code jam channels?\n\nThe channels to be deleted: {message}"
@codejam.command()
@commands.has_any_role(Roles.admins, Roles.code_jam_event_team)
diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py
index a86c9e409..bf0b96a58 100644
--- a/bot/exts/moderation/dm_relay.py
+++ b/bot/exts/moderation/dm_relay.py
@@ -5,7 +5,7 @@ from bot.bot import Bot
from bot.constants import Emojis, MODERATION_ROLES
from bot.log import get_logger
from bot.utils.channel import is_mod_channel
-from bot.utils.services import send_to_paste_service
+from bot.utils.services import PasteTooLongError, PasteUploadError, send_to_paste_service
log = get_logger(__name__)
@@ -53,14 +53,14 @@ class DMRelay(Cog):
f"User: {user} ({user.id})\n"
f"Channel ID: {user.dm_channel.id}\n\n"
)
-
- paste_link = await send_to_paste_service(metadata + output, extension="txt")
-
- if paste_link is None:
- await ctx.send(f"{Emojis.cross_mark} Failed to upload output to hastebin.")
- return
-
- await ctx.send(paste_link)
+ try:
+ message = await send_to_paste_service(metadata + output, extension="txt")
+ except PasteTooLongError:
+ message = f"{Emojis.cross_mark} Too long to upload to paste service."
+ except PasteUploadError:
+ message = f"{Emojis.cross_mark} Failed to upload to paste service."
+
+ await ctx.send(message)
async def cog_check(self, ctx: Context) -> bool:
"""Only allow moderators to invoke the commands in this cog in mod channels."""
diff --git a/bot/exts/moderation/metabase.py b/bot/exts/moderation/metabase.py
index 0ca9fd4a5..1b4b0cc71 100644
--- a/bot/exts/moderation/metabase.py
+++ b/bot/exts/moderation/metabase.py
@@ -17,6 +17,7 @@ from bot.converters import allowed_strings
from bot.log import get_logger
from bot.utils import send_to_paste_service
from bot.utils.channel import is_mod_channel
+from bot.utils.services import PasteTooLongError, PasteUploadError
log = get_logger(__name__)
@@ -141,11 +142,15 @@ class Metabase(Cog):
# Format it nicely for human eyes
out = json.dumps(out, indent=4, sort_keys=True)
- paste_link = await send_to_paste_service(out, extension=extension)
- if paste_link:
- message = f":+1: {ctx.author.mention} Here's your link: {paste_link}"
+ try:
+ paste_link = await send_to_paste_service(out, extension=extension)
+ except PasteTooLongError:
+ message = f":x: {ctx.author.mention} Too long to upload to paste service."
+ except PasteUploadError:
+ message = f":x: {ctx.author.mention} Failed to upload to paste service."
else:
- message = f":x: {ctx.author.mention} Link service is unavailible."
+ message = f":+1: {ctx.author.mention} Here's your link: {paste_link}"
+
await ctx.send(
f"{message}\nYou can also access this data within internal eval by doing: "
f"`bot.get_cog('Metabase').exports[{question_id}]`"
diff --git a/bot/exts/utils/internal.py b/bot/exts/utils/internal.py
index 2148a3676..3125cee75 100644
--- a/bot/exts/utils/internal.py
+++ b/bot/exts/utils/internal.py
@@ -16,6 +16,7 @@ from bot.bot import Bot
from bot.constants import DEBUG_MODE, Roles
from bot.log import get_logger
from bot.utils import find_nth_occurrence, send_to_paste_service
+from bot.utils.services import PasteTooLongError, PasteUploadError
log = get_logger(__name__)
@@ -194,11 +195,14 @@ async def func(): # (None,) -> Any
truncate_index = newline_truncate_index
if len(out) > truncate_index:
- paste_link = await send_to_paste_service(out, extension="py")
- if paste_link is not None:
- paste_text = f"full contents at {paste_link}"
- else:
+ try:
+ paste_link = await send_to_paste_service(out, extension="py")
+ except PasteTooLongError:
+ paste_text = "too long to upload to paste service."
+ except PasteUploadError:
paste_text = "failed to upload contents to paste service."
+ else:
+ paste_text = f"full contents at {paste_link}"
await ctx.send(
f"```py\n{out[:truncate_index]}\n```"
diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py
index 32b7c8761..98dfd2efa 100644
--- a/bot/exts/utils/snekbox.py
+++ b/bot/exts/utils/snekbox.py
@@ -18,6 +18,7 @@ from bot.decorators import redirect_output
from bot.log import get_logger
from bot.utils import send_to_paste_service
from bot.utils.messages import wait_for_deletion
+from bot.utils.services import PasteTooLongError, PasteUploadError
log = get_logger(__name__)
@@ -65,7 +66,7 @@ if not hasattr(sys, "_setup_finished"):
{setup}
"""
-MAX_PASTE_LEN = 10000
+MAX_PASTE_LENGTH = 10_000
# The Snekbox commands' whitelists and blacklists.
NO_SNEKBOX_CHANNELS = (Channels.python_general,)
@@ -137,10 +138,12 @@ class Snekbox(Cog):
"""Upload the job's output to a paste service and return a URL to it if successful."""
log.trace("Uploading full output to paste service...")
- if len(output) > MAX_PASTE_LEN:
- log.info("Full output is too long to upload")
+ try:
+ return await send_to_paste_service(output, extension="txt", max_length=MAX_PASTE_LENGTH)
+ except PasteTooLongError:
return "too long to upload"
- return await send_to_paste_service(output, extension="txt")
+ except PasteUploadError:
+ return "unable to upload"
@staticmethod
def prepare_timeit_input(codeblocks: list[str]) -> tuple[str, list[str]]:
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index 13533a467..567821126 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -1,4 +1,12 @@
from bot.utils.helpers import CogABCMeta, find_nth_occurrence, has_lines, pad_base64
-from bot.utils.services import send_to_paste_service
+from bot.utils.services import PasteTooLongError, PasteUploadError, send_to_paste_service
-__all__ = ['CogABCMeta', 'find_nth_occurrence', 'has_lines', 'pad_base64', 'send_to_paste_service']
+__all__ = [
+ 'CogABCMeta',
+ 'find_nth_occurrence',
+ 'has_lines',
+ 'pad_base64',
+ 'send_to_paste_service',
+ 'PasteUploadError',
+ 'PasteTooLongError',
+]
diff --git a/bot/utils/services.py b/bot/utils/services.py
index 439c8d500..a752ac0ec 100644
--- a/bot/utils/services.py
+++ b/bot/utils/services.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
from aiohttp import ClientConnectorError
import bot
@@ -9,18 +7,40 @@ from bot.log import get_logger
log = get_logger(__name__)
FAILED_REQUEST_ATTEMPTS = 3
+MAX_PASTE_LENGTH = 100_000
+
+
+class PasteUploadError(Exception):
+ """Raised when an error is encountered uploading to the paste service."""
+
+
+class PasteTooLongError(Exception):
+ """Raised when content is too large to upload to the paste service."""
-async def send_to_paste_service(contents: str, *, extension: str = "") -> Optional[str]:
+async def send_to_paste_service(contents: str, *, extension: str = "", max_length: int = MAX_PASTE_LENGTH) -> str:
"""
Upload `contents` to the paste service.
- `extension` is added to the output URL
+ Add `extension` to the output URL. Use `max_length` to limit the allowed contents length
+ to lower than the maximum allowed by the paste service.
- When an error occurs, `None` is returned, otherwise the generated URL with the suffix.
+ Raise `ValueError` if `max_length` is greater than the maximum allowed by the paste service.
+ Raise `PasteTooLongError` if `contents` is too long to upload, and `PasteUploadError` if uploading fails.
+
+ Return the generated URL with the extension.
"""
+ if max_length > MAX_PASTE_LENGTH:
+ raise ValueError(f"`max_length` must not be greater than {MAX_PASTE_LENGTH}")
+
extension = extension and f".{extension}"
- log.debug(f"Sending contents of size {len(contents.encode())} bytes to paste service.")
+
+ contents_size = len(contents.encode())
+ if contents_size > max_length:
+ log.info("Contents too large to send to paste service.")
+ raise PasteTooLongError(f"Contents of size {contents_size} greater than maximum size {max_length}")
+
+ log.debug(f"Sending contents of size {contents_size} bytes to paste service.")
paste_url = URLs.paste_service.format(key="documents")
for attempt in range(1, FAILED_REQUEST_ATTEMPTS + 1):
try:
@@ -59,3 +79,5 @@ async def send_to_paste_service(contents: str, *, extension: str = "") -> Option
f"Got unexpected JSON response from paste service: {response_json}\n"
f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
)
+
+ raise PasteUploadError("Failed to upload contents to paste service")
diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py
index 3c555c051..b870a9945 100644
--- a/tests/bot/exts/utils/test_snekbox.py
+++ b/tests/bot/exts/utils/test_snekbox.py
@@ -35,15 +35,15 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
resp.json.assert_awaited_once()
async def test_upload_output_reject_too_long(self):
- """Reject output longer than MAX_PASTE_LEN."""
- result = await self.cog.upload_output("-" * (snekbox.MAX_PASTE_LEN + 1))
+ """Reject output longer than MAX_PASTE_LENGTH."""
+ result = await self.cog.upload_output("-" * (snekbox.MAX_PASTE_LENGTH + 1))
self.assertEqual(result, "too long to upload")
@patch("bot.exts.utils.snekbox.send_to_paste_service")
async def test_upload_output(self, mock_paste_util):
"""Upload the eval output to the URLs.paste_service.format(key="documents") endpoint."""
await self.cog.upload_output("Test output.")
- mock_paste_util.assert_called_once_with("Test output.", extension="txt")
+ mock_paste_util.assert_called_once_with("Test output.", extension="txt", max_length=snekbox.MAX_PASTE_LENGTH)
async def test_codeblock_converter(self):
ctx = MockContext()
diff --git a/tests/bot/utils/test_services.py b/tests/bot/utils/test_services.py
index 3b71022db..d0e801299 100644
--- a/tests/bot/utils/test_services.py
+++ b/tests/bot/utils/test_services.py
@@ -4,7 +4,9 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
from aiohttp import ClientConnectorError
-from bot.utils.services import FAILED_REQUEST_ATTEMPTS, send_to_paste_service
+from bot.utils.services import (
+ FAILED_REQUEST_ATTEMPTS, MAX_PASTE_LENGTH, PasteTooLongError, PasteUploadError, send_to_paste_service
+)
from tests.helpers import MockBot
@@ -55,23 +57,34 @@ class PasteTests(unittest.IsolatedAsyncioTestCase):
for error_json in test_cases:
with self.subTest(error_json=error_json):
response.json = AsyncMock(return_value=error_json)
- result = await send_to_paste_service("")
+ with self.assertRaises(PasteUploadError):
+ await send_to_paste_service("")
self.assertEqual(self.bot.http_session.post.call_count, FAILED_REQUEST_ATTEMPTS)
- self.assertIsNone(result)
self.bot.http_session.post.reset_mock()
async def test_request_repeated_on_connection_errors(self):
"""Requests are repeated in the case of connection errors."""
self.bot.http_session.post = MagicMock(side_effect=ClientConnectorError(Mock(), Mock()))
- result = await send_to_paste_service("")
+ with self.assertRaises(PasteUploadError):
+ await send_to_paste_service("")
self.assertEqual(self.bot.http_session.post.call_count, FAILED_REQUEST_ATTEMPTS)
- self.assertIsNone(result)
async def test_general_error_handled_and_request_repeated(self):
"""All `Exception`s are handled, logged and request repeated."""
self.bot.http_session.post = MagicMock(side_effect=Exception)
- result = await send_to_paste_service("")
+ with self.assertRaises(PasteUploadError):
+ await send_to_paste_service("")
self.assertEqual(self.bot.http_session.post.call_count, FAILED_REQUEST_ATTEMPTS)
self.assertLogs("bot.utils", logging.ERROR)
- self.assertIsNone(result)
+
+ async def test_raises_error_on_too_long_input(self):
+ """Ensure PasteTooLongError is raised if `contents` is longer than `MAX_PASTE_LENGTH`."""
+ contents = "a" * (MAX_PASTE_LENGTH + 1)
+ with self.assertRaises(PasteTooLongError):
+ await send_to_paste_service(contents)
+
+ async def test_raises_on_too_large_max_length(self):
+ """Ensure ValueError is raised if `max_length` passed is greater than `MAX_PASTE_LENGTH`."""
+ with self.assertRaises(ValueError):
+ await send_to_paste_service("Hello World!", max_length=MAX_PASTE_LENGTH + 1)