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;
};
|