-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
152 lines (130 loc) · 4.79 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import asyncio
import json
import logging
import os
from json import JSONDecodeError
from typing import Literal
from cooldowns import CallableOnCooldown, Cooldown, CooldownBucket
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from starlette.requests import Request
from starlette.responses import HTMLResponse, Response, JSONResponse
from starlette.staticfiles import StaticFiles
from starlette.status import HTTP_204_NO_CONTENT
from starlette.templating import Jinja2Templates
from zonis import BaseZonisException, Packet
from zonis.server import Server
from garven import tags
from garven.schema import Message, ws, RateLimited
from garven import routers
logging.basicConfig(
level=logging.INFO,
format="%(levelname)-8s | %(asctime)s | %(message)s",
datefmt="%d/%m/%Y %I:%M:%S %p",
)
nav_links: list[dict[Literal["name", "url"], str]] = [
{"name": "Home", "url": "/"},
{"name": "Docs", "url": "/docs"},
{"name": "Redoc", "url": "/redoc"},
]
app = FastAPI(
title="Garven",
version="0.3.2",
terms_of_service="https://suggestions.gg/terms",
contact={
"name": "Suggestions Bot",
"url": "https://suggestions.gg/contact",
},
responses={
403: {"model": Message},
429: {
"model": RateLimited,
"description": "You are currently being rate-limited.",
},
},
openapi_tags=tags.tags_metadata,
description="Messages are sorted in order based on ID, "
"that is a message with an ID of 5 is newer then a message with an ID of 4.\n\n"
"Message ID's are generated globally and not per conversation.\n\n"
"**Global Rate-limit**\n\nAll non-authenticated requests are rate-limited globally "
"by client IP and are throttled to 25 requests every 10 seconds.\n\n\n",
)
log = logging.getLogger(__name__)
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
app.zonis = Server(
using_fastapi_websockets=True,
secret_key=os.environ["SECRET_KEY"],
override_key=os.environ["OVERRIDE_KEY"],
)
app.include_router(routers.aggregate_router)
app.include_router(routers.cluster_router)
global_ratelimit = Cooldown(25, 10, CooldownBucket.args)
@app.exception_handler(CallableOnCooldown)
async def route_on_cooldown(request: Request, exc: CallableOnCooldown):
return JSONResponse(
status_code=429,
content={
"retry_after": exc.retry_after,
"resets_at": exc.resets_at.isoformat(),
},
)
@app.middleware("http")
async def ratelimit_routes(request: Request, call_next):
"""Ensures all routes come under the global ratelimit"""
x_api_key = request.headers.get("X-API-KEY")
x_forwarded_for = request.headers.get("X-Forwarded-For", 1)
try:
if x_api_key and x_api_key == os.environ["X-API-KEY"]:
response = await call_next(request)
else:
async with global_ratelimit(x_forwarded_for):
response = await call_next(request)
except CallableOnCooldown as exc:
return JSONResponse(
status_code=429,
content={
"retry_after": exc.retry_after,
"resets_at": exc.resets_at.isoformat(),
},
)
return response
@app.get("/", response_class=HTMLResponse, tags=["General"])
async def index(request: Request):
return templates.TemplateResponse(
"index.html", {"request": request, "nav_links": nav_links}
)
@app.get(
"/ws",
name="Entrypoint",
description="Establish a websocket connection to this URL."
"\n\n*This route describes data that can be returned via the websocket and is not an existing API route.*\n\n"
"**WS Error Codes**\n\nThese can be served as the disconnect code or WS payload response.\n"
"""
| Code | Location | Description |
|------|-------------|-------------|
| 4001 | WS Response | Your WS payload was not valid JSON |
| 4100 | Close Code | Invalid secret key |
| 4101 | Close Code | You failed to identify correctly |
| 4102 | Close Code | You attempted to override an existing connection without valid override authorization |
""",
tags=["Websocket"],
status_code=101,
)
async def websocket_documentation(data: ws.IdentifyPacket):
return Response(status_code=HTTP_204_NO_CONTENT)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
d: str = await websocket.receive_text()
try:
data: Packet = json.loads(d)
identifier = await app.zonis.parse_identify(data, websocket)
except JSONDecodeError:
await websocket.close(code=4101, reason="Identify failed")
return
except BaseZonisException:
return
try:
await asyncio.Future()
except WebSocketDisconnect:
app.zonis.disconnect(identifier)