Newer
Older
percord / util / src / entities / User.ts
@Flam3rboy Flam3rboy on 29 Aug 2021 6 KB :sparkles: update util
import { Column, Entity, FindOneOptions, JoinColumn, OneToMany, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { BitField } from "../util/BitField";
import { Relationship } from "./Relationship";
import { ConnectedAccount } from "./ConnectedAccount";
import { HTTPError } from "lambert-server";

type PublicUserKeys =
	| "username"
	| "discriminator"
	| "id"
	| "public_flags"
	| "avatar"
	| "accent_color"
	| "banner"
	| "bio"
	| "bot";
export const PublicUserProjection: PublicUserKeys[] = [
	"username",
	"discriminator",
	"id",
	"public_flags",
	"avatar",
	"accent_color",
	"banner",
	"bio",
	"bot",
];

// Private user data that should never get sent to the client
export type PublicUser = Pick<User, PublicUserKeys>;

@Entity("users")
export class User extends BaseClass {
	@Column()
	username: string; // username max length 32, min 2 (should be configurable)

	@Column()
	discriminator: string; // #0001 4 digit long string from #0001 - #9999

	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 = 0; // banner color of user

	@Column({ nullable: true })
	banner?: string; // hash of the user banner

	@Column({ nullable: true })
	phone?: string; // phone number of the user

	@Column()
	desktop: boolean = false; // if the user has desktop app installed

	@Column()
	mobile: boolean = false; // if the user has mobile app installed

	@Column()
	premium: boolean = false; // if user bought nitro

	@Column()
	premium_type: number = 0; // nitro 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 sents this field type true, if the generated message comes from a system generated author

	@Column()
	nsfw_allowed: boolean = false; // if the user is older than 18 (resp. Config)

	@Column()
	mfa_enabled: boolean = false; // if multi factor authentication is enabled

	@Column()
	created_at: Date = new Date(); // registration date

	@Column()
	verified: boolean = false; // 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 })
	email?: string; // email of the user

	@Column({ type: "bigint" })
	flags: bigint = BigInt(0); // UserFlags

	@Column({ type: "bigint" })
	public_flags: bigint = BigInt(0);

	@RelationId((user: User) => user.relationships)
	relationship_ids: string[]; // array of guild ids the user is part of

	@JoinColumn({ name: "relationship_ids" })
	@OneToMany(() => Relationship, (relationship: Relationship) => relationship.user, { cascade: true })
	relationships: Relationship[];

	@RelationId((user: User) => user.connected_accounts)
	connected_account_ids: string[]; // array of guild ids the user is part of

	@JoinColumn({ name: "connected_account_ids" })
	@OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user)
	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)
	} = { valid_tokens_since: new Date() };

	@Column({ type: "simple-array" })
	fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts

	@Column({ type: "simple-json" })
	settings: UserSettings = defaultSettings;

	static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
		const user = await User.findOne(user_id, {
			...opts,
			select: [...PublicUserProjection, ...(opts?.select || [])],
		});
		if (!user) throw new HTTPError("User not found", 404);
		return user;
	}
}

export const defaultSettings: UserSettings = {
	afk_timeout: 300,
	allow_accessibility_detection: true,
	animate_emoji: true,
	animate_stickers: 0,
	contact_sync_enabled: false,
	convert_emoticons: false,
	custom_status: {
		emoji_id: undefined,
		emoji_name: undefined,
		expires_at: undefined,
		text: undefined,
	},
	default_guilds_restricted: false,
	detect_platform_accounts: true,
	developer_mode: false,
	disable_games_tab: false,
	enable_tts_command: true,
	explicit_content_filter: 0,
	friend_source_flags: { all: true },
	gateway_connected: false,
	gif_auto_play: true,
	guild_folders: [],
	guild_positions: [],
	inline_attachment_media: true,
	inline_embed_media: true,
	locale: "en",
	message_display_compact: false,
	native_phone_integration_enabled: true,
	render_embeds: true,
	render_reactions: true,
	restricted_guilds: [],
	show_current_game: true,
	status: "offline",
	stream_notifications_enabled: true,
	theme: "dark",
	timezone_offset: 0,
	// timezone_offset: // TODO: timezone from request
};

export interface UserSettings {
	afk_timeout: number;
	allow_accessibility_detection: boolean;
	animate_emoji: boolean;
	animate_stickers: number;
	contact_sync_enabled: boolean;
	convert_emoticons: boolean;
	custom_status: {
		emoji_id?: string;
		emoji_name?: string;
		expires_at?: number;
		text?: string;
	};
	default_guilds_restricted: boolean;
	detect_platform_accounts: boolean;
	developer_mode: boolean;
	disable_games_tab: boolean;
	enable_tts_command: boolean;
	explicit_content_filter: number;
	friend_source_flags: { all: boolean };
	gateway_connected: boolean;
	gif_auto_play: boolean;
	// every top guild is displayed as a "folder"
	guild_folders: {
		color: number;
		guild_ids: string[];
		id: number;
		name: string;
	}[];
	guild_positions: string[]; // guild ids ordered by position
	inline_attachment_media: boolean;
	inline_embed_media: boolean;
	locale: string; // en_US
	message_display_compact: boolean;
	native_phone_integration_enabled: boolean;
	render_embeds: boolean;
	render_reactions: boolean;
	restricted_guilds: string[];
	show_current_game: boolean;
	status: "online" | "offline" | "dnd" | "idle";
	stream_notifications_enabled: boolean;
	theme: "dark" | "white"; // dark
	timezone_offset: number; // e.g -60
}

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),
		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),
		SYSTEM: BigInt(1) << BigInt(12),
		BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14),
		VERIFIED_BOT: BigInt(1) << BigInt(16),
		EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17),
	};
}