summary refs log tree commit diff
path: root/src/util/entities/User.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/entities/User.ts')
-rw-r--r--src/util/entities/User.ts408
1 files changed, 0 insertions, 408 deletions
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
deleted file mode 100644

index afde3e9b..00000000 --- a/src/util/entities/User.ts +++ /dev/null
@@ -1,408 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { Request } from "express"; -import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm"; -import { Channel, Config, Email, FieldErrors, Snowflake, Stopwatch, trimSpecial } from ".."; -import { Random } from "../util"; -import { BaseClass } from "./BaseClass"; -import { ConnectedAccount } from "./ConnectedAccount"; -import { Member } from "./Member"; -import { Relationship } from "./Relationship"; -import { SecurityKey } from "./SecurityKey"; -import { Session } from "./Session"; -import { UserSettings } from "./UserSettings"; -import { - AvatarDecorationData, - ChannelType, - Collectibles, - DisplayNameStyle, - PrimaryGuild, - PrivateUserProjection, - PublicUser, - PublicUserProjection, - UserPrivate, -} from "@spacebar/schemas"; -import { JsonNumber } from "../util/Decorators"; - -@Entity({ - name: "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 - - @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 - - // TODO: Separate `User` and `UserProfile` models - // puyo: changed from [number, number] because it breaks openapi - @Column({ nullable: true, type: "int4", array: true }) - theme_colors?: number[]; - - @Column({ nullable: true }) - pronouns?: string; - - @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; // if user bought individual premium - - @Column() - premium_type: number; // individual premium level - - @Column() - bot: boolean = false; // if user is bot - - @Column() - bio: string = ""; // short description of the user - - @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 = false; // if multi factor authentication is enabled - - @Column({ select: false, default: false }) - webauthn_enabled: boolean = false; // if webauthn 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; // registration date - - @Column({ nullable: true }) - premium_since: Date; // premium date - - @Column({ select: false }) - verified: boolean; // email is 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({ type: "bigint" }) - @JsonNumber - flags: number = 0; // UserFlags // TODO: generate - - @Column({ type: "bigint" }) - @JsonNumber - public_flags: number = 0; - - @Column({ type: "bigint" }) - @JsonNumber - purchased_flags: number = 0; - - @Column() - premium_usage_flags: number = 0; - - @Column({ type: "bigint" }) - @JsonNumber - rights: string; - - @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: "jsonb", 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: "varchar", array: true, select: false }) - fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts - - @OneToOne(() => UserSettings, { - cascade: true, - orphanedRowAction: "delete", - nullable: true, - }) - @JoinColumn() - settings?: UserSettings; - - @OneToMany(() => SecurityKey, (key: SecurityKey) => key.user) - security_keys: SecurityKey[]; - - @Column({ type: "int8", array: true, nullable: true }) - badge_ids?: string[]; - - @Column({ type: "jsonb", nullable: true }) - avatar_decoration_data?: AvatarDecorationData; - - @Column({ type: "jsonb", nullable: true }) - display_name_styles?: DisplayNameStyle; - - @Column({ type: "jsonb", nullable: true }) - collectibles?: Collectibles; - - @Column({ type: "jsonb", nullable: true }) - primary_guild?: PrimaryGuild; - - // TODO: I don't like this method? - validate() { - if (this.discriminator) { - const discrim = Number(this.discriminator); - if (isNaN(discrim) || !Number.isInteger(discrim) || discrim <= 0 || discrim >= 10000) - throw FieldErrors({ - discriminator: { - message: "Discriminator must be a number.", - code: "DISCRIMINATOR_INVALID", - }, - }); - - this.discriminator = discrim.toString().padStart(4, "0"); - } - } - - toPublicUser() { - this.clean_data(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = {}; - PublicUserProjection.forEach((x) => { - user[x] = this[x]; - }); - return user as PublicUser; - } - - toPrivateUser(extraFields: (keyof User)[] = []) { - this.clean_data(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = {}; - [...PrivateUserProjection, ...extraFields].forEach((x) => { - user[x] = this[x]; - }); - return user as UserPrivate; - } - - static async getPublicUser(user_id: string): Promise<PublicUser> { - const user = await User.findOneOrFail({ - where: { id: user_id }, - select: PublicUserProjection, - }); - return user.toPublicUser(); - } - - public static async generateDiscriminator(username: string): Promise<string | undefined> { - 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: true }, - }); - 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? - const takenDiscriminators = (await User.find({ where: { username }, select: { discriminator: true } })).map((x) => x.discriminator); - if (takenDiscriminators.length >= 9999) return undefined; - - for (let tries = 0; tries < 15; tries++) { - const discriminator = Random.nextInt(1, 9999).toString().padStart(4, "0"); - if (!takenDiscriminators.includes(discriminator)) return discriminator; - } - - return undefined; - } - } - - public get tag(): string { - //const { uniqueUsernames } = Config.get().general; - const uniqueUsernames = false; - - return uniqueUsernames ? this.username : `${this.username}#${this.discriminator}`; - } - - static async register({ - email, - username, - password, - id, - req, - bot, - }: { - username: string; - password?: string; - email?: string; - date_of_birth?: Date; // "2000-04-03" - id?: string; - req?: Request; - bot?: boolean; - }) { - const totalSw = Stopwatch.startNew(); - const incSw = Stopwatch.startNew(); - const logTrace = (...data: unknown[]) => { - if (process.env.LOG_VERBOSE_TRACES !== "true") return; - console.log("[User.register]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); - }; - - // trim special utf8 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") || "", - }, - }); - } - logTrace("Generate discriminator"); - - // TODO: save date_of_birth - // apparently 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 settings = UserSettings.create({ - locale: language, - }); - - const user = User.create({ - username: username, - discriminator, - id: id || Snowflake.generate(), - email: email, - data: { - hash: password, - valid_tokens_since: new Date(), - }, - settings: settings, - - premium_since: Config.get().defaults.user.premium ? new Date() : undefined, - rights: Config.get().register.defaultRights, - premium: Config.get().defaults.user.premium ?? false, - premium_type: Config.get().defaults.user.premiumType ?? 0, - verified: Config.get().defaults.user.verified ?? true, - created_at: new Date(), - bot: !!bot, - }); - - user.validate(); - logTrace("Generate/validate user"); - - await Promise.all([user.save(), settings.save()]); - logTrace("Save user"); - - // send verification email if users aren't verified by default and we have an email - if (!Config.get().defaults.user.verified && email) { - await Email.sendVerifyEmail(user, email).catch((e) => { - console.error(`Failed to send verification email to ${user.tag}: ${e}`); - }); - logTrace("Send verify email"); - } - - const { autoJoin } = Config.get().guild; - if (autoJoin.enabled && autoJoin.guilds.length > 0 && !(bot && !autoJoin.bots)) { - await Promise.all(autoJoin.guilds.map((guild) => Member.addToGuild(user.id, guild, true).catch((e) => console.error("[Autojoin]", e)))); - logTrace("Autojoin", autoJoin.guilds.length, "guilds"); - } - - return user; - } - - async getDmChannelWith(user_id: string) { - const qry = await Channel.getRepository() - .createQueryBuilder() - .leftJoinAndSelect("Channel.recipients", "rcp") - .where("Channel.type = :type", { type: ChannelType.DM }) - .andWhere("rcp.user_id IN (:...user_ids)", { user_ids: [this.id, user_id] }) - .groupBy("Channel.id") - .having("COUNT(rcp.user_id) = 2") - .getMany(); - - // Emma [it/its]@Rory&: is this technically a bug, or am I being too over-cautious? - if (qry.length > 1) { - console.warn(`[WARN] User(${this.id})#getDmChannel(${user_id}) returned multiple channels:`); - for (const channel of qry) { - console.warn(JSON.stringify(channel)); - } - throw new Error("Array contains more than one matching element"); - } - - return qry[0]; - } - - async getDmChannels() { - const qry = await Channel.getRepository() - .createQueryBuilder("channel") - .leftJoinAndSelect("channel.recipients", "rcp") - .where("channel.type = :type", { type: ChannelType.DM }) - .andWhere("rcp.user_id = :user_id", { user_id: this.id }) - .groupBy("channel.id") - .addGroupBy("rcp.id") - .having("COUNT(rcp.id) = 2") - .getMany(); - - return qry; - } -}