2023-01-18 03:05:49 +01:00
|
|
|
/*
|
2023-03-30 16:51:15 +02:00
|
|
|
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
2023-03-30 17:15:42 +02:00
|
|
|
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
2023-01-18 03:05:49 +01:00
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2023-01-06 09:30:03 +01: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");
|
2023-03-24 23:14:47 +01:00
|
|
|
const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
|
|
|
|
// const specification = JSON.parse(
|
|
|
|
// fs.readFileSync(openapiPath, { encoding: "utf8" }),
|
|
|
|
// );
|
|
|
|
let specification = {
|
|
|
|
openapi: "3.1.0",
|
|
|
|
info: {
|
2023-04-13 21:48:27 +02:00
|
|
|
title: "Spacebar Server",
|
2023-03-24 23:14:47 +01:00
|
|
|
description:
|
2023-04-13 21:48:27 +02:00
|
|
|
"Spacebar is a free open source selfhostable discord compatible chat, voice and video platform",
|
2023-03-24 23:14:47 +01:00
|
|
|
license: {
|
|
|
|
name: "AGPLV3",
|
|
|
|
url: "https://www.gnu.org/licenses/agpl-3.0.en.html",
|
|
|
|
},
|
|
|
|
version: "1.0.0",
|
|
|
|
},
|
|
|
|
externalDocs: {
|
2023-04-13 21:48:27 +02:00
|
|
|
description: "Spacebar Docs",
|
|
|
|
url: "https://docs.spacebar.chat",
|
2023-03-24 23:14:47 +01:00
|
|
|
},
|
|
|
|
servers: [
|
|
|
|
{
|
2023-04-13 21:48:27 +02:00
|
|
|
url: "https://old.server.spacebar.chat/api/",
|
|
|
|
description: "Official Spacebar Instance",
|
2023-03-24 23:14:47 +01:00
|
|
|
},
|
|
|
|
],
|
|
|
|
components: {
|
|
|
|
securitySchemes: {
|
|
|
|
bearer: {
|
|
|
|
type: "http",
|
|
|
|
scheme: "bearer",
|
|
|
|
description: "Bearer/Bot prefixes are not required.",
|
|
|
|
bearerFormat: "JWT",
|
|
|
|
in: "header",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
tags: [],
|
|
|
|
paths: {},
|
|
|
|
};
|
2023-01-06 09:30:03 +01:00
|
|
|
|
|
|
|
function combineSchemas(schemas) {
|
|
|
|
var definitions = {};
|
|
|
|
|
|
|
|
for (const name in schemas) {
|
|
|
|
definitions = {
|
|
|
|
...definitions,
|
|
|
|
...schemas[name].definitions,
|
|
|
|
[name]: {
|
|
|
|
...schemas[name],
|
|
|
|
definitions: undefined,
|
|
|
|
$schema: undefined,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const key in definitions) {
|
2023-04-14 07:17:30 +02:00
|
|
|
const reg = new RegExp(/^[a-zA-Z0-9.\-_]+$/, "gm");
|
2023-03-24 23:14:47 +01:00
|
|
|
if (!reg.test(key)) {
|
|
|
|
console.error(`Invalid schema name: ${key} (${reg.test(key)})`);
|
|
|
|
continue;
|
|
|
|
}
|
2023-01-06 09:30:03 +01:00
|
|
|
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() {
|
|
|
|
const routes = getRouteDescriptions();
|
|
|
|
|
2023-03-24 23:14:47 +01:00
|
|
|
// 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 }));
|
2023-01-06 09:30:03 +01:00
|
|
|
|
|
|
|
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;
|
2023-03-24 23:14:47 +01:00
|
|
|
obj["x-fires-event"] = route.event;
|
2023-01-06 09:30:03 +01:00
|
|
|
|
|
|
|
if (
|
|
|
|
!NO_AUTHORIZATION_ROUTES.some((x) => {
|
|
|
|
if (typeof x === "string") return path.startsWith(x);
|
|
|
|
return x.test(path);
|
|
|
|
})
|
|
|
|
) {
|
2023-03-23 18:16:58 +01:00
|
|
|
obj.security = [{ bearer: [] }];
|
2023-01-06 09:30:03 +01:00
|
|
|
}
|
|
|
|
|
2023-03-24 23:14:47 +01:00
|
|
|
if (route.description) obj.description = route.description;
|
|
|
|
if (route.summary) obj.summary = route.summary;
|
2023-03-25 21:09:04 +01:00
|
|
|
if (route.deprecated) obj.deprecated = route.deprecated;
|
2023-03-24 23:14:47 +01:00
|
|
|
|
|
|
|
if (route.requestBody) {
|
2023-01-06 09:30:03 +01:00
|
|
|
obj.requestBody = {
|
|
|
|
required: true,
|
|
|
|
content: {
|
|
|
|
"application/json": {
|
2023-03-24 23:14:47 +01:00
|
|
|
schema: {
|
|
|
|
$ref: `#/components/schemas/${route.requestBody}`,
|
|
|
|
},
|
2023-01-06 09:30:03 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}.merge(obj.requestBody);
|
|
|
|
}
|
|
|
|
|
2023-03-23 15:40:37 +01:00
|
|
|
if (route.responses) {
|
|
|
|
for (const [k, v] of Object.entries(route.responses)) {
|
|
|
|
let schema = {
|
2023-03-24 23:14:47 +01:00
|
|
|
$ref: `#/components/schemas/${v.body}`,
|
2023-03-23 15:40:37 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
obj.responses = {
|
|
|
|
[k]: {
|
|
|
|
...(v.body
|
|
|
|
? {
|
|
|
|
description:
|
|
|
|
obj?.responses?.[k]?.description || "",
|
|
|
|
content: {
|
|
|
|
"application/json": {
|
|
|
|
schema: schema,
|
|
|
|
},
|
2023-01-06 09:30:03 +01:00
|
|
|
},
|
2023-03-23 15:40:37 +01:00
|
|
|
}
|
2023-03-24 23:14:47 +01:00
|
|
|
: {
|
|
|
|
description: "No description available",
|
|
|
|
}),
|
2023-03-23 15:40:37 +01:00
|
|
|
},
|
|
|
|
}.merge(obj.responses);
|
|
|
|
}
|
2023-03-24 23:28:32 +01:00
|
|
|
} else {
|
|
|
|
obj.responses = {
|
|
|
|
default: {
|
|
|
|
description: "No description available",
|
|
|
|
},
|
|
|
|
};
|
2023-01-06 09:30:03 +01:00
|
|
|
}
|
2023-03-24 23:14:47 +01:00
|
|
|
|
|
|
|
// handles path parameters
|
2023-01-06 09:30:03 +01:00
|
|
|
if (p.includes(":")) {
|
|
|
|
obj.parameters = p.match(/:\w+/g)?.map((x) => ({
|
|
|
|
name: x.replace(":", ""),
|
|
|
|
in: "path",
|
|
|
|
required: true,
|
|
|
|
schema: { type: "string" },
|
|
|
|
description: x.replace(":", ""),
|
|
|
|
}));
|
|
|
|
}
|
2023-03-24 23:14:47 +01:00
|
|
|
|
2023-03-24 23:57:44 +01:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
|
2023-01-06 09:30:03 +01:00
|
|
|
obj.tags = [...(obj.tags || []), getTag(p)].unique();
|
|
|
|
|
2023-04-14 07:17:30 +02:00
|
|
|
specification.paths[path] = Object.assign(
|
|
|
|
specification.paths[path] || {},
|
|
|
|
{
|
|
|
|
[method]: obj,
|
|
|
|
},
|
|
|
|
);
|
2023-01-06 09:30:03 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function main() {
|
2023-03-24 23:14:47 +01:00
|
|
|
console.log("Generating OpenAPI Specification...");
|
2023-01-06 09:30:03 +01:00
|
|
|
combineSchemas(schemas);
|
|
|
|
apiRoutes();
|
|
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
openapiPath,
|
|
|
|
JSON.stringify(specification, null, 4)
|
|
|
|
.replaceAll("#/definitions", "#/components/schemas")
|
|
|
|
.replaceAll("bigint", "number"),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
main();
|