summary refs log tree commit diff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/benchmark/connections.js82
-rw-r--r--scripts/changelog.js56
-rw-r--r--scripts/openapi.js186
-rw-r--r--scripts/schema.js48
-rw-r--r--scripts/stress/identify.js52
-rw-r--r--scripts/stress/login.js17
-rw-r--r--scripts/stress/users.js (renamed from scripts/benchmark/users.js)0
-rw-r--r--scripts/test.js1
-rw-r--r--scripts/util/getRouteDescriptions.js98
9 files changed, 230 insertions, 310 deletions
diff --git a/scripts/benchmark/connections.js b/scripts/benchmark/connections.js
deleted file mode 100644

index 4246c646..00000000 --- a/scripts/benchmark/connections.js +++ /dev/null
@@ -1,82 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - 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/>. -*/ - -require("dotenv").config(); -const cluster = require("cluster"); -const WebSocket = require("ws"); -const endpoint = process.env.GATEWAY || "ws://localhost:3001"; -const connections = Number(process.env.CONNECTIONS) || 50; -const token = process.env.TOKEN; -var cores = 1; -try { - cores = Number(process.env.THREADS) || os.cpus().length; -} catch { - console.log("[Bundle] Failed to get thread count! Using 1..."); -} - -if (!token) { - console.error("TOKEN env var missing"); - process.exit(); -} - -if (cluster.isMaster) { - for (let i = 0; i < cores; i++) { - cluster.fork(); - } - - cluster.on("exit", (worker, code, signal) => { - console.log(`worker ${worker.process.pid} died`); - }); -} else { - for (let i = 0; i < connections; i++) { - connect(); - } -} - -function connect() { - const client = new WebSocket(endpoint); - client.on("message", (data) => { - data = JSON.parse(data); - - switch (data.op) { - case 10: - client.interval = setInterval(() => { - client.send(JSON.stringify({ op: 1 })); - }, data.d.heartbeat_interval); - - client.send( - JSON.stringify({ - op: 2, - d: { - token, - properties: {}, - }, - }), - ); - - break; - } - }); - client.once("close", (code, reason) => { - clearInterval(client.interval); - connect(); - }); - client.on("error", (err) => { - // console.log(err); - }); -} diff --git a/scripts/changelog.js b/scripts/changelog.js deleted file mode 100644
index 658193ba..00000000 --- a/scripts/changelog.js +++ /dev/null
@@ -1,56 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - 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/>. -*/ - -/* - Changelogs are baked inside the discord.com web client. - To change them, we simply need to update the changelog in a specific file of the client. - For v134842, thats 9c4b2d313c6e1c864e89.js, but it'll be different for every version. - To find which file the changelog is stored in your client, simply grep for the changelog text given by the client, - and update the `CHANGELOG_SCRIPT` variable to use that instead. - - This grabs the new changelog from `spacebarchat/server/assets/changelog.txt` -*/ - -const fetch = require("node-fetch"); -const fs = require("fs/promises"); -const path = require("path"); - -const CACHE_PATH = path.join(__dirname, "..", "assets", "cache"); -const CHANGELOG_PATH = path.join(__dirname, "..", "assets", "changelog.txt"); -const BASE_URL = "https://discord.com"; - -const CHANGELOG_SCRIPT = "4ec0b5948572d31df88b.js"; - -(async () => { - const res = await fetch(`${BASE_URL}/assets/${CHANGELOG_SCRIPT}`); - const text = await res.text(); - - const newChangelogText = (await fs.readFile(CHANGELOG_PATH)) - .toString() - .replaceAll("\r", "") - .replaceAll("\n", "\\n") - .replaceAll("'", "\\'"); - - const index = text.indexOf("t.exports='---changelog---") + 11; - const endIndex = text.indexOf("'\n", index); // hmm - - await fs.writeFile( - path.join(CACHE_PATH, CHANGELOG_SCRIPT), - text.substring(0, index) + newChangelogText + text.substring(endIndex), - ); -})(); diff --git a/scripts/openapi.js b/scripts/openapi.js
index 49d5dfde..8258a76c 100644 --- a/scripts/openapi.js +++ b/scripts/openapi.js
@@ -27,34 +27,46 @@ require("missing-native-js-functions"); const openapiPath = path.join(__dirname, "..", "assets", "openapi.json"); const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json"); -let schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); - -for (var schema in schemas) { - const part = schemas[schema]; - for (var key in part.properties) { - if (part.properties[key].anyOf) { - const nullIndex = part.properties[key].anyOf.findIndex( - (x) => x.type == "null", - ); - if (nullIndex != -1) { - part.properties[key].nullable = true; - part.properties[key].anyOf.splice(nullIndex, 1); - - if (part.properties[key].anyOf.length == 1) { - Object.assign( - part.properties[key], - part.properties[key].anyOf[0], - ); - delete part.properties[key].anyOf; - } - } - } - } -} - -const specification = JSON.parse( - fs.readFileSync(openapiPath, { encoding: "utf8" }), -); +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: { + title: "Spacebar Server", + description: + "Spacebar is a free open source selfhostable discord compatible chat, voice and video platform", + license: { + name: "AGPLV3", + url: "https://www.gnu.org/licenses/agpl-3.0.en.html", + }, + version: "1.0.0", + }, + externalDocs: { + description: "Spacebar Docs", + url: "https://docs.spacebar.chat", + }, + servers: [ + { + url: "https://old.server.spacebar.chat/api/", + description: "Official Spacebar Instance", + }, + ], + components: { + securitySchemes: { + bearer: { + type: "http", + scheme: "bearer", + description: "Bearer/Bot prefixes are not required.", + bearerFormat: "JWT", + in: "header", + }, + }, + }, + tags: [], + paths: {}, +}; function combineSchemas(schemas) { var definitions = {}; @@ -72,6 +84,11 @@ function combineSchemas(schemas) { } for (const key in definitions) { + const reg = new RegExp(/^[a-zA-Z0-9.\-_]+$/, "gm"); + if (!reg.test(key)) { + console.error(`Invalid schema name: ${key} (${reg.test(key)})`); + continue; + } specification.components = specification.components || {}; specification.components.schemas = specification.components.schemas || {}; @@ -102,30 +119,20 @@ function getTag(key) { function apiRoutes() { const routes = getRouteDescriptions(); - const tags = Array.from(routes.keys()).map((x) => getTag(x)); - specification.tags = specification.tags || []; - specification.tags = [...specification.tags.map((x) => x.name), ...tags] - .unique() - .map((x) => ({ name: x })); - - specification.components = specification.components || {}; - specification.components.securitySchemes = { - bearer: { - type: "http", - scheme: "bearer", - description: "Bearer/Bot prefixes are not required.", - }, - }; + // 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}"); - specification.paths = specification.paths || {}; let obj = specification.paths[path]?.[method] || {}; obj["x-right-required"] = route.right; obj["x-permission-required"] = route.permission; - obj["x-fires-event"] = route.test?.event; + obj["x-fires-event"] = route.event; if ( !NO_AUTHORIZATION_ROUTES.some((x) => { @@ -136,48 +143,56 @@ function apiRoutes() { obj.security = [{ bearer: [] }]; } - if (route.body) { + 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.body}` }, + schema: { + $ref: `#/components/schemas/${route.requestBody}`, + }, }, }, }.merge(obj.requestBody); } - if (route.test?.response) { - const status = route.test.response.status || 200; - let schema = { - allOf: [ - { - $ref: `#/components/schemas/${route.test.response.body}`, - }, - { - example: route.test.body, + if (route.responses) { + for (const [k, v] of Object.entries(route.responses)) { + let schema = { + $ref: `#/components/schemas/${v.body}`, + }; + + obj.responses = { + [k]: { + ...(v.body + ? { + description: + obj?.responses?.[k]?.description || "", + content: { + "application/json": { + schema: schema, + }, + }, + } + : { + description: "No description available", + }), }, - ], - }; - if (!route.test.body) schema = schema.allOf[0]; - + }.merge(obj.responses); + } + } else { obj.responses = { - [status]: { - ...(route.test.response.body - ? { - description: - obj?.responses?.[status]?.description || "", - content: { - "application/json": { - schema: schema, - }, - }, - } - : {}), + default: { + description: "No description available", }, - }.merge(obj.responses); - delete obj.responses.default; + }; } + + // handles path parameters if (p.includes(":")) { obj.parameters = p.match(/:\w+/g)?.map((x) => ({ name: x.replace(":", ""), @@ -187,16 +202,33 @@ function apiRoutes() { 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(); - specification.paths[path] = { - ...specification.paths[path], - [method]: obj, - }; + specification.paths[path] = Object.assign( + specification.paths[path] || {}, + { + [method]: obj, + }, + ); }); } function main() { + console.log("Generating OpenAPI Specification..."); combineSchemas(schemas); apiRoutes(); diff --git a/scripts/schema.js b/scripts/schema.js
index c29d5bab..ff3280ac 100644 --- a/scripts/schema.js +++ b/scripts/schema.js
@@ -34,9 +34,7 @@ const settings = { noExtraProps: true, defaultProps: false, }; -const compilerOptions = { - strictNullChecks: true, -}; + const Excluded = [ "DefaultSchema", "Schema", @@ -57,16 +55,10 @@ const Excluded = [ "PropertiesSchema", "AsyncSchema", "AnySchema", + "SMTPConnection.CustomAuthenticationResponse", + "TransportMakeRequestResponse", ]; -function modify(obj) { - for (var k in obj) { - if (typeof obj[k] === "object" && obj[k] !== null) { - modify(obj[k]); - } - } -} - function main() { const program = TJS.programFromConfig( path.join(__dirname, "..", "tsconfig.json"), @@ -75,14 +67,14 @@ function main() { const generator = TJS.buildGenerator(program, settings); if (!generator || !program) return; - let schemas = generator - .getUserSymbols() - .filter( - (x) => - (x.endsWith("Schema") || x.endsWith("Response")) && - !Excluded.includes(x), + let schemas = generator.getUserSymbols().filter((x) => { + return ( + (x.endsWith("Schema") || + x.endsWith("Response") || + x.startsWith("API")) && + !Excluded.includes(x) ); - console.log(schemas); + }); var definitions = {}; @@ -109,32 +101,12 @@ function main() { delete part.properties[key]; continue; } - - // if (part.properties[key].anyOf) { - // const nullIndex = part.properties[key].anyOf.findIndex( - // (x) => x.type == "null", - // ); - // if (nullIndex != -1) { - // part.properties[key].nullable = true; - // part.properties[key].anyOf.splice(nullIndex, 1); - - // if (part.properties[key].anyOf.length == 1) { - // Object.assign( - // part.properties[key], - // part.properties[key].anyOf[0], - // ); - // delete part.properties[key].anyOf; - // } - // } - // } } } definitions = { ...definitions, [name]: { ...part } }; } - modify(definitions); - fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4)); } diff --git a/scripts/stress/identify.js b/scripts/stress/identify.js new file mode 100644
index 00000000..2a271bbc --- /dev/null +++ b/scripts/stress/identify.js
@@ -0,0 +1,52 @@ +/* eslint-env node */ + +require("dotenv").config(); +const { OPCODES } = require("../../dist/gateway/util/Constants.js"); +const WebSocket = require("ws"); +const ENDPOINT = `ws://localhost:3001?v=9&encoding=json`; +const TOKEN = process.env.TOKEN; +const TOTAL_ITERATIONS = process.env.ITER ? parseInt(process.env.ITER) : 500; + +const doTimedIdentify = () => + new Promise((resolve) => { + let start; + const ws = new WebSocket(ENDPOINT); + ws.on("message", (data) => { + const parsed = JSON.parse(data); + + switch (parsed.op) { + case OPCODES.Hello: + // send identify + start = performance.now(); + ws.send( + JSON.stringify({ + op: OPCODES.Identify, + d: { + token: TOKEN, + properties: {}, + }, + }), + ); + break; + case OPCODES.Dispatch: + if (parsed.t == "READY") { + ws.close(); + return resolve(performance.now() - start); + } + + break; + } + }); + }); + +(async () => { + const perfs = []; + while (perfs.length < TOTAL_ITERATIONS) { + const ret = await doTimedIdentify(); + perfs.push(ret); + // console.log(`${perfs.length}/${TOTAL_ITERATIONS} - this: ${Math.floor(ret)}ms`) + } + + const avg = perfs.reduce((prev, curr) => prev + curr) / (perfs.length - 1); + console.log(`Average identify time: ${Math.floor(avg * 100) / 100}ms`); +})(); diff --git a/scripts/stress/login.js b/scripts/stress/login.js new file mode 100644
index 00000000..473e2e95 --- /dev/null +++ b/scripts/stress/login.js
@@ -0,0 +1,17 @@ +const fetch = require("node-fetch"); +const ENDPOINT = process.env.API || "http://localhost:3001"; + +async function main() { + const ret = await fetch(`${ENDPOINT}/api/auth/login`, { + method: "POST", + body: JSON.stringify({ + login: process.argv[2], + password: process.argv[3], + }), + headers: { "content-type": "application/json " }, + }); + + console.log((await ret.json()).token); +} + +main(); diff --git a/scripts/benchmark/users.js b/scripts/stress/users.js
index 20f9f7c3..20f9f7c3 100644 --- a/scripts/benchmark/users.js +++ b/scripts/stress/users.js
diff --git a/scripts/test.js b/scripts/test.js
index 28ac3778..69e9fdd6 100644 --- a/scripts/test.js +++ b/scripts/test.js
@@ -34,6 +34,7 @@ server.stdout.on("data", (data) => { if (data.toString().toLowerCase().includes("listening")) { // we good :) console.log("we good"); + server.kill(); process.exit(); } }); diff --git a/scripts/util/getRouteDescriptions.js b/scripts/util/getRouteDescriptions.js
index fe36c238..a79dac96 100644 --- a/scripts/util/getRouteDescriptions.js +++ b/scripts/util/getRouteDescriptions.js
@@ -1,80 +1,64 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - 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/>. -*/ - -const { traverseDirectory } = require("lambert-server"); -const path = require("path"); const express = require("express"); +const path = require("path"); +const { traverseDirectory } = require("lambert-server"); const RouteUtility = require("../../dist/api/util/handlers/route.js"); -const Router = express.Router; +const methods = ["get", "post", "put", "delete", "patch"]; const routes = new Map(); -let currentPath = ""; let currentFile = ""; -const methods = ["get", "post", "put", "delete", "patch"]; - -function registerPath(file, method, prefix, path, ...args) { - const urlPath = prefix + path; - const sourceFile = file.replace("/dist/", "/src/").replace(".js", ".ts"); - const opts = args.find((x) => typeof x === "object"); - if (opts) { - routes.set(urlPath + "|" + method, opts); - opts.file = sourceFile; - // console.log(method, urlPath, opts); - } else { - console.log( - `${sourceFile}\nrouter.${method}("${path}") is missing the "route()" description middleware\n`, - ); - } -} +let currentPath = ""; -function routeOptions(opts) { - return opts; -} +/* + For some reason, if a route exports multiple functions, it won't be registered here! + If someone could fix that I'd really appreciate it, but for now just, don't do that :p +*/ -RouteUtility.route = routeOptions; +const proxy = (file, method, prefix, path, ...args) => { + const opts = args.find((x) => x?.prototype?.OPTS_MARKER == true); + if (!opts) + return console.error( + `${file} has route without route() description middleware`, + ); -express.Router = (opts) => { - const path = currentPath; - const file = currentFile; - const router = Router(opts); + console.log(prefix + path + " - " + method); + opts.file = file.replace("/dist/", "/src/").replace(".js", ".ts"); + routes.set(prefix + path + "|" + method, opts()); +}; - for (const method of methods) { - router[method] = registerPath.bind(null, file, method, path); - } +express.Router = () => { + return Object.fromEntries( + methods.map((method) => [ + method, + proxy.bind(null, currentFile, method, currentPath), + ]), + ); +}; - return router; +RouteUtility.route = (opts) => { + const func = function () { + return opts; + }; + func.prototype.OPTS_MARKER = true; + return func; }; module.exports = function getRouteDescriptions() { const root = path.join(__dirname, "..", "..", "dist", "api", "routes", "/"); traverseDirectory({ dirname: root, recursive: true }, (file) => { currentFile = file; - let path = file.replace(root.slice(0, -1), ""); - path = path.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path - path = path.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes - if (path.endsWith("/index")) path = path.slice(0, "/index".length * -1); // delete index from path - currentPath = path; + + currentPath = file.replace(root.slice(0, -1), ""); + currentPath = currentPath.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path + currentPath = currentPath.replaceAll("#", ":").replaceAll("\\", "/"); // replace # with : for path parameters and windows paths with slashes + if (currentPath.endsWith("/index")) + currentPath = currentPath.slice(0, "/index".length * -1); // delete index from path try { require(file); - } catch (error) { - console.error("error loading file " + file, error); + } catch (e) { + console.error(e); } }); + return routes; };