Source

classes/userClient.ts

import Collection from "@discordjs/collection";
import bcrypt from "bcrypt";

import User from "./user";
import { SnowflakeString } from "../types/snowflake";
import Token from "./token";
import Snowflake from "./snowflake";
import { UserType } from "../types/basicUser";
import Message from "./message";
import File from "./file";
import ModQueueItem from "./modQueueItem";
import { BasicAnimation } from "./animation";
import Comment from "./comment";
import { ModQueueItemType } from "../types/modQueueItem";
import { RequestContext, RequestContextWithClient } from "./requestContext";
import { UserRelationType } from "../types/userRelation";
import UserRelation from "./userRelation";
import { prisma } from "../../prisma";

/**
 * Holds data about currently logged in user
 */
class UserClient {
    ctx: RequestContextWithClient;
    token: Token;
    user: User;

    /**
     * Creates new userclient
     * @param {object} settings
     */
    constructor({
        ctx,
        token,
        user
    }: {
        ctx: RequestContext,
        token: Token,
        user: User
    }) {
        if(!ctx.client) ctx.client = this;
        this.user = user;
        if(!ctx.hasClient()) throw new Error("Context has no client");
        this.ctx = ctx;
        this.token = token;
    }

    /**
     * Updates last login time
     * @return {Promise<void>}
     */
    async updateLastLogin(): Promise<void> {
        await prisma.user.update({
            where: {
                snowflake: this.user.id.toBigInt()
            },
            data: {
                lastLogedIn: new Date()
            }
        });
        this.user.lastLogedIn = new Date();
    }

    /**
     * Tries to login with given password
     * @param {User} user
     * @param {string} password
     */
    static login(user: User, password: string): Promise<UserClient>;

    /**
     * Tries to login with given token
     * @param {RequestContext} ctx
     * @param {IToken} token
     */
    static login(ctx: RequestContext, token: Token | string): Promise<UserClient>;

    /**
     * Tries to login with given details
     * @param {User} user
     * @param {Token | string} auth Token or string
     * @return {Promise<UserClient>}
     */
    static login(user: User | RequestContext, auth: Token | string): Promise<UserClient> {
        if(typeof auth === "string" && user instanceof User) {
            return this.loginWithPassword(user, auth);
        } else if(!(user instanceof User)) {
            return this.loginWithToken(user, auth);
        }
        throw Error("Unknown overload");
    }

    /**
     * Tries to login with token
     * @param {RequestContext} ctx
     * @param {token} token
     * @return {Promise<UserClient>}
     */
    static async loginWithToken(ctx: RequestContext, token: Token | string): Promise<UserClient> {
        var t = (typeof token === "string" ? token : token.token);
        const tokenData = await prisma.token.findFirstOrThrow({
            where: {
                token: t
            },
            include: {
                user: true
            }
        });

        const user = User.fromDatabase(tokenData.user, ctx);

        const uc = new UserClient({
            ctx,
            token: Token.fromDatabase(tokenData, ctx),
            user
        });

        await uc.updateLastLogin();

        return uc;
    }

    /**
     * Tries to login with password
     * @param {User} user
     * @param {string} password
     * @return {Promise<UserClient>}
     */
    static async loginWithPassword(user: User, password: string): Promise<UserClient> {
        if(!await bcrypt.compare(password, user.password)) {
            throw new Error("bad_password");
        }

        const uc = new UserClient({
            user,
            token: new Token({
                id: Snowflake.newSnowflake(),
                token: null,
                created: new Date,
                long: false,
                ctx: user.ctx
            }),
            ctx: user.ctx
        });

        await uc.updateLastLogin();

        await prisma.token.create({
            data: {
                snowflake: uc.token.id.toBigInt(),
                token: uc.token.token,
                long: uc.token.long,
                user: {
                    connect: {
                        snowflake: uc.user.id.toBigInt()
                    }
                }
            }
        });

        return uc;
    }


