Server/scripts/openapi.js

243 lines
5.7 KiB
JavaScript
Raw Normal View History

2024-09-05 00:21:51 -07:00
require("module-alias/register");
const getRouteDescriptions = require("./util/getRouteDescriptions");
const path = require("path");
const fs = require("fs");
const {
NO_AUTHORIZATION_ROUTES,
} = require("../dist/api/middlewares/Authentication");
require("missing-native-js-functions");
const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json");
const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
let specification = {
openapi: "3.1.0",
info: {
title: "Valkyrie Server",
description:
"Valkyrie is a Discord.com server implementation and extension, with the goal of complete feature parity with Discord.com, all while adding some additional goodies, security, privacy, and configuration options.",
license: {
name: "Apache-2.0",
2024-09-06 21:08:38 -07:00
url: "http://www.apache.org/licenses/LICENSE-2.0.html",
2024-09-05 00:21:51 -07:00
},
version: "1.0.0",
},
externalDocs: {
description: "Valkyrie Docs",
url: "https://docs.valkyriecoms.com",
},
servers: [
{
url: "https://old.server.valkyriecoms.com/api/",
description: "Official Valkyrie Instance",
},
],
components: {
securitySchemes: {
bearer: {
type: "http",
scheme: "bearer",
description: "Bearer/Bot prefixes are not required.",
bearerFormat: "JWT",
in: "header",
},
},
},
tags: [],
paths: {},
};
const schemaRegEx = new RegExp(/^[\w.]+$/);
function combineSchemas(schemas) {
let definitions = {};
for (const name in schemas) {
definitions = {
...definitions,
...schemas[name].definitions,
[name]: {
...schemas[name],
definitions: undefined,
$schema: undefined,
},
};
}
for (const key in definitions) {
if (!schemaRegEx.test(key)) {
console.error(`Invalid schema name: ${key}`);
continue;
}
specification.components = specification.components || {};
specification.components.schemas =
specification.components.schemas || {};
specification.components.schemas[key] = definitions[key];
delete definitions[key].additionalProperties;
delete definitions[key].$schema;
const definition = definitions[key];
if (typeof definition.properties === "object") {
for (const property of Object.values(definition.properties)) {
if (Array.isArray(property.type)) {
if (property.type.includes("null")) {
property.type = property.type.find((x) => x !== "null");
property.nullable = true;
}
}
}
}
}
return definitions;
}
function getTag(key) {
return key.match(/\/([\w-]+)/)[1];
}
function apiRoutes(missingRoutes) {
const routes = getRouteDescriptions();
// populate tags
const tags = Array.from(routes.keys())
.map((x) => getTag(x))
.sort((a, b) => a.localeCompare(b));
specification.tags = tags.unique().map((x) => ({ name: x }));
routes.forEach((route, pathAndMethod) => {
const [p, method] = pathAndMethod.split("|");
const path = p.replace(/:(\w+)/g, "{$1}");
let obj = specification.paths[path]?.[method] || {};
obj["x-right-required"] = route.right;
obj["x-permission-required"] = route.permission;
obj["x-fires-event"] = route.event;
if (
!NO_AUTHORIZATION_ROUTES.some((x) => {
if (typeof x === "string")
return (method.toUpperCase() + " " + path).startsWith(x);
return x.test(method.toUpperCase() + " " + path);
})
) {
obj.security = [{ bearer: [] }];
}
if (route.description) obj.description = route.description;
if (route.summary) obj.summary = route.summary;
if (route.deprecated) obj.deprecated = route.deprecated;
if (route.requestBody) {
obj.requestBody = {
required: true,
content: {
"application/json": {
schema: {
$ref: `#/components/schemas/${route.requestBody}`,
},
},
},
};
}
if (route.responses) {
obj.responses = {};
for (const [k, v] of Object.entries(route.responses)) {
if (v.body)
obj.responses[k] = {
description: obj?.responses?.[k]?.description || "",
content: {
"application/json": {
schema: {
$ref: `#/components/schemas/${v.body}`,
},
},
},
};
else
obj.responses[k] = {
description:
obj?.responses?.[k]?.description ||
"No description available",
};
}
} else {
obj.responses = {
default: {
description: "No description available",
},
};
}
// handles path parameters
if (p.includes(":")) {
obj.parameters = p.match(/:\w+/g)?.map((x) => ({
name: x.replace(":", ""),
in: "path",
required: true,
schema: { type: "string" },
description: x.replace(":", ""),
}));
}
if (route.query) {
// map to array
const query = Object.entries(route.query).map(([k, v]) => ({
name: k,
in: "query",
required: v.required,
schema: { type: v.type },
description: v.description,
}));
obj.parameters = [...(obj.parameters || []), ...query];
}
obj.tags = [...(obj.tags || []), getTag(p)].unique();
if (missingRoutes.additional.includes(path.replace(/\/$/, ""))) {
obj["x-badges"] = [
{
label: "Valkyrie-only",
color: "red",
},
];
}
specification.paths[path] = Object.assign(
specification.paths[path] || {},
{
[method]: obj,
},
);
});
}
async function main() {
console.log("Generating OpenAPI Specification...");
const routesRes = await fetch(
"https://toastielab.dev/ValkyrieChat/missing-routes/raw/branch/main/missing.json",
{
headers: {
Accept: "application/json",
},
},
);
const missingRoutes = await routesRes.json();
combineSchemas(schemas);
apiRoutes(missingRoutes);
fs.writeFileSync(
openapiPath,
JSON.stringify(specification, null, 4)
.replaceAll("#/definitions", "#/components/schemas")
.replaceAll("bigint", "number"),
);
}
main();