diff options
| -rw-r--r-- | docs/changelog.rst | 2 | ||||
| -rw-r--r-- | pydis_core/utils/__init__.py | 4 | ||||
| -rw-r--r-- | pydis_core/utils/paste_service.py | 112 | ||||
| -rw-r--r-- | pyproject.toml | 2 | 
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" | 
