RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/apps/server/bridges/rooms.ts

Summary

Maintainability
D
1 day
Test Coverage
import type { IAppServerOrchestrator } from '@rocket.chat/apps';
import type { IMessage } from '@rocket.chat/apps-engine/definition/messages';
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import type { IUser } from '@rocket.chat/apps-engine/definition/users';
import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge';
import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom } from '@rocket.chat/core-typings';
import { Subscriptions, Users, Rooms } from '@rocket.chat/models';

import { createDirectMessage } from '../../../../server/methods/createDirectMessage';
import { createDiscussion } from '../../../discussion/server/methods/createDiscussion';
import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom';
import { deleteRoom } from '../../../lib/server/functions/deleteRoom';
import { createChannelMethod } from '../../../lib/server/methods/createChannel';
import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup';

export class AppRoomBridge extends RoomBridge {
    constructor(private readonly orch: IAppServerOrchestrator) {
        super();
    }

    protected async create(room: IRoom, members: Array<string>, appId: string): Promise<string> {
        this.orch.debugLog(`The App ${appId} is creating a new room.`, room);

        const rcRoom = await this.orch.getConverters()?.get('rooms').convertAppRoom(room);

        switch (room.type) {
            case RoomType.CHANNEL:
                return this.createChannel(room.creator.id, rcRoom, members);
            case RoomType.PRIVATE_GROUP:
                return this.createPrivateGroup(room.creator.id, rcRoom, members);
            case RoomType.DIRECT_MESSAGE:
                return this.createDirectMessage(room.creator.id, members);
            default:
                throw new Error('Only channels, private groups and direct messages can be created.');
        }
    }

    private prepareExtraData(room: Record<string, any>): Record<string, unknown> {
        const extraData = Object.assign({}, room);
        delete extraData.name;
        delete extraData.t;
        delete extraData.ro;
        delete extraData.customFields;

        return extraData;
    }

