Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Improve groups.create endpoint for large amounts of members #30499

Merged
merged 17 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion apps/meteor/app/api/server/v1/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,14 @@ async function createChannelValidator(params: {

async function createChannel(
userId: string,
params: { name?: string; members?: string[]; customFields?: Record<string, any>; extraData?: Record<string, any>; readOnly?: boolean },
params: {
name?: string;
members?: string[];
customFields?: Record<string, any>;
extraData?: Record<string, any>;
readOnly?: boolean;
excludeSelf?: boolean;
},
): Promise<{ channel: IRoom }> {
const readOnly = typeof params.readOnly !== 'undefined' ? params.readOnly : false;
const id = await createChannelMethod(
Expand All @@ -680,6 +687,7 @@ async function createChannel(
readOnly,
params.customFields,
params.extraData,
params.excludeSelf,
);

return {
Expand Down
44 changes: 24 additions & 20 deletions apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Team } from '@rocket.chat/core-services';
import { Team, isMeteorError } from '@rocket.chat/core-services';
import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings';
import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models';
import { check, Match } from 'meteor/check';
Expand Down Expand Up @@ -302,10 +302,6 @@ API.v1.addRoute(
{ authRequired: true },
{
async post() {
if (!(await hasPermissionAsync(this.userId, 'create-p'))) {
return API.v1.unauthorized();
}

if (!this.bodyParams.name) {
return API.v1.failure('Body param "name" is required');
}
Expand All @@ -323,24 +319,32 @@ API.v1.addRoute(

const readOnly = typeof this.bodyParams.readOnly !== 'undefined' ? this.bodyParams.readOnly : false;

const result = await createPrivateGroupMethod(
this.userId,
this.bodyParams.name,
this.bodyParams.members ? this.bodyParams.members : [],
readOnly,
this.bodyParams.customFields,
this.bodyParams.extraData,
);

const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude });
try {
const result = await createPrivateGroupMethod(
this.user,
this.bodyParams.name,
this.bodyParams.members ? this.bodyParams.members : [],
readOnly,
this.bodyParams.customFields,
this.bodyParams.extraData,
this.bodyParams.excludeSelf ?? false,
);

const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude });
if (!room) {
throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group');
}

if (!room) {
throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group');
return API.v1.success({
group: await composeRoomWithLastMessage(room, this.userId),
});
} catch (error: unknown) {
if (isMeteorError(error) && error.reason === 'error-not-allowed') {
return API.v1.unauthorized();
}
}

return API.v1.success({
group: await composeRoomWithLastMessage(room, this.userId),
});
return API.v1.internalError();
},
},
);
Expand Down
6 changes: 5 additions & 1 deletion apps/meteor/app/apps/server/bridges/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ export class AppRoomBridge extends RoomBridge {
}

private async createPrivateGroup(userId: string, room: ICoreRoom, members: string[]): Promise<string> {
return (await createPrivateGroupMethod(userId, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid;
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> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ const create = async ({
const discussion = await createRoom(
type,
name,
user.username as string,
user,
[...new Set(invitedUsers)].filter(Boolean),
false,
false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,11 @@ export class ImportDataConverter {
return;
}
if (roomData.t === 'p') {
roomInfo = await createPrivateGroupMethod(startedByUserId, roomData.name, members, false, {}, {}, true);
const user = await Users.findOneById(startedByUserId);
if (!user) {
throw new Error('importer-channel-invalid-creator');
}
roomInfo = await createPrivateGroupMethod(user, roomData.name, members, false, {}, {}, true);
} else {
roomInfo = await createChannelMethod(startedByUserId, roomData.name, members, false, {}, {}, true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?:
}).toArray();
for await (const room of defaultRooms) {
if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) {
const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user);
const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user);
// Add a subscription to this user
await Subscriptions.createWithRoomAndUser(room, user, {
ts: new Date(),
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/lib/server/functions/addUserToRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const addUserToRoom = async function (
await callbacks.run('beforeJoinRoom', userToBeAdded, room);
}

const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(userToBeAdded);
const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(userToBeAdded);

await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, {
ts: now,
Expand Down
140 changes: 88 additions & 52 deletions apps/meteor/app/lib/server/functions/createRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { Apps } from '../../../../ee/server/apps/orchestrator';
import { callbacks } from '../../../../lib/callbacks';
import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback';
import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig';
import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles';
import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName';
import { createDirectRoom } from './createDirectRoom';

Expand All @@ -21,10 +20,90 @@ const isValidName = (name: unknown): name is string => {
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 || {};
extra.open = true;
extra.ls = now;

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

await Subscriptions.createWithRoomAndUser(room, owner, extra);

return;
}

const subs = [];

const memberIds = [];

const membersCursor = Users.findUsersByUsernames(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 });
pierre-lehnen-rc marked this conversation as resolved.
Show resolved Hide resolved
} 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);
}

await Subscriptions.createWithRoomAndManyUsers(room, subs);

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

export const createRoom = async <T extends RoomType>(
type: T,
name: T extends 'd' ? undefined : string,
ownerUsername: string | undefined,
owner: T extends 'd' ? IUser | undefined : IUser,
members: T extends 'd' ? IUser[] : string[] = [],
excludeSelf?: boolean,
readOnly?: boolean,
Expand All @@ -47,7 +126,7 @@ export const createRoom = async <T extends RoomType>(
// options,
});
if (type === 'd') {
return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || ownerUsername });
return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?.username });
}

if (!onlyUsernames(members)) {
Expand All @@ -63,15 +142,13 @@ export const createRoom = async <T extends RoomType>(
});
}

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

const owner = await Users.findOneByUsernameIgnoringCase(ownerUsername, { projection: { username: 1, name: 1 } });

if (!ownerUsername || !owner) {
if (!owner?.username) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
function: 'RocketChat.createRoom',
});
Expand Down Expand Up @@ -140,53 +217,12 @@ export const createRoom = async <T extends RoomType>(
if (type === 'c') {
await callbacks.run('beforeCreateChannel', owner, roomProps);
}
const room = await Rooms.createWithFullRoomData(roomProps);
const shouldBeHandledByFederation = room.federated === true || ownerUsername.includes(':');
if (shouldBeHandledByFederation) {
const extra: Partial<ISubscriptionExtraData> = options?.subscriptionExtra || {};
extra.open = true;
extra.ls = now;

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

await Subscriptions.createWithRoomAndUser(room, owner, extra);
} else {
for await (const username of [...new Set(members)]) {
const member = await Users.findOneByUsername(username, {
projection: { 'username': 1, 'settings.preferences': 1, 'federated': 1, 'roles': 1 },
});
if (!member) {
continue;
}

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

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

extra.open = true;

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

if (username === owner.username) {
extra.ls = now;
}

const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(member);
const room = await Rooms.createWithFullRoomData(roomProps);

await Subscriptions.createWithRoomAndUser(room, member, { ...extra, ...autoTranslateConfig });
}
}
const shouldBeHandledByFederation = room.federated === true || owner.username.includes(':');

await addUserRolesAsync(owner._id, ['owner'], room._id);
await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation });

if (type === 'c') {
if (room.teamId) {
Expand All @@ -195,7 +231,7 @@ export const createRoom = async <T extends RoomType>(
await Message.saveSystemMessage('user-added-room-to-team', team.roomId, room.name || '', owner);
}
}
await callbacks.run('afterCreateChannel', owner, room);
callbacks.runAsync('afterCreateChannel', owner, room);
} else if (type === 'p') {
callbacks.runAsync('afterCreatePrivateGroup', owner, room);
}
Expand Down
5 changes: 2 additions & 3 deletions apps/meteor/app/lib/server/methods/createChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,15 @@ export const createChannelMethod = async (
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' });
}

const user = await Users.findOneById(userId, { projection: { username: 1 } });

const user = await Users.findOneById(userId);
if (!user?.username) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' });
}

if (!(await hasPermissionAsync(userId, 'create-c'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createChannel' });
}
return createRoom('c', name, user.username, members, excludeSelf, readOnly, {
return createRoom('c', name, user, members, excludeSelf, readOnly, {
customFields,
...extraData,
});
Expand Down
Loading