From 274f4f58812cc0409c62380ca4292a64b6ab04b5 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Thu, 19 Sep 2024 21:15:14 -0300 Subject: [PATCH] feat: E2EE messages mentions (#32510) --- .changeset/late-planes-sniff.md | 7 ++ .../app/lib/server/methods/updateMessage.ts | 2 +- apps/meteor/app/mentions/server/Mentions.ts | 20 ++++-- apps/meteor/client/startup/e2e.ts | 22 ++++++ apps/meteor/server/settings/e2e.ts | 6 ++ apps/meteor/tests/e2e/e2e-encryption.spec.ts | 71 +++++++++++++++++++ .../page-objects/fragments/home-content.ts | 2 +- .../core-typings/src/IMessage/IMessage.ts | 1 + packages/i18n/src/locales/en.i18n.json | 2 + 9 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 .changeset/late-planes-sniff.md diff --git a/.changeset/late-planes-sniff.md b/.changeset/late-planes-sniff.md new file mode 100644 index 000000000000..d702a938da78 --- /dev/null +++ b/.changeset/late-planes-sniff.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": patch +"@rocket.chat/i18n": patch +--- + +Added a new setting to enable mentions in end to end encrypted channels diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index 8cebe563cd23..c03208a438e9 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -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', 'customFields', 'content']; +const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content', 'e2eMentions']; export async function executeUpdateMessage( uid: IUser['_id'], diff --git a/apps/meteor/app/mentions/server/Mentions.ts b/apps/meteor/app/mentions/server/Mentions.ts index 9eda56fea21c..779af2087932 100644 --- a/apps/meteor/app/mentions/server/Mentions.ts +++ b/apps/meteor/app/mentions/server/Mentions.ts @@ -2,7 +2,7 @@ * Mentions is a named function that will process Mentions * @param {Object} message - The message object */ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { isE2EEMessage, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; import { type MentionsParserArgs, MentionsParser } from '../lib/MentionsParser'; @@ -43,8 +43,13 @@ export class MentionsServer extends MentionsParser { }); } - async getUsersByMentions({ msg, rid, u: sender }: Pick): Promise { - const mentions = this.getUserMentions(msg); + async getUsersByMentions(message: IMessage): Promise { + const { msg, rid, u: sender, e2eMentions }: Pick = message; + + const mentions = + isE2EEMessage(message) && e2eMentions?.e2eUserMentions && e2eMentions?.e2eUserMentions.length > 0 + ? e2eMentions?.e2eUserMentions + : this.getUserMentions(msg); const mentionsAll: { _id: string; username: string }[] = []; const userMentions = []; @@ -67,8 +72,13 @@ export class MentionsServer extends MentionsParser { return [...mentionsAll, ...(userMentions.length ? await this.getUsers(userMentions) : [])]; } - async getChannelbyMentions({ msg }: Pick) { - const channels = this.getChannelMentions(msg); + async getChannelbyMentions(message: IMessage) { + const { msg, e2eMentions }: Pick = message; + + const channels = + isE2EEMessage(message) && e2eMentions?.e2eChannelMentions && e2eMentions?.e2eChannelMentions.length > 0 + ? e2eMentions?.e2eChannelMentions + : this.getChannelMentions(msg); return this.getChannels(channels.map((c) => c.trim().substr(1))); } diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index de615e8f45de..e45b62563726 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -5,6 +5,7 @@ import { Tracker } from 'meteor/tracker'; import { E2EEState } from '../../app/e2e/client/E2EEState'; import { e2e } from '../../app/e2e/client/rocketchat.e2e'; +import { MentionsParser } from '../../app/mentions/lib/MentionsParser'; import { ChatRoom } from '../../app/models/client'; import { settings } from '../../app/settings/client'; import { onClientBeforeSendMessage } from '../lib/onClientBeforeSendMessage'; @@ -88,6 +89,27 @@ Meteor.startup(() => { return message; } + const mentionsEnabled = settings.get('E2E_Enabled_Mentions'); + + if (mentionsEnabled) { + const me = Meteor.user()?.username || ''; + const pattern = settings.get('UTF8_User_Names_Validation'); + const useRealName = settings.get('UI_Use_Real_Name'); + + const mentions = new MentionsParser({ + pattern: () => pattern, + useRealName: () => useRealName, + me: () => me, + }); + + const e2eMentions: IMessage['e2eMentions'] = { + e2eUserMentions: mentions.getUserMentions(message.msg), + e2eChannelMentions: mentions.getChannelMentions(message.msg), + }; + + message.e2eMentions = e2eMentions; + } + // Should encrypt this message. return e2eRoom.encryptMessage(message); }); diff --git a/apps/meteor/server/settings/e2e.ts b/apps/meteor/server/settings/e2e.ts index 6f22784f1709..c8a69757128b 100644 --- a/apps/meteor/server/settings/e2e.ts +++ b/apps/meteor/server/settings/e2e.ts @@ -35,4 +35,10 @@ export const createE2ESettings = () => public: true, enableQuery: { _id: 'E2E_Enable', value: true }, }); + + await this.add('E2E_Enabled_Mentions', false, { + type: 'boolean', + public: true, + enableQuery: { _id: 'E2E_Enable', value: true }, + }); }); diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 8c6297e9975d..ad98df1aaa53 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -133,11 +133,13 @@ test.describe.serial('e2e-encryption', () => { test.beforeAll(async ({ api }) => { expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true })).status()).toBe(200); + expect((await api.post('/settings/E2E_Enabled_Mentions', { value: true })).status()).toBe(200); }); test.afterAll(async ({ api }) => { expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + expect((await api.post('/settings/E2E_Enabled_Mentions', { value: false })).status()).toBe(200); }); test('expect create a private channel encrypted and send an encrypted message', async ({ page }) => { @@ -265,6 +267,75 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); + test('expect create a encrypted private channel and mention user', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('hello @user1'); + + const userMention = await page.getByRole('button', { + name: 'user1', + }); + + await expect(userMention).toBeVisible(); + }); + + test('expect create a encrypted private channel, mention a channel and navigate to it', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('Are you in the #general channel?'); + + const channelMention = await page.getByRole('button', { + name: 'general', + }); + + await expect(channelMention).toBeVisible(); + + await channelMention.click(); + + await expect(page).toHaveURL(`/channel/general`); + }); + + test('expect create a encrypted private channel, mention a channel and user', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('Are you in the #general channel, @user1 ?'); + + const channelMention = await page.getByRole('button', { + name: 'general', + }); + + const userMention = await page.getByRole('button', { + name: 'user1', + }); + + await expect(userMention).toBeVisible(); + await expect(channelMention).toBeVisible(); + }); + test('should encrypted field be available on edit room', async ({ page }) => { const channelName = faker.string.uuid(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 9d5e2081ca93..519d9a4102aa 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -85,7 +85,7 @@ export class HomeContent { await this.joinRoomIfNeeded(); await this.page.waitForSelector('[name="msg"]:not([disabled])'); await this.page.locator('[name="msg"]').fill(text); - await this.page.keyboard.press('Enter'); + await this.page.getByLabel('Send').click(); } async dispatchSlashCommand(text: string): Promise { diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 205cbaccd466..6c5511966ac8 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -170,6 +170,7 @@ export interface IMessage extends IRocketChatRecord { tcount?: number; t?: MessageTypesValues; e2e?: 'pending' | 'done'; + e2eMentions?: { e2eUserMentions?: string[]; e2eChannelMentions?: string[] }; otrAck?: string; urls?: MessageUrl[]; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 728ca256952d..8ce6bea2e117 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1805,6 +1805,8 @@ "E2E_Enabled": "E2E Enabled", "E2E_Enabled_Default_DirectRooms": "Enable encryption for Direct Rooms by default", "E2E_Enabled_Default_PrivateRooms": "Enable encryption for Private Rooms by default", + "E2E_Enabled_Mentions": "Mentions", + "E2E_Enabled_Mentions_Description": "Notify people, and highlight user, channel, and team mentions in encrypted content.", "E2E_Enable_Encrypt_Files": "Encrypt files", "E2E_Enable_Encrypt_Files_Description": "Encrypt files sent inside encrypted rooms. Check for possible conflicts in [file upload settings.](admin/settings/FileUpload)", "E2E_Encryption_Password_Change": "Change Encryption Password",