    /**
     * Registers user
     * @param {User} user
     * @return {Promise<UserClient>}
     */
    static async register(user: User): Promise<UserClient> {
        const data = await prisma.user.findFirst({
            where: {
                name: user.name,
                type: {
                    not: UserType.DELETED
                }
            }
        });

        if(data) throw new Error("user_exist");

        await prisma.user.create({
            data: {
                snowflake: user.id.toBigInt(),
                name: user.name,
                email: user.email || null,
                avatar: user.avatarId && {
                    connect: {
                        snowflake: user.avatarId.toBigInt()
                    }
                },
                description: user.description,
                type: user.type,
                password: user.password,
                auth: user.auth,
                language: {
                    connect: {
                        id: user.language
                    }
                },
                lastLogedIn: new Date
            }
        });

        const uc = new UserClient({
            user,
            ctx: user.ctx,
            token: new Token({
                id: Snowflake.newSnowflake(),
                token: null,
                created: new Date,
                long: false,
                ctx: user.ctx
            })
        });

        await prisma.token.create({
            data: {
                snowflake: uc.token.id.toBigInt(),
                token: uc.token.token,
                long: uc.token.long,
                user: {
                    connect: {
                        snowflake: uc.user.id.toBigInt()
                    }
                }
            }
        });

        return uc;
    }

    /**
     * Gets received messages
     * @param {number} max Max items per page
     * @param {number} page Current page
     * @return {Promise<Collection<SnowflakeString, Message>>}
     */
    async getReceivedMessages(max: number, page: number):
        Promise<Collection<SnowflakeString, Message>> {
        const data = await prisma.message.findMany({
            where: {
                targetId: this.user.id.toBigInt()
            },
            orderBy: {
                snowflake: "desc"
            },
            skip: max*page,
            take: max,
            include: {
                author: true
            }
        });

        const col = new Collection<SnowflakeString, Message>();
        for(const message of data) {
            col.set(message.snowflake.toString(), await Message.fromDatabase(message, this.ctx));
        }

        return col;
    }

    /**
     * Returns received message count
     * @return {Promise<number>}
     */
    async getReceivedMessageCount(): Promise<number> {
        return await prisma.message.count({
            where: {
                targetId: this.user.id.toBigInt()
            }
        });
    }

    /**
     * Returns message count
     * @param {bigint | User | SnowflakeString | Snowflake} uid
     * @return {Promise<number>}
     */
    async getMessageCount(uid: bigint | User | SnowflakeString | Snowflake):
        Promise<number> {
        var id: bigint;
        if(uid instanceof Snowflake) {
            id = uid.toBigInt();
        } else if(typeof uid === "bigint") {
            id = uid;
        } else if(uid instanceof User) {
            id = uid.id.toBigInt();
        } else {
            id = BigInt(uid);
        }

        return await prisma.message.count({
            where: {
                OR: [
                    {
                        authorId: id,
                        targetId: this.user.id.toBigInt()
                    }, {
                        authorId: this.user.id.toBigInt(),
                        targetId: id
                    }
                ]
            }
        });
    }

    /**
     * Gets conversation with user
     * @param {number | User | SnowflakeString | Snowflake} uid
     * @param {number} max Max items per page
     * @param {number} page Current page
     * @return {Promise<Collection<SnowflakeString, Message>>}
     */
    async getMessages(uid: number | User | SnowflakeString | Snowflake, max: number, page: number):
        Promise<Collection<SnowflakeString, Message>> {
        var id: bigint;
        if(uid instanceof Snowflake) {
            id = uid.toBigInt();
        } else if(typeof uid === "bigint") {
            id = uid;
        } else if(uid instanceof User) {
            id = uid.id.toBigInt();
        } else {
            id = BigInt(uid);
        }

        const data = await prisma.message.findMany({
            where: {
                OR: [
                    {
                        authorId: id,
                        targetId: this.user.id.toBigInt()
                    }, {
                        authorId: this.user.id.toBigInt(),
                        targetId: id
                    }
                ]
            },
            orderBy: {
                snowflake: "desc"
            },
            skip: max*page,
            take: max
        });

        const col = new Collection<SnowflakeString, Message>();
        for(const message of data) {
            col.set(message.snowflake.toString(), await Message.fromDatabase(message, this.ctx));
        }
        return col;
    }

