RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/lib/server/functions/createRoom.ts

Summary

Maintainability
D
1 day
Test Coverage
/* eslint-disable complexity */
import { AppEvents, Apps } from '@rocket.chat/apps';
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
import { Federation, FederationEE, License, Message, Team } from '@rocket.chat/core-services';
import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services';
import type { ICreatedRoom, IUser, IRoom, RoomType } from '@rocket.chat/core-typings';
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { callbacks } from '../../../../lib/callbacks';
import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback';
import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig';
import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref';
import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName';
import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener';
import { createDirectRoom } from './createDirectRoom';

const isValidName = (name: unknown): name is string => {
    return typeof name === 'string' && name.trim().length > 0;
};

const onlyUsernames = (members: unknown): members is string[] =>
    Array.isArray(members) && members.every((member) => typeof member === 'string');

async function createUsersSubscriptions({
    room,
    shouldBeHandledByFederation,
    members,
    now,
    owner,
    options,
}: {
    room: IRoom;
    shouldBeHandledByFederation: boolean;
    members: string[];
    now: Date;
    owner: IUser;
    options?: ICreateRoomParams['options'];
}) {
    if (shouldBeHandledByFederation) {
        const extra: Partial<ISubscriptionExtraData> = {
            ...options?.subscriptionExtra,
            open: true,
            ls: now,
            roles: ['owner'],
            ...(room.prid && { prid: room.prid }),
            ...getDefaultSubscriptionPref(owner),
        };

        const { insertedId } = await Subscriptions.createWithRoomAndUser(room, owner, extra);

        if (insertedId) {
            await notifyOnRoomChanged(room, 'inserted');
        }

        return;
    }

    const subs = [];

    const memberIds = [];

    const membersCursor = Users.findUsersByUsernames<Pick<IUser, '_id' | 'username' | 'settings' | 'federated' | 'roles'>>(members, {
        projection: { 'username': 1, 'settings.preferences': 1, 'federated': 1, 'roles': 1 },
    });

    for await (const member of membersCursor) {
        try {
            await callbacks.run('federation.beforeAddUserToARoom', { user: member, inviter: owner }, room);
            await callbacks.run('beforeAddedToRoom', { user: member, inviter: owner });
        } catch (error) {
            continue;
        }

        memberIds.push(member._id);

        const extra: Partial<ISubscriptionExtraData> = options?.subscriptionExtra || {};

        extra.open = true;

        if (room.prid) {
            extra.prid = room.prid;
        }

        if (member.username === owner.username) {
            extra.ls = now;
            extra.roles = ['owner'];
        }

        const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(member);

        subs.push({
            user: member,
            extraData: {
                ...extra,
                ...autoTranslateConfig,
            },
        });
    }

    if (!['d', 'l'].includes(room.t)) {
        await Users.addRoomByUserIds(memberIds, room._id);
    }

    const { insertedIds } = await Subscriptions.createWithRoomAndManyUsers(room, subs);

    Object.values(insertedIds).forEach((subId) => notifyOnSubscriptionChangedById(subId, 'inserted'));

    await Rooms.incUsersCountById(room._id, subs.length);
}

export const createRoom = async <T extends RoomType>(
    type: T,
    name: T extends 'd' ? undefined : string,
    owner: T extends 'd' ? IUser | undefined : IUser,
    members: T extends 'd' ? IUser[] : string[] = [],
    excludeSelf?: boolean,
    readOnly?: boolean,
    roomExtraData?: Partial<IRoom>,
    options?: ICreateRoomParams['options'],
    sidepanel?: ICreateRoomParams['sidepanel'],
): Promise<
    ICreatedRoom & {
        rid: string;
    }
> => {
    const { teamId, ...extraData } = roomExtraData || ({} as IRoom);

    await beforeCreateRoomCallback.run({
        type,
        // name,
        // owner: ownerUsername,
        // members,
        // readOnly,
        extraData,
        // options,
    });

    if (type === 'd') {
        return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?.username });
    }

    if (!onlyUsernames(members)) {
        throw new Meteor.Error(
            'error-invalid-members',
            'members should be an array of usernames if provided for rooms other than direct messages',
        );
    }

    if (!isValidName(name)) {
        throw new Meteor.Error('error-invalid-name', 'Invalid name', {
            function: 'RocketChat.createRoom',
        });
    }

    if (!owner) {
        throw new Meteor.Error('error-invalid-user', 'Invalid user', {
            function: 'RocketChat.createRoom',
        });
    }

    if (!owner?.username) {
        throw new Meteor.Error('error-invalid-user', 'Invalid user', {
            function: 'RocketChat.createRoom',
        });
    }

    if (!excludeSelf && owner.username && !members.includes(owner.username)) {
        members.push(owner.username);
    }

    if (extraData.broadcast) {
        readOnly = true;
        delete extraData.reactWhenReadOnly;
    }

    // this might not be the best way to check if the room is a discussion, we may need a specific field for that
    const isDiscussion = 'prid' in extraData && extraData.prid !== '';

    const now = new Date();

    const roomProps: Omit<IRoom, '_id' | '_updatedAt'> = {
        fname: name,
        _updatedAt: now,
        ...extraData,
        name: isDiscussion ? name : await getValidRoomName(name.trim(), undefined),
        t: type,
        msgs: 0,
        usersCount: 0,
        u: {
            _id: owner._id,
            username: owner.username,
            name: owner.name,
        },
        ts: now,
        ro: readOnly === true,
        sidepanel,
    };

    if (teamId) {
        const team = await Team.getOneById(teamId, { projection: { _id: 1 } });
        if (team) {
            roomProps.teamId = team._id;
        }
    }

    const tmp = {
        ...roomProps,
        _USERNAMES: members,
    };

    const prevent = await Apps.self?.triggerEvent(AppEvents.IPreRoomCreatePrevent, tmp).catch((error) => {
        if (error.name === AppsEngineException.name) {
            throw new Meteor.Error('error-app-prevented', error.message);
        }

        throw error;
    });

    if (prevent) {
        throw new Meteor.Error('error-app-prevented', 'A Rocket.Chat App prevented the room creation.');
    }

    const eventResult = await Apps.self?.triggerEvent(
        AppEvents.IPreRoomCreateModify,
        await Apps.triggerEvent(AppEvents.IPreRoomCreateExtend, tmp),
    );

    if (eventResult && typeof eventResult === 'object' && delete eventResult._USERNAMES) {
        Object.assign(roomProps, eventResult);
    }

    const shouldBeHandledByFederation = roomProps.federated === true || owner.username.includes(':');

    if (shouldBeHandledByFederation) {
        const federation = (await License.hasValidLicense()) ? FederationEE : Federation;
        await federation.beforeCreateRoom(roomProps);
    }

    if (type === 'c') {
        await callbacks.run('beforeCreateChannel', owner, roomProps);
    }

    const room = await Rooms.createWithFullRoomData(roomProps);

    void notifyOnRoomChanged(room, 'inserted');

    await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation });

    if (type === 'c') {
        if (room.teamId) {
            const team = await Team.getOneById(room.teamId);
            if (team) {
                await Message.saveSystemMessage('user-added-room-to-team', team.roomId, room.name || '', owner);
            }
        }
        callbacks.runAsync('afterCreateChannel', owner, room);
    } else if (type === 'p') {
        callbacks.runAsync('afterCreatePrivateGroup', owner, room);
    }
    callbacks.runAsync('afterCreateRoom', owner, room);
    if (shouldBeHandledByFederation) {
        callbacks.runAsync('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members });
    }

    void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, room);
    return {
        rid: room._id, // backwards compatible
        inserted: true,
        ...room,
    };
};