Merge branch 'fosscord:master' into fix-dm

This commit is contained in:
AlTech98 2021-09-18 08:03:52 +02:00 committed by GitHub
commit 5d6fa7697a
10 changed files with 88 additions and 76 deletions

View File

@ -26,8 +26,7 @@
"type": "string" "type": "string"
}, },
"date_of_birth": { "date_of_birth": {
"type": "string", "type": "string"
"format": "date-time"
}, },
"gift_code_sku_id": { "gift_code_sku_id": {
"type": "string" "type": "string"
@ -726,22 +725,13 @@
"type": "object", "type": "object",
"properties": { "properties": {
"target_user_id": { "target_user_id": {
"type": [ "type": "string"
"null",
"string"
]
}, },
"target_type": { "target_type": {
"type": [ "type": "string"
"null",
"string"
]
}, },
"validate": { "validate": {
"type": [ "type": "string"
"null",
"string"
]
}, },
"max_age": { "max_age": {
"type": "integer" "type": "integer"
@ -2545,7 +2535,10 @@
"type": "string" "type": "string"
}, },
"icon": { "icon": {
"type": "string" "type": [
"null",
"string"
]
}, },
"channels": { "channels": {
"type": "array", "type": "array",
@ -2557,10 +2550,7 @@
"type": "string" "type": "string"
}, },
"system_channel_id": { "system_channel_id": {
"type": [ "type": "string"
"null",
"string"
]
}, },
"rules_channel_id": { "rules_channel_id": {
"type": "string" "type": "string"
@ -2825,10 +2815,7 @@
] ]
}, },
"description": { "description": {
"type": [ "type": "string"
"null",
"string"
]
}, },
"features": { "features": {
"type": "array", "type": "array",
@ -2849,19 +2836,13 @@
"type": "integer" "type": "integer"
}, },
"public_updates_channel_id": { "public_updates_channel_id": {
"type": [ "type": "string"
"null",
"string"
]
}, },
"afk_timeout": { "afk_timeout": {
"type": "integer" "type": "integer"
}, },
"afk_channel_id": { "afk_channel_id": {
"type": [ "type": "string"
"null",
"string"
]
}, },
"preferred_locale": { "preferred_locale": {
"type": "string" "type": "string"
@ -2874,16 +2855,16 @@
"type": "string" "type": "string"
}, },
"icon": { "icon": {
"type": "string" "type": [
"null",
"string"
]
}, },
"guild_template_code": { "guild_template_code": {
"type": "string" "type": "string"
}, },
"system_channel_id": { "system_channel_id": {
"type": [ "type": "string"
"null",
"string"
]
}, },
"rules_channel_id": { "rules_channel_id": {
"type": "string" "type": "string"
@ -5712,7 +5693,10 @@
"type": "string" "type": "string"
}, },
"avatar": { "avatar": {
"type": "string" "type": [
"null",
"string"
]
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -6233,10 +6217,7 @@
"type": "string" "type": "string"
}, },
"accent_color": { "accent_color": {
"type": [ "type": "integer"
"null",
"integer"
]
}, },
"banner": { "banner": {
"type": [ "type": [

View File

@ -5,7 +5,8 @@
"main": "dist/Server.js", "main": "dist/Server.js",
"types": "dist/Server.d.ts", "types": "dist/Server.d.ts",
"scripts": { "scripts": {
"test": "npm run build && jest --coverage --verbose --forceExit ./tests", "test:only": "node -r ./scripts/tsconfig-paths-bootstrap.js node_modules/.bin/jest --coverage --verbose --forceExit ./tests",
"test": "npm run build && npm run test:only",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start", "start": "npm run build && node -r ./scripts/tsconfig-paths-bootstrap.js dist/start",
"build": "npx tsc -b .", "build": "npx tsc -b .",

View File

@ -1,10 +1,11 @@
const fs = require("fs"); const fs = require("fs");
const path = require("path");
const { FosscordServer } = require("../dist/Server"); const { FosscordServer } = require("../dist/Server");
const Server = new FosscordServer({ port: 3001 }); const Server = new FosscordServer({ port: 3001 });
global.server = Server; global.server = Server;
module.exports = async () => { module.exports = async () => {
try { try {
fs.unlinkSync(`${__dirname}/../database.db`); fs.unlinkSync(path.join(__dirname, "..", "database.db"));
} catch {} } catch {}
return await Server.start(); return await Server.start();
}; };

View File

@ -27,13 +27,16 @@ export interface RegisterSchema {
email?: string; email?: string;
fingerprint?: string; fingerprint?: string;
invite?: string; invite?: string;
/**
* @TJS-type string
*/
date_of_birth?: Date; // "2000-04-03" date_of_birth?: Date; // "2000-04-03"
gift_code_sku_id?: string; gift_code_sku_id?: string;
captcha_key?: string; captcha_key?: string;
} }
router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => {
const { let {
email, email,
username, username,
password, password,
@ -61,14 +64,11 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
// TODO: gift_code_sku_id? // TODO: gift_code_sku_id?
// TODO: check password strength // TODO: check password strength
// adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick // email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
let adjusted_email = adjustEmail(email); email = adjustEmail(email);
// adjusted_password will be the hash of the password
let adjusted_password = "";
// trim special uf8 control characters -> Backspace, Newline, ... // trim special uf8 control characters -> Backspace, Newline, ...
let adjusted_username = trimSpecial(username); username = trimSpecial(username);
// discriminator will be randomly generated // discriminator will be randomly generated
let discriminator = ""; let discriminator = "";
@ -96,10 +96,10 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
if (email) { if (email) {
// replace all dots and chars after +, if its a gmail.com email // replace all dots and chars after +, if its a gmail.com email
if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } }); if (!email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } });
// check if there is already an account with this email // check if there is already an account with this email
const exists = await User.findOneOrFail({ email: adjusted_email }).catch((e) => {}); const exists = await User.findOneOrFail({ email: email }).catch((e) => {});
if (exists) { if (exists) {
throw FieldErrors({ throw FieldErrors({
@ -122,6 +122,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
} else if (register.dateOfBirth.minimum) { } else if (register.dateOfBirth.minimum) {
const minimum = new Date(); const minimum = new Date();
minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum);
date_of_birth = new Date(date_of_birth);
// higher is younger // higher is younger
if (date_of_birth > minimum) { if (date_of_birth > minimum) {
@ -162,7 +163,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
} }
// the salt is saved in the password refer to bcrypt docs // the salt is saved in the password refer to bcrypt docs
adjusted_password = await bcrypt.hash(password, 12); password = await bcrypt.hash(password, 12);
let exists; let exists;
// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
@ -171,7 +172,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database? // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database?
for (let tries = 0; tries < 5; tries++) { for (let tries = 0; tries < 5; tries++) {
discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
exists = await User.findOne({ where: { discriminator, username: adjusted_username }, select: ["id"] }); exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
if (!exists) break; if (!exists) break;
} }
@ -190,7 +191,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
const user = await new User({ const user = await new User({
created_at: new Date(), created_at: new Date(),
username: adjusted_username, username: username,
discriminator, discriminator,
id: Snowflake.generate(), id: Snowflake.generate(),
bot: false, bot: false,
@ -204,12 +205,12 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
verified: false, verified: false,
disabled: false, disabled: false,
deleted: false, deleted: false,
email: adjusted_email, email: email,
nsfw_allowed: true, // TODO: depending on age nsfw_allowed: true, // TODO: depending on age
public_flags: "0", public_flags: "0",
flags: "0", // TODO: generate flags: "0", // TODO: generate
data: { data: {
hash: adjusted_password, hash: password,
valid_tokens_since: new Date() valid_tokens_since: new Date()
}, },
settings: { ...defaultSettings, locale: req.language || "en-US" }, settings: { ...defaultSettings, locale: req.language || "en-US" },
@ -220,6 +221,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
}); });
export function adjustEmail(email: string): string | undefined { export function adjustEmail(email: string): string | undefined {
if (!email) return email;
// body parser already checked if it is a valid email // body parser already checked if it is a valid email
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX); const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
// @ts-ignore // @ts-ignore

View File

@ -8,9 +8,9 @@ import { isTextChannel } from "./messages";
const router: Router = Router(); const router: Router = Router();
export interface InviteCreateSchema { export interface InviteCreateSchema {
target_user_id?: string | null; target_user_id?: string;
target_type?: string | null; target_type?: string;
validate?: string | null; // ? what is this validate?: string; // ? what is this
max_age?: number; max_age?: number;
max_uses?: number; max_uses?: number;
temporary?: boolean; temporary?: boolean;

View File

@ -11,15 +11,15 @@ const router = Router();
export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
banner?: string | null; banner?: string | null;
splash?: string | null; splash?: string | null;
description?: string | null; description?: string;
features?: string[]; features?: string[];
verification_level?: number; verification_level?: number;
default_message_notifications?: number; default_message_notifications?: number;
system_channel_flags?: number; system_channel_flags?: number;
explicit_content_filter?: number; explicit_content_filter?: number;
public_updates_channel_id?: string | null; public_updates_channel_id?: string;
afk_timeout?: number; afk_timeout?: number;
afk_channel_id?: string | null; afk_channel_id?: string;
preferred_locale?: string; preferred_locale?: string;
} }

View File

@ -12,10 +12,10 @@ export interface GuildCreateSchema {
*/ */
name: string; name: string;
region?: string; region?: string;
icon?: string; icon?: string | null;
channels?: ChannelModifySchema[]; channels?: ChannelModifySchema[];
guild_template_code?: string; guild_template_code?: string;
system_channel_id?: string | null; system_channel_id?: string;
rules_channel_id?: string; rules_channel_id?: string;
} }

View File

@ -6,7 +6,7 @@ import { DiscordApiErrors } from "@fosscord/util";
export interface GuildTemplateCreateSchema { export interface GuildTemplateCreateSchema {
name: string; name: string;
avatar?: string; avatar?: string | null;
} }
router.get("/:code", route({}), async (req: Request, res: Response) => { router.get("/:code", route({}), async (req: Request, res: Response) => {

View File

@ -16,7 +16,7 @@ export interface UserModifySchema {
* @maxLength 1024 * @maxLength 1024
*/ */
bio?: string; bio?: string;
accent_color?: number | null; accent_color?: number;
banner?: string | null; banner?: string | null;
password?: string; password?: string;
new_password?: string; new_password?: string;

View File

@ -43,10 +43,37 @@ export interface RouteOptions {
}; };
} }
// Normalizer is introduced to workaround https://github.com/ajv-validator/ajv/issues/1287
// this removes null values as ajv doesn't treat them as undefined
// normalizeBody allows to handle circular structures without issues
// taken from https://github.com/serverless/serverless/blob/master/lib/classes/ConfigSchemaHandler/index.js#L30 (MIT license)
const normalizeBody = (body: any = {}) => {
const normalizedObjectsSet = new WeakSet();
const normalizeObject = (object: any) => {
if (normalizedObjectsSet.has(object)) return;
normalizedObjectsSet.add(object);
if (Array.isArray(object)) {
for (const [index, value] of object.entries()) {
if (typeof value === "object") normalizeObject(value);
}
} else {
for (const [key, value] of Object.entries(object)) {
if (value == null) {
if (key === "icon" || key === "avatar" || key === "banner" || key === "splash") continue;
delete object[key];
} else if (typeof value === "object") {
normalizeObject(value);
}
}
}
};
normalizeObject(body);
return body;
};
export function route(opts: RouteOptions) { export function route(opts: RouteOptions) {
var validate: AnyValidateFunction<any>; var validate: AnyValidateFunction<any> | undefined;
if (opts.body) { if (opts.body) {
// @ts-ignore
validate = ajv.getSchema(opts.body); validate = ajv.getSchema(opts.body);
if (!validate) throw new Error(`Body schema ${opts.body} not found`); if (!validate) throw new Error(`Body schema ${opts.body} not found`);
} }
@ -60,14 +87,14 @@ export function route(opts: RouteOptions) {
if (!permission.has(required)) { if (!permission.has(required)) {
throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string); throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string);
} }
}
if (validate) { if (validate) {
const valid = validate(req.body); const valid = validate(normalizeBody(req.body));
if (!valid) { if (!valid) {
const fields: Record<string, { code?: string; message: string }> = {}; const fields: Record<string, { code?: string; message: string }> = {};
validate.errors?.forEach((x) => (fields[x.instancePath] = { code: x.keyword, message: x.message || "" })); validate.errors?.forEach((x) => (fields[x.instancePath.slice(1)] = { code: x.keyword, message: x.message || "" }));
throw FieldErrors(fields); throw FieldErrors(fields);
}
} }
} }
next(); next();