    /**
     * Sends new message
     * @param {Message} message Message to send
     * @return {Promise<Message>}
     */
    async sendMessage(message: Message): Promise<Message> {
        await prisma.message.create({
            data: {
                snowflake: message.id.toBigInt(),
                author: {
                    connect: {
                        snowflake: message.author.id.toBigInt()
                    }
                },
                target: {
                    connect: {
                        snowflake: message.target.id.toBigInt()
                    }
                },
                content: message.content
            }
        });

        return message;
    }

    /**
     * Gets blocked users
     * @return {Promise<UserRelation[]>}
     */
    async getBlockedUsers(): Promise<UserRelation[]> {
        const data = await prisma.userRelation.findMany({
            where: {
                authorId: this.user.id.toBigInt(),
                type: UserRelationType.BLOCKING
            },
            include: {
                target: true
            }
        });

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

    /**
     * Fetches items in mod queue
     * @param {number} page
     * @param {number} size
     * @return {Promise<ModQueueItem[]>}
     */
    async getModQueue(page: number, size: number): Promise<ModQueueItem[]> {
        if(this.user.type < UserType.MODERATOR) throw new Error("Missing permissions");

        const data = await prisma.modQueue.groupBy({
            by: ["resourceId", "type", "reason", "userId"],
            _count: {
                snowflake: true
            },
            orderBy: {
                resourceId: "desc"
            },
            skip: page * size,
            take: size
        });

        return await Promise.all(data.map((item) => ModQueueItem.fromAggregated({
            count: item._count.snowflake,
            reason: item.reason,
            resourceId: item.resourceId,
            type: item.type,
            userId: item.userId
        }, this.ctx)));
    }

    /**
     * Gets the reports of selected item
     * @param {any} item to get reports of
     * @param {number} page
     * @param {number} size
     * @return {Promise<Collection<SnowflakeString, ModQueueItem>>}
     */
    async getReportsOf(item: BasicAnimation | User | Comment | Message | File,
        page: number, size: number):
    Promise<Collection<SnowflakeString, ModQueueItem>> {
        if(this.user.type < UserType.MODERATOR) throw new Error("Missing permissions");
        // return new Promise((resolve, reject) => {
        let type: ModQueueItemType;
        if(item instanceof BasicAnimation) type = ModQueueItemType.ANIMATION;
        else if(item instanceof User) type = ModQueueItemType.USER;
        else if(item instanceof Comment) type = ModQueueItemType.ANIMATION_COMMENT;
        else if(item instanceof Message) type = ModQueueItemType.USER_MESSAGE;
        else if(item instanceof File) type = ModQueueItemType.FILE;
        else throw new TypeError("Unknown type");

        var resource: SnowflakeString = item.id.toString();

        const reports = await prisma.modQueue.findMany({
            where: {
                resourceId: BigInt(resource),
                type
            },
            include: {
                reporter: true
            },
            skip: page * size,
            take: size
        });

        const out = new Collection<SnowflakeString, ModQueueItem>();
        for(const item of reports) {
            out.set(item.snowflake.toString(), await ModQueueItem.fromDatabase(item, this.ctx));
        }
        return out;
    }

    /**
     * Returns the length of mod queue
     * @param {any} [item] to get reports of
     * @return {Promise<number>}
     */
    async getModQueueLength(item?: BasicAnimation | User | Comment | Message | File):
    Promise<number> {
        var type: ModQueueItemType | undefined;
        if(item instanceof BasicAnimation) type = ModQueueItemType.ANIMATION;
        else if(item instanceof User) type = ModQueueItemType.USER;
        else if(item instanceof Comment) type = ModQueueItemType.ANIMATION_COMMENT;
        else if(item instanceof Message) type = ModQueueItemType.USER_MESSAGE;
        else if(item instanceof File) type = ModQueueItemType.FILE;
        else if(typeof item !== "undefined") throw new TypeError("Unknown type");
        var id: SnowflakeString | undefined = item?.id.toString();

        return prisma.modQueue.count({
            where: {
                type,
                resourceId: id ? BigInt(id) : undefined
            }
        });
    }

    /**
     * Tries to find given report
     * @param {SnowflakeString} user
     * @param {ModQueueItemType} type
     * @param {SnowflakeString} resource
     * @return {Promise<ModQueueItem | void>}
     */
    async findReport(user: SnowflakeString, type: ModQueueItemType, resource: SnowflakeString):
        Promise<ModQueueItem | void> {
        const report = await prisma.modQueue.findFirst({
            where: {
                reporter: {
                    snowflake: BigInt(user)
                },
                type,
                resourceId: BigInt(resource)
            }
        });

        if(!report) return;

        return ModQueueItem.fromDatabase(report, this.ctx);
    }

    /**
     * Reports an item
     * @param {any} item to report
     * @param {string} reason of report
     * @return {Promise<ModQueueItem>}
     */
    async report(item: BasicAnimation | User | Comment | Message | File, reason: string):
    Promise<ModQueueItem> {
        var type: ModQueueItemType;
        if(item instanceof BasicAnimation) type = ModQueueItemType.ANIMATION;
        else if(item instanceof User) type = ModQueueItemType.USER;
        else if(item instanceof Comment) type = ModQueueItemType.ANIMATION_COMMENT;
        else if(item instanceof Message) type = ModQueueItemType.USER_MESSAGE;
        else if(item instanceof File) type = ModQueueItemType.FILE;
        else throw new TypeError("Unknown type");

        if(await this.findReport(this.user.id.toString(), type, item.id.toString())) {
            throw new Error("Already reported");
        }

        let user: User;
        switch(type) {
            case ModQueueItemType.ANIMATION: user = (item as BasicAnimation).author;
                break;
            case ModQueueItemType.ANIMATION_COMMENT: user = (item as Comment).user;
                break;
            case ModQueueItemType.USER_MESSAGE: user = (item as Message).author;
                break;
            case ModQueueItemType.USER: user = item as User;
                break;
            case ModQueueItemType.FILE: user = (item as File).author;
                break;
            default: throw new Error("invalid type");
        }

        const data = await prisma.modQueue.upsert({
            create: {
                snowflake: Snowflake.newSnowflake().toBigInt(),
                type,
                reason,
                reporter: {
                    connect: {
                        snowflake: this.user.id.toBigInt()
                    }
                },
                user: {
                    connect: {
                        snowflake: user.id.toBigInt()
                    }
                },
                resourceId: item.id.toBigInt()
            },
            where: {
                resourceId_reporterId: {
                    reporterId: this.user.id.toBigInt(),
                    resourceId: item.id.toBigInt()
                }
            },
            update: {
                reason
            }
        });

        return ModQueueItem.fromDatabase(data, this.ctx);
    }


    /**
     * Updates user data (synchronizes DB with class)
     * @return {Promise<void>}
     */
    async update(): Promise<void> {
        await prisma.user.update({
            where: {
                snowflake: this.user.id.toBigInt()
            },
            data: {
                email: this.user.email,
                avatarId: this.user.avatar?.id?.toBigInt() || null,
                description: this.user.description,
                type: this.user.type,
                password: this.user.password,
                auth: this.user.auth,
                languageId: this.user.language
            }
        });
    }

    /**
     * Deletes user data (deletes from DB)
     * @return {Promise<void>}
     */
    delete(): Promise<void> {
        this.user.type = UserType.DELETED;
        this.user.email = "";
        this.user.name = "[DELETED]";
        this.user.description = "";
        return this.update();
    }

    /**
     * Creates a JSON-compatible object
     * @return {Object}
     */
    toJSON() {
        return {
            user: this.user.toJSON(),
            token: this.token.token
        };
    }
}

export default UserClient;