Source

classes/user.ts

import IUser, { UserResolvable } from "../types/user";
import Snowflake from "./snowflake";
import Language, { LanguageCodes } from "../types/language";
import { SnowflakeResolvable, SnowflakeString } from "../types/snowflake";
import { UserType } from "../types/basicUser";
import Collection from "@discordjs/collection";
import Animation, { BasicAnimation } from "./animation";
import Category from "./category";
import Comment from "./comment";
import SystemRelation from "./systemRelation";
import File from "./file";
import Rating from "./rating";
import UserRelation from "./userRelation";
import { RequestContext } from "./requestContext";
import UserFilters from "./userFilters";
import { DBUser, prisma } from "../../prisma";
import { FileState } from "../types/file";

/**
 * User data class
 */
class User {
    id: Snowflake;
    ctx: RequestContext;

    avatar?: File;
    avatarId?: Snowflake;
    name: string;
    description: string;
    email?: string;
    password: string;
    language: Language;

    auth: string;
    type: UserType;
    lastLogedIn?: Date;

    reported?: boolean;

    /**
     * User class constructor
     * @param {Object} param0 user data
     */
    constructor({
        id,
        ctx,
        name = "",
        avatar,
        avatarId,
        description = "",
        email = "",
        auth = "",
        password = "",
        language = Language.EN,
        type = UserType.MEMBER,
        lastLogedIn,
        reported
    }: {
        id?: Snowflake,
        ctx: RequestContext,

        name?: string,
        avatar?: File,
        avatarId?: Snowflake,
        description?: string,
        email?: string,
        password?: string,
        language?: Language,

        auth?: string,
        type?: UserType,
        lastLogedIn?: Date,

        reported?: boolean
    }) {
        this.id = id || Snowflake.newSnowflake();
        this.ctx = ctx;
        this.name = name;
        this.avatar = avatar;
        this.avatarId = avatarId;
        this.description = description;
        this.email = email;
        this.auth = auth;
        this.type = type;
        this.language = language;
        this.password = password;
        this.lastLogedIn = lastLogedIn;
        this.reported = reported;
    }

    /**
     * Checks if user resolvable is user or not. Used for TS.
     * @param {UserResolvable} user
     * @return {boolean}
     */
    static isUser(user: UserResolvable): user is User | IUser {
        return user instanceof User;
    }

    /**
     * Resolves params to string
     * @param {UserResolvable} user
     * @return {SnowflakeString}
     */
    static resolveUserID(user: UserResolvable): SnowflakeString {
        if(User.isUser(user)) return user.id.toString();
        return user.toString();
    }

    /**
     * Loads the user from database data
     *
     * @param {DBUser} data
     * @param {RequestContext} ctx
     *
     * @return {User}
     */
    static fromDatabase(data: DBUser, ctx: RequestContext) {
        if(ctx.users.has(data.snowflake.toString())) {
            return ctx.users.get(data.snowflake.toString()) as User;
        }

        const user = new User({
            ctx,
            id: new Snowflake(data.snowflake),
            name: data.name,
            description: data.description || undefined,
            email: data.email || undefined,
            auth: data.auth || undefined,
            type: data.type,
            lastLogedIn: data.lastLogedIn,
            language: data.languageId,
            avatarId: data.avatarId ? new Snowflake(data.avatarId) : undefined,
            password: data.password
        });

        ctx.users.set(user.id.toString(), user);

        return user;
    }

    /**
     * Loads user from DB (OLD)
     *
     * @deprecated
     * @param {SnowflakeResolvable} ids
     * @param {any} user
     * @param {RequestContext} ctx
     *
     * @return {User}
     */
    private static userFromDB(ids: SnowflakeResolvable, user: any, ctx: RequestContext) {
        return new User({
            id: new Snowflake(ids),
            ctx,
            name: user.name,
            description: user.description,
            email: user.email,
            auth: user.auth,
            type: user.type,
            lastLogedIn: new Date(user.last_loged),
            language: user.language,
            reported: user.reported
        });
    }

    /**
     * Loads user from DB
     * @param {number|Snowflake|string} id User snowflake to load
     * @param {RequestContext} ctx
     * @param {UserClient} client
     * @return {Promise<User>} user
     */
    static async getUser(id: UserResolvable, ctx: RequestContext):
        Promise<User> {
        if(User.isUser(id)) return id as User;
        if(ctx.users.has(id.toString())) return ctx.users.get(id.toString())!;
        if(ctx.user && ctx.user.id.toString() == id.toString()) return ctx.user;
        let useSnowflake = false;
        if(/^[0-9]+$/.test(id.toString())) useSnowflake = true;

        const userData = await prisma.user.findFirstOrThrow({
            where: {
                OR: [...(useSnowflake ? [{
                    snowflake: new Snowflake(id).toBigInt()
                }] : []), {
                    name: id.toString()
                }]
            },
            orderBy: {
                snowflake: "desc"
            }
        });

        const user = this.fromDatabase(userData, ctx);

        if(ctx.client) {
            const reported = await prisma.modQueue.findFirst({
                where: {
                    reporterId: ctx.client.user.id.toBigInt(),
                    userId: user.id.toBigInt()
                }
            });

            user.reported = !!reported;
        }

        return user;
    }

