Skip to content

Commit 7345b10

Browse files
committed
Add paste service utility
1 parent 1a362c0 commit 7345b10

File tree

4 files changed

+118
-2
lines changed

4 files changed

+118
-2
lines changed

docs/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
44
Changelog
55
=========
6+
- :release:`9.7.0 <4th June 2023>`
7+
- :feature:`179` Add paste service utility to upload text to our paste service.
68
- :feature:`176` Migrate repo to use ruff for linting
79

810

pydis_core/utils/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Useful utilities and tools for Discord bot development."""
22

33
from pydis_core.utils import (
4-
_monkey_patches, caching, channel, commands, cooldown, function, interactions, logging, members, regex, scheduling
4+
_monkey_patches, caching, channel, commands, cooldown, function, interactions, logging, members, paste_service,
5+
regex, scheduling
56
)
67
from pydis_core.utils._extensions import unqualify
78

@@ -32,6 +33,7 @@ def apply_monkey_patches() -> None:
3233
interactions,
3334
logging,
3435
members,
36+
paste_service,
3537
regex,
3638
scheduling,
3739
unqualify,

pydis_core/utils/paste_service.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from typing import TypedDict
2+
3+
from aiohttp import ClientConnectorError, ClientSession
4+
5+
from pydis_core.utils import logging
6+
7+
log = logging.get_logger(__name__)
8+
9+
FAILED_REQUEST_ATTEMPTS = 3
10+
MAX_PASTE_SIZE = 128 * 1024 # 128kB
11+
"""The maximum allows size of a paste, in bytes."""
12+
13+
14+
class PasteResponse(TypedDict):
15+
"""
16+
A successful response from the paste service.
17+
18+
args:
19+
link: The URL to the saved paste.
20+
removal: The URL to delete the saved paste.
21+
"""
22+
23+
link: str
24+
removal: str
25+
26+
27+
class PasteUploadError(Exception):
28+
"""Raised when an error is encountered uploading to the paste service."""
29+
30+
31+
class PasteTooLongError(Exception):
32+
"""Raised when content is too large to upload to the paste service."""
33+
34+
35+
async def send_to_paste_service(
36+
*,
37+
contents: str,
38+
paste_url: str,
39+
http_session: ClientSession,
40+
file_name: str = "",
41+
lexer: str = "python",
42+
max_size: int = MAX_PASTE_SIZE,
43+
) -> PasteResponse:
44+
"""
45+
Upload some contents to the paste service.
46+
47+
Args:
48+
contents: The content to upload to the paste service.
49+
paste_url: The base url to the paste service.
50+
http_session (aiohttp.ClientSession): The session to use when POSTing the content to the paste service.
51+
file_name: The name of the file to save to the paste service.
52+
lexer: The lexer to save the content with.
53+
max_size: The max number of bytes to be allowed. Anything larger than :obj:`MAX_PASTE_SIZE` will be rejected.
54+
55+
Raises:
56+
:exc:`ValueError`: ``max_length`` greater than the maximum allowed by the paste service.
57+
:exc:`PasteTooLongError`: ``contents`` too long to upload.
58+
:exc:`PasteUploadError`: Uploading failed.
59+
60+
Returns:
61+
A :obj:`TypedDict` containing both the URL of the paste, and a URL to remove the paste.
62+
"""
63+
if max_size > MAX_PASTE_SIZE:
64+
raise ValueError(f"`max_length` must not be greater than {MAX_PASTE_SIZE}")
65+
66+
contents_size = len(contents.encode())
67+
if contents_size > max_size:
68+
log.info("Contents too large to send to paste service.")
69+
raise PasteTooLongError(f"Contents of size {contents_size} greater than maximum size {max_size}")
70+
71+
log.debug(f"Sending contents of size {contents_size} bytes to paste service.")
72+
payload = {
73+
"expiry": "1month",
74+
"long": "on", # Use a longer URI for the paste.
75+
"files": [
76+
{"name": file_name, "lexer": lexer, "content": contents},
77+
]
78+
}
79+
for attempt in range(1, FAILED_REQUEST_ATTEMPTS + 1):
80+
try:
81+
async with http_session.post(f"{paste_url}/api/v1/paste", json=payload) as response:
82+
response_json = await response.json()
83+
except ClientConnectorError:
84+
log.warning(
85+
f"Failed to connect to paste service at url {paste_url}, "
86+
f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
87+
)
88+
continue
89+
except Exception:
90+
log.exception(
91+
f"An unexpected error has occurred during handling of the request, "
92+
f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
93+
)
94+
continue
95+
96+
if response.status == 400:
97+
log.warning(
98+
f"Paste service returned error {response_json['message']} with status code {response.status}, "
99+
f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
100+
)
101+
continue
102+
103+
if response.status == 200:
104+
log.info(f"Successfully uploaded contents to {response_json['link']}.")
105+
return PasteResponse(link=response_json["link"], removal=response_json["removal"])
106+
107+
log.warning(
108+
f"Got unexpected JSON response from paste service: {response_json}\n"
109+
f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
110+
)
111+
112+
raise PasteUploadError("Failed to upload contents to paste service")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pydis_core"
3-
version = "9.6.0"
3+
version = "9.7.0"
44
description = "PyDis core provides core functionality and utility to the bots of the Python Discord community."
55
authors = ["Python Discord <info@pythondiscord.com>"]
66
license = "MIT"

0 commit comments

Comments
 (0)