snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,277 @@
import { Message } from '@rocket.chat/core-services';
import type { IMessage, IRoom, IUser, MessageAttachmentDefault } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Messages, Rooms, Users } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import { check, Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { afterSaveMessageAsync } from '../../hooks/messages/afterSaveMessage';
import { canSendMessageAsync } from '../../lib/authorization/canSendMessage';
import { hasAtLeastOnePermissionAsync } from '../../lib/authorization/hasPermission';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
import { i18n } from '../../lib/i18n';
import { attachMessage } from '../../lib/messages/attachMessage';
import { sendMessage } from '../../lib/messages/sendMessage';
import { addUserToRoom } from '../../lib/rooms/addUserToRoom';
import { createRoom } from '../../lib/rooms/createRoom';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { settings } from '../../settings';
const getParentRoom = async (rid: IRoom['_id']) => {
const room = await Rooms.findOne(rid);
return room && (room.prid ? Rooms.findOne(room.prid, { projection: { _id: 1 } }) : room);
};
async function createDiscussionMessage(
rid: IRoom['_id'],
user: IUser,
drid: IRoom['_id'],
msg: IMessage['msg'],
messageEmbedded?: MessageAttachmentDefault,
): Promise<IMessage> {
return Message.saveSystemMessage('discussion-created', rid, msg, user, {
drid,
...(messageEmbedded && { attachments: [messageEmbedded] }),
});
}
async function mentionMessage(
rid: IRoom['_id'],
{ _id, username, name }: Pick<IUser, '_id' | 'username' | 'name'>,
messageEmbedded?: MessageAttachmentDefault,
) {
if (!username) {
return null;
}
await Messages.insertOne({
rid,
msg: '',
u: { _id, username, name },
ts: new Date(),
_updatedAt: new Date(),
...(messageEmbedded && { attachments: [messageEmbedded] }),
});
}
type CreateDiscussionProperties = {
prid: IRoom['_id'];
pmid?: IMessage['_id'];
t_name: string;
reply?: string;
users: Array<Exclude<IUser['username'], undefined>>;
user: IUser;
encrypted?: boolean;
topic?: string;
};
const create = async ({
prid,
pmid,
t_name: discussionName,
reply,
users,
user,
encrypted,
topic,
}: CreateDiscussionProperties): Promise<IRoom & { rid: string }> => {
// if you set both, prid and pmid, and the rooms dont match... should throw an error)
let message: null | IMessage = null;
if (pmid) {
message = await Messages.findOneById(pmid);
if (!message) {
throw new Meteor.Error('error-invalid-message', 'Invalid message', {
method: 'DiscussionCreation',
});
}
if (prid) {
const parentRoom = await getParentRoom(message.rid);
if (prid !== parentRoom?._id) {
throw new Meteor.Error('error-invalid-arguments', 'Root message room ID does not match parent room ID ', {
method: 'DiscussionCreation',
});
}
} else {
prid = message.rid;
}
}
if (!prid) {
throw new Meteor.Error('error-invalid-arguments', 'Missing parent room ID', { method: 'DiscussionCreation' });
}
let parentRoom;
try {
parentRoom = await canSendMessageAsync(prid, { uid: user._id, username: user.username, type: user.type });
} catch (error) {
throw new Meteor.Error((error as Error).message);
}
if (parentRoom.prid) {
throw new Meteor.Error('error-nested-discussion', 'Cannot create nested discussions', {
method: 'DiscussionCreation',
});
}
if (typeof encrypted !== 'boolean') {
encrypted = Boolean(parentRoom.encrypted);
}
if (encrypted && reply) {
throw new Meteor.Error('error-invalid-arguments', 'Encrypted discussions must not receive an initial reply.', {
method: 'DiscussionCreation',
});
}
if (pmid) {
// No projection: the full room is spread into the returned discussion below.
const discussionAlreadyExists = await Rooms.findOne({
prid,
pmid,
});
if (discussionAlreadyExists) {
// do not allow multiple discussions to the same message'\
await addUserToRoom(discussionAlreadyExists._id, user);
return { ...discussionAlreadyExists, rid: discussionAlreadyExists._id };
}
}
const name = Random.id();
// auto invite the replied message owner
const invitedUsers = message ? [message.u.username, ...users] : users;
const type = await roomCoordinator.getRoomDirectives(parentRoom.t).getDiscussionType(parentRoom);
const description = parentRoom.encrypted ? '' : message?.msg;
const discussionTopic = topic || parentRoom.name;
if (!type) {
throw new Meteor.Error('error-invalid-type', 'Cannot define discussion room type', {
method: 'DiscussionCreation',
});
}
if (
type === 'p' &&
!encrypted &&
settings.get<boolean>('E2E_Enable') &&
settings.get<boolean>('E2E_Force_Encryption_For_Private_Rooms')
) {
throw new Meteor.Error(
'error-encrypted-private-rooms-enforced-discussion',
'Workspace policy requires all private rooms to be encrypted. To create this discussion, make the parent channel public or enable encryption on it.',
{ method: 'DiscussionCreation' },
);
}
const discussion = await createRoom(
type,
name,
user,
[...new Set(invitedUsers)].filter(Boolean),
false,
false,
{
fname: discussionName,
description, // TODO discussions remove
topic: discussionTopic,
prid,
encrypted,
},
{
creator: user._id,
},
);
let discussionMsg;
if (message) {
if (parentRoom.encrypted) {
message.msg = i18n.t('Encrypted_message');
}
await mentionMessage(discussion._id, user, attachMessage(message, parentRoom));
discussionMsg = await createDiscussionMessage(message.rid, user, discussion._id, discussionName, attachMessage(message, parentRoom));
} else {
discussionMsg = await createDiscussionMessage(prid, user, discussion._id, discussionName);
}
if (reply) {
await sendMessage(user, { msg: reply }, discussion);
}
if (discussionMsg) {
afterSaveMessageAsync(discussionMsg, parentRoom, user);
}
return discussion;
};
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
createDiscussion: typeof create;
}
}
export const createDiscussion = async (
userId: string,
{ prid, pmid, t_name: discussionName, reply, users, encrypted, topic }: Omit<CreateDiscussionProperties, 'user'>,
): Promise<
IRoom & {
rid: string;
}
> => {
if (!settings.get('Discussion_enabled')) {
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' });
}
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'DiscussionCreation',
});
}
if (!(await hasAtLeastOnePermissionAsync(userId, ['start-discussion', 'start-discussion-other-user'], prid))) {
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' });
}
const user = await Users.findOneById(userId, { projection: { services: 0 } });
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'createDiscussion',
});
}
return create({ prid, pmid, t_name: discussionName, reply, users, user, encrypted, topic });
};
Meteor.methods<ServerMethods>({
/**
* Create discussion by room or message
* @constructor
* @param {string} prid - Parent Room Id - The room id, optional if you send pmid.
* @param {string} pmid - Parent Message Id - Create the discussion by a message, optional.
* @param {string} reply - The reply, optional
* @param {string} t_name - discussion name
* @param {string[]} users - users to be added
* @param {boolean} encrypted - if the discussion's e2e encryption should be enabled.
*/
async createDiscussion({ prid, pmid, t_name: discussionName, reply, users, encrypted }: CreateDiscussionProperties) {
methodDeprecationLogger.method('createDiscussion', '9.0.0', '/v1/rooms.createDiscussion');
check(prid, Match.Maybe(String));
check(pmid, Match.Maybe(String));
check(reply, Match.Maybe(String));
check(discussionName, String);
check(users, [String]);
check(encrypted, Match.Maybe(Boolean));
const uid = Meteor.userId();
if (!uid) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'DiscussionCreation',
});
}
return createDiscussion(uid, { prid, pmid, t_name: discussionName, reply, users, encrypted });
},
});