    /**
     * Searches users
     *
     * @param {UserFilters} filters
     * @return {Promise<Collection<string, User>>}
     */
    static searchUsers(filters: UserFilters): Promise<Collection<string, User>> {
        const { query, variables } = filters.buildQuery();
        return new Promise((resolve, reject) => {
            filters.ctx.conn.query(query, variables, async (err, res) => {
                if(err) return void reject(err);
                const users = new Collection<string, User>();
                for(const user of res) {
                    const u = User.userFromDB(
                        new Snowflake(BigInt(user.snowflake)), user, filters.ctx
                    );
                    if(user.avatar) u.avatar = await File.load(user.avatar, filters.ctx);
                    users.set(u.id.toString(), u);
                }
                resolve(users);
            });
        });
    }

    /**
     * Returns number of results for a specific search
     *
     * @param {UserFilters} filters
     * @return {number}
     */
    static searchUsersCount(filters: UserFilters): Promise<number> {
        const { query, variables } = filters.buildCountQuery();
        return new Promise((resolve, reject) => {
            filters.ctx.conn.query(query, variables, (err, res) => {
                if(err) return void reject(err);
                resolve(res[0].count);
            });
        });
    }

    /**
     * Gets user's avatar
     *
     * @return {Promise<File|undefined>}
     */
    async getAvatar(): Promise<File | undefined> {
        if(!this.avatarId) return undefined;
        if(this.avatar) return this.avatar;
        return await File.load(this.avatarId, this.ctx);
    }

    /**
     * Counts friends
     * @return {Promise<number>}
     */
    async countFollowers(): Promise<number> {
        const res = await prisma.userRelation.count({
            where: {
                targetId: this.id.toBigInt(),
                type: 1
            }
        });

        return res;
    }


    /**
     * Gets relation between client and specified user
     * @param {UserResolvable} uid
     * @return {Promise<UserRelation>}
     */
    async getRelation(uid: UserResolvable):
        Promise<UserRelation|null> {
        const res = await prisma.userRelation.findFirst({
            where: {
                authorId: this.id.toBigInt(),
                targetId: BigInt(User.resolveUserID(uid))
            }
        });

        if(!res) return null;
        return UserRelation.fromDatabase(res, this.ctx);
    }

    /**
     * Gets followed users
     *
     * @return {Promise<UserRelation[]>}
     */
    async getFollows(): Promise<UserRelation[]> {
        const relations = await prisma.userRelation.findMany({
            where: {
                authorId: this.id.toBigInt(),
                type: 1
            },

            include: {
                target: true
            }
        });

        return await Promise.all(
            relations.map((data) =>UserRelation.fromDatabase(data, this.ctx))
        );
    }

    /**
     * Gets followers
     *
     * @return {Promise<UserRelation[]>}
     */
    async getFollowers(): Promise<UserRelation[]> {
        const relations = await prisma.userRelation.findMany({
            where: {
                targetId: this.id.toBigInt(),
                type: 1
            },
            include: {
                author: true
            }
        });

        return await Promise.all(
            relations.map((data) => UserRelation.fromDatabase(data, this.ctx))
        );
    }

    /**
     * Gets animations created by user
     * @param {number} max Max animations per page
     * @param {number} page Current page to show
     * @return {Promise<Collection<SnowflakeString, BasicAnimation>>}
     */
    async getAnimations(max: number, page: number):
        Promise<Collection<SnowflakeString, BasicAnimation>> {
        const animations = await prisma.animation.findMany({
            where: {
                authorId: this.id.toBigInt(),
                category: this.ctx.client && this.ctx.client.user.type >= UserType.MODERATOR ? {
                    snowflake: {
                        not: 0
                    }
                } : undefined,
                published: this.ctx.client &&
                    (this.ctx.client.user.id.toString() === this.id.toString() ||
                        this.ctx.client.user.type >= UserType.MODERATOR) ?
                    undefined : {
                        not: null
                    }
            },
            orderBy: {
                snowflake: "desc"
            },
            skip: page * max,
            take: max
        });

        var col = new Collection<SnowflakeString, BasicAnimation>();
        for(var anim of animations) {
            col.set(anim.snowflake.toString(), await BasicAnimation.fromDatabase(anim, this.ctx));
        }

        return col;
    }

    /**
     * Gets number of animations created by user
     * @return {Promise<number>}
     */
    async getAnimationsCount(): Promise<number> {
        return prisma.animation.count({
            where: {
                authorId: this.id.toBigInt()
            }
        });
    }

    /**
     * Gets if the user has active premium or not.
     * Should not be used for 'is user getting billed' - can be mod for that.
     *
     * @return {boolean}
     */
    hasPremium(): boolean {
        return this.type >= UserType.PREMIUM;
        // if higher than premium, user is mod which also includes having premium perks.
    }

