/* ValkyrieChat: A re-implementation and extension of the Discord.com backend. Copyright (C) 2024 ValkyrieChat Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ 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", url: "http://www.apache.org/licenses/LICENSE-2.0.html", }, 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();