From 5e86d7ab9c5200d794c3adb2b422d20a2aefd2ce Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Sat, 13 Aug 2022 02:00:50 +0200 Subject: restructure to single project --- src/util/entities/User.ts | 324 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 src/util/entities/User.ts (limited to 'src/util/entities/User.ts') diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts new file mode 100644 index 00000000..61343e81 --- /dev/null +++ b/src/util/entities/User.ts @@ -0,0 +1,324 @@ +import { Column, Entity, FindOneOptions, FindOptionsSelectByString, JoinColumn, OneToMany, OneToOne } from "typeorm"; +import { OrmUtils } from "../util/imports/OrmUtils"; +import { BaseClass } from "./BaseClass"; +import { BitField } from "../util/BitField"; +import { Relationship } from "./Relationship"; +import { ConnectedAccount } from "./ConnectedAccount"; +import { Config, FieldErrors, Snowflake, trimSpecial } from ".."; +import { Member, Session, UserSettings } from "."; + +export enum PublicUserEnum { + username, + discriminator, + id, + public_flags, + avatar, + accent_color, + banner, + bio, + bot, + premium_since, +} +export type PublicUserKeys = keyof typeof PublicUserEnum; + +export enum PrivateUserEnum { + flags, + mfa_enabled, + email, + phone, + verified, + nsfw_allowed, + premium, + premium_type, + disabled, + settings, + // locale +} +export type PrivateUserKeys = keyof typeof PrivateUserEnum | PublicUserKeys; + +export const PublicUserProjection = Object.values(PublicUserEnum).filter( + (x) => typeof x === "string" +) as PublicUserKeys[]; +export const PrivateUserProjection = [ + ...PublicUserProjection, + ...Object.values(PrivateUserEnum).filter((x) => typeof x === "string"), +] as PrivateUserKeys[]; + +// Private user data that should never get sent to the client +export type PublicUser = Pick; + +export interface UserPublic extends Pick {} + +export interface UserPrivate extends Pick { + locale: string; +} + +// TODO: add purchased_flags, premium_usage_flags + +@Entity("users") +export class User extends BaseClass { + @Column() + username: string; // username max length 32, min 2 (should be configurable) + + @Column() + discriminator: string; // opaque string: 4 digits on discord.com + + setDiscriminator(val: string) { + const number = Number(val); + if (isNaN(number)) throw new Error("invalid discriminator"); + if (number <= 0 || number >= 10000) throw new Error("discriminator must be between 1 and 9999"); + this.discriminator = val.toString().padStart(4, "0"); + } + + @Column({ nullable: true }) + avatar?: string; // hash of the user avatar + + @Column({ nullable: true }) + accent_color?: number; // banner color of user + + @Column({ nullable: true }) + banner?: string; // hash of the user banner + + @Column({ nullable: true, select: false }) + phone?: string; // phone number of the user + + @Column({ select: false }) + desktop: boolean = false; // if the user has desktop app installed + + @Column({ select: false }) + mobile: boolean = false; // if the user has mobile app installed + + @Column() + premium: boolean = Config.get().defaults.user.premium; // if user bought individual premium + + @Column() + premium_type: number = Config.get().defaults.user.premium_type; // individual premium level + + @Column() + bot: boolean = false; // if user is bot + + @Column() + bio: string; // short description of the user (max 190 chars -> should be configurable) + + @Column() + system: boolean = false; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author + + @Column({ select: false }) + nsfw_allowed: boolean = true; // if the user can do age-restricted actions (NSFW channels/guilds/commands) // TODO: depending on age + + @Column({ select: false }) + mfa_enabled: boolean; // if multi factor authentication is enabled + + @Column({ select: false, nullable: true }) + totp_secret?: string; + + @Column({ nullable: true, select: false }) + totp_last_ticket?: string; + + @Column() + created_at: Date = new Date(); // registration date + + @Column({ nullable: true }) + premium_since: Date = new Date(); // premium date + + @Column({ select: false }) + verified: boolean = Config.get().defaults.user.verified; // if the user is offically verified + + @Column() + disabled: boolean = false; // if the account is disabled + + @Column() + deleted: boolean = false; // if the user was deleted + + @Column({ nullable: true, select: false }) + email?: string; // email of the user + + @Column() + flags: string = "0"; // UserFlags // TODO: generate + + @Column() + public_flags: number = 0; + + @Column({ type: "bigint" }) + rights: string = Config.get().register.defaultRights; // Rights + + @OneToMany(() => Session, (session: Session) => session.user) + sessions: Session[]; + + @JoinColumn({ name: "relationship_ids" }) + @OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, { + cascade: true, + orphanedRowAction: "delete", + }) + relationships: Relationship[]; + + @JoinColumn({ name: "connected_account_ids" }) + @OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, { + cascade: true, + orphanedRowAction: "delete", + }) + connected_accounts: ConnectedAccount[]; + + @Column({ type: "simple-json", select: false }) + data: { + valid_tokens_since: Date; // all tokens with a previous issue date are invalid + hash?: string; // hash of the password, salt is saved in password (bcrypt) + }; + + @Column({ type: "simple-array", select: false }) + fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts + + + @OneToOne(()=> UserSettings, { + cascade: true, + orphanedRowAction: "delete", + eager: false + }) + @JoinColumn() + settings: UserSettings; + + // workaround to prevent fossord-unaware clients from deleting settings not used by them + @Column({ type: "simple-json", select: false }) + extended_settings: string = "{}"; + + @Column({ type: "simple-json" }) + notes: { [key: string]: string } = {}; //key is ID of user + + async save(): Promise { + if(!this.settings) this.settings = new UserSettings(); + this.settings.id = this.id; + //await this.settings.save(); + return super.save(); + } + + toPublicUser() { + const user: any = {}; + PublicUserProjection.forEach((x) => { + user[x] = this[x]; + }); + return user as PublicUser; + } + + static async getPublicUser(user_id: string, opts?: FindOneOptions) { + return await User.findOneOrFail({ + where: { id: user_id }, + select: [...PublicUserProjection, ...((opts?.select as FindOptionsSelectByString) || [])], + ...opts, + }); + } + + public static async generateDiscriminator(username: string): Promise { + if (Config.get().register.incrementingDiscriminators) { + // discriminator will be incrementally generated + + // First we need to figure out the currently highest discrimnator for the given username and then increment it + const users = await User.find({ where: { username }, select: ["discriminator"] }); + const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator))); + + const discriminator = highestDiscriminator + 1; + if (discriminator >= 10000) { + return undefined; + } + + return discriminator.toString().padStart(4, "0"); + } else { + // discriminator will be randomly generated + + // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists + // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database? + for (let tries = 0; tries < 5; tries++) { + const discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); + const exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] }); + if (!exists) return discriminator; + } + + return undefined; + } + } + + static async register({ + email, + username, + password, + date_of_birth, + req, + }: { + username: string; + password?: string; + email?: string; + date_of_birth?: Date; // "2000-04-03" + req?: any; + }) { + // trim special uf8 control characters -> Backspace, Newline, ... + username = trimSpecial(username); + + const discriminator = await User.generateDiscriminator(username); + if (!discriminator) { + // We've failed to generate a valid and unused discriminator + throw FieldErrors({ + username: { + code: "USERNAME_TOO_MANY_USERS", + message: req?.t("auth:register.USERNAME_TOO_MANY_USERS"), + }, + }); + } + + // TODO: save date_of_birth + // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed + // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false + const language = req?.language === "en" ? "en-US" : req?.language || "en-US"; + + const user = OrmUtils.mergeDeep(new User(), { + //required: + username: username, + discriminator, + id: Snowflake.generate(), + email: email, + data: { + hash: password, + valid_tokens_since: new Date(), + }, + settings: { ...new UserSettings(), locale: language } + }); + + await user.save(); + await user.settings.save(); + + setImmediate(async () => { + if (Config.get().guild.autoJoin.enabled) { + for (const guild of Config.get().guild.autoJoin.guilds || []) { + await Member.addToGuild(user.id, guild).catch((e) => {}); + } + } + }); + + return user; + } +} + +export const CUSTOM_USER_FLAG_OFFSET = BigInt(1) << BigInt(32); + +export class UserFlags extends BitField { + static FLAGS = { + DISCORD_EMPLOYEE: BigInt(1) << BigInt(0), + PARTNERED_SERVER_OWNER: BigInt(1) << BigInt(1), + HYPESQUAD_EVENTS: BigInt(1) << BigInt(2), + BUGHUNTER_LEVEL_1: BigInt(1) << BigInt(3), + MFA_SMS: BigInt(1) << BigInt(4), + PREMIUM_PROMO_DISMISSED: BigInt(1) << BigInt(5), + HOUSE_BRAVERY: BigInt(1) << BigInt(6), + HOUSE_BRILLIANCE: BigInt(1) << BigInt(7), + HOUSE_BALANCE: BigInt(1) << BigInt(8), + EARLY_SUPPORTER: BigInt(1) << BigInt(9), + TEAM_USER: BigInt(1) << BigInt(10), + TRUST_AND_SAFETY: BigInt(1) << BigInt(11), + SYSTEM: BigInt(1) << BigInt(12), + HAS_UNREAD_URGENT_MESSAGES: BigInt(1) << BigInt(13), + BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14), + UNDERAGE_DELETED: BigInt(1) << BigInt(15), + VERIFIED_BOT: BigInt(1) << BigInt(16), + EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17), + CERTIFIED_MODERATOR: BigInt(1) << BigInt(18), + BOT_HTTP_INTERACTIONS: BigInt(1) << BigInt(19), + }; +} -- cgit 1.5.1 From 6d9ea1a7bc9d44a547fe1566393e122ae252eb8c Mon Sep 17 00:00:00 2001 From: TheArcaneBrony Date: Mon, 15 Aug 2022 09:47:49 +0200 Subject: Fix nullables, fix user settings hanging stuff --- scripts/db_migrations.sh | 6 +- src/gateway/opcodes/Identify.ts | 2 +- src/util/entities/User.ts | 6 +- .../mariadb/1660549252130-fix_nullables.ts | 30 +++ .../postgres/1660549242936-fix_nullables.ts | 30 +++ .../sqlite/1660549233583-fix_nullables.ts | 240 +++++++++++++++++++++ 6 files changed, 307 insertions(+), 7 deletions(-) create mode 100644 src/util/migrations/mariadb/1660549252130-fix_nullables.ts create mode 100644 src/util/migrations/postgres/1660549242936-fix_nullables.ts create mode 100644 src/util/migrations/sqlite/1660549233583-fix_nullables.ts (limited to 'src/util/entities/User.ts') diff --git a/scripts/db_migrations.sh b/scripts/db_migrations.sh index 89404878..9ec8230a 100755 --- a/scripts/db_migrations.sh +++ b/scripts/db_migrations.sh @@ -19,9 +19,9 @@ make_migration() { mkdir "src/util/migrations/$2" 2>/dev/null # npm run build clean logerrors pretty-errors THREADS=1 DATABASE="$1" DB_MIGRATE=a npm run start:bundle - THREADS=1 DATABASE="$1" DB_MIGRATE=a npx typeorm-ts-node-commonjs migration:generate "src/migrations/$2/$FILENAME" -d src/util/util/Database.ts -p - npm run build clean logerrors pretty-errors - THREADS=1 DATABASE="$1" DB_MIGRATE=a npm run start:bundle + THREADS=1 DATABASE="$1" DB_MIGRATE=a npx typeorm-ts-node-commonjs migration:generate "src/util/migrations/$2/$FILENAME" -d src/util/util/Database.ts -p + #npm run build clean logerrors pretty-errors + #THREADS=1 DATABASE="$1" DB_MIGRATE=a npm run start:bundle } npm i sqlite3 diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index d5dae7b0..44db598c 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -105,7 +105,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { if (!user.settings) { //settings may not exist after updating... user.settings = new UserSettings(); user.settings.id = user.id; - await user.settings.save(); + //await (user.settings as UserSettings).save(); } if (!identify.intents) identify.intents = "30064771071"; diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 61343e81..5432f298 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -97,7 +97,7 @@ export class User extends BaseClass { @Column() bot: boolean = false; // if user is bot - @Column() + @Column({ nullable: true }) bio: string; // short description of the user (max 190 chars -> should be configurable) @Column() @@ -106,7 +106,7 @@ export class User extends BaseClass { @Column({ select: false }) nsfw_allowed: boolean = true; // if the user can do age-restricted actions (NSFW channels/guilds/commands) // TODO: depending on age - @Column({ select: false }) + @Column({ select: false, nullable: true }) mfa_enabled: boolean; // if multi factor authentication is enabled @Column({ select: false, nullable: true }) @@ -281,8 +281,8 @@ export class User extends BaseClass { settings: { ...new UserSettings(), locale: language } }); + //await (user.settings as UserSettings).save(); await user.save(); - await user.settings.save(); setImmediate(async () => { if (Config.get().guild.autoJoin.enabled) { diff --git a/src/util/migrations/mariadb/1660549252130-fix_nullables.ts b/src/util/migrations/mariadb/1660549252130-fix_nullables.ts new file mode 100644 index 00000000..c9456b54 --- /dev/null +++ b/src/util/migrations/mariadb/1660549252130-fix_nullables.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class fixNullables1660549252130 implements MigrationInterface { + name = 'fixNullables1660549252130' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX \`IDX_76ba283779c8441fd5ff819c8c\` ON \`users\` + `); + await queryRunner.query(` + ALTER TABLE \`users\` CHANGE \`bio\` \`bio\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`users\` CHANGE \`mfa_enabled\` \`mfa_enabled\` tinyint NULL + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE \`users\` CHANGE \`mfa_enabled\` \`mfa_enabled\` tinyint NOT NULL + `); + await queryRunner.query(` + ALTER TABLE \`users\` CHANGE \`bio\` \`bio\` varchar(255) NOT NULL + `); + await queryRunner.query(` + CREATE UNIQUE INDEX \`IDX_76ba283779c8441fd5ff819c8c\` ON \`users\` (\`settingsId\`) + `); + } + +} diff --git a/src/util/migrations/postgres/1660549242936-fix_nullables.ts b/src/util/migrations/postgres/1660549242936-fix_nullables.ts new file mode 100644 index 00000000..b9a0194d --- /dev/null +++ b/src/util/migrations/postgres/1660549242936-fix_nullables.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class fixNullables1660549242936 implements MigrationInterface { + name = 'fixNullables1660549242936' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "users" + ALTER COLUMN "bio" DROP NOT NULL + `); + await queryRunner.query(` + ALTER TABLE "users" + ALTER COLUMN "mfa_enabled" DROP NOT NULL + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "users" + ALTER COLUMN "mfa_enabled" + SET NOT NULL + `); + await queryRunner.query(` + ALTER TABLE "users" + ALTER COLUMN "bio" + SET NOT NULL + `); + } + +} diff --git a/src/util/migrations/sqlite/1660549233583-fix_nullables.ts b/src/util/migrations/sqlite/1660549233583-fix_nullables.ts new file mode 100644 index 00000000..68f650c7 --- /dev/null +++ b/src/util/migrations/sqlite/1660549233583-fix_nullables.ts @@ -0,0 +1,240 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class fixNullables1660549233583 implements MigrationInterface { + name = 'fixNullables1660549233583' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "temporary_users" ( + "id" varchar PRIMARY KEY NOT NULL, + "username" varchar NOT NULL, + "discriminator" varchar NOT NULL, + "avatar" varchar, + "accent_color" integer, + "banner" varchar, + "phone" varchar, + "desktop" boolean NOT NULL, + "mobile" boolean NOT NULL, + "premium" boolean NOT NULL, + "premium_type" integer NOT NULL, + "bot" boolean NOT NULL, + "bio" varchar, + "system" boolean NOT NULL, + "nsfw_allowed" boolean NOT NULL, + "mfa_enabled" boolean, + "totp_secret" varchar, + "totp_last_ticket" varchar, + "created_at" datetime NOT NULL, + "premium_since" datetime, + "verified" boolean NOT NULL, + "disabled" boolean NOT NULL, + "deleted" boolean NOT NULL, + "email" varchar, + "flags" varchar NOT NULL, + "public_flags" integer NOT NULL, + "rights" bigint NOT NULL, + "data" text NOT NULL, + "fingerprints" text NOT NULL, + "extended_settings" text NOT NULL, + "notes" text NOT NULL, + "settingsId" varchar, + CONSTRAINT "UQ_b1dd13b6ed980004a795ca184a6" UNIQUE ("settingsId"), + CONSTRAINT "FK_76ba283779c8441fd5ff819c8cf" FOREIGN KEY ("settingsId") REFERENCES "user_settings" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_users"( + "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "notes", + "settingsId" + ) + SELECT "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "notes", + "settingsId" + FROM "users" + `); + await queryRunner.query(` + DROP TABLE "users" + `); + await queryRunner.query(` + ALTER TABLE "temporary_users" + RENAME TO "users" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "users" + RENAME TO "temporary_users" + `); + await queryRunner.query(` + CREATE TABLE "users" ( + "id" varchar PRIMARY KEY NOT NULL, + "username" varchar NOT NULL, + "discriminator" varchar NOT NULL, + "avatar" varchar, + "accent_color" integer, + "banner" varchar, + "phone" varchar, + "desktop" boolean NOT NULL, + "mobile" boolean NOT NULL, + "premium" boolean NOT NULL, + "premium_type" integer NOT NULL, + "bot" boolean NOT NULL, + "bio" varchar NOT NULL, + "system" boolean NOT NULL, + "nsfw_allowed" boolean NOT NULL, + "mfa_enabled" boolean NOT NULL, + "totp_secret" varchar, + "totp_last_ticket" varchar, + "created_at" datetime NOT NULL, + "premium_since" datetime, + "verified" boolean NOT NULL, + "disabled" boolean NOT NULL, + "deleted" boolean NOT NULL, + "email" varchar, + "flags" varchar NOT NULL, + "public_flags" integer NOT NULL, + "rights" bigint NOT NULL, + "data" text NOT NULL, + "fingerprints" text NOT NULL, + "extended_settings" text NOT NULL, + "notes" text NOT NULL, + "settingsId" varchar, + CONSTRAINT "UQ_b1dd13b6ed980004a795ca184a6" UNIQUE ("settingsId"), + CONSTRAINT "FK_76ba283779c8441fd5ff819c8cf" FOREIGN KEY ("settingsId") REFERENCES "user_settings" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION + ) + `); + await queryRunner.query(` + INSERT INTO "users"( + "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "notes", + "settingsId" + ) + SELECT "id", + "username", + "discriminator", + "avatar", + "accent_color", + "banner", + "phone", + "desktop", + "mobile", + "premium", + "premium_type", + "bot", + "bio", + "system", + "nsfw_allowed", + "mfa_enabled", + "totp_secret", + "totp_last_ticket", + "created_at", + "premium_since", + "verified", + "disabled", + "deleted", + "email", + "flags", + "public_flags", + "rights", + "data", + "fingerprints", + "extended_settings", + "notes", + "settingsId" + FROM "temporary_users" + `); + await queryRunner.query(` + DROP TABLE "temporary_users" + `); + } + +} -- cgit 1.5.1