    /**
     * Gets ratings created by user
     * @param {number} max Max items per page
     * @param {number} page Current page
     * @return {Promise<Collection<SnowflakeString, Rating>>}
     */
    async getRatings(max: number, page: number):
        Promise<Collection<SnowflakeString, Rating>> {
        const ratings = await prisma.rating.findMany({
            where: {
                authorId: this.id.toBigInt()
            },
            orderBy: {
                snowflake: "desc"
            },
            include: {
                animation: {
                    include: {
                        author: true
                    }
                }
            },
            skip: page * max,
            take: max
        });

        var col = new Collection<SnowflakeString, Rating>();
        for(var rating of ratings) {
            col.set(rating.snowflake.toString(), await Rating.fromDatabase(rating, this.ctx));
        }
        return col;
    }

    /**
     * Gets number of ratings created by user
     * @return {Promise<number>}
     */
    async getRatingsCount(): Promise<number> {
        return prisma.rating.count({
            where: {
                authorId: this.id.toBigInt()
            }
        });
    }

    /**
     * Gets files uploaded by user
     * @param {number} limit Max items per page
     * @param {number} page Current page
     * @param {boolean} [publicOnly=true] If true, only public files are returned
     * @return {Promise<Collection<SnowflakeString, File>>}
     */
    async getFiles(limit: number, page: number, publicOnly: boolean = false):
        Promise<Collection<SnowflakeString, File>> {
        const files = await prisma.file.findMany({
            where: {
                authorId: this.id.toBigInt(),
                state: publicOnly ? FileState.PUBLIC : undefined
            },
            orderBy: {
                snowflake: "desc"
            },
            skip: page * limit,
            take: limit
        });

        var col = new Collection<SnowflakeString, File>();
        for(var file of files) {
            col.set(file.snowflake.toString(), await File.fromDatabase(file, this.ctx));
        }

        return col;
    }

    /**
     * Gets number of files uploaded by user
     * @param {boolean} [publicOnly=true] If true, only public files are counted
     * @return {Promise<number>}
     */
    getFilesCount(publicOnly: boolean = false): Promise<number> {
        return prisma.file.count({
            where: {
                authorId: this.id.toBigInt(),
                state: publicOnly ? FileState.PUBLIC : undefined
            }
        });
    }

    /**
     * Gets comments created by user
     * @param {number} limit Max items per page
     * @param {number} page Current page
     * @return {Promise<Collection<SnowflakeString, Comment>>}
     */
    async getComments(limit: number, page: number):
        Promise<Collection<SnowflakeString, Comment>> {
        const comments = await prisma.comment.findMany({
            where: {
                authorId: this.id.toBigInt()
            },
            orderBy: {
                snowflake: "desc"
            },
            include: {
                animation: {
                    include: {
                        author: true
                    }
                }
            },
            skip: page * limit,
            take: limit
        });

        var col = new Collection<SnowflakeString, Comment>();
        for(var comment of comments) {
            col.set(comment.snowflake.toString(), await Comment.fromDatabase(comment, this.ctx));
        }

        return col;
    }

    /**
     * Compares positions of two users to determine rights
     * @param {User} user to compare
     */
    comparePosition(user: User):boolean;

    /**
     * Compares position of user and type
     * @param {UserType} type to compare
     */
    comparePosition(type: UserType): boolean;

    /**
     * Compares position of user and user/type
     * @param {User | UserType} userOrType to compare
     * @return {boolean}
     */
    comparePosition(userOrType: User | UserType): boolean {
        if(this.type < 2) return false;
        if(userOrType instanceof User) {
            return userOrType.type < this.type;
        } else {
            return userOrType < this.type;
        }
    }

    /**
     * Fetches system relation
     * @return {Promise<SystemRelation>}
     */
    async getType(): Promise<SystemRelation> {
        if(!this.ctx.client) throw new Error("client_required");

        const relationData = await prisma.systemRelation.findFirst({
            where: {
                targetId: this.id.toBigInt()
            },
            orderBy: {
                snowflake: "desc"
            }
        });

        if(!relationData) throw new Error("member");

        return SystemRelation.fromDatabase(relationData, this.ctx);
    }

    /**
     * Sets new type of user
     * @param {RequestContext} ctx
     * @param {UserType} type New type to set
     * @return {Promise<void>}
     */
    async setType(ctx: RequestContext, type: UserType): Promise<void> {
        if(!ctx.user || !ctx.user.comparePosition(this)) throw new Error("perms");

        await SystemRelation.createRelation(ctx, this, type);
        await prisma.user.update({
            where: {
                snowflake: this.id.toBigInt()
            },
            data: {
                type
            }
        });
    }

    /**
     * Returns JSON-compatible object to be used in stringify
     * @return {Object}
     */
    toJSON() {
        return {
            id: this.id.getSnowflake(),
            avatar: this.avatarId?.toString(),
            avatarURL: this.avatarId ? File.formatPublicURL(this.avatarId.toString()) : null,
            registered: this.id.time,
            name: this.name,
            description: this.description,
            language: LanguageCodes[this.language],
            type: this.type,
            lastLogedIn: this.lastLogedIn,
            reported: this.reported
        };
    }
}

export default User;