feat: Disable slash commands for encrypted rooms (#32548)

pull/32428/head
Yash Rajpal 2 years ago committed by GitHub
parent c5edd04352
commit ee43f2c57c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/popular-bulldogs-accept.md
  2. 10
      apps/meteor/client/lib/chats/ChatAPI.ts
  3. 13
      apps/meteor/client/lib/chats/flows/sendMessage.ts
  4. 11
      apps/meteor/client/views/room/composer/ComposerBoxPopup.tsx
  5. 8
      apps/meteor/client/views/room/composer/ComposerBoxPopupSlashCommand.tsx
  6. 13
      apps/meteor/client/views/room/composer/ComposerMessage.tsx
  7. 8
      apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx
  8. 1
      apps/meteor/client/views/room/contexts/ComposerPopupContext.ts
  9. 9
      apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx
  10. 63
      apps/meteor/tests/e2e/e2e-encryption.spec.ts
  11. 1
      packages/i18n/src/locales/en.i18n.json

@ -0,0 +1,6 @@
---
'@rocket.chat/i18n': patch
'@rocket.chat/meteor': patch
---
Disable slash commands in encrypted rooms and show a disabled warning.

@ -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: (

@ -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)) {
@ -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;
}
@ -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 {
@ -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 });

@ -9,6 +9,7 @@ export type ComposerBoxPopupProps<
T extends {
_id: string;
sort?: number;
disabled?: boolean;
},
> = {
title?: string;
@ -22,6 +23,7 @@ function ComposerBoxPopup<
T extends {
_id: string;
sort?: number;
disabled?: boolean;
},
>({
title,
@ -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;
}
@ -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(
@ -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>

@ -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>
</>
);

@ -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) {

@ -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,
@ -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;
@ -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);
@ -176,6 +179,7 @@ const MessageBox = ({
value: text,
tshow,
previewUrls,
isSlashCommandAllowed,
});
});

@ -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[];

@ -25,7 +25,7 @@ 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');
@ -33,6 +33,9 @@ const ComposerPopupProvider = ({ children, room }: { children: ReactNode; room:
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(() => {
@ -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)
@ -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) => {
@ -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} />;
};

@ -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,
}) => {

@ -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",

Loading…
Cancel
Save