Skip to content

Commit

Permalink
feat: Disable slash commands for encrypted rooms (#32548)
Browse files Browse the repository at this point in the history
  • Loading branch information
yash-rajpal authored Jun 19, 2024
1 parent c5edd04 commit ee43f2c
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/popular-bulldogs-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/i18n': patch
'@rocket.chat/meteor': patch
---

Disable slash commands in encrypted rooms and show a disabled warning.
10 changes: 9 additions & 1 deletion apps/meteor/client/lib/chats/ChatAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,15 @@ export type ChatAPI = {

readonly flows: {
readonly uploadFiles: (files: readonly File[], resetFileInput?: () => void) => Promise<void>;
readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean; previewUrls?: string[] }) => Promise<boolean>;
readonly sendMessage: ({
text,
tshow,
}: {
text: string;
tshow?: boolean;
previewUrls?: string[];
isSlashCommandAllowed?: boolean;
}) => Promise<boolean>;
readonly processSlashCommand: (message: IMessage, userId: string | null) => Promise<boolean>;
readonly processTooLongMessage: (message: IMessage) => Promise<boolean>;
readonly processMessageEditing: (
Expand Down
13 changes: 9 additions & 4 deletions apps/meteor/client/lib/chats/flows/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { processSetReaction } from './processSetReaction';
import { processSlashCommand } from './processSlashCommand';
import { processTooLongMessage } from './processTooLongMessage';

const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[]): Promise<void> => {
const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], isSlashCommandAllowed?: boolean): Promise<void> => {
KonchatNotification.removeRoomNotification(message.rid);

if (await processSetReaction(chat, message)) {
Expand All @@ -25,7 +25,7 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[])
return;
}

if (await processSlashCommand(chat, message)) {
if (isSlashCommandAllowed && (await processSlashCommand(chat, message))) {
return;
}

Expand All @@ -34,7 +34,12 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[])

