From 6d4618335c125a122a2136082e02132e2d515649 Mon Sep 17 00:00:00 2001
From: Diego Sampaio <chinello@gmail.com>
Date: Thu, 5 Oct 2023 20:17:34 -0300
Subject: [PATCH] chore: Improve `groups.create` endpoint for large amounts of
 members (#30499)

---
 apps/meteor/app/api/server/v1/channels.ts     |  10 +-
 apps/meteor/app/api/server/v1/groups.ts       |  44 +++---
 apps/meteor/app/apps/server/bridges/rooms.ts  |   6 +-
 .../server/methods/createDiscussion.ts        |   2 +-
 .../server/classes/ImportDataConverter.ts     |   6 +-
 .../functions/addUserToDefaultChannels.ts     |   2 +-
 .../app/lib/server/functions/addUserToRoom.ts |   2 +-
 .../app/lib/server/functions/createRoom.ts    | 140 +++++++++++-------
 .../app/lib/server/methods/createChannel.ts   |   5 +-
 .../lib/server/methods/createPrivateGroup.ts  |  28 ++--
 .../meteor-accounts-saml/server/lib/SAML.ts   |   4 +-
 .../app/slashcommands-create/server/server.ts |   8 +-
 .../slashcommands-inviteall/server/server.ts  |   5 +-
 .../utils/lib/getDefaultSubscriptionPref.ts   |   4 +-
 apps/meteor/ee/server/lib/ldap/Manager.ts     |  18 ++-
 apps/meteor/ee/server/lib/oauth/Manager.ts    |  10 +-
 apps/meteor/lib/callbacks.ts                  |   3 +-
 ...tSubscriptionAutotranslateDefaultConfig.ts |  27 ++--
 apps/meteor/server/lib/roles/addUserRoles.ts  |   7 +-
 .../meteor/server/methods/addAllUserToRoom.ts |   2 +-
 .../server/methods/createDirectMessage.ts     |   6 +-
 .../meteor/server/models/raw/Subscriptions.ts |  44 +++++-
 apps/meteor/server/models/raw/Users.js        |  16 ++
 .../rocket-chat/adapters/Room.ts              |  15 +-
 apps/meteor/server/services/room/service.ts   |   6 +-
 .../core-services/src/types/IRoomService.ts   |   1 +
 .../src/models/ISubscriptionsModel.ts         |  18 ++-
 .../model-typings/src/models/IUsersModel.ts   |   3 +-
 .../src/v1/channels/ChannelsCreateProps.ts    |   1 +
 .../src/v1/groups/GroupsCreateProps.ts        |   1 +
 30 files changed, 295 insertions(+), 149 deletions(-)

diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts
index 4a7aec07344..8e0541b8040 100644
--- a/apps/meteor/app/api/server/v1/channels.ts
+++ b/apps/meteor/app/api/server/v1/channels.ts
@@ -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(
@@ -680,6 +687,7 @@ async function createChannel(
 		readOnly,
 		params.customFields,
 		params.extraData,
+		params.excludeSelf,
 	);
 
 	return {
diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts
index df54b683fda..ef18d425634 100644
--- a/apps/meteor/app/api/server/v1/groups.ts
+++ b/apps/meteor/app/api/server/v1/groups.ts
@@ -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';
@@ -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');
 			}
@@ -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();
 		},
 	},
 );
diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts
index 481292d6179..91b0049513f 100644
--- a/apps/meteor/app/apps/server/bridges/rooms.ts
+++ b/apps/meteor/app/apps/server/bridges/rooms.ts
@@ -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> {
diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts
index c08378fd64f..c3869f8ff96 100644
--- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts
+++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts
@@ -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,
diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts
index f241879cdc6..1b596d625d9 100644
--- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts
+++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts
@@ -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);
 				}
diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts
index ad632a3b7df..835f59419ad 100644
--- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts
+++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts
@@ -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(),
diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts
index 41000cda203..4e29576cf3b 100644
--- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts
+++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts
@@ -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,
diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts
index 30cf2a59370..312451f5484 100644
--- a/apps/meteor/app/lib/server/functions/createRoom.ts
+++ b/apps/meteor/app/lib/server/functions/createRoom.ts
@@ -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';
 
@@ -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<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);
+	}
+
+	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,
@@ -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)) {
@@ -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',
 		});
@@ -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) {
@@ -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);
 	}
diff --git a/apps/meteor/app/lib/server/methods/createChannel.ts b/apps/meteor/app/lib/server/methods/createChannel.ts
index ff8182cec8c..98cea517bed 100644
--- a/apps/meteor/app/lib/server/methods/createChannel.ts
+++ b/apps/meteor/app/lib/server/methods/createChannel.ts
@@ -35,8 +35,7 @@ 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' });
 	}
@@ -44,7 +43,7 @@ export const createChannelMethod = async (
 	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,
 	});
diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts
index 65298949a34..75097b5c89b 100644
--- a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts
+++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts
@@ -1,4 +1,4 @@
-import type { ICreatedRoom } from '@rocket.chat/core-typings';
+import type { ICreatedRoom, IUser } from '@rocket.chat/core-typings';
 import { Users } from '@rocket.chat/models';
 import type { ServerMethods } from '@rocket.chat/ui-contexts';
 import { Match, check } from 'meteor/check';
