From b289bbf26c0ee0a88120148409fec10623c876f1 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Mon, 2 Oct 2023 19:51:20 +0000 Subject: [PATCH 01/12] Bump 6.4.1 --- .changeset/bump-patch-1696276280745.md | 5 +++++ yarn.lock | 18 +++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 .changeset/bump-patch-1696276280745.md diff --git a/.changeset/bump-patch-1696276280745.md b/.changeset/bump-patch-1696276280745.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1696276280745.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/yarn.lock b/yarn.lock index 08bafa660252..84c187b685cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8226,16 +8226,16 @@ __metadata: typescript: ~5.2.2 peerDependencies: "@rocket.chat/apps-engine": "*" - "@rocket.chat/eslint-config": 0.6.0-rc.0 + "@rocket.chat/eslint-config": 0.6.0 "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/fuselage-polyfills": "*" "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 2.0.0-rc.5 + "@rocket.chat/ui-contexts": 2.0.0 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": 2.0.0-rc.5 + "@rocket.chat/ui-video-conf": 2.0.0 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -8317,14 +8317,14 @@ __metadata: ts-jest: ~29.0.5 typescript: ~5.2.2 peerDependencies: - "@rocket.chat/core-typings": 6.4.0-rc.5 + "@rocket.chat/core-typings": 6.4.0 "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 2.0.0-rc.5 - "@rocket.chat/ui-contexts": 2.0.0-rc.5 + "@rocket.chat/ui-client": 2.0.0 + "@rocket.chat/ui-contexts": 2.0.0 katex: "*" react: "*" languageName: unknown @@ -9447,7 +9447,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 2.0.0-rc.5 + "@rocket.chat/ui-contexts": 2.0.0 react: ~17.0.2 languageName: unknown linkType: soft @@ -9599,7 +9599,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 2.0.0-rc.5 + "@rocket.chat/ui-contexts": 2.0.0 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -9683,7 +9683,7 @@ __metadata: typescript: ~5.2.2 peerDependencies: "@rocket.chat/layout": "*" - "@rocket.chat/ui-contexts": 2.0.0-rc.5 + "@rocket.chat/ui-contexts": 2.0.0 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 0f7289174229744e7ce3bfca850e8a5609601d33 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Fri, 29 Sep 2023 01:40:51 -0300 Subject: [PATCH 02/12] chore: Prevent call license and registration status endpoints when not enough permission (#30336) --- apps/meteor/client/hooks/useLicense.ts | 20 ++++++++++++++----- .../client/hooks/useRegistrationStatus.ts | 20 ++++++++++++++----- .../hooks/useAdministrationItems.spec.tsx | 9 +++++++-- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/apps/meteor/client/hooks/useLicense.ts b/apps/meteor/client/hooks/useLicense.ts index 99b7e5e3461c..0f568d9bd5cc 100644 --- a/apps/meteor/client/hooks/useLicense.ts +++ b/apps/meteor/client/hooks/useLicense.ts @@ -1,13 +1,23 @@ import type { OperationResult } from '@rocket.chat/rest-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; export const useLicense = (): UseQueryResult> => { const getLicenses = useEndpoint('GET', '/v1/licenses.get'); + const canViewLicense = usePermission('view-privileged-setting'); - return useQuery(['licenses', 'getLicenses'], () => getLicenses(), { - staleTime: Infinity, - keepPreviousData: true, - }); + return useQuery( + ['licenses', 'getLicenses'], + () => { + if (!canViewLicense) { + throw new Error('unauthorized api call'); + } + return getLicenses(); + }, + { + staleTime: Infinity, + keepPreviousData: true, + }, + ); }; diff --git a/apps/meteor/client/hooks/useRegistrationStatus.ts b/apps/meteor/client/hooks/useRegistrationStatus.ts index 8b091459291b..9260d672bec5 100644 --- a/apps/meteor/client/hooks/useRegistrationStatus.ts +++ b/apps/meteor/client/hooks/useRegistrationStatus.ts @@ -1,13 +1,23 @@ import type { OperationResult } from '@rocket.chat/rest-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; export const useRegistrationStatus = (): UseQueryResult> => { const getRegistrationStatus = useEndpoint('GET', '/v1/cloud.registrationStatus'); + const canViewregistrationStatus = usePermission('manage-cloud'); - return useQuery(['getRegistrationStatus'], () => getRegistrationStatus(), { - keepPreviousData: true, - staleTime: Infinity, - }); + return useQuery( + ['getRegistrationStatus'], + () => { + if (!canViewregistrationStatus) { + throw new Error('unauthorized api call'); + } + return getRegistrationStatus(); + }, + { + keepPreviousData: true, + staleTime: Infinity, + }, + ); }; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx index 248b91418739..b0b20972d346 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.spec.tsx @@ -19,12 +19,14 @@ it('should not show upgrade item if has license and not have trial', async () => workspaceRegistered: false, } as any, })) + .withPermission('view-privileged-setting') + .withPermission('manage-cloud') .build(), }); await waitFor(() => !!(result.all.length > 1)); - expect(result.current).toEqual([]); + expect(result.current.length).toEqual(1); }); it('should return an upgrade item if not have license or if have a trial', async () => { @@ -42,10 +44,13 @@ it('should return an upgrade item if not have license or if have a trial', async workspaceRegistered: false, } as any, })) + .withPermission('view-privileged-setting') + .withPermission('manage-cloud') .build(), }); - await waitFor(() => !!result.current[0]); + // Workspace admin is also expected to be here + await waitFor(() => result.current.length > 1); expect(result.current[0]).toEqual( expect.objectContaining({ From 5e3473a2c9a3150e549bee022ea466b2f5de6359 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Fri, 29 Sep 2023 01:50:27 -0300 Subject: [PATCH 03/12] feat: Auto-enable autotranslate (#30370) --- .changeset/large-pandas-beam.md | 5 + .../functions/addUserToDefaultChannels.ts | 3 + .../app/lib/server/functions/addUserToRoom.ts | 6 +- .../app/lib/server/functions/createRoom.ts | 6 +- .../rocketchat-i18n/i18n/en.i18n.json | 2 + ...tSubscriptionAutotranslateDefaultConfig.ts | 28 ++++ .../meteor/server/methods/addAllUserToRoom.ts | 3 + apps/meteor/server/settings/message.ts | 8 ++ .../tests/end-to-end/api/00-autotranslate.js | 130 +++++++++++++++++- 9 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 .changeset/large-pandas-beam.md create mode 100644 apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts diff --git a/.changeset/large-pandas-beam.md b/.changeset/large-pandas-beam.md new file mode 100644 index 000000000000..19f1eade9a9b --- /dev/null +++ b/.changeset/large-pandas-beam.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +New setting to automatically enable autotranslate when joining rooms diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index 6dc477a2926f..ad632a3b7dfc 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -3,6 +3,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise { await callbacks.run('beforeJoinDefaultChannels', user); @@ -11,6 +12,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: }).toArray(); for await (const room of defaultRooms) { if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { + const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user); // Add a subscription to this user await Subscriptions.createWithRoomAndUser(room, user, { ts: new Date(), @@ -20,6 +22,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: userMentions: 1, groupMentions: 0, ...(room.favorite && { f: true }), + ...autoTranslateConfig, }); // Insert user joined message diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 660af823de9e..41000cda2038 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; import { AppEvents, Apps } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; export const addUserToRoom = async function ( @@ -24,7 +25,7 @@ export const addUserToRoom = async function ( }); } - const userToBeAdded = typeof user !== 'string' ? user : await Users.findOneByUsername(user.replace('@', '')); + const userToBeAdded = typeof user === 'string' ? await Users.findOneByUsername(user.replace('@', '')) : await Users.findOneById(user._id); const roomDirectives = roomCoordinator.getRoomDirectives(room.t); if (!userToBeAdded) { @@ -70,6 +71,8 @@ export const addUserToRoom = async function ( await callbacks.run('beforeJoinRoom', userToBeAdded, room); } + const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(userToBeAdded); + await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { ts: now, open: true, @@ -77,6 +80,7 @@ export const addUserToRoom = async function ( unread: 1, userMentions: 1, groupMentions: 0, + ...autoTranslateConfig, }); if (!userToBeAdded.username) { diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 192139f96b7c..30cf2a593700 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Message, Team } from '@rocket.chat/core-services'; import type { ICreateRoomParams, ISubscriptionExtraData } from '@rocket.chat/core-services'; @@ -8,6 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { Apps } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; +import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { createDirectRoom } from './createDirectRoom'; @@ -178,7 +180,9 @@ export const createRoom = async ( extra.ls = now; } - await Subscriptions.createWithRoomAndUser(room, member, extra); + const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(member); + + await Subscriptions.createWithRoomAndUser(room, member, { ...extra, ...autoTranslateConfig }); } } diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 13a0e561739b..270d2f4575c7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -713,6 +713,8 @@ "AutoTranslate_DeepL": "DeepL", "AutoTranslate_Enabled": "Enable Auto-Translate", "AutoTranslate_Enabled_Description": "Enabling auto-translation will allow people with the `auto-translate` permission to have all messages automatically translated into their selected language. Fees may apply.", + "AutoTranslate_AutoEnableOnJoinRoom": "Auto-Translate for non-default language members", + "AutoTranslate_AutoEnableOnJoinRoom_Description": "If enabled, whenever a user with a language preference different than the workspace default joins a room, it will be automatically translated for them.", "AutoTranslate_Google": "Google", "AutoTranslate_Microsoft": "Microsoft", "AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key", diff --git a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts new file mode 100644 index 000000000000..13540246f0e6 --- /dev/null +++ b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts @@ -0,0 +1,28 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; + +export const getSubscriptionAutotranslateDefaultConfig = async ( + user: IUser, +): Promise< + | { + autoTranslate: boolean; + autoTranslateLanguage: string; + } + | undefined +> => { + const [autoEnableSetting, languageSetting] = await Promise.all([ + Settings.findOneById('AutoTranslate_AutoEnableOnJoinRoom'), + Settings.findOneById('Language'), + ]); + const { language: userLanguage } = user.settings?.preferences || {}; + + if (!autoEnableSetting?.value) { + return; + } + + if (!userLanguage || userLanguage === 'default' || languageSetting?.value === userLanguage) { + return; + } + + return { autoTranslate: true, autoTranslateLanguage: userLanguage }; +}; diff --git a/apps/meteor/server/methods/addAllUserToRoom.ts b/apps/meteor/server/methods/addAllUserToRoom.ts index cbbafccfe60b..acba1bed406b 100644 --- a/apps/meteor/server/methods/addAllUserToRoom.ts +++ b/apps/meteor/server/methods/addAllUserToRoom.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { settings } from '../../app/settings/server'; import { callbacks } from '../../lib/callbacks'; +import { getSubscriptionAutotranslateDefaultConfig } from '../lib/getSubscriptionAutotranslateDefaultConfig'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -55,6 +56,7 @@ Meteor.methods({ continue; } await callbacks.run('beforeJoinRoom', user, room); + const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user); await Subscriptions.createWithRoomAndUser(room, user, { ts: now, open: true, @@ -62,6 +64,7 @@ Meteor.methods({ unread: 1, userMentions: 1, groupMentions: 0, + ...autoTranslateConfig, }); await Message.saveSystemMessage('uj', rid, user.username || '', user, { ts: now }); await callbacks.run('afterJoinRoom', user, room); diff --git a/apps/meteor/server/settings/message.ts b/apps/meteor/server/settings/message.ts index b0cda60fe60a..17dd1f7b230d 100644 --- a/apps/meteor/server/settings/message.ts +++ b/apps/meteor/server/settings/message.ts @@ -245,6 +245,14 @@ export const createMessageSettings = () => public: true, }); + await this.add('AutoTranslate_AutoEnableOnJoinRoom', false, { + type: 'boolean', + group: 'Message', + section: 'AutoTranslate', + public: true, + enableQuery: [{ _id: 'AutoTranslate_Enabled', value: true }], + }); + await this.add('AutoTranslate_ServiceProvider', 'google-translate', { type: 'select', group: 'Message', diff --git a/apps/meteor/tests/end-to-end/api/00-autotranslate.js b/apps/meteor/tests/end-to-end/api/00-autotranslate.js index 52adb69f17c7..48bb021ce388 100644 --- a/apps/meteor/tests/end-to-end/api/00-autotranslate.js +++ b/apps/meteor/tests/end-to-end/api/00-autotranslate.js @@ -1,9 +1,12 @@ import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, describe, after, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage } from '../../data/chat.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; +import { createRoom } from '../../data/rooms.helper'; +import { password } from '../../data/user'; +import { createUser, login } from '../../data/users.helper.js'; describe('AutoTranslate', function () { this.retries(0); @@ -314,5 +317,130 @@ describe('AutoTranslate', function () { .end(done); }); }); + describe('Autoenable setting', () => { + let userA; + let userB; + let credA; + let credB; + let channel; + + const createChannel = async (members, cred) => + (await createRoom({ type: 'c', members, name: `channel-test-${Date.now()}`, credentials: cred })).body.channel; + + const setLanguagePref = async (language, cred) => { + await request + .post(api('users.setPreferences')) + .set(cred) + .send({ data: { language } }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }; + + const getSub = async (roomId, cred) => + ( + await request + .get(api('subscriptions.getOne')) + .set(cred) + .query({ + roomId, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription').and.to.be.an('object'); + }) + ).body.subscription; + + before(async () => { + await updateSetting('AutoTranslate_Enabled', true); + await updateSetting('AutoTranslate_AutoEnableOnJoinRoom', true); + await updateSetting('Language', 'pt-BR'); + + channel = await createChannel(); + userA = await createUser(); + userB = await createUser(); + + credA = await login(userA.username, password); + credB = await login(userB.username, password); + + await setLanguagePref('en', credB); + }); + + after(async () => { + await updateSetting('AutoTranslate_AutoEnableOnJoinRoom', false); + await updateSetting('AutoTranslate_Enabled', false); + await updateSetting('Language', ''); + }); + + it("should do nothing if the user hasn't changed his language preference", async () => { + const sub = await getSub(channel._id, credentials); + expect(sub).to.not.have.property('autoTranslate'); + expect(sub).to.not.have.property('autoTranslateLanguage'); + }); + + it("should do nothing if the user changed his language preference to be the same as the server's", async () => { + await setLanguagePref('pt-BR', credA); + + const channel = await createChannel(undefined, credA); + const sub = await getSub(channel._id, credA); + expect(sub).to.not.have.property('autoTranslate'); + expect(sub).to.not.have.property('autoTranslateLanguage'); + }); + + it('should enable autotranslate with the correct language when creating a new room', async () => { + await setLanguagePref('en', credA); + + const channel = await createChannel(undefined, credA); + const sub = await getSub(channel._id, credA); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate for all the members added to the room upon creation', async () => { + const channel = await createChannel([userA.username, userB.username]); + const subA = await getSub(channel._id, credA); + expect(subA).to.have.property('autoTranslate'); + expect(subA).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + + const subB = await getSub(channel._id, credB); + expect(subB).to.have.property('autoTranslate'); + expect(subB).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate with the correct language when joining a room', async () => { + await request + .post(api('channels.join')) + .set(credA) + .send({ + roomId: channel._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const sub = await getSub(channel._id, credA); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + + it('should enable autotranslate with the correct language when added to a room', async () => { + await request + .post(api('channels.invite')) + .set(credentials) + .send({ + roomId: channel._id, + userId: userB._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const sub = await getSub(channel._id, credB); + expect(sub).to.have.property('autoTranslate'); + expect(sub).to.have.property('autoTranslateLanguage').and.to.be.equal('en'); + }); + }); }); }); From 919fe1f33d2cd40344e56044b9181abf3c444cb6 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Tue, 3 Oct 2023 18:23:02 -0300 Subject: [PATCH 04/12] chore: get translations from CDN (#30331) --- .changeset/soft-cows-juggle.md | 5 +++++ apps/meteor/client/providers/TranslationProvider.tsx | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/soft-cows-juggle.md diff --git a/.changeset/soft-cows-juggle.md b/.changeset/soft-cows-juggle.md new file mode 100644 index 000000000000..6fcb20506483 --- /dev/null +++ b/.changeset/soft-cows-juggle.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +download translation files through CDN diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index e379d13dfb79..2cf47066c4e4 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -2,7 +2,7 @@ import { useLocalStorage, useMutableCallback } from '@rocket.chat/fuselage-hooks import languages from '@rocket.chat/i18n/dist/languages'; import en from '@rocket.chat/i18n/src/locales/en.i18n.json'; import type { TranslationKey, TranslationContextValue } from '@rocket.chat/ui-contexts'; -import { useMethod, useSetting, TranslationContext, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; +import { useMethod, useSetting, TranslationContext } from '@rocket.chat/ui-contexts'; import type i18next from 'i18next'; import I18NextHttpBackend from 'i18next-http-backend'; import sprintf from 'i18next-sprintf-postprocessor'; @@ -12,6 +12,7 @@ import React, { useEffect, useMemo } from 'react'; import { I18nextProvider, initReactI18next, useTranslation } from 'react-i18next'; import { CachedCollectionManager } from '../../app/ui-cached-collection/client'; +import { getURL } from '../../app/utils/client'; import { i18n, addSprinfToI18n } from '../../app/utils/lib/i18n'; import { AppClientOrchestratorInstance } from '../../ee/client/apps/orchestrator'; import { applyCustomTranslations } from '../lib/utils/applyCustomTranslations'; @@ -39,8 +40,6 @@ const parseToJSON = (customTranslations: string): Record>(); const useI18next = (lng: string): typeof i18next => { - const basePath = useAbsoluteUrl()('/i18n'); - const customTranslations = useSetting('Custom_Translations'); const parsedCustomTranslations = useMemo(() => { @@ -105,17 +104,18 @@ const useI18next = (lng: string): typeof i18next => { partialBundledLanguages: true, defaultNS: 'core', backend: { - loadPath: `${basePath}/{{lng}}.json`, + loadPath: 'i18n/{{lng}}.json', parse: (data: string, lngs?: string | string[], namespaces: string | string[] = []) => extractKeys(JSON.parse(data), lngs, namespaces), request: (_options, url, _payload, callback) => { const params = url.split('/'); + const lng = params[params.length - 1]; let promise = localeCache.get(lng); if (!promise) { - promise = fetch(url).then((res) => res.text()); + promise = fetch(getURL(url)).then((res) => res.text()); localeCache.set(lng, promise); } From 2fa78b0563c6e4baf4f841980a4071f88ca1aaaa Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 5 Oct 2023 16:22:30 -0600 Subject: [PATCH 05/12] feat: Monitors able to forward chats without joining (#30549) --- .changeset/selfish-hounds-pay.md | 5 +++++ .../Omnichannel/QuickActions/hooks/useQuickActions.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/selfish-hounds-pay.md diff --git a/.changeset/selfish-hounds-pay.md b/.changeset/selfish-hounds-pay.md new file mode 100644 index 000000000000..3ca321bd392f --- /dev/null +++ b/.changeset/selfish-hounds-pay.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Monitors now able to forward a chat without taking it first diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 7f376341992d..54ce71bd80ec 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -300,8 +300,9 @@ export const useQuickActions = (): { const manualOnHoldAllowed = useSetting('Livechat_allow_manual_on_hold'); const hasManagerRole = useRole('livechat-manager'); + const hasMonitorRole = useRole('livechat-monitor'); - const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole) && room?.lastMessage?.t !== 'livechat-close'; + const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole || hasMonitorRole) && room?.lastMessage?.t !== 'livechat-close'; const canMoveQueue = !!omnichannelRouteConfig?.returnQueue && room?.u !== undefined; const canForwardGuest = usePermission('transfer-livechat-guest'); const canSendTranscriptEmail = usePermission('send-omnichannel-chat-transcript'); From dacdebbd86179a7e3b1befec44b5903a326184d5 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 5 Oct 2023 20:17:34 -0300 Subject: [PATCH 06/12] chore: Improve `groups.create` endpoint for large amounts of members (#30499) --- apps/meteor/app/api/server/v1/channels.ts | 10 +- apps/meteor/app/api/server/v1/groups.ts | 44 +++--- apps/meteor/app/apps/server/bridges/rooms.ts | 6 +- .../server/methods/createDiscussion.ts | 2 +- .../server/classes/ImportDataConverter.ts | 6 +- .../functions/addUserToDefaultChannels.ts | 2 +- .../app/lib/server/functions/addUserToRoom.ts | 2 +- .../app/lib/server/functions/createRoom.ts | 140 +++++++++++------- .../app/lib/server/methods/createChannel.ts | 5 +- .../lib/server/methods/createPrivateGroup.ts | 28 ++-- .../meteor-accounts-saml/server/lib/SAML.ts | 4 +- .../app/slashcommands-create/server/server.ts | 8 +- .../slashcommands-inviteall/server/server.ts | 5 +- .../utils/lib/getDefaultSubscriptionPref.ts | 4 +- apps/meteor/ee/server/lib/ldap/Manager.ts | 18 ++- apps/meteor/ee/server/lib/oauth/Manager.ts | 10 +- apps/meteor/lib/callbacks.ts | 3 +- ...tSubscriptionAutotranslateDefaultConfig.ts | 27 ++-- apps/meteor/server/lib/roles/addUserRoles.ts | 7 +- .../meteor/server/methods/addAllUserToRoom.ts | 2 +- .../server/methods/createDirectMessage.ts | 6 +- .../meteor/server/models/raw/Subscriptions.ts | 44 +++++- apps/meteor/server/models/raw/Users.js | 16 ++ .../rocket-chat/adapters/Room.ts | 15 +- apps/meteor/server/services/room/service.ts | 6 +- .../core-services/src/types/IRoomService.ts | 1 + .../src/models/ISubscriptionsModel.ts | 18 ++- .../model-typings/src/models/IUsersModel.ts | 3 +- .../src/v1/channels/ChannelsCreateProps.ts | 1 + .../src/v1/groups/GroupsCreateProps.ts | 1 + 30 files changed, 295 insertions(+), 149 deletions(-) diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 70b7fc875082..84f8b604a644 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -671,7 +671,14 @@ async function createChannelValidator(params: { async function createChannel( userId: string, - params: { name?: string; members?: string[]; customFields?: Record; extraData?: Record; readOnly?: boolean }, + params: { + name?: string; + members?: string[]; + customFields?: Record; + extraData?: Record; + readOnly?: boolean; + excludeSelf?: boolean; + }, ): Promise<{ channel: IRoom }> { const readOnly = typeof params.readOnly !== 'undefined' ? params.readOnly : false; const id = await createChannelMethod( @@ -681,6 +688,7 @@ async function createChannel( readOnly, params.customFields, params.extraData, + params.excludeSelf, ); return { diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index df54b683fda4..ef18d4256348 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,4 +1,4 @@ -import { Team } from '@rocket.chat/core-services'; +import { Team, isMeteorError } from '@rocket.chat/core-services'; import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { check, Match } from 'meteor/check'; @@ -302,10 +302,6 @@ API.v1.addRoute( { authRequired: true }, { async post() { - if (!(await hasPermissionAsync(this.userId, 'create-p'))) { - return API.v1.unauthorized(); - } - if (!this.bodyParams.name) { return API.v1.failure('Body param "name" is required'); } @@ -323,24 +319,32 @@ API.v1.addRoute( const readOnly = typeof this.bodyParams.readOnly !== 'undefined' ? this.bodyParams.readOnly : false; - const result = await createPrivateGroupMethod( - this.userId, - this.bodyParams.name, - this.bodyParams.members ? this.bodyParams.members : [], - readOnly, - this.bodyParams.customFields, - this.bodyParams.extraData, - ); - - const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude }); + try { + const result = await createPrivateGroupMethod( + this.user, + this.bodyParams.name, + this.bodyParams.members ? this.bodyParams.members : [], + readOnly, + this.bodyParams.customFields, + this.bodyParams.extraData, + this.bodyParams.excludeSelf ?? false, + ); + + const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude }); + if (!room) { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } - if (!room) { - throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + return API.v1.success({ + group: await composeRoomWithLastMessage(room, this.userId), + }); + } catch (error: unknown) { + if (isMeteorError(error) && error.reason === 'error-not-allowed') { + return API.v1.unauthorized(); + } } - return API.v1.success({ - group: await composeRoomWithLastMessage(room, this.userId), - }); + return API.v1.internalError(); }, }, ); diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 481292d61790..91b0049513f0 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -55,7 +55,11 @@ export class AppRoomBridge extends RoomBridge { } private async createPrivateGroup(userId: string, room: ICoreRoom, members: string[]): Promise { - return (await createPrivateGroupMethod(userId, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid; + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('Invalid user'); + } + return (await createPrivateGroupMethod(user, room.name || '', members, room.ro, room.customFields, this.prepareExtraData(room))).rid; } protected async getById(roomId: string, appId: string): Promise { diff --git a/apps/meteor/app/discussion/server/methods/createDiscussion.ts b/apps/meteor/app/discussion/server/methods/createDiscussion.ts index ce5c09947a60..50f2f5877657 100644 --- a/apps/meteor/app/discussion/server/methods/createDiscussion.ts +++ b/apps/meteor/app/discussion/server/methods/createDiscussion.ts @@ -156,7 +156,7 @@ const create = async ({ const discussion = await createRoom( type, name, - user.username as string, + user, [...new Set(invitedUsers)].filter(Boolean), false, false, diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index f241879cdc67..1b596d625d9b 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -1034,7 +1034,11 @@ export class ImportDataConverter { return; } if (roomData.t === 'p') { - roomInfo = await createPrivateGroupMethod(startedByUserId, roomData.name, members, false, {}, {}, true); + const user = await Users.findOneById(startedByUserId); + if (!user) { + throw new Error('importer-channel-invalid-creator'); + } + roomInfo = await createPrivateGroupMethod(user, roomData.name, members, false, {}, {}, true); } else { roomInfo = await createChannelMethod(startedByUserId, roomData.name, members, false, {}, {}, true); } diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index ad632a3b7dfc..835f59419ad5 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -12,7 +12,7 @@ export const addUserToDefaultChannels = async function (user: IUser, silenced?: }).toArray(); for await (const room of defaultRooms) { if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { - const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user); + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); // Add a subscription to this user await Subscriptions.createWithRoomAndUser(room, user, { ts: new Date(), diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 41000cda2038..4e29576cf3bb 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -71,7 +71,7 @@ export const addUserToRoom = async function ( await callbacks.run('beforeJoinRoom', userToBeAdded, room); } - const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(userToBeAdded); + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(userToBeAdded); await Subscriptions.createWithRoomAndUser(room, userToBeAdded as IUser, { ts: now, diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 30cf2a593700..312451f54845 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -10,7 +10,6 @@ import { Apps } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; -import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; import { createDirectRoom } from './createDirectRoom'; @@ -21,10 +20,90 @@ const isValidName = (name: unknown): name is string => { const onlyUsernames = (members: unknown): members is string[] => Array.isArray(members) && members.every((member) => typeof member === 'string'); +async function createUsersSubscriptions({ + room, + shouldBeHandledByFederation, + members, + now, + owner, + options, +}: { + room: IRoom; + shouldBeHandledByFederation: boolean; + members: string[]; + now: Date; + owner: IUser; + options?: ICreateRoomParams['options']; +}) { + if (shouldBeHandledByFederation) { + const extra: Partial = options?.subscriptionExtra || {}; + extra.open = true; + extra.ls = now; + + if (room.prid) { + extra.prid = room.prid; + } + + await Subscriptions.createWithRoomAndUser(room, owner, extra); + + return; + } + + const subs = []; + + const memberIds = []; + + const membersCursor = Users.findUsersByUsernames>(members, { + projection: { 'username': 1, 'settings.preferences': 1, 'federated': 1, 'roles': 1 }, + }); + + for await (const member of membersCursor) { + try { + await callbacks.run('federation.beforeAddUserToARoom', { user: member, inviter: owner }, room); + await callbacks.run('beforeAddedToRoom', { user: member, inviter: owner }); + } catch (error) { + continue; + } + + memberIds.push(member._id); + + const extra: Partial = options?.subscriptionExtra || {}; + + extra.open = true; + + if (room.prid) { + extra.prid = room.prid; + } + + if (member.username === owner.username) { + extra.ls = now; + extra.roles = ['owner']; + } + + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(member); + + subs.push({ + user: member, + extraData: { + ...extra, + ...autoTranslateConfig, + }, + }); + } + + if (!['d', 'l'].includes(room.t)) { + await Users.addRoomByUserIds(memberIds, room._id); + } + + await Subscriptions.createWithRoomAndManyUsers(room, subs); + + await Rooms.incUsersCountById(room._id, subs.length); +} + export const createRoom = async ( type: T, name: T extends 'd' ? undefined : string, - ownerUsername: string | undefined, + owner: T extends 'd' ? IUser | undefined : IUser, members: T extends 'd' ? IUser[] : string[] = [], excludeSelf?: boolean, readOnly?: boolean, @@ -47,7 +126,7 @@ export const createRoom = async ( // options, }); if (type === 'd') { - return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || ownerUsername }); + return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?.username }); } if (!onlyUsernames(members)) { @@ -63,15 +142,13 @@ export const createRoom = async ( }); } - if (!ownerUsername) { + if (!owner) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createRoom', }); } - const owner = await Users.findOneByUsernameIgnoringCase(ownerUsername, { projection: { username: 1, name: 1 } }); - - if (!ownerUsername || !owner) { + if (!owner?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createRoom', }); @@ -140,53 +217,12 @@ export const createRoom = async ( if (type === 'c') { await callbacks.run('beforeCreateChannel', owner, roomProps); } - const room = await Rooms.createWithFullRoomData(roomProps); - const shouldBeHandledByFederation = room.federated === true || ownerUsername.includes(':'); - if (shouldBeHandledByFederation) { - const extra: Partial = options?.subscriptionExtra || {}; - extra.open = true; - extra.ls = now; - if (room.prid) { - extra.prid = room.prid; - } - - await Subscriptions.createWithRoomAndUser(room, owner, extra); - } else { - for await (const username of [...new Set(members)]) { - const member = await Users.findOneByUsername(username, { - projection: { 'username': 1, 'settings.preferences': 1, 'federated': 1, 'roles': 1 }, - }); - if (!member) { - continue; - } - - try { - await callbacks.run('federation.beforeAddUserToARoom', { user: member, inviter: owner }, room); - await callbacks.run('beforeAddedToRoom', { user: member, inviter: owner }); - } catch (error) { - continue; - } - - const extra: Partial = options?.subscriptionExtra || {}; - - extra.open = true; - - if (room.prid) { - extra.prid = room.prid; - } - - if (username === owner.username) { - extra.ls = now; - } - - const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(member); + const room = await Rooms.createWithFullRoomData(roomProps); - await Subscriptions.createWithRoomAndUser(room, member, { ...extra, ...autoTranslateConfig }); - } - } + const shouldBeHandledByFederation = room.federated === true || owner.username.includes(':'); - await addUserRolesAsync(owner._id, ['owner'], room._id); + await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation }); if (type === 'c') { if (room.teamId) { @@ -195,7 +231,7 @@ export const createRoom = async ( await Message.saveSystemMessage('user-added-room-to-team', team.roomId, room.name || '', owner); } } - await callbacks.run('afterCreateChannel', owner, room); + callbacks.runAsync('afterCreateChannel', owner, room); } else if (type === 'p') { callbacks.runAsync('afterCreatePrivateGroup', owner, room); } diff --git a/apps/meteor/app/lib/server/methods/createChannel.ts b/apps/meteor/app/lib/server/methods/createChannel.ts index ff8182cec8c9..98cea517bed4 100644 --- a/apps/meteor/app/lib/server/methods/createChannel.ts +++ b/apps/meteor/app/lib/server/methods/createChannel.ts @@ -35,8 +35,7 @@ export const createChannelMethod = async ( throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } - const user = await Users.findOneById(userId, { projection: { username: 1 } }); - + const user = await Users.findOneById(userId); if (!user?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'createChannel' }); } @@ -44,7 +43,7 @@ export const createChannelMethod = async ( if (!(await hasPermissionAsync(userId, 'create-c'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createChannel' }); } - return createRoom('c', name, user.username, members, excludeSelf, readOnly, { + return createRoom('c', name, user, members, excludeSelf, readOnly, { customFields, ...extraData, }); diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts index 65298949a345..75097b5c89b8 100644 --- a/apps/meteor/app/lib/server/methods/createPrivateGroup.ts +++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.ts @@ -1,4 +1,4 @@ -import type { ICreatedRoom } from '@rocket.chat/core-typings'; +import type { ICreatedRoom, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Match, check } from 'meteor/check'; @@ -21,7 +21,7 @@ declare module '@rocket.chat/ui-contexts' { } export const createPrivateGroupMethod = async ( - userId: string, + user: IUser, name: string, members: string[], readOnly = false, @@ -35,23 +35,12 @@ export const createPrivateGroupMethod = async ( > => { check(name, String); check(members, Match.Optional([String])); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createPrivateGroup', - }); - } - const user = await Users.findOneById(userId, { projection: { username: 1 } }); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'createPrivateGroup', - }); - } - if (!(await hasPermissionAsync(userId, 'create-p'))) { + if (!(await hasPermissionAsync(user._id, 'create-p'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createPrivateGroup' }); } - return createRoom('p', name, user.username, members, excludeSelf, readOnly, { + return createRoom('p', name, user, members, excludeSelf, readOnly, { customFields, ...extraData, }); @@ -67,6 +56,13 @@ Meteor.methods({ }); } - return createPrivateGroupMethod(uid, name, members, readOnly, customFields, extraData); + const user = await Users.findOneById(uid); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'createPrivateGroup', + }); + } + + return createPrivateGroupMethod(user, name, members, readOnly, customFields, extraData); }, }); diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index 06c3014a8a56..f62ab71f2302 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -480,7 +480,6 @@ export class SAML { continue; } - const room = await Rooms.findOneByNameAndType(roomName, 'c', {}); const privRoom = await Rooms.findOneByNameAndType(roomName, 'p', {}); if (privRoom && includePrivateChannelsInUpdate === true) { @@ -488,6 +487,7 @@ export class SAML { continue; } + const room = await Rooms.findOneByNameAndType(roomName, 'c', {}); if (room) { await addUserToRoom(room._id, user); continue; @@ -496,7 +496,7 @@ export class SAML { if (!room && !privRoom) { // If the user doesn't have an username yet, we can't create new rooms for them if (user.username) { - await createRoom('c', roomName, user.username); + await createRoom('c', roomName, user); } } } diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts index a3c70f012fa1..104d50c56926 100644 --- a/apps/meteor/app/slashcommands-create/server/server.ts +++ b/apps/meteor/app/slashcommands-create/server/server.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { Rooms, Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { createChannelMethod } from '../../lib/server/methods/createChannel'; @@ -50,7 +50,11 @@ slashCommands.add({ } if (getParams(params).indexOf('private') > -1) { - await createPrivateGroupMethod(userId, channelStr, []); + const user = await Users.findOneById(userId); + if (!user) { + return; + } + await createPrivateGroupMethod(user, channelStr, []); return; } diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index 9917775aca06..5376bd6ae64b 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -37,6 +37,9 @@ function inviteAll(type: T): SlashCommand['callback'] { } const user = await Users.findOneById(userId); + if (!user) { + return; + } const lng = user?.language || settings.get('Language') || 'en'; const baseChannel = type === 'to' ? await Rooms.findOneById(message.rid) : await Rooms.findOneByName(channel); @@ -69,7 +72,7 @@ function inviteAll(type: T): SlashCommand['callback'] { const users = (await cursor.toArray()).map((s: ISubscription) => s.u.username).filter(isTruthy); if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) { - baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(userId, channel, users); + baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(user, channel, users); void api.broadcast('notify.ephemeralMessage', userId, message.rid, { msg: i18n.t('Channel_created', { postProcess: 'sprintf', diff --git a/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts b/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts index a388548c18a8..adb4c2ab1ae9 100644 --- a/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts +++ b/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts @@ -1,4 +1,4 @@ -import type { ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, ISubscription, IUser } from '@rocket.chat/core-typings'; /** * @type {(userPref: Pick) => { @@ -7,7 +7,7 @@ import type { ISubscription, IUser } from '@rocket.chat/core-typings'; * emailPrefOrigin: 'user'; * }} */ -export const getDefaultSubscriptionPref = (userPref: IUser) => { +export const getDefaultSubscriptionPref = (userPref: AtLeast) => { const subscription: Partial = {}; const { desktopNotifications, pushNotifications, emailNotificationMode, highlights } = userPref.settings?.preferences || {}; diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index deb6cdcec666..6c04574ad557 100644 --- a/apps/meteor/ee/server/lib/ldap/Manager.ts +++ b/apps/meteor/ee/server/lib/ldap/Manager.ts @@ -1,6 +1,6 @@ import { Team } from '@rocket.chat/core-services'; import type { ILDAPEntry, IUser, IRoom, IRole, IImportUser, IImportRecord } from '@rocket.chat/core-typings'; -import { Users as UsersRaw, Roles, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models'; +import { Users, Roles, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models'; import type ldapjs from 'ldapjs'; import type { @@ -271,10 +271,12 @@ export class LDAPEEManager extends LDAPManager { logger.debug(`Channel '${channel}' doesn't exist, creating it.`); const roomOwner = settings.get('LDAP_Sync_User_Data_Channels_Admin') || ''; - // #ToDo: Remove typecastings when createRoom is converted to ts. - const room = await createRoom('c', channel, roomOwner, [], false, false, { + + const user = await Users.findOneByUsernameIgnoringCase(roomOwner); + + const room = await createRoom('c', channel, user, [], false, false, { customFields: { ldap: true }, - } as any); + }); if (!room?.rid) { logger.error(`Unable to auto-create channel '${channel}' during ldap sync.`); return; @@ -574,7 +576,7 @@ export class LDAPEEManager extends LDAPManager { } private static async updateExistingUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise { - const users = await UsersRaw.findLDAPUsers().toArray(); + const users = await Users.findLDAPUsers().toArray(); for await (const user of users) { const ldapUser = await this.findLDAPUser(ldap, user); @@ -586,7 +588,7 @@ export class LDAPEEManager extends LDAPManager { } private static async updateUserAvatars(ldap: LDAPConnection): Promise { - const users = await UsersRaw.findLDAPUsers().toArray(); + const users = await Users.findLDAPUsers().toArray(); for await (const user of users) { const ldapUser = await this.findLDAPUser(ldap, user); if (!ldapUser) { @@ -615,7 +617,7 @@ export class LDAPEEManager extends LDAPManager { } private static async logoutDeactivatedUsers(ldap: LDAPConnection): Promise { - const users = await UsersRaw.findConnectedLDAPUsers().toArray(); + const users = await Users.findConnectedLDAPUsers().toArray(); for await (const user of users) { const ldapUser = await this.findLDAPUser(ldap, user); @@ -624,7 +626,7 @@ export class LDAPEEManager extends LDAPManager { } if (this.isUserDeactivated(ldapUser)) { - await UsersRaw.unsetLoginTokens(user._id); + await Users.unsetLoginTokens(user._id); } } } diff --git a/apps/meteor/ee/server/lib/oauth/Manager.ts b/apps/meteor/ee/server/lib/oauth/Manager.ts index b24d7436a784..b75c8aa9a7a5 100644 --- a/apps/meteor/ee/server/lib/oauth/Manager.ts +++ b/apps/meteor/ee/server/lib/oauth/Manager.ts @@ -1,6 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; -import { Roles, Rooms } from '@rocket.chat/models'; +import { Roles, Rooms, Users } from '@rocket.chat/models'; import { addUserToRoom } from '../../../../app/lib/server/functions/addUserToRoom'; import { createRoom } from '../../../../app/lib/server/functions/createRoom'; @@ -20,6 +20,12 @@ export class OAuthEEManager { if (channelsMap && user && identity && groupClaimName) { const groupsFromSSO = identity[groupClaimName] || []; + const userChannelAdmin = await Users.findOneByUsernameIgnoringCase(channelsAdmin); + if (!userChannelAdmin) { + logger.error(`could not create channel, user not found: ${channelsAdmin}`); + return; + } + for await (const ssoGroup of Object.keys(channelsMap)) { if (typeof ssoGroup === 'string') { let channels = channelsMap[ssoGroup]; @@ -30,7 +36,7 @@ export class OAuthEEManager { const name = await getValidRoomName(channel.trim(), undefined, { allowDuplicates: true }); let room = await Rooms.findOneByNonValidatedName(name); if (!room) { - const createdRoom = await createRoom('c', channel, channelsAdmin, [], false, false); + const createdRoom = await createRoom('c', channel, userChannelAdmin, [], false, false); if (!createdRoom?.rid) { logger.error(`could not create channel ${channel}`); return; diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 46a27357f546..27e362f07e09 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -20,6 +20,7 @@ import type { InquiryWithAgentInfo, ILivechatTagRecord, TransferData, + AtLeast, } from '@rocket.chat/core-typings'; import type { FilterOperators } from 'mongodb'; @@ -59,7 +60,7 @@ interface EventLikeCallbackSignatures { 'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void; 'livechat.afterAgentRemoved': (params: { agent: Pick }) => void; 'afterAddedToRoom': (params: { user: IUser; inviter?: IUser }, room: IRoom) => void; - 'beforeAddedToRoom': (params: { user: IUser; inviter: IUser }) => void; + 'beforeAddedToRoom': (params: { user: AtLeast; inviter: IUser }) => void; 'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id'] }) => void; 'beforeDeleteRoom': (params: IRoom) => void; 'beforeJoinDefaultChannels': (user: IUser) => void; diff --git a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts index 13540246f0e6..92e76d8c2ec1 100644 --- a/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts +++ b/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts @@ -1,28 +1,23 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Settings } from '@rocket.chat/models'; +import type { AtLeast, IUser } from '@rocket.chat/core-typings'; -export const getSubscriptionAutotranslateDefaultConfig = async ( - user: IUser, -): Promise< +import { settings } from '../../app/settings/server'; + +export function getSubscriptionAutotranslateDefaultConfig(user: AtLeast): | { autoTranslate: boolean; autoTranslateLanguage: string; } - | undefined -> => { - const [autoEnableSetting, languageSetting] = await Promise.all([ - Settings.findOneById('AutoTranslate_AutoEnableOnJoinRoom'), - Settings.findOneById('Language'), - ]); - const { language: userLanguage } = user.settings?.preferences || {}; - - if (!autoEnableSetting?.value) { + | undefined { + if (!settings.get('AutoTranslate_AutoEnableOnJoinRoom')) { return; } - if (!userLanguage || userLanguage === 'default' || languageSetting?.value === userLanguage) { + const languageSetting = settings.get('Language'); + + const { language: userLanguage } = user.settings?.preferences || {}; + if (!userLanguage || userLanguage === 'default' || languageSetting === userLanguage) { return; } return { autoTranslate: true, autoTranslateLanguage: userLanguage }; -}; +} diff --git a/apps/meteor/server/lib/roles/addUserRoles.ts b/apps/meteor/server/lib/roles/addUserRoles.ts index 395056903ae4..a064553f5cb4 100644 --- a/apps/meteor/server/lib/roles/addUserRoles.ts +++ b/apps/meteor/server/lib/roles/addUserRoles.ts @@ -1,6 +1,6 @@ import { MeteorError } from '@rocket.chat/core-services'; import type { IRole, IUser, IRoom } from '@rocket.chat/core-typings'; -import { Users, Roles } from '@rocket.chat/models'; +import { Roles } from '@rocket.chat/models'; import { validateRoleList } from './validateRoleList'; @@ -9,11 +9,6 @@ export const addUserRolesAsync = async (userId: IUser['_id'], roleIds: IRole['_i return false; } - const user = await Users.findOneById(userId, { projection: { _id: 1 } }); - if (!user) { - throw new MeteorError('error-invalid-user', 'Invalid user'); - } - if (!(await validateRoleList(roleIds))) { throw new MeteorError('error-invalid-role', 'Invalid role'); } diff --git a/apps/meteor/server/methods/addAllUserToRoom.ts b/apps/meteor/server/methods/addAllUserToRoom.ts index acba1bed406b..11232908b847 100644 --- a/apps/meteor/server/methods/addAllUserToRoom.ts +++ b/apps/meteor/server/methods/addAllUserToRoom.ts @@ -56,7 +56,7 @@ Meteor.methods({ continue; } await callbacks.run('beforeJoinRoom', user, room); - const autoTranslateConfig = await getSubscriptionAutotranslateDefaultConfig(user); + const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); await Subscriptions.createWithRoomAndUser(room, user, { ts: now, open: true, diff --git a/apps/meteor/server/methods/createDirectMessage.ts b/apps/meteor/server/methods/createDirectMessage.ts index d92c7e46292e..ccbfe8916cae 100644 --- a/apps/meteor/server/methods/createDirectMessage.ts +++ b/apps/meteor/server/methods/createDirectMessage.ts @@ -104,7 +104,11 @@ export async function createDirectMessage( } catch (error) { throw new Meteor.Error((error as any)?.message); } - const { _id: rid, inserted, ...room } = await createRoom('d', undefined, undefined, roomUsers as IUser[], false, undefined, {}, options); + const { + _id: rid, + inserted, + ...room + } = await createRoom<'d'>('d', undefined, undefined, roomUsers as IUser[], false, undefined, {}, options); return { // @ts-expect-error - room type is already defined in the `createRoom` return type diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 4b42367bad05..c4ba44bdd7f9 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -1,4 +1,13 @@ -import type { IRole, IRoom, ISubscription, IUser, RocketChatRecordDeleted, RoomType, SpotlightUser } from '@rocket.chat/core-typings'; +import type { + AtLeast, + IRole, + IRoom, + ISubscription, + IUser, + RocketChatRecordDeleted, + RoomType, + SpotlightUser, +} from '@rocket.chat/core-typings'; import type { ISubscriptionsModel } from '@rocket.chat/model-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -17,6 +26,7 @@ import type { IndexDescription, UpdateFilter, InsertOneResult, + InsertManyResult, } from 'mongodb'; import { getDefaultSubscriptionPref } from '../../../app/utils/lib/getDefaultSubscriptionPref'; @@ -1605,6 +1615,38 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return result; } + async createWithRoomAndManyUsers( + room: IRoom, + users: { user: AtLeast; extraData: Record }[] = [], + ): Promise> { + const subscriptions = users.map(({ user, extraData }) => ({ + open: false, + alert: false, + unread: 0, + userMentions: 0, + groupMentions: 0, + ts: room.ts, + rid: room._id, + name: room.name, + fname: room.fname, + ...(room.customFields && { customFields: room.customFields }), + t: room.t, + u: { + _id: user._id, + username: user.username, + name: user.name, + }, + ...(room.prid && { prid: room.prid }), + ...getDefaultSubscriptionPref(user), + ...extraData, + })); + + // @ts-expect-error - types not good :( + const result = await this.insertMany(subscriptions); + + return result; + } + // REMOVE async removeByUserId(userId: string): Promise { const query = { diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 0663bbdcda28..113f18ea83da 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -384,6 +384,10 @@ export class UsersRaw extends BaseRaw { } findOneByUsernameIgnoringCase(username, options) { + if (!username) { + throw new Error('invalid username'); + } + const query = { username }; return this.findOne(query, { @@ -1488,6 +1492,18 @@ export class UsersRaw extends BaseRaw { ); } + addRoomByUserIds(uids, rid) { + return this.updateMany( + { + _id: { $in: uids }, + __rooms: { $ne: rid }, + }, + { + $addToSet: { __rooms: rid }, + }, + ); + } + removeRoomByRoomIds(rids) { return this.updateMany( { diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts index 018a5f87704c..c4aee8bcf2aa 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Room.ts @@ -58,7 +58,12 @@ export class RocketChatRoomAdapter { .trim() .replace(/ /g, '-'), ); - const { rid, _id } = await createRoom(federatedRoom.getRoomType(), roomName, usernameOrId); + const owner = await Users.findOneByUsernameIgnoringCase(usernameOrId); + if (!owner) { + throw new Error('Cannot create a room without a creator'); + } + + const { rid, _id } = await createRoom(federatedRoom.getRoomType(), roomName, owner); const roomId = rid || _id; await MatrixBridgedRoom.createOrUpdateByLocalRoomId( roomId, @@ -90,10 +95,16 @@ export class RocketChatRoomAdapter { const readonly = false; const excludeSelf = false; const extraData = undefined; + + const owner = await Users.findOneByUsernameIgnoringCase(usernameOrId); + if (!owner) { + throw new Error('Cannot create a room without a creator'); + } + const { rid, _id } = await createRoom( federatedRoom.getRoomType(), federatedRoom.getDisplayName(), - usernameOrId, + owner, federatedRoom.getMembersUsernames(), excludeSelf, readonly, diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index ac978da88c77..a0c4ac703de2 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -22,15 +22,13 @@ export class RoomService extends ServiceClassInternal implements IRoomService { throw new Error('no-permission'); } - const user = await Users.findOneById>(uid, { - projection: { username: 1 }, - }); + const user = await Users.findOneById(uid); if (!user?.username) { throw new Error('User not found'); } // TODO convert `createRoom` function to "raw" and move to here - return createRoom(type, name, user.username, members, false, readOnly, extraData, options) as unknown as IRoom; + return createRoom(type, name, user, members, false, readOnly, extraData, options) as unknown as IRoom; } async createDirectMessage({ to, from }: { to: string; from: string }): Promise<{ rid: string }> { diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index e69707e18a36..35d96a417d86 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -4,6 +4,7 @@ export interface ISubscriptionExtraData { open: boolean; ls?: Date; prid?: string; + roles?: string[]; } interface ICreateRoomOptions extends Partial> { diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index aebda87c78cb..53b0a69ec232 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -1,5 +1,15 @@ -import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Document, AggregateOptions, Filter, InsertOneResult } from 'mongodb'; +import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser, AtLeast } from '@rocket.chat/core-typings'; +import type { + FindOptions, + FindCursor, + UpdateResult, + DeleteResult, + Document, + AggregateOptions, + Filter, + InsertOneResult, + InsertManyResult, +} from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -216,6 +226,10 @@ export interface ISubscriptionsModel extends IBaseModel { ): Promise; removeByUserId(userId: string): Promise; createWithRoomAndUser(room: IRoom, user: IUser, extraData?: Record): Promise>; + createWithRoomAndManyUsers( + room: IRoom, + users: { user: AtLeast; extraData: Record }[], + ): Promise>; removeByRoomIdsAndUserId(rids: string[], userId: string): Promise; removeByRoomIdAndUserId(roomId: string, userId: string): Promise; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index c0ce51f79f45..cf49283e7fc7 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -239,6 +239,7 @@ export interface IUsersModel extends IBaseModel { removeAllRoomsByUserId(userId: string): Promise; removeRoomByUserId(userId: string, rid: string): Promise; addRoomByUserId(userId: string, rid: string): Promise; + addRoomByUserIds(uids: string[], rid: string): Promise; removeRoomByRoomIds(rids: string[]): Promise; getLoginTokensByUserId(userId: string): FindCursor; addPersonalAccessTokenToUser(data: { userId: string; loginTokenObject: IPersonalAccessToken }): Promise; @@ -317,7 +318,7 @@ export interface IUsersModel extends IBaseModel { findByUsernameNameOrEmailAddress(nameOrUsernameOrEmail: string, options?: FindOptions): FindCursor; findCrowdUsers(options?: FindOptions): FindCursor; getLastLogin(options?: FindOptions): Promise; - findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; + findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; findUsersByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIdsNotOffline(userIds: string[], options?: FindOptions): FindCursor; diff --git a/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts b/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts index c8bb88cfc1c6..e25dfb0ce2fb 100644 --- a/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts +++ b/packages/rest-typings/src/v1/channels/ChannelsCreateProps.ts @@ -12,6 +12,7 @@ export type ChannelsCreateProps = { encrypted?: boolean; teamId?: string; }; + excludeSelf?: boolean; }; const channelsCreatePropsSchema = { diff --git a/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts b/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts index c34a720bd4b7..7c3781d787c4 100644 --- a/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts +++ b/packages/rest-typings/src/v1/groups/GroupsCreateProps.ts @@ -14,6 +14,7 @@ export type GroupsCreateProps = { encrypted: boolean; teamId?: string; }; + excludeSelf?: boolean; }; const GroupsCreatePropsSchema = { From 636a412866eff5a7016b3bf95f5bd403e30a09a2 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 6 Oct 2023 11:27:15 -0600 Subject: [PATCH 07/12] fix: Remove monitors query restrictions on update (#30550) --- .changeset/dull-trainers-drive.md | 5 ++++ .../app/livechat/server/lib/Livechat.js | 2 +- .../hooks/applyDepartmentRestrictions.ts | 9 +++--- .../server/hooks/applyRoomRestrictions.ts | 1 + .../server/methods/getUnitsFromUserRoles.ts | 30 ++++++++++++++----- .../ee/server/models/raw/LivechatRooms.ts | 30 ------------------- .../ee/server/models/raw/LivechatUnit.ts | 10 ------- .../meteor/server/models/raw/LivechatRooms.ts | 15 ---------- 8 files changed, 35 insertions(+), 67 deletions(-) create mode 100644 .changeset/dull-trainers-drive.md diff --git a/.changeset/dull-trainers-drive.md b/.changeset/dull-trainers-drive.md new file mode 100644 index 000000000000..f5a673cd8c30 --- /dev/null +++ b/.changeset/dull-trainers-drive.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Remove model-level query restrictions for monitors diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 52138740e295..0cba7c66c9eb 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -285,7 +285,7 @@ export const Livechat = { Livechat.logger.debug(`Closing open chats for user ${userId}`); const user = await Users.findOneById(userId); - const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}); + const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); const promises = []; await openChats.forEach((room) => { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts index ef3261b83482..3c3b1aae6135 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyDepartmentRestrictions.ts @@ -4,16 +4,17 @@ import type { FilterOperators } from 'mongodb'; import { hasRoleAsync } from '../../../../../app/authorization/server/functions/hasRole'; import { callbacks } from '../../../../../lib/callbacks'; import { cbLogger } from '../lib/logger'; -import { getUnitsFromUser } from '../lib/units'; +import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles'; -export const addQueryRestrictionsToDepartmentsModel = async (originalQuery: FilterOperators = {}) => { +export const addQueryRestrictionsToDepartmentsModel = async (originalQuery: FilterOperators = {}, userId: string) => { const query: FilterOperators = { ...originalQuery, type: { $ne: 'u' } }; - const units = await getUnitsFromUser(); + const units = await getUnitsFromUser(userId); if (Array.isArray(units)) { query.ancestors = { $in: units }; } + cbLogger.debug({ msg: 'Applying department query restrictions', userId, units }); return query; }; @@ -26,7 +27,7 @@ callbacks.add( } cbLogger.debug('Applying department query restrictions'); - return addQueryRestrictionsToDepartmentsModel(originalQuery); + return addQueryRestrictionsToDepartmentsModel(originalQuery, userId); }, callbacks.priority.HIGH, 'livechat-apply-department-restrictions', diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts index bde3b6d9e31a..cacbd550ef04 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts @@ -21,6 +21,7 @@ export const restrictQuery = async (originalQuery: FilterOperators { +async function getUnitsFromUserRoles(user: string): Promise { + return LivechatUnit.findByMonitorId(user); +} + +async function getDepartmentsFromUserRoles(user: string): Promise { + return (await LivechatDepartmentAgents.findByAgentId(user).toArray()).map((department) => department.departmentId); +} + +const memoizedGetUnitFromUserRoles = mem(getUnitsFromUserRoles, { maxAge: 10000 }); +const memoizedGetDepartmentsFromUserRoles = mem(getDepartmentsFromUserRoles, { maxAge: 5000 }); + +export const getUnitsFromUser = async (user: string): Promise => { if (!user || (await hasAnyRoleAsync(user, ['admin', 'livechat-manager']))) { return; } @@ -14,10 +26,11 @@ async function getUnitsFromUserRoles(user: string | null): Promise({ - 'livechat:getUnitsFromUser'(): Promise { + async 'livechat:getUnitsFromUser'(): Promise { const user = Meteor.userId(); - return memoizedGetUnitFromUserRoles(user); + if (!user) { + return; + } + return getUnitsFromUser(user); }, }); diff --git a/apps/meteor/ee/server/models/raw/LivechatRooms.ts b/apps/meteor/ee/server/models/raw/LivechatRooms.ts index 5c3bbb1296e0..3fc8dd84954b 100644 --- a/apps/meteor/ee/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/ee/server/models/raw/LivechatRooms.ts @@ -12,7 +12,6 @@ import type { FindCursor, UpdateResult, Document, FindOptions, Db, Collection, F import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { LivechatRoomsRaw } from '../../../../server/models/raw/LivechatRooms'; import { queriesLogger } from '../../../app/livechat-enterprise/server/lib/logger'; -import { addQueryRestrictionsToRoomsModel } from '../../../app/livechat-enterprise/server/lib/query.helper'; declare module '@rocket.chat/model-typings' { interface ILivechatRoomsModel { @@ -310,35 +309,6 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo return this.updateOne(query, update); } - /** @deprecated Use updateOne or updateMany instead */ - async update(...args: Parameters) { - const [query, ...restArgs] = args; - const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.update', query: restrictedQuery }); - return super.update(restrictedQuery, ...restArgs); - } - - async updateOne(...args: [...Parameters, { bypassUnits?: boolean }?]) { - const [query, update, opts, extraOpts] = args; - if (extraOpts?.bypassUnits) { - // When calling updateOne from a service, we cannot call the meteor code inside the query restrictions - // So the solution now is to pass a bypassUnits flag to the updateOne method which prevents checking - // units restrictions on the query, but just for the query the service is actually using - // We need to find a way of remove the meteor dependency when fetching units, and then, we can remove this flag - return super.updateOne(query, update, opts); - } - const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateOne', query: restrictedQuery }); - return super.updateOne(restrictedQuery, update, opts); - } - - async updateMany(...args: Parameters) { - const [query, ...restArgs] = args; - const restrictedQuery = await addQueryRestrictionsToRoomsModel(query); - queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateMany', query: restrictedQuery }); - return super.updateMany(restrictedQuery, ...restArgs); - } - getConversationsBySource(start: Date, end: Date, extraQuery: Filter): AggregationCursor { return this.col.aggregate( [ diff --git a/apps/meteor/ee/server/models/raw/LivechatUnit.ts b/apps/meteor/ee/server/models/raw/LivechatUnit.ts index b49cbb959df1..aaceb4437699 100644 --- a/apps/meteor/ee/server/models/raw/LivechatUnit.ts +++ b/apps/meteor/ee/server/models/raw/LivechatUnit.ts @@ -54,16 +54,6 @@ export class LivechatUnitRaw extends BaseRaw implement return this.col.findOne(query, options); } - async update( - originalQuery: Filter, - update: Filter, - options: FindOptions, - ): Promise { - const query = await addQueryRestrictions(originalQuery); - queriesLogger.debug({ msg: 'LivechatUnit.update', query }); - return this.col.updateOne(query, update, options); - } - remove(query: Filter): Promise { return this.deleteMany(query); } diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index 38eab9056586..73afcadefa41 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -1516,11 +1516,6 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive { $set: { pdfTranscriptRequested: true }, }, - {}, - // @ts-expect-error - extra arg not on base types - { - bypassUnits: true, - }, ); } @@ -1532,11 +1527,6 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive { $unset: { pdfTranscriptRequested: 1 }, }, - {}, - // @ts-expect-error - extra arg not on base types - { - bypassUnits: true, - }, ); } @@ -1548,11 +1538,6 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive { $set: { pdfTranscriptFileId: fileId }, }, - {}, - // @ts-expect-error - extra arg not on base types - { - bypassUnits: true, - }, ); } From 851c40c7a211d70ba60d64b2320896b70285d7a5 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 6 Oct 2023 16:32:24 -0300 Subject: [PATCH 08/12] chore: add missing `_id` field to `AtLeast` (#30592) --- apps/meteor/lib/callbacks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 27e362f07e09..a1480b825edb 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -60,7 +60,7 @@ interface EventLikeCallbackSignatures { 'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void; 'livechat.afterAgentRemoved': (params: { agent: Pick }) => void; 'afterAddedToRoom': (params: { user: IUser; inviter?: IUser }, room: IRoom) => void; - 'beforeAddedToRoom': (params: { user: AtLeast; inviter: IUser }) => void; + 'beforeAddedToRoom': (params: { user: AtLeast; inviter: IUser }) => void; 'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id'] }) => void; 'beforeDeleteRoom': (params: IRoom) => void; 'beforeJoinDefaultChannels': (user: IUser) => void; From eb4881ca534b1530498ff816b00b7cadd371825d Mon Sep 17 00:00:00 2001 From: Guilherme Jun Grillo <48109548+guijun13@users.noreply.github.com> Date: Fri, 6 Oct 2023 17:19:15 -0300 Subject: [PATCH 09/12] fix: user dropdown menu position on RTL layout (#30490) --- .changeset/sweet-feet-relate.md | 5 +++++ apps/meteor/client/sidebar/header/UserMenu.tsx | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/sweet-feet-relate.md diff --git a/.changeset/sweet-feet-relate.md b/.changeset/sweet-feet-relate.md new file mode 100644 index 000000000000..f7da740ebcc0 --- /dev/null +++ b/.changeset/sweet-feet-relate.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: user dropdown menu position on RTL layout diff --git a/apps/meteor/client/sidebar/header/UserMenu.tsx b/apps/meteor/client/sidebar/header/UserMenu.tsx index 9fcc7a0d2274..a53836eda311 100644 --- a/apps/meteor/client/sidebar/header/UserMenu.tsx +++ b/apps/meteor/client/sidebar/header/UserMenu.tsx @@ -24,6 +24,7 @@ const UserMenu = ({ user }: { user: IUser }) => { } + placement='bottom-end' selectionMode='multiple' sections={sections} title={t('User_menu')} @@ -36,6 +37,7 @@ const UserMenu = ({ user }: { user: IUser }) => { } medium + placement='bottom-end' selectionMode='multiple' sections={sections} title={t('User_menu')} From 0d14dc49c3e53cf933d1af1cc889163ad86d38cd Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 6 Oct 2023 20:29:39 -0300 Subject: [PATCH 10/12] feat: New permission to kick users (#30535) Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- .changeset/seven-carpets-march.md | 5 ++ apps/meteor/app/api/server/v1/groups.ts | 74 ++++++++++--------- .../server/constant/permissions.ts | 2 + .../rocketchat-i18n/i18n/en.i18n.json | 4 + apps/meteor/server/lib/migrations.ts | 27 +++++-- .../server/methods/removeUserFromRoom.ts | 28 ++++--- apps/meteor/server/startup/migrations/xrun.js | 6 +- .../core-typings/src/migrations/IControl.ts | 1 + 8 files changed, 96 insertions(+), 51 deletions(-) create mode 100644 .changeset/seven-carpets-march.md diff --git a/.changeset/seven-carpets-march.md b/.changeset/seven-carpets-march.md new file mode 100644 index 000000000000..46fd1b7ddb62 --- /dev/null +++ b/.changeset/seven-carpets-march.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Add new permission to allow kick users from rooms without being a member diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index ef18d4256348..8f2999cee71e 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -26,29 +26,7 @@ import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; -// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -async function findPrivateGroupByIdOrName({ - params, - checkedArchived = true, - userId, -}: { - params: - | { - roomId?: string; - } - | { - roomName?: string; - }; - userId: string; - checkedArchived?: boolean; -}): Promise<{ - rid: string; - open: boolean; - ro: boolean; - t: string; - name: string; - broadcast: boolean; -}> { +async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise { if ( (!('roomId' in params) && !('roomName' in params)) || ('roomId' in params && !(params as { roomId?: string }).roomId && 'roomName' in params && !(params as { roomName?: string }).roomName) @@ -68,17 +46,48 @@ async function findPrivateGroupByIdOrName({ broadcast: 1, }, }; - let room: IRoom | null = null; - if ('roomId' in params) { - room = await Rooms.findOneById(params.roomId || '', roomOptions); - } else if ('roomName' in params) { - room = await Rooms.findOneByName(params.roomName || '', roomOptions); - } + + const room = await (() => { + if ('roomId' in params) { + return Rooms.findOneById(params.roomId || '', roomOptions); + } + if ('roomName' in params) { + return Rooms.findOneByName(params.roomName || '', roomOptions); + } + })(); if (!room || room.t !== 'p') { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); } + return room; +} + +// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property +async function findPrivateGroupByIdOrName({ + params, + checkedArchived = true, + userId, +}: { + params: + | { + roomId?: string; + } + | { + roomName?: string; + }; + userId: string; + checkedArchived?: boolean; +}): Promise<{ + rid: string; + open: boolean; + ro: boolean; + t: string; + name: string; + broadcast: boolean; +}> { + const room = await getRoomFromParams(params); + const user = await Users.findOneById(userId, { projections: { username: 1 } }); if (!room || !user || !(await canAccessRoomAsync(room, user))) { @@ -585,17 +594,14 @@ API.v1.addRoute( { authRequired: true }, { async post() { - const findResult = await findPrivateGroupByIdOrName({ - params: this.bodyParams, - userId: this.userId, - }); + const room = await getRoomFromParams(this.bodyParams); const user = await getUserFromParams(this.bodyParams); if (!user?.username) { return API.v1.failure('Invalid user'); } - await removeUserFromRoomMethod(this.userId, { rid: findResult.rid, username: user.username }); + await removeUserFromRoomMethod(this.userId, { rid: room._id, username: user.username }); return API.v1.success(); }, diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index fc917028c33f..7b5f1594e5c3 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -10,6 +10,8 @@ export const permissions = [ { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, { _id: 'add-user-to-any-c-room', roles: ['admin'] }, { _id: 'add-user-to-any-p-room', roles: [] }, + { _id: 'kick-user-from-any-c-room', roles: ['admin'] }, + { _id: 'kick-user-from-any-p-room', roles: [] }, { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] }, { _id: 'archive-room', roles: ['admin', 'owner'] }, { _id: 'assign-admin-role', roles: ['admin'] }, diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 270d2f4575c7..00398e8c2f66 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2750,6 +2750,10 @@ "Jump_to_message": "Jump to message", "Jump_to_recent_messages": "Jump to recent messages", "Just_invited_people_can_access_this_channel": "Just invited people can access this channel.", + "kick-user-from-any-c-room": "Kick User from Any Public Channel", + "kick-user-from-any-c-room_description": "Permission to kick a user from any public channel", + "kick-user-from-any-p-room": "Kick User from Any Private Channel", + "kick-user-from-any-p-room_description": "Permission to kick a user from any private channel", "Katex_Dollar_Syntax": "Allow Dollar Syntax", "Katex_Dollar_Syntax_Description": "Allow using $$katex block$$ and $inline katex$ syntaxes", "Katex_Enabled": "Katex Enabled", diff --git a/apps/meteor/server/lib/migrations.ts b/apps/meteor/server/lib/migrations.ts index da3aeec761e6..f70b5bcca9ff 100644 --- a/apps/meteor/server/lib/migrations.ts +++ b/apps/meteor/server/lib/migrations.ts @@ -292,9 +292,24 @@ export async function migrateDatabase(targetVersion: 'latest' | number, subcomma return true; } -export const onFreshInstall = - (await getControl()).version !== 0 - ? async (): Promise => { - /* noop */ - } - : (fn: () => unknown): unknown => fn(); +export async function onServerVersionChange(cb: () => Promise): Promise { + const result = await Migrations.findOneAndUpdate( + { + _id: 'upgrade', + }, + { + $set: { + hash: Info.commit.hash, + }, + }, + { + upsert: true, + }, + ); + + if (result.value?.hash === Info.commit.hash) { + return; + } + + await cb(); +} diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index ea5bfa9edcff..2f29b1f55039 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -4,7 +4,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { getUsersInRole } from '../../app/authorization/server'; +import { canAccessRoomAsync, getUsersInRole } from '../../app/authorization/server'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../app/authorization/server/functions/hasRole'; import { RoomMemberActions } from '../../definition/IRoomTypeConfig'; @@ -35,8 +35,6 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri }); } - const removedUser = await Users.findOneByUsernameIgnoringCase(data.username); - const fromUser = await Users.findOneById(fromId); if (!fromUser) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { @@ -44,13 +42,25 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri }); } - const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, removedUser._id, { - projection: { _id: 1 }, - }); - if (!subscription) { - throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { - method: 'removeUserFromRoom', + // did this way so a ctrl-f would find the permission being used + const kickAnyUserPermission = room.t === 'c' ? 'kick-user-from-any-c-room' : 'kick-user-from-any-p-room'; + + const canKickAnyUser = await hasPermissionAsync(fromId, kickAnyUserPermission); + if (!canKickAnyUser && !(await canAccessRoomAsync(room, fromUser))) { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } + + const removedUser = await Users.findOneByUsernameIgnoringCase(data.username); + + if (!canKickAnyUser) { + const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, removedUser._id, { + projection: { _id: 1 }, }); + if (!subscription) { + throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { + method: 'removeUserFromRoom', + }); + } } if (await hasRoleAsync(removedUser._id, 'owner', room._id)) { diff --git a/apps/meteor/server/startup/migrations/xrun.js b/apps/meteor/server/startup/migrations/xrun.js index bd3d19a7cbee..1af7cb8ad8ad 100644 --- a/apps/meteor/server/startup/migrations/xrun.js +++ b/apps/meteor/server/startup/migrations/xrun.js @@ -1,9 +1,11 @@ import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions'; -import { migrateDatabase, onFreshInstall } from '../../lib/migrations'; +import { migrateDatabase, onServerVersionChange } from '../../lib/migrations'; const { MIGRATION_VERSION = 'latest' } = process.env; const [version, ...subcommands] = MIGRATION_VERSION.split(','); await migrateDatabase(version === 'latest' ? version : parseInt(version), subcommands); -await onFreshInstall(upsertPermissions); + +// if the server is starting with a different version we update the permissions +await onServerVersionChange(() => upsertPermissions()); diff --git a/packages/core-typings/src/migrations/IControl.ts b/packages/core-typings/src/migrations/IControl.ts index 9ff993703550..3f89ce730f1a 100644 --- a/packages/core-typings/src/migrations/IControl.ts +++ b/packages/core-typings/src/migrations/IControl.ts @@ -2,6 +2,7 @@ export type IControl = { _id: string; version: number; locked: boolean; + hash?: string; buildAt?: string | Date; lockedAt?: string | Date; }; From 5ee909bd94371963d63e7a6833db55659d93153e Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Mon, 9 Oct 2023 14:33:45 -0300 Subject: [PATCH 11/12] chore: Improve cache of static files (#30290) --- .changeset/wicked-humans-hang.md | 5 +++ apps/meteor/app/cors/server/cors.ts | 41 +++++++++++++++++++ .../custom-sounds/client/lib/CustomSounds.ts | 29 ++++++------- apps/meteor/app/ui-master/server/inject.ts | 24 +++++++---- apps/meteor/app/utils/client/getURL.ts | 6 +++ 5 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 .changeset/wicked-humans-hang.md diff --git a/.changeset/wicked-humans-hang.md b/.changeset/wicked-humans-hang.md new file mode 100644 index 000000000000..e793bc978902 --- /dev/null +++ b/.changeset/wicked-humans-hang.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Improve cache of static files diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 03a42e45a17b..cb6fa94273a2 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -1,4 +1,6 @@ +import { createHash } from 'crypto'; import type http from 'http'; +import type { UrlWithParsedQuery } from 'url'; import url from 'url'; import { Logger } from '@rocket.chat/logger'; @@ -77,6 +79,19 @@ WebApp.rawConnectHandlers.use((_req: http.IncomingMessage, res: http.ServerRespo }); const _staticFilesMiddleware = WebAppInternals.staticFilesMiddleware; +declare module 'meteor/webapp' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace WebApp { + function categorizeRequest( + req: http.IncomingMessage, + ): { arch: string; path: string; url: UrlWithParsedQuery } & Record; + } +} + +// These routes already handle cache control on their own +const cacheControlledRoutes: Array = ['/assets', '/custom-sounds', '/emoji-custom', '/avatar', '/file-upload'].map( + (route) => new RegExp(`^${route}`, 'i'), +); // @ts-expect-error - accessing internal property of webapp WebAppInternals.staticFilesMiddleware = function ( @@ -86,6 +101,32 @@ WebAppInternals.staticFilesMiddleware = function ( next: NextFunction, ) { res.setHeader('Access-Control-Allow-Origin', '*'); + const { arch, path, url } = WebApp.categorizeRequest(req); + + if (Meteor.isProduction && !cacheControlledRoutes.some((regexp) => regexp.test(path))) { + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + + // Prevent meteor_runtime_config.js to load from a different expected hash possibly causing + // a cache of the file for the wrong hash and start a client loop due to the mismatch + // of the hashes of ui versions which would be checked against a websocket response + if (path === '/meteor_runtime_config.js') { + const program = WebApp.clientPrograms[arch] as (typeof WebApp.clientPrograms)[string] & { + meteorRuntimeConfigHash?: string; + meteorRuntimeConfig: string; + }; + + if (!program?.meteorRuntimeConfigHash) { + program.meteorRuntimeConfigHash = createHash('sha1') + .update(JSON.stringify(encodeURIComponent(program.meteorRuntimeConfig))) + .digest('hex'); + } + + if (program.meteorRuntimeConfigHash !== url.query.hash) { + res.writeHead(404); + return res.end(); + } + } return _staticFilesMiddleware(staticFiles, req, res, next); }; diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts index a4f59136a1f9..f881c15f9886 100644 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts +++ b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts @@ -7,21 +7,22 @@ import { getURL } from '../../../utils/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; const getCustomSoundId = (soundId: ICustomSound['_id']) => `custom-sound-${soundId}`; +const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); const defaultSounds = [ - { _id: 'chime', name: 'Chime', extension: 'mp3', src: getURL('sounds/chime.mp3') }, - { _id: 'door', name: 'Door', extension: 'mp3', src: getURL('sounds/door.mp3') }, - { _id: 'beep', name: 'Beep', extension: 'mp3', src: getURL('sounds/beep.mp3') }, - { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getURL('sounds/chelle.mp3') }, - { _id: 'ding', name: 'Ding', extension: 'mp3', src: getURL('sounds/ding.mp3') }, - { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getURL('sounds/droplet.mp3') }, - { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getURL('sounds/highbell.mp3') }, - { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getURL('sounds/seasons.mp3') }, - { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getURL('sounds/telephone.mp3') }, - { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getURL('sounds/outbound-call-ringing.mp3') }, - { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getURL('sounds/call-ended.mp3') }, - { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getURL('sounds/dialtone.mp3') }, - { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getURL('sounds/ringtone.mp3') }, + { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, + { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, + { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, + { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, + { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, + { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, + { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, + { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, + { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, + { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, + { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, + { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, + { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, ]; class CustomSoundsClass { @@ -85,7 +86,7 @@ class CustomSoundsClass { } getURL(sound: ICustomSound) { - return getURL(`/custom-sounds/${sound._id}.${sound.extension}?_dc=${sound.random || 0}`); + return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); } getList() { diff --git a/apps/meteor/app/ui-master/server/inject.ts b/apps/meteor/app/ui-master/server/inject.ts index 78112bcee343..1e00a0e47433 100644 --- a/apps/meteor/app/ui-master/server/inject.ts +++ b/apps/meteor/app/ui-master/server/inject.ts @@ -32,7 +32,7 @@ const callback: NextHandleFunction = (req, res, next) => { return; } - const injection = headInjections.get(pathname.replace(/^\//, '')) as Injection | undefined; + const injection = headInjections.get(pathname.replace(/^\//, '').split('_')[0]) as Injection | undefined; if (!injection || typeof injection === 'string') { next(); @@ -76,27 +76,37 @@ export const injectIntoHead = (key: string, value: Injection): void => { }; export const addScript = (key: string, content: string): void => { + if (/_/.test(key)) { + throw new Error('inject.js > addScript - key cannot contain "_" (underscore)'); + } + if (!content.trim()) { - injectIntoHead(`${key}.js`, ''); + injectIntoHead(key, ''); return; } const currentHash = crypto.createHash('sha1').update(content).digest('hex'); - injectIntoHead(`${key}.js`, { + + injectIntoHead(key, { type: 'JS', - tag: ``, + tag: ``, content, }); }; export const addStyle = (key: string, content: string): void => { + if (/_/.test(key)) { + throw new Error('inject.js > addStyle - key cannot contain "_" (underscore)'); + } + if (!content.trim()) { - injectIntoHead(`${key}.css`, ''); + injectIntoHead(key, ''); return; } const currentHash = crypto.createHash('sha1').update(content).digest('hex'); - injectIntoHead(`${key}.css`, { + + injectIntoHead(key, { type: 'CSS', - tag: ``, + tag: ``, content, }); }; diff --git a/apps/meteor/app/utils/client/getURL.ts b/apps/meteor/app/utils/client/getURL.ts index 91ef0989bd19..040b6dfa9dc2 100644 --- a/apps/meteor/app/utils/client/getURL.ts +++ b/apps/meteor/app/utils/client/getURL.ts @@ -1,13 +1,19 @@ import { settings } from '../../settings/client'; import { getURLWithoutSettings } from '../lib/getURL'; +import { Info } from '../rocketchat.info'; export const getURL = function ( path: string, // eslint-disable-next-line @typescript-eslint/naming-convention params: Record = {}, cloudDeepLinkUrl?: string, + cacheKey?: boolean, ): string { const cdnPrefix = settings.get('CDN_PREFIX') || ''; const siteUrl = settings.get('Site_Url') || ''; + if (cacheKey) { + path += `${path.includes('?') ? '&' : '?'}cacheKey=${Info.version}`; + } + return getURLWithoutSettings(path, params, cdnPrefix, siteUrl, cloudDeepLinkUrl); }; From 2cb2047a92ad554fe8f12ca7718f9d515e18f972 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 9 Oct 2023 14:47:35 -0300 Subject: [PATCH 12/12] chore: change changeset to patch --- .changeset/large-pandas-beam.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/large-pandas-beam.md b/.changeset/large-pandas-beam.md index 19f1eade9a9b..e6a2e24b1562 100644 --- a/.changeset/large-pandas-beam.md +++ b/.changeset/large-pandas-beam.md @@ -1,5 +1,5 @@ --- -"@rocket.chat/meteor": minor +"@rocket.chat/meteor": patch --- New setting to automatically enable autotranslate when joining rooms