Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
328 lines
9.7 KiB
TypeScript
328 lines
9.7 KiB
TypeScript
import { AppEvents, Apps } from '@rocket.chat/apps';
|
|
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
|
|
import { FederationMatrix, Message, Room, Team } from '@rocket.chat/core-services';
|
|
import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services';
|
|
import { type ICreatedRoom, type IUser, type IRoom, type RoomType, isUserNativeFederated } from '@rocket.chat/core-typings';
|
|
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';
|
|
import { Meteor } from 'meteor/meteor';
|
|
|
|
import { createDirectRoom } from './createDirectRoom';
|
|
import { calculateRoomRolePriorityFromRoles } from '../../../lib/roles/calculateRoomRolePriorityFromRoles';
|
|
import { callbacks } from '../callbacks';
|
|
import { beforeAddUserToRoom } from '../callbacks/beforeAddUserToRoom';
|
|
import { beforeCreateRoomCallback, prepareCreateRoomCallback } from '../callbacks/beforeCreateRoomCallback';
|
|
import { getSubscriptionAutotranslateDefaultConfig } from '../getSubscriptionAutotranslateDefaultConfig';
|
|
import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../notifyListener';
|
|
import { syncRoomRolePriorityForUserAndRoom } from '../roles/syncRoomRolePriority';
|
|
import { getDefaultSubscriptionPref } from '../utils/lib/getDefaultSubscriptionPref';
|
|
import { getValidRoomName } from '../utils/lib/getValidRoomName';
|
|
|
|
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);
|
|
await syncRoomRolePriorityForUserAndRoom(owner._id, room._id, ['owner']);
|
|
|
|
if (insertedId) {
|
|
await notifyOnSubscriptionChangedById(insertedId, 'inserted');
|
|
await notifyOnRoomChanged(room, 'inserted');
|
|
}
|
|
|
|
// Invite federated members to the room SYNCRONOUSLY,
|
|
// since we do not use to invite lots of users at once, this is acceptable.
|
|
const membersToInvite = members.filter((m) => m !== owner.username);
|
|
|
|
await FederationMatrix.ensureFederatedUsersExistLocally(membersToInvite);
|
|
|
|
for (const memberUsername of membersToInvite) {
|
|
const member = await Users.findOneByUsername(memberUsername);
|
|
if (!member) {
|
|
throw new Error('Federated user not found locally');
|
|
}
|
|
|
|
await Room.createUserSubscription({
|
|
ts: new Date(),
|
|
room,
|
|
userToBeAdded: member,
|
|
inviter: owner,
|
|
status: 'INVITED',
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const subs = [];
|
|
|
|
const memberIds = [];
|
|
|
|
const memberIdAndRolePriorityMap: Record<IUser['_id'], number> = {};
|
|
|
|
const membersCursor = Users.findUsersByUsernames(members);
|
|
|
|
// TODO: Check re new federation-service - should we add them here or keep on createRoom inside of homeserver?!
|
|
for await (const member of membersCursor) {
|
|
try {
|
|
await beforeAddUserToRoom.run({ user: member, inviter: owner }, room);
|
|
await callbacks.run('beforeAddedToRoom', { user: member, inviter: owner });
|
|
} catch (error) {
|
|
continue;
|
|
}
|
|
|
|
memberIds.push(member._id);
|
|
|
|
const extra: Partial<ISubscriptionExtraData> = { open: true, ...options?.subscriptionExtra };
|
|
|
|
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,
|
|
...getDefaultSubscriptionPref(member),
|
|
},
|
|
});
|
|
|
|
if (extra.roles) {
|
|
memberIdAndRolePriorityMap[member._id] = calculateRoomRolePriorityFromRoles(extra.roles);
|
|
}
|
|
}
|
|
|
|
if (!['d', 'l'].includes(room.t)) {
|
|
await Users.addRoomByUserIds(memberIds, room._id);
|
|
}
|
|
|
|
const { insertedIds } = await Subscriptions.createWithRoomAndManyUsers(room, subs);
|
|
await Users.assignRoomRolePrioritiesByUserIdPriorityMap(memberIdAndRolePriorityMap, room._id);
|
|
|
|
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'],
|
|
): Promise<
|
|
ICreatedRoom & {
|
|
rid: string;
|
|
}
|
|
> => {
|
|
const { teamId, ...extraData } = roomExtraData || ({} as IRoom);
|
|
|
|
// TODO: use a shared helper to check whether a user is federated
|
|
const hasFederatedMembers = members.some((member) => {
|
|
if (typeof member === 'string') {
|
|
return member.includes(':') && member.includes('@');
|
|
}
|
|
return member.username?.includes(':') && member.username?.includes('@');
|
|
});
|
|
|
|
// Prevent adding federated users to rooms that are not marked as federated explicitly
|
|
if (hasFederatedMembers && extraData.federated !== true) {
|
|
throw new Meteor.Error('error-federated-users-in-non-federated-rooms', 'Cannot add federated users to non-federated rooms', {
|
|
method: 'createRoom',
|
|
});
|
|
}
|
|
|
|
await prepareCreateRoomCallback.run({
|
|
type,
|
|
// name,
|
|
// owner: ownerUsername,
|
|
// members,
|
|
// readOnly,
|
|
extraData,
|
|
// options,
|
|
});
|
|
|
|
const shouldBeHandledByFederation = extraData.federated === true;
|
|
|
|
if (shouldBeHandledByFederation && owner && !isUserNativeFederated(owner) && !(await FederationMatrix.canUserAccessFederation(owner))) {
|
|
throw new Meteor.Error('error-not-authorized-federation', 'Not authorized to access federation', {
|
|
method: 'createRoom',
|
|
});
|
|
}
|
|
|
|
if (type === 'd') {
|
|
return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?._id });
|
|
}
|
|
|
|
const memberList = [...members];
|
|
|
|
if (!onlyUsernames(memberList)) {
|
|
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 && !memberList.includes(owner.username)) {
|
|
memberList.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,
|
|
};
|
|
|
|
if (teamId) {
|
|
const team = await Team.getOneById(teamId, { projection: { _id: 1 } });
|
|
if (team) {
|
|
roomProps.teamId = team._id;
|
|
}
|
|
}
|
|
|
|
const tmp = {
|
|
...roomProps,
|
|
_USERNAMES: memberList,
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
await beforeCreateRoomCallback.run({
|
|
owner,
|
|
room: roomProps,
|
|
});
|
|
|
|
if (type === 'c') {
|
|
await callbacks.run('beforeCreateChannel', owner, roomProps);
|
|
}
|
|
|
|
const room = await Rooms.createWithFullRoomData(roomProps);
|
|
|
|
void notifyOnRoomChanged(room, 'inserted');
|
|
|
|
// If federated, we must create Matrix room BEFORE subscriptions so invites can be sent.
|
|
if (shouldBeHandledByFederation) {
|
|
// Reusing unused callback to create Matrix room.
|
|
// We should discuss the opportunity to rename it to something with "before" prefix.
|
|
await callbacks.run('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: memberList, options });
|
|
}
|
|
|
|
await createUsersSubscriptions({ room, members: memberList, 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);
|
|
|
|
void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, room);
|
|
return {
|
|
rid: room._id, // backwards compatible
|
|
inserted: true,
|
|
...room,
|
|
};
|
|
};
|