@@ -21,7 +21,7 @@ declare module '@rocket.chat/ui-contexts' {
 }
 
 export const createPrivateGroupMethod = async (
-	userId: string,
+	user: IUser,
 	name: string,
 	members: string[],
 	readOnly = false,
@@ -35,23 +35,12 @@ export const createPrivateGroupMethod = async (
 > => {
 	check(name, String);
 	check(members, Match.Optional([String]));
-	if (!userId) {
-		throw new Meteor.Error('error-invalid-user', 'Invalid user', {
-			method: 'createPrivateGroup',
-		});
-	}
-	const user = await Users.findOneById(userId, { projection: { username: 1 } });
-	if (!user) {
-		throw new Meteor.Error('error-invalid-user', 'Invalid user', {
-			method: 'createPrivateGroup',
-		});
-	}
 
-	if (!(await hasPermissionAsync(userId, 'create-p'))) {
+	if (!(await hasPermissionAsync(user._id, 'create-p'))) {
 		throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createPrivateGroup' });
 	}
 
-	return createRoom('p', name, user.username, members, excludeSelf, readOnly, {
+	return createRoom('p', name, user, members, excludeSelf, readOnly, {
 		customFields,
 		...extraData,
 	});
@@ -67,6 +56,13 @@ Meteor.methods<ServerMethods>({
 			});
 		}
 
-		return createPrivateGroupMethod(uid, name, members, readOnly, customFields, extraData);
+		const user = await Users.findOneById(uid);
+		if (!user) {
+			throw new Meteor.Error('error-invalid-user', 'Invalid user', {
+				method: 'createPrivateGroup',
+			});
+		}
+
+		return createPrivateGroupMethod(user, name, members, readOnly, customFields, extraData);
 	},
 });
diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts
index 06c3014a8a5..f62ab71f230 100644
--- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts
+++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts
@@ -480,7 +480,6 @@ export class SAML {
 					continue;
 				}
 
-				const room = await Rooms.findOneByNameAndType(roomName, 'c', {});
 				const privRoom = await Rooms.findOneByNameAndType(roomName, 'p', {});
 
 				if (privRoom && includePrivateChannelsInUpdate === true) {
@@ -488,6 +487,7 @@ export class SAML {
 					continue;
 				}
 
+				const room = await Rooms.findOneByNameAndType(roomName, 'c', {});
 				if (room) {
 					await addUserToRoom(room._id, user);
 					continue;
@@ -496,7 +496,7 @@ export class SAML {
 				if (!room && !privRoom) {
 					// If the user doesn't have an username yet, we can't create new rooms for them
 					if (user.username) {
-						await createRoom('c', roomName, user.username);
+						await createRoom('c', roomName, user);
 					}
 				}
 			}
diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts
index a3c70f012fa..104d50c5692 100644
--- a/apps/meteor/app/slashcommands-create/server/server.ts
+++ b/apps/meteor/app/slashcommands-create/server/server.ts
@@ -1,6 +1,6 @@
 import { api } from '@rocket.chat/core-services';
 import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
-import { Rooms } from '@rocket.chat/models';
+import { Rooms, Users } from '@rocket.chat/models';
 
 import { i18n } from '../../../server/lib/i18n';
 import { createChannelMethod } from '../../lib/server/methods/createChannel';
@@ -50,7 +50,11 @@ slashCommands.add({
 		}
 
 		if (getParams(params).indexOf('private') > -1) {
-			await createPrivateGroupMethod(userId, channelStr, []);
+			const user = await Users.findOneById(userId);
+			if (!user) {
+				return;
+			}
+			await createPrivateGroupMethod(user, channelStr, []);
 			return;
 		}
 
diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts
index 9917775aca0..5376bd6ae64 100644
--- a/apps/meteor/app/slashcommands-inviteall/server/server.ts
+++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts
@@ -37,6 +37,9 @@ function inviteAll<T extends string>(type: T): SlashCommand<T>['callback'] {
 		}
 
 		const user = await Users.findOneById(userId);
+		if (!user) {
+			return;
+		}
 		const lng = user?.language || settings.get('Language') || 'en';
 
 		const baseChannel = type === 'to' ? await Rooms.findOneById(message.rid) : await Rooms.findOneByName(channel);
@@ -69,7 +72,7 @@ function inviteAll<T extends string>(type: T): SlashCommand<T>['callback'] {
 			const users = (await cursor.toArray()).map((s: ISubscription) => s.u.username).filter(isTruthy);
 
 			if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) {
-				baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(userId, channel, users);
+				baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(user, channel, users);
 				void api.broadcast('notify.ephemeralMessage', userId, message.rid, {
 					msg: i18n.t('Channel_created', {
 						postProcess: 'sprintf',
diff --git a/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts b/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts
index a388548c18a..adb4c2ab1ae 100644
--- a/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts
+++ b/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts
@@ -1,4 +1,4 @@
-import type { ISubscription, IUser } from '@rocket.chat/core-typings';
+import type { AtLeast, ISubscription, IUser } from '@rocket.chat/core-typings';
 
 /**
  * @type {(userPref: Pick<import('@rocket.chat/core-typings').IUser, 'settings'>) => {
@@ -7,7 +7,7 @@ import type { ISubscription, IUser } from '@rocket.chat/core-typings';
  * 	emailPrefOrigin: 'user';
  * }}
  */
-export const getDefaultSubscriptionPref = (userPref: IUser) => {
+export const getDefaultSubscriptionPref = (userPref: AtLeast<IUser, 'settings'>) => {
 	const subscription: Partial<ISubscription> = {};
 
 	const { desktopNotifications, pushNotifications, emailNotificationMode, highlights } = userPref.settings?.preferences || {};
diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts
index deb6cdcec66..6c04574ad55 100644
--- a/apps/meteor/ee/server/lib/ldap/Manager.ts
+++ b/apps/meteor/ee/server/lib/ldap/Manager.ts
@@ -1,6 +1,6 @@
 import { Team } from '@rocket.chat/core-services';
 import type { ILDAPEntry, IUser, IRoom, IRole, IImportUser, IImportRecord } from '@rocket.chat/core-typings';
-import { Users as UsersRaw, Roles, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models';
+import { Users, Roles, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models';
 import type ldapjs from 'ldapjs';
 
 import type {
@@ -271,10 +271,12 @@ export class LDAPEEManager extends LDAPManager {
 		logger.debug(`Channel '${channel}' doesn't exist, creating it.`);
 
 		const roomOwner = settings.get<string>('LDAP_Sync_User_Data_Channels_Admin') || '';
-		// #ToDo: Remove typecastings when createRoom is converted to ts.
-		const room = await createRoom('c', channel, roomOwner, [], false, false, {
+
+		const user = await Users.findOneByUsernameIgnoringCase(roomOwner);
+
+		const room = await createRoom('c', channel, user, [], false, false, {
 			customFields: { ldap: true },
-		} as any);
+		});
 		if (!room?.rid) {
 			logger.error(`Unable to auto-create channel '${channel}' during ldap sync.`);
 			return;
@@ -574,7 +576,7 @@ export class LDAPEEManager extends LDAPManager {
 	}
 
 	private static async updateExistingUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise<void> {
-		const users = await UsersRaw.findLDAPUsers().toArray();
+		const users = await Users.findLDAPUsers().toArray();
 		for await (const user of users) {
 			const ldapUser = await this.findLDAPUser(ldap, user);
 
@@ -586,7 +588,7 @@ export class LDAPEEManager extends LDAPManager {
 	}
 
 	private static async updateUserAvatars(ldap: LDAPConnection): Promise<void> {
-		const users = await UsersRaw.findLDAPUsers().toArray();
+		const users = await Users.findLDAPUsers().toArray();
 		for await (const user of users) {
 			const ldapUser = await this.findLDAPUser(ldap, user);
 			if (!ldapUser) {
@@ -615,7 +617,7 @@ export class LDAPEEManager extends LDAPManager {
 	}
 
 	private static async logoutDeactivatedUsers(ldap: LDAPConnection): Promise<void> {
-		const users = await UsersRaw.findConnectedLDAPUsers().toArray();
+		const users = await Users.findConnectedLDAPUsers().toArray();
 
 		for await (const user of users) {
 			const ldapUser = await this.findLDAPUser(ldap, user);
@@ -624,7 +626,7 @@ export class LDAPEEManager extends LDAPManager {
 			}
 
 			if (this.isUserDeactivated(ldapUser)) {
-				await UsersRaw.unsetLoginTokens(user._id);
+				await Users.unsetLoginTokens(user._id);
 			}
 		}
 	}
diff --git a/apps/meteor/ee/server/lib/oauth/Manager.ts b/apps/meteor/ee/server/lib/oauth/Manager.ts
index b24d7436a78..b75c8aa9a7a 100644
--- a/apps/meteor/ee/server/lib/oauth/Manager.ts
+++ b/apps/meteor/ee/server/lib/oauth/Manager.ts
@@ -1,6 +1,6 @@
 import type { IUser } from '@rocket.chat/core-typings';
 import { Logger } from '@rocket.chat/logger';
-import { Roles, Rooms } from '@rocket.chat/models';
+import { Roles, Rooms, Users } from '@rocket.chat/models';
 
 import { addUserToRoom } from '../../../../app/lib/server/functions/addUserToRoom';
 import { createRoom } from '../../../../app/lib/server/functions/createRoom';
@@ -20,6 +20,12 @@ export class OAuthEEManager {
 		if (channelsMap && user && identity && groupClaimName) {
 			const groupsFromSSO = identity[groupClaimName] || [];
 
+			const userChannelAdmin = await Users.findOneByUsernameIgnoringCase(channelsAdmin);
+			if (!userChannelAdmin) {
+				logger.error(`could not create channel, user not found: ${channelsAdmin}`);
+				return;
+			}
+
 			for await (const ssoGroup of Object.keys(channelsMap)) {
 				if (typeof ssoGroup === 'string') {
 					let channels = channelsMap[ssoGroup];
@@ -30,7 +36,7 @@ export class OAuthEEManager {
 						const name = await getValidRoomName(channel.trim(), undefined, { allowDuplicates: true });
 						let room = await Rooms.findOneByNonValidatedName(name);
 						if (!room) {
-							const createdRoom = await createRoom('c', channel, channelsAdmin, [], false, false);
+							const createdRoom = await createRoom('c', channel, userChannelAdmin, [], false, false);
 							if (!createdRoom?.rid) {
 								logger.error(`could not create channel ${channel}`);
 								return;
diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts
index 9c7333a355b..2d683bf27e2 100644
--- a/apps/meteor/lib/callbacks.ts
+++ b/apps/meteor/lib/callbacks.ts
@@ -17,6 +17,7 @@ import type {
 	InquiryWithAgentInfo,
 	ILivechatTagRecord,
 	TransferData,
+	AtLeast,
 } from '@rocket.chat/core-typings';
 import type { FilterOperators } from 'mongodb';
 
@@ -56,7 +57,7 @@ interface EventLikeCallbackSignatures {
 	'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void;
 	'livechat.afterAgentRemoved': (params: { agent: Pick<IUser, '_id' | 'username'> }) => void;
 	'afterAddedToRoom': (params: { user: IUser; inviter?: IUser }, room: IRoom) => void;
-	'beforeAddedToRoom': (params: { user: IUser; inviter: IUser }) => void;
+	'beforeAddedToRoom': (params: { user: AtLeast<IUser, 'federated' | 'roles'>; inviter: IUser }) => void;
 	'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id'] }) => void;
 	'beforeDeleteRoom': (params: IRoom) => void;
 	'beforeJoinDefaultChannels': (user: IUser) => void;
diff --git a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts
index 13540246f0e..92e76d8c2ec 100644
--- a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts
+++ b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts
@@ -1,28 +1,23 @@
-import type { IUser } from '@rocket.chat/core-typings';
-import { Settings } from '@rocket.chat/models';
+import type { AtLeast, IUser } from '@rocket.chat/core-typings';
 
-export const getSubscriptionAutotranslateDefaultConfig = async (
-	user: IUser,
-): Promise<
+import { settings } from '../../app/settings/server';
+
+export function getSubscriptionAutotranslateDefaultConfig(user: AtLeast<IUser, 'settings'>):
 	| {
 			autoTranslate: boolean;
 			autoTranslateLanguage: string;
 	  }
-	| undefined
-> => {
-	const [autoEnableSetting, languageSetting] = await Promise.all([
-		Settings.findOneById('AutoTranslate_AutoEnableOnJoinRoom'),
-		Settings.findOneById('Language'),
-	]);
-	const { language: userLanguage } = user.settings?.preferences || {};
-
-	if (!autoEnableSetting?.value) {
+	| undefined {
+	if (!settings.get('AutoTranslate_AutoEnableOnJoinRoom')) {
 		return;
 	}
 
-	if (!userLanguage || userLanguage === 'default' || languageSetting?.value === userLanguage) {
+	const languageSetting = settings.get('Language');
+
+	const { language: userLanguage } = user.settings?.preferences || {};
+	if (!userLanguage || userLanguage === 'default' || languageSetting === userLanguage) {
 		return;
 	}
 
 	return { autoTranslate: true, autoTranslateLanguage: userLanguage };
-};
+}
diff --git a/apps/meteor/server/lib/roles/addUserRoles.ts b/apps/meteor/server/lib/roles/addUserRoles.ts
index 395056903ae..a064553f5cb 100644
--- a/apps/meteor/server/lib/roles/addUserRoles.ts
+++ b/apps/meteor/server/lib/roles/addUserRoles.ts
@@ -1,6 +1,6 @@
 import { MeteorError } from '@rocket.chat/core-services';
 import type { IRole, IUser, IRoom } from '@rocket.chat/core-typings';
-import { Users, Roles } from '@rocket.chat/models';
+import { Roles } from '@rocket.chat/models';
 
 import { validateRoleList } from './validateRoleList';
 
@@ -9,11 +9,6 @@ export const addUserRolesAsync = async (userId: IUser['_id'], roleIds: IRole['_i
 		return false;
 	}
 
-	const user = await Users.findOneById(userId, { projection: { _id: 1 } });
-	if (!user) {
-		throw new MeteorError('error-invalid-user', 'Invalid user');
-	}
-
 	if (!(await validateRoleList(roleIds))) {
 		throw new MeteorError('error-invalid-role', 'Invalid role');
 	}
diff --git a/apps/meteor/server/methods/addAllUserToRoom.ts b/apps/meteor/server/methods/addAllUserToRoom.ts
index acba1bed406..11232908b84 100644
--- a/apps/meteor/server/methods/addAllUserToRoom.ts
+++ b/apps/meteor/server/methods/addAllUserToRoom.ts
@@ -56,7 +56,7 @@ Meteor.methods<ServerMethods>({
 				continue;
 			}
 			await callbacks.run('beforeJoinRoom', user, room);
-			const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user);
+			const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user);
 			await Subscriptions.createWithRoomAndUser(room, user, {
 				ts: now,
 				open: true,
diff --git a/apps/meteor/server/methods/createDirectMessage.ts b/apps/meteor/server/methods/createDirectMessage.ts
index d92c7e46292..ccbfe8916ca 100644
--- a/apps/meteor/server/methods/createDirectMessage.ts
+++ b/apps/meteor/server/methods/createDirectMessage.ts
@@ -104,7 +104,11 @@ export async function createDirectMessage(
 	} catch (error) {
 		throw new Meteor.Error((error as any)?.message);
 	}
-	const { _id: rid, inserted, ...room } = await createRoom('d', undefined, undefined, roomUsers as IUser[], false, undefined, {}, options);
+	const {
+		_id: rid,
+		inserted,
+		...room
+	} = await createRoom<'d'>('d', undefined, undefined, roomUsers as IUser[], false, undefined, {}, options);
 
 	return {
 		// @ts-expect-error - room type is already defined in the `createRoom` return type
diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts
index 4b42367bad0..c4ba44bdd7f 100644
--- a/apps/meteor/server/models/raw/Subscriptions.ts
+++ b/apps/meteor/server/models/raw/Subscriptions.ts
@@ -1,4 +1,13 @@
-import type { IRole, IRoom, ISubscription, IUser, RocketChatRecordDeleted, RoomType, SpotlightUser } from '@rocket.chat/core-typings';
+import type {
+	AtLeast,
+	IRole,
+	IRoom,
+	ISubscription,
+	IUser,
+	RocketChatRecordDeleted,
+	RoomType,
+	SpotlightUser,
+} from '@rocket.chat/core-typings';
 import type { ISubscriptionsModel } from '@rocket.chat/model-typings';
 import { Rooms, Users } from '@rocket.chat/models';
 import { escapeRegExp } from '@rocket.chat/string-helpers';
@@ -17,6 +26,7 @@ import type {
 	IndexDescription,
 	UpdateFilter,
 	InsertOneResult,
+	InsertManyResult,
 } from 'mongodb';
 
 import { getDefaultSubscriptionPref } from '../../../app/utils/lib/getDefaultSubscriptionPref';
@@ -1605,6 +1615,38 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
 		return result;
 	}
 
+	async createWithRoomAndManyUsers(
+		room: IRoom,
+		users: { user: AtLeast<IUser, '_id' | 'username' | 'name' | 'settings'>; extraData: Record<string, any> }[] = [],
+	): Promise<InsertManyResult<ISubscription>> {
+		const subscriptions = users.map(({ user, extraData }) => ({
+			open: false,
+			alert: false,
+			unread: 0,
+			userMentions: 0,
+			groupMentions: 0,
+			ts: room.ts,
+			rid: room._id,
+			name: room.name,
+			fname: room.fname,
+			...(room.customFields && { customFields: room.customFields }),
+			t: room.t,
+			u: {
+				_id: user._id,
+				username: user.username,
+				name: user.name,
+			},
+			...(room.prid && { prid: room.prid }),
+			...getDefaultSubscriptionPref(user),
+			...extraData,
+		}));
+
+		// @ts-expect-error - types not good :(
+		const result = await this.insertMany(subscriptions);
+
+		return result;
+	}
+
 	// REMOVE
 	async removeByUserId(userId: string): Promise<number> {
 		const query = {
diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js
index 0663bbdcda2..113f18ea83d 100644
--- a/apps/meteor/server/models/raw/Users.js
+++ b/apps/meteor/server/models/raw/Users.js
@@ -384,6 +384,10 @@ export class UsersRaw extends BaseRaw {
 	}
 
 	findOneByUsernameIgnoringCase(username, options) {
+		if (!username) {
+			throw new Error('invalid username');
+		}
+
 		const query = { username };
 
 		return this.findOne(query, {
@@ -1488,6 +1492,18 @@ export class UsersRaw extends BaseRaw {
 		);
 	}
 
+	addRoomByUserIds(uids, rid) {
+		return this.updateMany(
+			{
+				_id: { $in: uids },
+				__rooms: { $ne: rid },
+			},
+			{
+				$addToSet: { __rooms: rid },
+			},
+		);
+	}
+
 	removeRoomByRoomIds(rids) {
 		return this.updateMany(
 			{
diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts
index 018a5f87704..c4aee8bcf2a 100644
--- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts
+++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts
@@ -58,7 +58,12 @@ export class RocketChatRoomAdapter {
 				.trim()
 				.replace(/ /g, '-'),
 		);
-		const { rid, _id } = await createRoom(federatedRoom.getRoomType(), roomName, usernameOrId);
+		const owner = await Users.findOneByUsernameIgnoringCase(usernameOrId);
+		if (!owner) {
+			throw new Error('Cannot create a room without a creator');
+		}
+
+		const { rid, _id } = await createRoom(federatedRoom.getRoomType(), roomName, owner);
 		const roomId = rid || _id;
 		await MatrixBridgedRoom.createOrUpdateByLocalRoomId(
 			roomId,
@@ -90,10 +95,16 @@ export class RocketChatRoomAdapter {
 		const readonly = false;
 		const excludeSelf = false;
 		const extraData = undefined;
+
+		const owner = await Users.findOneByUsernameIgnoringCase(usernameOrId);
+		if (!owner) {
+			throw new Error('Cannot create a room without a creator');
+		}
+
 		const { rid, _id } = await createRoom(
 			federatedRoom.getRoomType(),
 			federatedRoom.getDisplayName(),
-			usernameOrId,
+			owner,
 			federatedRoom.getMembersUsernames(),
 			excludeSelf,
 			readonly,
diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts
index 61b5bfeee50..7b9b85cecbd 100644
--- a/apps/meteor/server/services/room/service.ts
+++ b/apps/meteor/server/services/room/service.ts
@@ -23,15 +23,13 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
 			throw new Error('no-permission');
 		}
 
-		const user = await Users.findOneById<Pick<IUser, 'username'>>(uid, {
-			projection: { username: 1 },
-		});
+		const user = await Users.findOneById(uid);
 		if (!user?.username) {
 			throw new Error('User not found');
 		}
 
 		// TODO convert `createRoom` function to "raw" and move to here
-		return createRoom(type, name, user.username, members, false, readOnly, extraData, options) as unknown as IRoom;
+		return createRoom(type, name, user, members, false, readOnly, extraData, options) as unknown as IRoom;
 	}
 
 	async createDirectMessage({ to, from }: { to: string; from: string }): Promise<{ rid: string }> {
diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts
index d9eee82029a..f7be69ce2a7 100644
--- a/packages/core-services/src/types/IRoomService.ts
+++ b/packages/core-services/src/types/IRoomService.ts
@@ -4,6 +4,7 @@ export interface ISubscriptionExtraData {
 	open: boolean;
 	ls?: Date;
 	prid?: string;
+	roles?: string[];
 }
 
 interface ICreateRoomOptions extends Partial<Record<string, string | ISubscriptionExtraData>> {
diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts
index aebda87c78c..53b0a69ec23 100644
--- a/packages/model-typings/src/models/ISubscriptionsModel.ts
+++ b/packages/model-typings/src/models/ISubscriptionsModel.ts
@@ -1,5 +1,15 @@
-import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser } from '@rocket.chat/core-typings';
-import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Document, AggregateOptions, Filter, InsertOneResult } from 'mongodb';
+import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser, AtLeast } from '@rocket.chat/core-typings';
+import type {
+	FindOptions,
+	FindCursor,
+	UpdateResult,
+	DeleteResult,
+	Document,
+	AggregateOptions,
+	Filter,
+	InsertOneResult,
+	InsertManyResult,
+} from 'mongodb';
 
 import type { IBaseModel } from './IBaseModel';
 
@@ -216,6 +226,10 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> {
 	): Promise<UpdateResult | Document>;
 	removeByUserId(userId: string): Promise<number>;
 	createWithRoomAndUser(room: IRoom, user: IUser, extraData?: Record<string, any>): Promise<InsertOneResult<ISubscription>>;
+	createWithRoomAndManyUsers(
+		room: IRoom,
+		users: { user: AtLeast<IUser, '_id' | 'username' | 'name' | 'settings'>; extraData: Record<string, any> }[],
+	): Promise<InsertManyResult<ISubscription>>;
 	removeByRoomIdsAndUserId(rids: string[], userId: string): Promise<number>;
 	removeByRoomIdAndUserId(roomId: string, userId: string): Promise<number>;
 
diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts
index 1ee2a432c3d..f14f5bc90d0 100644
--- a/packages/model-typings/src/models/IUsersModel.ts
+++ b/packages/model-typings/src/models/IUsersModel.ts
@@ -239,6 +239,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
 	removeAllRoomsByUserId(userId: string): Promise<UpdateResult>;
 	removeRoomByUserId(userId: string, rid: string): Promise<UpdateResult>;
 	addRoomByUserId(userId: string, rid: string): Promise<UpdateResult>;
+	addRoomByUserIds(uids: string[], rid: string): Promise<UpdateResult>;
 	removeRoomByRoomIds(rids: string[]): Promise<UpdateResult | Document>;
 	getLoginTokensByUserId(userId: string): FindCursor<ILoginToken>;
 	addPersonalAccessTokenToUser(data: { userId: string; loginTokenObject: IPersonalAccessToken }): Promise<UpdateResult>;
@@ -317,7 +318,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
 	findByUsernameNameOrEmailAddress(nameOrUsernameOrEmail: string, options?: FindOptions<IUser>): FindCursor<IUser>;
 	findCrowdUsers(options?: FindOptions<IUser>): FindCursor<IUser>;
 	getLastLogin(options?: FindOptions<IUser>): Promise<Date | undefined>;
-	findUsersByUsernames(usernames: string[], options?: FindOptions<IUser>): FindCursor<IUser>;
+	findUsersByUsernames<T = IUser>(usernames: string[], options?: FindOptions<IUser>): FindCursor<T>;
 	findUsersByIds(userIds: string[], options?: FindOptions<IUser>): FindCursor<IUser>;
 	findUsersWithUsernameByIds(userIds: string[], options?: FindOptions<IUser>): FindCursor<IUser>;
 	findUsersWithUsernameByIdsNotOffline(userIds: string[], options?: FindOptions<IUser>): FindCursor<IUser>;
diff --git a/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts b/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts
index c8bb88cfc1c..e25dfb0ce2f 100644
--- a/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts
+++ b/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts
@@ -12,6 +12,7 @@ export type ChannelsCreateProps = {
 		encrypted?: boolean;
 		teamId?: string;
 	};
+	excludeSelf?: boolean;
 };
 
 const channelsCreatePropsSchema = {
diff --git a/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts b/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts
index c34a720bd4b..7c3781d787c 100644
--- a/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts
+++ b/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts
@@ -14,6 +14,7 @@ export type GroupsCreateProps = {
 		encrypted: boolean;
 		teamId?: string;
 	};
+	excludeSelf?: boolean;
 };
 
 const GroupsCreatePropsSchema = {
-- 
GitLab