From 108ec7f1a72ee344b0db61d52f61b9d8c0fa3563 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 11 Jun 2024 15:05:39 -0300 Subject: [PATCH 01/26] fix(Omnichannel): nonstop sound on current chats using continuous notifications (#32572) --- .changeset/thin-suns-invent.md | 5 +++++ .../client/views/room/body/hooks/useUnreadMessages.ts | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/thin-suns-invent.md diff --git a/.changeset/thin-suns-invent.md b/.changeset/thin-suns-invent.md new file mode 100644 index 000000000000..945f44420797 --- /dev/null +++ b/.changeset/thin-suns-invent.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issues causing nonstop sound notification when taking a chat from the `Current Chats` view diff --git a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts index 5e74164b7882..343b9cb88a98 100644 --- a/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts +++ b/apps/meteor/client/views/room/body/hooks/useUnreadMessages.ts @@ -93,9 +93,11 @@ export const useHandleUnread = ( const debouncedReadMessageRead = useMemo( () => withDebouncing({ wait: 500 })(() => { - chat.readStateManager.attemptMarkAsRead(); + if (subscribed) { + chat.readStateManager.attemptMarkAsRead(); + } }), - [chat.readStateManager], + [chat.readStateManager, subscribed], ); useEffect( From c4e59b9f5853478f9fa7bbd16b8c1b8b3487f2c4 Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Tue, 11 Jun 2024 22:18:53 +0200 Subject: [PATCH 02/26] test: Add Omnichannel Business Hours E2E tests (#32303) --- .../omnichannel-business-hours.spec.ts | 139 ++++++++++++++++++ apps/meteor/tests/e2e/page-objects/index.ts | 1 + .../omnichannel-business-hours.ts | 67 +++++++++ .../e2e/utils/omnichannel/businessHours.ts | 47 ++++++ 4 files changed, 254 insertions(+) create mode 100644 apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts create mode 100644 apps/meteor/tests/e2e/utils/omnichannel/businessHours.ts diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts new file mode 100644 index 000000000000..2c5dc162e509 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts @@ -0,0 +1,139 @@ +import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; + +import { IS_EE } from '../config/constants'; +import { Users } from '../fixtures/userStates'; +import { OmnichannelBusinessHours } from '../page-objects'; +import { createAgent } from '../utils/omnichannel/agents'; +import { createBusinessHour } from '../utils/omnichannel/businessHours'; +import { createDepartment } from '../utils/omnichannel/departments'; +import { test, expect } from '../utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe('OC - Business Hours', () => { + test.skip(!IS_EE, 'OC - Manage Business Hours > Enterprise Edition Only'); + + let poOmnichannelBusinessHours: OmnichannelBusinessHours; + let department: Awaited>; + let department2: Awaited>; + let agent: Awaited>; + + test.beforeAll(async ({ api }) => { + department = await createDepartment(api); + department2 = await createDepartment(api); + agent = await createAgent(api, 'user2'); + await api.post('/settings/Livechat_enable_business_hours', { value: true }).then((res) => expect(res.status()).toBe(200)); + await api.post('/settings/Livechat_business_hour_type', { value: 'Multiple' }).then((res) => expect(res.status()).toBe(200)); + }); + + test.afterAll(async ({ api }) => { + await department.delete(); + await department2.delete(); + await agent.delete(); + await api.post('/settings/Livechat_enable_business_hours', { value: false }).then((res) => expect(res.status()).toBe(200)); + await api.post('/settings/Livechat_business_hour_type', { value: 'Single' }).then((res) => expect(res.status()).toBe(200)); + }); + + test.beforeEach(async ({ page }: { page: Page }) => { + poOmnichannelBusinessHours = new OmnichannelBusinessHours(page); + }); + + test('OC - Manage Business Hours - Create Business Hours', async ({ page }) => { + const BHName = faker.string.uuid(); + + await page.goto('/omnichannel'); + await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + + await test.step('expect correct form default state', async () => { + await poOmnichannelBusinessHours.btnCreateBusinessHour.click(); + await poOmnichannelBusinessHours.btnBack.click(); + await expect(poOmnichannelBusinessHours.inputSearch).toBeVisible(); + }); + + await test.step('expect to create a new business hours', async () => { + await poOmnichannelBusinessHours.btnCreateBusinessHour.click(); + await poOmnichannelBusinessHours.inputName.fill(BHName); + await poOmnichannelBusinessHours.selectDepartment(department.data); + await poOmnichannelBusinessHours.btnSave.click(); + + await test.step('expect business hours to have been created', async () => { + await poOmnichannelBusinessHours.search(BHName); + await expect(poOmnichannelBusinessHours.findRowByName(BHName)).toBeVisible(); + }); + }); + + await test.step('expect to be able to delete business hours', async () => { + await test.step('expect to be able to cancel delete', async () => { + await poOmnichannelBusinessHours.btnDeleteByName(BHName).click(); + await expect(poOmnichannelBusinessHours.confirmDeleteModal).toBeVisible(); + await poOmnichannelBusinessHours.btnCancelDeleteModal.click(); + await expect(poOmnichannelBusinessHours.confirmDeleteModal).not.toBeVisible(); + }); + + await test.step('expect to confirm delete', async () => { + await poOmnichannelBusinessHours.btnDeleteByName(BHName).click(); + await expect(poOmnichannelBusinessHours.confirmDeleteModal).toBeVisible(); + await poOmnichannelBusinessHours.btnConfirmDeleteModal.click(); + await expect(poOmnichannelBusinessHours.confirmDeleteModal).not.toBeVisible(); + }); + }); + + await test.step('expect business hours to have been deleted', async () => { + await poOmnichannelBusinessHours.search(BHName); + await expect(poOmnichannelBusinessHours.findRowByName(BHName)).not.toBeVisible(); + }); + }); + + test('OC - Business hours - Edit BH departments', async ({ api, page }) => { + const BHName = faker.string.uuid(); + + await test.step('expect to create new businessHours', async () => { + const createBH = await createBusinessHour(api, { + id: '33', + name: BHName, + departments: [department.data._id], + }); + + expect(createBH.status()).toBe(200); + }); + + await page.goto('/omnichannel'); + await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + + await test.step('expect to add business hours departments', async () => { + await poOmnichannelBusinessHours.search(BHName); + await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await poOmnichannelBusinessHours.selectDepartment({ name: department2.data.name, _id: department2.data._id }); + await poOmnichannelBusinessHours.btnSave.click(); + }); + + await test.step('expect department to be in the chosen departments list', async () => { + await poOmnichannelBusinessHours.search(BHName); + await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await expect(page.getByRole('option', { name: department2.data.name })).toBeVisible(); + await poOmnichannelBusinessHours.btnBack.click(); + }); + + await test.step('expect to remove business hours departments', async () => { + await poOmnichannelBusinessHours.search(BHName); + await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await poOmnichannelBusinessHours.selectDepartment({ name: department2.data.name, _id: department2.data._id }); + await poOmnichannelBusinessHours.btnSave.click(); + }); + + await test.step('expect department to not be in the chosen departments list', async () => { + await poOmnichannelBusinessHours.search(BHName); + await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await expect(page.getByRole('option', { name: department2.data.name })).toBeHidden(); + await poOmnichannelBusinessHours.btnBack.click(); + }); + + await test.step('expect delete business hours', async () => { + await poOmnichannelBusinessHours.btnDeleteByName(BHName).click(); + await expect(poOmnichannelBusinessHours.confirmDeleteModal).toBeVisible(); + await poOmnichannelBusinessHours.btnConfirmDeleteModal.click(); + await expect(poOmnichannelBusinessHours.confirmDeleteModal).not.toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index 5d8284fb420b..64126e4daed4 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -16,5 +16,6 @@ export * from './omnichannel-units'; export * from './home-omnichannel'; export * from './omnichannel-monitors'; export * from './omnichannel-settings'; +export * from './omnichannel-business-hours'; export * from './omnichannel-tags'; export * from './utils'; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts new file mode 100644 index 000000000000..19e93cefb307 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts @@ -0,0 +1,67 @@ +import type { Locator } from '@playwright/test'; + +import { OmnichannelAdministration } from './omnichannel-administration'; + +export class OmnichannelBusinessHours extends OmnichannelAdministration { + get btnCreateBusinessHour(): Locator { + return this.page.locator('header').locator('role=button[name="New"]'); + } + + get btnSave(): Locator { + return this.page.locator('role=button[name="Save"]'); + } + + get btnCancel(): Locator { + return this.page.locator('role=button[name="Cancel"]'); + } + + get btnBack(): Locator { + return this.page.locator('role=button[name="Back"]'); + } + + get inputSearch(): Locator { + return this.page.locator('[placeholder="Search"]'); + } + + get inputName(): Locator { + return this.page.locator('[name="name"]'); + } + + get inputDepartments(): Locator { + return this.page.locator('input[placeholder="Select an option"]'); + } + + findRowByName(name: string): Locator { + return this.page.locator(`tr:has-text("${name}")`); + } + + btnDeleteByName(name: string): Locator { + return this.page.locator(`tr:has-text("${name}") button[title="Remove"]`); + } + + get confirmDeleteModal(): Locator { + return this.page.locator('dialog:has(h2:has-text("Are you sure?"))'); + } + + get btnCancelDeleteModal(): Locator { + return this.confirmDeleteModal.locator('role=button[name="Cancel"]'); + } + + get btnConfirmDeleteModal(): Locator { + return this.confirmDeleteModal.locator('role=button[name="Delete"]'); + } + + private selectOption(name: string): Locator { + return this.page.locator(`[role=option][value="${name}"]`); + } + + async selectDepartment({ name, _id }: { name: string; _id: string }) { + await this.inputDepartments.click(); + await this.inputDepartments.fill(name); + await this.selectOption(_id).click(); + } + + async search(text: string) { + await this.inputSearch.fill(text); + } +} diff --git a/apps/meteor/tests/e2e/utils/omnichannel/businessHours.ts b/apps/meteor/tests/e2e/utils/omnichannel/businessHours.ts new file mode 100644 index 000000000000..7d4aced1e36c --- /dev/null +++ b/apps/meteor/tests/e2e/utils/omnichannel/businessHours.ts @@ -0,0 +1,47 @@ +import type { BaseTest } from '../test'; + +type CreateBusinessHoursParams = { + id?: string | null; + name?: string; + description?: string; + departments?: { departmentId: string }[]; +}; + +export const createBusinessHour = async (api: BaseTest['api'], { id = null, name, departments = [] }: CreateBusinessHoursParams = {}) => { + const departmentIds = departments.join(','); + + const response = await api.post('/method.call/livechat:saveBusinessHour', { + message: JSON.stringify({ + msg: 'method', + id: id || '33', + method: 'livechat:saveBusinessHour', + params: [ + { + name, + timezoneName: 'America/Sao_Paulo', + daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], + daysTime: [ + { day: 'Monday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true }, + { day: 'Tuesday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true }, + { day: 'Wednesday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true }, + { day: 'Thursday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true }, + { day: 'Friday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true }, + ], + departmentsToApplyBusinessHour: departmentIds, + active: true, + type: 'custom', + timezone: 'America/Sao_Paulo', + workHours: [ + { day: 'Monday', start: '08:00', finish: '18:00', open: true }, + { day: 'Tuesday', start: '08:00', finish: '18:00', open: true }, + { day: 'Wednesday', start: '08:00', finish: '18:00', open: true }, + { day: 'Thursday', start: '08:00', finish: '18:00', open: true }, + { day: 'Friday', start: '08:00', finish: '18:00', open: true }, + ], + }, + ], + }), + }); + + return response; +}; From 972ba42f43c2591df64ad9e40f3bd11d993c446a Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 12 Jun 2024 08:53:48 -0300 Subject: [PATCH 03/26] fix: Run QueryClient.clear() on logout (#32587) --- .changeset/fuzzy-readers-bake.md | 5 +++ .../providers/UserProvider/UserProvider.tsx | 7 +++-- ...mnichannel-enterprise-menus-logout.spec.ts | 31 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 .changeset/fuzzy-readers-bake.md create mode 100644 apps/meteor/tests/e2e/omnichannel/omnichannel-enterprise-menus-logout.spec.ts diff --git a/.changeset/fuzzy-readers-bake.md b/.changeset/fuzzy-readers-bake.md new file mode 100644 index 000000000000..a487096a312e --- /dev/null +++ b/.changeset/fuzzy-readers-bake.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issues with loading license modules when loading the page while logged out diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 27c540928e86..27bba21eae95 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -4,7 +4,7 @@ import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import { UserContext, useEndpoint } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import type { ContextType, ReactElement, ReactNode } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { Subscriptions, ChatRoom } from '../../../app/models/client'; import { getUserPreference } from '../../../app/utils/client'; @@ -43,6 +43,7 @@ type UserProviderProps = { const UserProvider = ({ children }: UserProviderProps): ReactElement => { const userId = useReactiveValue(getUserId); + const previousUserId = useRef(userId); const user = useReactiveValue(getUser); const [userLanguage, setUserLanguage] = useLocalStorage('userLanguage', ''); const [preferedLanguage, setPreferedLanguage] = useLocalStorage('preferedLanguage', ''); @@ -94,9 +95,11 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { }, [preferedLanguage, setPreferedLanguage, setUserLanguage, user?.language, userLanguage, userId, setUserPreferences]); useEffect(() => { - if (!userId) { + if (previousUserId.current && previousUserId.current !== userId) { queryClient.clear(); } + + previousUserId.current = userId; }, [userId]); return ; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-enterprise-menus-logout.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-enterprise-menus-logout.spec.ts new file mode 100644 index 000000000000..057dd8bd73dc --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-enterprise-menus-logout.spec.ts @@ -0,0 +1,31 @@ +import type { Page } from '@playwright/test'; + +import { ADMIN_CREDENTIALS, IS_EE } from '../config/constants'; +import injectInitialData from '../fixtures/inject-initial-data'; +import { test, expect } from '../utils/test'; + +test.describe('OC - Enterprise Menu Items After Relogin', () => { + // Create page object and redirect to home + test.beforeEach(async ({ page }: { page: Page }) => { + await page.goto('/omnichannel/current'); + + await page.locator('role=textbox[name=/username/i]').waitFor({ state: 'visible' }); + await page.locator('role=textbox[name=/username/i]').fill(ADMIN_CREDENTIALS.email); + await page.locator('[name=password]').fill(ADMIN_CREDENTIALS.password); + await page.locator('role=button[name="Login"]').click(); + + await page.locator('.main-content').waitFor(); + }); + + // Delete all data + test.afterAll(async () => { + await injectInitialData(); + }); + + test('OC - Enterprise Menu Items - Logout & Login', async ({ page }) => { + test.skip(!IS_EE); + await test.step('expect EE menu items to be visible', async () => { + await expect(page.locator('a[href="/omnichannel/tags"]')).toBeVisible(); + }); + }); +}); From 16b67aa0ff4f0d88be9208098e2a66cfe87b537c Mon Sep 17 00:00:00 2001 From: Gustavo Reis Bauer Date: Wed, 12 Jun 2024 11:10:26 -0300 Subject: [PATCH 04/26] feat: Limit of members that can be added using the autojoin feature in a team's channel to the value of the API_Users_Limit setting (#31859) Co-authored-by: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> --- .changeset/clean-moose-cover.md | 6 ++ .../channels/TeamsChannelItem.tsx | 5 +- .../channels/TeamsChannelItemMenu.tsx | 4 +- .../contextualBar/channels/TeamsChannels.tsx | 6 +- .../channels/TeamsChannelsWithData.tsx | 1 + .../channels/hooks/useToggleAutoJoin.tsx | 31 +++++++- apps/meteor/server/services/team/service.ts | 17 +++- apps/meteor/tests/data/teams.helper.ts | 13 ++- apps/meteor/tests/end-to-end/api/25-teams.js | 79 ++++++++++++++++++- packages/i18n/src/locales/en.i18n.json | 2 + 10 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 .changeset/clean-moose-cover.md diff --git a/.changeset/clean-moose-cover.md b/.changeset/clean-moose-cover.md new file mode 100644 index 000000000000..39e6204ce9b4 --- /dev/null +++ b/.changeset/clean-moose-cover.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Introduced the use of the `API_User_Limit` setting to limit amount of members to simultaneously auto-join a room in a team diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx index 981c65630d0a..01f92d38488c 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx @@ -22,11 +22,12 @@ import TeamsChannelItemMenu from './TeamsChannelItemMenu'; type TeamsChannelItemProps = { room: IRoom; + mainRoom: IRoom; onClickView: (room: IRoom) => void; reload: () => void; }; -const TeamsChannelItem = ({ room, onClickView, reload }: TeamsChannelItemProps) => { +const TeamsChannelItem = ({ room, mainRoom, onClickView, reload }: TeamsChannelItemProps) => { const t = useTranslation(); const rid = room._id; const type = room.t; @@ -68,7 +69,7 @@ const TeamsChannelItem = ({ room, onClickView, reload }: TeamsChannelItemProps) {(canRemoveTeamChannel || canEditTeamChannel || canDeleteTeamChannel) && ( - {showButton ? : } + {showButton ? : } )} diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx index 54ee216eaaa0..97b1bdceb670 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx @@ -9,12 +9,12 @@ import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom'; import { useRemoveRoomFromTeam } from './hooks/useRemoveRoomFromTeam'; import { useToggleAutoJoin } from './hooks/useToggleAutoJoin'; -const TeamsChannelItemMenu = ({ room, reload }: { room: IRoom; reload?: () => void }) => { +const TeamsChannelItemMenu = ({ room, mainRoom, reload }: { room: IRoom; mainRoom: IRoom; reload?: () => void }) => { const t = useTranslation(); const { handleRemoveRoom, canRemoveTeamChannel } = useRemoveRoomFromTeam(room, { reload }); const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload }); - const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload }); + const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload, mainRoom }); const toggleAutoJoin = { id: 'toggleAutoJoin', diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx index 6d0bfddd8610..d82dd19b1af8 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx @@ -23,6 +23,7 @@ import TeamsChannelItem from './TeamsChannelItem'; type TeamsChannelsProps = { loading: boolean; channels: IRoom[]; + mainRoom: IRoom; text: string; type: 'all' | 'autoJoin'; setType: Dispatch>; @@ -39,6 +40,7 @@ type TeamsChannelsProps = { const TeamsChannels = ({ loading, channels = [], + mainRoom, text, type, setText, @@ -123,7 +125,9 @@ const TeamsChannels = ({ data={channels} // eslint-disable-next-line react/no-multi-comp components={{ Scroller: VirtuosoScrollbars, Footer: () => }} - itemContent={(index, data) => } + itemContent={(index, data) => ( + + )} /> diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx index 965414400ee5..a886479b535b 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx @@ -54,6 +54,7 @@ const TeamsChannelsWithData = () => { return ( void }) => { +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; + +export const useToggleAutoJoin = (room: IRoom, { reload, mainRoom }: { reload?: () => void; mainRoom: IRoom }) => { const dispatchToastMessage = useToastMessageDispatch(); const updateRoomEndpoint = useEndpoint('POST', '/v1/teams.updateRoom'); const canEditTeamChannel = usePermission('edit-team-channel', room._id); + const maxNumberOfAutoJoinMembers = useSetting('API_User_Limit'); const handleToggleAutoJoin = async () => { + // Sanity check, the setting has a default value, therefore it should always be defined + if (!maxNumberOfAutoJoinMembers) { + return; + } + try { - await updateRoomEndpoint({ + const { room: updatedRoom } = await updateRoomEndpoint({ roomId: room._id, isDefault: !room.teamDefault, }); - dispatchToastMessage({ type: 'success', message: room.teamDefault ? 'channel set as non autojoin' : 'channel set as autojoin' }); + if (updatedRoom.teamDefault) { + // If the number of members in the mainRoom (the team) is greater than the limit, show an info message + // informing that not all members will be auto-joined to the channel + const messageType = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'info' : 'success'; + const message = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'Team_Auto-join_exceeded_user_limit' : 'Team_Auto-join_updated'; + + dispatchToastMessage({ + type: messageType, + message: t(message, { + channelName: roomCoordinator.getRoomName(room.t, room), + numberOfMembers: updatedRoom.usersCount, + limit: maxNumberOfAutoJoinMembers, + }), + }); + } reload?.(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 71d403fd84b5..f81b21d7fa01 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -32,6 +32,7 @@ import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; import { checkUsernameAvailability } from '../../../app/lib/server/functions/checkUsernameAvailability'; import { getSubscribedRoomsForUserWithDetails } from '../../../app/lib/server/functions/getRoomsWithSingleOwner'; import { removeUserFromRoom } from '../../../app/lib/server/functions/removeUserFromRoom'; +import { settings } from '../../../app/settings/server'; export class TeamService extends ServiceClassInternal implements ITeamService { protected name = 'team'; @@ -472,11 +473,21 @@ export class TeamService extends ServiceClassInternal implements ITeamService { room.teamDefault = isDefault; await Rooms.setTeamDefaultById(rid, isDefault); - if (room.teamDefault) { - const teamMembers = await this.members(uid, room.teamId, true, undefined, undefined); + if (isDefault) { + const maxNumberOfAutoJoinMembers = settings.get('API_User_Limit'); + const teamMembers = await this.members( + uid, + room.teamId, + true, + { offset: 0, count: maxNumberOfAutoJoinMembers }, + // We should not get the owner of the room, since he is already a member + { _id: { $ne: room.u._id } }, + ); for await (const m of teamMembers.records) { - await addUserToRoom(room._id, m.user, user); + if (await addUserToRoom(room._id, m.user, user)) { + room.usersCount++; + } } } diff --git a/apps/meteor/tests/data/teams.helper.ts b/apps/meteor/tests/data/teams.helper.ts index 308fc60f445e..62da06eea71a 100644 --- a/apps/meteor/tests/data/teams.helper.ts +++ b/apps/meteor/tests/data/teams.helper.ts @@ -1,5 +1,6 @@ import { ITeam, TEAM_TYPE } from "@rocket.chat/core-typings" import { api, request } from "./api-data" +import { IUser } from "@rocket.chat/apps-engine/definition/users"; export const createTeam = async (credentials: Record, teamName: string, type: TEAM_TYPE): Promise => { const response = await request.post(api('teams.create')).set(credentials).send({ @@ -14,4 +15,14 @@ export const deleteTeam = async (credentials: Record, teamName: str await request.post(api('teams.delete')).set(credentials).send({ teamName, }); -}; \ No newline at end of file +}; + +export const addMembers = async (credentials: Record, teamName: string, members: IUser['id'][]): Promise => { + await request + .post(api('teams.addMembers')) + .set(credentials) + .send({ + teamName: teamName, + members: members.map((userId) => ({ userId, roles: ['member'] })) + }); +}; diff --git a/apps/meteor/tests/end-to-end/api/25-teams.js b/apps/meteor/tests/end-to-end/api/25-teams.js index 34383e3aaa69..49a1d663aef2 100644 --- a/apps/meteor/tests/end-to-end/api/25-teams.js +++ b/apps/meteor/tests/end-to-end/api/25-teams.js @@ -1,11 +1,11 @@ import { TEAM_TYPE } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it, after } from 'mocha'; +import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; -import { updatePermission } from '../../data/permissions.helper'; +import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; -import { createTeam, deleteTeam } from '../../data/teams.helper'; +import { addMembers, createTeam, deleteTeam } from '../../data/teams.helper'; import { adminUsername, password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; @@ -1594,6 +1594,79 @@ describe('[Teams]', () => { .end(done); }); }); + + describe('team auto-join', () => { + let testTeam; + let createdRoom; + let testUser1; + let testUser2; + + before(async () => { + const [testUserResult, testUser1Result] = await Promise.all([createUser(), createUser()]); + testUser1 = testUserResult; + testUser2 = testUser1Result; + }); + + beforeEach(async () => { + const createTeamPromise = createTeam(credentials, `test-team-name${Date.now()}`, 0); + const createRoomPromise = createRoom({ name: `test-room-name${Date.now()}`, type: 'c' }); + const [testTeamCreationResult, testRoomCreationResult] = await Promise.all([createTeamPromise, createRoomPromise]); + + testTeam = testTeamCreationResult; + createdRoom = testRoomCreationResult; + + await request + .post(api('teams.addRooms')) + .set(credentials) + .expect(200) + .send({ + rooms: [createdRoom.body.channel._id], + teamName: testTeam.name, + }); + }); + + afterEach(() => + Promise.all([deleteTeam(credentials, testTeam.name), deleteRoom({ roomId: createdRoom.body.channel._id, type: 'c' })]), + ); + + after(() => Promise.all([updateSetting('API_User_Limit', 500), deleteUser(testUser1), deleteUser(testUser2)])); + + it('should add members when the members count is less than or equal to the API_User_Limit setting', async () => { + await updateSetting('API_User_Limit', 2); + + await addMembers(credentials, testTeam.name, [testUser1._id, testUser2._id]); + await request + .post(api('teams.updateRoom')) + .set(credentials) + .send({ + roomId: createdRoom.body.channel._id, + isDefault: true, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('room.usersCount').and.to.be.equal(3); + }); + }); + + it('should not add all members when we update a team channel to be auto-join and the members count is greater than the API_User_Limit setting', async () => { + await updateSetting('API_User_Limit', 1); + + await addMembers(credentials, testTeam.name, [testUser1._id, testUser2._id]); + await request + .post(api('teams.updateRoom')) + .set(credentials) + .send({ + roomId: createdRoom.body.channel._id, + isDefault: true, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('room.usersCount').and.to.be.equal(2); + }); + }); + }); }); describe('/teams.removeRoom', () => { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 9c90020d94d0..3a50e7d39df6 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5127,6 +5127,8 @@ "Team_Add_existing_channels": "Add Existing Channels", "Team_Add_existing": "Add Existing", "Team_Auto-join": "Auto-join", + "Team_Auto-join_exceeded_user_limit": "Auto-join has a limit of {{limit}} members, #{{channelName}} now has {{numberOfMembers}} members", + "Team_Auto-join_updated": "#{{channelName}} now has {{numberOfMembers}} members", "Team_Channels": "Team Channels", "Team_Delete_Channel_modal_content_danger": "This can’t be undone.", "Team_Delete_Channel_modal_content": "Would you like to delete this Channel?", From 99de6d2ec4e98340971e39493b78e71b355fc7b1 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 12 Jun 2024 12:10:07 -0300 Subject: [PATCH 05/26] chore: disable db watchers on local envs (#32589) --- packages/core-services/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index ccc936ad6ed5..ce51f4695aec 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -138,7 +138,8 @@ export { IUserService, }; -export const dbWatchersDisabled = ['yes', 'true'].includes(String(process.env.DISABLE_DB_WATCHERS).toLowerCase()); +export const dbWatchersDisabled = + ['yes', 'true'].includes(String(process.env.DISABLE_DB_WATCHERS).toLowerCase()) || process.env.NODE_ENV !== 'production'; // TODO think in a way to not have to pass the service name to proxify here as well export const Authorization = proxifyWithWait('authorization'); From bd9eb6ba84229e6dcf6429e36d939caf3f6d9b17 Mon Sep 17 00:00:00 2001 From: Gustavo Reis Bauer Date: Wed, 12 Jun 2024 14:14:27 -0300 Subject: [PATCH 06/26] chore: Add a way to remove an invalid token if sendFCM returns 404 (#32440) --- apps/meteor/app/push/server/fcm.ts | 38 ++++++++++++++++++------------ 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/meteor/app/push/server/fcm.ts b/apps/meteor/app/push/server/fcm.ts index 819e26e4f003..87ced6e130df 100644 --- a/apps/meteor/app/push/server/fcm.ts +++ b/apps/meteor/app/push/server/fcm.ts @@ -55,15 +55,17 @@ type FCMError = { }; /** - * Set at least a 10 second timeout on send requests before retrying. - * Most of FCM's internal Remote Procedure Calls use a 10 second timeout. + * Send a push notification using Firebase Cloud Messaging (FCM). + * implements the Firebase Cloud Messaging HTTP v1 API, and all of its retry logic, + * see: https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode * * Errors: - * - For 400, 401, 403, 404 errors: abort, and do not retry. + * - For 400, 401, 403 errors: abort, and do not retry. + * - For 404 errors: remove the token from the database. * - For 429 errors: retry after waiting for the duration set in the retry-after header. If no retry-after header is set, default to 60 seconds. * - For 500 errors: retry with exponential backoff. */ -async function fetchWithRetry(url: string, options: RequestInit, retries = 0): Promise { +async function fetchWithRetry(url: string, _removeToken: () => void, options: RequestInit, retries = 0): Promise { const MAX_RETRIES = 5; const response = await fetch(url, options); @@ -79,15 +81,20 @@ async function fetchWithRetry(url: string, options: RequestInit, retries = 0): P const retryAfter = response.headers.get('retry-after'); const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; + if (response.status === 404) { + _removeToken(); + return response; + } + if (response.status === 429) { await new Promise((resolve) => setTimeout(resolve, retryAfterSeconds * 1000)); - return fetchWithRetry(url, options, retries + 1); + return fetchWithRetry(url, _removeToken, options, retries + 1); } if (response.status >= 500 && response.status < 600) { const backoff = Math.pow(2, retries) * 10000; await new Promise((resolve) => setTimeout(resolve, backoff)); - return fetchWithRetry(url, options, retries + 1); + return fetchWithRetry(url, _removeToken, options, retries + 1); } const error: FCMError = await response.json(); @@ -145,12 +152,7 @@ function getFCMMessagesFromPushData(userTokens: string[], notification: PendingP return userTokens.map((token) => ({ message: { ...message, token } })); } -export const sendFCM = function ({ userTokens, notification, _replaceToken, _removeToken, options }: NativeNotificationParameters): void { - // We don't use these parameters, but we need to keep them to keep the function signature - // TODO: Remove them when we remove the old sendGCM function - _replaceToken; - _removeToken; - +export const sendFCM = function ({ userTokens, notification, _removeToken, options }: NativeNotificationParameters): void { const tokens = typeof userTokens === 'string' ? [userTokens] : userTokens; if (!tokens.length) { logger.log('sendFCM no push tokens found'); @@ -173,9 +175,15 @@ export const sendFCM = function ({ userTokens, notification, _replaceToken, _rem const url = `https://fcm.googleapis.com/v1/projects/${options.gcm.projectNumber}/messages:send`; - for (const message of messages) { - logger.debug('sendFCM message', message); - const response = fetchWithRetry(url, { method: 'POST', headers, body: JSON.stringify(message) }); + for (const fcmRequest of messages) { + logger.debug('sendFCM message', fcmRequest); + + const removeToken = () => { + const { token } = fcmRequest.message; + token && _removeToken({ gcm: token }); + }; + + const response = fetchWithRetry(url, removeToken, { method: 'POST', headers, body: JSON.stringify(fcmRequest) }); response.catch((err) => { logger.error('sendFCM error', err); From 65d16883dc606df032e5fc013644aac42a76e8d3 Mon Sep 17 00:00:00 2001 From: Gustavo Reis Bauer Date: Wed, 12 Jun 2024 17:55:12 -0300 Subject: [PATCH 07/26] test: Rewrote the rooms.helper file in typescript (#32556) --- apps/meteor/tests/data/rooms.helper.js | 118 ------------- apps/meteor/tests/data/rooms.helper.ts | 167 ++++++++++++++++++ apps/meteor/tests/data/uploads.helper.ts | 43 +++-- apps/meteor/tests/end-to-end/api/05-chat.js | 2 +- .../api/07-incoming-integrations.js | 2 +- .../tests/end-to-end/api/16-commands.js | 2 +- .../end-to-end/apps/05-video-conferences.ts | 4 +- 7 files changed, 197 insertions(+), 141 deletions(-) delete mode 100644 apps/meteor/tests/data/rooms.helper.js create mode 100644 apps/meteor/tests/data/rooms.helper.ts diff --git a/apps/meteor/tests/data/rooms.helper.js b/apps/meteor/tests/data/rooms.helper.js deleted file mode 100644 index 6e74509deab4..000000000000 --- a/apps/meteor/tests/data/rooms.helper.js +++ /dev/null @@ -1,118 +0,0 @@ -import { resolve } from 'path'; -import { api, credentials, request } from './api-data'; - -export const createRoom = ({ - name, - type, - username, - token, - agentId, - members, - credentials: customCredentials, - extraData, - voipCallDirection = 'inbound', -}) => { - if (!type) { - throw new Error('"type" is required in "createRoom.ts" test helper'); - } - if (type === 'v') { - /* Special handling for voip type of rooms. - * The endpoints below do not have a way to create - * a voip room. Hence creation of a voip room - * is handled separately here. - */ - return request - .get(api(`voip/room?token=${token}&agentId=${agentId}&direction=${voipCallDirection}`)) - .set(customCredentials || credentials) - .send(); - } - if (type === 'd' && !username) { - throw new Error('To be able to create DM Room, you must provide the username'); - } - const endpoints = { - c: 'channels.create', - p: 'groups.create', - d: 'im.create', - }; - const params = type === 'd' ? { username } : { name }; - - return request - .post(api(endpoints[type])) - .set(customCredentials || credentials) - .send({ - ...params, - ...(members && { members }), - ...(extraData && { extraData }), - }); -}; - -export const asyncCreateRoom = ({ name, type, username, members = [] }) => - new Promise((resolve) => { - createRoom({ name, type, username, members }).end(resolve); - }); - -function actionRoom({ action, type, roomId, overrideCredentials = credentials, extraData = {} }) { - if (!type) { - throw new Error(`"type" is required in "${action}Room" test helper`); - } - if (!roomId) { - throw new Error(`"roomId" is required in "${action}Room" test helper`); - } - const endpoints = { - c: 'channels', - p: 'groups', - d: 'im', - }; - return new Promise((resolve) => { - request - .post(api(`${endpoints[type]}.${action}`)) - .set(overrideCredentials) - .send({ - roomId, - ...extraData, - }) - .end(resolve); - }); -} - -export const deleteRoom = ({ type, roomId }) => actionRoom({ action: 'delete', type, roomId, overrideCredentials: credentials }); - -export const closeRoom = ({ type, roomId }) => actionRoom({ action: 'close', type, roomId }); - -export const joinChannel = ({ overrideCredentials = credentials, roomId }) => - request.post(api('channels.join')).set(overrideCredentials).send({ - roomId, - }); - -export const inviteToChannel = ({ overrideCredentials = credentials, roomId, userId }) => - request.post(api('channels.invite')).set(credentials).send({ - userId, - roomId, - }); - -export const addRoomOwner = ({ type, roomId, userId }) => actionRoom({ action: 'addOwner', type, roomId, extraData: { userId } }); - -export const removeRoomOwner = ({ type, roomId, userId }) => actionRoom({ action: 'removeOwner', type, roomId, extraData: { userId } }); - -export const getChannelRoles = async ({ roomId, overrideCredentials = credentials }) => - ( - await request.get(api('channels.roles')).set(overrideCredentials).query({ - roomId, - }) - ).body.roles; - -export const setRoomConfig = ({ roomId, favorite, isDefault }) => { - return request - .post(api('rooms.saveRoomSettings')) - .set(credentials) - .send({ - rid: roomId, - default: isDefault, - favorite: favorite - ? { - defaultValue: true, - favorite: false, - } - : undefined, - }); -}; diff --git a/apps/meteor/tests/data/rooms.helper.ts b/apps/meteor/tests/data/rooms.helper.ts new file mode 100644 index 000000000000..384ac5614e41 --- /dev/null +++ b/apps/meteor/tests/data/rooms.helper.ts @@ -0,0 +1,167 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { api, credentials, request } from './api-data'; +import type { IUser } from '@rocket.chat/core-typings'; + +type Credentials = { 'X-Auth-Token'?: string; 'X-User-Id'?: string }; + +type CreateRoomParams = { + name?: IRoom['name']; + type: IRoom['t']; + username?: string; + token?: string; + agentId?: string; + members?: string[]; + credentials?: Credentials; + extraData?: Record; + voipCallDirection?: 'inbound' | 'outbound'; +}; + +export const createRoom = ({ + name, + type, + username, + token, + agentId, + members, + credentials: customCredentials, + extraData, + voipCallDirection = 'inbound', +}: CreateRoomParams) => { + if (!type) { + throw new Error('"type" is required in "createRoom.ts" test helper'); + } + + if (type === 'v') { + /* Special handling for voip type of rooms. + * The endpoints below do not have a way to create + * a voip room. Hence creation of a voip room + * is handled separately here. + */ + return request + .get(api(`voip/room?token=${token}&agentId=${agentId}&direction=${voipCallDirection}`)) + .set(customCredentials || credentials) + .send(); + } + + if (type === 'd' && !username) { + throw new Error('To be able to create DM Room, you must provide the username'); + } + + const endpoints = { + c: 'channels.create', + p: 'groups.create', + d: 'im.create', + }; + const params = type === 'd' ? { username } : { name }; + + // Safe assertion because we already checked the type is not 'v' + // which is the only case where type is not in the endpoints object + const roomType = endpoints[type as keyof typeof endpoints]; + + return request + .post(api(roomType)) + .set(customCredentials || credentials) + .send({ + ...params, + ...(members && { members }), + ...(extraData && { extraData }), + }); +}; + +export const asyncCreateRoom = ({ name, type, username, members = [] }: Pick) => + new Promise((resolve) => { + createRoom({ name, type, username, members }).end(resolve); + }); + +type ActionType = 'delete' | 'close' | 'addOwner' | 'removeOwner'; +type ActionRoomParams = { + action: ActionType; + type: Exclude; + roomId: IRoom['_id']; + overrideCredentials?: Credentials; + extraData?: Record; +}; + +function actionRoom({ action, type, roomId, overrideCredentials = credentials, extraData = {} }: ActionRoomParams) { + if (!type) { + throw new Error(`"type" is required in "${action}Room" test helper`); + } + if (!roomId) { + throw new Error(`"roomId" is required in "${action}Room" test helper`); + } + const endpoints = { + c: 'channels', + p: 'groups', + d: 'im', + }; + return new Promise((resolve) => { + request + .post(api(`${endpoints[type]}.${action}`)) + .set(overrideCredentials) + .send({ + roomId, + ...extraData, + }) + .end(resolve); + }); +} + +export const deleteRoom = ({ type, roomId }: { type: ActionRoomParams['type']; roomId: IRoom['_id'] }) => + actionRoom({ action: 'delete', type, roomId, overrideCredentials: credentials }); + +export const closeRoom = ({ type, roomId }: { type: ActionRoomParams['type']; roomId: IRoom['_id'] }) => + actionRoom({ action: 'close', type, roomId }); + +export const joinChannel = ({ overrideCredentials = credentials, roomId }: { overrideCredentials: Credentials; roomId: IRoom['_id'] }) => + request.post(api('channels.join')).set(overrideCredentials).send({ + roomId, + }); + +export const inviteToChannel = ({ + overrideCredentials = credentials, + roomId, + userId, +}: { + overrideCredentials: Credentials; + roomId: IRoom['_id']; + userId: IUser['_id']; +}) => + request.post(api('channels.invite')).set(overrideCredentials).send({ + userId, + roomId, + }); + +export const addRoomOwner = ({ type, roomId, userId }: { type: ActionRoomParams['type']; roomId: IRoom['_id']; userId: IUser['_id'] }) => + actionRoom({ action: 'addOwner', type, roomId, extraData: { userId } }); + +export const removeRoomOwner = ({ type, roomId, userId }: { type: ActionRoomParams['type']; roomId: IRoom['_id']; userId: IUser['_id'] }) => + actionRoom({ action: 'removeOwner', type, roomId, extraData: { userId } }); + +export const getChannelRoles = async ({ + roomId, + overrideCredentials = credentials, +}: { + roomId: IRoom['_id']; + overrideCredentials: Credentials; +}) => + ( + await request.get(api('channels.roles')).set(overrideCredentials).query({ + roomId, + }) + ).body.roles; + +export const setRoomConfig = ({ roomId, favorite, isDefault }: { roomId: IRoom['_id']; favorite: boolean; isDefault: boolean }) => { + return request + .post(api('rooms.saveRoomSettings')) + .set(credentials) + .send({ + rid: roomId, + default: isDefault, + favorite: favorite + ? { + defaultValue: true, + favorite: false, + } + : undefined, + }); +}; diff --git a/apps/meteor/tests/data/uploads.helper.ts b/apps/meteor/tests/data/uploads.helper.ts index eabd49e11a3b..5f6e4924d9ff 100644 --- a/apps/meteor/tests/data/uploads.helper.ts +++ b/apps/meteor/tests/data/uploads.helper.ts @@ -9,29 +9,38 @@ import { imgURL } from './interactions'; import { updateSetting } from './permissions.helper'; import { createRoom, deleteRoom } from './rooms.helper'; import { createVisitor } from './livechat/rooms'; - -export async function testFileUploads(filesEndpoint: 'channels.files' | 'groups.files' | 'im.files', roomType: 'c' | 'd' | 'p', invalidRoomError = 'error-room-not-found') { - let testRoom: Record; +import { IRoom } from '@rocket.chat/core-typings'; + +export async function testFileUploads( + filesEndpoint: 'channels.files' | 'groups.files' | 'im.files', + roomType: 'c' | 'd' | 'p', + invalidRoomError = 'error-room-not-found', +) { + let testRoom: IRoom; const propertyMap = { - 'c': 'channel', - 'p': 'group', - 'd': 'room', + c: 'channel', + p: 'group', + d: 'room', }; - before(async function () { - await updateSetting('VoIP_Enabled', true); - await updateSetting('Message_KeepHistory', true); + before(async () => { + await Promise.all([updateSetting('VoIP_Enabled', true), updateSetting('Message_KeepHistory', true)]); - testRoom = (await createRoom({ type: roomType, ...(roomType === 'd' ? { username: 'rocket.cat' } : { name: `channel-files-${Date.now()}` }) } as any)).body[propertyMap[roomType]]; + testRoom = ( + await createRoom({ + type: roomType, + ...(roomType === 'd' ? { username: 'rocket.cat' } : { name: `channel-files-${Date.now()}` }), + } as any) + ).body[propertyMap[roomType]]; }); - after(async function () { - await Promise.all([ - deleteRoom({ type: 'c', roomId: testRoom._id }), + after(() => + Promise.all([ + deleteRoom({ type: 'c' as const, roomId: testRoom._id }), updateSetting('VoIP_Enabled', false), updateSetting('Message_KeepHistory', false), - ]); - }); + ]), + ); const createVoipRoom = async function () { const testUser = await createUser({ roles: ['user', 'livechat-agent'] }); @@ -42,10 +51,6 @@ export async function testFileUploads(filesEndpoint: 'channels.files' | 'groups. type: 'v', agentId: testUser._id, credentials: testUserCredentials, - name: null, - username: null, - members: null, - extraData: null, }); return roomResponse.body.room; diff --git a/apps/meteor/tests/end-to-end/api/05-chat.js b/apps/meteor/tests/end-to-end/api/05-chat.js index fe02112c2811..96aa276e1a9c 100644 --- a/apps/meteor/tests/end-to-end/api/05-chat.js +++ b/apps/meteor/tests/end-to-end/api/05-chat.js @@ -5,7 +5,7 @@ import { getCredentials, api, request, credentials, message } from '../../data/a import { sendSimpleMessage, deleteMessage, pinMessage } from '../../data/chat.helper.js'; import { imgURL } from '../../data/interactions'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; -import { createRoom, deleteRoom } from '../../data/rooms.helper.js'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; diff --git a/apps/meteor/tests/end-to-end/api/07-incoming-integrations.js b/apps/meteor/tests/end-to-end/api/07-incoming-integrations.js index 37ce98ade6c5..7c325a9e3cd2 100644 --- a/apps/meteor/tests/end-to-end/api/07-incoming-integrations.js +++ b/apps/meteor/tests/end-to-end/api/07-incoming-integrations.js @@ -4,7 +4,7 @@ import { after, before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission } from '../../data/permissions.helper'; -import { createRoom, deleteRoom } from '../../data/rooms.helper.js'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; diff --git a/apps/meteor/tests/end-to-end/api/16-commands.js b/apps/meteor/tests/end-to-end/api/16-commands.js index 74e9255524c4..c0781167b67f 100644 --- a/apps/meteor/tests/end-to-end/api/16-commands.js +++ b/apps/meteor/tests/end-to-end/api/16-commands.js @@ -3,7 +3,7 @@ import { before, describe, it, after } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage } from '../../data/chat.helper.js'; -import { createRoom, deleteRoom } from '../../data/rooms.helper.js'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper.js'; diff --git a/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts b/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts index fb7f42ccdabd..d7772b7a233a 100644 --- a/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts +++ b/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts @@ -31,7 +31,9 @@ describe('Apps - Video Conferences', function () { roomId = res.body.group._id; }); - after(() => Promise.all([cleanupApps(), deleteRoom({ type: 'p', roomId }), updateSetting('VideoConf_Default_Provider', '')])); + after(() => + Promise.all([cleanupApps(), deleteRoom({ type: 'p', roomId: roomId as string }), updateSetting('VideoConf_Default_Provider', '')]), + ); describe('[With No App]', () => { before(async () => { From 4afd41cc604f97f1e19f70648dd4b8a66bee26b9 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Thu, 13 Jun 2024 10:09:51 -0300 Subject: [PATCH 08/26] chore: change e2e password placeholder (#32569) --- apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx | 5 +++-- packages/i18n/src/locales/ar.i18n.json | 3 +-- packages/i18n/src/locales/ca.i18n.json | 3 +-- packages/i18n/src/locales/cs.i18n.json | 3 +-- packages/i18n/src/locales/da.i18n.json | 3 +-- packages/i18n/src/locales/de-IN.i18n.json | 3 +-- packages/i18n/src/locales/de.i18n.json | 3 +-- packages/i18n/src/locales/en.i18n.json | 7 ++++--- packages/i18n/src/locales/es.i18n.json | 3 +-- packages/i18n/src/locales/fi.i18n.json | 3 +-- packages/i18n/src/locales/fr.i18n.json | 3 +-- packages/i18n/src/locales/hr.i18n.json | 3 +-- packages/i18n/src/locales/hu.i18n.json | 3 +-- packages/i18n/src/locales/ja.i18n.json | 3 +-- packages/i18n/src/locales/ka-GE.i18n.json | 3 +-- packages/i18n/src/locales/km.i18n.json | 3 +-- packages/i18n/src/locales/ko.i18n.json | 3 +-- packages/i18n/src/locales/nl.i18n.json | 3 +-- packages/i18n/src/locales/pl.i18n.json | 3 +-- packages/i18n/src/locales/pt-BR.i18n.json | 7 ++++--- packages/i18n/src/locales/pt.i18n.json | 3 +-- packages/i18n/src/locales/ru.i18n.json | 3 +-- packages/i18n/src/locales/sv.i18n.json | 3 +-- packages/i18n/src/locales/tr.i18n.json | 3 +-- packages/i18n/src/locales/uk.i18n.json | 3 +-- packages/i18n/src/locales/zh-TW.i18n.json | 3 +-- packages/i18n/src/locales/zh.i18n.json | 3 +-- 27 files changed, 35 insertions(+), 56 deletions(-) diff --git a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx index 161500da0cc3..711b5a6d21a1 100644 --- a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx @@ -42,8 +42,9 @@ const EnterE2EPasswordModal = ({ wrapperFunction={(props) => } variant='warning' title={t('Enter_E2E_password')} + icon='warning' cancelText={t('Do_It_Later')} - confirmText={t('Decode_Key')} + confirmText={t('Enable_encryption')} onClose={onClose} onCancel={onCancel} > @@ -51,7 +52,7 @@ const EnterE2EPasswordModal = ({ - + {passwordError} diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index 85dfe30a4839..5b011c5663e2 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -1331,7 +1331,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "الحد حسب المستخدم: الطلبات المسموح بها", "Deactivate": "إلغاء التنشيط", "Decline": "تراجع", - "Decode_Key": "مفتاح إلغاء التشفير", "Default": "افتراضي", "Default_value": "القيمة الافتراضية", "Delete": "حذف", @@ -4897,4 +4896,4 @@ "Enterprise": "مؤسسة", "UpgradeToGetMore_engagement-dashboard_Title": "التحليلات", "UpgradeToGetMore_auditing_Title": "تدقيق الرسائل" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 02908f809714..6c61025e16cf 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -1320,7 +1320,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Límit per usuari: peticions permeses", "Deactivate": "Desactivar", "Decline": "Rebutja", - "Decode_Key": "Clau de descodificació", "Default": "Per defecte", "Default_value": "Valor per defecte", "Delete": "Elimina", @@ -4696,4 +4695,4 @@ "Enterprise": "Empresa", "UpgradeToGetMore_engagement-dashboard_Title": "Analítiques", "UpgradeToGetMore_auditing_Title": "Auditoria de missatges" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index a02152a66277..d122f1d89337 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -1131,7 +1131,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Limit podle uživatele: požadavky povoleny", "Deactivate": "Deaktivovat", "Decline": "Zamítnout", - "Decode_Key": "Dekódovat klíč", "Default": "Výchozí", "Default_value": "Výchozí hodnota", "Delete": "Smazat", @@ -3982,4 +3981,4 @@ "Enterprise": "Korporace", "UpgradeToGetMore_engagement-dashboard_Title": "Analytika", "UpgradeToGetMore_auditing_Title": "Audit zpráv" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 68f5b4a146d3..22183a5a0604 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -1210,7 +1210,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Begræns efter bruger: anmodninger tilladt", "Deactivate": "Deaktivér", "Decline": "Afslå", - "Decode_Key": "Afkodningsnøgle", "Default": "Standard", "Default_value": "Standardværdi", "Delete": "Slet", @@ -4097,4 +4096,4 @@ "Enterprise": "Firma", "UpgradeToGetMore_engagement-dashboard_Title": "Analyse", "UpgradeToGetMore_auditing_Title": "Meddelelsesovervågning" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/de-IN.i18n.json b/packages/i18n/src/locales/de-IN.i18n.json index 719cfe9da4fd..afb9dade56f5 100644 --- a/packages/i18n/src/locales/de-IN.i18n.json +++ b/packages/i18n/src/locales/de-IN.i18n.json @@ -931,7 +931,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Beschränkung durch Benutzer: Anforderungen zulässig", "Deactivate": "Deaktivieren", "Decline": "ablehnen", - "Decode_Key": "Entschlüsseln", "Default": "Voreinstellung", "Delete": "Löschen", "Delete_message": "Nachricht löschen", @@ -3099,4 +3098,4 @@ "Your_question": "Deine Frage", "Your_server_link": "Dein Server-Link", "Your_workspace_is_ready": "Dein Arbeitsbereich ist einsatzbereit 🎉" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index ae2e193ec30e..b67d318f1176 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -1463,7 +1463,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Beschränkung durch Benutzer: Anforderungen zulässig", "Deactivate": "Deaktivieren", "Decline": "Ablehnen", - "Decode_Key": "Entschlüsseln", "default": "Standard", "Default": "Voreinstellung", "Default_provider": "Standardanbieter", @@ -5517,4 +5516,4 @@ "Enterprise": "Unternehmen", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Nachrichtenüberprüfung" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 3a50e7d39df6..4e658735e130 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1576,7 +1576,6 @@ "Deactivate": "Deactivate", "Deactivated": "Deactivated", "Decline": "Decline", - "Decode_Key": "Decode Key", "default": "default", "Default": "Default", "Default_provider": "Default provider", @@ -1798,7 +1797,7 @@ "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.

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.
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.", + "E2E_password_request_text": "To access your encrypted channels and direct messages, enter your encryption password. This is not stored on the server, so you’ll need to use it on every device.", "E2E_password_reveal_text": "Create secure private rooms and direct messages with end-to-end encryption. This password won’t be stored on the server. You can use it on all your devices.", "E2E_password_save_text": "This will only be displayed once, please save it now.", "E2E_Reset_Email_Content": "You've been automatically logged out. When you login again, Rocket.Chat will generate a new key and restore your access to any encrypted room that has one or more members online. Due to the nature of the E2E encryption, Rocket.Chat will not be able to restore access to any encrypted room that has no member online.", @@ -1925,6 +1924,7 @@ "Extra_CSP_Domains": "Extra CSP Domains", "Extra_CSP_Domains_Description": "Extra domains to add to the Content-Security-Policy", "Enable_Desktop_Notifications": "Enable Desktop Notifications", + "Enable_encryption" : "Enable encryption", "Enable_inquiry_fetch_by_stream": "Enable inquiry data fetch from server using a stream", "Enable_omnichannel_auto_close_abandoned_rooms": "Enable automatic closing of rooms abandoned by the visitor", "Enable_Password_History": "Enable Password History", @@ -1974,7 +1974,7 @@ "Enter_Behaviour": "Enter key Behaviour", "Enter_Behaviour_Description": "This changes if the enter key will send a message or do a line break", "Enter_code_here": "Enter code here", - "Enter_E2E_password": "Enter E2E password", + "Enter_E2E_password": "Enter E2EE password", "Enter_name_here": "Enter name here", "Enter_Normal": "Normal mode (send with Enter)", "Enter_the_code_we_just_emailed_you": "Enter the code we just emailed you.", @@ -4179,6 +4179,7 @@ "Please_enter_value_for_url": "Please enter a value for the url of your avatar.", "Please_enter_your_new_password_below": "Please enter your new password below:", "Please_enter_your_password": "Please enter your password", + "Please_enter_E2EE_password": "Please enter your E2EE password", "Please_fill_a_label": "Please fill a label", "Please_fill_a_name": "Please fill a name", "Please_fill_a_token_name": "Please fill a valid token name", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index d79d3fa71cb1..bbaaff711afb 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -1336,7 +1336,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Limitar por usuario: solicitudes permitidas", "Deactivate": "Desactivar", "Decline": "Rechazar", - "Decode_Key": "Clave de decodificación", "Default": "Por defecto", "Default_value": "Valor por defecto", "Delete": "Eliminar", @@ -5095,4 +5094,4 @@ "Unlimited_seats": "Puestos ilimitados", "Unlimited_MACs": "Contactos Activos por Mes (MAC) ilimitados", "Unlimited_seats_MACs": "Puestos y Contactos Activos por Mes (MAC) ilimitados" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index bfdc4b6f872c..a55af647ff04 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -1484,7 +1484,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Rajoitus käyttäjän mukaan: sallitut pyynnöt", "Deactivate": "Poista käytöstä", "Decline": "Hylkää", - "Decode_Key": "Purkuavain", "default": "oletus", "Default": "Oletus", "Default_provider": "Oletuspalveluntarjoaja", @@ -5755,4 +5754,4 @@ "Theme_Appearence": "Teeman ulkoasu", "Enterprise": "Yritys", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index ff96989499f8..d5f92179372f 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -1333,7 +1333,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Limite par utilisateur : demandes autorisées", "Deactivate": "Désactiver", "Decline": "Refuser", - "Decode_Key": "Décoder la clé", "Default": "Défaut", "Default_value": "Valeur par défaut", "Delete": "Supprimer", @@ -4892,4 +4891,4 @@ "Enterprise": "Entreprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analyses", "UpgradeToGetMore_auditing_Title": "Audit des messages" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/hr.i18n.json b/packages/i18n/src/locales/hr.i18n.json index 1228f11009fb..c1046fdac669 100644 --- a/packages/i18n/src/locales/hr.i18n.json +++ b/packages/i18n/src/locales/hr.i18n.json @@ -916,7 +916,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Ograniči po korisniku: dopušteni zahtjevi", "Deactivate": "Isključi", "Decline": "Odbij", - "Decode_Key": "Ključ za dekodiranje", "Default": "Zadano", "Delete": "Obriši", "Delete_message": "Obriši poruku", @@ -2904,4 +2903,4 @@ "registration.component.form.sendConfirmationEmail": "Pošalji potvrdni email", "Enterprise": "Poduzeće", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index aebe30ba343d..74fa10105efd 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -1440,7 +1440,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Korlátozás felhasználó szerint: kérések engedélyezve", "Deactivate": "Inaktiválás", "Decline": "Elutasítás", - "Decode_Key": "Visszafejtő kulcs", "default": "alapértelmezett", "Default": "Alapértelmezett", "Default_value": "Alapértelmezett érték", @@ -5432,4 +5431,4 @@ "Enterprise": "Vállalati", "UpgradeToGetMore_engagement-dashboard_Title": "Analitika", "UpgradeToGetMore_auditing_Title": "Üzenet ellenőrzés" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index 7000ad51998b..e976c9559a78 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -1314,7 +1314,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "ユーザーによる制限:許可された要求", "Deactivate": "無効にする", "Decline": "拒否", - "Decode_Key": "キーのデコード", "Default": "デフォルト", "Default_value": "デフォルト値", "Delete": "削除", @@ -4835,4 +4834,4 @@ "Enterprise": "エンタープライズ", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "UpgradeToGetMore_auditing_Title": "メッセージ監査" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index 7e28a9792559..ea1cde6e162e 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -1069,7 +1069,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "მომხმარებლის მიერ შეზღუდვა: მოთხოვნა დაშვებულია", "Deactivate": "გამორთვა", "Decline": "უარყოფა", - "Decode_Key": "დეკოდირების გასაღები", "Default": "ნაგულისხმევი (default)", "Default_value": "ნაგულისხმევი მნიშვნელობა", "Delete": "წაშლა", @@ -3683,4 +3682,4 @@ "onboarding.form.registerOfflineForm.title": "ხელით დარეგისტრირება", "UpgradeToGetMore_engagement-dashboard_Title": "ანალიტიკა", "UpgradeToGetMore_auditing_Title": "შეტყობინებების შემოწმება" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/km.i18n.json b/packages/i18n/src/locales/km.i18n.json index 6254254f89cf..cc6d2f815193 100644 --- a/packages/i18n/src/locales/km.i18n.json +++ b/packages/i18n/src/locales/km.i18n.json @@ -996,7 +996,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "កំណត់ដោយអ្នកប្រើ: ការស្នើសុំត្រូវបានអនុញ្ញាត", "Deactivate": "ធ្វើឲ្យមិនសកម្ម", "Decline": "បដិសេធ", - "Decode_Key": "សោរដោះលេខកូដ", "Default": "លំនាំដើម", "Default_value": "តម្លៃ​លំនាំដើម", "Delete": "លុប", @@ -3128,4 +3127,4 @@ "Enterprise": "សហគ្រាស", "UpgradeToGetMore_engagement-dashboard_Title": "វិភាគ", "UpgradeToGetMore_auditing_Title": "សវនកម្មសារ" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index 49a542171868..b4b4ed006b47 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -1170,7 +1170,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "사용자 제한 : 요청 허용", "Deactivate": "비활성화", "Decline": "거절", - "Decode_Key": "복호화 키", "Default": "기본", "Default_value": "기본값", "Delete": "삭제", @@ -4040,4 +4039,4 @@ "Enterprise": "기업", "UpgradeToGetMore_engagement-dashboard_Title": "분석(에널리틱스)", "UpgradeToGetMore_auditing_Title": "메시지 감사" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index 7665b7a4ea54..41052e0fe06e 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -1325,7 +1325,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Limiet per gebruiker: verzoeken toegestaan", "Deactivate": "Deactiveren", "Decline": "Weigeren", - "Decode_Key": "Decodeer sleutel", "Default": "Standaard", "Default_value": "Standaardwaarde", "Delete": "Verwijderen", @@ -4877,4 +4876,4 @@ "Enterprise": "Onderneming", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Bericht auditing" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 0936ec845df1..27e8e712d978 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -1429,7 +1429,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Limit według użytkowników: dozwolone żądania", "Deactivate": "Dezaktywuj", "Decline": "Odrzuć", - "Decode_Key": "Klucz dekodowania", "default": "Domyślny", "Default": "Domyślny", "Default_value": "Domyślna wartość", @@ -5353,4 +5352,4 @@ "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analityka", "UpgradeToGetMore_auditing_Title": "Audyt wiadomości" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index 0fecc1fa944a..2259e79d64d6 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -1372,7 +1372,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Limite por usuário: solicitações permitidas", "Deactivate": "Desativar", "Decline": "Recusar", - "Decode_Key": "Chave de decodificação", "Default": "Padrão", "Default_value": "Valor padrão", "Delete": "Excluir", @@ -1633,6 +1632,7 @@ "Enable_CSP": "Habilitar política de segurança de conteúdo", "Enable_CSP_Description": "Não desative esta opção a não ser que você tenha uma versão personalizada e esteja tendo problemas devido a scrips inline", "Enable_Desktop_Notifications": "Habilitar notificações da área de trabalho", + "Enable_encryption" : "Ativar criptografia", "Enable_inquiry_fetch_by_stream": "Habilitar carga de dados de novas pesquisas de omnichannel utilizando stream", "Enable_omnichannel_auto_close_abandoned_rooms": "Habilitar o fechamento automático de salas abandonadas pelo visitante", "Enable_Password_History": "Habilitar histórico de senha", @@ -1669,7 +1669,7 @@ "Enter_authentication_code": "Digite o código de autenticação", "Enter_Behaviour": "Insira o comportamento da tecla", "Enter_Behaviour_Description": "Muda se a tecla Enter enviará uma mensagem ou fará quebra de linha", - "Enter_E2E_password": "Entre com senha E2E", + "Enter_E2E_password": "Entre com senha E2EE", "Enter_name_here": "Insira o nome aqui", "Enter_Normal": "Modo normal (enviar com Enter)", "Enter_to": "Enter para", @@ -3380,6 +3380,7 @@ "Please_enter_value_for_url": "Insira um valor para o URL do seu avatar.", "Please_enter_your_new_password_below": "Digite sua nova senha abaixo:", "Please_enter_your_password": "Digite sua senha", + "Please_enter_E2EE_password": "Por favor, digite a senha de E2EE", "Please_fill_a_label": "Preencha um rótulo", "Please_fill_a_name": "Preencha um nome", "Please_fill_a_token_name": "Preencha um nome de token válido", @@ -5014,4 +5015,4 @@ "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics", "UpgradeToGetMore_auditing_Title": "Auditoria de mensagem" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index 22dacb356003..3f9bfe543dcc 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -973,7 +973,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Limite por Usuário: Solicitações permitidas", "Deactivate": "Desactivar", "Decline": "Recusar", - "Decode_Key": "Chave de decodificação", "Default": "Padrão", "Delete": "Apagar", "Delete_message": "Apagar mensagem", @@ -3187,4 +3186,4 @@ "registration.component.form.sendConfirmationEmail": "Enviar email de confirmação", "Enterprise": "Empreendimento", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 9b4b31910ba4..3af2f4dfdf88 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -1444,7 +1444,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Ограничение по пользователю: запросы разрешены", "Deactivate": "Деактивировать", "Decline": "Отклонить", - "Decode_Key": "Код декодирования", "Default": "По умолчанию", "Default_value": "Значение по умолчанию", "Delete": "Удалить", @@ -5104,4 +5103,4 @@ "Enterprise": "Корпорация", "UpgradeToGetMore_engagement-dashboard_Title": "Аналитика", "UpgradeToGetMore_auditing_Title": "Аудит сообщений" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index bea0ea1cb238..67f1912f25f0 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -1485,7 +1485,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Gräns per användare: förfrågningar tillåts", "Deactivate": "Inaktivera", "Decline": "Tacka nej", - "Decode_Key": "Avkodningsnyckel", "default": "standard", "Default": "Standard", "Default_provider": "Standardleverantör", @@ -5759,4 +5758,4 @@ "Theme_Appearence": "Utseende för tema", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/tr.i18n.json b/packages/i18n/src/locales/tr.i18n.json index 232b7eef4607..e8475e235715 100644 --- a/packages/i18n/src/locales/tr.i18n.json +++ b/packages/i18n/src/locales/tr.i18n.json @@ -976,7 +976,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Kullanıcı ile sınırlama: istekler izinli", "Deactivate": "Devre dışı bırak", "Decline": "Reddet", - "Decode_Key": "Kod Çözme Anahtarı", "Default": "Varsayılan", "Delete": "Sil", "Delete_all_closed_chats": "Tüm kapalı sohbetleri sil", @@ -3276,4 +3275,4 @@ "RegisterWorkspace_Features_Omnichannel_Title": "Çoklu Kanal", "Enterprise": "Kuruluş", "UpgradeToGetMore_engagement-dashboard_Title": "Mantıksal Analiz" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index 7cf127f8c8bf..70dca33be576 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -1059,7 +1059,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "Обмеження по користувачу: запити дозволені", "Deactivate": "Деактивувати", "Decline": "Відхилити", - "Decode_Key": "Ключ декодування", "Default": "За замовчуванням", "Default_value": "Значення за замовчуванням", "Delete": "Видалити", @@ -3363,4 +3362,4 @@ "Enterprise": "Підприємство", "UpgradeToGetMore_engagement-dashboard_Title": "Аналітика", "UpgradeToGetMore_auditing_Title": "Аудит повідомлень" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index dee0b0e801f4..93360b12c6b5 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -1312,7 +1312,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "限制使用者: 允許需求", "Deactivate": "停用", "Decline": "拒絕", - "Decode_Key": "解鎖金鑰", "Default": "預設", "Default_value": "預設值", "Delete": "刪除", @@ -4590,4 +4589,4 @@ "Enterprise": "企業", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "UpgradeToGetMore_auditing_Title": "訊息稽核" -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index 2c72cc0d4151..2445fd2b3b08 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -1191,7 +1191,6 @@ "DDP_Rate_Limit_User_Requests_Allowed": "用户限制: 允许请求", "Deactivate": "禁用", "Decline": "下降", - "Decode_Key": "解码密钥", "Default": "默认", "Default_value": "默认值", "Delete": "删除", @@ -4147,4 +4146,4 @@ "Enterprise": "企业", "UpgradeToGetMore_engagement-dashboard_Title": "分析", "UpgradeToGetMore_auditing_Title": "消息审计" -} \ No newline at end of file +} From 1056f220dfc2316cad7540f7f7f18d9b98b5b9cf Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Thu, 13 Jun 2024 10:50:51 -0300 Subject: [PATCH 09/26] fix: prevent OTR in E2EE (#32459) --- .changeset/famous-scissors-teach.md | 6 + .../roomActions/useE2EERoomAction.spec.ts | 122 ++++++++++++++++++ .../hooks/roomActions/useE2EERoomAction.ts | 13 +- apps/meteor/client/hooks/useOTR.spec.tsx | 70 ++++++++++ apps/meteor/client/hooks/useOTR.ts | 28 ++++ .../views/room/contextualBar/OTR/OTR.tsx | 22 +++- .../room/contextualBar/OTR/OTRWithData.tsx | 17 +-- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 26 ++++ .../page-objects/fragments/home-flextab.ts | 4 + packages/i18n/src/locales/en.i18n.json | 3 + 10 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 .changeset/famous-scissors-teach.md create mode 100644 apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts create mode 100644 apps/meteor/client/hooks/useOTR.spec.tsx create mode 100644 apps/meteor/client/hooks/useOTR.ts diff --git a/.changeset/famous-scissors-teach.md b/.changeset/famous-scissors-teach.md new file mode 100644 index 000000000000..05d4cbbf8ea5 --- /dev/null +++ b/.changeset/famous-scissors-teach.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Prevent usage of OTR messages with End-to-end Encryption, both feature shouldn't and can't work together. diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts new file mode 100644 index 000000000000..eb0cbe5b24f4 --- /dev/null +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.spec.ts @@ -0,0 +1,122 @@ +import { useSetting, usePermission, useEndpoint } from '@rocket.chat/ui-contexts'; +import { act, renderHook } from '@testing-library/react-hooks'; + +import { E2EEState } from '../../../app/e2e/client/E2EEState'; +import { e2e } from '../../../app/e2e/client/rocketchat.e2e'; +import { OtrRoomState } from '../../../app/otr/lib/OtrRoomState'; +import { dispatchToastMessage } from '../../lib/toast'; +import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; +import { useE2EEState } from '../../views/room/hooks/useE2EEState'; +import { useOTR } from '../useOTR'; +import { useE2EERoomAction } from './useE2EERoomAction'; + +jest.mock('@rocket.chat/ui-contexts', () => ({ + useSetting: jest.fn(), + usePermission: jest.fn(), + useEndpoint: jest.fn(), +})); + +jest.mock('../../lib/toast', () => ({ + dispatchToastMessage: jest.fn(), +})); + +jest.mock('../../views/room/contexts/RoomContext', () => ({ + useRoom: jest.fn(), + useRoomSubscription: jest.fn(), +})); + +jest.mock('../useOTR', () => ({ + useOTR: jest.fn(), +})); + +jest.mock('../../../app/e2e/client/rocketchat.e2e', () => ({ + e2e: { + isReady: jest.fn(), + }, +})); + +jest.mock('../../views/room/hooks/useE2EEState', () => ({ + useE2EEState: jest.fn(), +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +jest.mock('meteor/tracker', () => ({ + Tracker: { + autorun: jest.fn(), + }, +})); + +describe('useE2EERoomAction', () => { + const mockRoom = { _id: 'roomId', encrypted: false, t: 'd', name: 'Test Room' }; + const mockSubscription = { autoTranslate: false }; + + beforeEach(() => { + (useSetting as jest.Mock).mockReturnValue(true); + (useRoom as jest.Mock).mockReturnValue(mockRoom); + (useRoomSubscription as jest.Mock).mockReturnValue(mockSubscription); + (useE2EEState as jest.Mock).mockReturnValue(E2EEState.READY); + (usePermission as jest.Mock).mockReturnValue(true); + (useEndpoint as jest.Mock).mockReturnValue(jest.fn().mockResolvedValue({ success: true })); + (e2e.isReady as jest.Mock).mockReturnValue(true); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should dispatch error toast message when otrState is ESTABLISHED', async () => { + (useOTR as jest.Mock).mockReturnValue({ otrState: OtrRoomState.ESTABLISHED }); + + const { result } = renderHook(() => useE2EERoomAction()); + + await act(async () => { + await result?.current?.action?.(); + }); + + expect(dispatchToastMessage).toHaveBeenCalledWith({ type: 'error', message: 'E2EE_not_available_OTR' }); + }); + + it('should dispatch error toast message when otrState is ESTABLISHING', async () => { + (useOTR as jest.Mock).mockReturnValue({ otrState: OtrRoomState.ESTABLISHING }); + + const { result } = renderHook(() => useE2EERoomAction()); + + await act(async () => { + await result?.current?.action?.(); + }); + + expect(dispatchToastMessage).toHaveBeenCalledWith({ type: 'error', message: 'E2EE_not_available_OTR' }); + }); + + it('should dispatch error toast message when otrState is REQUESTED', async () => { + (useOTR as jest.Mock).mockReturnValue({ otrState: OtrRoomState.REQUESTED }); + + const { result } = renderHook(() => useE2EERoomAction()); + + await act(async () => { + await result?.current?.action?.(); + }); + + expect(dispatchToastMessage).toHaveBeenCalledWith({ type: 'error', message: 'E2EE_not_available_OTR' }); + }); + + it('should dispatch success toast message when encryption is enabled', async () => { + (useOTR as jest.Mock).mockReturnValue({ otrState: OtrRoomState.NOT_STARTED }); + + const { result } = renderHook(() => useE2EERoomAction()); + + await act(async () => { + await result?.current?.action?.(); + }); + + expect(dispatchToastMessage).toHaveBeenCalledWith({ + type: 'success', + message: 'E2E_Encryption_enabled_for_room', + }); + }); +}); diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts index abc84f372594..bed73ab45c6b 100644 --- a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts @@ -1,14 +1,16 @@ import { isRoomFederated } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useSetting, usePermission, useEndpoint } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { E2EEState } from '../../../app/e2e/client/E2EEState'; +import { OtrRoomState } from '../../../app/otr/lib/OtrRoomState'; import { dispatchToastMessage } from '../../lib/toast'; import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; import { useE2EEState } from '../../views/room/hooks/useE2EEState'; +import { useOTR } from '../useOTR'; export const useE2EERoomAction = () => { const enabled = useSetting('E2E_Enable', false); @@ -22,10 +24,17 @@ export const useE2EERoomAction = () => { const permitted = (room.t === 'd' || (permittedToEditRoom && permittedToToggleEncryption)) && readyToEncrypt; const federated = isRoomFederated(room); const { t } = useTranslation(); + const { otrState } = useOTR(); const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); - const action = useMutableCallback(async () => { + const action = useEffectEvent(async () => { + if (otrState === OtrRoomState.ESTABLISHED || otrState === OtrRoomState.ESTABLISHING || otrState === OtrRoomState.REQUESTED) { + dispatchToastMessage({ type: 'error', message: t('E2EE_not_available_OTR') }); + + return; + } + const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted }); if (!success) { return; diff --git a/apps/meteor/client/hooks/useOTR.spec.tsx b/apps/meteor/client/hooks/useOTR.spec.tsx new file mode 100644 index 000000000000..0206d96ca176 --- /dev/null +++ b/apps/meteor/client/hooks/useOTR.spec.tsx @@ -0,0 +1,70 @@ +import { useUserId } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react-hooks'; + +import OTR from '../../app/otr/client/OTR'; +import { OtrRoomState } from '../../app/otr/lib/OtrRoomState'; +import { useRoom } from '../views/room/contexts/RoomContext'; +import { useOTR } from './useOTR'; + +jest.mock('@rocket.chat/ui-contexts', () => ({ + useUserId: jest.fn(), +})); + +jest.mock('../views/room/contexts/RoomContext', () => ({ + useRoom: jest.fn(), +})); + +jest.mock('../../app/otr/client/OTR', () => ({ + getInstanceByRoomId: jest.fn(), +})); + +jest.mock('./useReactiveValue', () => ({ + useReactiveValue: jest.fn((fn) => fn()), +})); + +describe('useOTR', () => { + it('should return error state when user ID is not available', () => { + (useUserId as jest.Mock).mockReturnValue(undefined); + (useRoom as jest.Mock).mockReturnValue({ _id: 'roomId' }); + + const { result } = renderHook(() => useOTR()); + + expect(result.current.otr).toBeUndefined(); + expect(result.current.otrState).toBe(OtrRoomState.ERROR); + }); + + it('should return error state when room ID is not available', () => { + (useUserId as jest.Mock).mockReturnValue('userId'); + (useRoom as jest.Mock).mockReturnValue(undefined); + + const { result } = renderHook(() => useOTR()); + + expect(result.current.otr).toBeUndefined(); + expect(result.current.otrState).toBe(OtrRoomState.ERROR); + }); + + it('should return error state when OTR instance is not available', () => { + (useUserId as jest.Mock).mockReturnValue('userId'); + (useRoom as jest.Mock).mockReturnValue({ _id: 'roomId' }); + (OTR.getInstanceByRoomId as jest.Mock).mockReturnValue(undefined); + + const { result } = renderHook(() => useOTR()); + + expect(result.current.otr).toBeUndefined(); + expect(result.current.otrState).toBe(OtrRoomState.ERROR); + }); + + it('should return the correct OTR instance and state when available', () => { + const mockOtrInstance = { + getState: jest.fn().mockReturnValue(OtrRoomState.NOT_STARTED), + }; + (useUserId as jest.Mock).mockReturnValue('userId'); + (useRoom as jest.Mock).mockReturnValue({ _id: 'roomId' }); + (OTR.getInstanceByRoomId as jest.Mock).mockReturnValue(mockOtrInstance); + + const { result } = renderHook(() => useOTR()); + + expect(result.current.otr).toBe(mockOtrInstance); + expect(result.current.otrState).toBe(OtrRoomState.NOT_STARTED); + }); +}); diff --git a/apps/meteor/client/hooks/useOTR.ts b/apps/meteor/client/hooks/useOTR.ts new file mode 100644 index 000000000000..65f9004cf323 --- /dev/null +++ b/apps/meteor/client/hooks/useOTR.ts @@ -0,0 +1,28 @@ +import { useUserId } from '@rocket.chat/ui-contexts'; +import { useMemo, useCallback } from 'react'; + +import OTR from '../../app/otr/client/OTR'; +import type { OTRRoom } from '../../app/otr/client/OTRRoom'; +import { OtrRoomState } from '../../app/otr/lib/OtrRoomState'; +import { useRoom } from '../views/room/contexts/RoomContext'; +import { useReactiveValue } from './useReactiveValue'; + +export const useOTR = (): { otr: OTRRoom | undefined; otrState: OtrRoomState } => { + const uid = useUserId(); + const room = useRoom(); + + const otr = useMemo(() => { + if (!uid || !room) { + return; + } + + return OTR.getInstanceByRoomId(uid, room._id); + }, [uid, room]); + + const otrState = useReactiveValue(useCallback(() => (otr ? otr.getState() : OtrRoomState.ERROR), [otr])); + + return { + otr, + otrState, + }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/OTR/OTR.tsx b/apps/meteor/client/views/room/contextualBar/OTR/OTR.tsx index 6b980dfcc390..aaf119e703ff 100644 --- a/apps/meteor/client/views/room/contextualBar/OTR/OTR.tsx +++ b/apps/meteor/client/views/room/contextualBar/OTR/OTR.tsx @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { Box, Button, Throbber } from '@rocket.chat/fuselage'; +import { Box, Button, Callout, Throbber } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { MouseEventHandler, ReactElement } from 'react'; import React from 'react'; @@ -12,6 +12,7 @@ import { ContextualbarClose, ContextualbarScrollableContent, } from '../../../../components/Contextualbar'; +import { useRoom } from '../../contexts/RoomContext'; import OTREstablished from './components/OTREstablished'; import OTRStates from './components/OTRStates'; @@ -27,6 +28,7 @@ type OTRProps = { const OTR = ({ isOnline, onClickClose, onClickStart, onClickEnd, onClickRefresh, otrState, peerUsername }: OTRProps): ReactElement => { const t = useTranslation(); + const room = useRoom(); const renderOTRState = (): ReactElement => { switch (otrState) { @@ -77,6 +79,22 @@ const OTR = ({ isOnline, onClickClose, onClickStart, onClickEnd, onClickRefresh, } }; + const renderOTRBody = (): ReactElement => { + if (room.encrypted) { + return ( + + {t('OTR_not_available_e2ee')} + + ); + } + + if (!isOnline) { + return {t('OTR_is_only_available_when_both_users_are_online')}; + } + + return renderOTRState(); + }; + return ( <> @@ -86,7 +104,7 @@ const OTR = ({ isOnline, onClickClose, onClickStart, onClickEnd, onClickRefresh, {t('Off_the_record_conversation')} - {isOnline ? renderOTRState() : {t('OTR_is_only_available_when_both_users_are_online')}} + {renderOTRBody()} ); diff --git a/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx b/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx index a4c642bdea64..c64d76892cb2 100644 --- a/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx @@ -1,27 +1,16 @@ -import { useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; -import OTR from '../../../../../app/otr/client/OTR'; import { OtrRoomState } from '../../../../../app/otr/lib/OtrRoomState'; +import { useOTR } from '../../../../hooks/useOTR'; import { usePresence } from '../../../../hooks/usePresence'; -import { useReactiveValue } from '../../../../hooks/useReactiveValue'; -import { useRoom } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import OTRComponent from './OTR'; const OTRWithData = (): ReactElement => { - const uid = useUserId(); - const room = useRoom(); + const { otr, otrState } = useOTR(); const { closeTab } = useRoomToolbox(); - const otr = useMemo(() => { - if (!uid) { - return; - } - return OTR.getInstanceByRoomId(uid, room._id); - }, [uid, room._id]); - const otrState = useReactiveValue(useCallback(() => (otr ? otr.getState() : OtrRoomState.ERROR), [otr])); const peerUserPresence = usePresence(otr?.getPeerId()); const userStatus = peerUserPresence?.status; const peerUsername = peerUserPresence?.username; diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index b33365c5dce0..bb4d2f478219 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -205,6 +205,32 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); + test('expect create a Direct message, encrypt it and attempt to enable OTR', async ({ page }) => { + await poHomeChannel.sidenav.openNewByLabel('Direct message'); + await poHomeChannel.sidenav.inputDirectUsername.click(); + await page.keyboard.type('user2'); + await page.waitForTimeout(1000); + await page.keyboard.press('Enter'); + await poHomeChannel.sidenav.btnCreate.click(); + + await expect(page).toHaveURL(`/direct/rocketchat.internal.admin.testuser2`); + + await poHomeChannel.tabs.kebab.click({ force: true }); + await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); + await poHomeChannel.tabs.btnEnableE2E.click({ force: true }); + await page.waitForTimeout(1000); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.dismissToast(); + + await poHomeChannel.tabs.kebab.click({ force: true }); + await expect(poHomeChannel.tabs.btnEnableOTR).toBeVisible(); + await poHomeChannel.tabs.btnEnableOTR.click({ force: true }); + + await expect(page.getByText('OTR not available')).toBeVisible(); + }); + test('expect placeholder text in place of encrypted message, when E2EE is not setup', async ({ page }) => { const channelName = faker.string.uuid(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts index 8e4a6961a790..b8bd673c9cf3 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts @@ -52,6 +52,10 @@ export class HomeFlextab { return this.page.locator('role=menuitem[name="Enable E2E"]'); } + get btnEnableOTR(): Locator { + return this.page.locator('role=menuitem[name="OTR"]'); + } + get flexTabViewThreadMessage(): Locator { return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('[data-qa-type="message-body"]'); } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 4e658735e130..e9d1894ef74e 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1782,6 +1782,7 @@ "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}}", + "E2EE_not_available_OTR": "This room has OTR enabled, E2E encryption cannot work with OTR.", "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.", @@ -4069,6 +4070,8 @@ "OTR_Chat_Timeout_Description": "%s failed to accept OTR chat invite in time. For privacy protection local cache was deleted, including all related system messages.", "OTR_Enable_Description": "Enable option to use off-the-record (OTR) messages in direct messages between 2 users. OTR messages are not recorded on the server and exchanged directly and encrypted between the 2 users.", "OTR_message": "OTR Message", + "OTR_not_available": "OTR not available", + "OTR_not_available_e2ee": "This room has E2E encryption enabled, OTR cannot work with encrypted messages.", "OTR_is_only_available_when_both_users_are_online": "OTR is only available when both users are online", "OTR_Session_ended_other_user_went_offline": "OTR Session has ended. User {{username}} went offline", "outbound-voip-calls": "Outbound Voip Calls", From afa560da52749c46802c1016cafddf0e8071c078 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 13 Jun 2024 11:07:07 -0300 Subject: [PATCH 10/26] chore: bump meteor 2.16 (#32585) --- apps/meteor/.meteor/packages | 14 +++++----- apps/meteor/.meteor/release | 2 +- apps/meteor/.meteor/versions | 26 +++++++++---------- .../.npm/package/npm-shrinkwrap.json | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 8107c249add2..307b0d89eb0d 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -15,12 +15,12 @@ rocketchat:streamer rocketchat:version rocketchat:user-presence -accounts-base@2.2.10 +accounts-base@2.2.11 accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-oauth@1.4.3 +accounts-oauth@1.4.4 accounts-password@2.4.0 accounts-twitter@1.5.0 @@ -29,20 +29,20 @@ google-oauth@1.4.4 oauth@2.2.1 oauth2@1.3.2 -check@1.3.2 +check@1.4.1 ddp-rate-limiter@1.2.1 rate-limit@1.1.1 -email@2.2.5 +email@2.2.6 http@2.0.0 meteor-base@1.5.1 -ddp-common@1.4.0 +ddp-common@1.4.1 webapp@1.13.8 -mongo@1.16.8 +mongo@1.16.10 reload@1.3.1 -service-configuration@1.3.3 +service-configuration@1.3.4 session@1.2.1 shell-server@0.5.0 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index 966586ce54fe..5152abe9d582 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@2.15 +METEOR@2.16 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index a4483a5cf40e..416ae456f05b 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,9 +1,9 @@ -accounts-base@2.2.10 +accounts-base@2.2.11 accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-oauth@1.4.3 +accounts-oauth@1.4.4 accounts-password@2.4.0 accounts-twitter@1.5.0 allow-deny@1.1.1 @@ -15,14 +15,14 @@ binary-heap@1.0.11 boilerplate-generator@1.7.2 caching-compiler@1.2.2 callback-hook@1.5.1 -check@1.3.2 +check@1.4.1 coffeescript@2.7.0 coffeescript-compiler@2.4.1 ddp@1.4.1 -ddp-client@2.6.1 -ddp-common@1.4.0 +ddp-client@2.6.2 +ddp-common@1.4.1 ddp-rate-limiter@1.2.1 -ddp-server@2.7.0 +ddp-server@2.7.1 diff-sequence@1.1.2 dispatch:run-as-user@1.1.1 dynamic-import@0.7.3 @@ -31,7 +31,7 @@ ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 ejson@1.1.3 -email@2.2.5 +email@2.2.6 es5-shim@4.8.0 facebook-oauth@1.11.3 facts-base@1.0.1 @@ -45,17 +45,17 @@ id-map@1.1.1 inter-process-messaging@0.1.1 kadira:flow-router@2.12.1 localstorage@1.2.0 -logging@1.3.3 +logging@1.3.4 meteor@1.11.5 meteor-base@1.5.1 meteor-developer-oauth@1.3.2 meteorhacks:inject-initial@1.0.5 minifier-css@1.6.4 -minimongo@1.9.3 +minimongo@1.9.4 modern-browsers@0.1.10 modules@0.20.0 modules-runtime@0.13.1 -mongo@1.16.8 +mongo@1.16.10 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 @@ -84,7 +84,7 @@ rocketchat:streamer@1.1.0 rocketchat:user-presence@2.6.3 rocketchat:version@1.0.0 routepolicy@1.1.1 -service-configuration@1.3.3 +service-configuration@1.3.4 session@1.2.1 sha@1.0.9 shell-server@0.5.0 @@ -93,10 +93,10 @@ standard-minifier-css@1.9.2 tracker@1.3.3 twitter-oauth@1.3.3 typescript@4.9.5 -underscore@1.6.0 +underscore@1.6.1 url@1.3.2 webapp@1.13.8 webapp-hashing@1.1.1 zodern:caching-minifier@0.5.0 zodern:standard-minifier-js@5.3.1 -zodern:types@1.0.11 +zodern:types@1.0.13 diff --git a/apps/meteor/packages/flow-router/.npm/package/npm-shrinkwrap.json b/apps/meteor/packages/flow-router/.npm/package/npm-shrinkwrap.json index 47445b724946..8110d45c9f30 100644 --- a/apps/meteor/packages/flow-router/.npm/package/npm-shrinkwrap.json +++ b/apps/meteor/packages/flow-router/.npm/package/npm-shrinkwrap.json @@ -25,4 +25,4 @@ "integrity": "sha512-VH4FeG98gs6AkHivaW2O14vsOPBL9E80Sj7fITunoDijiYQ1lsVwJYmm1CSL+oLyO2N5HPdo23GXAG64uKOAZQ==" } } -} \ No newline at end of file +} From 97eaa176807a64d9cf10833eeda789d9c964d42b Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 14 Jun 2024 08:09:31 -0300 Subject: [PATCH 11/26] fix: Accepted Media Types settings validation (#32478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Henrique Guimarães Ribeiro <43561537+rique223@users.noreply.github.com> --- .changeset/chilly-toys-hunt.md | 5 ++ .../app/file-upload/server/lib/FileUpload.ts | 3 +- apps/meteor/app/utils/client/restrictions.ts | 2 +- apps/meteor/app/utils/lib/restrictions.ts | 34 ++++++++----- apps/meteor/app/utils/server/restrictions.ts | 2 +- .../client/lib/chats/flows/uploadFiles.ts | 2 +- .../unit/app/utils/lib/restrictions.spec.ts | 49 +++++++++++++++++++ 7 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 .changeset/chilly-toys-hunt.md create mode 100644 apps/meteor/tests/unit/app/utils/lib/restrictions.spec.ts diff --git a/.changeset/chilly-toys-hunt.md b/.changeset/chilly-toys-hunt.md new file mode 100644 index 000000000000..79be3fcfc74c --- /dev/null +++ b/.changeset/chilly-toys-hunt.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed "File Upload > Accepted Media Types" setting to allow all type of files uploads diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 4458f9d61881..c824ba6c31a5 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -170,7 +170,7 @@ export const FileUpload = { throw new Meteor.Error('error-file-too-large', reason); } - if (!fileUploadIsValidContentType(file.type as string, '')) { + if (!fileUploadIsValidContentType(file?.type)) { const reason = i18n.t('File_type_is_not_accepted', { lng: language }); throw new Meteor.Error('error-invalid-file-type', reason); } @@ -420,7 +420,6 @@ export const FileUpload = { await Avatars.deleteFile(oldAvatar._id); } await Avatars.updateFileNameById(file._id, user.username); - // console.log('upload finished ->', file); }, async requestCanAccessFiles({ headers = {}, url }: http.IncomingMessage, file?: IUpload) { diff --git a/apps/meteor/app/utils/client/restrictions.ts b/apps/meteor/app/utils/client/restrictions.ts index 261eddf4467d..d4c6b62d68dd 100644 --- a/apps/meteor/app/utils/client/restrictions.ts +++ b/apps/meteor/app/utils/client/restrictions.ts @@ -1,7 +1,7 @@ import { settings } from '../../settings/client'; import { fileUploadIsValidContentTypeFromSettings } from '../lib/restrictions'; -export const fileUploadIsValidContentType = function (type: string, customWhiteList?: string): boolean { +export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean { const blackList = settings.get('FileUpload_MediaTypeBlackList'); const whiteList = customWhiteList || settings.get('FileUpload_MediaTypeWhiteList'); diff --git a/apps/meteor/app/utils/lib/restrictions.ts b/apps/meteor/app/utils/lib/restrictions.ts index ebebe113f31e..bf859e5b4700 100644 --- a/apps/meteor/app/utils/lib/restrictions.ts +++ b/apps/meteor/app/utils/lib/restrictions.ts @@ -1,12 +1,10 @@ -import _ from 'underscore'; - export const fileUploadMediaWhiteList = function (customWhiteList: string): string[] | undefined { const mediaTypeWhiteList = customWhiteList; if (!mediaTypeWhiteList || mediaTypeWhiteList === '*') { return; } - return _.map(mediaTypeWhiteList.split(','), (item) => { + return mediaTypeWhiteList.split(',').map((item) => { return item.trim(); }); }; @@ -17,37 +15,47 @@ const fileUploadMediaBlackList = function (customBlackList: string): string[] | return; } - return _.map(blacklist.split(','), (item) => item.trim()); + return blacklist.split(',').map((item) => item.trim()); }; -const isTypeOnList = function (type: string, list: string[]): boolean | undefined { - if (_.contains(list, type)) { +const isTypeOnList = function (type?: string, list?: string[]): boolean { + if (!type || !list) { + return false; + } + + if (list.includes(type)) { return true; } const wildCardGlob = '/*'; - const wildcards = _.filter(list, (item) => { + const wildcards = list.filter((item) => { return item.indexOf(wildCardGlob) > 0; }); - if (_.contains(wildcards, type.replace(/(\/.*)$/, wildCardGlob))) { + if (wildcards.includes(type.replace(/(\/.*)$/, wildCardGlob))) { return true; } + + return false; }; -export const fileUploadIsValidContentTypeFromSettings = function (type: string, customWhiteList: string, customBlackList: string): boolean { +export const fileUploadIsValidContentTypeFromSettings = function ( + type: string | undefined, + customWhiteList: string, + customBlackList: string, +): boolean { const blackList = fileUploadMediaBlackList(customBlackList); const whiteList = fileUploadMediaWhiteList(customWhiteList); - if (!type && blackList) { + if (blackList && type && isTypeOnList(type, blackList)) { return false; } - if (blackList && isTypeOnList(type, blackList)) { - return false; + if (whiteList) { + return isTypeOnList(type, whiteList); } if (!whiteList) { return true; } - return !!isTypeOnList(type, whiteList); + return false; }; diff --git a/apps/meteor/app/utils/server/restrictions.ts b/apps/meteor/app/utils/server/restrictions.ts index ca524b09d351..6eb1c9a655d4 100644 --- a/apps/meteor/app/utils/server/restrictions.ts +++ b/apps/meteor/app/utils/server/restrictions.ts @@ -1,7 +1,7 @@ import { settings } from '../../settings/server'; import { fileUploadIsValidContentTypeFromSettings } from '../lib/restrictions'; -export const fileUploadIsValidContentType = function (type: string, customWhiteList?: string): boolean { +export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean { const blackList = settings.get('FileUpload_MediaTypeBlackList'); const whiteList = customWhiteList || settings.get('FileUpload_MediaTypeWhiteList'); diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 82572aa2dbf5..62458d53fd60 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -46,7 +46,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi imperativeModal.close(); uploadNextFile(); }, - invalidContentType: !(file.type && fileUploadIsValidContentType(file.type)), + invalidContentType: !fileUploadIsValidContentType(file?.type), }, }); }; diff --git a/apps/meteor/tests/unit/app/utils/lib/restrictions.spec.ts b/apps/meteor/tests/unit/app/utils/lib/restrictions.spec.ts new file mode 100644 index 000000000000..f7a6b4439f21 --- /dev/null +++ b/apps/meteor/tests/unit/app/utils/lib/restrictions.spec.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; + +import { fileUploadIsValidContentTypeFromSettings } from '../../../../../app/utils/lib/restrictions'; + +describe('fileUploadIsValidContentTypeFromSettings', () => { + it('should return true if type is not defined and whiteList is not defined', () => { + expect(fileUploadIsValidContentTypeFromSettings(undefined, '', '')).to.be.true; + }); + + it('should return false if type is not defined and whiteList is defined', () => { + expect(fileUploadIsValidContentTypeFromSettings(undefined, 'image/jpeg', '')).to.be.false; + }); + + it('should return true if type is defined and whiteList is not defined', () => { + expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', '', '')).to.be.true; + }); + + it('should return true if type is defined and whiteList is defined and type is in whiteList', () => { + expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', 'image/jpeg', '')).to.be.true; + }); + + it('should return false if type is defined and whiteList is defined and type is not in whiteList', () => { + expect(fileUploadIsValidContentTypeFromSettings('image/png', 'image/jpeg', '')).to.be.false; + }); + + it('should return false if type is defined and whiteList is not defined and type is in blackList', () => { + expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', '', 'image/jpeg')).to.be.false; + }); + + it('should return true if type is defined and whiteList is not defined and type is not in blackList', () => { + expect(fileUploadIsValidContentTypeFromSettings('image/png', '', 'image/jpeg')).to.be.true; + }); + + it('should return true if type is defined and whiteList is defined and type is in whiteList with wildcard', () => { + expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', 'image/*', '')).to.be.true; + }); + + it('should return false if type is defined and whiteList is defined and type is not in whiteList with wildcard', () => { + expect(fileUploadIsValidContentTypeFromSettings('text/plain', 'image/*', '')).to.be.false; + }); + + it('should return false if type is defined and whiteList is not defined and type is in blackList with wildcard', () => { + expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', '', 'image/*')).to.be.false; + }); + + it('should return true if type is defined and whiteList is defined and type is not in blackList with wildcard', () => { + expect(fileUploadIsValidContentTypeFromSettings('text/plain', '', 'image/*')).to.be.true; + }); +}); From d3ab41681e1b4061209987d1c1811ad8310707ea Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:50:11 +0200 Subject: [PATCH 12/26] chore: remove message column on moderation console (#32432) --- .../views/admin/moderation/ModerationConsoleTable.tsx | 10 ---------- .../admin/moderation/ModerationConsoleTableRow.tsx | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx index 6de7cdda1675..805d89c54c37 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx @@ -85,16 +85,6 @@ const ModerationConsoleTable: FC = () => { > {t('User')} , - - - {t('Moderation_Reported_message')} - , {t('Room')} , diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx index 56419c61223c..65bf7069e074 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx @@ -13,7 +13,7 @@ export type ModerationConsoleRowProps = { }; const ModerationConsoleTableRow = ({ report, onClick, isDesktopOrLarger }: ModerationConsoleRowProps): JSX.Element => { - const { userId: _id, rooms, name, count, message, username, ts } = report; + const { userId: _id, rooms, name, count, username, ts } = report; const roomNames = rooms.map((room) => { if (room.t === 'd') { @@ -31,7 +31,6 @@ const ModerationConsoleTableRow = ({ report, onClick, isDesktopOrLarger }: Moder - {message} {concatenatedRoomNames} {formatDateAndTime(ts)} {count} From eaf2f11a6c7fbd1635f777b88019355c3fb4f9be Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Mon, 17 Jun 2024 09:52:53 -0300 Subject: [PATCH 13/26] fix: sidebar last message E2EE (#32431) --- .changeset/lastmessage-e2ee.md | 6 +++ .../lib/server/functions/cleanRoomHistory.ts | 2 +- .../app/lib/server/functions/deleteMessage.ts | 2 +- apps/meteor/server/models/raw/Messages.ts | 9 ++-- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 45 +++++++++++++++++++ .../page-objects/fragments/home-sidenav.ts | 6 +++ .../src/models/IMessagesModel.ts | 2 +- 7 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 .changeset/lastmessage-e2ee.md diff --git a/.changeset/lastmessage-e2ee.md b/.changeset/lastmessage-e2ee.md new file mode 100644 index 000000000000..b3c8642dcff6 --- /dev/null +++ b/.changeset/lastmessage-e2ee.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Fixed last message preview in Sidebar for E2E Ecrypted channels diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index f53061995152..2bfb1086c635 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -111,7 +111,7 @@ export async function cleanRoomHistory({ } if (count) { - const lastMessage = await Messages.getLastVisibleMessageSentWithNoTypeByRoomId(rid); + const lastMessage = await Messages.getLastVisibleUserMessageSentByRoomId(rid); await Rooms.resetLastMessageById(rid, lastMessage, -count); diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index 4582a88823ec..e977874b3454 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -81,7 +81,7 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise implements IMessagesModel { return this.findOne(query, options); } - getLastVisibleMessageSentWithNoTypeByRoomId(rid: string, messageId?: string): Promise { - const query = { + getLastVisibleUserMessageSentByRoomId(rid: string, messageId?: string): Promise { + const query: Filter = { rid, _hidden: { $ne: true }, - t: { $exists: false }, - $or: [{ tmid: { $exists: false } }, { tshow: true }], + $or: [{ t: 'e2e' }, { t: { $exists: false }, tmid: { $exists: false } }, { t: { $exists: false }, tshow: true }], ...(messageId && { _id: { $ne: messageId } }), }; @@ -1055,7 +1054,7 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }, }; - return this.findOne(query, options); + return this.findOne(query, options); } async cloneAndSaveAsHistoryByRecord(record: IMessage, user: IMessage['u']): Promise> { diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index bb4d2f478219..c2d1fcc24cb7 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -265,6 +265,51 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); + 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, + }) => { + const channelName = faker.string.uuid(); + + // Enable Sidebar Extended display mode + await poHomeChannel.sidenav.setDisplayMode('Extended'); + + // Create private channel + await poHomeChannel.sidenav.openNewByLabel('Channel'); + await poHomeChannel.sidenav.inputChannelName.fill(channelName); + await poHomeChannel.sidenav.btnCreate.click(); + await expect(page).toHaveURL(`/group/${channelName}`); + await expect(poHomeChannel.toastSuccess).toBeVisible(); + await poHomeChannel.dismissToast(); + + // Send Unencrypted Messages + await poHomeChannel.content.sendMessage('first unencrypted message'); + await poHomeChannel.content.sendMessage('second unencrypted message'); + + // Encrypt channel + await poHomeChannel.tabs.kebab.click({ force: true }); + await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); + await poHomeChannel.tabs.btnEnableE2E.click({ force: true }); + await page.waitForTimeout(1000); + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + // Send Encrypted Messages + const encriptedMessage1 = 'first ENCRYPTED message'; + const encriptedMessage2 = 'second ENCRYPTED message'; + await poHomeChannel.content.sendMessage(encriptedMessage1); + await poHomeChannel.content.sendMessage(encriptedMessage2); + + // Delete last message + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText(encriptedMessage2); + await poHomeChannel.content.openLastMessageMenu(); + await page.locator('role=menuitem[name="Delete"]').click(); + await page.locator('#modal-root .rcx-button-group--align-end .rcx-button--danger').click(); + + // Check last message in the sidebar + const sidebarChannel = await poHomeChannel.sidenav.getSidebarItemByName(channelName); + await expect(sidebarChannel).toBeVisible(); + await expect(sidebarChannel.locator('span')).toContainText(encriptedMessage1); + }); + test.describe('reset keys', () => { let anotherClientPage: Page; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 3d689b27e3fc..dfae6c668bb9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -49,6 +49,12 @@ export class HomeSidenav { return this.page.getByRole('toolbar', { name: 'Sidebar actions' }); } + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { + await this.sidebarToolbar.getByRole('button', { name: 'Display' }).click(); + await this.sidebarToolbar.getByRole('menuitemcheckbox', { name: mode }).click(); + await this.sidebarToolbar.click(); + } + // Note: this is different from openChat because queued chats are not searchable getQueuedChat(name: string): Locator { return this.page.locator('[data-qa="sidebar-item-title"]', { hasText: name }).first(); diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts index 143080799671..0fb02a778c9d 100644 --- a/packages/model-typings/src/models/IMessagesModel.ts +++ b/packages/model-typings/src/models/IMessagesModel.ts @@ -205,7 +205,7 @@ export interface IMessagesModel extends IBaseModel { updateAllUsernamesByUserId(userId: string, username: string): Promise; setUrlsById(_id: string, urls: NonNullable): Promise; - getLastVisibleMessageSentWithNoTypeByRoomId(rid: string, messageId?: string): Promise; + getLastVisibleUserMessageSentByRoomId(rid: string, messageId?: string): Promise; findOneBySlackTs(slackTs: Date): Promise; From 9795e58c2cc9bbb40760706c16d88f57f42f554a Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 17 Jun 2024 11:46:46 -0300 Subject: [PATCH 14/26] fix: Force highlighted code language registration (#32507) --- .changeset/weak-books-tell.md | 5 ++ .../meteor/client/hooks/useHighlightedCode.ts | 16 +++++- .../admin/integrations/IntegrationsTable.tsx | 2 +- apps/meteor/tests/e2e/administration.spec.ts | 49 ++++++++++++++++++- apps/meteor/tests/e2e/page-objects/admin.ts | 40 ++++++++++++++- apps/meteor/tests/e2e/utils/index.ts | 1 + packages/i18n/src/locales/en.i18n.json | 1 + 7 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 .changeset/weak-books-tell.md diff --git a/.changeset/weak-books-tell.md b/.changeset/weak-books-tell.md new file mode 100644 index 000000000000..675901263f31 --- /dev/null +++ b/.changeset/weak-books-tell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Forces the highlight code language registration, preventing it to not being available when trying to use on the UI diff --git a/apps/meteor/client/hooks/useHighlightedCode.ts b/apps/meteor/client/hooks/useHighlightedCode.ts index 43c4a5e6ea58..4e3405ca3034 100644 --- a/apps/meteor/client/hooks/useHighlightedCode.ts +++ b/apps/meteor/client/hooks/useHighlightedCode.ts @@ -1,7 +1,19 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; -import hljs from '../../app/markdown/lib/hljs'; +import hljs, { register } from '../../app/markdown/lib/hljs'; export function useHighlightedCode(language: string, text: string): string { - return useMemo(() => hljs.highlight(language, text).value, [language, text]); + const t = useTranslation(); + const { isLoading } = useQuery(['register-highlight-language', language], async () => { + try { + await register(language); + return true; + } catch (error) { + console.error('Not possible to register the provided language'); + } + }); + + return useMemo(() => (isLoading ? t('Loading') : hljs.highlight(language, text).value), [isLoading, language, text, t]); } diff --git a/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx b/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx index 206458e09c9b..e2942f384c9d 100644 --- a/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx +++ b/apps/meteor/client/views/admin/integrations/IntegrationsTable.tsx @@ -107,7 +107,7 @@ const IntegrationsTable = ({ type }: { type?: string }) => { )} {isSuccess && data && data.integrations.length > 0 && ( <> - + {headers} {isSuccess && diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index 902d4f09b12c..e4af3c26c106 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -3,8 +3,7 @@ import { faker } from '@faker-js/faker'; import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; import { Admin, Utils } from './page-objects'; -import { createTargetChannel } from './utils'; -import { setSettingValueById } from './utils/setSettingValueById'; +import { createTargetChannel, setSettingValueById } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -275,6 +274,52 @@ test.describe.parallel('administration', () => { }); }); + test.describe('Integrations', () => { + const messageCodeHighlightDefault = + 'javascript,css,markdown,dockerfile,json,go,rust,clean,bash,plaintext,powershell,scss,shell,yaml,vim'; + const incomingIntegrationName = faker.string.uuid(); + + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'Message_Code_highlight', ''); + }); + + test.beforeEach(async ({ page }) => { + await page.goto('/admin/integrations'); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Message_Code_highlight', messageCodeHighlightDefault); + }); + + test('should display the example payload correctly', async () => { + await poAdmin.btnNew.click(); + await poAdmin.btnInstructions.click(); + + await expect(poAdmin.codeExamplePayload('Loading')).not.toBeVisible(); + }); + + test('should be able to create new incoming integration', async () => { + await poAdmin.btnNew.click(); + await poAdmin.inputName.fill(incomingIntegrationName); + await poAdmin.inputPostToChannel.fill('#general'); + await poAdmin.inputPostAs.fill(Users.admin.data.username); + await poAdmin.btnSave.click(); + + await expect(poAdmin.inputWebhookUrl).not.toHaveValue('Will be available here after saving.'); + + await poAdmin.btnBack.click(); + await expect(poAdmin.getIntegrationByName(incomingIntegrationName)).toBeVisible(); + }); + + test('should be able to delete an incoming integration', async () => { + await poAdmin.getIntegrationByName(incomingIntegrationName).click(); + await poAdmin.btnDelete.click(); + await poUtils.btnModalConfirmDelete.click(); + + await expect(poAdmin.getIntegrationByName(incomingIntegrationName)).not.toBeVisible(); + }); + }); + test.describe('Settings', () => { test.describe('General', () => { test.beforeEach(async ({ page }) => { diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index f3567652fe6e..210dbc95f326 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -213,11 +213,47 @@ export class Admin { return this.page.getByRole('button', { name: 'Add' }); } + getUserRowByUsername(username: string): Locator { + return this.page.locator('tr', { hasText: username }); + } + get btnBack(): Locator { return this.page.getByRole('button', { name: 'Back' }); } - getUserRowByUsername(username: string): Locator { - return this.page.locator('tr', { hasText: username }); + get btnNew(): Locator { + return this.page.getByRole('button', { name: 'New' }); + } + + get btnDelete(): Locator { + return this.page.getByRole('button', { name: 'Delete' }); + } + + get btnInstructions(): Locator { + return this.page.getByRole('button', { name: 'Instructions' }); + } + + get inputName(): Locator { + return this.page.getByRole('textbox', { name: 'Name' }); + } + + get inputPostToChannel(): Locator { + return this.page.getByRole('textbox', { name: 'Post to Channel' }); + } + + get inputPostAs(): Locator { + return this.page.getByRole('textbox', { name: 'Post as' }); + } + + codeExamplePayload(text: string): Locator { + return this.page.locator('code', { hasText: text }); + } + + getIntegrationByName(name: string): Locator { + return this.page.getByRole('table', { name: 'Integrations table' }).locator('tr', { hasText: name }); + } + + get inputWebhookUrl(): Locator { + return this.page.getByRole('textbox', { name: 'Webhook URL' }); } } diff --git a/apps/meteor/tests/e2e/utils/index.ts b/apps/meteor/tests/e2e/utils/index.ts index 9f83fc0ae246..e1daf601f648 100644 --- a/apps/meteor/tests/e2e/utils/index.ts +++ b/apps/meteor/tests/e2e/utils/index.ts @@ -1,2 +1,3 @@ export * from './create-target-channel'; export * from './setSettingValueById'; +export * from './getSettingValueById'; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index e9d1894ef74e..779c71c9d755 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2763,6 +2763,7 @@ "Integrations_Outgoing_Type_RoomLeft": "User Left Room", "Integrations_Outgoing_Type_SendMessage": "Message Sent", "Integrations_Outgoing_Type_UserCreated": "User Created", + "Integrations_table": "Integrations table", "InternalHubot": "Internal Hubot", "InternalHubot_EnableForChannels": "Enable for Public Channels", "InternalHubot_EnableForDirectMessages": "Enable for Direct Messages", From 59df102d0cdcb6968ef04fd5f683d3ef6af9b1f3 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 17 Jun 2024 15:16:17 -0300 Subject: [PATCH 15/26] fix: Long katex strings breaking overflow in x axis (#32609) --- .changeset/healthy-clouds-hide.md | 5 +++++ packages/gazzodown/src/katex/KatexBlock.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/healthy-clouds-hide.md diff --git a/.changeset/healthy-clouds-hide.md b/.changeset/healthy-clouds-hide.md new file mode 100644 index 000000000000..528a1bf27568 --- /dev/null +++ b/.changeset/healthy-clouds-hide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/gazzodown": patch +--- + +Fixes long katex lines overflowing the message component diff --git a/packages/gazzodown/src/katex/KatexBlock.tsx b/packages/gazzodown/src/katex/KatexBlock.tsx index 25e9c77dc863..5913185d3969 100644 --- a/packages/gazzodown/src/katex/KatexBlock.tsx +++ b/packages/gazzodown/src/katex/KatexBlock.tsx @@ -19,7 +19,7 @@ const KatexBlock = ({ code }: KatexBlockProps): ReactElement => { [code], ); - return
; + return
; }; export default KatexBlock; From 1c4b7024880c1ba6a77464a8bd24da0361113d43 Mon Sep 17 00:00:00 2001 From: Aaron Ogle Date: Mon, 17 Jun 2024 14:47:04 -0500 Subject: [PATCH 16/26] chore: Add telemetry to CI so we can get a better understanding of resource usage (#32113) --- .github/workflows/ci-test-e2e.yml | 7 +++++++ .github/workflows/ci-test-unit.yml | 6 ++++++ .github/workflows/ci.yml | 12 ++++++++++++ 3 files changed, 25 insertions(+) diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 1dc8993bfa87..b46c124d149b 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -90,6 +90,13 @@ jobs: name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'true' && ' [no watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }})${{ matrix.mongodb-version == '6.0' && ' - Alpine' || '' }} steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false + - name: Login to GitHub Container Registry if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') uses: docker/login-action@v2 diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 066cc2e3773e..cf28bbdfc01a 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -21,6 +21,12 @@ jobs: name: Unit Tests steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - uses: actions/checkout@v4 - name: Setup NodeJS diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b16ab459d6bd..b9aa8f616857 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,12 @@ jobs: runs-on: ubuntu-20.04 steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - name: Github Info run: | echo "GITHUB_ACTION: $GITHUB_ACTION" @@ -192,6 +198,12 @@ jobs: runs-on: ubuntu-20.04 steps: + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + theme: dark + job_summary: true + comment_on_pr: false - name: Github Info run: | echo "GITHUB_ACTION: $GITHUB_ACTION" From 465c8edff56ecbcd10c2f10be613cf1f25f106c7 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Tue, 18 Jun 2024 02:05:30 +0530 Subject: [PATCH 17/26] fix: E2EE thread main message reactivity (#32381) --- .changeset/three-squids-brake.md | 5 +++ .../hooks/useThreadMainMessageQuery.ts | 6 ++-- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 34 +++++++++++++++++++ .../page-objects/fragments/home-content.ts | 4 +++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 .changeset/three-squids-brake.md diff --git a/.changeset/three-squids-brake.md b/.changeset/three-squids-brake.md new file mode 100644 index 000000000000..89ed21f8048c --- /dev/null +++ b/.changeset/three-squids-brake.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed Encrypted thread main message reactivity issues. Earlier the encrypted thread main message was having some reactivity issues and flaky behavior. diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts index ecf2d54a45f7..76e80d774ca3 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts @@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { withDebouncing } from '../../../../../../lib/utils/highOrderFunctions'; import type { FieldExpression, Query } from '../../../../../lib/minimongo'; import { createFilterFromQuery } from '../../../../../lib/minimongo'; +import { onClientMessageReceived } from '../../../../../lib/onClientMessageReceived'; import { useRoom } from '../../../contexts/RoomContext'; import { useGetMessageByID } from './useGetMessageByID'; @@ -107,8 +108,9 @@ export const useThreadMainMessageQuery = ( unsubscribeRef.current = unsubscribeRef.current || subscribeToMessage(mainMessage, { - onMutate: (message) => { - queryClient.setQueryData(queryKey, () => message); + onMutate: async (message) => { + const msg = await onClientMessageReceived(message); + queryClient.setQueryData(queryKey, () => msg); debouncedInvalidate(); }, onDelete: () => { diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index c2d1fcc24cb7..69b77d1ceffb 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -179,6 +179,40 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); + test('expect create a private encrypted channel and send a encrypted thread message', 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 the thread main message.'); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is the thread main message.'); + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + + await page.locator('[data-qa-type="message"]').last().hover(); + await page.locator('role=button[name="Reply in thread"]').click(); + + await expect(page).toHaveURL(/.*thread/); + + await expect(poHomeChannel.content.mainThreadMessageText).toContainText('This is the thread main message.'); + await expect(poHomeChannel.content.mainThreadMessageText.locator('.rcx-icon--name-key')).toBeVisible(); + + await poHomeChannel.content.toggleAlsoSendThreadToChannel(true); + await page.getByRole('dialog').locator('[name="msg"]').last().fill('This is an encrypted thread message also sent in channel'); + await page.keyboard.press('Enter'); + await expect(poHomeChannel.content.lastThreadMessageText).toContainText('This is an encrypted thread message also sent in channel'); + await expect(poHomeChannel.content.lastThreadMessageText.locator('.rcx-icon--name-key')).toBeVisible(); + await expect(poHomeChannel.content.lastUserMessage).toContainText('This is an encrypted thread message also sent in channel'); + await expect(poHomeChannel.content.mainThreadMessageText).toContainText('This is the thread main message.'); + await expect(poHomeChannel.content.mainThreadMessageText.locator('.rcx-icon--name-key')).toBeVisible(); + }); + test('expect create a private channel, encrypt it and send an encrypted message', 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 02f7dde09a85..786c5217dadf 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -143,6 +143,10 @@ export class HomeContent { return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('.rcx-attachment__details'); } + get mainThreadMessageText(): Locator { + return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').first(); + } + get lastThreadMessageText(): Locator { return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last(); } From 2ef71e8ea6793a59af45a8089b04177a2f48d153 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Tue, 18 Jun 2024 02:58:53 +0530 Subject: [PATCH 18/26] feat: E2EE room setup header (#32446) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Co-authored-by: Hugo Costa <20212776+hugocostadev@users.noreply.github.com> --- .changeset/gold-flowers-shake.md | 6 +++ apps/meteor/client/ui.ts | 2 + .../client/views/room/Header/Header.tsx | 24 +++++++---- .../client/views/room/Header/RoomHeader.tsx | 5 ++- .../views/room/Header/RoomHeaderE2EESetup.tsx | 29 +++++++++++++ .../RoomToolbox/RoomToolboxE2EESetup.tsx | 41 +++++++++++++++++++ apps/meteor/tests/e2e/e2e-encryption.spec.ts | 23 +++++++++-- .../page-objects/fragments/home-flextab.ts | 4 ++ 8 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 .changeset/gold-flowers-shake.md create mode 100644 apps/meteor/client/views/room/Header/RoomHeaderE2EESetup.tsx create mode 100644 apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx diff --git a/.changeset/gold-flowers-shake.md b/.changeset/gold-flowers-shake.md new file mode 100644 index 000000000000..26182d785c22 --- /dev/null +++ b/.changeset/gold-flowers-shake.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Added E2EE room setup header, with just limited functionality and room actions. diff --git a/apps/meteor/client/ui.ts b/apps/meteor/client/ui.ts index 98d3233134a5..6c7971a8cca0 100644 --- a/apps/meteor/client/ui.ts +++ b/apps/meteor/client/ui.ts @@ -78,3 +78,5 @@ export const quickActionHooks = [ useCloseChatQuickAction, useOnHoldChatQuickAction, ] satisfies (() => QuickActionsActionConfig | undefined)[]; + +export const roomActionHooksForE2EESetup = [useChannelSettingsRoomAction, useMembersListRoomAction, useE2EERoomAction]; diff --git a/apps/meteor/client/views/room/Header/Header.tsx b/apps/meteor/client/views/room/Header/Header.tsx index bd5537e0098f..c350544e8157 100644 --- a/apps/meteor/client/views/room/Header/Header.tsx +++ b/apps/meteor/client/views/room/Header/Header.tsx @@ -1,15 +1,16 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { isVoipRoom } from '@rocket.chat/core-typings'; +import { isDirectMessageRoom, isVoipRoom } from '@rocket.chat/core-typings'; import { HeaderToolbar } from '@rocket.chat/ui-client'; -import { useLayout } from '@rocket.chat/ui-contexts'; +import { useLayout, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { lazy, memo, useMemo } from 'react'; import SidebarToggler from '../../../components/SidebarToggler'; -const DirectRoomHeader = lazy(() => import('./DirectRoomHeader')); const OmnichannelRoomHeader = lazy(() => import('./Omnichannel/OmnichannelRoomHeader')); const VoipRoomHeader = lazy(() => import('./Omnichannel/VoipRoomHeader')); +const RoomHeaderE2EESetup = lazy(() => import('./RoomHeaderE2EESetup')); +const DirectRoomHeader = lazy(() => import('./DirectRoomHeader')); const RoomHeader = lazy(() => import('./RoomHeader')); type HeaderProps = { @@ -18,6 +19,9 @@ type HeaderProps = { const Header = ({ room }: HeaderProps): ReactElement | null => { const { isMobile, isEmbedded, showTopNavbarEmbeddedLayout } = useLayout(); + const encrypted = Boolean(room.encrypted); + const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages'); + const shouldDisplayE2EESetup = encrypted && !unencryptedMessagesAllowed; const slots = useMemo( () => ({ @@ -34,10 +38,6 @@ const Header = ({ room }: HeaderProps): ReactElement | null => { return null; } - if (room.t === 'd' && (room.uids?.length ?? 0) < 3) { - return ; - } - if (room.t === 'l') { return ; } @@ -46,7 +46,15 @@ const Header = ({ room }: HeaderProps): ReactElement | null => { return ; } - return ; + if (shouldDisplayE2EESetup) { + return ; + } + + if (isDirectMessageRoom(room) && (room.uids?.length ?? 0) < 3) { + return ; + } + + return ; }; export default memo(Header); diff --git a/apps/meteor/client/views/room/Header/RoomHeader.tsx b/apps/meteor/client/views/room/Header/RoomHeader.tsx index 05f80a984982..fee9be6a55a6 100644 --- a/apps/meteor/client/views/room/Header/RoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/RoomHeader.tsx @@ -30,9 +30,10 @@ export type RoomHeaderProps = { pos?: unknown; }; }; + roomToolbox?: JSX.Element; }; -const RoomHeader = ({ room, topic = '', slots = {} }: RoomHeaderProps) => { +const RoomHeader = ({ room, topic = '', slots = {}, roomToolbox }: RoomHeaderProps) => { const t = useTranslation(); return ( @@ -65,7 +66,7 @@ const RoomHeader = ({ room, topic = '', slots = {} }: RoomHeaderProps) => { {slots?.toolbox?.pre} - {slots?.toolbox?.content || } + {slots?.toolbox?.content || roomToolbox || } {slots?.toolbox?.pos} diff --git a/apps/meteor/client/views/room/Header/RoomHeaderE2EESetup.tsx b/apps/meteor/client/views/room/Header/RoomHeaderE2EESetup.tsx new file mode 100644 index 000000000000..2b868c28882d --- /dev/null +++ b/apps/meteor/client/views/room/Header/RoomHeaderE2EESetup.tsx @@ -0,0 +1,29 @@ +import { isDirectMessageRoom } from '@rocket.chat/core-typings'; +import React, { lazy } from 'react'; + +import { E2EEState } from '../../../../app/e2e/client/E2EEState'; +import { E2ERoomState } from '../../../../app/e2e/client/E2ERoomState'; +import { useE2EERoomState } from '../hooks/useE2EERoomState'; +import { useE2EEState } from '../hooks/useE2EEState'; +import DirectRoomHeader from './DirectRoomHeader'; +import RoomHeader from './RoomHeader'; +import type { RoomHeaderProps } from './RoomHeader'; + +const RoomToolboxE2EESetup = lazy(() => import('./RoomToolbox/RoomToolboxE2EESetup')); + +const RoomHeaderE2EESetup = ({ room, topic = '', slots = {} }: RoomHeaderProps) => { + const e2eeState = useE2EEState(); + const e2eRoomState = useE2EERoomState(room._id); + + if (e2eeState === E2EEState.SAVE_PASSWORD || e2eeState === E2EEState.ENTER_PASSWORD || e2eRoomState === E2ERoomState.WAITING_KEYS) { + return } />; + } + + if (isDirectMessageRoom(room) && (room.uids?.length ?? 0) < 3) { + return ; + } + + return ; +}; + +export default RoomHeaderE2EESetup; diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx new file mode 100644 index 000000000000..9b79dff5a6bd --- /dev/null +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx @@ -0,0 +1,41 @@ +import { useStableArray } from '@rocket.chat/fuselage-hooks'; +import { HeaderToolbarAction } from '@rocket.chat/ui-client'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import { roomActionHooksForE2EESetup } from '../../../../ui'; +import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; +import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; + +const RoomToolboxE2EESetup = () => { + const t = useTranslation(); + const toolbox = useRoomToolbox(); + + const { tab } = toolbox; + + const actions = useStableArray( + roomActionHooksForE2EESetup + .map((roomActionHook) => roomActionHook()) + .filter((roomAction): roomAction is RoomToolboxActionConfig => !!roomAction), + ); + + return ( + <> + {actions.map(({ id, icon, title, action, disabled, tooltip }, index) => ( + toolbox.openTab(id))} + disabled={disabled} + tooltip={tooltip} + /> + ))} + + ); +}; + +export default RoomToolboxE2EESetup; diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 69b77d1ceffb..8b0753c952d2 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -429,11 +429,17 @@ test.describe.serial('e2ee room setup', () => { await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + await poHomeChannel.content.encryptedRoomHeaderIcon.first().waitFor(); + await expect(poHomeChannel.content.encryptedRoomHeaderIcon.first()).toBeVisible(); await page.locator('role=button[name="Save E2EE password"]').waitFor(); await expect(page.locator('role=button[name="Save E2EE password"]')).toBeVisible(); + await poHomeChannel.tabs.btnE2EERoomSetupDisableE2E.waitFor(); + await expect(poHomeChannel.tabs.btnE2EERoomSetupDisableE2E).toBeVisible(); + await expect(poHomeChannel.tabs.btnTabMembers).toBeVisible(); + await expect(poHomeChannel.tabs.btnRoomInfo).toBeVisible(); + await expect(poHomeChannel.content.inputMessage).not.toBeVisible(); await page.locator('role=button[name="Save E2EE password"]').click(); @@ -477,11 +483,17 @@ test.describe.serial('e2ee room setup', () => { await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + await expect(poHomeChannel.content.encryptedRoomHeaderIcon.first()).toBeVisible(); await page.locator('role=button[name="Enter your E2E password"]').waitFor(); await expect(page.locator('role=banner >> text="Enter your E2E password"')).toBeVisible(); + + await poHomeChannel.tabs.btnE2EERoomSetupDisableE2E.waitFor(); + await expect(poHomeChannel.tabs.btnE2EERoomSetupDisableE2E).toBeVisible(); + await expect(poHomeChannel.tabs.btnTabMembers).toBeVisible(); + await expect(poHomeChannel.tabs.btnRoomInfo).toBeVisible(); + await expect(poHomeChannel.content.inputMessage).not.toBeVisible(); await page.locator('role=button[name="Enter your E2E password"]').click(); @@ -518,7 +530,7 @@ test.describe.serial('e2ee room setup', () => { await poHomeChannel.dismissToast(); - await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + await expect(poHomeChannel.content.encryptedRoomHeaderIcon.first()).toBeVisible(); await poHomeChannel.content.sendMessage('hello world'); @@ -551,5 +563,10 @@ test.describe.serial('e2ee room setup', () => { await expect(poHomeChannel.content.inputMessage).not.toBeVisible(); await expect(page.locator('.rcx-states__title')).toContainText('Check back later'); + + await poHomeChannel.tabs.btnE2EERoomSetupDisableE2E.waitFor(); + await expect(poHomeChannel.tabs.btnE2EERoomSetupDisableE2E).toBeVisible(); + await expect(poHomeChannel.tabs.btnTabMembers).toBeVisible(); + await expect(poHomeChannel.tabs.btnRoomInfo).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts index b8bd673c9cf3..a6d91bc6fc34 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts @@ -44,6 +44,10 @@ export class HomeFlextab { return this.page.locator('role=menuitem[name="Notifications Preferences"]'); } + get btnE2EERoomSetupDisableE2E(): Locator { + return this.page.locator('[data-qa-id=ToolBoxAction-key]'); + } + get btnDisableE2E(): Locator { return this.page.locator('role=menuitem[name="Disable E2E"]'); } From 4f72d62aa73b12456280861be884c99a1e12c2a1 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Mon, 17 Jun 2024 19:17:08 -0300 Subject: [PATCH 19/26] feat: Apps-Engine Deno Runtime update (#31821) Co-authored-by: Rafael Tapia <18501599+tapiarafael@users.noreply.github.com> --- .changeset/tame-weeks-shout.md | 41 +++ .github/actions/setup-node/action.yml | 8 + apps/meteor/.docker/Dockerfile | 25 +- apps/meteor/.docker/Dockerfile.alpine | 73 ++++- .../app/apps/server/bridges/commands.ts | 8 +- apps/meteor/app/apps/server/bridges/http.ts | 94 +++--- .../app/metrics/server/lib/collectMetrics.ts | 2 +- .../server/lib/getAppsStatistics.js | 18 - .../server/lib/getAppsStatistics.ts | 51 +++ .../app/statistics/server/lib/statistics.ts | 2 +- .../ee/lib/misc/formatAppInstanceForRest.ts | 4 +- .../endpoints/appsCountHandler.ts | 4 +- .../ee/server/apps/communication/rest.ts | 26 +- .../server/apps/communication/websockets.ts | 4 +- apps/meteor/ee/server/apps/cron.ts | 2 +- apps/meteor/ee/server/apps/orchestrator.js | 49 +-- .../server/apps/storage/AppRealLogStorage.ts | 36 ++ apps/meteor/ee/server/apps/storage/index.js | 2 +- .../ee/server/apps/storage/logs-storage.js | 32 -- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- .../server/services/apps-engine/service.ts | 7 +- .../apps/02-send-messages-as-user.js | 2 +- docker-compose-ci.yml | 1 + ee/apps/ddp-streamer/package.json | 2 +- ee/packages/presence/package.json | 2 +- packages/apps/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- .../model-typings/src/models/IAppLogsModel.ts | 3 + packages/rest-typings/package.json | 2 +- yarn.lock | 310 +++++++++++++++++- 33 files changed, 622 insertions(+), 200 deletions(-) create mode 100644 .changeset/tame-weeks-shout.md delete mode 100644 apps/meteor/app/statistics/server/lib/getAppsStatistics.js create mode 100644 apps/meteor/app/statistics/server/lib/getAppsStatistics.ts create mode 100644 apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts delete mode 100644 apps/meteor/ee/server/apps/storage/logs-storage.js diff --git a/.changeset/tame-weeks-shout.md b/.changeset/tame-weeks-shout.md new file mode 100644 index 000000000000..72bfc864274f --- /dev/null +++ b/.changeset/tame-weeks-shout.md @@ -0,0 +1,41 @@ +--- +'@rocket.chat/omnichannel-services': minor +'rocketchat-services': minor +'@rocket.chat/omnichannel-transcript': minor +'@rocket.chat/authorization-service': minor +'@rocket.chat/web-ui-registration': minor +'@rocket.chat/stream-hub-service': minor +'@rocket.chat/uikit-playground': minor +'@rocket.chat/presence-service': minor +'@rocket.chat/fuselage-ui-kit': minor +'@rocket.chat/instance-status': minor +'@rocket.chat/account-service': minor +'@rocket.chat/mock-providers': minor +'@rocket.chat/api-client': minor +'@rocket.chat/ddp-client': minor +'@rocket.chat/pdf-worker': minor +'@rocket.chat/ui-theming': minor +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/ui-video-conf': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ddp-streamer': minor +'@rocket.chat/queue-worker': minor +'@rocket.chat/presence': minor +'@rocket.chat/ui-composer': minor +'@rocket.chat/ui-contexts': minor +'@rocket.chat/license': minor +'@rocket.chat/gazzodown': minor +'@rocket.chat/ui-avatar': minor +'@rocket.chat/ui-client': minor +'@rocket.chat/livechat': minor +'@rocket.chat/models': minor +'@rocket.chat/ui-kit': minor +'@rocket.chat/apps': minor +'@rocket.chat/cron': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +New runtime for apps in the Apps-Engine based on the Deno platform diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 0e921e81f1f3..caa3c63e00f0 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -10,6 +10,10 @@ inputs: install: required: false type: boolean + deno-dir: + required: false + type: string + default: ~/.deno-cache outputs: node-version: @@ -19,6 +23,9 @@ runs: using: composite steps: + - run: echo 'DENO_DIR=${{ inputs.deno-dir }}' >> $GITHUB_ENV + shell: bash + - name: Cache Node Modules if: inputs.cache-modules id: cache-node-modules @@ -26,6 +33,7 @@ runs: with: path: | node_modules + ${{ env.DENO_DIR }} apps/meteor/node_modules apps/meteor/ee/server/services/node_modules key: node-modules-${{ hashFiles('yarn.lock') }} diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile index 456ed4becafd..1e9ed3f5e592 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile @@ -13,12 +13,24 @@ RUN groupadd -g 65533 -r rocketchat \ # --chown requires Docker 17.12 and works only on Linux ADD --chown=rocketchat:rocketchat . /app +# needs a mongoinstance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads \ + DENO_DIR=/usr/share/deno + RUN aptMark="$(apt-mark showmanual)" \ && apt-get install -y --no-install-recommends g++ make python3 ca-certificates \ && cd /app/bundle/programs/server \ && npm install \ - && cd npm/node_modules/isolated-vm \ - && npm install \ + && cd npm/node_modules/isolated-vm \ + && npm install \ + && cd /app/bundle/programs/server/npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ + && ../../../deno-bin/bin/deno cache main.ts \ && apt-mark auto '.*' > /dev/null \ && apt-mark manual $aptMark > /dev/null \ && find /usr/local -type f -executable -exec ldd '{}' ';' \ @@ -37,15 +49,6 @@ VOLUME /app/uploads WORKDIR /app/bundle -# needs a mongoinstance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - EXPOSE 3000 CMD ["node", "main.js"] diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 94baef809217..feebf76a03e7 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,13 +1,68 @@ FROM node:14.21.3-alpine3.16 -RUN apk add --no-cache ttf-dejavu +ENV LANG=C.UTF-8 + +# Installing glibc deps required by Deno +# This replaces libc6-compat +# Copied from https://github.com/Docker-Hub-frolvlad/docker-alpine-glibc, which denoland/deno:alpine-1.37.1 uses +# NOTE: Glibc 2.35 package is broken: https://github.com/sgerrand/alpine-pkg-glibc/issues/176, so we stick to 2.34 for now +RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \ + ALPINE_GLIBC_PACKAGE_VERSION="2.34-r0" && \ + ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ + apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \ + echo \ + "-----BEGIN PUBLIC KEY-----\ + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\ + y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\ + tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\ + m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\ + KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\ + Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\ + 1QIDAQAB\ + -----END PUBLIC KEY-----" | sed 's/ */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \ + wget \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + mv /etc/nsswitch.conf /etc/nsswitch.conf.bak && \ + apk add --no-cache --force-overwrite \ + "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + \ + mv /etc/nsswitch.conf.bak /etc/nsswitch.conf && \ + rm "/etc/apk/keys/sgerrand.rsa.pub" && \ + (/usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true) && \ + echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \ + \ + apk del glibc-i18n && \ + \ + rm "/root/.wget-hsts" && \ + apk del .build-dependencies && \ + rm \ + "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ + "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ + apk add --no-cache ttf-dejavu ADD . /app LABEL maintainer="buildmaster@rocket.chat" +# needs a mongo instance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads \ + DENO_DIR=/usr/share/deno + RUN set -x \ - && apk add --no-cache --virtual .fetch-deps python3 make g++ libc6-compat \ + && apk add --no-cache --virtual .fetch-deps python3 make g++ \ && cd /app/bundle/programs/server \ && npm install --production \ # Start hack for sharp... @@ -20,20 +75,14 @@ RUN set -x \ && npm install isolated-vm@4.4.2 \ && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ # End hack for isolated-vm - && cd npm \ + # Cache Deno dependencies for Apps-Engine + && cd npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ + && /app/bundle/programs/server/npm/node_modules/deno-bin/bin/deno cache main.ts \ + && cd /app/bundle/programs/server/npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force \ && apk del .fetch-deps -# needs a mongo instance - defaults to container linking with alias 'mongo' -ENV DEPLOY_METHOD=docker \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - VOLUME /app/uploads WORKDIR /app/bundle diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index 5e018c51de89..5ffef6473051 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -111,8 +111,8 @@ export class AppCommandsBridge extends CommandBridge { permission: command.permission, callback: this._appCommandExecutor.bind(this), providesPreview: command.providesPreview, - previewer: !command.previewer ? undefined : this._appCommandPreviewer.bind(this), - previewCallback: (!command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this)) as + previewer: command.providesPreview ? this._appCommandPreviewer.bind(this) : undefined, + previewCallback: (command.providesPreview ? this._appCommandPreviewExecutor.bind(this) : undefined) as | (typeof slashCommands.commands)[string]['previewCallback'] | undefined, } as SlashCommand; @@ -155,10 +155,6 @@ export class AppCommandsBridge extends CommandBridge { if (typeof command.providesPreview !== 'boolean') { throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); } - - if (typeof command.executor !== 'function') { - throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); - } } private async _appCommandExecutor({ command, message, params, triggerId, userId }: SlashCommandCallbackParams): Promise { diff --git a/apps/meteor/app/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index 1535a18823c5..9d62769336a2 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -72,55 +72,51 @@ export class AppHttpBridge extends HttpBridge { this.orch.debugLog(`The App ${info.appId} is requesting from the outter webs:`, info); - try { - const response = await fetch( - url.href, - { - method, - body: content, - headers, - timeout, - }, - (request.hasOwnProperty('strictSSL') && !request.strictSSL) || - (request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized), - ); - - const result: IHttpResponse = { - url: info.url, - method: info.method, - statusCode: response.status, - headers: Object.fromEntries(response.headers as unknown as any), - }; - - const body = Buffer.from(await response.arrayBuffer()); - - if (request.encoding === null) { - /** - * The property `content` is not appropriately typed in the - * Apps-engine definition, and we can't simply change it there - * as it would be a breaking change. Thus, we're left with this - * type assertion. - */ - result.content = body as any; - } else { - result.content = body.toString(request.encoding as BufferEncoding); - result.data = ((): any => { - const contentType = (response.headers.get('content-type') || '').split(';')[0]; - if (!['application/json', 'text/javascript', 'application/javascript', 'application/x-javascript'].includes(contentType)) { - return null; - } - - try { - return JSON.parse(result.content); - } catch { - return null; - } - })(); - } - - return result; - } catch (e: any) { - return e.response; + const response = await fetch( + url.href, + { + method, + body: content, + headers, + timeout, + }, + (request.hasOwnProperty('strictSSL') && !request.strictSSL) || + (request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized), + ); + + const result: IHttpResponse = { + url: info.url, + method: info.method, + statusCode: response.status, + headers: Object.fromEntries(response.headers as unknown as any), + }; + + const body = Buffer.from(await response.arrayBuffer()); + + if (request.encoding === null) { + /** + * The property `content` is not appropriately typed in the + * Apps-engine definition, and we can't simply change it there + * as it would be a breaking change. Thus, we're left with this + * type assertion. + */ + result.content = body as any; + } else { + result.content = body.toString(request.encoding as BufferEncoding); + result.data = ((): any => { + const contentType = (response.headers.get('content-type') || '').split(';')[0]; + if (!['application/json', 'text/javascript', 'application/javascript', 'application/x-javascript'].includes(contentType)) { + return null; + } + + try { + return JSON.parse(result.content); + } catch { + return null; + } + })(); } + + return result; } } diff --git a/apps/meteor/app/metrics/server/lib/collectMetrics.ts b/apps/meteor/app/metrics/server/lib/collectMetrics.ts index 136686f49c9e..978b3d59ec98 100644 --- a/apps/meteor/app/metrics/server/lib/collectMetrics.ts +++ b/apps/meteor/app/metrics/server/lib/collectMetrics.ts @@ -39,7 +39,7 @@ const setPrometheusData = async (): Promise => { metrics.ddpConnectedUsers.set(_.unique(authenticatedSessions.map((s) => s.userId)).length); // Apps metrics - const { totalInstalled, totalActive, totalFailed } = getAppsStatistics(); + const { totalInstalled, totalActive, totalFailed } = await getAppsStatistics(); metrics.totalAppsInstalled.set(totalInstalled || 0); metrics.totalAppsEnabled.set(totalActive || 0); diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js b/apps/meteor/app/statistics/server/lib/getAppsStatistics.js deleted file mode 100644 index 1d84bead3e85..000000000000 --- a/apps/meteor/app/statistics/server/lib/getAppsStatistics.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Apps } from '@rocket.chat/apps'; -import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; - -import { Info } from '../../../utils/rocketchat.info'; - -export function getAppsStatistics() { - return { - engineVersion: Info.marketplaceApiVersion, - totalInstalled: (Apps.self?.isInitialized() && Apps.getManager().get().length) ?? 0, - totalActive: (Apps.self?.isInitialized() && Apps.getManager().get({ enabled: true }).length) ?? 0, - totalFailed: - (Apps.self?.isInitialized() && - Apps.getManager() - .get({ disabled: true }) - .filter(({ app: { status } }) => status !== AppStatus.MANUALLY_DISABLED).length) ?? - 0, - }; -} diff --git a/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts new file mode 100644 index 000000000000..930fc15a9c55 --- /dev/null +++ b/apps/meteor/app/statistics/server/lib/getAppsStatistics.ts @@ -0,0 +1,51 @@ +import { Apps } from '@rocket.chat/apps'; +import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; + +import { Info } from '../../../utils/rocketchat.info'; + +export type AppsStatistics = { + engineVersion: string; + totalInstalled: number | false; + totalActive: number | false; + totalFailed: number | false; +}; + +export async function getAppsStatistics(): Promise { + if (!Apps.self?.isInitialized()) { + return { + engineVersion: Info.marketplaceApiVersion, + totalInstalled: false, + totalActive: false, + totalFailed: false, + }; + } + + const apps = await Apps.getManager().get(); + + let totalInstalled = 0; + let totalActive = 0; + let totalFailed = 0; + + await Promise.all( + apps.map(async (app) => { + totalInstalled++; + + const status = await app.getStatus(); + + if (status === AppStatus.MANUALLY_DISABLED) { + totalFailed++; + } + + if (AppStatusUtils.isEnabled(status)) { + totalActive++; + } + }), + ); + + return { + engineVersion: Info.marketplaceApiVersion, + totalInstalled, + totalActive, + totalFailed, + }; +} diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index a6fcc5b17b5b..cff2aaefcc5a 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -467,7 +467,7 @@ export const statistics = { }), ); - statistics.apps = getAppsStatistics(); + statistics.apps = await getAppsStatistics(); statistics.services = await getServicesStatistics(); statistics.importer = getImporterStatistics(); statistics.videoConf = await VideoConf.getStatistics(); diff --git a/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts b/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts index d76b7ae59aeb..bf096122c50f 100644 --- a/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts +++ b/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts @@ -14,10 +14,10 @@ interface IAppInfoRest extends IAppInfo { migrated: boolean; } -export function formatAppInstanceForRest(app: ProxiedApp): IAppInfoRest { +export async function formatAppInstanceForRest(app: ProxiedApp): Promise { const appRest: IAppInfoRest = { ...app.getInfo(), - status: app.getStatus(), + status: await app.getStatus(), languages: app.getStorageItem().languageContent, private: getInstallationSourceFromAppStorageItem(app.getStorageItem()) === 'private', migrated: !!app.getStorageItem().migrated, diff --git a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts index fc436b8229cf..878dd9aab92c 100644 --- a/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts +++ b/apps/meteor/ee/server/apps/communication/endpoints/appsCountHandler.ts @@ -19,10 +19,10 @@ export const appsCountHandler = (apiManager: AppsRestApi) => authRequired: false, }, { - get(): SuccessResult { + async get(): Promise> { const manager = apiManager._manager as AppManager; - const apps = manager.get({ enabled: true }); + const apps = await manager.get({ enabled: true }); const { maxMarketplaceApps, maxPrivateApps } = License.getAppsConfig(); return API.v1.success({ diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index df30cccc8e73..02f8aeb7b344 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -209,8 +209,9 @@ export class AppsRestApi { { authRequired: true }, { async get() { - const apps = manager.get().map(formatAppInstanceForRest); - return API.v1.success({ apps }); + const apps = await manager.get(); + const formatted = await Promise.all(apps.map(formatAppInstanceForRest)); + return API.v1.success({ apps: formatted }); }, }, ); @@ -302,7 +303,8 @@ export class AppsRestApi { } apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, 'Use /apps/installed to get the installed apps list.'); - const apps = manager.get().map(formatAppInstanceForRest); + const proxiedApps = await manager.get(); + const apps = await Promise.all(proxiedApps.map(formatAppInstanceForRest)); return API.v1.success({ apps }); }, @@ -412,7 +414,7 @@ export class AppsRestApi { }); } - info.status = aff.getApp().getStatus(); + info.status = await aff.getApp().getStatus(); void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'install', info); @@ -508,8 +510,8 @@ export class AppsRestApi { 'languages', { authRequired: false }, { - get() { - const apps = manager.get().map((prl) => ({ + async get() { + const apps = (await manager.get()).map((prl) => ({ id: prl.getID(), languages: prl.getStorageItem().languageContent, })); @@ -760,7 +762,7 @@ export class AppsRestApi { }); } - info.status = aff.getApp().getStatus(); + info.status = await aff.getApp().getStatus(); void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'update', info); @@ -784,10 +786,14 @@ export class AppsRestApi { ?.get('users') .convertToApp(await Meteor.userAsync()); - await manager.remove(prl.getID(), { user }); - const info: IAppInfo & { status?: AppStatus } = prl.getInfo(); - info.status = prl.getStatus(); + try { + await manager.remove(prl.getID(), { user }); + info.status = AppStatus.DISABLED; + } catch (e) { + info.status = await prl.getStatus(); + return API.v1.failure({ app: info }); + } void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'uninstall', info); diff --git a/apps/meteor/ee/server/apps/communication/websockets.ts b/apps/meteor/ee/server/apps/communication/websockets.ts index 65a2a73ca063..83a161427143 100644 --- a/apps/meteor/ee/server/apps/communication/websockets.ts +++ b/apps/meteor/ee/server/apps/communication/websockets.ts @@ -51,7 +51,7 @@ export class AppServerListener { async onAppStatusUpdated({ appId, status }: { appId: string; status: AppStatus }): Promise { const app = this.orch.getManager()?.getOneById(appId); - if (!app || app.getStatus() === status) { + if (!app || (await app.getStatus()) === status) { return; } @@ -76,10 +76,12 @@ export class AppServerListener { setting, when: new Date(), }); + await this.orch .getManager()! .getSettingsManager() .updateAppSetting(appId, setting as any); // TO-DO: fix type of `setting` + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId, setting }); } diff --git a/apps/meteor/ee/server/apps/cron.ts b/apps/meteor/ee/server/apps/cron.ts index e58610d2cbf0..f904486bf610 100644 --- a/apps/meteor/ee/server/apps/cron.ts +++ b/apps/meteor/ee/server/apps/cron.ts @@ -57,7 +57,7 @@ const notifyAdminsAboutRenewedApps = async function _notifyAdminsAboutRenewedApp } const renewedApps = apps.filter( - (app) => app.getStatus() === AppStatus.DISABLED && app.getPreviousStatus() === AppStatus.INVALID_LICENSE_DISABLED, + async (app) => (await app.getStatus()) === AppStatus.DISABLED && app.getPreviousStatus() === AppStatus.INVALID_LICENSE_DISABLED, ); if (renewedApps.length === 0) { diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index 37c31e890e89..c252579138cb 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -21,7 +21,7 @@ import { AppThreadsConverter } from '../../../app/apps/server/converters/threads import { settings } from '../../../app/settings/server'; import { canEnableApp } from '../../app/license/server/canEnableApp'; import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; -import { AppRealLogsStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; +import { AppRealLogStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; function isTesting() { return process.env.TEST_MODE === 'true'; @@ -54,7 +54,7 @@ export class AppServerOrchestrator { this._logModel = AppLogs; this._persistModel = AppsPersistence; this._storage = new AppRealStorage(this._model); - this._logStorage = new AppRealLogsStorage(this._logModel); + this._logStorage = new AppRealLogStorage(this._logModel); this._appSourceStorage = new ConfigurableAppSourceStorage(appsSourceStorageType, appsSourceStorageFilesystemPath); this._converters = new Map(); @@ -172,34 +172,35 @@ export class AppServerOrchestrator { await this.getManager().load(); // Before enabling each app we verify if there is still room for it - await this.getManager() - .get() - // We reduce everything to a promise chain so it runs sequentially - .reduce( - (control, app) => - control.then(async () => { - const canEnable = await canEnableApp(app.getStorageItem()); - - if (canEnable) { - return this.getManager().loadOne(app.getID()); - } - - this._rocketchatLogger.warn(`App "${app.getInfo().name}" can't be enabled due to CE limits.`); - }), - Promise.resolve(), - ); + const apps = await this.getManager().get(); + + /* eslint-disable no-await-in-loop */ + // This needs to happen sequentially to keep track of app limits + for (const app of apps) { + const canEnable = await canEnableApp(app.getStorageItem()); + + if (!canEnable) { + this._rocketchatLogger.warn(`App "${app.getInfo().name}" can't be enabled due to CE limits.`); + // We need to continue as the limits are applied depending on the app installation source + // i.e. if one limit is hit, we can't break the loop as the following apps might still be valid + continue; + } + + await this.getManager().loadOne(app.getID()); + } + /* eslint-enable no-await-in-loop */ await this.getBridges().getSchedulerBridge().startScheduler(); - this._rocketchatLogger.info(`Loaded the Apps Framework and loaded a total of ${this.getManager().get({ enabled: true }).length} Apps!`); + const appCount = (await this.getManager().get({ enabled: true })).length; + + this._rocketchatLogger.info(`Loaded the Apps Framework and loaded a total of ${appCount} Apps!`); } async disableApps() { - await this.getManager() - .get() - .forEach((app) => { - this.getManager().disable(app.getID()); - }); + const apps = await this.getManager().get(); + + await Promise.all(apps.map((app) => this.getManager().disable(app.getID()))); } async unload() { diff --git a/apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts b/apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts new file mode 100644 index 000000000000..17a8f2f838aa --- /dev/null +++ b/apps/meteor/ee/server/apps/storage/AppRealLogStorage.ts @@ -0,0 +1,36 @@ +import type { ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging'; +import type { IAppLogStorageFindOptions } from '@rocket.chat/apps-engine/server/storage'; +import { AppLogStorage } from '@rocket.chat/apps-engine/server/storage'; +import { InstanceStatus } from '@rocket.chat/instance-status'; +import type { AppLogs } from '@rocket.chat/models'; + +export class AppRealLogStorage extends AppLogStorage { + constructor(private db: typeof AppLogs) { + super('mongodb'); + } + + async find( + query: { + [field: string]: any; + }, + { fields, ...options }: IAppLogStorageFindOptions, + ): Promise { + return this.db.findPaginated(query, { projection: fields, ...options }).cursor.toArray(); + } + + async storeEntries(logEntry: ILoggerStorageEntry): Promise { + logEntry.instanceId = InstanceStatus.id(); + + const id = (await this.db.insertOne(logEntry)).insertedId; + + return this.db.findOneById(id); + } + + async getEntriesFor(appId: string): Promise { + return this.db.find({ appId }).toArray(); + } + + async removeEntriesFor(appId: string): Promise { + await this.db.deleteOne({ appId }); + } +} diff --git a/apps/meteor/ee/server/apps/storage/index.js b/apps/meteor/ee/server/apps/storage/index.js index 7f8d90715a96..7b8d2f5af96e 100644 --- a/apps/meteor/ee/server/apps/storage/index.js +++ b/apps/meteor/ee/server/apps/storage/index.js @@ -1,6 +1,6 @@ import './AppFileSystemSourceStorage'; import './AppGridFSSourceStorage'; -export { AppRealLogsStorage } from './logs-storage'; +export { AppRealLogStorage } from './AppRealLogStorage'; export { AppRealStorage } from './AppRealStorage'; export { ConfigurableAppSourceStorage } from './ConfigurableAppSourceStorage'; diff --git a/apps/meteor/ee/server/apps/storage/logs-storage.js b/apps/meteor/ee/server/apps/storage/logs-storage.js deleted file mode 100644 index b48599ca2d38..000000000000 --- a/apps/meteor/ee/server/apps/storage/logs-storage.js +++ /dev/null @@ -1,32 +0,0 @@ -import { AppConsole } from '@rocket.chat/apps-engine/server/logging'; -import { AppLogStorage } from '@rocket.chat/apps-engine/server/storage'; -import { InstanceStatus } from '@rocket.chat/instance-status'; - -export class AppRealLogsStorage extends AppLogStorage { - constructor(model) { - super('mongodb'); - this.db = model; - } - - async find(...args) { - return this.db.find(...args).toArray(); - } - - async storeEntries(appId, logger) { - const item = AppConsole.toStorageEntry(appId, logger); - - item.instanceId = InstanceStatus.id(); - - const id = (await this.db.insertOne(item)).insertedId; - - return this.db.findOneById(id); - } - - async getEntriesFor(appId) { - return this.db.find({ appId }).toArray(); - } - - async removeEntriesFor(appId) { - await this.db.remove({ appId }); - } -} diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index c2119b1d3b24..20af1002f738 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -18,7 +18,7 @@ "author": "Rocket.Chat", "license": "MIT", "dependencies": { - "@rocket.chat/apps-engine": "1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index d79155167845..9d02e91c6cba 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -231,7 +231,7 @@ "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", "@rocket.chat/apps": "workspace:^", - "@rocket.chat/apps-engine": "1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/base64": "workspace:^", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-services": "workspace:^", diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts index 41a53cf5bbb6..19838fd8411d 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -75,7 +75,7 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi return; } - if (app.getStatus() === status) { + if ((await app.getStatus()) === status) { Apps.self?.getRocketChatLogger().info(`"apps.statusUpdate" event received for app "${appId}", but the status is the same`); return; } @@ -116,10 +116,7 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi } async getApps(query: IGetAppsFilter): Promise { - return Apps.self - ?.getManager() - ?.get(query) - .map((app) => app.getApp().getInfo()); + return (await Apps.self?.getManager()?.get(query))?.map((app) => app.getInfo()); } async getAppStorageItemById(appId: string): Promise { diff --git a/apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.js b/apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.js index a0be32734306..61d812571d7d 100644 --- a/apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.js +++ b/apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.js @@ -95,7 +95,7 @@ describe('Apps - Send Messages As User', function () { after(() => Promise.all([deleteRoom({ type: 'p', roomId: group._id }), deleteUser(user)])); - it('should send a message as app user', (done) => { + it('should return 500 when sending a message as user that has no permissions', (done) => { request .post(apps(`/public/${app.id}/send-message-as-user?userId=${user._id}`)) .set(userCredentials) diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index a533ae29d2df..67b89d61ef52 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -11,6 +11,7 @@ services: image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${RC_DOCKER_TAG} environment: - TEST_MODE=true + - DEBUG=${DEBUG} - EXIT_UNHANDLEDPROMISEREJECTION=true - 'MONGO_URL=${MONGO_URL}' - 'MONGO_OPLOG_URL=${MONGO_OPLOG_URL}' diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index c1dd482d2e75..9a2fd6065765 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -15,7 +15,7 @@ ], "author": "Rocket.Chat", "dependencies": { - "@rocket.chat/apps-engine": "1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 4e609c5d954b..4de31f3214b4 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@babel/preset-env": "~7.22.20", "@babel/preset-typescript": "~7.22.15", - "@rocket.chat/apps-engine": "1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@types/node": "^14.18.63", diff --git a/packages/apps/package.json b/packages/apps/package.json index ee99ba288fab..359bb605e82f 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -22,7 +22,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/model-typings": "workspace:^" } diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 6033814bf2fa..9a4705673b18 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -34,7 +34,7 @@ "extends": "../../package.json" }, "dependencies": { - "@rocket.chat/apps-engine": "1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/message-parser": "workspace:^", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 9256b3e8b6e7..b9b259a1b78e 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -22,7 +22,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~" diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 25d44e876dff..119a23759280 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -63,7 +63,7 @@ "@babel/preset-env": "~7.22.20", "@babel/preset-react": "~7.22.15", "@babel/preset-typescript": "~7.22.15", - "@rocket.chat/apps-engine": "^1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.54.2", diff --git a/packages/model-typings/src/models/IAppLogsModel.ts b/packages/model-typings/src/models/IAppLogsModel.ts index f73964c57687..6a6bc765cd8e 100644 --- a/packages/model-typings/src/models/IAppLogsModel.ts +++ b/packages/model-typings/src/models/IAppLogsModel.ts @@ -1,6 +1,9 @@ +import type { DeleteResult, Filter } from 'mongodb'; + import type { IBaseModel } from './IBaseModel'; // TODO: type for AppLogs export interface IAppLogsModel extends IBaseModel { resetTTLIndex(expireAfterSeconds: number): Promise; + remove(query: Filter): Promise; } diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 2d567eb10fab..91ec61eb40e7 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -24,7 +24,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.42.2", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", diff --git a/yarn.lock b/yarn.lock index 1f73a73df3e1..6321b0aff77f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3653,6 +3653,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/aix-ppc64@npm:0.20.2" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-arm64@npm:0.17.19" @@ -3660,6 +3667,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-arm64@npm:0.20.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-arm@npm:0.17.19" @@ -3667,6 +3681,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-arm@npm:0.20.2" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/android-x64@npm:0.17.19" @@ -3674,6 +3695,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/android-x64@npm:0.20.2" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/darwin-arm64@npm:0.17.19" @@ -3681,6 +3709,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/darwin-arm64@npm:0.20.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/darwin-x64@npm:0.17.19" @@ -3688,6 +3723,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/darwin-x64@npm:0.20.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/freebsd-arm64@npm:0.17.19" @@ -3695,6 +3737,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/freebsd-arm64@npm:0.20.2" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/freebsd-x64@npm:0.17.19" @@ -3702,6 +3751,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/freebsd-x64@npm:0.20.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-arm64@npm:0.17.19" @@ -3709,6 +3765,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-arm64@npm:0.20.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-arm@npm:0.17.19" @@ -3716,6 +3779,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-arm@npm:0.20.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-ia32@npm:0.17.19" @@ -3723,6 +3793,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-ia32@npm:0.20.2" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-loong64@npm:0.17.19" @@ -3730,6 +3807,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-loong64@npm:0.20.2" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-mips64el@npm:0.17.19" @@ -3737,6 +3821,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-mips64el@npm:0.20.2" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-ppc64@npm:0.17.19" @@ -3744,6 +3835,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-ppc64@npm:0.20.2" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-riscv64@npm:0.17.19" @@ -3751,6 +3849,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-riscv64@npm:0.20.2" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-s390x@npm:0.17.19" @@ -3758,6 +3863,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-s390x@npm:0.20.2" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/linux-x64@npm:0.17.19" @@ -3765,6 +3877,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/linux-x64@npm:0.20.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/netbsd-x64@npm:0.17.19" @@ -3772,6 +3891,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/netbsd-x64@npm:0.20.2" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/openbsd-x64@npm:0.17.19" @@ -3779,6 +3905,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/openbsd-x64@npm:0.20.2" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/sunos-x64@npm:0.17.19" @@ -3786,6 +3919,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/sunos-x64@npm:0.20.2" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-arm64@npm:0.17.19" @@ -3793,6 +3933,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-arm64@npm:0.20.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-ia32@npm:0.17.19" @@ -3800,6 +3947,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-ia32@npm:0.20.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.17.19": version: 0.17.19 resolution: "@esbuild/win32-x64@npm:0.17.19" @@ -3807,6 +3961,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.20.2": + version: 0.20.2 + resolution: "@esbuild/win32-x64@npm:0.20.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -4725,6 +4886,13 @@ __metadata: languageName: node linkType: hard +"@msgpack/msgpack@npm:3.0.0-beta2": + version: 3.0.0-beta2 + resolution: "@msgpack/msgpack@npm:3.0.0-beta2" + checksum: d86e5d48146051952d6bea35a6cf733a401cf65ad5614d79689aa48c7076021737ca2c782978dd1b6c0c9c45888b246e379e45ae906179e3a0e8ef4ee6f221c1 + languageName: node + linkType: hard + "@napi-rs/cli@npm:^2.2.0": version: 2.6.2 resolution: "@napi-rs/cli@npm:2.6.2" @@ -8340,21 +8508,25 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/apps-engine@npm:1.42.2, @rocket.chat/apps-engine@npm:^1.42.2": - version: 1.42.2 - resolution: "@rocket.chat/apps-engine@npm:1.42.2" +"@rocket.chat/apps-engine@npm:alpha": + version: 1.43.0-alpha.763 + resolution: "@rocket.chat/apps-engine@npm:1.43.0-alpha.763" dependencies: + "@msgpack/msgpack": 3.0.0-beta2 adm-zip: ^0.5.9 cryptiles: ^4.1.3 + debug: ^4.3.4 + deno-bin: 1.37.1 + esbuild: ^0.20.2 jose: ^4.11.1 + jsonrpc-lite: ^2.2.0 lodash.clonedeep: ^4.5.0 semver: ^5.7.1 stack-trace: 0.0.10 uuid: ~8.3.2 - vm2: ^3.9.19 peerDependencies: "@rocket.chat/ui-kit": "*" - checksum: d7aa23249823e37072b6b7af16a40d9a4e7cb6b8047f2a87e52163dfe516d6c8a09b21cafd4f28dfbe4dd3da9cd0190d71f7623fec8c573a3f215ca4f9529b56 + checksum: f4498febb2f11766c6cab98c3738ca8ba50e9cfa68d129577a781a6fbbe91cfc48c82d6e418f05f0db6b3561becd6aab09b5b19b26652deb5e5621d7fd7b4c4a languageName: node linkType: hard @@ -8362,7 +8534,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/apps@workspace:packages/apps" dependencies: - "@rocket.chat/apps-engine": 1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@types/jest": ~29.5.7 @@ -8441,7 +8613,7 @@ __metadata: "@babel/core": ~7.22.20 "@babel/preset-env": ~7.22.20 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": 1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/icons": ^0.36.0 @@ -8467,7 +8639,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/core-typings@workspace:packages/core-typings" dependencies: - "@rocket.chat/apps-engine": 1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/icons": ^0.36.0 "@rocket.chat/message-parser": "workspace:^" @@ -8544,7 +8716,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer" dependencies: - "@rocket.chat/apps-engine": 1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": ~0.31.25 @@ -8740,7 +8912,7 @@ __metadata: "@babel/preset-env": ~7.22.20 "@babel/preset-react": ~7.22.15 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": ^1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": ^0.54.2 @@ -9184,7 +9356,7 @@ __metadata: "@rocket.chat/agenda": "workspace:^" "@rocket.chat/api-client": "workspace:^" "@rocket.chat/apps": "workspace:^" - "@rocket.chat/apps-engine": 1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/base64": "workspace:^" "@rocket.chat/cas-validate": "workspace:^" "@rocket.chat/core-services": "workspace:^" @@ -9822,7 +9994,7 @@ __metadata: "@babel/core": ~7.22.20 "@babel/preset-env": ~7.22.20 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": 1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" @@ -9937,7 +10109,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/rest-typings@workspace:packages/rest-typings" dependencies: - "@rocket.chat/apps-engine": 1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/message-parser": "workspace:^" @@ -20891,6 +21063,19 @@ __metadata: languageName: node linkType: hard +"deno-bin@npm:1.37.1": + version: 1.37.1 + resolution: "deno-bin@npm:1.37.1" + dependencies: + adm-zip: ^0.5.4 + follow-redirects: ^1.10.0 + bin: + deno: bin/deno.js + deno-bin: bin/deno.js + checksum: 1c985611aa67b4b72f68b5608644af73dbff75619ac4fba67fecf036763acf76c66a9bb537cf09865c855313ceec6af1190474fa0163a616620b8af705c82736 + languageName: node + linkType: hard + "depd@npm:2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -22166,6 +22351,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.20.2": + version: 0.20.2 + resolution: "esbuild@npm:0.20.2" + dependencies: + "@esbuild/aix-ppc64": 0.20.2 + "@esbuild/android-arm": 0.20.2 + "@esbuild/android-arm64": 0.20.2 + "@esbuild/android-x64": 0.20.2 + "@esbuild/darwin-arm64": 0.20.2 + "@esbuild/darwin-x64": 0.20.2 + "@esbuild/freebsd-arm64": 0.20.2 + "@esbuild/freebsd-x64": 0.20.2 + "@esbuild/linux-arm": 0.20.2 + "@esbuild/linux-arm64": 0.20.2 + "@esbuild/linux-ia32": 0.20.2 + "@esbuild/linux-loong64": 0.20.2 + "@esbuild/linux-mips64el": 0.20.2 + "@esbuild/linux-ppc64": 0.20.2 + "@esbuild/linux-riscv64": 0.20.2 + "@esbuild/linux-s390x": 0.20.2 + "@esbuild/linux-x64": 0.20.2 + "@esbuild/netbsd-x64": 0.20.2 + "@esbuild/openbsd-x64": 0.20.2 + "@esbuild/sunos-x64": 0.20.2 + "@esbuild/win32-arm64": 0.20.2 + "@esbuild/win32-ia32": 0.20.2 + "@esbuild/win32-x64": 0.20.2 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: bc88050fc1ca5c1bd03648f9979e514bdefb956a63aa3974373bb7b9cbac0b3aac9b9da1b5bdca0b3490e39d6b451c72815dbd6b7d7f978c91fbe9c9e9aa4e4c + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -23826,6 +24091,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.10.0": + version: 1.15.4 + resolution: "follow-redirects@npm:1.15.4" + peerDependenciesMeta: + debug: + optional: true + checksum: e178d1deff8b23d5d24ec3f7a94cde6e47d74d0dc649c35fc9857041267c12ec5d44650a0c5597ef83056ada9ea6ca0c30e7c4f97dbf07d035086be9e6a5b7b6 + languageName: node + linkType: hard + "fontkit@npm:^2.0.2": version: 2.0.2 resolution: "fontkit@npm:2.0.2" @@ -28792,6 +29067,13 @@ __metadata: languageName: node linkType: hard +"jsonrpc-lite@npm:^2.2.0": + version: 2.2.0 + resolution: "jsonrpc-lite@npm:2.2.0" + checksum: 3062101d3c93401d176c1c24b90e0feebdd063546f8ed89c299531dd792c4d37c6766666d160efb83b94f17f7e2deed4346cdd9124b99581ed4620779e8733bb + languageName: node + linkType: hard + "jsonwebtoken@npm:^8.1.0, jsonwebtoken@npm:^8.5.1": version: 8.5.1 resolution: "jsonwebtoken@npm:8.5.1" @@ -36853,7 +37135,7 @@ __metadata: version: 0.0.0-use.local resolution: "rocketchat-services@workspace:apps/meteor/ee/server/services" dependencies: - "@rocket.chat/apps-engine": 1.42.2 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": ~0.31.25 From 94b12edfc6a561a4db0c318c65c6f40786b1fbf8 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 18 Jun 2024 08:44:05 -0300 Subject: [PATCH 20/26] fix: Not possible to edit room without proper permission with retention policy enabled (#32547) --- .changeset/nervous-wolves-collect.md | 5 +++ .../EditRoomInfo/useEditRoomInitialValues.ts | 19 +++++++----- .../meteor/tests/e2e/retention-policy.spec.ts | 31 ++++++++++++++----- 3 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 .changeset/nervous-wolves-collect.md diff --git a/.changeset/nervous-wolves-collect.md b/.changeset/nervous-wolves-collect.md new file mode 100644 index 000000000000..e32377f54179 --- /dev/null +++ b/.changeset/nervous-wolves-collect.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the issue not allowing users without edit-room-retention-policy permission try to edit the room with the retention policy enabled diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts index 128f6c3c66f8..1cb76cadd335 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -1,4 +1,5 @@ import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { usePermission } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; @@ -6,6 +7,8 @@ import { useRetentionPolicy } from '../../../hooks/useRetentionPolicy'; export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { const retentionPolicy = useRetentionPolicy(room); + const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); + const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room; return useMemo( @@ -24,13 +27,14 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { systemMessages: Array.isArray(sysMes) ? sysMes : [], hideSysMes: Array.isArray(sysMes) ? !!sysMes?.length : !!sysMes, encrypted, - ...(retentionPolicy?.enabled && { - retentionEnabled: retention?.enabled ?? retentionPolicy.isActive, - retentionOverrideGlobal: !!retention?.overrideGlobal, - retentionMaxAge: retention?.maxAge ?? retentionPolicy.maxAge, - retentionExcludePinned: retention?.excludePinned ?? retentionPolicy.excludePinned, - retentionFilesOnly: retention?.filesOnly ?? retentionPolicy.filesOnly, - }), + ...(canEditRoomRetentionPolicy && + retentionPolicy?.enabled && { + retentionEnabled: retention?.enabled ?? retentionPolicy.isActive, + retentionOverrideGlobal: !!retention?.overrideGlobal, + retentionMaxAge: retention?.maxAge ?? retentionPolicy.maxAge, + retentionExcludePinned: retention?.excludePinned ?? retentionPolicy.excludePinned, + retentionFilesOnly: retention?.filesOnly ?? retentionPolicy.filesOnly, + }), }), [ announcement, @@ -46,6 +50,7 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { topic, encrypted, reactWhenReadOnly, + canEditRoomRetentionPolicy, ], ); }; diff --git a/apps/meteor/tests/e2e/retention-policy.spec.ts b/apps/meteor/tests/e2e/retention-policy.spec.ts index 9c18edcf4af3..d17276a160af 100644 --- a/apps/meteor/tests/e2e/retention-policy.spec.ts +++ b/apps/meteor/tests/e2e/retention-policy.spec.ts @@ -85,15 +85,30 @@ test.describe.serial('retention-policy', () => { await expect(poHomeChannel.tabs.room.pruneAccordion).toBeVisible(); }); - test('should not show prune section in edit channel for users without permission', async ({ browser }) => { - const { page } = await createAuxContext(browser, Users.user1); - const auxContext = { page, poHomeChannel: new HomeChannel(page) }; - await auxContext.poHomeChannel.sidenav.openChat(targetChannel); - await auxContext.poHomeChannel.tabs.btnRoomInfo.click(); - await auxContext.poHomeChannel.tabs.room.btnEdit.click(); + test.describe('edit-room-retention-policy permission', async () => { + test('should not show prune section in edit channel for users without permission', async ({ browser }) => { + const { page } = await createAuxContext(browser, Users.user1); + const auxContext = { page, poHomeChannel: new HomeChannel(page) }; + await auxContext.poHomeChannel.sidenav.openChat(targetChannel); + await auxContext.poHomeChannel.tabs.btnRoomInfo.click(); + await auxContext.poHomeChannel.tabs.room.btnEdit.click(); + + await expect(poHomeChannel.tabs.room.pruneAccordion).not.toBeVisible(); + await auxContext.page.close(); + }); - await expect(poHomeChannel.tabs.room.pruneAccordion).not.toBeVisible(); - await auxContext.page.close(); + test('users without permission should be able to edit the channel', async ({ browser }) => { + const { page } = await createAuxContext(browser, Users.user1); + const auxContext = { page, poHomeChannel: new HomeChannel(page) }; + await auxContext.poHomeChannel.sidenav.openChat(targetChannel); + await auxContext.poHomeChannel.tabs.btnRoomInfo.click(); + await auxContext.poHomeChannel.tabs.room.btnEdit.click(); + await auxContext.poHomeChannel.tabs.room.checkboxReadOnly.check(); + await auxContext.poHomeChannel.tabs.room.btnSave.click(); + + await expect(auxContext.poHomeChannel.getSystemMessageByText('set room to read only')).toBeVisible(); + await auxContext.page.close(); + }); }); test.describe('retention policy applies enabled by default', () => { From faf386bf65ea76bdec78dd3fe6c710f4ad89b571 Mon Sep 17 00:00:00 2001 From: Allan RIbeiro <35040806+AllanPazRibeiro@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:30:37 -0300 Subject: [PATCH 21/26] fix: prevent broadcasting of system messages when hide setting is enabled (#32522) --- .changeset/grumpy-games-greet.md | 4 + .../server/functions/loadMessageHistory.ts | 7 +- .../lib/server/lib/getHiddenSystemMessages.ts | 24 +- .../lib/server/methods/getChannelHistory.ts | 7 +- .../lib/systemMessage/hideSystemMessage.ts | 13 ++ .../server/modules/watchers/lib/messages.ts | 15 +- .../server/lib/getHiddenSystemMessage.spec.ts | 90 ++++++++ .../systemMessage/hideSystemMessage.spec.ts | 48 ++++ .../modules/watchers/lib/messages.spec.ts | 215 ++++++++++++++++++ 9 files changed, 397 insertions(+), 26 deletions(-) create mode 100644 .changeset/grumpy-games-greet.md create mode 100644 apps/meteor/server/lib/systemMessage/hideSystemMessage.ts create mode 100644 apps/meteor/tests/unit/app/lib/server/lib/getHiddenSystemMessage.spec.ts create mode 100644 apps/meteor/tests/unit/server/lib/systemMessage/hideSystemMessage.spec.ts create mode 100644 apps/meteor/tests/unit/server/modules/watchers/lib/messages.spec.ts diff --git a/.changeset/grumpy-games-greet.md b/.changeset/grumpy-games-greet.md new file mode 100644 index 000000000000..1e7f03658ad3 --- /dev/null +++ b/.changeset/grumpy-games-greet.md @@ -0,0 +1,4 @@ +--- +'@rocket.chat/meteor': patch +--- +Changed streaming logic to prevent hidden system messages from being broadcasted through `stream-room-messages`. diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index 2f6b7a1f694d..fee7061cae96 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -1,7 +1,8 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import type { FindOptions } from 'mongodb'; +import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -29,7 +30,9 @@ export async function loadMessageHistory({ throw new Error('error-invalid-room'); } - const hiddenMessageTypes = getHiddenSystemMessages(room); + const hiddenSystemMessages = settings.get('Hide_System_Messages'); + + const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages); const options: FindOptions = { sort: { diff --git a/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts b/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts index 74b31b63da6a..08f52620e080 100644 --- a/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts +++ b/apps/meteor/app/lib/server/lib/getHiddenSystemMessages.ts @@ -1,26 +1,10 @@ import type { MessageTypesValues, IRoom } from '@rocket.chat/core-typings'; -import { settings } from '../../../settings/server'; - -const hideMessagesOfTypeServer = new Set(); - -settings.watch('Hide_System_Messages', (values) => { - if (!values || !Array.isArray(values)) { - return; - } - - const hiddenTypes = values.reduce((array, value): MessageTypesValues[] => { +export const getHiddenSystemMessages = (room: IRoom, hiddenSystemMessages: MessageTypesValues[]): MessageTypesValues[] => { + const hiddenTypes = hiddenSystemMessages.reduce((array, value): MessageTypesValues[] => { const newValue: MessageTypesValues[] = value === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [value]; - return [...array, ...newValue]; }, [] as MessageTypesValues[]); - hideMessagesOfTypeServer.clear(); - - hiddenTypes.forEach((item) => hideMessagesOfTypeServer.add(item)); -}); - -// TODO probably remove on chained event system -export function getHiddenSystemMessages(room: IRoom): MessageTypesValues[] { - return Array.isArray(room?.sysMes) ? room.sysMes : [...hideMessagesOfTypeServer]; -} + return Array.isArray(room?.sysMes) ? room.sysMes : hiddenTypes; +}; diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index 3c68fb7a2bf2..00ff01639593 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -1,4 +1,4 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import { Messages, Subscriptions, Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; @@ -7,6 +7,7 @@ import _ from 'underscore'; import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { settings } from '../../../settings/server/cached'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -67,7 +68,9 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-date', 'Invalid date', { method: 'getChannelHistory' }); } - const hiddenMessageTypes = getHiddenSystemMessages(room); + const hiddenSystemMessages = settings.get('Hide_System_Messages'); + + const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages); const options: Record = { sort: { diff --git a/apps/meteor/server/lib/systemMessage/hideSystemMessage.ts b/apps/meteor/server/lib/systemMessage/hideSystemMessage.ts new file mode 100644 index 000000000000..cc6b7c13b85e --- /dev/null +++ b/apps/meteor/server/lib/systemMessage/hideSystemMessage.ts @@ -0,0 +1,13 @@ +import type { MessageTypesValues } from '@rocket.chat/core-typings'; + +export const isMutedUnmuted = (messageType: string): boolean => { + return messageType === 'user-muted' || messageType === 'user-unmuted'; +}; + +export const shouldHideSystemMessage = (messageType: MessageTypesValues, hideSystemMessage?: MessageTypesValues[]): boolean => { + if (!hideSystemMessage?.length) { + return false; + } + + return hideSystemMessage.includes(messageType) || (isMutedUnmuted(messageType) && hideSystemMessage.includes('mute_unmute')); +}; diff --git a/apps/meteor/server/modules/watchers/lib/messages.ts b/apps/meteor/server/modules/watchers/lib/messages.ts index 576f27f83b96..2f71e0ed1b66 100644 --- a/apps/meteor/server/modules/watchers/lib/messages.ts +++ b/apps/meteor/server/modules/watchers/lib/messages.ts @@ -1,9 +1,9 @@ import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; -import type { IMessage, SettingValue, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IUser, MessageTypesValues } from '@rocket.chat/core-typings'; import { Messages, Settings, Users } from '@rocket.chat/models'; import mem from 'mem'; -const getSettingCached = mem(async (setting: string): Promise => Settings.getValueById(setting), { maxAge: 10000 }); +import { shouldHideSystemMessage } from '../../../lib/systemMessage/hideSystemMessage'; const getUserNameCached = mem( async (userId: string): Promise => { @@ -13,12 +13,23 @@ const getUserNameCached = mem( { maxAge: 10000 }, ); +const getSettingCached = mem(Settings.getValueById, { maxAge: 10000 }); + export async function getMessageToBroadcast({ id, data }: { id: IMessage['_id']; data?: IMessage }): Promise { const message = data ?? (await Messages.findOneById(id)); if (!message) { return; } + if (message.t) { + const hiddenSystemMessages = (await getSettingCached('Hide_System_Messages')) as MessageTypesValues[]; + const shouldHide = shouldHideSystemMessage(message.t, hiddenSystemMessages); + + if (shouldHide) { + return; + } + } + if (message._hidden || message.imported != null) { return; } diff --git a/apps/meteor/tests/unit/app/lib/server/lib/getHiddenSystemMessage.spec.ts b/apps/meteor/tests/unit/app/lib/server/lib/getHiddenSystemMessage.spec.ts new file mode 100644 index 000000000000..4fb3df00b8c5 --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/lib/getHiddenSystemMessage.spec.ts @@ -0,0 +1,90 @@ +import type { MessageTypesValues, IRoom, IUser } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + +import { getHiddenSystemMessages } from '../../../../../../app/lib/server/lib/getHiddenSystemMessages'; + +describe('getHiddenSystemMessages', () => { + it('should return room.sysMes if it is an array', async () => { + const room: IRoom = { + _id: 'roomId', + sysMes: ['mute_unmute', 'room_changed_description'] as MessageTypesValues[], + t: 'c', + msgs: 0, + u: {} as IUser, + usersCount: 0, + _updatedAt: new Date(), + }; + + const result = getHiddenSystemMessages(room, []); + + expect(result).to.deep.equal(room.sysMes); + }); + + it('should return cached hidden system messages if room.sysMes is not an array', async () => { + const cachedHiddenSystemMessage: MessageTypesValues[] = ['mute_unmute', 'room_changed_description']; + + const room: IRoom = { + _id: 'roomId', + t: 'c', + msgs: 0, + u: {} as IUser, + usersCount: 0, + _updatedAt: new Date(), + }; + + const result = getHiddenSystemMessages(room, cachedHiddenSystemMessage); + + expect(result).to.deep.equal(['user-muted', 'user-unmuted', 'room_changed_description']); + }); + + it('should return an empty array if both room.sysMes and cached hidden system messages are undefined', async () => { + const room: IRoom = { + _id: 'roomId', + t: 'c', + msgs: 0, + u: {} as IUser, + usersCount: 0, + _updatedAt: new Date(), + }; + + const result = getHiddenSystemMessages(room, []); + + expect(result).to.deep.equal([]); + }); + + it('should return cached hidden system messages if room.sysMes is null', async () => { + const cachedHiddenSystemMessage: MessageTypesValues[] = ['subscription-role-added', 'room_changed_announcement']; + + const room: IRoom = { + _id: 'roomId', + sysMes: undefined, + t: 'c', + msgs: 0, + u: {} as IUser, + usersCount: 0, + _updatedAt: new Date(), + }; + + const result = getHiddenSystemMessages(room, cachedHiddenSystemMessage); + + expect(result).to.deep.equal(cachedHiddenSystemMessage); + }); + + it('should return cached hidden system messages if room.sysMes array and hidden system message is available', async () => { + const cachedHiddenSystemMessage: MessageTypesValues[] = ['room_changed_announcement', 'room-archived']; + + const room: IRoom = { + _id: 'roomId', + sysMes: ['mute_unmute', 'room_changed_description'] as MessageTypesValues[], + t: 'c', + msgs: 0, + u: {} as IUser, + usersCount: 0, + _updatedAt: new Date(), + }; + + const result = getHiddenSystemMessages(room, cachedHiddenSystemMessage); + + expect(result).to.deep.equal(['mute_unmute', 'room_changed_description']); + }); +}); diff --git a/apps/meteor/tests/unit/server/lib/systemMessage/hideSystemMessage.spec.ts b/apps/meteor/tests/unit/server/lib/systemMessage/hideSystemMessage.spec.ts new file mode 100644 index 000000000000..61d2696068ae --- /dev/null +++ b/apps/meteor/tests/unit/server/lib/systemMessage/hideSystemMessage.spec.ts @@ -0,0 +1,48 @@ +import type { MessageTypesValues } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + +import { isMutedUnmuted, shouldHideSystemMessage } from '../../../../../server/lib/systemMessage/hideSystemMessage'; + +describe('hideSystemMessage', () => { + describe('isMutedUnmuted', () => { + it('should return true for user-muted', () => { + expect(isMutedUnmuted('user-muted')).to.be.true; + }); + + it('should return true for user-unmuted', () => { + expect(isMutedUnmuted('user-unmuted')).to.be.true; + }); + + it('should return false for other message types', () => { + expect(isMutedUnmuted('some-other-type')).to.be.false; + }); + }); + + describe('shouldHideSystemMessage', () => { + it('should return true if message type is in hidden system messages', async () => { + const hiddenMessages: MessageTypesValues[] = ['user-muted', 'mute_unmute']; + + const result = shouldHideSystemMessage('user-muted', hiddenMessages); + expect(result).to.be.true; + }); + + it('should return true if message type is user-muted and mute_unmute is in hidden system messages', async () => { + const hiddenMessages: MessageTypesValues[] = ['mute_unmute']; + + const result = shouldHideSystemMessage('user-muted', hiddenMessages); + expect(result).to.be.true; + }); + + it('should return false if message type is not in hidden system messages', async () => { + const hiddenMessages: MessageTypesValues[] = ['room-archived']; + + const result = shouldHideSystemMessage('user-muted', hiddenMessages); + expect(result).to.be.false; + }); + + it('should return false if hidden system messages are undefined', async () => { + const result = shouldHideSystemMessage('user-muted', undefined); + expect(result).to.be.false; + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/modules/watchers/lib/messages.spec.ts b/apps/meteor/tests/unit/server/modules/watchers/lib/messages.spec.ts new file mode 100644 index 000000000000..5ef8bbdd1634 --- /dev/null +++ b/apps/meteor/tests/unit/server/modules/watchers/lib/messages.spec.ts @@ -0,0 +1,215 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +describe('Message Broadcast Tests', () => { + let getSettingValueByIdStub: sinon.SinonStub; + let usersFindOneStub: sinon.SinonStub; + let messagesFindOneStub: sinon.SinonStub; + let broadcastStub: sinon.SinonStub; + let getMessageToBroadcast: any; + let broadcastMessageFromData: any; + let memStub: sinon.SinonStub; + + const sampleMessage: IMessage = { + _id: '123', + rid: 'room1', + msg: 'Hello', + ts: new Date(), + u: { _id: 'user1', username: 'user1', name: 'Real User' }, + mentions: [], + t: 'user-muted', + _updatedAt: new Date(), + }; + + const modelsStubs = () => ({ + Messages: { + findOneById: messagesFindOneStub, + }, + Users: { + findOne: usersFindOneStub, + }, + Settings: { + getValueById: getSettingValueByIdStub, + }, + }); + + const coreStubs = (dbWatchersDisabled: boolean) => ({ + api: { + broadcast: broadcastStub, + }, + dbWatchersDisabled, + }); + + beforeEach(() => { + getSettingValueByIdStub = sinon.stub(); + usersFindOneStub = sinon.stub(); + messagesFindOneStub = sinon.stub(); + broadcastStub = sinon.stub(); + memStub = sinon.stub().callsFake((fn: any) => fn); + + const proxyMock = proxyquire.noCallThru().load('../../../../../../server/modules/watchers/lib/messages', { + '@rocket.chat/models': modelsStubs(), + '@rocket.chat/core-services': coreStubs(false), + 'mem': memStub, + }); + + getMessageToBroadcast = proxyMock.getMessageToBroadcast; + broadcastMessageFromData = proxyMock.broadcastMessageFromData; + }); + + afterEach(() => { + sinon.reset(); + }); + + describe('getMessageToBroadcast', () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + sinon.resetHistory(); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + const testCases = [ + { + description: 'should return undefined if message is hidden or imported', + message: { ...sampleMessage, _hidden: true }, + hideSystemMessages: [], + useRealName: false, + expectedResult: undefined, + }, + { + description: 'should hide message if type is in hideSystemMessage settings', + message: sampleMessage, + hideSystemMessages: ['user-muted', 'mute_unmute'], + useRealName: false, + expectedResult: undefined, + }, + { + description: 'should return the message with real name if useRealName is true', + message: sampleMessage, + hideSystemMessages: [], + useRealName: true, + expectedResult: { ...sampleMessage, u: { ...sampleMessage.u, name: 'Real User' } }, + }, + { + description: 'should return the message if Hide_System_Messages is undefined', + message: sampleMessage, + hideSystemMessages: undefined, + useRealName: false, + expectedResult: sampleMessage, + }, + { + description: 'should return undefined if the message type is muted and a mute_unmute is received', + message: { ...sampleMessage, t: 'mute_unmute' }, + hideSystemMessages: ['user-muted', 'mute_unmute'], + useRealName: false, + expectedResult: undefined, + }, + { + description: 'should return the message if no system messages are muted', + message: sampleMessage, + hideSystemMessages: [], + useRealName: false, + expectedResult: sampleMessage, + }, + { + description: 'should hide message if type is room-archived', + message: { ...sampleMessage, t: 'room-archived' }, + hideSystemMessages: ['room-archived'], + useRealName: false, + expectedResult: undefined, + }, + { + description: 'should hide message if type is user-unmuted', + message: { ...sampleMessage, t: 'user-unmuted' }, + hideSystemMessages: ['user-unmuted'], + useRealName: false, + expectedResult: undefined, + }, + { + description: 'should hide message if type is subscription-role-added', + message: { ...sampleMessage, t: 'subscription-role-added' }, + hideSystemMessages: ['subscription-role-added'], + useRealName: false, + expectedResult: undefined, + }, + { + description: 'should hide message if type is message_pinned', + message: { ...sampleMessage, t: 'message_pinned' }, + hideSystemMessages: ['message_pinned'], + useRealName: false, + expectedResult: undefined, + }, + { + description: 'should hide message if type is new-owner', + message: { ...sampleMessage, t: 'new-owner' }, + hideSystemMessages: ['new-owner'], + useRealName: false, + expectedResult: undefined, + }, + ]; + + testCases.forEach(({ description, message, hideSystemMessages, useRealName, expectedResult }) => { + it(description, async () => { + messagesFindOneStub.resolves(message); + getSettingValueByIdStub.withArgs('Hide_System_Messages').resolves(hideSystemMessages); + getSettingValueByIdStub.withArgs('UI_Use_Real_Name').resolves(useRealName); + + if (useRealName) { + usersFindOneStub.resolves({ name: 'Real User' }); + } + + const result = await getMessageToBroadcast({ id: '123' }); + + expect(result).to.deep.equal(expectedResult); + }); + }); + }); + + describe('broadcastMessageFromData', () => { + const setupProxyMock = (dbWatchersDisabled: boolean) => { + const proxyMock = proxyquire.noCallThru().load('../../../../../../server/modules/watchers/lib/messages', { + '@rocket.chat/models': modelsStubs(), + '@rocket.chat/core-services': coreStubs(dbWatchersDisabled), + 'mem': memStub, + }); + broadcastMessageFromData = proxyMock.broadcastMessageFromData; + }; + + const testCases = [ + { + description: 'should broadcast the message if dbWatchersDisabled is true', + dbWatchersDisabled: true, + expectBroadcast: true, + }, + { + description: 'should not broadcast the message if dbWatchersDisabled is false', + dbWatchersDisabled: false, + expectBroadcast: false, + }, + ]; + + testCases.forEach(({ description, dbWatchersDisabled, expectBroadcast }) => { + it(description, async () => { + setupProxyMock(dbWatchersDisabled); + messagesFindOneStub.resolves(sampleMessage); + getSettingValueByIdStub.resolves([]); + + await broadcastMessageFromData({ id: '123', data: sampleMessage }); + + if (expectBroadcast) { + expect(broadcastStub.calledOnce).to.be.true; + expect(broadcastStub.calledOnceWith('watch.messages', { message: sampleMessage })).to.be.true; + } else { + expect(broadcastStub.called).to.be.false; + } + }); + }); + }); +}); From ffbbd8c40bb7a7cd7fe16cb41464a543adabc344 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 18 Jun 2024 10:11:23 -0300 Subject: [PATCH 22/26] regression: don't notify role changes during startup (#32619) --- .../server/lib/roles/createOrUpdateProtectedRole.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts b/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts index cdc43cdad93a..ea59a2961ea4 100644 --- a/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts +++ b/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts @@ -1,8 +1,6 @@ import type { IRole, AtLeast } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; -import { notifyOnRoleChanged, notifyOnRoleChangedById } from '../../../app/lib/server/lib/notifyListener'; - export const createOrUpdateProtectedRoleAsync = async ( roleId: string, roleData: AtLeast, 'name'>, @@ -12,7 +10,7 @@ export const createOrUpdateProtectedRoleAsync = async ( }); if (role) { - const updatedRole = await Roles.updateById( + await Roles.updateById( roleId, roleData.name || role.name, roleData.scope || role.scope, @@ -20,8 +18,6 @@ export const createOrUpdateProtectedRoleAsync = async ( roleData.mandatory2fa || role.mandatory2fa, ); - void notifyOnRoleChanged(updatedRole); - return; } @@ -33,6 +29,4 @@ export const createOrUpdateProtectedRoleAsync = async ( ...roleData, protected: true, }); - - void notifyOnRoleChangedById(roleId); }; From b23f9ed91ab21ad83ff01bc512fafc0c9987a3e5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 18 Jun 2024 11:32:29 -0300 Subject: [PATCH 23/26] fix: Supported Versions misbehaving (#32610) --- .changeset/dry-shoes-tap.md | 5 ++++ .../supportedVersionsChooseLatest.ts | 2 +- .../supportedVersionsToken.ts | 26 +++++++++++++++++++ apps/meteor/jest.config.ts | 1 + .../plugin/compile-version.js | 23 ++++++++++++---- 5 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 .changeset/dry-shoes-tap.md diff --git a/.changeset/dry-shoes-tap.md b/.changeset/dry-shoes-tap.md new file mode 100644 index 000000000000..f5abf51c0df0 --- /dev/null +++ b/.changeset/dry-shoes-tap.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes the supported versions problem, where in most cases the data chosen was the oldest diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts index 3493401144cf..32753ba00429 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsChooseLatest.ts @@ -2,7 +2,7 @@ import type { SignedSupportedVersions } from '@rocket.chat/server-cloud-communic export const supportedVersionsChooseLatest = async (...tokens: (SignedSupportedVersions | undefined)[]) => { const [token] = (tokens.filter((r) => r?.timestamp != null) as SignedSupportedVersions[]).sort((a, b) => { - return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); + return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); }); return token; diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts index 473acef88c29..a543c0681f38 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -62,6 +62,7 @@ const cacheValueInSettings = ( reset: () => Promise; } => { const reset = async () => { + SystemLogger.debug(`Resetting cached value ${key} in settings`); const value = await fn(); await Settings.updateValueById(key, value); @@ -134,6 +135,31 @@ const getSupportedVersionsToken = async () => { (response.success && response.result) || undefined, ); + SystemLogger.debug({ + msg: 'Supported versions', + supportedVersionsFromBuild: supportedVersionsFromBuild.timestamp, + versionsFromLicense: versionsFromLicense?.supportedVersions?.timestamp, + response: response.success && response.result?.timestamp, + }); + + switch (supportedVersions) { + case supportedVersionsFromBuild: + SystemLogger.info({ + msg: 'Using supported versions from build', + }); + break; + case versionsFromLicense?.supportedVersions: + SystemLogger.info({ + msg: 'Using supported versions from license', + }); + break; + case response.success && response.result: + SystemLogger.info({ + msg: 'Using supported versions from cloud', + }); + break; + } + await buildVersionUpdateMessage(supportedVersions?.versions); return supportedVersions?.signed; diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index adbd8c26344f..72538cf14d16 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -33,6 +33,7 @@ const config: Config = { '/app/livechat/server/business-hour/**/*.spec.ts?(x)', '/app/livechat/server/api/**/*.spec.ts', '/ee/app/authorization/server/validateUserRoles.spec.ts', + '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', ], transformIgnorePatterns: ['!/node_modules/jose'], errorOnDeprecated: true, diff --git a/apps/meteor/packages/rocketchat-version/plugin/compile-version.js b/apps/meteor/packages/rocketchat-version/plugin/compile-version.js index e737f07663f3..8750c6851202 100644 --- a/apps/meteor/packages/rocketchat-version/plugin/compile-version.js +++ b/apps/meteor/packages/rocketchat-version/plugin/compile-version.js @@ -21,10 +21,10 @@ class VersionCompiler { function handleError(err) { console.error(err); // TODO remove this when we are ready to fail - // if (process.env.NODE_ENV !== 'development') { - // reject(err); - // return; - // } + if (process.env.NODE_ENV !== 'development') { + reject(err); + return; + } resolve({}); } @@ -34,11 +34,24 @@ class VersionCompiler { response.on('data', function (chunk) { data += chunk; }); - response.on('end', function () { + response.on('end', async function () { const supportedVersions = JSON.parse(data); if (!supportedVersions?.signed) { return handleError(new Error(`Invalid supportedVersions result:\n URL: ${url} \n RESULT: ${data}`)); } + + // check if timestamp is inside 1 hour within build + if (Math.abs(new Date().getTime() - new Date(supportedVersions.timestamp).getTime()) > 1000 * 60 * 60) { + return handleError(new Error(`Invalid supportedVersions timestamp:\n URL: ${url} \n RESULT: ${data}`)); + } + + for await (const version of supportedVersions.versions) { + // check if expiration is after the first rocket.chat release + if (new Date(version.expiration) < new Date('2019-04-01T00:00:00.000Z')) { + return handleError(new Error(`Invalid supportedVersions expiration:\n URL: ${url} \n RESULT: ${data}`)); + } + } + resolve(supportedVersions); }); response.on('error', function (err) { From 1bdffcde914bf21cb947c8636574096060fbe9bd Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 18 Jun 2024 10:58:22 -0600 Subject: [PATCH 24/26] fix: `QueueInactivityMonitor` not processing inquiries (#32452) --- .changeset/green-camels-repair.md | 5 + .../server/hooks/afterInquiryQueued.ts | 20 +-- .../server/hooks/afterInquiryQueued.spec.ts | 120 ++++++++++++++++++ 3 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 .changeset/green-camels-repair.md create mode 100644 apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/afterInquiryQueued.spec.ts diff --git a/.changeset/green-camels-repair.md b/.changeset/green-camels-repair.md new file mode 100644 index 000000000000..58b0f6f1a00c --- /dev/null +++ b/.changeset/green-camels-repair.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed 2 issues with `QueueInactivityMonitor` callback. This callback was in charge of scheduling the job that would close the inquiry, but it was checking on a property that didn't exist. This caused the callback to early return without scheduling the job, making the feature to not to work. diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterInquiryQueued.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterInquiryQueued.ts index cb6993b38aec..4dfb3fc9d6eb 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterInquiryQueued.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterInquiryQueued.ts @@ -1,3 +1,4 @@ +import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; import moment from 'moment'; import { settings } from '../../../../../app/settings/server'; @@ -5,28 +6,27 @@ import { callbacks } from '../../../../../lib/callbacks'; import { OmnichannelQueueInactivityMonitor } from '../lib/QueueInactivityMonitor'; import { cbLogger } from '../lib/logger'; -let timer = 0; - -const scheduleInquiry = async (inquiry: any): Promise => { - if (!inquiry?._id) { +export const afterInquiryQueued = async (inquiry: ILivechatInquiryRecord) => { + if (!inquiry?._id || !inquiry?._updatedAt) { return; } - if (!inquiry?._updatedAt || !inquiry?._createdAt) { + const timer = settings.get('Livechat_max_queue_wait_time'); + + if (timer <= 0) { return; } // schedule individual jobs instead of property for close inactivty - const newQueueTime = moment(inquiry._updatedAt || inquiry._createdAt).add(timer, 'minutes'); + const newQueueTime = moment(inquiry._updatedAt).add(timer, 'minutes'); cbLogger.debug(`Scheduling estimated close time at ${newQueueTime} for queued inquiry ${inquiry._id}`); await OmnichannelQueueInactivityMonitor.scheduleInquiry(inquiry._id, new Date(newQueueTime.format())); }; -settings.watch('Livechat_max_queue_wait_time', (value) => { - timer = value as number; - if (timer <= 0) { +settings.watch('Livechat_max_queue_wait_time', (value) => { + if (value <= 0) { callbacks.remove('livechat.afterInquiryQueued', 'livechat-inquiry-queued-set-queue-timer'); return; } - callbacks.add('livechat.afterInquiryQueued', scheduleInquiry, callbacks.priority.HIGH, 'livechat-inquiry-queued-set-queue-timer'); + callbacks.add('livechat.afterInquiryQueued', afterInquiryQueued, callbacks.priority.HIGH, 'livechat-inquiry-queued-set-queue-timer'); }); diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/afterInquiryQueued.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/afterInquiryQueued.spec.ts new file mode 100644 index 000000000000..dcbefd1dfd6e --- /dev/null +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/afterInquiryQueued.spec.ts @@ -0,0 +1,120 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import moment from 'moment'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const settingStub = { + watch: sinon.stub(), + get: sinon.stub(), +}; + +const callbackStub = { + add: sinon.stub(), + remove: sinon.stub(), + priority: { HIGH: 'high' }, +}; + +const queueMonitorStub = { + scheduleInquiry: sinon.stub(), +}; + +const { afterInquiryQueued } = proxyquire + .noCallThru() + .load('../../../../../../app/livechat-enterprise/server/hooks/afterInquiryQueued.ts', { + '../../../../../app/settings/server': { + settings: settingStub, + }, + '../../../../../lib/callbacks': { + callbacks: callbackStub, + }, + '../lib/QueueInactivityMonitor': { + OmnichannelQueueInactivityMonitor: queueMonitorStub, + }, + '../lib/logger': { + cbLogger: { debug: sinon.stub() }, + }, + }); + +describe('hooks/afterInquiryQueued', () => { + beforeEach(() => { + callbackStub.add.resetHistory(); + callbackStub.remove.resetHistory(); + queueMonitorStub.scheduleInquiry.resetHistory(); + settingStub.get.resetHistory(); + }); + + it('should call settings.watch at first', () => { + expect(settingStub.watch.callCount).to.be.equal(1); + }); + + it('should call the callback on settings.watch with proper values', () => { + const func = settingStub.watch.getCall(0).args[1]; + + func(1); + expect(callbackStub.add.callCount).to.be.equal(1); + + func(2); + expect(callbackStub.add.callCount).to.be.equal(2); + + func(0); + expect(callbackStub.remove.callCount).to.be.equal(1); + + func(-1); + expect(callbackStub.remove.callCount).to.be.equal(2); + + func(3); + expect(callbackStub.add.callCount).to.be.equal(3); + }); + + it('should return undefined if no inquiry is passed, or if inquiry doesnt have valid properties', async () => { + expect(await afterInquiryQueued(null)).to.be.equal(undefined); + expect(await afterInquiryQueued({})).to.be.equal(undefined); + expect(await afterInquiryQueued({ _id: 'invalid' })).to.be.equal(undefined); + expect(await afterInquiryQueued({ _updatedAt: new Date() })); + expect(await afterInquiryQueued({ _updatedAt: null, _id: 'afsd34asdX' })).to.be.equal(undefined); + }); + + it('should do nothing if timer is set to 0 or less', async () => { + const inquiry = { + _id: 'afsd34asdX', + _updatedAt: new Date(), + }; + + settingStub.get.returns(0); + await afterInquiryQueued(inquiry); + expect(queueMonitorStub.scheduleInquiry.callCount).to.be.equal(0); + + settingStub.get.returns(-1); + await afterInquiryQueued(inquiry); + expect(queueMonitorStub.scheduleInquiry.callCount).to.be.equal(0); + }); + + it('should call .scheduleInquiry with proper data', async () => { + const inquiry = { + _id: 'afsd34asdX', + _updatedAt: new Date(), + }; + + settingStub.get.returns(1); + await afterInquiryQueued(inquiry); + + const newQueueTime = moment(inquiry._updatedAt).add(1, 'minutes'); + + expect(queueMonitorStub.scheduleInquiry.calledWith(inquiry._id, new Date(newQueueTime.format()))).to.be.true; + }); + + it('should call .scheduleInquiry with proper data when more than 1 min is passed as param', async () => { + const inquiry = { + _id: 'afv34avzx', + _updatedAt: new Date(), + }; + + settingStub.get.returns(3); + await afterInquiryQueued(inquiry); + + const newQueueTime = moment(inquiry._updatedAt).add(3, 'minutes'); + + expect(queueMonitorStub.scheduleInquiry.calledWith(inquiry._id, new Date(newQueueTime.format()))).to.be.true; + }); +}); From 30399688fce5bb1482b7737c34cfa92a896ea877 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 18 Jun 2024 13:45:38 -0600 Subject: [PATCH 25/26] fix: PDFs uploaded by "PDF transcript" feature were returnig 403 when attempting to download (#32329) --- .changeset/rude-llamas-notice.md | 8 ++ .../app/file-upload/server/lib/FileUpload.ts | 18 ++++- apps/meteor/server/settings/file-upload.ts | 28 ++++++- apps/meteor/tests/end-to-end/api/09-rooms.js | 34 +++++++++ .../src/OmnichannelTranscript.ts | 76 +++++++++++++------ packages/i18n/src/locales/en.i18n.json | 2 + 6 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 .changeset/rude-llamas-notice.md diff --git a/.changeset/rude-llamas-notice.md b/.changeset/rude-llamas-notice.md new file mode 100644 index 000000000000..90c0ca3bd20a --- /dev/null +++ b/.changeset/rude-llamas-notice.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +"@rocket.chat/omnichannel-services": patch +--- + +Added a new setting `Restrict files access to users who can access room` that controls file visibility. This new setting allows users that "can access a room" to also download the files that are there. This is specially important for users with livechat manager or monitor roles, or agents that have special permissions to view closed rooms, since this allows them to download files on the conversation even after the conversation is closed. +New setting is disabled by default and it is mutually exclusive with the setting `Restrict file access to room members` since this allows _more_ types of users to download files. diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index c824ba6c31a5..08e2ccb0a52b 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -28,7 +28,7 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { UploadFS } from '../../../../server/ufs'; import { ufsComplete } from '../../../../server/ufs/ufs-methods'; import type { Store, StoreOptions } from '../../../../server/ufs/ufs-store'; -import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { settings } from '../../../settings/server'; import { mime } from '../../../utils/lib/mimeTypes'; import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper'; @@ -463,16 +463,26 @@ export const FileUpload = { return false; } - if (!settings.get('FileUpload_Restrict_to_room_members') || !file?.rid) { + if (!file?.rid) { return true; } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } }); + const fileUploadRestrictedToMembers = settings.get('FileUpload_Restrict_to_room_members'); + const fileUploadRestrictToUsersWhoCanAccessRoom = settings.get('FileUpload_Restrict_to_users_who_can_access_room'); - if (subscription) { + if (!fileUploadRestrictToUsersWhoCanAccessRoom && !fileUploadRestrictedToMembers) { return true; } + if (fileUploadRestrictedToMembers && !fileUploadRestrictToUsersWhoCanAccessRoom) { + const sub = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } }); + return !!sub; + } + + if (fileUploadRestrictToUsersWhoCanAccessRoom && !fileUploadRestrictedToMembers) { + return canAccessRoomIdAsync(file.rid, user._id); + } + return false; }, diff --git a/apps/meteor/server/settings/file-upload.ts b/apps/meteor/server/settings/file-upload.ts index 76e788cda0e2..505520b73e57 100644 --- a/apps/meteor/server/settings/file-upload.ts +++ b/apps/meteor/server/settings/file-upload.ts @@ -33,10 +33,30 @@ export const createFileUploadSettings = () => await this.add('FileUpload_Restrict_to_room_members', true, { type: 'boolean', - enableQuery: { - _id: 'FileUpload_ProtectFiles', - value: true, - }, + enableQuery: [ + { + _id: 'FileUpload_ProtectFiles', + value: true, + }, + { + _id: 'FileUpload_Restrict_to_users_who_can_access_room', + value: false, + }, + ], + }); + + await this.add('FileUpload_Restrict_to_users_who_can_access_room', false, { + type: 'boolean', + enableQuery: [ + { + _id: 'FileUpload_ProtectFiles', + value: true, + }, + { + _id: 'FileUpload_Restrict_to_room_members', + value: false, + }, + ], }); await this.add('FileUpload_RotateImages', true, { diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index dc8a6a209300..72bd5819593e 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -87,11 +87,13 @@ describe('[Rooms]', function () { let userCredentials; const testChannelName = `channel.test.upload.${Date.now()}-${Math.random()}`; let blockedMediaTypes; + let testPrivateChannel; before(async () => { user = await createUser({ joinDefaultChannels: false }); userCredentials = await login(user.username, password); testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; + testPrivateChannel = (await createRoom({ type: 'p', name: `channel.test.private.${Date.now()}-${Math.random()}` })).body.group; blockedMediaTypes = await getSettingValueById('FileUpload_MediaTypeBlackList'); const newBlockedMediaTypes = blockedMediaTypes .split(',') @@ -105,8 +107,10 @@ describe('[Rooms]', function () { deleteRoom({ type: 'c', roomId: testChannel._id }), deleteUser(user), updateSetting('FileUpload_Restrict_to_room_members', true), + updateSetting('FileUpload_Restrict_to_users_who_can_access_room', false), updateSetting('FileUpload_ProtectFiles', true), updateSetting('FileUpload_MediaTypeBlackList', blockedMediaTypes), + deleteRoom({ roomId: testPrivateChannel._id, type: 'p' }), ]), ); @@ -221,6 +225,7 @@ describe('[Rooms]', function () { it('should be able to get the file when no access to the room if setting allows it', async () => { await updateSetting('FileUpload_Restrict_to_room_members', false); + await updateSetting('FileUpload_Restrict_to_users_who_can_access_room', false); await request.get(fileNewUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200); await request.get(fileOldUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200); }); @@ -237,6 +242,35 @@ describe('[Rooms]', function () { await request.get(fileOldUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); }); + it('should be able to get the file if not member but can access room if setting allows', async () => { + await updateSetting('FileUpload_Restrict_to_room_members', false); + await updateSetting('FileUpload_Restrict_to_users_who_can_access_room', true); + + await request.get(fileNewUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200); + await request.get(fileOldUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200); + }); + + it('should not be able to get the file if not member and cannot access room', async () => { + const { body } = await request + .post(api(`rooms.upload/${testPrivateChannel._id}`)) + .set(credentials) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(200); + + const fileUrl = `/file-upload/${body.message.file._id}/${body.message.file.name}`; + + await request.get(fileUrl).set(userCredentials).expect(403); + }); + + it('should respect the setting with less permissions when both are true', async () => { + await updateSetting('FileUpload_ProtectFiles', true); + await updateSetting('FileUpload_Restrict_to_room_members', true); + await updateSetting('FileUpload_Restrict_to_users_who_can_access_room', true); + await request.get(fileNewUrl).set(userCredentials).expect(403); + await request.get(fileOldUrl).set(userCredentials).expect(403); + }); + it('should not be able to get the file without credentials', async () => { await request.get(fileNewUrl).attach('file', imgURL).expect(403); await request.get(fileOldUrl).attach('file', imgURL).expect(403); diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 2d273991508b..5f99269f671b 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -336,22 +336,15 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT const outBuff = await streamToBuffer(stream as Readable); try { - const file = await uploadService.uploadFile({ - userId: details.userId, + const { rid } = await roomService.createDirectMessage({ to: details.userId, from: 'rocket.cat' }); + const [rocketCatFile, transcriptFile] = await this.uploadFiles({ + details, buffer: outBuff, - details: { - // transcript_{company-name)_{date}_{hour}.pdf - name: `${transcriptText}_${data.siteName}_${new Intl.DateTimeFormat('en-US').format(new Date())}_${ - data.visitor?.name || data.visitor?.username || 'Visitor' - }.pdf`, - type: 'application/pdf', - rid: details.rid, - // Rocket.cat is the goat - userId: 'rocket.cat', - size: outBuff.length, - }, + roomIds: [rid, details.rid], + data, + transcriptText, }); - await this.pdfComplete({ details, file }); + await this.pdfComplete({ details, transcriptFile, rocketCatFile }); } catch (e: any) { this.pdfFailed({ details, e }); } @@ -380,7 +373,49 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT }); } - private async pdfComplete({ details, file }: { details: WorkDetailsWithSource; file: IUpload }): Promise { + private async uploadFiles({ + details, + buffer, + roomIds, + data, + transcriptText, + }: { + details: WorkDetailsWithSource; + buffer: Buffer; + roomIds: string[]; + data: any; + transcriptText: string; + }): Promise { + return Promise.all( + roomIds.map((roomId) => { + return uploadService.uploadFile({ + userId: details.userId, + buffer, + details: { + // transcript_{company-name}_{date}_{hour}.pdf + name: `${transcriptText}_${data.siteName}_${new Intl.DateTimeFormat('en-US').format(new Date())}_${ + data.visitor?.name || data.visitor?.username || 'Visitor' + }.pdf`, + type: 'application/pdf', + rid: roomId, + // Rocket.cat is the goat + userId: 'rocket.cat', + size: buffer.length, + }, + }); + }), + ); + } + + private async pdfComplete({ + details, + transcriptFile, + rocketCatFile, + }: { + details: WorkDetailsWithSource; + transcriptFile: IUpload; + rocketCatFile: IUpload; + }): Promise { this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Complete`); const user = await Users.findOneById(details.userId); if (!user) { @@ -388,17 +423,14 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } // Send the file to the livechat room where this was requested, to keep it in context try { - const [, { rid }] = await Promise.all([ - LivechatRooms.setPdfTranscriptFileIdById(details.rid, file._id), - roomService.createDirectMessage({ to: details.userId, from: 'rocket.cat' }), - ]); + await LivechatRooms.setPdfTranscriptFileIdById(details.rid, transcriptFile._id); this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Sending success message to user`); const result = await Promise.allSettled([ uploadService.sendFileMessage({ roomId: details.rid, userId: 'rocket.cat', - file, + file: transcriptFile, message: { // Translate from service msg: await translationService.translateToServerLanguage('pdf_success_message'), @@ -406,9 +438,9 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT }), // Send the file to the user who requested it, so they can download it uploadService.sendFileMessage({ - roomId: rid, + roomId: rocketCatFile.rid || '', userId: 'rocket.cat', - file, + file: rocketCatFile, message: { // Translate from service msg: await translationService.translate('pdf_success_message', user), diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 779c71c9d755..836b534df601 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2354,6 +2354,8 @@ "FileUpload_Enable_json_web_token_for_files_description": "Appends a JWT to uploaded files urls", "FileUpload_Restrict_to_room_members": "Restrict files to rooms' members", "FileUpload_Restrict_to_room_members_Description": "Restrict the access of files uploaded on rooms to the rooms' members only", + "FileUpload_Restrict_to_users_who_can_access_room": "Restrict files to users who can access the room", + "FileUpload_Restrict_to_users_who_can_access_room_Description": "Restrict the access of files uploaded on rooms to the users who can access the room. This option is mutually exclusive with the \"Restrict files to rooms' members\" option as this one allows for users that are not part of some rooms but have special permissions that allow them to see it to access the files uploaded, for example, Omnichannel Managers & Monitors", "FileUpload_Enabled": "File Uploads Enabled", "FileUpload_Enabled_Direct": "File Uploads Enabled in Direct Messages ", "FileUpload_Error": "File Upload Error", From 02dd87574bc0939fb38a428c1fb623c3b752d6ca Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Wed, 19 Jun 2024 08:13:21 -0300 Subject: [PATCH 26/26] feat: Use `application/octet-stream` as a fallback media type to avoid "Unknown media type" errors (#32471) --- .changeset/breezy-pens-sing.md | 6 +++ .../app/api/server/lib/getUploadFormData.ts | 6 ++- apps/meteor/app/utils/lib/mimeTypes.ts | 7 ++- .../body/hooks/useFileUploadDropTarget.ts | 7 ++- .../hooks/useFileUploadAction.ts | 6 +-- apps/meteor/server/settings/file-upload.ts | 1 + apps/meteor/tests/data/interactions.ts | 1 + apps/meteor/tests/e2e/file-upload.spec.ts | 21 +++++++++ .../tests/e2e/fixtures/files/diagram.drawio | 13 +++++ apps/meteor/tests/end-to-end/api/09-rooms.js | 47 ++++++++++++++++--- packages/i18n/src/locales/en.i18n.json | 1 + 11 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 .changeset/breezy-pens-sing.md create mode 100644 apps/meteor/tests/e2e/fixtures/files/diagram.drawio diff --git a/.changeset/breezy-pens-sing.md b/.changeset/breezy-pens-sing.md new file mode 100644 index 000000000000..0725999ef62b --- /dev/null +++ b/.changeset/breezy-pens-sing.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Removed "Unknown media type" errors on the client side by using `application/octet-stream` as a fallback media type (MIME type) for all files diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 9b8f69fb3a66..85fc0658542d 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -5,6 +5,8 @@ import type { ValidateFunction } from 'ajv'; import busboy from 'busboy'; import type { Request } from 'express'; +import { getMimeType } from '../../../utils/lib/mimeTypes'; + type UploadResult = { file: Readable & { truncated: boolean }; fieldname: string; @@ -61,7 +63,7 @@ export async function getUploadFormData< function onFile( fieldname: string, file: Readable & { truncated: boolean }, - { filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string }, + { filename, encoding }: { filename: string; encoding: string }, ) { if (options.field && fieldname !== options.field) { file.resume(); @@ -83,7 +85,7 @@ export async function getUploadFormData< file, filename, encoding, - mimetype, + mimetype: getMimeType(filename), fieldname, fields, fileBuffer: Buffer.concat(fileChunks), diff --git a/apps/meteor/app/utils/lib/mimeTypes.ts b/apps/meteor/app/utils/lib/mimeTypes.ts index f2da185f84ba..909a955d6724 100644 --- a/apps/meteor/app/utils/lib/mimeTypes.ts +++ b/apps/meteor/app/utils/lib/mimeTypes.ts @@ -12,4 +12,9 @@ const getExtension = (param: string): string => { return !extension || typeof extension === 'boolean' ? '' : extension; }; -export { mime, getExtension }; +const getMimeType = (fileName: string): string => { + const fileMimeType = mime.lookup(fileName); + return typeof fileMimeType === 'string' ? fileMimeType : 'application/octet-stream'; +}; + +export { mime, getExtension, getMimeType }; diff --git a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts index 2427f7217401..414b91c52493 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts @@ -29,7 +29,7 @@ export const useFileUploadDropTarget = (): readonly [ const t = useTranslation(); - const fileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; + const fileUploadEnabled = useSetting('FileUpload_Enabled'); const user = useUser(); const fileUploadAllowedForUser = useReactiveValue( useCallback(() => !roomCoordinator.readOnly(room._id, { username: user?.username }), [room._id, user?.username]), @@ -38,8 +38,7 @@ export const useFileUploadDropTarget = (): readonly [ const chat = useChat(); const onFileDrop = useMutableCallback(async (files: File[]) => { - const { mime } = await import('../../../../../app/utils/lib/mimeTypes'); - + const { getMimeType } = await import('../../../../../app/utils/lib/mimeTypes'); const getUniqueFiles = () => { const uniqueFiles: File[] = []; const st: Set = new Set(); @@ -55,7 +54,7 @@ export const useFileUploadDropTarget = (): readonly [ const uniqueFiles = getUniqueFiles(); const uploads = Array.from(uniqueFiles).map((file) => { - Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }); + Object.defineProperty(file, 'type', { value: getMimeType(file.name) }); return file; }); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index aba008a353a5..03229c5dceb3 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -9,7 +9,7 @@ const fileInputProps = { type: 'file', multiple: true }; export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => { const t = useTranslation(); - const fileUploadEnabled = useSetting('FileUpload_Enabled'); + const fileUploadEnabled = useSetting('FileUpload_Enabled'); const fileInputRef = useFileInput(fileInputProps); const chat = useChat(); @@ -23,10 +23,10 @@ export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => }; const handleUploadChange = async () => { - const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); + const { getMimeType } = await import('../../../../../../../app/utils/lib/mimeTypes'); const filesToUpload = Array.from(fileInputRef?.current?.files ?? []).map((file) => { Object.defineProperty(file, 'type', { - value: mime.lookup(file.name), + value: getMimeType(file.name), }); return file; }); diff --git a/apps/meteor/server/settings/file-upload.ts b/apps/meteor/server/settings/file-upload.ts index 505520b73e57..90032266651c 100644 --- a/apps/meteor/server/settings/file-upload.ts +++ b/apps/meteor/server/settings/file-upload.ts @@ -23,6 +23,7 @@ export const createFileUploadSettings = () => type: 'string', public: true, i18nDescription: 'FileUpload_MediaTypeBlackListDescription', + alert: 'FileUpload_MediaTypeBlackList_Alert', }); await this.add('FileUpload_ProtectFiles', true, { diff --git a/apps/meteor/tests/data/interactions.ts b/apps/meteor/tests/data/interactions.ts index d14749181193..085d97d4ece3 100644 --- a/apps/meteor/tests/data/interactions.ts +++ b/apps/meteor/tests/data/interactions.ts @@ -1,5 +1,6 @@ export const targetUser = 'rocket.cat'; export const imgURL = './public/images/logo/1024x1024.png'; export const lstURL = './tests/e2e/fixtures/files/lst-test.lst'; +export const drawioURL = './tests/e2e/fixtures/files/diagram.drawio'; export const svgLogoURL = './public/images/logo/logo.svg'; export const svgLogoFileName = 'logo.svg'; diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index b382476c0a2f..0a5d1cfd2512 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -1,6 +1,7 @@ import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; import { createTargetChannel } from './utils'; +import { setSettingValueById } from './utils/setSettingValueById'; import { expect, test } from './utils/test'; test.use({ storageState: Users.user1.state }); @@ -10,6 +11,7 @@ test.describe.serial('file-upload', () => { let targetChannel: string; test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); targetChannel = await createTargetChannel(api); }); @@ -21,6 +23,7 @@ test.describe.serial('file-upload', () => { }); test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml'); expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200); }); @@ -54,4 +57,22 @@ test.describe.serial('file-upload', () => { await expect(poHomeChannel.content.getFileDescription).toHaveText('lst_description'); await expect(poHomeChannel.content.lastMessageFileName).toContainText('lst-test.lst'); }); + + test('expect send drawio (unknown media type) file succesfully', async ({ page }) => { + await page.reload(); + await poHomeChannel.content.sendFileMessage('diagram.drawio'); + await poHomeChannel.content.descriptionInput.fill('drawio_description'); + await poHomeChannel.content.btnModalConfirm.click(); + + await expect(poHomeChannel.content.getFileDescription).toHaveText('drawio_description'); + await expect(poHomeChannel.content.lastMessageFileName).toContainText('diagram.drawio'); + }); + + test('expect not to send drawio file (unknown media type) when the default media type is blocked', async ({ api, page }) => { + await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream'); + + await page.reload(); + await poHomeChannel.content.sendFileMessage('diagram.drawio'); + await expect(poHomeChannel.content.btnModalConfirm).not.toBeVisible(); + }); }); diff --git a/apps/meteor/tests/e2e/fixtures/files/diagram.drawio b/apps/meteor/tests/e2e/fixtures/files/diagram.drawio new file mode 100644 index 000000000000..a86c2673ab98 --- /dev/null +++ b/apps/meteor/tests/e2e/fixtures/files/diagram.drawio @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 72bd5819593e..0809d06b324d 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -7,7 +7,7 @@ import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; import { sleep } from '../../../lib/utils/sleep'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; -import { imgURL, lstURL, svgLogoFileName, svgLogoURL } from '../../data/interactions'; +import { drawioURL, imgURL, lstURL, svgLogoFileName, svgLogoURL } from '../../data/interactions'; import { getSettingValueById, updateEEPermission, updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { deleteTeam } from '../../data/teams.helper'; @@ -183,8 +183,8 @@ describe('[Rooms]', function () { }); }); - it('should upload a LST file to room', (done) => { - request + it('should upload a LST file to room', () => { + return request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) .attach('file', lstURL) @@ -200,12 +200,33 @@ describe('[Rooms]', function () { expect(res.body.message).to.have.property('files'); expect(res.body.message.files).to.be.an('array').of.length(1); expect(res.body.message.files[0]).to.have.property('name', 'lst-test.lst'); - }) - .end(done); + expect(res.body.message.files[0]).to.have.property('type', 'text/plain'); + }); + }); + + it('should upload a DRAWIO file (unknown media type) to room', () => { + return request + .post(api(`rooms.upload/${testChannel._id}`)) + .set(credentials) + .attach('file', drawioURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body.message).to.have.property('attachments'); + expect(res.body.message.attachments).to.be.an('array').of.length(1); + expect(res.body.message.attachments[0]).to.have.property('format', 'DRAWIO'); + expect(res.body.message.attachments[0]).to.have.property('title', 'diagram.drawio'); + expect(res.body.message).to.have.property('files'); + expect(res.body.message.files).to.be.an('array').of.length(1); + expect(res.body.message.files[0]).to.have.property('name', 'diagram.drawio'); + expect(res.body.message.files[0]).to.have.property('type', 'application/octet-stream'); + }); }); it('should not allow uploading a blocked media type to a room', async () => { - await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream'); + await updateSetting('FileUpload_MediaTypeBlackList', 'text/plain'); await request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) @@ -218,6 +239,20 @@ describe('[Rooms]', function () { }); }); + it('should not allow uploading an unknown media type to a room if the default one is blocked', async () => { + await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream'); + await request + .post(api(`rooms.upload/${testChannel._id}`)) + .set(credentials) + .attach('file', drawioURL) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-invalid-file-type'); + }); + }); + it('should be able to get the file', async () => { await request.get(fileNewUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); await request.get(fileOldUrl).set(credentials).expect('Content-Type', 'image/png').expect(200); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 836b534df601..1277a02375bf 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2383,6 +2383,7 @@ "FileUpload_MediaType_NotAccepted": "Media Types Not Accepted", "FileUpload_MediaTypeBlackList": "Blocked Media Types", "FileUpload_MediaTypeBlackListDescription": "Comma-separated list of media types. This setting has priority over the Accepted Media Types.", + "FileUpload_MediaTypeBlackList_Alert": "The default media type for unknown file extensions is \"application/octet-stream\", to work only with known file extensions you can add it to the \"Blocked Media Types\" list.", "FileUpload_MediaTypeWhiteList": "Accepted Media Types", "FileUpload_MediaTypeWhiteListDescription": "Comma-separated list of media types. Leave it blank for accepting all media types.", "FileUpload_ProtectFiles": "Protect Uploaded Files",