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
122 lines
4.7 KiB
TypeScript
122 lines
4.7 KiB
TypeScript
import type { IEditedMessage, IMessage, IUser, AtLeast } from '@rocket.chat/core-typings';
|
|
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
|
import { Messages, Users } from '@rocket.chat/models';
|
|
import { Match, check } from 'meteor/check';
|
|
import { Meteor } from 'meteor/meteor';
|
|
import moment from 'moment';
|
|
|
|
import { canSendMessageAsync } from '../../lib/authorization/canSendMessage';
|
|
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
|
import { applyAirGappedRestrictionsValidation } from '../../lib/cloud/license/airGappedRestrictionsWrapper';
|
|
import { updateMessage } from '../../lib/messages/updateMessage';
|
|
import { settings } from '../../settings';
|
|
|
|
const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content', 'e2eMentions'];
|
|
|
|
export async function executeUpdateMessage(
|
|
uid: IUser['_id'],
|
|
message: AtLeast<IMessage, '_id' | 'rid' | 'msg' | 'customFields'> | AtLeast<IMessage, '_id' | 'rid' | 'content'>,
|
|
previewUrls?: string[],
|
|
) {
|
|
const originalMessage = await Messages.findOneById(message._id);
|
|
if (!originalMessage?._id) {
|
|
return;
|
|
}
|
|
|
|
Object.entries(message).forEach(([key, value]) => {
|
|
if (!allowedEditedFields.includes(key) && value !== originalMessage[key as keyof IMessage]) {
|
|
throw new Meteor.Error('error-invalid-update-key', `Cannot update the message ${key}`, {
|
|
method: 'updateMessage',
|
|
});
|
|
}
|
|
});
|
|
|
|
// IF the message has custom fields, always update
|
|
// Ideally, we'll compare the custom fields to check for change, but since we don't know the shape of
|
|
// custom fields, as it's user defined, we're gonna update
|
|
const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg;
|
|
if (msgText === message.msg && !previewUrls && !message.customFields) {
|
|
return;
|
|
}
|
|
|
|
if (!!message.tmid && originalMessage._id === message.tmid) {
|
|
throw new Meteor.Error('error-message-same-as-tmid', 'Cannot set tmid the same as the _id', {
|
|
method: 'updateMessage',
|
|
});
|
|
}
|
|
|
|
if (!originalMessage.tmid && !!message.tmid) {
|
|
throw new Meteor.Error('error-message-change-to-thread', 'Cannot update message to a thread', { method: 'updateMessage' });
|
|
}
|
|
|
|
const _hasPermission = await hasPermissionAsync(uid, 'edit-message', message.rid);
|
|
const editAllowed = settings.get('Message_AllowEditing');
|
|
const editOwn = originalMessage.u && originalMessage.u._id === uid;
|
|
|
|
if (!_hasPermission && (!editAllowed || !editOwn)) {
|
|
throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', {
|
|
method: 'updateMessage',
|
|
action: 'Message_editing',
|
|
});
|
|
}
|
|
|
|
const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes');
|
|
const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete', message.rid);
|
|
|
|
if (!bypassBlockTimeLimit && Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) {
|
|
let currentTsDiff = 0;
|
|
let msgTs;
|
|
|
|
if (originalMessage.ts instanceof Date || Match.test(originalMessage.ts, Number)) {
|
|
msgTs = moment(originalMessage.ts);
|
|
}
|
|
if (msgTs) {
|
|
currentTsDiff = moment().diff(msgTs, 'minutes');
|
|
}
|
|
if (currentTsDiff >= blockEditInMinutes) {
|
|
throw new Meteor.Error('error-message-editing-blocked', 'Message editing is blocked', {
|
|
method: 'updateMessage',
|
|
});
|
|
}
|
|
}
|
|
|
|
const user = await Users.findOneById(uid);
|
|
if (!user) {
|
|
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' });
|
|
}
|
|
await canSendMessageAsync(message.rid, { uid: user._id, username: user.username ?? undefined, ...user });
|
|
|
|
// It is possible to have an empty array as the attachments property, so ensure both things exist
|
|
if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) {
|
|
originalMessage.attachments[0].description = message.msg;
|
|
message.attachments = originalMessage.attachments;
|
|
message.msg = originalMessage.msg;
|
|
}
|
|
|
|
message.u = originalMessage.u;
|
|
|
|
return updateMessage(message, user, originalMessage, previewUrls);
|
|
}
|
|
|
|
declare module '@rocket.chat/ddp-client' {
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
interface ServerMethods {
|
|
updateMessage(message: IEditedMessage, previewUrls?: string[]): void;
|
|
}
|
|
}
|
|
|
|
Meteor.methods<ServerMethods>({
|
|
async updateMessage(message: IEditedMessage, previewUrls?: string[]) {
|
|
check(message, Match.ObjectIncluding({ _id: String }));
|
|
check(previewUrls, Match.Maybe([String]));
|
|
|
|
const uid = Meteor.userId();
|
|
|
|
if (!uid) {
|
|
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' });
|
|
}
|
|
|
|
return applyAirGappedRestrictionsValidation(() => executeUpdateMessage(uid, message, previewUrls));
|
|
},
|
|
});
|