    private async createChannel(userId: string, room: ICoreRoom, members: string[]): Promise<string> {
        return (await createChannelMethod(userId, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid;
    }

    private async createDirectMessage(userId: string, members: string[]): Promise<string> {
        return (await createDirectMessage(members, userId)).rid;
    }

    private async createPrivateGroup(userId: string, room: ICoreRoom, members: string[]): Promise<string> {
        const user = await Users.findOneById(userId);
        if (!user) {
            throw new Error('Invalid user');
        }
        return (await createPrivateGroupMethod(user, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid;
    }

    protected async getById(roomId: string, appId: string): Promise<IRoom> {
        this.orch.debugLog(`The App ${appId} is getting the roomById: "${roomId}"`);

        // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed.
        const promise: Promise<IRoom | undefined> = this.orch.getConverters()?.get('rooms').convertById(roomId);
        return promise as Promise<IRoom>;
    }

    protected async getByName(roomName: string, appId: string): Promise<IRoom> {
        this.orch.debugLog(`The App ${appId} is getting the roomByName: "${roomName}"`);

        // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed.
        const promise: Promise<IRoom | undefined> = this.orch.getConverters()?.get('rooms').convertByName(roomName);
        return promise as Promise<IRoom>;
    }

    protected async getCreatorById(roomId: string, appId: string): Promise<IUser | undefined> {
        this.orch.debugLog(`The App ${appId} is getting the room's creator by id: "${roomId}"`);

        const room = await Rooms.findOneById(roomId);

        if (!room?.u?._id) {
            return undefined;
        }

        return this.orch.getConverters()?.get('users').convertById(room.u._id);
    }

    protected async getCreatorByName(roomName: string, appId: string): Promise<IUser | undefined> {
        this.orch.debugLog(`The App ${appId} is getting the room's creator by name: "${roomName}"`);

        const room = await Rooms.findOneByName(roomName, {});

        if (!room?.u?._id) {
            return undefined;
        }

        return this.orch.getConverters()?.get('users').convertById(room.u._id);
    }

    protected async getMembers(roomId: string, appId: string): Promise<Array<IUser>> {
        this.orch.debugLog(`The App ${appId} is getting the room's members by room id: "${roomId}"`);
        const subscriptions = await Subscriptions.findByRoomId(roomId, {});
        // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed.
        const promises: Promise<(IUser | undefined)[]> = Promise.all(
            (await subscriptions.toArray()).map((sub: ISubscription) => this.orch.getConverters()?.get('users').convertById(sub.u?._id)),
        );

        return promises as Promise<IUser[]>;
    }

    protected async getDirectByUsernames(usernames: Array<string>, appId: string): Promise<IRoom | undefined> {
        this.orch.debugLog(`The App ${appId} is getting direct room by usernames: "${usernames}"`);
        const room = await Rooms.findDirectRoomContainingAllUsernames(usernames, {});
        if (!room) {
            return undefined;
        }
        return this.orch.getConverters()?.get('rooms').convertRoom(room);
    }

    protected async update(room: IRoom, members: Array<string> = [], appId: string): Promise<void> {
        this.orch.debugLog(`The App ${appId} is updating a room.`);

        if (!room.id || !(await Rooms.findOneById(room.id))) {
            throw new Error('A room must exist to update.');
        }

        const rm = await this.orch.getConverters()?.get('rooms').convertAppRoom(room);

        await Rooms.updateOne({ _id: rm._id }, { $set: rm as Partial<ICoreRoom> });

        for await (const username of members) {
            const member = await Users.findOneByUsername(username, {});

            if (!member) {
                continue;
            }

            await addUserToRoom(rm._id, member);
        }
    }

    protected async delete(roomId: string, appId: string): Promise<void> {
        this.orch.debugLog(`The App ${appId} is deleting a room.`);
        await deleteRoom(roomId);
    }

    protected async createDiscussion(
        room: IRoom,
        parentMessage: IMessage | undefined = undefined,
        reply: string | undefined = '',
        members: Array<string> = [],
        appId: string,
    ): Promise<string> {
        this.orch.debugLog(`The App ${appId} is creating a new discussion.`, room);

        const rcRoom = await this.orch.getConverters()?.get('rooms').convertAppRoom(room);

        let rcMessage;
        if (parentMessage) {
            rcMessage = await this.orch.getConverters()?.get('messages').convertAppMessage(parentMessage);
        }

        if (!rcRoom.prid || !(await Rooms.findOneById(rcRoom.prid))) {
            throw new Error('There must be a parent room to create a discussion.');
        }

        // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed.
        const discussion = {
            prid: rcRoom.prid,
            t_name: rcRoom.fname as string,
            pmid: rcMessage ? rcMessage._id : undefined,
            reply: reply && reply.trim() !== '' ? reply : undefined,
            users: members.length > 0 ? members : [],
        };

        const { rid } = await createDiscussion(room.creator.id, discussion);

        return rid;
    }

    protected getModerators(roomId: string, appId: string): Promise<IUser[]> {
        this.orch.debugLog(`The App ${appId} is getting room moderators for room id: ${roomId}`);
        return this.getUsersByRoomIdAndSubscriptionRole(roomId, 'moderator');
    }

    protected getOwners(roomId: string, appId: string): Promise<IUser[]> {
        this.orch.debugLog(`The App ${appId} is getting room owners for room id: ${roomId}`);
        return this.getUsersByRoomIdAndSubscriptionRole(roomId, 'owner');
    }

    protected getLeaders(roomId: string, appId: string): Promise<IUser[]> {
        this.orch.debugLog(`The App ${appId} is getting room leaders for room id: ${roomId}`);
        return this.getUsersByRoomIdAndSubscriptionRole(roomId, 'leader');
    }

    private async getUsersByRoomIdAndSubscriptionRole(roomId: string, role: string): Promise<IUser[]> {
        const subs = (await Subscriptions.findByRoomIdAndRoles(roomId, [role], {
            projection: { uid: '$u._id', _id: 0 },
        }).toArray()) as unknown as {
            uid: string;
        }[];
        // Was this a bug?
        const users = await Users.findByIds(subs.map((user: { uid: string }) => user.uid)).toArray();
        const userConverter = this.orch.getConverters().get('users');
        return users.map((user: ICoreUser) => userConverter.convertToApp(user));
    }
}