feat: standalone rest server (#290)

* feat: standalone rest server

* desc

* fmt

* <3 vlad

* Update src/rest/README.md

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/rest/README.md

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update README.md

* Update src/rest/deps.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/rest/queue.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* chore: ignore no-explicit-any rule

* fix(rest): replace with correct import paths

* deno fmt

* fixes

* fmt

* use user agent cons

* fix typings

* Update src/rest/cache.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

* Update src/rest/cache.ts

Co-authored-by: Ayyan <ayyantee@gmail.com>

Co-authored-by: Ayyan <ayyantee@gmail.com>
This commit is contained in:
Skillz4Killz
2021-01-09 12:53:14 -05:00
committed by GitHub
parent 27fb44128f
commit cd0347a5e1
22 changed files with 622 additions and 61 deletions
+162
View File
@@ -0,0 +1,162 @@
import { USER_AGENT } from "../util/constants.ts";
import { restCache } from "./cache.ts";
import { ServerRequest } from "./deps.ts";
import { startQueue } from "./queue.ts";
import {
QueuedRequest,
RestServerOptions,
RunMethodOptions,
} from "./types/mod.ts";
/** Processes a request and assigns it to a queue or creates a queue if none exists for it. */
export function processRequest(
request: ServerRequest,
payload: RunMethodOptions,
options: RestServerOptions,
) {
const route = payload.url.substring(payload.url.indexOf("api/"));
const parts = route.split("/");
// REMOVE THE API
parts.shift();
// REMOVES THE VERSION NUMBER
if (parts[0]?.startsWith("v")) parts.shift();
// REMOVE THE MAJOR PARAM
parts.shift();
const [id] = parts;
const queue = restCache.pathQueues.get(id);
// IF THE QUEUE EXISTS JUST ADD THIS TO THE QUEUE
if (queue) {
queue.push({ request, payload, options });
} else {
// CREATES A NEW QUEUE
restCache.pathQueues.set(id, [{ request, payload, options }]);
}
startQueue();
}
/** Creates the request body and headers that are necessary to send a request. Will handle different types of methods and everything necessary for discord. */
export function createRequestBody(queuedRequest: QueuedRequest) {
const headers: { [key: string]: string } = {
Authorization: queuedRequest.options.token,
"User-Agent": USER_AGENT,
};
// GET METHODS SHOULD NOT HAVE A BODY
if (queuedRequest.payload.method === "get") {
queuedRequest.payload.body = undefined;
}
// IF A REASON IS PROVIDED ENCODE IT IN HEADERS
if (queuedRequest.payload.body?.reason) {
headers["X-Audit-Log-Reason"] = encodeURIComponent(
queuedRequest.payload.body.reason,
);
}
// IF A FILE/ATTACHMENT IS PRESENT WE NEED SPECIAL HANDLING
if (queuedRequest.payload.body?.file) {
const form = new FormData();
form.append(
"file",
queuedRequest.payload.body.file.blob,
queuedRequest.payload.body.file.name,
);
form.append(
"payload_json",
JSON.stringify({ ...queuedRequest.payload.body, file: undefined }),
);
queuedRequest.payload.body.file = form;
} else if (
queuedRequest.payload.body &&
!["get", "delete"].includes(queuedRequest.payload.method)
) {
headers["Content-Type"] = "application/json";
}
return {
headers,
body: queuedRequest.payload.body?.file ||
JSON.stringify(queuedRequest.payload.body),
method: queuedRequest.payload.method.toUpperCase(),
};
}
/** Processes the rate limit headers and determines if it needs to be ratelimited and returns the bucket id if available */
export function processRequestHeaders(url: string, headers: Headers) {
let ratelimited = false;
// GET ALL NECESSARY HEADERS
const remaining = headers.get("x-ratelimit-remaining");
const resetTimestamp = headers.get("x-ratelimit-reset");
const retryAfter = headers.get("retry-after");
const global = headers.get("x-ratelimit-global");
const bucketID = headers.get("x-ratelimit-bucket");
// IF THERE IS NO REMAINING RATE LIMIT, MARK IT AS RATE LIMITED
if (remaining && remaining === "0") {
ratelimited = true;
// SAVE THE URL AS LIMITED, IMPORTANT FOR NEW REQUESTS BY USER WITHOUT BUCKET
restCache.ratelimitedPaths.set(url, {
url,
resetTimestamp: Number(resetTimestamp) * 1000,
bucketID,
});
// SAVE THE BUCKET AS LIMITED SINCE DIFFERENT URLS MAY SHARE A BUCKET
if (bucketID) {
restCache.ratelimitedPaths.set(bucketID, {
url,
resetTimestamp: Number(resetTimestamp) * 1000,
bucketID,
});
}
}
// IF THERE IS NO REMAINING GLOBAL LIMIT, MARK IT RATE LIMITED GLOBALLY
if (global) {
const reset = Date.now() + (Number(retryAfter) * 1000);
restCache.eventHandlers.globallyRateLimited(url, reset);
restCache.globallyRateLimited = true;
ratelimited = true;
restCache.ratelimitedPaths.set("global", {
url: "global",
resetTimestamp: reset,
bucketID,
});
if (bucketID) {
restCache.ratelimitedPaths.set(bucketID, {
url: "global",
resetTimestamp: reset,
bucketID,
});
}
}
return ratelimited ? bucketID : undefined;
}
/** This wll create a infinite loop running in 1 seconds using tail recursion to keep rate limits clean. When a rate limit resets, this will remove it so the queue can proceed. */
function processRateLimitedPaths() {
const now = Date.now();
restCache.ratelimitedPaths.forEach((value, key) => {
// IF THE TIME HAS NOT REACHED CANCEL
if (value.resetTimestamp > now) return;
// RATE LIMIT IS OVER, DELETE THE RATE LIMITER
restCache.ratelimitedPaths.delete(key);
// IF IT WAS GLOBAL ALSO MARK THE GLOBAL VALUE AS FALSE
if (key === "global") restCache.globallyRateLimited = false;
});
// RECHECK IN 1 SECOND
setTimeout(() => processRateLimitedPaths(), 1000);
}
/** Starts the loop */
processRateLimitedPaths();