diff --git a/.changeset/angry-rocks-try.md b/.changeset/angry-rocks-try.md new file mode 100644 index 000000000000..8072b9db48fb --- /dev/null +++ b/.changeset/angry-rocks-try.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue causing monitors to dissapear from a saved unit every time a user saved the item. This was caused by the UI not sending the correct _id of the monitors that were already saved, and this caused the Backend to ignore them and remove from the list. diff --git a/.changeset/breezy-geckos-sparkle.md b/.changeset/breezy-geckos-sparkle.md new file mode 100644 index 000000000000..c64ffe920282 --- /dev/null +++ b/.changeset/breezy-geckos-sparkle.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix an issue where read receipts menu item wasn't considering the enabled setting to be displayed diff --git a/.changeset/cold-beds-hope.md b/.changeset/cold-beds-hope.md new file mode 100644 index 000000000000..33fc910e424f --- /dev/null +++ b/.changeset/cold-beds-hope.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue not allowing override retention policy in channels diff --git a/.changeset/cuddly-cycles-nail.md b/.changeset/cuddly-cycles-nail.md new file mode 100644 index 000000000000..ee49600ee865 --- /dev/null +++ b/.changeset/cuddly-cycles-nail.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/fuselage-ui-kit": minor +"@rocket.chat/ui-kit": minor +--- + +Introduced new elements for apps to select users diff --git a/.changeset/cuddly-maps-peel.md b/.changeset/cuddly-maps-peel.md new file mode 100644 index 000000000000..1d4d8913ec7c --- /dev/null +++ b/.changeset/cuddly-maps-peel.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed streams being called when the user is not logged in diff --git a/.changeset/cuddly-owls-join.md b/.changeset/cuddly-owls-join.md new file mode 100644 index 000000000000..0ace3d145d37 --- /dev/null +++ b/.changeset/cuddly-owls-join.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue that prevented CAS users from being merged with existing user data on login diff --git a/.changeset/fair-grapes-thank.md b/.changeset/fair-grapes-thank.md new file mode 100644 index 000000000000..2d8962f40db9 --- /dev/null +++ b/.changeset/fair-grapes-thank.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Allow visitors & integrations to access downloaded files after a room has closed. This was a known limitation in our codebase, where visitors were only able to access uploaded files in a livechat conversation while the conversation was open. diff --git a/.changeset/four-onions-camp.md b/.changeset/four-onions-camp.md new file mode 100644 index 000000000000..8068ac023638 --- /dev/null +++ b/.changeset/four-onions-camp.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +When using `DISABLE_DB_WATCHERS=true` this fixes message updates with URLs that were missing the link preview. diff --git a/.changeset/ninety-rivers-mix.md b/.changeset/ninety-rivers-mix.md new file mode 100644 index 000000000000..fbd10b2a04d1 --- /dev/null +++ b/.changeset/ninety-rivers-mix.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": minor +--- + +Fixed issue with "Export room as file" feature (`rooms.export` endpoint) generating an empty export when given an invalid date diff --git a/.changeset/real-bobcats-train.md b/.changeset/real-bobcats-train.md new file mode 100644 index 000000000000..6d51414c9fc4 --- /dev/null +++ b/.changeset/real-bobcats-train.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': patch +'@rocket.chat/meteor': patch +--- + +Don't show Join default channels option on edit user form. diff --git a/.changeset/shy-oranges-provide.md b/.changeset/shy-oranges-provide.md new file mode 100644 index 000000000000..7141a58da575 --- /dev/null +++ b/.changeset/shy-oranges-provide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes link image preview not opening in gallery mode diff --git a/.changeset/slow-cars-press.md b/.changeset/slow-cars-press.md new file mode 100644 index 000000000000..de4d08ff52ff --- /dev/null +++ b/.changeset/slow-cars-press.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduced a new setting which doesn't allow users to access encrypted rooms until E2EE is configured and also doesn't allow users to send un-encrypted messages in encrypted rooms. + +New room setup for E2EE feature which helps users to setup their E2EE keys and introduced states to E2EE feature. diff --git a/.changeset/smooth-knives-turn.md b/.changeset/smooth-knives-turn.md new file mode 100644 index 000000000000..3964ecc8481b --- /dev/null +++ b/.changeset/smooth-knives-turn.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +--- + +Executing a logout and login action in the same "tab/instance", some streams were not being recreated, causing countless types of bugs. + +PS: as a workaround reloading after logout or login in also solves the problem. diff --git a/.changeset/strong-humans-bow.md b/.changeset/strong-humans-bow.md new file mode 100644 index 000000000000..b718cbe7fedd --- /dev/null +++ b/.changeset/strong-humans-bow.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/fuselage-ui-kit": minor +"@rocket.chat/ui-kit": minor +--- + +Introduced new elements for apps to select channels diff --git a/.changeset/unlucky-berries-guess.md b/.changeset/unlucky-berries-guess.md new file mode 100644 index 000000000000..5a4cc9aba3e9 --- /dev/null +++ b/.changeset/unlucky-berries-guess.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Replace the read receipt receipt indicator in order to improve the accessibility complience diff --git a/.changeset/weak-starfishes-fail.md b/.changeset/weak-starfishes-fail.md new file mode 100644 index 000000000000..38e510229f6e --- /dev/null +++ b/.changeset/weak-starfishes-fail.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the missing spacing on don`t ask again checkbox inside modals diff --git a/.changeset/weak-turkeys-sit.md b/.changeset/weak-turkeys-sit.md new file mode 100644 index 000000000000..c4673b9d049d --- /dev/null +++ b/.changeset/weak-turkeys-sit.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed a bad behavior with the interaction between OTR system messages & trash collection. We use trash collection as a temporary storage that holds recently deleted items from some collections. Messages is one of those. This was causing "User joined OTR" messages to be viewable when querying the trash collection. +Since OTR messages are by definition private, code was updated to bypass trash collection when removing these special messages. + +Note: this only applies to these system messages. OTR user's messages are not stored on the database. diff --git a/.changeset/wicked-points-deliver.md b/.changeset/wicked-points-deliver.md new file mode 100644 index 000000000000..91985578917f --- /dev/null +++ b/.changeset/wicked-points-deliver.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/meteor': patch +--- + +This fuselage`s bump fixes: +- The message toolbar visibility on hover (Firefox ESR) +- `Bubble` missing font-family + +[more details](https://github.com/RocketChat/fuselage/releases/tag/%40rocket.chat%2Ffuselage%400.53.7) diff --git a/.changeset/wise-pianos-explode.md b/.changeset/wise-pianos-explode.md new file mode 100644 index 000000000000..3473275e20ab --- /dev/null +++ b/.changeset/wise-pianos-explode.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with external users being able to reset their passwords even when the "Allow Password Change for OAuth Users" setting is disabled diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 48b0af912d15..a834776aeff5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,6 +28,6 @@ apps/meteor/server/startup/migrations @RocketChat/Architecture /apps/meteor/ee/app/canned-responses @RocketChat/omnichannel /apps/meteor/ee/app/livechat @RocketChat/omnichannel /apps/meteor/ee/app/livechat-enterprise @RocketChat/omnichannel -/apps/meteor/ee/client/omnichannel @RocketChat/omnichannel +/apps/meteor/client/omnichannel @RocketChat/omnichannel /apps/meteor/client/components/omnichannel @RocketChat/omnichannel /apps/meteor/client/components/voip @RocketChat/omnichannel diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index 356ac10c9759..bc9d1f042d58 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -12,6 +12,6 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: thehanimo/pr-title-checker@v1.3.7 + - uses: thehanimo/pr-title-checker@v1.4.1 with: GITHUB_TOKEN: ${{ secrets.RC_TITLE_CHECKER }} diff --git a/apps/meteor/.storybook/main.js b/apps/meteor/.storybook/main.js index 0e0b6db7c0e9..d70d3c5d7cc3 100644 --- a/apps/meteor/.storybook/main.js +++ b/apps/meteor/.storybook/main.js @@ -7,7 +7,6 @@ module.exports = { '../client/**/*.stories.{js,tsx}', '../app/**/*.stories.{js,tsx}', '../ee/app/**/*.stories.{js,tsx}', - '../ee/client/**/*.stories.{js,tsx}', ], addons: [ '@storybook/addon-essentials', diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts index 4b860d6e1eac..3613cc171354 100644 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ b/apps/meteor/app/api/server/v1/permissions.ts @@ -4,6 +4,7 @@ import { isBodyParamsValidPermissionUpdate } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { API } from '../api'; API.v1.addRoute( @@ -70,6 +71,7 @@ API.v1.addRoute( for await (const permission of bodyParams.permissions) { await Permissions.setRoles(permission._id, permission.roles); + void notifyOnPermissionChangedById(permission._id); } const result = (await Meteor.callAsync('permissions/get')) as IPermission[]; diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index 6727f4f970cb..66c6677a9eed 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -9,7 +9,7 @@ import { getUsersInRolePaginated } from '../../../authorization/server/functions import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync, hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { notifyListenerOnRoleChanges } from '../../../lib/server/lib/notifyListenerOnRoleChanges'; +import { notifyOnRoleChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server/index'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -180,7 +180,7 @@ API.v1.addRoute( await Roles.removeById(role._id); - void notifyListenerOnRoleChanges(role._id, 'removed', role); + void notifyOnRoleChanged(role, 'removed'); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 9576a79f6678..40998201b03b 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -2,7 +2,7 @@ import { Media } from '@rocket.chat/core-services'; import type { IRoom, IUpload } from '@rocket.chat/core-typings'; import { Messages, Rooms, Users, Uploads } from '@rocket.chat/models'; import type { Notifications } from '@rocket.chat/rest-typings'; -import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps } from '@rocket.chat/rest-typings'; +import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps, isRoomsExportProps } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { isTruthy } from '../../../../lib/isTruthy'; @@ -599,15 +599,11 @@ API.v1.addRoute( API.v1.addRoute( 'rooms.export', - { authRequired: true }, + { authRequired: true, validateParams: isRoomsExportProps }, { async post() { const { rid, type } = this.bodyParams; - if (!rid || !type || !['email', 'file'].includes(type)) { - throw new Meteor.Error('error-invalid-params'); - } - if (!(await hasPermissionAsync(this.userId, 'mail-messages', rid))) { throw new Meteor.Error('error-action-not-allowed', 'Mailing is not allowed'); } @@ -627,12 +623,8 @@ API.v1.addRoute( const { dateFrom, dateTo } = this.bodyParams; const { format } = this.bodyParams; - if (!['html', 'json'].includes(format || '')) { - throw new Meteor.Error('error-invalid-format'); - } - - const convertedDateFrom = new Date(dateFrom || ''); - const convertedDateTo = new Date(dateTo || ''); + const convertedDateFrom = dateFrom ? new Date(dateFrom) : new Date(0); + const convertedDateTo = dateTo ? new Date(dateTo) : new Date(); convertedDateTo.setDate(convertedDateTo.getDate() + 1); if (convertedDateFrom > convertedDateTo) { @@ -658,10 +650,6 @@ API.v1.addRoute( throw new Meteor.Error('error-invalid-recipient'); } - if (messages?.length === 0) { - throw new Meteor.Error('error-invalid-messages'); - } - const result = await dataExport.sendViaEmail( { rid, diff --git a/apps/meteor/app/authorization/client/index.ts b/apps/meteor/app/authorization/client/index.ts index dd335c13030e..7dc1a3466f49 100644 --- a/apps/meteor/app/authorization/client/index.ts +++ b/apps/meteor/app/authorization/client/index.ts @@ -1,4 +1,5 @@ import { hasAllPermission, hasAtLeastOnePermission, hasPermission, userHasAllPermission } from './hasPermission'; import { hasRole, hasAnyRole } from './hasRole'; +import './restrictedRoles'; export { hasAllPermission, hasAtLeastOnePermission, hasRole, hasAnyRole, hasPermission, userHasAllPermission }; diff --git a/apps/meteor/app/authorization/client/restrictedRoles.ts b/apps/meteor/app/authorization/client/restrictedRoles.ts new file mode 100644 index 000000000000..5aa5e426c2bd --- /dev/null +++ b/apps/meteor/app/authorization/client/restrictedRoles.ts @@ -0,0 +1,12 @@ +import { Meteor } from 'meteor/meteor'; + +import { sdk } from '../../utils/client/lib/SDKClient'; +import { AuthorizationUtils } from '../lib'; + +Meteor.startup(async () => { + const result = await sdk.call('license:isEnterprise'); + if (result) { + // #ToDo: Load this from the server with an API call instead of having a duplicate list + AuthorizationUtils.addRolePermissionWhiteList('guest', ['view-d-room', 'view-joined-room', 'view-p-room', 'start-discussion']); + } +}); diff --git a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts index cb6422a03142..13a114732bd2 100644 --- a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts +++ b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts @@ -2,6 +2,7 @@ import { Permissions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { CONSTANTS, AuthorizationUtils } from '../../lib'; import { hasPermissionAsync } from '../functions/hasPermission'; @@ -41,11 +42,15 @@ Meteor.methods({ action: 'Adding_permission', }); } + // for setting-based-permissions, authorize the group access as well if (permission.groupPermissionId) { await Permissions.addRole(permission.groupPermissionId, role); + void notifyOnPermissionChangedById(permission.groupPermissionId); } await Permissions.addRole(permission._id, role); + + void notifyOnPermissionChangedById(permission._id); }, }); diff --git a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts index 30a1b2a759b6..91a4df1eddf7 100644 --- a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts +++ b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts @@ -2,6 +2,7 @@ import { Permissions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { CONSTANTS } from '../../lib'; import { hasPermissionAsync } from '../functions/hasPermission'; @@ -36,10 +37,12 @@ Meteor.methods({ // for setting based permissions, revoke the group permission once all setting permissions // related to this group have been removed - if (permission.groupPermissionId) { await Permissions.removeRole(permission.groupPermissionId, role); + void notifyOnPermissionChangedById(permission.groupPermissionId); } + await Permissions.removeRole(permission._id, role); + void notifyOnPermissionChangedById(permission._id); }, }); diff --git a/apps/meteor/ee/app/canned-responses/client/collections/CannedResponse.ts b/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts similarity index 100% rename from apps/meteor/ee/app/canned-responses/client/collections/CannedResponse.ts rename to apps/meteor/app/canned-responses/client/collections/CannedResponse.ts diff --git a/apps/meteor/ee/app/canned-responses/client/index.ts b/apps/meteor/app/canned-responses/client/index.ts similarity index 100% rename from apps/meteor/ee/app/canned-responses/client/index.ts rename to apps/meteor/app/canned-responses/client/index.ts diff --git a/apps/meteor/ee/app/canned-responses/client/startup/responses.js b/apps/meteor/app/canned-responses/client/startup/responses.js similarity index 84% rename from apps/meteor/ee/app/canned-responses/client/startup/responses.js rename to apps/meteor/app/canned-responses/client/startup/responses.js index 6d5834d91cc0..595945283261 100644 --- a/apps/meteor/ee/app/canned-responses/client/startup/responses.js +++ b/apps/meteor/app/canned-responses/client/startup/responses.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { hasPermission } from '../../../../../app/authorization/client'; -import { settings } from '../../../../../app/settings/client'; -import { sdk } from '../../../../../app/utils/client/lib/SDKClient'; +import { hasPermission } from '../../../authorization/client'; +import { settings } from '../../../settings/client'; +import { sdk } from '../../../utils/client/lib/SDKClient'; import { CannedResponse } from '../collections/CannedResponse'; const events = { diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts index 8aa8dc4578a5..0fc15f878bcf 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts @@ -8,6 +8,7 @@ import type { Document, UpdateResult } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; +import { notifyOnIntegrationChangedByChannels } from '../../../lib/server/lib/notifyListener'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; const updateFName = async (rid: string, displayName: string): Promise<(UpdateResult | Document)[]> => { @@ -73,10 +74,13 @@ export async function saveRoomName( if (room.name && !isDiscussion) { await Integrations.updateRoomName(room.name, slugifiedRoomName); + void notifyOnIntegrationChangedByChannels([slugifiedRoomName]); } + if (sendMessage) { await Message.saveSystemMessage('r', rid, displayName, user); } + await callbacks.run('afterRoomNameChange', { rid, name: displayName, oldName: room.name }); return displayName; } diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 4f6c06780942..e17faebea384 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -117,14 +117,10 @@ const validators: RoomSettingsValidators = { } }, async retentionEnabled({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.enabled) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.enabled) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -132,14 +128,10 @@ const validators: RoomSettingsValidators = { } }, async retentionMaxAge({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.maxAge) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.maxAge) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -147,14 +139,10 @@ const validators: RoomSettingsValidators = { } }, async retentionExcludePinned({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.excludePinned) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.excludePinned) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -162,14 +150,10 @@ const validators: RoomSettingsValidators = { } }, async retentionFilesOnly({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.filesOnly) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.filesOnly) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -177,14 +161,10 @@ const validators: RoomSettingsValidators = { } }, async retentionIgnoreThreads({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.ignoreThreads) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.ignoreThreads) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -469,7 +449,7 @@ export async function saveRoomSettings( rid, }); - if (setting === 'retentionOverrideGlobal') { + if (setting === 'retentionOverrideGlobal' && settings.retentionOverrideGlobal === false) { delete settings.retentionMaxAge; delete settings.retentionExcludePinned; delete settings.retentionFilesOnly; diff --git a/apps/meteor/app/e2e/client/E2EEState.ts b/apps/meteor/app/e2e/client/E2EEState.ts new file mode 100644 index 000000000000..0e505ec4a1bd --- /dev/null +++ b/apps/meteor/app/e2e/client/E2EEState.ts @@ -0,0 +1,9 @@ +export enum E2EEState { + NOT_STARTED = 'NOT_STARTED', + DISABLED = 'DISABLED', + LOADING_KEYS = 'LOADING_KEYS', + READY = 'READY', + SAVE_PASSWORD = 'SAVE_PASSWORD', + ENTER_PASSWORD = 'ENTER_PASSWORD', + ERROR = 'ERROR', +} diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index bd0863d691a9..554cd6a327a8 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -41,6 +41,7 @@ const permitedMutations = { E2ERoomState.ERROR, E2ERoomState.DISABLED, E2ERoomState.WAITING_KEYS, + E2ERoomState.CREATING_KEYS, ], }; @@ -92,6 +93,10 @@ export class E2ERoom extends Emitter { logError(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg); } + getState() { + return this.state; + } + setState(requestedState) { const currentState = this.state; const nextState = filterMutation(currentState, requestedState); @@ -208,6 +213,10 @@ export class E2ERoom extends Emitter { // Initiates E2E Encryption async handshake() { + if (!e2e.isReady()) { + return; + } + if (this.state !== E2ERoomState.KEYS_RECEIVED && this.state !== E2ERoomState.NOT_STARTED) { return; } @@ -459,5 +468,11 @@ export class E2ERoom extends Emitter { } this.encryptKeyForOtherParticipants(); + this.setState(E2ERoomState.READY); + } + + onStateChange(cb) { + this.on('STATE_CHANGED', cb); + return () => this.off('STATE_CHANGED', cb); } } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 1a98ce857f01..aeb51c292715 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -6,8 +6,7 @@ import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; import { Meteor } from 'meteor/meteor'; -import type { ReactiveVar as ReactiveVarType } from 'meteor/reactive-var'; -import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; import * as banners from '../../../client/lib/banners'; import type { LegacyBannerPayload } from '../../../client/lib/banners'; @@ -24,6 +23,7 @@ import { settings } from '../../settings/client'; import { getUserAvatarURL } from '../../utils/client'; import { sdk } from '../../utils/client/lib/SDKClient'; import { t } from '../../utils/lib/i18n'; +import { E2EEState } from './E2EEState'; import { toString, toArrayBuffer, @@ -49,36 +49,39 @@ type KeyPair = { private_key: string | null; }; +const E2EEStateDependency = new Tracker.Dependency(); + class E2E extends Emitter { private started: boolean; - public enabled: ReactiveVarType; - - private _ready: ReactiveVarType; - private instancesByRoomId: Record; - private db_public_key: string | null; + private db_public_key: string | null | undefined; - private db_private_key: string | null; + private db_private_key: string | null | undefined; public privateKey: CryptoKey | undefined; + private state: E2EEState; + constructor() { super(); this.started = false; - this.enabled = new ReactiveVar(false); - this._ready = new ReactiveVar(false); this.instancesByRoomId = {}; - this.on('ready', async () => { - this._ready.set(true); - this.log('startClient -> Done'); - this.log('decryptSubscriptions'); + this.on('E2E_STATE_CHANGED', ({ prevState, nextState }) => { + this.log(`${prevState} -> ${nextState}`); + }); + + this.on(E2EEState.READY, async () => { + await this.onE2EEReady(); + }); - await this.decryptSubscriptions(); - this.log('decryptSubscriptions -> Done'); + this.on(E2EEState.SAVE_PASSWORD, async () => { + await this.onE2EEReady(); }); + + this.setState(E2EEState.NOT_STARTED); } log(...msg: unknown[]) { @@ -89,12 +92,46 @@ class E2E extends Emitter { logError('E2E', ...msg); } + getState() { + return this.state; + } + isEnabled(): boolean { - return this.enabled.get(); + return this.state !== E2EEState.DISABLED; } isReady(): boolean { - return this.enabled.get() && this._ready.get(); + E2EEStateDependency.depend(); + + // Save_Password state is also a ready state for E2EE + return this.state === E2EEState.READY || this.state === E2EEState.SAVE_PASSWORD; + } + + async onE2EEReady() { + this.log('startClient -> Done'); + this.log('decryptSubscriptions'); + this.initiateHandshake(); + await this.decryptSubscriptions(); + this.log('decryptSubscriptions -> Done'); + await this.initiateDecryptingPendingMessages(); + this.log('DecryptingPendingMessages -> Done'); + } + + shouldAskForE2EEPassword() { + const { private_key } = this.getKeysFromLocalStorage(); + return this.db_private_key && !private_key; + } + + setState(nextState: E2EEState) { + const prevState = this.state; + + this.state = nextState; + + E2EEStateDependency.changed(); + + this.emit('E2E_STATE_CHANGED', { prevState, nextState }); + + this.emit(nextState); } async getInstanceByRoomId(rid: IRoom['_id']): Promise { @@ -155,6 +192,35 @@ class E2E extends Emitter { }; } + initiateHandshake() { + Object.keys(this.instancesByRoomId).map((key) => this.instancesByRoomId[key].handshake()); + } + + async initiateDecryptingPendingMessages() { + await Promise.all(Object.keys(this.instancesByRoomId).map((key) => this.instancesByRoomId[key].decryptPendingMessages())); + } + + openSaveE2EEPasswordModal(randomPassword: string) { + imperativeModal.open({ + component: SaveE2EPasswordModal, + props: { + randomPassword, + onClose: imperativeModal.close, + onCancel: () => { + this.closeAlert(); + imperativeModal.close(); + }, + onConfirm: () => { + Meteor._localStorage.removeItem('e2e.randomPassword'); + this.setState(E2EEState.READY); + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); + this.closeAlert(); + imperativeModal.close(); + }, + }, + }); + } + async startClient(): Promise { if (this.started) { return; @@ -172,9 +238,10 @@ class E2E extends Emitter { public_key = this.db_public_key; } - if (!private_key && this.db_private_key) { + if (this.shouldAskForE2EEPassword()) { try { - private_key = await this.decodePrivateKey(this.db_private_key); + this.setState(E2EEState.ENTER_PASSWORD); + private_key = await this.decodePrivateKey(this.db_private_key as string); } catch (error) { this.started = false; failedToDecodeKey = true; @@ -195,44 +262,29 @@ class E2E extends Emitter { if (public_key && private_key) { await this.loadKeys({ public_key, private_key }); + this.setState(E2EEState.READY); } else { await this.createAndLoadKeys(); + this.setState(E2EEState.READY); } if (!this.db_public_key || !this.db_private_key) { + this.setState(E2EEState.LOADING_KEYS); await this.persistKeys(this.getKeysFromLocalStorage(), await this.createRandomPassword()); } const randomPassword = Meteor._localStorage.getItem('e2e.randomPassword'); if (randomPassword) { + this.setState(E2EEState.SAVE_PASSWORD); this.openAlert({ title: () => t('Save_your_encryption_password'), html: () => t('Click_here_to_view_and_copy_your_password'), modifiers: ['large'], closable: false, icon: 'key', - action: () => { - imperativeModal.open({ - component: SaveE2EPasswordModal, - props: { - randomPassword, - onClose: imperativeModal.close, - onCancel: () => { - this.closeAlert(); - imperativeModal.close(); - }, - onConfirm: () => { - Meteor._localStorage.removeItem('e2e.randomPassword'); - this.closeAlert(); - dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Set') }); - imperativeModal.close(); - }, - }, - }); - }, + action: () => this.openSaveE2EEPasswordModal(randomPassword), }); } - this.emit('ready'); } async stopClient(): Promise { @@ -243,9 +295,8 @@ class E2E extends Emitter { Meteor._localStorage.removeItem('private_key'); this.instancesByRoomId = {}; this.privateKey = undefined; - this.enabled.set(false); - this._ready.set(false); this.started = false; + this.setState(E2EEState.DISABLED); } async changePassword(newPassword: string): Promise { @@ -258,11 +309,13 @@ class E2E extends Emitter { async loadKeysFromDB(): Promise { try { + this.setState(E2EEState.LOADING_KEYS); const { public_key, private_key } = await sdk.rest.get('/v1/e2e.fetchMyKeys'); this.db_public_key = public_key; this.db_private_key = private_key; } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error fetching RSA keys: ', error); } } @@ -275,17 +328,20 @@ class E2E extends Emitter { Meteor._localStorage.setItem('private_key', private_key); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error importing private key: ', error); } } async createAndLoadKeys(): Promise { // Could not obtain public-private keypair from server. + this.setState(E2EEState.LOADING_KEYS); let key; try { key = await generateRSAKey(); this.privateKey = key.privateKey; } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error generating key: ', error); } @@ -294,6 +350,7 @@ class E2E extends Emitter { Meteor._localStorage.setItem('public_key', JSON.stringify(publicKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error exporting public key: ', error); } @@ -302,6 +359,7 @@ class E2E extends Emitter { Meteor._localStorage.setItem('private_key', JSON.stringify(privateKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error exporting private key: ', error); } @@ -327,6 +385,7 @@ class E2E extends Emitter { return EJSON.stringify(joinVectorAndEcryptedData(vector, encodedPrivateKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error encrypting encodedPrivateKey: ', error); } } @@ -341,6 +400,7 @@ class E2E extends Emitter { try { baseKey = await importRawKey(toArrayBuffer(password)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error creating a key based on user password: ', error); } @@ -348,30 +408,34 @@ class E2E extends Emitter { try { return await deriveKey(toArrayBuffer(Meteor.userId()), baseKey); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error deriving baseKey: ', error); } } - async requestPassword(): Promise { + openEnterE2EEPasswordModal(onEnterE2EEPassword?: (password: string) => void) { + imperativeModal.open({ + component: EnterE2EPasswordModal, + props: { + onClose: imperativeModal.close, + onCancel: () => { + failedToDecodeKey = false; + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); + this.closeAlert(); + imperativeModal.close(); + }, + onConfirm: (password) => { + onEnterE2EEPassword?.(password); + this.closeAlert(); + imperativeModal.close(); + }, + }, + }); + } + + async requestPasswordAlert(): Promise { return new Promise((resolve) => { - const showModal = () => { - imperativeModal.open({ - component: EnterE2EPasswordModal, - props: { - onClose: imperativeModal.close, - onCancel: () => { - failedToDecodeKey = false; - this.closeAlert(); - imperativeModal.close(); - }, - onConfirm: (password) => { - resolve(password); - this.closeAlert(); - imperativeModal.close(); - }, - }, - }); - }; + const showModal = () => this.openEnterE2EEPasswordModal((password) => resolve(password)); const showAlert = () => { this.openAlert({ @@ -394,8 +458,42 @@ class E2E extends Emitter { }); } + async requestPasswordModal(): Promise { + return new Promise((resolve) => this.openEnterE2EEPasswordModal((password) => resolve(password))); + } + + async decodePrivateKeyFlow() { + const password = await this.requestPasswordModal(); + const masterKey = await this.getMasterKey(password); + + if (!this.db_private_key) { + return; + } + + const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(this.db_private_key)); + + try { + const privKey = await decryptAES(vector, masterKey, cipherText); + const privateKey = toString(privKey) as string; + + if (this.db_public_key && privateKey) { + await this.loadKeys({ public_key: this.db_public_key, private_key: privateKey }); + this.setState(E2EEState.READY); + } else { + await this.createAndLoadKeys(); + this.setState(E2EEState.READY); + } + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); + } catch (error) { + this.setState(E2EEState.ENTER_PASSWORD); + dispatchToastMessage({ type: 'error', message: t('Your_E2EE_password_is_incorrect') }); + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); + throw new Error('E2E -> Error decrypting private key'); + } + } + async decodePrivateKey(privateKey: string): Promise { - const password = await this.requestPassword(); + const password = await this.requestPasswordAlert(); const masterKey = await this.getMasterKey(password); @@ -405,6 +503,9 @@ class E2E extends Emitter { const privKey = await decryptAES(vector, masterKey, cipherText); return toString(privKey); } catch (error) { + this.setState(E2EEState.ENTER_PASSWORD); + dispatchToastMessage({ type: 'error', message: t('Your_E2EE_password_is_incorrect') }); + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); throw new Error('E2E -> Error decrypting private key'); } } diff --git a/apps/meteor/ee/app/ecdh/Session.ts b/apps/meteor/app/ecdh/Session.ts similarity index 100% rename from apps/meteor/ee/app/ecdh/Session.ts rename to apps/meteor/app/ecdh/Session.ts diff --git a/apps/meteor/ee/app/ecdh/client/ClientSession.ts b/apps/meteor/app/ecdh/client/ClientSession.ts similarity index 100% rename from apps/meteor/ee/app/ecdh/client/ClientSession.ts rename to apps/meteor/app/ecdh/client/ClientSession.ts diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index 07f7a3d903a2..7a2992f91510 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -6,6 +6,7 @@ import _ from 'underscore'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; +import { notifyOnIntegrationChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { outgoingEvents } from '../../lib/outgoingEvents'; import { outgoingLogger } from '../logger'; @@ -579,6 +580,7 @@ class RocketChatIntegrationHandler { await updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); outgoingLogger.error(`Disabling the Integration "${trigger.name}" because the status code was 401 (Gone).`); await Integrations.updateOne({ _id: trigger._id }, { $set: { enabled: false } }); + void notifyOnIntegrationChangedById(trigger._id); return; } diff --git a/apps/meteor/app/integrations/server/lib/updateHistory.ts b/apps/meteor/app/integrations/server/lib/updateHistory.ts index ed304403e8c7..e8068ad82ac1 100644 --- a/apps/meteor/app/integrations/server/lib/updateHistory.ts +++ b/apps/meteor/app/integrations/server/lib/updateHistory.ts @@ -1,8 +1,8 @@ import type { IIntegrationHistory, OutgoingIntegrationEvent, IIntegration, IMessage, AtLeast } from '@rocket.chat/core-typings'; import { IntegrationHistory } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; import { omit } from '../../../../lib/utils/omit'; +import { notifyOnIntegrationHistoryChangedById, notifyOnIntegrationHistoryChanged } from '../../../lib/server/lib/notifyListener'; export const updateHistory = async ({ historyId, @@ -77,7 +77,12 @@ export const updateHistory = async ({ }; if (historyId) { - await IntegrationHistory.updateOne({ _id: historyId }, { $set: history }); + // Projecting just integration field to comply with existing listener behaviour + const integrationHistory = await IntegrationHistory.updateById(historyId, history, { projection: { 'integration._id': 1 } }); + if (!integrationHistory) { + throw new Error('error-updating-integration-history'); + } + void notifyOnIntegrationHistoryChanged(integrationHistory, 'updated', history); return historyId; } @@ -86,11 +91,15 @@ export const updateHistory = async ({ throw new Error('error-invalid-integration'); } - history._createdAt = new Date(); + // TODO: Had to force type cast here because of function's signature + // It would be easier if we separate into create and update functions + const { insertedId } = await IntegrationHistory.create(history as IIntegrationHistory); - const _id = Random.id(); + if (!insertedId) { + throw new Error('error-creating-integration-history'); + } - await IntegrationHistory.insertOne({ _id, ...history } as IIntegrationHistory); + void notifyOnIntegrationHistoryChangedById(insertedId, 'inserted'); - return _id; + return insertedId; }; diff --git a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts index 2447683bd291..5b8f13ef1a3a 100644 --- a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts +++ b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts @@ -41,6 +41,7 @@ Meteor.methods({ }); } + // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. await IntegrationHistory.removeByIntegrationId(integrationId); notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed', id: integrationId }); diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 45548a17a565..db058bec960b 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; @@ -155,9 +156,13 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn await Roles.addUserRoles(user._id, ['bot']); - const result = await Integrations.insertOne(integrationData); + const { insertedId } = await Integrations.insertOne(integrationData); - integrationData._id = result.insertedId; + if (insertedId) { + void notifyOnIntegrationChanged({ ...integrationData, _id: insertedId }, 'inserted'); + } + + integrationData._id = insertedId; return integrationData; }; diff --git a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts index 06fb3e3485e3..e73a46bb27db 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -34,6 +35,7 @@ export const deleteIncomingIntegration = async (integrationId: string, userId: s } await Integrations.removeById(integrationId); + void notifyOnIntegrationChangedById(integrationId, 'removed'); }; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 5358e3233ce7..0ea5028130da 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; @@ -164,7 +165,7 @@ Meteor.methods({ await Roles.addUserRoles(user._id, ['bot']); - await Integrations.updateOne( + const updatedIntegration = await Integrations.findOneAndUpdate( { _id: integrationId }, { $set: { @@ -190,6 +191,10 @@ Meteor.methods({ }, ); - return Integrations.findOneById(integrationId); + if (updatedIntegration.value) { + void notifyOnIntegrationChanged(updatedIntegration.value); + } + + return updatedIntegration.value; }, }); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts index 59879f99d475..c8dc31e08446 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts @@ -5,6 +5,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; import { validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -58,8 +59,13 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu const integrationData = await validateOutgoingIntegration(integration, userId); - const result = await Integrations.insertOne(integrationData); - integrationData._id = result.insertedId; + const { insertedId } = await Integrations.insertOne(integrationData); + + if (insertedId) { + void notifyOnIntegrationChanged({ ...integrationData, _id: insertedId }, 'inserted'); + } + + integrationData._id = insertedId; return integrationData; }; diff --git a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index 27750bca50f2..c9f2211d835b 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -40,7 +41,9 @@ export const deleteOutgoingIntegration = async (integrationId: string, userId: s } await Integrations.removeById(integrationId); + // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. await IntegrationHistory.removeByIntegrationId(integrationId); + void notifyOnIntegrationChangedById(integrationId, 'removed'); }; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 9e62561ebf9a..116dbd043039 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -5,6 +5,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -66,7 +67,7 @@ Meteor.methods({ const isFrozen = isScriptEngineFrozen(scriptEngine); - await Integrations.updateOne( + const updatedIntegration = await Integrations.findOneAndUpdate( { _id: integrationId }, { $set: { @@ -110,6 +111,10 @@ Meteor.methods({ }, ); - return Integrations.findOneById(integrationId); + if (updatedIntegration.value) { + await notifyOnIntegrationChanged(updatedIntegration.value); + } + + return updatedIntegration.value; }, }); diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 66112bdb695b..3af123a72bf7 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -19,7 +19,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnIntegrationChangedByUserId } from '../lib/notifyListener'; import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; @@ -114,7 +114,9 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele await FileUpload.getStore('Avatars').deleteByName(user.username); } - await Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. + // Disables all the integrations which rely on the user being deleted. + await Integrations.disableByUserId(userId); + void notifyOnIntegrationChangedByUserId(userId); // Don't broadcast user.deleted for Erasure Type of 'Keep' so that messages don't disappear from logged in sessions if (messageErasureType === 'Delete') { diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index 8327b0892b26..2954517fb018 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -94,17 +94,21 @@ export const updateMessage = async function ( setImmediate(async () => { const msg = await Messages.findOneById(_id); - if (msg) { - await callbacks.run('afterSaveMessage', msg, room, user._id); + if (!msg) { + return; + } + + // although this is an "afterSave" kind callback, we know they can extend message's properties + // so we wait for it to run before broadcasting + const data = await callbacks.run('afterSaveMessage', msg, room, user._id); - void broadcastMessageFromData({ - id: msg._id, - data: msg, - }); + void broadcastMessageFromData({ + id: msg._id, + data: data as any, // TODO move "afterSaveMessage" type definition to specify a return value + }); - if (room?.lastMessage?._id === msg._id) { - void notifyOnRoomChangedById(message.rid); - } + if (room?.lastMessage?._id === msg._id) { + void notifyOnRoomChangedById(message.rid); } }); }; diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 821751f13540..df0073423642 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -1,9 +1,44 @@ import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; -import type { IRocketChatRecord, IRoom } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import type { + IRocketChatRecord, + IRoom, + ILoginServiceConfiguration, + ISetting, + IRole, + IPermission, + IIntegration, + IPbxEvent, + LoginServiceConfiguration as LoginServiceConfigurationData, + ILivechatPriority, + IIntegrationHistory, + AtLeast, +} from '@rocket.chat/core-typings'; +import { + Rooms, + Permissions, + Settings, + PbxEvents, + Roles, + Integrations, + LoginServiceConfiguration, + IntegrationHistory, +} from '@rocket.chat/models'; type ClientAction = 'inserted' | 'updated' | 'removed'; +export async function notifyOnLivechatPriorityChanged( + data: Pick, + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const { _id, ...rest } = data; + + void api.broadcast('watch.priorities', { clientAction, id: _id, diff: { ...rest } }); +} + export async function notifyOnRoomChanged( data: T | T[], clientAction: ClientAction = 'updated', @@ -28,6 +63,7 @@ export async function notifyOnRoomChangedById( } const eligibleIds = Array.isArray(ids) ? ids : [ids]; + const items = Rooms.findByIds(eligibleIds); for await (const item of items) { @@ -65,3 +101,197 @@ export async function notifyOnRoomChangedByUserDM( void api.broadcast('watch.rooms', { clientAction, room: item }); } } + +export async function notifyOnSettingChanged(setting: ISetting, clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.settings', { clientAction, setting }); +} + +export async function notifyOnPermissionChanged(permission: IPermission, clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('permission.changed', { clientAction, data: permission }); + + if (permission.level === 'settings' && permission.settingId) { + const setting = await Settings.findOneNotHiddenById(permission.settingId); + if (!setting) { + return; + } + void notifyOnSettingChanged(setting, 'updated'); + } +} + +export async function notifyOnPermissionChangedById(pid: IPermission['_id'], clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + const permission = await Permissions.findOneById(pid); + if (!permission) { + return; + } + + return notifyOnPermissionChanged(permission, clientAction); +} + +export async function notifyOnPbxEventChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await PbxEvents.findOneById(id); + if (!item) { + return; + } + + void api.broadcast('watch.pbxevents', { clientAction, id, data: item }); +} + +export async function notifyOnRoleChanged(role: T, clientAction: 'removed' | 'changed' = 'changed'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.roles', { clientAction, role }); +} + +export async function notifyOnRoleChangedById( + id: T['_id'], + clientAction: 'removed' | 'changed' = 'changed', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const role = await Roles.findOneById(id); + if (!role) { + return; + } + + void notifyOnRoleChanged(role, clientAction); +} + +export async function notifyOnLoginServiceConfigurationChanged( + service: Partial & Pick, + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.loginServiceConfiguration', { + clientAction, + id: service._id, + data: service, + }); +} + +export async function notifyOnLoginServiceConfigurationChangedByService( + service: T['service'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await LoginServiceConfiguration.findOneByService>(service, { + projection: { secret: 0 }, + }); + if (!item) { + return; + } + + void notifyOnLoginServiceConfigurationChanged(item, clientAction); +} + +export async function notifyOnIntegrationChanged(data: T, clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.integrations', { clientAction, id: data._id, data }); +} + +export async function notifyOnIntegrationChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await Integrations.findOneById(id); + if (!item) { + return; + } + + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); +} + +export async function notifyOnIntegrationChangedByUserId( + id: T['userId'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Integrations.findByUserId(id); + + for await (const item of items) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnIntegrationChangedByChannels( + channels: T['channel'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Integrations.findByChannels(channels); + + for await (const item of items) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnIntegrationHistoryChanged( + data: AtLeast, + clientAction: ClientAction = 'updated', + diff: Partial = {}, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.integrationHistory', { clientAction, id: data._id, data, diff }); +} + +export async function notifyOnIntegrationHistoryChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', + diff: Partial = {}, +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await IntegrationHistory.findOneById(id); + + if (!item) { + return; + } + + void api.broadcast('watch.integrationHistory', { clientAction, id: item._id, data: item, diff }); +} diff --git a/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts b/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts deleted file mode 100644 index b0a2bfb459f4..000000000000 --- a/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; -import type { IRole } from '@rocket.chat/core-typings'; -import { Roles } from '@rocket.chat/models'; - -type ClientAction = 'inserted' | 'updated' | 'removed'; - -export async function notifyListenerOnRoleChanges( - rid: IRole['_id'], - clientAction: ClientAction = 'updated', - existingRoleData?: IRole, -): Promise { - if (!dbWatchersDisabled) { - return; - } - - const role = existingRoleData || (await Roles.findOneById(rid)); - if (!role) { - return; - } - - void api.broadcast('watch.roles', { - clientAction, - role, - }); -} diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 5749daa980f3..c78407782059 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -81,6 +81,14 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast('E2E_Enable') && !settings.get('E2E_Allow_Unencrypted_Messages')) { + if (message.t !== 'e2e' || message.e2e !== 'pending') { + throw new Meteor.Error('error-not-allowed', 'Not allowed to send un-encrypted messages in an encrypted room', { + method: 'sendMessage', + }); + } + } + metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return await sendMessage(user, message, room, false, previewUrls); } catch (err: any) { diff --git a/apps/meteor/ee/app/license/client/index.ts b/apps/meteor/app/license/client/index.ts similarity index 85% rename from apps/meteor/ee/app/license/client/index.ts rename to apps/meteor/app/license/client/index.ts index f0340c8d0ae5..efe1f68bc5e8 100644 --- a/apps/meteor/ee/app/license/client/index.ts +++ b/apps/meteor/app/license/client/index.ts @@ -1,5 +1,5 @@ -import { queryClient } from '../../../../client/lib/queryClient'; import { fetchFeatures } from '../../../client/lib/fetchFeatures'; +import { queryClient } from '../../../client/lib/queryClient'; export async function hasLicense(feature: string): Promise { try { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx b/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx similarity index 100% rename from apps/meteor/ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx rename to apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx diff --git a/apps/meteor/ee/app/livechat-enterprise/client/index.ts b/apps/meteor/app/livechat-enterprise/client/index.ts similarity index 90% rename from apps/meteor/ee/app/livechat-enterprise/client/index.ts rename to apps/meteor/app/livechat-enterprise/client/index.ts index 1fc3ef704139..7be870039276 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/index.ts +++ b/apps/meteor/app/livechat-enterprise/client/index.ts @@ -1,5 +1,4 @@ import { hasLicense } from '../../license/client'; -import '../lib/messageTypes'; import './startup'; void hasLicense('livechat-enterprise').then((enabled) => { diff --git a/apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts b/apps/meteor/app/livechat-enterprise/client/messageTypes.ts similarity index 67% rename from apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts rename to apps/meteor/app/livechat-enterprise/client/messageTypes.ts index 9c15c277a1b2..90d390fe6be7 100644 --- a/apps/meteor/ee/app/livechat-enterprise/lib/messageTypes.ts +++ b/apps/meteor/app/livechat-enterprise/client/messageTypes.ts @@ -1,7 +1,26 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { MessageTypes } from '../../../../app/ui-utils/client'; -import { t } from '../../../../app/utils/lib/i18n'; +import { MessageTypes } from '../../ui-utils/client'; +import { t } from '../../utils/lib/i18n'; + +MessageTypes.registerType({ + id: 'livechat_transfer_history_fallback', + system: true, + message: 'New_chat_transfer_fallback', + data(message: any) { + if (!message.transferData) { + return { + fallback: 'SHOULD_NEVER_HAPPEN', + }; + } + const from = message.transferData.prevDepartment; + const to = message.transferData.department.name; + + return { + fallback: t('Livechat_transfer_failed_fallback', { from, to }), + }; + }, +}); MessageTypes.registerType({ id: 'omnichannel_priority_change_history', diff --git a/apps/meteor/ee/app/livechat-enterprise/client/startup.ts b/apps/meteor/app/livechat-enterprise/client/startup.ts similarity index 60% rename from apps/meteor/ee/app/livechat-enterprise/client/startup.ts rename to apps/meteor/app/livechat-enterprise/client/startup.ts index 3c3ec1c02139..0535f8926a7d 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/startup.ts +++ b/apps/meteor/app/livechat-enterprise/client/startup.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import { businessHourManager } from '../../../../app/livechat/client/views/app/business-hours/BusinessHours'; -import type { IBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior'; -import { SingleBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/Single'; -import { settings } from '../../../../app/settings/client'; import { hasLicense } from '../../license/client'; +import { businessHourManager } from '../../livechat/client/views/app/business-hours/BusinessHours'; +import type { IBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/IBusinessHourBehavior'; +import { SingleBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/Single'; +import { settings } from '../../settings/client'; import { MultipleBusinessHoursBehavior } from './views/business-hours/Multiple'; const businessHours: Record = { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts b/apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts similarity index 79% rename from apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts rename to apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts index a57344da73dc..698462dcaed2 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/views/business-hours/Multiple.ts +++ b/apps/meteor/app/livechat-enterprise/client/views/business-hours/Multiple.ts @@ -1,7 +1,7 @@ import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import type { IBusinessHourBehavior } from '../../../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior'; +import type { IBusinessHourBehavior } from '../../../../livechat/client/views/app/business-hours/IBusinessHourBehavior'; export class MultipleBusinessHoursBehavior implements IBusinessHourBehavior { getView(): string { diff --git a/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts b/apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts similarity index 91% rename from apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts rename to apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts index c89931208451..6d9d9f31e24c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/client/views/livechatSideNavItems.ts +++ b/apps/meteor/app/livechat-enterprise/client/views/livechatSideNavItems.ts @@ -1,5 +1,5 @@ -import { hasPermission, hasAtLeastOnePermission } from '../../../../../app/authorization/client'; -import { registerOmnichannelSidebarItem } from '../../../../../client/views/omnichannel/sidebarItems'; +import { registerOmnichannelSidebarItem } from '../../../../client/views/omnichannel/sidebarItems'; +import { hasPermission, hasAtLeastOnePermission } from '../../../authorization/client'; registerOmnichannelSidebarItem({ href: '/omnichannel/reports', diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts index 8f6791c36302..bb9567260337 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts @@ -3,6 +3,10 @@ import { LoginServiceConfiguration } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { + notifyOnLoginServiceConfigurationChanged, + notifyOnLoginServiceConfigurationChangedByService, +} from '../../../lib/server/lib/notifyListener'; import { settings, settingsRegistry } from '../../../settings/server'; import type { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; import { SAMLUtils } from './Utils'; @@ -117,9 +121,22 @@ export const loadSamlServiceProviders = async function (): Promise { const samlConfigs = getSamlConfigs(key); SAMLUtils.log(key); await LoginServiceConfiguration.createOrUpdateService(serviceName, samlConfigs); + void notifyOnLoginServiceConfigurationChangedByService(serviceName); return configureSamlService(samlConfigs); } - await LoginServiceConfiguration.removeService(serviceName); + + const service = await LoginServiceConfiguration.findOneByService(serviceName, { projection: { _id: 1 } }); + if (!service?._id) { + return false; + } + + const { deletedCount } = await LoginServiceConfiguration.removeService(service._id); + if (!deletedCount) { + return false; + } + + void notifyOnLoginServiceConfigurationChanged({ _id: service._id }, 'removed'); + return false; }), ) diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts index 77190992612a..545e1e73342d 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts @@ -356,12 +356,6 @@ export class CachedCollection extends Emitter< this.trySync(); }); - if (!this.userRelated) { - return this.setupListener(); - } - - CachedCollectionManager.onLogin(async () => { - await this.setupListener(); - }); + return this.setupListener(); } } diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index 18ff309970df..c174f9125f49 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -51,6 +51,7 @@ type EventMap = Str type StreamMapValue = { stop: () => void; + error: (cb: (...args: any[]) => void) => void; onChange: ReturnType['onChange']; ready: () => Promise; isReady: boolean; @@ -62,6 +63,7 @@ const createNewMeteorStream = (streamName: StreamNames, key: StreamKeys { - console.error(err); ee.emit('ready', [err]); + ee.emit('error', err); }, }, ); @@ -115,6 +117,11 @@ const createNewMeteorStream = (streamName: StreamNames, key: StreamKeys void) => + ee.once('error', (error) => { + cb(error); + }), + get isReady() { return meta.ready; }, @@ -179,6 +186,7 @@ const createStreamManager = () => { if (!streams.has(eventLiteral)) { streams.set(eventLiteral, stream); } + stream.error(() => stop()); return { id: '', diff --git a/apps/meteor/ee/client/apps/@types/IOrchestrator.ts b/apps/meteor/client/apps/@types/IOrchestrator.ts similarity index 100% rename from apps/meteor/ee/client/apps/@types/IOrchestrator.ts rename to apps/meteor/client/apps/@types/IOrchestrator.ts diff --git a/apps/meteor/ee/client/apps/RealAppsEngineUIHost.js b/apps/meteor/client/apps/RealAppsEngineUIHost.js similarity index 78% rename from apps/meteor/ee/client/apps/RealAppsEngineUIHost.js rename to apps/meteor/client/apps/RealAppsEngineUIHost.js index bcd1a254a4bd..4377f7c66aba 100644 --- a/apps/meteor/ee/client/apps/RealAppsEngineUIHost.js +++ b/apps/meteor/client/apps/RealAppsEngineUIHost.js @@ -1,11 +1,11 @@ import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; import { Meteor } from 'meteor/meteor'; -import { ChatRoom } from '../../../app/models/client'; -import { getUserAvatarURL } from '../../../app/utils/client/getUserAvatarURL'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { RoomManager } from '../../../client/lib/RoomManager'; -import { baseURI } from '../../../client/lib/baseURI'; +import { ChatRoom } from '../../app/models/client'; +import { getUserAvatarURL } from '../../app/utils/client/getUserAvatarURL'; +import { sdk } from '../../app/utils/client/lib/SDKClient'; +import { RoomManager } from '../lib/RoomManager'; +import { baseURI } from '../lib/baseURI'; export class RealAppsEngineUIHost extends AppsEngineUIHost { constructor() { diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx b/apps/meteor/client/apps/gameCenter/GameCenter.tsx similarity index 87% rename from apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx rename to apps/meteor/client/apps/gameCenter/GameCenter.tsx index 75f4882ce747..3261d1e1c51e 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenter.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenter.tsx @@ -3,8 +3,8 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { useState } from 'react'; import type { ReactElement } from 'react'; -import { preventSyntheticEvent } from '../../../../client/lib/utils/preventSyntheticEvent'; -import { useRoomToolbox } from '../../../../client/views/room/contexts/RoomToolboxContext'; +import { preventSyntheticEvent } from '../../lib/utils/preventSyntheticEvent'; +import { useRoomToolbox } from '../../views/room/contexts/RoomToolboxContext'; import GameCenterContainer from './GameCenterContainer'; import GameCenterList from './GameCenterList'; import { useExternalComponentsQuery } from './hooks/useExternalComponentsQuery'; diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx similarity index 95% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx index 1f37e5d6358a..f589dd21ed50 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx @@ -9,7 +9,7 @@ import { ContextualbarBack, ContextualbarContent, ContextualbarClose, -} from '../../../../client/components/Contextualbar'; +} from '../../components/Contextualbar'; import type { IGame } from './GameCenter'; interface IGameCenterContainerProps { diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx similarity index 79% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx index e7afa0d9e689..d0dcc6fad4fe 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx @@ -4,11 +4,11 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; -import GenericModal from '../../../../client/components/GenericModal'; -import UserAutoCompleteMultipleFederated from '../../../../client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { useOpenedRoom } from '../../../../client/lib/RoomManager'; -import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; +import GenericModal from '../../components/GenericModal'; +import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { useOpenedRoom } from '../../lib/RoomManager'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { callWithErrorHandling } from '../../lib/utils/callWithErrorHandling'; import type { IGame } from './GameCenter'; type Username = Exclude; diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx similarity index 91% rename from apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx rename to apps/meteor/client/apps/gameCenter/GameCenterList.tsx index f45ba934ba3b..58a4f05f5362 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterList.tsx @@ -3,13 +3,8 @@ import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; -import { - ContextualbarHeader, - ContextualbarTitle, - ContextualbarClose, - ContextualbarContent, -} from '../../../../client/components/Contextualbar'; -import { FormSkeleton } from '../../../../client/components/Skeleton'; +import { ContextualbarHeader, ContextualbarTitle, ContextualbarClose, ContextualbarContent } from '../../components/Contextualbar'; +import { FormSkeleton } from '../../components/Skeleton'; import type { IGame } from './GameCenter'; import GameCenterInvitePlayersModal from './GameCenterInvitePlayersModal'; diff --git a/apps/meteor/ee/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts b/apps/meteor/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts similarity index 100% rename from apps/meteor/ee/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts rename to apps/meteor/client/apps/gameCenter/hooks/useExternalComponentsQuery.ts diff --git a/apps/meteor/ee/client/apps/orchestrator.ts b/apps/meteor/client/apps/orchestrator.ts similarity index 94% rename from apps/meteor/ee/client/apps/orchestrator.ts rename to apps/meteor/client/apps/orchestrator.ts index d16be3d0c8c7..f33807d25be4 100644 --- a/apps/meteor/ee/client/apps/orchestrator.ts +++ b/apps/meteor/client/apps/orchestrator.ts @@ -4,10 +4,10 @@ import type { IPermission } from '@rocket.chat/apps-engine/definition/permission import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { Serialized } from '@rocket.chat/core-typings'; -import { hasAtLeastOnePermission } from '../../../app/authorization/client'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import type { App } from '../../../client/views/marketplace/types'; +import { hasAtLeastOnePermission } from '../../app/authorization/client'; +import { sdk } from '../../app/utils/client/lib/SDKClient'; +import { dispatchToastMessage } from '../lib/toast'; +import type { App } from '../views/marketplace/types'; import type { IAppExternalURL, ICategory } from './@types/IOrchestrator'; import { RealAppsEngineUIHost } from './RealAppsEngineUIHost'; diff --git a/apps/meteor/client/components/GenericModal/withDoNotAskAgain.tsx b/apps/meteor/client/components/GenericModal/withDoNotAskAgain.tsx index 9d3d754d17d4..8d3644e0dc93 100644 --- a/apps/meteor/client/components/GenericModal/withDoNotAskAgain.tsx +++ b/apps/meteor/client/components/GenericModal/withDoNotAskAgain.tsx @@ -1,4 +1,5 @@ -import { Box, CheckBox } from '@rocket.chat/fuselage'; +import { Box, Label, CheckBox } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useUserPreference, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { FC, ReactElement, ComponentType } from 'react'; import React, { useState } from 'react'; @@ -23,6 +24,7 @@ function withDoNotAskAgain( ): FC> { const WrappedComponent: FC> = function ({ onConfirm, dontAskAgain, ...props }) { const t = useTranslation(); + const dontAskAgainId = useUniqueId(); const dontAskAgainList = useUserPreference('dontAskAgainList'); const { action, label } = dontAskAgain; @@ -49,8 +51,10 @@ function withDoNotAskAgain( {...props} dontAskAgain={ - - + + } onConfirm={handleConfirm} diff --git a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx index 4e8e44b1f932..39242161ed46 100644 --- a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx +++ b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx @@ -52,7 +52,7 @@ export const Default: ComponentStory = () => ( - + ); diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx index 27202afa496c..be513e477cd9 100644 --- a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx @@ -6,21 +6,23 @@ import React from 'react'; import { useFormattedRelativeTime } from '../../hooks/useFormattedRelativeTime'; type RetentionPolicyCalloutProps = { - filesOnlyDefault: boolean; - excludePinnedDefault: boolean; - maxAgeDefault: number; + filesOnly: boolean; + excludePinned: boolean; + maxAge: number; }; -const RetentionPolicyCallout: FC = ({ filesOnlyDefault, excludePinnedDefault, maxAgeDefault }) => { +const RetentionPolicyCallout: FC = ({ filesOnly, excludePinned, maxAge }) => { const t = useTranslation(); - const time = useFormattedRelativeTime(maxAgeDefault); + const time = useFormattedRelativeTime(maxAge); return ( - - {filesOnlyDefault && excludePinnedDefault &&

{t('RetentionPolicy_RoomWarning_FilesOnly', { time })}

} - {filesOnlyDefault && !excludePinnedDefault &&

{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time })}

} - {!filesOnlyDefault && excludePinnedDefault &&

{t('RetentionPolicy_RoomWarning', { time })}

} - {!filesOnlyDefault && !excludePinnedDefault &&

{t('RetentionPolicy_RoomWarning_Unpinned', { time })}

} + +
+ {filesOnly && excludePinned &&

{t('RetentionPolicy_RoomWarning_FilesOnly', { time })}

} + {filesOnly && !excludePinned &&

{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time })}

} + {!filesOnly && excludePinned &&

{t('RetentionPolicy_RoomWarning', { time })}

} + {!filesOnly && !excludePinned &&

{t('RetentionPolicy_RoomWarning_Unpinned', { time })}

} +
); }; diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx index 0fd882f2c5cc..401448ceb396 100644 --- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx @@ -18,7 +18,7 @@ import type { ReactElement } from 'react'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; -import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import { dispatchToastMessage } from '../../../lib/toast'; import GenericModal from '../../GenericModal'; import Tags from '../Tags'; diff --git a/apps/meteor/client/components/RoomAutoComplete/Avatar.tsx b/apps/meteor/client/components/RoomAutoComplete/Avatar.tsx deleted file mode 100644 index 1437c4ebbe89..000000000000 --- a/apps/meteor/client/components/RoomAutoComplete/Avatar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Options } from '@rocket.chat/fuselage'; -import { RoomAvatar } from '@rocket.chat/ui-avatar'; -import type { FC } from 'react'; -import React from 'react'; - -type AvatarProps = { - value: string; - type: string; - avatarETag?: string; -}; - -const Avatar: FC = ({ value, type, avatarETag, ...props }) => ( - -); - -export default Avatar; diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 39fbf9577776..10bea7cc0b60 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -3,34 +3,49 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import type { ReactElement, ComponentProps } from 'react'; +import type { ComponentProps } from 'react'; import React, { memo, useMemo, useState } from 'react'; -import Avatar from './Avatar'; - const generateQuery = ( term = '', ): { selector: string; } => ({ selector: JSON.stringify({ name: term }) }); -type RoomAutoCompleteProps = Omit, 'filter'>; +type RoomAutoCompleteProps = Omit, 'filter'> & { scope?: 'admin' | 'regular' }; + +const AVATAR_SIZE = 'x20'; -const RoomAutoComplete = ({ value, onChange, ...props }: RoomAutoCompleteProps): ReactElement => { +const ROOM_AUTOCOMPLETE_PARAMS = { + admin: { + endpoint: '/v1/rooms.autocomplete.adminRooms', + key: 'roomsAutoCompleteAdmin', + }, + regular: { + endpoint: '/v1/rooms.autocomplete.channelAndPrivate', + key: 'roomsAutoCompleteRegular', + }, +} as const; + +const RoomAutoComplete = ({ value, onChange, scope = 'regular', ...props }: RoomAutoCompleteProps) => { const [filter, setFilter] = useState(''); const filterDebounced = useDebouncedValue(filter, 300); - const autocomplete = useEndpoint('GET', '/v1/rooms.autocomplete.channelAndPrivate'); + const roomsAutoCompleteEndpoint = useEndpoint('GET', ROOM_AUTOCOMPLETE_PARAMS[scope].endpoint); - const result = useQuery(['rooms.autocomplete.channelAndPrivate', filterDebounced], () => autocomplete(generateQuery(filterDebounced)), { - keepPreviousData: true, - }); + const result = useQuery( + [ROOM_AUTOCOMPLETE_PARAMS[scope].key, filterDebounced], + () => roomsAutoCompleteEndpoint(generateQuery(filterDebounced)), + { + keepPreviousData: true, + }, + ); const options = useMemo( () => result.isSuccess - ? result.data.items.map(({ name, _id, avatarETag, t }) => ({ + ? result.data.items.map(({ name, fname, _id, avatarETag, t }) => ({ value: _id, - label: { name, avatarETag, type: t }, + label: { name: fname || name, avatarETag, type: t }, })) : [], [result.data?.items, result.isSuccess], @@ -43,18 +58,18 @@ const RoomAutoComplete = ({ value, onChange, ...props }: RoomAutoCompleteProps): onChange={onChange} filter={filter} setFilter={setFilter} - renderSelected={({ selected: { value, label } }): ReactElement => ( + renderSelected={({ selected: { value, label } }) => ( <> - + {label?.name} )} - renderItem={({ value, label, ...props }): ReactElement => ( -