Merge pull request #372 from fosscord/unittests
Automatic Unittests + documentation
This commit is contained in:
commit
5823f8efb4
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6
api/babel.config.js
Normal file
6
api/babel.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
["@babel/preset-typescript", { allowDeclareFields: true }]
|
||||
]
|
||||
};
|
||||
66
api/jest/getRouteDescriptions.js
Normal file
66
api/jest/getRouteDescriptions.js
Normal file
@ -0,0 +1,66 @@
|
||||
const { traverseDirectory } = require("lambert-server");
|
||||
const path = require("path");
|
||||
const express = require("express");
|
||||
const RouteUtility = require("../dist/util/route");
|
||||
const Router = express.Router;
|
||||
|
||||
/**
|
||||
* Some documentation.
|
||||
*
|
||||
* @type {Map<string, RouteUtility.RouteOptions>}
|
||||
*/
|
||||
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); // @ts-ignore
|
||||
opts.file = sourceFile;
|
||||
// console.log(method, urlPath, opts);
|
||||
} else {
|
||||
console.log(`${sourceFile}\nrouter.${method}("${path}") is missing the "route()" description middleware\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function routeOptions(opts) {
|
||||
return opts;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
RouteUtility.route = routeOptions;
|
||||
|
||||
express.Router = (opts) => {
|
||||
const path = currentPath;
|
||||
const file = currentFile;
|
||||
const router = Router(opts);
|
||||
|
||||
for (const method of methods) {
|
||||
router[method] = registerPath.bind(null, file, method, path);
|
||||
}
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
module.exports = function getRouteDescriptions() {
|
||||
const root = path.join(__dirname, "..", "dist", "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;
|
||||
|
||||
try {
|
||||
require(file);
|
||||
} catch (error) {
|
||||
console.error("error loading file " + file, error);
|
||||
}
|
||||
});
|
||||
return routes;
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
const { Config, initDatabase } = require("@fosscord/util");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { FosscordServer } = require("../dist/Server");
|
||||
@ -5,8 +6,12 @@ const Server = new FosscordServer({ port: 3001 });
|
||||
global.server = Server;
|
||||
module.exports = async () => {
|
||||
try {
|
||||
fs.unlinkSync(path.join(__dirname, "..", "database.db"));
|
||||
fs.unlinkSync(path.join(process.cwd(), "database.db"));
|
||||
} catch {}
|
||||
|
||||
await initDatabase();
|
||||
await Config.init();
|
||||
Config.get().limits.rate.disabled = true;
|
||||
return await Server.start();
|
||||
};
|
||||
|
||||
BIN
api/package-lock.json
generated
BIN
api/package-lock.json
generated
Binary file not shown.
@ -5,17 +5,17 @@
|
||||
"main": "dist/Server.js",
|
||||
"types": "dist/Server.d.ts",
|
||||
"scripts": {
|
||||
"test:only": "node -r ./scripts/tsconfig-paths-bootstrap.js node_modules/.bin/jest --coverage --verbose --forceExit ./tests",
|
||||
"test:only": "jest --coverage --verbose --forceExit ./tests",
|
||||
"test": "npm run build && npm run test:only",
|
||||
"test:watch": "jest --watch",
|
||||
"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start",
|
||||
"start": "npm run build && node dist/start",
|
||||
"build": "npx tsc -b .",
|
||||
"build-docker": "tsc -p tsconfig-docker.json",
|
||||
"dev": "tsnd --respawn src/start.ts",
|
||||
"patch": "npx patch-package",
|
||||
"patch": "ts-patch install -s && npx patch-package",
|
||||
"postinstall": "npm run patch",
|
||||
"generate:docs": "ts-node scripts/generate_openapi_schema.ts",
|
||||
"generate:schema": "ts-node scripts/generate_body_schema.ts"
|
||||
"generate:docs": "node scripts/generate_openapi.ts",
|
||||
"generate:schema": "node scripts/generate_schema.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -36,11 +36,15 @@
|
||||
},
|
||||
"homepage": "https://fosscord.com",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/amqplib": "^0.8.1",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/i18next-node-fs-backend": "^2.1.0",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/jest-expect-message": "^1.0.3",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/mongodb": "^3.6.9",
|
||||
"@types/mongoose": "^5.10.5",
|
||||
@ -49,14 +53,19 @@
|
||||
"@types/multer": "^1.4.5",
|
||||
"@types/node": "^14.17.9",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
"0x": "^4.10.2",
|
||||
"babel-jest": "^27.2.0",
|
||||
"caxa": "^2.1.0",
|
||||
"image-size": "^1.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"jest-runtime": "^27.2.1",
|
||||
"saslprep": "^1.0.3",
|
||||
"ts-node": "^9.1.1",
|
||||
"ts-node-dev": "^1.1.6",
|
||||
"ts-patch": "^1.4.4",
|
||||
"typescript": "^4.4.2",
|
||||
"typescript-json-schema": "0.50.1"
|
||||
},
|
||||
@ -77,7 +86,7 @@
|
||||
"express": "^4.17.1",
|
||||
"express-validator": "^6.9.2",
|
||||
"form-data": "^3.0.0",
|
||||
"i18next": "^19.8.5",
|
||||
"i18next": "^19.9.2",
|
||||
"i18next-http-middleware": "^3.1.3",
|
||||
"i18next-node-fs-backend": "^2.1.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
@ -98,7 +107,10 @@
|
||||
"setupFiles": [
|
||||
"<rootDir>/jest/setup.js"
|
||||
],
|
||||
"globalSetup": "<rootDir>/scripts/globalSetup.js",
|
||||
"setupFilesAfterEnv": [
|
||||
"jest-expect-message"
|
||||
],
|
||||
"globalSetup": "<rootDir>/jest/globalSetup.js",
|
||||
"verbose": true
|
||||
}
|
||||
}
|
||||
|
||||
137
api/scripts/generate_openapi.js
Normal file
137
api/scripts/generate_openapi.js
Normal file
@ -0,0 +1,137 @@
|
||||
// https://mermade.github.io/openapi-gui/#
|
||||
// https://editor.swagger.io/
|
||||
const getRouteDescriptions = require("../jest/getRouteDescriptions");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
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" }));
|
||||
const specification = JSON.parse(fs.readFileSync(openapiPath, { encoding: "utf8" }));
|
||||
|
||||
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) {
|
||||
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();
|
||||
|
||||
const tags = Array.from(routes.keys()).map((x) => getTag(x));
|
||||
specification.tags = [...specification.tags.map((x) => x.name), ...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] || {};
|
||||
if (!obj.description) {
|
||||
const permission = route.permission ? `##### Requires the \`\`${route.permission}\`\` permission\n` : "";
|
||||
const event = route.test?.event ? `##### Fires a \`\`${route.test?.event}\`\` event\n` : "";
|
||||
obj.description = permission + event;
|
||||
}
|
||||
if (route.body) {
|
||||
obj.requestBody = {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: `#/components/schemas/${route.body}` }
|
||||
}
|
||||
}
|
||||
}.merge(obj.requestBody);
|
||||
}
|
||||
if (!obj.responses) {
|
||||
obj.responses = {
|
||||
default: {
|
||||
description: "not documented"
|
||||
}
|
||||
};
|
||||
}
|
||||
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.test.body) schema = schema.allOf[0];
|
||||
|
||||
obj.responses = {
|
||||
[status]: {
|
||||
...(route.test.response.body
|
||||
? {
|
||||
description: obj.responses[status].description || "",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schema
|
||||
}
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}.merge(obj.responses);
|
||||
delete obj.responses.default;
|
||||
}
|
||||
if (p.includes(":")) {
|
||||
obj.parameters = p.match(/:\w+/g)?.map((x) => ({
|
||||
name: x.replace(":", ""),
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: x.replace(":", "")
|
||||
}));
|
||||
}
|
||||
obj.tags = [...(obj.tags || []), getTag(p)].unique();
|
||||
|
||||
specification.paths[path] = { ...specification.paths[path], [method]: obj };
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
combineSchemas(schemas);
|
||||
apiRoutes();
|
||||
|
||||
fs.writeFileSync(
|
||||
openapiPath,
|
||||
JSON.stringify(specification, null, 4).replaceAll("#/definitions", "#/components/schemas").replaceAll("bigint", "number")
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
@ -1,99 +0,0 @@
|
||||
// https://mermade.github.io/openapi-gui/#
|
||||
// https://editor.swagger.io/
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import * as TJS from "typescript-json-schema";
|
||||
import "missing-native-js-functions";
|
||||
|
||||
const settings: TJS.PartialArgs = {
|
||||
required: true,
|
||||
ignoreErrors: true,
|
||||
excludePrivate: true,
|
||||
defaultNumberType: "integer",
|
||||
noExtraProps: true,
|
||||
defaultProps: false
|
||||
};
|
||||
const compilerOptions: TJS.CompilerOptions = {
|
||||
strictNullChecks: false
|
||||
};
|
||||
const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
|
||||
var specification = JSON.parse(fs.readFileSync(openapiPath, { encoding: "utf8" }));
|
||||
|
||||
async function utilSchemas() {
|
||||
const program = TJS.getProgramFromFiles([path.join(__dirname, "..", "..", "util", "src", "index.ts")], compilerOptions);
|
||||
const generator = TJS.buildGenerator(program, settings);
|
||||
|
||||
const schemas = ["UserPublic", "UserPrivate", "PublicConnectedAccount"];
|
||||
|
||||
// @ts-ignore
|
||||
combineSchemas({ schemas, generator, program });
|
||||
}
|
||||
|
||||
function combineSchemas(opts: { program: TJS.Program; generator: TJS.JsonSchemaGenerator; schemas: string[] }) {
|
||||
var definitions: any = {};
|
||||
|
||||
for (const name of opts.schemas) {
|
||||
const part = TJS.generateSchema(opts.program, name, settings, [], opts.generator as TJS.JsonSchemaGenerator);
|
||||
if (!part) continue;
|
||||
|
||||
definitions = { ...definitions, [name]: { ...part, definitions: undefined, $schema: undefined } };
|
||||
}
|
||||
|
||||
for (const key in definitions) {
|
||||
specification.components.schemas[key] = definitions[key];
|
||||
delete definitions[key].additionalProperties;
|
||||
delete definitions[key].$schema;
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function apiSchemas() {
|
||||
const program = TJS.getProgramFromFiles([path.join(__dirname, "..", "src", "schema", "index.ts")], compilerOptions);
|
||||
const generator = TJS.buildGenerator(program, settings);
|
||||
|
||||
const schemas = [
|
||||
"BanCreateSchema",
|
||||
"DmChannelCreateSchema",
|
||||
"ChannelModifySchema",
|
||||
"ChannelGuildPositionUpdateSchema",
|
||||
"ChannelGuildPositionUpdateSchema",
|
||||
"EmojiCreateSchema",
|
||||
"GuildCreateSchema",
|
||||
"GuildUpdateSchema",
|
||||
"GuildTemplateCreateSchema",
|
||||
"GuildUpdateWelcomeScreenSchema",
|
||||
"InviteCreateSchema",
|
||||
"MemberCreateSchema",
|
||||
"MemberNickChangeSchema",
|
||||
"MemberChangeSchema",
|
||||
"MessageCreateSchema",
|
||||
"RoleModifySchema",
|
||||
"TemplateCreateSchema",
|
||||
"TemplateModifySchema",
|
||||
"UserModifySchema",
|
||||
"UserSettingsSchema",
|
||||
"WidgetModifySchema",
|
||||
""
|
||||
];
|
||||
|
||||
// @ts-ignore
|
||||
combineSchemas({ schemas, generator, program });
|
||||
}
|
||||
|
||||
function addDefaultResponses() {
|
||||
Object.values(specification.paths).forEach((path: any) => Object.values(path).forEach((request: any) => {}));
|
||||
}
|
||||
|
||||
function main() {
|
||||
addDefaultResponses();
|
||||
utilSchemas();
|
||||
apiSchemas();
|
||||
|
||||
fs.writeFileSync(
|
||||
openapiPath,
|
||||
JSON.stringify(specification, null, 4).replaceAll("#/definitions", "#/components/schemas").replaceAll("bigint", "number")
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
@ -6,7 +6,7 @@ import * as TJS from "typescript-json-schema";
|
||||
import "missing-native-js-functions";
|
||||
const schemaPath = path.join(__dirname, "..", "assets", "schemas.json");
|
||||
|
||||
const settings: TJS.PartialArgs = {
|
||||
const settings = {
|
||||
required: true,
|
||||
ignoreErrors: true,
|
||||
excludePrivate: true,
|
||||
@ -14,23 +14,34 @@ const settings: TJS.PartialArgs = {
|
||||
noExtraProps: true,
|
||||
defaultProps: false
|
||||
};
|
||||
const compilerOptions: TJS.CompilerOptions = {
|
||||
const compilerOptions = {
|
||||
strictNullChecks: true
|
||||
};
|
||||
const ExcludedSchemas = ["DefaultSchema", "Schema", "EntitySchema"];
|
||||
const Excluded = [
|
||||
"DefaultSchema",
|
||||
"Schema",
|
||||
"EntitySchema",
|
||||
"ServerResponse",
|
||||
"Http2ServerResponse",
|
||||
"global.Express.Response",
|
||||
"Response",
|
||||
"e.Response",
|
||||
"request.Response",
|
||||
"supertest.Response"
|
||||
];
|
||||
|
||||
function main() {
|
||||
const program = TJS.getProgramFromFiles(walk(path.join(__dirname, "..", "src", "routes")), compilerOptions);
|
||||
const generator = TJS.buildGenerator(program, settings);
|
||||
if (!generator || !program) return;
|
||||
|
||||
const schemas = generator.getUserSymbols().filter((x) => x.endsWith("Schema") && !ExcludedSchemas.includes(x));
|
||||
const schemas = generator.getUserSymbols().filter((x) => (x.endsWith("Schema") || x.endsWith("Response")) && !Excluded.includes(x));
|
||||
console.log(schemas);
|
||||
|
||||
var definitions: any = {};
|
||||
var definitions = {};
|
||||
|
||||
for (const name of schemas) {
|
||||
const part = TJS.generateSchema(program, name, settings, [], generator as TJS.JsonSchemaGenerator);
|
||||
const part = TJS.generateSchema(program, name, settings, [], generator);
|
||||
if (!part) continue;
|
||||
|
||||
definitions = { ...definitions, [name]: { ...part } };
|
||||
@ -39,11 +50,10 @@ function main() {
|
||||
fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4));
|
||||
}
|
||||
|
||||
// #/definitions/
|
||||
main();
|
||||
|
||||
function walk(dir: string) {
|
||||
var results = [] as string[];
|
||||
function walk(dir) {
|
||||
var results = [];
|
||||
var list = fs.readdirSync(dir);
|
||||
list.forEach(function (file) {
|
||||
file = dir + "/" + file;
|
||||
@ -1,10 +0,0 @@
|
||||
const tsConfigPaths = require("tsconfig-paths");
|
||||
const path = require("path");
|
||||
|
||||
const cleanup = tsConfigPaths.register({
|
||||
baseUrl: path.join(__dirname, ".."),
|
||||
paths: {
|
||||
"@fosscord/api": ["dist/index.js"],
|
||||
"@fosscord/api/*": ["dist/*"]
|
||||
}
|
||||
});
|
||||
@ -6,6 +6,8 @@ export function BodyParser(opts?: OptionsJson) {
|
||||
const jsonParser = bodyParser.json(opts);
|
||||
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.headers["content-type"]) req.headers["content-type"] = "application/json";
|
||||
|
||||
jsonParser(req, res, (err) => {
|
||||
if (err) {
|
||||
// TODO: different errors for body parser (request size limit, wrong body type, invalid body, ...)
|
||||
|
||||
@ -3,6 +3,7 @@ import { HTTPError } from "lambert-server";
|
||||
import { EntityNotFoundError } from "typeorm";
|
||||
import { FieldError } from "@fosscord/api";
|
||||
import { ApiError } from "@fosscord/util";
|
||||
const EntityNotFoundErrorRegex = /"(\w+)"/;
|
||||
|
||||
export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) {
|
||||
if (!error) return next();
|
||||
@ -18,9 +19,9 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
|
||||
code = error.code;
|
||||
message = error.message;
|
||||
httpcode = error.httpStatus;
|
||||
} else if (error instanceof EntityNotFoundError) {
|
||||
message = `${(error as any).stringifyTarget || "Item"} could not be found`;
|
||||
code = 404;
|
||||
} else if (error.name === "EntityNotFoundError") {
|
||||
message = `${error.message.match(EntityNotFoundErrorRegex)?.[1] || "Item"} could not be found`;
|
||||
code = httpcode = 404;
|
||||
} else if (error instanceof FieldError) {
|
||||
code = Number(error.code);
|
||||
message = error.message;
|
||||
|
||||
@ -107,7 +107,8 @@ export default function rateLimit(opts: {
|
||||
}
|
||||
|
||||
export async function initRateLimits(app: Router) {
|
||||
const { routes, global, ip, error } = Config.get().limits.rate;
|
||||
const { routes, global, ip, error, disabled } = Config.get().limits.rate;
|
||||
if (disabled) return;
|
||||
await listenEvent(EventRateLimit, (event) => {
|
||||
Cache.set(event.channel_id as string, event.data);
|
||||
event.acknowledge?.();
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
res.json([]).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { FieldErrors, route } from "@fosscord/api";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Config, User } from "@fosscord/util";
|
||||
import { adjustEmail } from "./register";
|
||||
import { Config, User, generateToken, adjustEmail } from "@fosscord/util";
|
||||
|
||||
const router: Router = Router();
|
||||
export default router;
|
||||
@ -68,25 +66,6 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
|
||||
res.json({ token, settings: user.settings });
|
||||
});
|
||||
|
||||
export async function generateToken(id: string) {
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
const algorithm = "HS256";
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
jwt.sign(
|
||||
{ id: id, iat },
|
||||
Config.get().security.jwtSecret,
|
||||
{
|
||||
algorithm
|
||||
},
|
||||
(err, token) => {
|
||||
if (err) return rej(err);
|
||||
return res(token);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/login
|
||||
* @argument { login: "email@gmail.com", password: "cleartextpassword", undelete: false, captcha_key: null, login_source: null, gift_code_sku_id: null, }
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { trimSpecial, User, Snowflake, Config, defaultSettings, Member, Invite } from "@fosscord/util";
|
||||
import { trimSpecial, User, Snowflake, Config, defaultSettings, generateToken, Invite, adjustEmail } from "@fosscord/util";
|
||||
import bcrypt from "bcrypt";
|
||||
import { EMAIL_REGEX, FieldErrors, route } from "@fosscord/api";
|
||||
import { FieldErrors, route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api";
|
||||
import "missing-native-js-functions";
|
||||
import { generateToken } from "./login";
|
||||
import { getIpAdress, IPAnalysis, isProxy } from "@fosscord/api";
|
||||
import { HTTPError } from "lambert-server";
|
||||
|
||||
const router: Router = Router();
|
||||
@ -228,24 +226,6 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
|
||||
return res.json({ token: await generateToken(user.id) });
|
||||
});
|
||||
|
||||
export function adjustEmail(email: string): string | undefined {
|
||||
if (!email) return email;
|
||||
// body parser already checked if it is a valid email
|
||||
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
|
||||
// @ts-ignore
|
||||
if (!parts || parts.length < 5) return undefined;
|
||||
const domain = parts[5];
|
||||
const user = parts[1];
|
||||
|
||||
// TODO: check accounts with uncommon email domains
|
||||
if (domain === "gmail.com" || domain === "googlemail.com") {
|
||||
// replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
|
||||
return user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
||||
/**
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { Channel, ChannelRecipientAddEvent, ChannelType, DiscordApiErrors, DmChannelDTO, emitEvent, PublicUserProjection, Recipient, User } from "@fosscord/util";
|
||||
import { route } from "@fosscord/api"
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.put("/:user_id", async (req: Request, res: Response) => {
|
||||
router.put("/:user_id", route({}), async (req: Request, res: Response) => {
|
||||
const { channel_id, user_id } = req.params;
|
||||
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] });
|
||||
|
||||
@ -39,7 +40,7 @@ router.put("/:user_id", async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:user_id", async (req: Request, res: Response) => {
|
||||
router.delete("/:user_id", route({}), async (req: Request, res: Response) => {
|
||||
const { channel_id, user_id } = req.params;
|
||||
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] });
|
||||
if (!(channel.type === ChannelType.GROUP_DM && (channel.owner_id === req.user_id || user_id === req.user_id)))
|
||||
|
||||
@ -5,7 +5,7 @@ import { route } from "@fosscord/api";
|
||||
import { ChannelModifySchema } from "../../channels/#channel_id";
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
const channels = await Channel.find({ guild_id });
|
||||
|
||||
|
||||
@ -4,14 +4,14 @@ import { Request, Response, Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.delete("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
|
||||
router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
|
||||
const { guild_id, role_id, member_id } = req.params;
|
||||
|
||||
await Member.removeRole(member_id, guild_id, role_id);
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.put("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
|
||||
router.put("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
|
||||
const { guild_id, role_id, member_id } = req.params;
|
||||
|
||||
await Member.addRole(member_id, guild_id, role_id);
|
||||
|
||||
@ -29,7 +29,7 @@ export type RolePositionUpdateSchema = {
|
||||
position: number;
|
||||
}[];
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
|
||||
await Member.IsInGuildOrFail(req.user_id, guild_id);
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
res.json([]).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
res.json({
|
||||
id: "",
|
||||
@ -15,4 +16,4 @@ router.get("/", async (req: Request, res: Response) => {
|
||||
}).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
res.json({ sticker_packs: [] }).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/applications/:id", async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
const { id } = req.params;
|
||||
router.get("/applications/:id", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
const { id } = req.params;
|
||||
res.json([]).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/skus/:id", async (req: Request, res: Response) => {
|
||||
router.get("/skus/:id", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
const { id } = req.params;
|
||||
const { id } = req.params;
|
||||
res.json([]).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -11,7 +11,7 @@ export interface UserProfileResponse {
|
||||
premium_since?: Date;
|
||||
}
|
||||
|
||||
router.get("/", route({ response: { body: "UserProfileResponse" } }), async (req: Request, res: Response) => {
|
||||
router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => {
|
||||
if (req.params.id === "@me") req.params.id = req.user_id;
|
||||
const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] });
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
res.json([]).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
res.json({ "country_code": "US" }).status(200);
|
||||
res.json({ country_code: "US" }).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@fosscord/api";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
//TODO
|
||||
res.json([]).status(200);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -22,7 +22,7 @@ export interface UserModifySchema {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
router.get("/", route({}), async (req: Request, res: Response) => {
|
||||
res.json(await User.findOne({ select: PrivateUserProjection, where: { id: req.user_id } }));
|
||||
});
|
||||
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { Request } from "express";
|
||||
import { ntob } from "./Base64";
|
||||
import { FieldErrors } from "./FieldError";
|
||||
export const EMAIL_REGEX =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
export function checkLength(str: string, min: number, max: number, key: string, req: Request) {
|
||||
if (str.length < min || str.length > max) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DiscordApiErrors, Event, EventData, getPermission, PermissionResolvable, Permissions } from "@fosscord/util";
|
||||
import { DiscordApiErrors, EVENT, Event, EventData, getPermission, PermissionResolvable, Permissions } from "@fosscord/util";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@ -29,17 +29,16 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export type RouteSchema = string; // typescript interface name
|
||||
export type RouteResponse = { status?: number; body?: RouteSchema; headers?: Record<string, string> };
|
||||
export type RouteResponse = { status?: number; body?: `${string}Response`; headers?: Record<string, string> };
|
||||
|
||||
export interface RouteOptions {
|
||||
permission?: PermissionResolvable;
|
||||
body?: RouteSchema;
|
||||
response?: RouteResponse;
|
||||
example?: {
|
||||
body?: `${string}Schema`; // typescript interface name
|
||||
test?: {
|
||||
response?: RouteResponse;
|
||||
body?: any;
|
||||
path?: string;
|
||||
event?: EventData;
|
||||
event?: EVENT | EVENT[];
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
// TODO: check every route based on route() paramters: https://github.com/fosscord/fosscord-server/issues/308
|
||||
// TODO: check every route with different database engine
|
||||
136
api/tests/routes.test.ts
Normal file
136
api/tests/routes.test.ts
Normal file
@ -0,0 +1,136 @@
|
||||
// TODO: check every route based on route() parameters: https://github.com/fosscord/fosscord-server/issues/308
|
||||
// TODO: check every route with different database engine
|
||||
|
||||
import getRouteDescriptions from "../jest/getRouteDescriptions";
|
||||
import { join } from "path";
|
||||
import fs from "fs";
|
||||
import Ajv from "ajv";
|
||||
import addFormats from "ajv-formats";
|
||||
import fetch from "node-fetch";
|
||||
import { Event, User, events } from "@fosscord/util";
|
||||
|
||||
const SchemaPath = join(__dirname, "..", "assets", "schemas.json");
|
||||
const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
|
||||
export const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
parseDate: true,
|
||||
allowDate: true,
|
||||
schemas,
|
||||
messages: true,
|
||||
strict: true,
|
||||
strictRequired: true,
|
||||
coerceTypes: true
|
||||
});
|
||||
addFormats(ajv);
|
||||
|
||||
var token: string;
|
||||
var user: User;
|
||||
beforeAll(async (done) => {
|
||||
try {
|
||||
const response = await fetch("http://localhost:3001/api/auth/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw",
|
||||
email: "test@example.com",
|
||||
username: "tester",
|
||||
password: "wtp9gep9gw",
|
||||
invite: null,
|
||||
consent: true,
|
||||
date_of_birth: "2000-01-01",
|
||||
gift_code_sku_id: null,
|
||||
captcha_key: null
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
});
|
||||
const json = await response.json();
|
||||
token = json.token;
|
||||
user = await (
|
||||
await fetch(`http://localhost:3001/api/users/@me`, {
|
||||
headers: { authorization: token }
|
||||
})
|
||||
).json();
|
||||
|
||||
done();
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
|
||||
const emit = events.emit;
|
||||
events.emit = (event: string | symbol, ...args: any[]) => {
|
||||
events.emit("event", args[0]);
|
||||
return emit(event, ...args);
|
||||
};
|
||||
|
||||
describe("Automatic unit tests with route description middleware", () => {
|
||||
const routes = getRouteDescriptions();
|
||||
|
||||
routes.forEach((route, pathAndMethod) => {
|
||||
const [path, method] = pathAndMethod.split("|");
|
||||
|
||||
test(`${method.toUpperCase()} ${path}`, async (done) => {
|
||||
if (!route.test) {
|
||||
console.log(`${(route as any).file}\nrouter.${method} is missing the test property`);
|
||||
return done();
|
||||
}
|
||||
const urlPath = path.replace(":id", user.id) || route.test?.path;
|
||||
var validate: any;
|
||||
if (route.test.body) {
|
||||
validate = ajv.getSchema(route.test.body);
|
||||
if (!validate) return done(new Error(`Response schema ${route.test.body} not found`));
|
||||
}
|
||||
|
||||
var body = "";
|
||||
let eventEmitted = Promise.resolve();
|
||||
|
||||
if (route.test.event) {
|
||||
if (!Array.isArray(route.test.event)) route.test.event = [route.test.event];
|
||||
|
||||
eventEmitted = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject, 1000);
|
||||
const received = [];
|
||||
|
||||
events.on("event", (event: Event) => {
|
||||
if (!route.test.event.includes(event.event)) return;
|
||||
|
||||
received.push(event.event);
|
||||
if (received.length === route.test.event.length) resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://localhost:3001/api${urlPath}`, {
|
||||
method: method.toUpperCase(),
|
||||
body: JSON.stringify(route.test.body),
|
||||
headers: { ...route.test.headers, authorization: token }
|
||||
});
|
||||
|
||||
body = await response.text();
|
||||
|
||||
expect(response.status, body).toBe(route.test.response.status || 200);
|
||||
|
||||
// TODO: check headers
|
||||
// TODO: expect event
|
||||
|
||||
if (validate) {
|
||||
body = JSON.parse(body);
|
||||
const valid = validate(body);
|
||||
if (!valid) return done(validate.errors);
|
||||
}
|
||||
} catch (error) {
|
||||
return done(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await eventEmitted;
|
||||
} catch (error) {
|
||||
return done(new Error(`Event ${route.test.event} was not emitted`));
|
||||
}
|
||||
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -66,8 +66,9 @@
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@fosscord/api": ["src/index.ts"],
|
||||
"@fosscord/api": ["src/index"],
|
||||
"@fosscord/api/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"plugins": [{ "transform": "@zerollup/ts-transform-paths" }]
|
||||
}
|
||||
}
|
||||
|
||||
BIN
bundle/package-lock.json
generated
BIN
bundle/package-lock.json
generated
Binary file not shown.
@ -4,15 +4,16 @@
|
||||
"description": "",
|
||||
"main": "src/start.js",
|
||||
"scripts": {
|
||||
"preinstall": "cd ../util && npm i && cd ../api && npm i && cd ../cdn && npm i && cd ../gateway && npm i",
|
||||
"setup": "cd ../util && npm --production=false i && cd ../api && npm --production=false i && cd ../cdn && npm --production=false i && cd ../gateway && npm --production=false i && npm install && npm run start",
|
||||
"build": "npm run build:util && npm run build:api && npm run build:cdn && npm run build:gateway && npm run build:bundle",
|
||||
"postinstall": "ts-patch install -s",
|
||||
"build:bundle": "npx tsc -b .",
|
||||
"build:util": "cd ../util/ && npm run build",
|
||||
"build:api": "cd ../api/ && npm run build",
|
||||
"build:cdn": "cd ../cdn/ && npm run build",
|
||||
"build:gateway": "cd ../gateway/ && npm run build",
|
||||
"start": "npm run build && npm run start:bundle",
|
||||
"start:bundle": "node -r ./tsconfig-paths-bootstrap.js dist/start.js",
|
||||
"start:bundle": "node dist/start.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
@ -44,6 +45,7 @@
|
||||
"@types/ws": "^7.4.0",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
"ts-node": "^10.2.1",
|
||||
"ts-patch": "^1.4.4",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
const tsConfigPaths = require("tsconfig-paths");
|
||||
const path = require("path");
|
||||
|
||||
const cleanup = tsConfigPaths.register({
|
||||
baseUrl: path.join(__dirname, "node_modules", "@fosscord"),
|
||||
paths: {
|
||||
"@fosscord/api": ["api/dist/index.js"],
|
||||
"@fosscord/api/*": ["api/dist/*"],
|
||||
"@fosscord/gateway": ["gateway/dist/index.js"],
|
||||
"@fosscord/gateway/*": ["gateway/dist/*"],
|
||||
"@fosscord/cdn": ["cdn/dist/index.js"],
|
||||
"@fosscord/cdn/*": ["cdn/dist/*"],
|
||||
},
|
||||
});
|
||||
BIN
cdn/package-lock.json
generated
BIN
cdn/package-lock.json
generated
Binary file not shown.
@ -1,25 +1,26 @@
|
||||
{
|
||||
"name": "@fosscord/cdn",
|
||||
"version": "1.0.0",
|
||||
"description": "cdn for discord clone",
|
||||
"description": "cdn for fosscord",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"postinstall": "ts-patch install -s",
|
||||
"test": "npm run build && jest --coverage ./tests",
|
||||
"build": "npx tsc -b .",
|
||||
"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start.js"
|
||||
"start": "npm run build && node dist/start.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/discord-open-source/discord-cdn.git"
|
||||
"url": "git+https://github.com/fosscord/fosscord-server.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/discord-open-source/discord-cdn/issues"
|
||||
"url": "https://github.com/fosscord/fosscord-server/issues"
|
||||
},
|
||||
"homepage": "https://github.com/discord-open-source/discord-cdn#readme",
|
||||
"homepage": "https://github.com/fosscord/fosscord-server#readme",
|
||||
"devDependencies": {
|
||||
"@types/amqplib": "^0.8.1",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
@ -34,7 +35,9 @@
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^14.17.0",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/uuid": "^8.3.0"
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
"ts-patch": "^1.4.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fosscord/util": "file:../util",
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
const tsConfigPaths = require("tsconfig-paths");
|
||||
const path = require("path");
|
||||
|
||||
const cleanup = tsConfigPaths.register({
|
||||
baseUrl: path.join(__dirname, ".."),
|
||||
paths: {
|
||||
"@fosscord/cdn": ["dist/index.js"],
|
||||
"@fosscord/cdn/*": ["dist/*"],
|
||||
},
|
||||
});
|
||||
@ -68,8 +68,9 @@
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@fosscord/cdn/": ["src/index.ts"],
|
||||
"@fosscord/cdn/": ["src/index"],
|
||||
"@fosscord/cdn/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"plugins": [{ "transform": "@zerollup/ts-transform-paths" }]
|
||||
}
|
||||
}
|
||||
|
||||
BIN
gateway/package-lock.json
generated
BIN
gateway/package-lock.json
generated
Binary file not shown.
@ -4,8 +4,9 @@
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"postinstall": "ts-patch install -s",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start.js",
|
||||
"start": "npm run build && node dist/start.js",
|
||||
"build": "npx tsc -b .",
|
||||
"dev": "tsnd --respawn src/start.ts"
|
||||
},
|
||||
@ -22,7 +23,9 @@
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
"ts-node-dev": "^1.1.6",
|
||||
"ts-patch": "^1.4.4",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
const tsConfigPaths = require("tsconfig-paths");
|
||||
const path = require("path");
|
||||
|
||||
const cleanup = tsConfigPaths.register({
|
||||
baseUrl: path.join(__dirname, ".."),
|
||||
paths: {
|
||||
"@fosscord/gateway": ["dist/index.js"],
|
||||
"@fosscord/gateway/*": ["dist/*"],
|
||||
},
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { WebSocket } from "@fosscord/gateway";
|
||||
import { Message } from "./Message";
|
||||
import { Session } from "@fosscord/util";
|
||||
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import WS from "ws";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import {
|
||||
setHeartbeat,
|
||||
Send,
|
||||
CLOSECODES,
|
||||
OPCODES,
|
||||
WebSocket,
|
||||
} from "@fosscord/gateway";
|
||||
import { IncomingMessage } from "http";
|
||||
import { Close } from "./Close";
|
||||
import { Message } from "./Message";
|
||||
import { setHeartbeat } from "@fosscord/gateway/util/setHeartbeat";
|
||||
import { Send } from "@fosscord/gateway/util/Send";
|
||||
import { CLOSECODES, OPCODES } from "@fosscord/gateway/util/Constants";
|
||||
import { createDeflate } from "zlib";
|
||||
import { URL } from "url";
|
||||
import { Session } from "@fosscord/util";
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { WebSocket, Payload, CLOSECODES, OPCODES } from "@fosscord/gateway";
|
||||
var erlpack: any;
|
||||
try {
|
||||
erlpack = require("@yukikaze-bot/erlpack");
|
||||
} catch (error) {}
|
||||
import OPCodeHandlers from "../opcodes";
|
||||
import { Payload, CLOSECODES, OPCODES } from "@fosscord/gateway/util/Constants";
|
||||
import { instanceOf, Tuple } from "lambert-server";
|
||||
import { check } from "../opcodes/instanceOf";
|
||||
import WS from "ws";
|
||||
|
||||
@ -9,12 +9,9 @@ import {
|
||||
ListenEventOpts,
|
||||
Member,
|
||||
} from "@fosscord/util";
|
||||
import { OPCODES } from "@fosscord/gateway/util/Constants";
|
||||
import { Send } from "@fosscord/gateway/util/Send";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { OPCODES, WebSocket, Send } from "@fosscord/gateway";
|
||||
import "missing-native-js-functions";
|
||||
import { Channel as AMQChannel } from "amqplib";
|
||||
import { In, Like } from "typeorm";
|
||||
import { Recipient } from "@fosscord/util";
|
||||
|
||||
// TODO: close connection on Invalidated Token
|
||||
@ -116,7 +113,7 @@ async function consume(this: WebSocket, opts: EventOpts) {
|
||||
.has("VIEW_CHANNEL")
|
||||
)
|
||||
return;
|
||||
//No break needed here, we need to call the listenEvent function below
|
||||
//No break needed here, we need to call the listenEvent function below
|
||||
case "GUILD_CREATE":
|
||||
this.events[id] = await listenEvent(id, consumer, listenOpts);
|
||||
break;
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import { CLOSECODES, Payload } from "@fosscord/gateway/util/Constants";
|
||||
import { Send } from "@fosscord/gateway/util/Send";
|
||||
import { setHeartbeat } from "@fosscord/gateway/util/setHeartbeat";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { Payload, Send, setHeartbeat, WebSocket } from "@fosscord/gateway";
|
||||
|
||||
export async function onHeartbeat(this: WebSocket, data: Payload) {
|
||||
// TODO: validate payload
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { CLOSECODES, Payload, OPCODES } from "@fosscord/gateway/util/Constants";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import {
|
||||
WebSocket,
|
||||
CLOSECODES,
|
||||
Payload,
|
||||
OPCODES,
|
||||
genSessionId,
|
||||
} from "@fosscord/gateway";
|
||||
import {
|
||||
Channel,
|
||||
checkToken,
|
||||
@ -24,7 +29,6 @@ import { Send } from "@fosscord/gateway/util/Send";
|
||||
const experiments: any = [];
|
||||
import { check } from "./instanceOf";
|
||||
import { Recipient } from "@fosscord/util";
|
||||
import { genSessionId } from "@fosscord/gateway/util/SessionUtils";
|
||||
|
||||
// TODO: bot sharding
|
||||
// TODO: check priviliged intents
|
||||
@ -98,7 +102,9 @@ export async function onIdentify(this: WebSocket, data: Payload) {
|
||||
//TODO is this needed? check if users in group dm that are not friends are sent in the READY event
|
||||
//users = users.concat(x.channel.recipients);
|
||||
if (x.channel.isDm()) {
|
||||
x.channel.recipients = x.channel.recipients!.filter((x) => x.id !== this.user_id);
|
||||
x.channel.recipients = x.channel.recipients!.filter(
|
||||
(x) => x.id !== this.user_id
|
||||
);
|
||||
}
|
||||
return x.channel;
|
||||
});
|
||||
@ -109,7 +115,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
|
||||
if (!user) return this.close(CLOSECODES.Authentication_failed);
|
||||
|
||||
for (let relation of user.relationships) {
|
||||
const related_user = relation.to
|
||||
const related_user = relation.to;
|
||||
const public_related_user = {
|
||||
username: related_user.username,
|
||||
discriminator: related_user.discriminator,
|
||||
|
||||
@ -2,13 +2,10 @@ import {
|
||||
getPermission,
|
||||
Member,
|
||||
PublicMemberProjection,
|
||||
PublicUserProjection,
|
||||
Role,
|
||||
} from "@fosscord/util";
|
||||
import { LazyRequest } from "../schema/LazyRequest";
|
||||
import { OPCODES, Payload } from "@fosscord/gateway/util/Constants";
|
||||
import { Send } from "@fosscord/gateway/util/Send";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { WebSocket, Send, OPCODES, Payload } from "@fosscord/gateway";
|
||||
import { check } from "./instanceOf";
|
||||
import "missing-native-js-functions";
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { CLOSECODES, Payload } from "@fosscord/gateway/util/Constants";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { WebSocket, Payload } from "@fosscord/gateway";
|
||||
|
||||
export function onPresenceUpdate(this: WebSocket, data: Payload) {
|
||||
// return this.close(CLOSECODES.Unknown_error);
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { CLOSECODES, Payload } from "@fosscord/gateway/util/Constants";
|
||||
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { Payload, WebSocket } from "@fosscord/gateway";
|
||||
|
||||
export function onRequestGuildMembers(this: WebSocket, data: Payload) {
|
||||
// return this.close(CLOSECODES.Unknown_error);
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import { CLOSECODES, Payload } from "@fosscord/gateway/util/Constants";
|
||||
import { Send } from "@fosscord/gateway/util/Send";
|
||||
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { WebSocket, Payload, Send } from "@fosscord/gateway";
|
||||
|
||||
export async function onResume(this: WebSocket, data: Payload) {
|
||||
console.log("Got Resume -> cancel not implemented");
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { VoiceStateUpdateSchema } from "../schema/VoiceStateUpdateSchema";
|
||||
import { Payload } from "@fosscord/gateway/util/Constants";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { Payload, WebSocket, genVoiceToken } from "@fosscord/gateway";
|
||||
import { check } from "./instanceOf";
|
||||
import {
|
||||
Config,
|
||||
@ -12,7 +11,6 @@ import {
|
||||
VoiceState,
|
||||
VoiceStateUpdateEvent,
|
||||
} from "@fosscord/util";
|
||||
import { genVoiceToken } from "@fosscord/gateway/util/SessionUtils";
|
||||
// TODO: check if a voice server is setup
|
||||
// Notice: Bot users respect the voice channel's user limit, if set. When the voice channel is full, you will not receive the Voice State Update or Voice Server Update events in response to your own Voice State Update. Having MANAGE_CHANNELS permission bypasses this limit and allows you to join regardless of the channel being full or not.
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Payload } from "@fosscord/gateway/util/Constants";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { WebSocket, Payload } from "@fosscord/gateway";
|
||||
import { onHeartbeat } from "./Heartbeat";
|
||||
import { onIdentify } from "./Identify";
|
||||
import { onLazyRequest } from "./LazyRequest";
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { instanceOf } from "lambert-server";
|
||||
import { CLOSECODES } from "@fosscord/gateway/util/Constants";
|
||||
import WebSocket from "@fosscord/gateway/util/WebSocket";
|
||||
import { WebSocket, CLOSECODES } from "@fosscord/gateway";
|
||||
|
||||
export function check(this: WebSocket, schema: any, data: any) {
|
||||
try {
|
||||
|
||||
@ -2,9 +2,7 @@ var erlpack: any;
|
||||
try {
|
||||
erlpack = require("@yukikaze-bot/erlpack");
|
||||
} catch (error) {}
|
||||
import { Payload } from "@fosscord/gateway/util/Constants";
|
||||
|
||||
import WebSocket from "./WebSocket";
|
||||
import { Payload, WebSocket } from "@fosscord/gateway";
|
||||
|
||||
export async function Send(socket: WebSocket, data: Payload) {
|
||||
let buffer: Buffer | string;
|
||||
@ -20,7 +18,7 @@ export async function Send(socket: WebSocket, data: Payload) {
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
socket.send(buffer, (err) => {
|
||||
socket.send(buffer, (err: any) => {
|
||||
if (err) return rej(err);
|
||||
return res(null);
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ import WS from "ws";
|
||||
import { Deflate } from "zlib";
|
||||
import { Channel } from "amqplib";
|
||||
|
||||
interface WebSocket extends WS {
|
||||
export interface WebSocket extends WS {
|
||||
version: number;
|
||||
user_id: string;
|
||||
session_id: string;
|
||||
@ -19,5 +19,3 @@ interface WebSocket extends WS {
|
||||
permissions: Record<string, Permissions>;
|
||||
events: Record<string, Function>;
|
||||
}
|
||||
|
||||
export default WebSocket;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CLOSECODES } from "./Constants";
|
||||
import WebSocket from "./WebSocket";
|
||||
import { WebSocket } from "./WebSocket";
|
||||
|
||||
// TODO: make heartbeat timeout configurable
|
||||
export function setHeartbeat(socket: WebSocket) {
|
||||
|
||||
@ -70,8 +70,10 @@
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@fosscord/gateway": ["src/index"],
|
||||
"@fosscord/gateway/*": ["src/*"],
|
||||
"@fosscord/util/*": ["../util/src/*"]
|
||||
}
|
||||
},
|
||||
"plugins": [{ "transform": "@zerollup/ts-transform-paths" }]
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,19 +12,19 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/fosscord/fosscord-server-util.git"
|
||||
"url": "git+https://github.com/fosscord/fosscord-server.git"
|
||||
},
|
||||
"keywords": [
|
||||
"discord",
|
||||
"fosscord",
|
||||
"fosscord-server-util",
|
||||
"fosscord-server",
|
||||
"discord open source",
|
||||
"discord-open-source"
|
||||
],
|
||||
"author": "Fosscord",
|
||||
"license": "GPLV3",
|
||||
"bugs": {
|
||||
"url": "https://github.com/fosscord/fosscord-server-util/issues"
|
||||
"url": "https://github.com/fosscord/fosscord-server/issues"
|
||||
},
|
||||
"homepage": "https://docs.fosscord.com/",
|
||||
"devDependencies": {
|
||||
|
||||
@ -77,6 +77,7 @@ export interface ConfigValue {
|
||||
maxWebhooks: number;
|
||||
};
|
||||
rate: {
|
||||
disabled: boolean;
|
||||
ip: Omit<RateLimitOptions, "bot_count">;
|
||||
global: RateLimitOptions;
|
||||
error: RateLimitOptions;
|
||||
@ -188,6 +189,7 @@ export const DefaultConfigOptions: ConfigValue = {
|
||||
maxWebhooks: 10,
|
||||
},
|
||||
rate: {
|
||||
disabled: true,
|
||||
ip: {
|
||||
count: 500,
|
||||
window: 5,
|
||||
|
||||
@ -161,15 +161,13 @@ export class User extends BaseClass {
|
||||
}
|
||||
|
||||
static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
|
||||
const user = await User.findOne(
|
||||
return await User.findOneOrFail(
|
||||
{ id: user_id },
|
||||
{
|
||||
...opts,
|
||||
select: [...PublicUserProjection, ...(opts?.select || [])],
|
||||
}
|
||||
);
|
||||
if (!user) throw new HTTPError("User not found", 404);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ export const Config = {
|
||||
get: function get() {
|
||||
return config.value as ConfigValue;
|
||||
},
|
||||
set: function set(val: any) {
|
||||
set: function set(val: Partial<ConfigValue>) {
|
||||
if (!config) return;
|
||||
config.value = val.merge(config?.value || {});
|
||||
return config.save();
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import path from "path";
|
||||
import "reflect-metadata";
|
||||
import { Connection, createConnection, ValueTransformer } from "typeorm";
|
||||
import * as Models from "../entities";
|
||||
@ -15,7 +16,7 @@ export function initDatabase() {
|
||||
// @ts-ignore
|
||||
promise = createConnection({
|
||||
type: "sqlite",
|
||||
database: "database.db",
|
||||
database: path.join(process.cwd(), "database.db"),
|
||||
// type: "postgres",
|
||||
// url: "postgres://fosscord:wb94SmuURM2Syv&@localhost/fosscord",
|
||||
//
|
||||
|
||||
20
util/src/util/Email.ts
Normal file
20
util/src/util/Email.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export const EMAIL_REGEX =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
export function adjustEmail(email: string): string | undefined {
|
||||
if (!email) return email;
|
||||
// body parser already checked if it is a valid email
|
||||
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
|
||||
// @ts-ignore
|
||||
if (!parts || parts.length < 5) return undefined;
|
||||
const domain = parts[5];
|
||||
const user = parts[1];
|
||||
|
||||
// TODO: check accounts with uncommon email domains
|
||||
if (domain === "gmail.com" || domain === "googlemail.com") {
|
||||
// replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
|
||||
return user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { Channel } from "amqplib";
|
||||
import { RabbitMQ } from "./RabbitMQ";
|
||||
import EventEmitter from "events";
|
||||
import { EVENT, Event } from "../interfaces";
|
||||
const events = new EventEmitter();
|
||||
export const events = new EventEmitter();
|
||||
|
||||
export async function emitEvent(payload: Omit<Event, "created_at">) {
|
||||
const id = (payload.channel_id || payload.user_id || payload.guild_id) as string;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import jwt, { VerifyOptions } from "jsonwebtoken";
|
||||
import { Config } from "./Config";
|
||||
import { User } from "../entities";
|
||||
|
||||
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
|
||||
@ -21,3 +22,22 @@ export function checkToken(token: string, jwtSecret: string): Promise<any> {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateToken(id: string) {
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
const algorithm = "HS256";
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
jwt.sign(
|
||||
{ id: id, iat },
|
||||
Config.get().security.jwtSecret,
|
||||
{
|
||||
algorithm,
|
||||
},
|
||||
(err, token) => {
|
||||
if (err) return rej(err);
|
||||
return res(token);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
export * from "./ApiError";
|
||||
export * from "./BitField";
|
||||
export * from "./checkToken";
|
||||
export * from "./Token";
|
||||
export * from "./cdn";
|
||||
export * from "./Config";
|
||||
export * from "./Constants";
|
||||
export * from "./Database";
|
||||
export * from "./Event";
|
||||
export * from "./Email";
|
||||
export * from "./Intents";
|
||||
export * from "./MessageFlags";
|
||||
export * from "./Permissions";
|
||||
|
||||
@ -68,12 +68,6 @@
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"resolveJsonModule": true,
|
||||
"plugins": [
|
||||
{
|
||||
"transform": "ts-transform-json-schema",
|
||||
"type": "program"
|
||||
}
|
||||
]
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user