Skip to content

Commit

Permalink
fix: Auto-translate doesn't work on E2E rooms (#30369)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Jun Grillo <[email protected]>
Co-authored-by: Tasso Evangelista <[email protected]>
  • Loading branch information
3 people authored and hugocostadev committed Oct 11, 2023
1 parent 7c7b060 commit 5f32fa6
Show file tree
Hide file tree
Showing 16 changed files with 168 additions and 20 deletions.
6 changes: 6 additions & 0 deletions .changeset/popular-actors-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
---

Do not allow auto-translation to be enabled in E2E rooms
9 changes: 8 additions & 1 deletion apps/meteor/app/autotranslate/server/methods/saveSettings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Subscriptions } from '@rocket.chat/models';
import { Subscriptions, Rooms } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -46,6 +46,13 @@ Meteor.methods<ServerMethods>({

switch (field) {
case 'autoTranslate':
const room = await Rooms.findE2ERoomById(rid, { projection: { _id: 1 } });
if (room && value === '1') {
throw new Meteor.Error('error-e2e-enabled', 'Enabling auto-translation in E2E encrypted rooms is not allowed', {
method: 'saveAutoTranslateSettings',
});
}

await Subscriptions.updateAutoTranslateById(subscription._id, value === '1');
if (!subscription.autoTranslateLanguage && options.defaultLanguage) {
await Subscriptions.updateAutoTranslateLanguageById(subscription._id, options.defaultLanguage);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Message } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import { isRegisterUser } from '@rocket.chat/core-typings';
import { Rooms } from '@rocket.chat/models';
import { Rooms, Subscriptions } from '@rocket.chat/models';
import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import type { UpdateResult } from 'mongodb';
Expand All @@ -25,5 +25,9 @@ export const saveRoomEncrypted = async function (rid: string, encrypted: boolean

await Message.saveSystemMessage(type, rid, user.username, user);
}

if (encrypted) {
await Subscriptions.disableAutoTranslateByRoomId(rid);
}
return update;
};
22 changes: 19 additions & 3 deletions apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import { e2e } from '../../../app/e2e/client/rocketchat.e2e';
import { useRoom } from '../../views/room/contexts/RoomContext';
import { dispatchToastMessage } from '../../lib/toast';
import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext';
import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext';
import { useReactiveValue } from '../useReactiveValue';

export const useE2EERoomAction = () => {
const enabled = useSetting('E2E_Enable', false);
const room = useRoom();
const subscription = useRoomSubscription();
const readyToEncrypt = useReactiveValue(useCallback(() => e2e.isReady(), [])) || room.encrypted;
const permittedToToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id);
const permittedToEditRoom = usePermission('edit-room', room._id);
Expand All @@ -21,8 +23,22 @@ export const useE2EERoomAction = () => {

const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings');

const action = useMutableCallback(() => {
void toggleE2E({ rid: room._id, encrypted: !room.encrypted });
const action = useMutableCallback(async () => {
const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted });
if (!success) {
return;
}

dispatchToastMessage({
type: 'success',
message: room.encrypted
? t('E2E_Encryption_disabled_for_room', { roomName: room.name })
: t('E2E_Encryption_enabled_for_room', { roomName: room.name }),
});

if (subscription?.autoTranslate) {
dispatchToastMessage({ type: 'success', message: t('AutoTranslate_Disabled_for_room', { roomName: room.name }) });
}
});

const enabledOnRoom = !!room.encrypted;
Expand Down
19 changes: 15 additions & 4 deletions apps/meteor/client/views/room/Header/icons/Encrypted.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@ import type { IRoom } from '@rocket.chat/core-typings';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import colors from '@rocket.chat/fuselage-tokens/colors';
import { HeaderState } from '@rocket.chat/ui-client';
import { useSetting, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import { useSetting, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import React, { memo } from 'react';

import { dispatchToastMessage } from '../../../../lib/toast';

const Encrypted = ({ room }: { room: IRoom }) => {
const t = useTranslation();
const e2eEnabled = useSetting('E2E_Enable');
const toggleE2E = useMethod('saveRoomSettings');
const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings');
const canToggleE2E = usePermission('toggle-room-e2e-encryption');
const encryptedLabel = canToggleE2E ? t('Encrypted_key_title') : t('Encrypted');
const handleE2EClick = useMutableCallback(() => {
const handleE2EClick = useMutableCallback(async () => {
if (!canToggleE2E) {
return;
}
toggleE2E(room._id, 'encrypted', !room?.encrypted);

const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted });
if (!success) {
return;
}

dispatchToastMessage({
type: 'success',
message: t('E2E_Encryption_disabled_for_room', { roomName: room.name }),
});
});
return e2eEnabled && room?.encrypted ? (
<HeaderState title={encryptedLabel} icon='key' onClick={handleE2EClick} color={colors.s500} tiny />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FieldGroup, Field, FieldLabel, FieldRow, ToggleSwitch, Select } from '@rocket.chat/fuselage';
import { Callout, FieldGroup, Field, FieldLabel, FieldRow, ToggleSwitch, Select } from '@rocket.chat/fuselage';
import type { SelectOption } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement, ChangeEvent } from 'react';
Expand All @@ -11,6 +11,7 @@ import {
ContextualbarIcon,
ContextualbarContent,
} from '../../../../components/Contextualbar';
import { useRoom } from '../../contexts/RoomContext';

type AutoTranslateProps = {
language: string;
Expand All @@ -30,6 +31,7 @@ const AutoTranslate = ({
handleClose,
}: AutoTranslateProps): ReactElement => {
const t = useTranslation();
const room = useRoom();

return (
<>
Expand All @@ -40,14 +42,24 @@ const AutoTranslate = ({
</ContextualbarHeader>
<ContextualbarContent pbs={24}>
<FieldGroup>
{room.encrypted && (
<Callout title={t('Automatic_translation_not_available')} type='warning'>
{t('Automatic_translation_not_available_info')}
</Callout>
)}
<Field>
<FieldRow>
<ToggleSwitch id='automatic-translation' onChange={handleSwitch} defaultChecked={translateEnable} />
<ToggleSwitch
id='automatic-translation'
onChange={handleSwitch}
defaultChecked={translateEnable}
disabled={room.encrypted && !translateEnable}
/>
<FieldLabel htmlFor='automatic-translation'>{t('Automatic_Translation')}</FieldLabel>
</FieldRow>
</Field>
<Field>
<FieldLabel htmlFor='language'>{t('Language')}</FieldLabel>
<FieldLabel htmlFor='translate-to'>{t('Translate_to')}</FieldLabel>
<FieldRow verticalAlign='middle'>
<Select
id='language'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useLanguage } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useMemo, useEffect, useState, memo } from 'react';
import { useTranslation } from 'react-i18next';

import { useEndpointAction } from '../../../../hooks/useEndpointAction';
import { useEndpointData } from '../../../../hooks/useEndpointData';
import { dispatchToastMessage } from '../../../../lib/toast';
import { useRoom, useRoomSubscription } from '../../contexts/RoomContext';
import { useRoomToolbox } from '../../contexts/RoomToolboxContext';
import AutoTranslate from './AutoTranslate';
Expand All @@ -16,10 +18,12 @@ const AutoTranslateWithData = (): ReactElement => {
const userLanguage = useLanguage();
const [currentLanguage, setCurrentLanguage] = useState(subscription?.autoTranslateLanguage ?? '');
const saveSettings = useEndpointAction('POST', '/v1/autotranslate.saveSettings');
const { t } = useTranslation();

const { value: translateData } = useEndpointData('/v1/autotranslate.getSupportedLanguages', {
params: useMemo(() => ({ targetLanguage: userLanguage }), [userLanguage]),
});
const languagesDict = translateData ? Object.fromEntries(translateData.languages.map((lang) => [lang.language, lang.name])) : {};

const handleChangeLanguage = useMutableCallback((value) => {
setCurrentLanguage(value);
Expand All @@ -29,6 +33,10 @@ const AutoTranslateWithData = (): ReactElement => {
field: 'autoTranslateLanguage',
value,
});
dispatchToastMessage({
type: 'success',
message: t('AutoTranslate_language_set_to', { language: languagesDict[value] }),
});
});

const handleSwitch = useMutableCallback((event) => {
Expand All @@ -37,6 +45,18 @@ const AutoTranslateWithData = (): ReactElement => {
field: 'autoTranslate',
value: event.target.checked,
});
dispatchToastMessage({
type: 'success',
message: event.target.checked
? t('AutoTranslate_Enabled_for_room', { roomName: room.name })
: t('AutoTranslate_Disabled_for_room', { roomName: room.name }),
});
if (event.target.checked && currentLanguage) {
dispatchToastMessage({
type: 'success',
message: t('AutoTranslate_language_set_to', { language: languagesDict[currentLanguage] }),
});
}
});

useEffect(() => {
Expand Down
16 changes: 12 additions & 4 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,8 @@
"Authorization_URL": "Authorization URL",
"Authorize": "Authorize",
"Authorize_access_to_your_account": "Authorize access to your account",
"Automatic_translation_not_available": "Automatic translation not available",
"Automatic_translation_not_available_info": "This room has E2E encryption enabled, translation cannot work with encrypted messages",
"Auto_Load_Images": "Auto Load Images",
"Auto_Selection": "Auto Selection",
"Auto_Translate": "Auto-Translate",
Expand All @@ -715,11 +717,14 @@
"AutoTranslate_APIKey": "API Key",
"AutoTranslate_Change_Language_Description": "Changing the auto-translate language does not translate previous messages.",
"AutoTranslate_DeepL": "DeepL",
"AutoTranslate_Disabled_for_room": "Auto-translate disabled for #{{roomName}}",
"AutoTranslate_Enabled": "Enable Auto-Translate",
"AutoTranslate_Enabled_Description": "Enabling auto-translation will allow people with the `auto-translate` permission to have all messages automatically translated into their selected language. Fees may apply.",
"AutoTranslate_Enabled_for_room": "Auto-translate enabled for #{{roomName}}",
"AutoTranslate_AutoEnableOnJoinRoom": "Auto-Translate for non-default language members",
"AutoTranslate_AutoEnableOnJoinRoom_Description": "If enabled, whenever a user with a language preference different than the workspace default joins a room, it will be automatically translated for them.",
"AutoTranslate_Google": "Google",
"AutoTranslate_language_set_to": "Auto-translate language set to {{language}}",
"AutoTranslate_Microsoft": "Microsoft",
"AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key",
"AutoTranslate_ServiceProvider": "Service Provider",
Expand Down Expand Up @@ -1676,7 +1681,7 @@
"Discussion": "Discussion",
"Discussion_Description": "Discussions are an additional way to organize conversations that allows inviting users from outside channels to participate in specific conversations.",
"Discussion_description": "Help keep an overview of what's going on! By creating a discussion, a sub-channel of the one you selected is created and both are linked.",
"Discussion_first_message_disabled_due_to_e2e": "You can start sending End-to-End encrypted messages in this discussion after its creation.",
"Discussion_first_message_disabled_due_to_e2e": "You can start sending End-to-end encrypted messages in this discussion after its creation.",
"Discussion_first_message_title": "Your message",
"Discussion_name": "Discussion name",
"Discussion_start": "Start a Discussion",
Expand Down Expand Up @@ -1739,6 +1744,8 @@
"Markdown_Marked_Tables": "Enable Marked Tables",
"duplicated-account": "Duplicated account",
"E2E Encryption": "E2E Encryption",
"E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}",
"E2E_Encryption_disabled_for_room": "End-to-end encryption disabled for #{{roomName}}",
"Markdown_Parser": "Markdown Parser",
"Markdown_SupportSchemesForLink": "Markdown Support Schemes for Link",
"E2E Encryption_Description": "Keep conversations private, ensuring only the sender and intended recipients are able to read them.",
Expand All @@ -1751,7 +1758,7 @@
"E2E_Enabled_Default_DirectRooms": "Enable encryption for Direct Rooms by default",
"E2E_Enabled_Default_PrivateRooms": "Enable encryption for Private Rooms by default",
"E2E_Encryption_Password_Change": "Change Encryption Password",
"E2E_Encryption_Password_Explanation": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.<br/><br/>This is end to end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.",
"E2E_Encryption_Password_Explanation": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.<br/><br/>This is end-to-end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.",
"E2E_key_reset_email": "E2E Key Reset Notification",
"E2E_message_encrypted_placeholder": "This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.",
"E2E_password_request_text": "To access your encrypted private groups and direct messages, enter your encryption password. <br/>You need to enter this password to encode/decode your messages on every client you use, since the key is not stored on the server.",
Expand Down Expand Up @@ -1876,7 +1883,7 @@
"Enable_unlimited_apps": "Enable unlimited apps",
"Enabled": "Enabled",
"Encrypted": "Encrypted",
"Encrypted_channel_Description": "End to end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.",
"Encrypted_channel_Description": "End-to-end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.",
"Encrypted_key_title": "Click here to disable end-to-end encryption for this channel (requires e2ee-permission)",
"Encrypted_message": "Encrypted message",
"Encrypted_setting_changed_successfully": "Encrypted setting changed successfully",
Expand Down Expand Up @@ -4988,7 +4995,7 @@
"Teams_New_Description_Label": "Topic",
"Teams_New_Description_Placeholder": "What is this team about",
"Teams_New_Encrypted_Description_Disabled": "Only available for private team",
"Teams_New_Encrypted_Description_Enabled": "End to end encrypted team. Search will not work with encrypted Teams and notifications may not show the messages content.",
"Teams_New_Encrypted_Description_Enabled": "End-to-end encrypted team. Search will not work with encrypted Teams and notifications may not show the messages content.",
"Teams_New_Encrypted_Label": "Encrypted",
"Teams_New_Private_Description_Disabled": "When disabled, anyone can join the team",
"Teams_New_Private_Description_Enabled": "Only invited people can join",
Expand Down Expand Up @@ -5187,6 +5194,7 @@
"Transferred": "Transferred",
"Translate": "Translate",
"Translated": "Translated",
"Translate_to": "Translate to",
"Translations": "Translations",
"Travel_and_Places": "Travel & Places",
"Trigger_removed": "Trigger removed",
Expand Down
7 changes: 7 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,8 @@
"Author_Site": "Página do autor",
"Authorization_URL": "URL de autorização",
"Authorize": "Autorizar",
"Automatic_translation_not_available": "Tradução automática indisponível",
"Automatic_translation_not_available_info": "Esta sala tem criptografia E2E ativada, a tradução não pode funcionar com mensagens criptografadas",
"Auto_Load_Images": "Carregar imagens automaticamente",
"Auto_Selection": "Seleção automática",
"Auto_Translate": "Traduzir automaticamente",
Expand All @@ -631,9 +633,12 @@
"AutoTranslate_APIKey": "Key da API",
"AutoTranslate_Change_Language_Description": "Alterar o idioma de tradução automática não traduz mensagens anteriores.",
"AutoTranslate_DeepL": "DeepL",
"AutoTranslate_Disabled_for_room": "Tradução automática desabilitada para #{{roomName}}",
"AutoTranslate_Enabled": "Habilitar tradução automática",
"AutoTranslate_Enabled_Description": "Habilitar a tradução automática implicará em permitir que as pessoas com a permissão `auto-translate` tenham todas as suas mensagens automaticamente traduzidas para seu idioma. Taxas podem ser cobradas.",
"AutoTranslate_Enabled_for_room": "Tradução automática habilitada para #{{roomName}}",
"AutoTranslate_Google": "Google",
"AutoTranslate_language_set_to": "Linguagem para tradução automática definida como {{language}}",
"AutoTranslate_Microsoft": "Microsoft",
"AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key",
"AutoTranslate_ServiceProvider": "Provedor de serviço",
Expand Down Expand Up @@ -1515,6 +1520,7 @@
"Markdown_Marked_Tables": "Ativar tabelas marcadas",
"duplicated-account": "Conta duplicada",
"E2E Encryption": "Criptografia E2E",
"E2E_Encryption_enabled_for_room": "Criptografia E2E habilitada para #{{roomName}}",
"Markdown_Parser": "Parser de marcação",
"Markdown_SupportSchemesForLink": "Esquemas de links compatíveis com marcação",
"Markdown_SupportSchemesForLink_Description": "Lista de esquemas permitidos separados por vírgulas",
Expand Down Expand Up @@ -4336,6 +4342,7 @@
"Transferred": "Transferido",
"Translate": "Traduzir",
"Translated": "Traduzido",
"Translate_to": "Traduzir para",
"Translations": "Traduções",
"Travel_and_Places": "Viagem e Locais",
"Trigger_removed": "Gatilho removido",
Expand Down
10 changes: 10 additions & 0 deletions apps/meteor/server/models/raw/Rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,16 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel {
);
}

findE2ERoomById(roomId: IRoom['_id'], options: FindOptions<IRoom> = {}): Promise<IRoom | null> {
return this.findOne(
{
_id: roomId,
encrypted: true,
},
options,
);
}

findRoomsInsideTeams(autoJoin = false): FindCursor<IRoom> {
return this.find({
teamId: { $exists: true },
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/server/models/raw/Subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,14 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
return this.updateOne(query, update);
}

disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise<UpdateResult | Document> {
const query = {
rid: roomId,
};

return this.updateMany(query, { $unset: { autoTranslate: 1 } });
}

updateAutoTranslateLanguageById(_id: string, autoTranslateLanguage: string): Promise<UpdateResult> {
const query = {
_id,
Expand Down
Loading

0 comments on commit 5f32fa6

Please sign in to comment.