export const sendMessage = async (
chat: ChatAPI,
{ text, tshow, previewUrls }: { text: string; tshow?: boolean; previewUrls?: string[] },
{
text,
tshow,
previewUrls,
isSlashCommandAllowed,
}: { text: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean },
): Promise<boolean> => {
if (!(await chat.data.isSubscribedToRoom())) {
try {
Expand Down Expand Up @@ -63,7 +68,7 @@ export const sendMessage = async (
});

try {
await process(chat, message, previewUrls);
await process(chat, message, previewUrls, isSlashCommandAllowed);
chat.composer?.dismissAllQuotedMessages();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
Expand Down
11 changes: 10 additions & 1 deletion apps/meteor/client/views/room/composer/ComposerBoxPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ComposerBoxPopupProps<
T extends {
_id: string;
sort?: number;
disabled?: boolean;
},
> = {
title?: string;
Expand All @@ -22,6 +23,7 @@ function ComposerBoxPopup<
T extends {
_id: string;
sort?: number;
disabled?: boolean;
},
>({
title,
Expand All @@ -37,7 +39,9 @@ function ComposerBoxPopup<

const variant = popupSizes && popupSizes.inlineSize < 480 ? 'small' : 'large';

const getOptionTitle = <T extends { _id: string; sort?: number; outside?: boolean; suggestion?: boolean }>(item: T) => {
const getOptionTitle = <T extends { _id: string; sort?: number; outside?: boolean; suggestion?: boolean; disabled?: boolean }>(
item: T,
) => {
if (variant !== 'small') {
return undefined;
}
Expand All @@ -49,6 +53,10 @@ function ComposerBoxPopup<
if (item.suggestion) {
return t('Suggestion_from_recent_messages');
}

if (item.disabled) {
return t('Unavailable_in_encrypted_channels');
}
};

const itemsFlat = useMemo(
Expand Down Expand Up @@ -96,6 +104,7 @@ function ComposerBoxPopup<
id={`popup-item-${item._id}`}
tabIndex={item === focused ? 0 : -1}
aria-selected={item === focused}
disabled={item.disabled}
>
{renderItem({ item: { ...item, variant } })}
</Option>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { OptionColumn, OptionContent, OptionDescription, OptionInput } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';

export type ComposerBoxPopupSlashCommandProps = {
_id: string;
description?: string;
params?: string;
disabled?: boolean;
};

function ComposerBoxPopupSlashCommand({ _id, description, params }: ComposerBoxPopupSlashCommandProps) {
function ComposerBoxPopupSlashCommand({ _id, description, params, disabled }: ComposerBoxPopupSlashCommandProps) {
const t = useTranslation();

return (
<>
<OptionContent>
{_id} <OptionDescription>{params}</OptionDescription>
</OptionContent>
<OptionColumn>
<OptionInput>{description}</OptionInput>
<OptionInput>{disabled ? t('Unavailable_in_encrypted_channels') : description}</OptionInput>
</OptionColumn>
</>
);
Expand Down
13 changes: 12 additions & 1 deletion apps/meteor/client/views/room/composer/ComposerMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,24 @@ const ComposerMessage = ({ tmid, readOnly, onSend, ...props }: ComposerMessagePr
}
},

onSend: async ({ value: text, tshow, previewUrls }: { value: string; tshow?: boolean; previewUrls?: string[] }): Promise<void> => {
onSend: async ({
value: text,
tshow,
previewUrls,
isSlashCommandAllowed,
}: {
value: string;
tshow?: boolean;
previewUrls?: string[];
isSlashCommandAllowed?: boolean;
}): Promise<void> => {
try {
await chat?.action.stop('typing');
const newMessageSent = await chat?.flows.sendMessage({
text,
tshow,
previewUrls,
isSlashCommandAllowed,
});
if (newMessageSent) onSend?.();
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
MessageComposerHint,
MessageComposerButton,
} from '@rocket.chat/ui-composer';
import { useTranslation, useUserPreference, useLayout } from '@rocket.chat/ui-contexts';
import { useTranslation, useUserPreference, useLayout, useSetting } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import type {
ReactElement,
Expand Down Expand Up @@ -92,7 +92,7 @@ const getEmptyArray = () => a;
type MessageBoxProps = {
tmid?: IMessage['_id'];
readOnly: boolean;
onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[] }) => Promise<void>;
onSend?: (params: { value: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean }) => Promise<void>;
onJoin?: () => Promise<void>;
onResize?: () => void;
onTyping?: () => void;
Expand Down Expand Up @@ -123,6 +123,9 @@ const MessageBox = ({
const chat = useChat();
const room = useRoom();
const t = useTranslation();
const e2eEnabled = useSetting<boolean>('E2E_Enable');
const unencryptedMessagesAllowed = useSetting<boolean>('E2E_Allow_Unencrypted_Messages');
const isSlashCommandAllowed = !e2eEnabled || !room.encrypted || unencryptedMessagesAllowed;
const composerPlaceholder = useMessageBoxPlaceholder(t('Message'), room);

const [typing, setTyping] = useReducer(reducer, false);
Expand Down Expand Up @@ -176,6 +179,7 @@ const MessageBox = ({
value: text,
tshow,
previewUrls,
isSlashCommandAllowed,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type ComposerPopupOption<T extends { _id: string; sort?: number } = { _id
getValue: (item: T) => string;

renderItem?: ({ item }: { item: T }) => ReactElement;
disabled?: boolean;
};

export type ComposerPopupContextValue = ComposerPopupOption[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ import type { ComposerPopupContextValue } from '../contexts/ComposerPopupContext
import { ComposerPopupContext, createMessageBoxPopupConfig } from '../contexts/ComposerPopupContext';

const ComposerPopupProvider = ({ children, room }: { children: ReactNode; room: IRoom }) => {
const { _id: rid } = room;
const { _id: rid, encrypted: isRoomEncrypted } = room;
const userSpotlight = useMethod('spotlight');
const suggestionsCount = useSetting<number>('Number_of_users_autocomplete_suggestions');
const cannedResponseEnabled = useSetting<boolean>('Canned_Responses_Enable');
const [recentEmojis] = useLocalStorage('emoji.recent', []);
const isOmnichannel = isOmnichannelRoom(room);
const useEmoji = useUserPreference('useEmojis');
const t = useTranslation();
const e2eEnabled = useSetting<boolean>('E2E_Enable');
const unencryptedMessagesAllowed = useSetting<boolean>('E2E_Allow_Unencrypted_Messages');
const encrypted = isRoomEncrypted && e2eEnabled && !unencryptedMessagesAllowed;

const call = useMethod('getSlashCommandPreviews');
const value: ComposerPopupContextValue = useMemo(() => {
Expand Down Expand Up @@ -278,6 +281,7 @@ const ComposerPopupProvider = ({ children, room }: { children: ReactNode; room:
trigger: '/',
suffix: ' ',
triggerAnywhere: false,
disabled: encrypted,
renderItem: ({ item }) => <ComposerBoxPopupSlashCommand {...item} />,
getItemsFromLocal: async (filter: string) => {
return Object.keys(slashCommands.commands)
Expand All @@ -288,6 +292,7 @@ const ComposerPopupProvider = ({ children, room }: { children: ReactNode; room:
params: item.params && t.has(item.params) ? t(item.params) : item.params ?? '',
description: item.description && t.has(item.description) ? t(item.description) : item.description,
permission: item.permission,
...(encrypted && { disabled: encrypted }),
};
})
.filter((command) => {
Expand Down Expand Up @@ -360,7 +365,7 @@ const ComposerPopupProvider = ({ children, room }: { children: ReactNode; room:
},
}),
].filter(Boolean);
}, [t, cannedResponseEnabled, isOmnichannel, recentEmojis, suggestionsCount, userSpotlight, rid, call, useEmoji]);
}, [t, cannedResponseEnabled, isOmnichannel, recentEmojis, suggestionsCount, userSpotlight, rid, call, useEmoji, encrypted]);

return <ComposerPopupContext.Provider value={value} children={children} />;
};
Expand Down
63 changes: 63 additions & 0 deletions apps/meteor/tests/e2e/e2e-encryption.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,69 @@ test.describe.serial('e2e-encryption', () => {
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});

test('expect slash commands to be enabled in an e2ee room', 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('This is an encrypted message.');

await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();

await page.locator('[name="msg"]').type('/');
await expect(page.locator('#popup-item-contextualbar')).not.toHaveClass(/disabled/);
await page.locator('[name="msg"]').clear();

await poHomeChannel.content.dispatchSlashCommand('/contextualbar');
await expect(poHomeChannel.btnContextualbarClose).toBeVisible();

await poHomeChannel.btnContextualbarClose.click();
await expect(poHomeChannel.btnContextualbarClose).toBeHidden();
});

test.describe('un-encrypted messages not allowed in e2ee rooms', () => {
let poHomeChannel: HomeChannel;

test.beforeEach(async ({ page }) => {
poHomeChannel = new HomeChannel(page);
await page.goto('/home');
});
test.beforeAll(async ({ api }) => {
expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200);
});

test.afterAll(async ({ api }) => {
expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true })).status()).toBe(200);
});

test('expect slash commands to be disabled in an e2ee room', 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('This is an encrypted message.');

await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();

await page.locator('[name="msg"]').type('/');
await expect(page.locator('#popup-item-contextualbar')).toHaveClass(/disabled/);
});
});

test('expect create a private channel, send unecrypted messages, encrypt the channel and delete the last message and check the last message in the sidebar', async ({
page,
}) => {
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -5475,6 +5475,7 @@
"Unassigned": "Unassigned",
"unauthorized": "Not authorized",
"Unavailable": "Unavailable",
"Unavailable_in_encrypted_channels": "Unavailable in encrypted channels",
"Unblock": "Unblock",
"Unblock_User": "Unblock User",
"Uncheck_All": "Uncheck All",
Expand Down

0 comments on commit ee43f2c

Please sign in to comment.