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

feat(messages): Allow Custom Fields in Messages #32224

Merged
merged 19 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
7 changes: 7 additions & 0 deletions .changeset/four-eyes-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-typings": patch
"@rocket.chat/i18n": patch
rodrigok marked this conversation as resolved.
Show resolved Hide resolved
---

feat(messages): Allow Custom Fields in Messages. API-only feature. It can be enabled and configured in Workspace Settings.
rodrigok marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 11 additions & 1 deletion apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ API.v1.addRoute(
roomId: String,
msgId: String,
text: String, // Using text to be consistant with chat.postMessage
customFields: Match.Maybe(Object),
rodrigok marked this conversation as resolved.
Show resolved Hide resolved
rodrigok marked this conversation as resolved.
Show resolved Hide resolved
previewUrls: Match.Maybe([String]),
}),
);
Expand All @@ -328,7 +329,16 @@ API.v1.addRoute(
}

// Permission checks are already done in the updateMessage method, so no need to duplicate them
await executeUpdateMessage(this.userId, { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid }, this.bodyParams.previewUrls);
await executeUpdateMessage(
this.userId,
{
_id: msg._id,
msg: this.bodyParams.text,
rid: msg.rid,
customFields: this.bodyParams.customFields as Record<string, any> | undefined,
},
this.bodyParams.previewUrls,
);

const updatedMessage = await Messages.findOneById(msg._id);
const [message] = await normalizeMessagesForUser(updatedMessage ? [updatedMessage] : [], this.userId);
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/app/lib/server/functions/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP
import { FileUpload } from '../../../file-upload/server';
import notifications from '../../../notifications/server/lib/Notifications';
import { settings } from '../../../settings/server';
import { validateCustomMessageFields } from '../lib/validateCustomMessageFields';
import { parseUrlsInMessage } from './parseUrlsInMessage';

// TODO: most of the types here are wrong, but I don't want to change them now
Expand Down Expand Up @@ -172,6 +173,14 @@ export const validateMessage = async (message: any, room: any, user: any) => {
if (Array.isArray(message.attachments) && message.attachments.length) {
validateBodyAttachments(message.attachments);
}

if (message.customFields) {
validateCustomMessageFields({
customFields: message.customFields,
messageCustomFieldsEnabled: settings.get<boolean>('Message_CustomFields_Enabled'),
messageCustomFields: settings.get<string>('Message_CustomFields'),
});
}
};

export function prepareMessageObject(
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/app/lib/server/functions/updateMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../../lib/callbacks';
import { broadcastMessageFromData } from '../../../../server/modules/watchers/lib/messages';
import { settings } from '../../../settings/server';
import { validateCustomMessageFields } from '../lib/validateCustomMessageFields';
import { parseUrlsInMessage } from './parseUrlsInMessage';

export const updateMessage = async function (
Expand Down Expand Up @@ -59,6 +60,14 @@ export const updateMessage = async function (

messageData = await Message.beforeSave({ message: messageData, room, user });

if (messageData.customFields) {
validateCustomMessageFields({
customFields: messageData.customFields,
messageCustomFieldsEnabled: settings.get<boolean>('Message_CustomFields_Enabled'),
messageCustomFields: settings.get<string>('Message_CustomFields'),
});
}

const { _id, ...editedMessage } = messageData;

if (!editedMessage.msg) {
Expand Down
41 changes: 41 additions & 0 deletions apps/meteor/app/lib/server/lib/validateCustomMessageFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Ajv from 'ajv';
import mem from 'mem';

const ajv = new Ajv();

const customFieldsValidate = mem((customFieldsSetting: string) => {
rodrigok marked this conversation as resolved.
Show resolved Hide resolved
const schema = JSON.parse(customFieldsSetting);
rodrigok marked this conversation as resolved.
Show resolved Hide resolved

if (schema.type && schema.type !== 'object') {
throw new Error('Invalid custom fields config');
}

return ajv.compile({
...schema,
type: 'object',
additionalProperties: false,
});
});

export const validateCustomMessageFields = ({
customFields,
messageCustomFieldsEnabled,
messageCustomFields,
}: {
customFields: Record<string, any>;
messageCustomFieldsEnabled: boolean;
messageCustomFields: string;
}) => {
// get the json schema for the custom fields of the message and validate it using ajv
// if the validation fails, throw an error
// if there are no custom fields, the message object remains unchanged

if (messageCustomFieldsEnabled !== true) {
throw new Error('Custom fields not enabled');
}

const validate = customFieldsValidate(messageCustomFields);
if (!validate(customFields)) {
throw new Error('Invalid custom fields');
}
};
2 changes: 1 addition & 1 deletion apps/meteor/app/lib/server/methods/updateMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP
import { settings } from '../../../settings/server';
import { updateMessage } from '../functions/updateMessage';

const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg'];
const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields'];

export async function executeUpdateMessage(uid: IUser['_id'], message: AtLeast<IMessage, '_id' | 'rid' | 'msg'>, previewUrls?: string[]) {
const originalMessage = await Messages.findOneById(message._id);
Expand Down
31 changes: 31 additions & 0 deletions apps/meteor/server/settings/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,35 @@ export const createMessageSettings = () =>
type: 'boolean',
public: true,
});

await this.add('Message_CustomFields_Enabled', false, {
type: 'boolean',
});
await this.add(
'Message_CustomFields',
`
{
rodrigok marked this conversation as resolved.
Show resolved Hide resolved
"properties": {
"priority": {
"type": "string",
"nullable": false,
"enum": ["low", "medium", "high"]
}
},
"required": ["priority"]
}
rodrigok marked this conversation as resolved.
Show resolved Hide resolved
`,
{
type: 'code',
code: 'application/json',
invalidValue: '',
multiline: true,
enableQuery: [
{
_id: 'Message_CustomFields_Enabled',
value: true,
},
],
},
);
});
Loading
Loading