Skip to content

Commit e779f0e

Browse files
committed
give more information about ratelimits, update compat post endpoint to database model
1 parent da6664d commit e779f0e

File tree

3 files changed

+70
-25
lines changed

3 files changed

+70
-25
lines changed

config-template.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"authed_global": "120/minute",
5050
"premium_global": "360/minute",
5151
"postpastes": "5/minute",
52-
"authed_postpages": "10/minute",
52+
"authed_postpastes": "10/minute",
5353
"premium_postpastes": "20/minute",
5454
"getpaste": "20/minute",
5555
"authed_getpaste": "40/minute",

mystbin/backend/models/errors.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@
2323

2424
class Unauthorized(BaseModel):
2525
error: str = "Unauthorized"
26+
notice: str
27+
28+
class Config:
29+
schema_extras = {
30+
"example": {
31+
"error": "Unauthorized",
32+
"notice": "You must be signed in to use this route"
33+
}
34+
}
2635

2736

2837
class Forbidden(BaseModel):

mystbin/backend/routers/pastes.py

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import datetime
2020
import pathlib
2121
import re
22+
import json
2223
from random import sample
2324
from typing import Dict, List, Optional, Union
2425

@@ -37,6 +38,14 @@
3738

3839
router = APIRouter()
3940

41+
__p = pathlib.Path("./config.json")
42+
if not __p.exists():
43+
__p = pathlib.Path("../../config.json")
44+
45+
with __p.open() as __f:
46+
__config = json.load(__f)
47+
48+
del __p, __f # micro-opt, don't keep unneeded variables in-ram
4049

4150
def generate_paste_id():
4251
"""Generate three random words."""
@@ -116,15 +125,18 @@ async def find_discord_tokens(request: Request, pastes: payloads.PastePut):
116125
400: {"content": {"application/json": {"example": {"error": "files.length: You have provided a bad paste"}}}},
117126
},
118127
status_code=201,
119-
name="Create a paste.",
128+
name="Create paste",
120129
)
121130
@limit("postpastes")
122131
async def put_pastes(
123132
request: Request,
124133
payload: payloads.PastePut,
125134
) -> Union[Dict[str, Optional[Union[str, int, datetime.datetime]]], UJSONResponse]:
126-
"""Post a paste to MystBin.
127-
This endpoint accepts a single or many files."""
135+
f"""Post a paste.
136+
137+
This endpoint falls under the `postpastes` ratelimit bucket.
138+
The `postpastes` bucket has a default ratelimit of {__config['ratelimits']['postpastes']}, and a ratelimit of {__config['ratelimits']['authed_postpastes']} when signed in
139+
"""
128140

129141
author_: Optional[Record] = request.state.user
130142

