aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2023-06-10 19:44:29 +0100
committerGravatar GitHub <[email protected]>2023-06-10 19:44:29 +0100
commit5ee9489e60bc4b019dd61763172c5e6f1e25dcee (patch)
tree9a45e7c9fad2d8417369fdac2ab97f23a083d2a6
parentlint: fix typo (#180) (diff)
Add paste service utility (#179)
-rw-r--r--docs/changelog.rst2
-rw-r--r--pydis_core/utils/__init__.py4
-rw-r--r--pydis_core/utils/paste_service.py112
-rw-r--r--pyproject.toml2
4 files changed, 118 insertions, 2 deletions
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 1817102e..dda1012e 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -3,6 +3,8 @@
Changelog
=========
+- :release:`9.7.0 <10th June 2023>`
+- :feature:`179` Add paste service utility to upload text to our paste service.
- :feature:`176` Migrate repo to use ruff for linting
diff --git a/pydis_core/utils/__init__.py b/pydis_core/utils/__init__.py
index 8a61082a..0ca265c9 100644
--- a/pydis_core/utils/__init__.py
+++ b/pydis_core/utils/__init__.py
@@ -1,7 +1,8 @@
"""Useful utilities and tools for Discord bot development."""
from pydis_core.utils import (
- _monkey_patches, caching, channel, commands, cooldown, function, interactions, logging, members, regex, scheduling
+ _monkey_patches, caching, channel, commands, cooldown, function, interactions, logging, members, paste_service,
+ regex, scheduling
)
from pydis_core.utils._extensions import unqualify
@@ -32,6 +33,7 @@ __all__ = [
interactions,
logging,
members,
+ paste_service,
regex,
scheduling,
unqualify,
diff --git a/pydis_core/utils/paste_service.py b/pydis_core/utils/paste_service.py
new file mode 100644
index 00000000..227197e5
--- /dev/null
+++ b/pydis_core/utils/paste_service.py
@@ -0,0 +1,112 @@
+from typing import TypedDict
+
+from aiohttp import ClientConnectorError, ClientSession
+
+from pydis_core.utils import logging
+
+log = logging.get_logger(__name__)
+
+FAILED_REQUEST_ATTEMPTS = 3
+MAX_PASTE_SIZE = 128 * 1024 # 128kB
+"""The maximum allows size of a paste, in bytes."""
+
+
+class PasteResponse(TypedDict):
+ """
+ A successful response from the paste service.
+
+ args:
+ link: The URL to the saved paste.
+ removal: The URL to delete the saved paste.
+ """
+
+ link: str
+ removal: str
+
+
+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,
+ paste_url: str,
+ http_session: ClientSession,
+ file_name: str = "",
+ lexer: str = "python",
+ max_size: int = MAX_PASTE_SIZE,
+) -> PasteResponse:
+ """
+ Upload some contents to the paste service.
+
+ Args:
+ contents: The content to upload to the paste service.
+ paste_url: The base url to the paste service.
+ http_session (aiohttp.ClientSession): The session to use when POSTing the content to the paste service.
+ file_name: The name of the file to save to the paste service.
+ lexer: The lexer to save the content with.
+ max_size: The max number of bytes to be allowed. Anything larger than :obj:`MAX_PASTE_SIZE` will be rejected.
+
+ Raises:
+ :exc:`ValueError`: ``max_length`` greater than the maximum allowed by the paste service.
+ :exc:`PasteTooLongError`: ``contents`` too long to upload.
+ :exc:`PasteUploadError`: Uploading failed.
+
+ Returns:
+ A :obj:`TypedDict` containing both the URL of the paste, and a URL to remove the paste.
+ """
+ if max_size > MAX_PASTE_SIZE:
+ raise ValueError(f"`max_length` must not be greater than {MAX_PASTE_SIZE}")
+
+ contents_size = len(contents.encode())
+ if contents_size > max_size:
+ log.info("Contents too large to send to paste service.")
+ raise PasteTooLongError(f"Contents of size {contents_size} greater than maximum size {max_size}")
+
+ log.debug(f"Sending contents of size {contents_size} bytes to paste service.")
+ payload = {
+ "expiry": "1month",
+ "long": "on", # Use a longer URI for the paste.
+ "files": [
+ {"name": file_name, "lexer": lexer, "content": contents},
+ ]
+ }
+ for attempt in range(1, FAILED_REQUEST_ATTEMPTS + 1):
+ try:
+ async with http_session.post(f"{paste_url}/api/v1/paste", json=payload) as response:
+ response_json = await response.json()
+ except ClientConnectorError:
+ log.warning(
+ f"Failed to connect to paste service at url {paste_url}, "
+ f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
+ )
+ continue
+ except Exception:
+ log.exception(
+ f"An unexpected error has occurred during handling of the request, "
+ f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
+ )
+ continue
+
+ if response.status == 400:
+ log.warning(
+ f"Paste service returned error {response_json['message']} with status code {response.status}, "
+ f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
+ )
+ continue
+
+ if response.status == 200:
+ log.info(f"Successfully uploaded contents to {response_json['link']}.")
+ return PasteResponse(link=response_json["link"], removal=response_json["removal"])
+
+ log.warning(
+ 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/pyproject.toml b/pyproject.toml
index 641fda7f..dc64789e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydis_core"
-version = "9.6.0"
+version = "9.7.0"
description = "PyDis core provides core functionality and utility to the bots of the Python Discord community."
authors = ["Python Discord <[email protected]>"]
license = "MIT"