Skip to content

Commit

Permalink
feat: E2EE messages mentions (#32510)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugocostadev committed Sep 20, 2024
1 parent 7faba77 commit 274f4f5
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 7 deletions.
7 changes: 7 additions & 0 deletions .changeset/late-planes-sniff.md
Original file line number Diff line number Diff line change
@@ -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
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', 'customFields', 'content'];
const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content', 'e2eMentions'];

export async function executeUpdateMessage(
uid: IUser['_id'],
Expand Down
20 changes: 15 additions & 5 deletions apps/meteor/app/mentions/server/Mentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -43,8 +43,13 @@ export class MentionsServer extends MentionsParser {
});
}

async getUsersByMentions({ msg, rid, u: sender }: Pick<IMessage, 'msg' | 'rid' | 'u'>): Promise<IMessage['mentions']> {
const mentions = this.getUserMentions(msg);
async getUsersByMentions(message: IMessage): Promise<IMessage['mentions']> {
const { msg, rid, u: sender, e2eMentions }: Pick<IMessage, 'msg' | 'rid' | 'u' | 't' | 'e2eMentions'> = message;

const mentions =
isE2EEMessage(message) && e2eMentions?.e2eUserMentions && e2eMentions?.e2eUserMentions.length > 0
? e2eMentions?.e2eUserMentions
: this.getUserMentions(msg);
const mentionsAll: { _id: string; username: string }[] = [];
const userMentions = [];

Expand All @@ -67,8 +72,13 @@ export class MentionsServer extends MentionsParser {
return [...mentionsAll, ...(userMentions.length ? await this.getUsers(userMentions) : [])];
}

async getChannelbyMentions({ msg }: Pick<IMessage, 'msg'>) {
const channels = this.getChannelMentions(msg);
async getChannelbyMentions(message: IMessage) {
const { msg, e2eMentions }: Pick<IMessage, 'msg' | 't' | 'e2eMentions'> = 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)));
}

Expand Down
22 changes: 22 additions & 0 deletions apps/meteor/client/startup/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -88,6 +89,27 @@ Meteor.startup(() => {
return message;
}

const mentionsEnabled = settings.get<boolean>('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);
});
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/server/settings/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
});
71 changes: 71 additions & 0 deletions apps/meteor/tests/e2e/e2e-encryption.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand Down
1 change: 1 addition & 0 deletions packages/core-typings/src/IMessage/IMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 274f4f5

Please sign in to comment.