@@ -165,11 +177,15 @@ async def put_pastes(
165177
401: {"model": errors.Unauthorized},
166178
404: {"model": errors.NotFound},
167179
},
168-
name="Retrieve paste file(s)",
180+
name="Get paste",
169181
)
170182
@limit("getpaste")
171183
async def get_paste(request: Request, paste_id: str, password: Optional[str] = None) -> UJSONResponse:
172-
"""Get a paste from MystBin."""
184+
f"""Get a paste by ID.
185+
186+
This endpoint falls under the `getpaste` ratelimit bucket.
187+
The `getpaste` bucket has a default ratelimit of {__config['ratelimits']['getpaste']}, and a ratelimit of {__config['ratelimits']['authed_getpaste']} when signed in
188+
"""
173189
paste = await request.app.state.db.get_paste(paste_id, password)
174190
if paste is None:
175191
return UJSONResponse({"error": "Not Found"}, status_code=404)
@@ -182,26 +198,29 @@ async def get_paste(request: Request, paste_id: str, password: Optional[str] = N
182198

183199

184200
@router.get(
185-
"/pastes",
201+
"/pastes/@me",
186202
tags=["pastes"],
187203
response_model=List[responses.PasteGetAllResponse],
188204
responses={
189205
200: {"model": Optional[List[responses.PasteGetAllResponse]]},
190-
403: {"model": errors.Forbidden},
206+
401: {"model": errors.Unauthorized},
191207
},
192-
name="Get multiple pastes",
208+
name="Get user pastes",
193209
)
194210
@limit("getpaste")
195211
async def get_all_pastes(
196212
request: Request,
197213
limit: Optional[int] = None,
198214
) -> Union[UJSONResponse, Dict[str, List[Dict[str, str]]]]:
199-
"""Get all pastes for a specified author.
215+
f"""Get all pastes for the user you are signed in as via the Authorization header.
200216
* Requires authentication.
217+
218+
This endpoint falls under the `getpaste` ratelimit bucket.
219+
The `getpaste` bucket has a default ratelimit of {__config['ratelimits']['getpaste']}, and a ratelimit of {__config['ratelimits']['authed_getpaste']} when signed in
201220
"""
202221
user = request.state.user
203222
if not user:
204-
return UJSONResponse({"error": "Forbidden"}, status_code=403)
223+
return UJSONResponse({"error": "Unathorized", "notice": "You must be signed in to use this route"}, status_code=401)
205224

206225
pastes = await request.app.state.db.get_all_user_pastes(user["id"], limit)
207226
pastes = [dict(entry) for entry in pastes]
@@ -218,20 +237,25 @@ async def get_all_pastes(
218237
403: {"model": errors.Forbidden},
219238
404: {"model": errors.NotFound},
220239
},
221-
name="Edit paste",
240+
name="Edit paste"
222241
)
223242
@limit("postpastes")
224243
async def edit_paste(
225244
request: Request,
226245
paste_id: str,
227246
payload: payloads.PastePatch,
228247
) -> Union[UJSONResponse, Dict[str, Optional[Union[str, int, datetime.datetime]]]]:
229-
"""Edit a paste on MystBin.
230-
* Requires authentication.
248+
f"""Edit a paste.
249+
You must be the author of the paste (IE, the paste must be created under your account).
250+
251+
* Requires authentication
252+
253+
This endpoint falls under the `postpastes` ratelimit bucket.
254+
The `postpastes` bucket has a default ratelimit of {__config['ratelimits']['postpastes']}, and a ratelimit of {__config['ratelimits']['authed_postpastes']} when signed in
231255
"""
232256
author = request.state.user
233257
if not author:
234-
return UJSONResponse({"error": "Forbidden"}, status_code=403)
258+
return UJSONResponse({"error": "Unathorized", "notice": "You must be signed in to use this route"}, status_code=401)
235259

236260
paste: Union[Record, int] = await request.app.state.db.edit_paste(
237261
paste_id,
@@ -254,24 +278,28 @@ async def edit_paste(
254278
responses={
255279
200: {"content": {"application/json": {"example": {"deleted": "SomePasteID"}}}},
256280
401: {"model": errors.Unauthorized},
257-
403: {"model": errors.Forbidden},
258281
},
259282
status_code=200,
260283
name="Delete paste",
261284
)
262285
@limit("deletepaste")
263-
async def delete_paste(request: Request, paste_id: str = None) -> Union[UJSONResponse, Dict[str, str]]:
264-
"""Deletes pastes on MystBin.
286+
async def delete_paste(request: Request, paste_id: str) -> Union[UJSONResponse, Dict[str, str]]:
287+
f"""Deletes pastes on MystBin.
288+
You must be the author of the paste (IE, the paste must be created under your account).
289+
265290
* Requires authentication.
291+
292+
This endpoint falls under the `deletepaste` ratelimit bucket.
293+
The `deletepaste` bucket has a default ratelimit of {__config['ratelimits']['deletepaste']}, and a ratelimit of {__config['ratelimits']['authed_deletepaste']} when signed in
266294
"""
267295
user = request.state.user
268296
if not user:
269-
return UJSONResponse({"error": "Forbidden"}, status_code=403)
297+
return UJSONResponse({"error": "Unathorized", "notice": "You must be signed in to use this route"}, status_code=401)
270298

271299
if not user["admin"]:
272300
is_owner: bool = await request.app.state.db.ensure_author(paste_id, user["id"])
273301
if not is_owner:
274-
return UJSONResponse({"error": "Unauthorized"}, status_code=401)
302+
return UJSONResponse({"error": "Unauthorized", "notice": f"You do not own paste '{paste_id}'"}, status_code=401)
275303

276304
deleted: Record = await request.app.state.db.delete_paste(paste_id, user["id"], admin=False)
277305

@@ -303,8 +331,13 @@ async def delete_pastes(
303331
request: Request,
304332
payload: payloads.PasteDelete,
305333
) -> Union[UJSONResponse, Dict[str, List[str]]]:
306-
"""Deletes pastes on MystBin.
334+
f"""Deletes pastes.
335+
You must be the author of the pastes (IE, the pastes must be created under your account).
336+
307337
* Requires authentication.
338+
339+
This endpoint falls under the `deletepaste` ratelimit bucket.
340+
The `deletepaste` bucket has a default ratelimit of {__config['ratelimits']['deletepaste']}, and a ratelimit of {__config['ratelimits']['authed_deletepaste']} when signed in
308341
"""
309342
# We will filter out the pastes that are authorized and unauthorized, and return a clear response
310343
response = {"succeeded": [], "failed": []}
@@ -333,16 +366,19 @@ async def delete_pastes(
333366
)
334367
@limit("postpastes")
335368
async def compat_create_paste(request: Request):
336-
"""
337-
A compatibility endpoint to maintain hastbin compat. Depreciated in favour of /paste
369+
f"""
370+
A compatibility endpoint to maintain hastbin compatibility. Depreciated in favour of /paste
338371
This endpoint does not allow for syntax highlighting, multi-file, password protection, expiry, etc. Use the /paste endpoint for these features
372+
373+
This endpoint falls under the `postpastes` ratelimit bucket.
374+
The `postpastes` bucket has a default ratelimit of {__config['ratelimits']['postpastes']}, and a ratelimit of {__config['ratelimits']['authed_postpastes']} when signed in
339375
"""
340376
content = await request.body()
341377
paste: Record = await request.app.state.db.put_paste(
342378
paste_id=generate_paste_id(),
343-
content=content,
379+
pages=[{"filename": "file.txt", "content": content}],
344380
origin_ip=request.headers.get("x-forwarded-for", request.client.host)
345381
if request.app.config["paste"]["log_ip"]
346-
else None,
382+
else None
347383
)
348384
return UJSONResponse({"key": paste["id"]})

0 commit comments

Comments
 (0)