From d80cb71c3ceb4e0e61950355779a16076787067e Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:25:57 +0000 Subject: [PATCH] temp --- .changeset/clean-flies-collect.md | 5 - .changeset/curvy-flies-greet.md | 5 - .changeset/fair-colts-remain.md | 13 - .changeset/forty-gorillas-kneel.md | 6 - .changeset/light-terms-ring.md | 5 - .changeset/nervous-rivers-fry.md | 6 - .changeset/old-coins-bow.md | 5 - .changeset/real-jeans-worry.md | 5 - .changeset/serious-mice-film.md | 5 - .changeset/smart-radios-reflect.md | 6 - .changeset/spicy-spiders-search.md | 5 - .changeset/two-guests-tan.md | 5 - .changeset/weak-trees-exercise.md | 5 - .github/actions/build-docker-image/action.yml | 91 + .github/actions/build-docker/action.yml | 4 +- .github/workflows/ci-deploy-gh-pages.yml | 2 +- .github/workflows/ci-test-e2e.yml | 20 +- .github/workflows/ci.yml | 82 +- .github/workflows/new-release.yml | 2 +- .github/workflows/pr-update-description.yml | 2 +- .github/workflows/publish-release.yml | 2 +- .github/workflows/release-candidate.yml | 2 +- .../workflows/update-version-durability.yml | 32 +- README.md | 4 +- apps/meteor/.docker-mongo/Dockerfile | 2 +- .../.docker/{Dockerfile.alpine => Dockerfile} | 2 +- apps/meteor/.docker/Dockerfile.debian | 2 +- apps/meteor/.eslintignore | 1 - apps/meteor/.meteor/packages | 8 +- apps/meteor/.meteor/release | 2 +- apps/meteor/.meteor/versions | 18 +- apps/meteor/.mocharc.js | 1 - apps/meteor/.stylelintignore | 1 - apps/meteor/app/api/server/api.ts | 11 +- apps/meteor/app/api/server/definition.ts | 40 +- apps/meteor/app/api/server/v1/roles.ts | 6 +- .../meteor/app/api/server/v1/subscriptions.ts | 2 +- apps/meteor/app/api/server/v1/users.ts | 4 +- apps/meteor/app/apps/server/bridges/email.ts | 9 +- .../app/apps/server/bridges/livechat.ts | 6 +- .../server/converters/transformMappedData.ts | 4 +- apps/meteor/app/assets/server/assets.ts | 14 +- .../server/functions/getRoles.ts | 3 - .../server/methods/deleteRole.ts | 2 +- .../autotranslate/client/lib/actionButton.ts | 6 +- .../startup/{responses.ts => responses.js} | 23 +- .../supportedVersionsToken.ts | 2 +- .../app/custom-oauth/client/CustomOAuth.ts | 5 +- .../client/createDiscussionMessageAction.ts | 5 +- .../app/e2e/client/{events.ts => events.js} | 2 +- .../app/e2e/client/{helper.ts => helper.js} | 64 +- ...hat.e2e.room.ts => rocketchat.e2e.room.js} | 110 +- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 2 +- .../lib/{emojiCustom.ts => emojiCustom.js} | 85 +- .../emoji-custom/client/lib/function-isSet.js | 9 + .../client/{emojiParser.ts => emojiParser.js} | 16 +- apps/meteor/app/emoji/client/helpers.ts | 13 +- apps/meteor/app/emoji/lib/rocketchat.ts | 32 +- .../server/lib/RocketChat.ErrorHandler.ts | 6 +- .../federation/server/functions/helpers.ts | 11 +- .../server/handler/{index.ts => index.js} | 18 +- .../app/file-upload/server/lib/FileUpload.ts | 2 +- .../server/PendingAvatarImporter.ts | 3 +- .../importer-slack/server/SlackImporter.ts | 17 +- .../classes/converters/UserConverter.ts | 2 +- .../integrations/server/lib/updateHistory.ts | 2 +- .../incoming/updateIncomingIntegration.ts | 2 +- .../outgoing/updateOutgoingIntegration.ts | 4 +- .../localHandlers/{index.ts => index.js} | 0 .../peerHandlers/{index.ts => index.js} | 0 .../irc/server/servers/{index.ts => index.js} | 0 .../client/{OAuthProxy.ts => OAuthProxy.js} | 4 +- .../lib/server/functions/closeLivechatRoom.ts | 6 +- .../server/functions/loadMessageHistory.ts | 2 +- .../server/functions/notifications/index.ts | 19 + .../notifications/messageContainsHighlight.ts | 22 - .../app/lib/server/functions/saveUser.js | 475 + .../server/functions/saveUser/handleBio.ts | 22 - .../functions/saveUser/handleNickname.ts | 22 - .../lib/server/functions/saveUser/index.ts | 2 - .../server/functions/saveUser/saveNewUser.ts | 84 - .../lib/server/functions/saveUser/saveUser.ts | 179 - .../functions/saveUser/sendUserEmail.ts | 54 - .../functions/saveUser/validateUserData.ts | 107 - .../functions/saveUser/validateUserEditing.ts | 96 - .../server/functions/updateGroupDMsName.ts | 4 +- .../server/lib/interceptDirectReplyEmails.js | 11 +- .../lib/server/lib/notifyUsersOnMessage.ts | 11 +- .../server/lib/sendNotificationsOnMessage.ts | 3 +- .../lib/server/methods/getChannelHistory.ts | 4 +- .../lib/server/methods/insertOrUpdateUser.ts | 6 +- .../app/lib/server/methods/leaveRoom.ts | 3 +- .../client/collections/LivechatInquiry.js | 3 + .../client/collections/LivechatInquiry.ts | 4 - .../client/lib/stream/queueManager.ts | 6 +- .../imports/server/rest/departments.ts | 21 +- .../app/livechat/imports/server/rest/sms.ts | 14 +- .../app/livechat/server/api/v1/agent.ts | 3 +- .../livechat/server/api/v1/offlineMessage.ts | 4 +- .../app/livechat/server/api/v1/pageVisited.ts | 4 +- .../meteor/app/livechat/server/api/v1/room.ts | 11 +- .../server/hooks/processRoomAbandonment.ts | 2 +- .../hooks/sendEmailTranscriptOnClose.ts | 2 +- .../app/livechat/server/lib/Departments.ts | 69 + apps/meteor/app/livechat/server/lib/Helper.ts | 21 +- .../app/livechat/server/lib/LivechatTyped.ts | 491 +- .../app/livechat/server/lib/QueueManager.ts | 26 +- .../app/livechat/server/lib/RoutingManager.ts | 14 +- .../server/lib/analytics/dashboards.ts | 4 +- .../app/livechat/server/lib/departmentsLib.ts | 304 - .../livechat/server/lib/getOnlineAgents.ts | 24 - .../livechat/server/lib/getRoomMessages.ts | 23 - .../app/livechat/server/lib/localTypes.ts | 26 +- apps/meteor/app/livechat/server/lib/logger.ts | 1 - .../app/livechat/server/lib/messages.ts | 91 - .../app/livechat/server/lib/sendTranscript.ts | 2 +- .../app/livechat/server/lib/tracking.ts | 54 - .../livechat/server/methods/saveDepartment.ts | 4 +- .../server/methods/sendMessageLivechat.ts | 2 +- apps/meteor/app/livechat/server/startup.ts | 7 +- apps/meteor/app/mailer/server/api.ts | 2 +- .../client/actionButton.ts | 5 +- apps/meteor/app/push/server/push.ts | 4 +- apps/meteor/app/reactions/client/init.ts | 4 +- ...client.ts => slackbridge_import.client.js} | 2 +- .../app/slackbridge/server/SlackAdapter.js | 2 +- .../app/slashcommands-hide/server/hide.ts | 2 +- .../slashcommands-inviteall/server/server.ts | 10 +- .../app/statistics/server/lib/statistics.ts | 41 +- .../client/lib/normalizeThreadTitle.ts | 4 +- .../client/messageAction/replyInThread.ts | 4 +- .../ui-master/server/{index.ts => index.js} | 29 +- apps/meteor/app/ui-master/server/inject.ts | 4 +- .../app/ui-utils/client/lib/MessageAction.ts | 1 + .../client/lib/messageActionDefault.ts | 26 +- .../app/ui-utils/client/lib/messageBox.ts | 17 +- apps/meteor/app/ui/client/lib/ChatMessages.ts | 2 +- apps/meteor/app/utils/lib/i18n.ts | 9 - apps/meteor/app/utils/rocketchat.info | 2 +- .../functions/normalizeMessageFileUpload.ts | 2 +- .../webdav/server/lib/getWebdavCredentials.ts | 2 +- .../client/{WebRTCClass.ts => WebRTCClass.js} | 477 +- apps/meteor/app/webrtc/client/adapter.js | 6 + apps/meteor/app/webrtc/client/adapter.ts | 7 - .../client/{screenShare.ts => screenShare.js} | 21 +- .../NavBarItemOmnichannelLivechatToggle.tsx | 5 +- .../NavBarItemAuditMenu.tsx | 5 +- .../NavBarItemMarketPlaceMenu.tsx | 5 +- .../NavBarPagesToolbar/hooks/useAuditMenu.tsx | 5 +- .../NavBarItemAdministrationMenu.tsx | 5 +- .../NavBarItemLoginPage.tsx | 5 +- .../UserMenu/UserMenuButton.tsx | 2 +- .../UserMenu/UserMenuHeader.tsx | 7 +- .../UserMenu/hooks/useAccountItems.tsx | 5 +- .../UserMenu/hooks/useStatusItems.tsx | 2 +- .../UserMenu/hooks/useUserMenu.tsx | 5 +- .../hooks/useAdministrationMenu.tsx | 5 +- .../client/apps/gameCenter/GameCenterList.tsx | 5 +- .../AutoCompleteDepartmentMultiple.tsx | 2 +- .../Contextualbar/ContextualbarContent.tsx | 29 +- .../Contextualbar/ContextualbarFooter.tsx | 29 +- .../Contextualbar/ContextualbarSection.tsx | 29 +- .../meteor/client/components/FilterByText.tsx | 21 +- .../client/components/GazzodownText.tsx | 2 +- .../hooks/useUpsellActions.ts | 2 +- .../components/Header/HeaderToolbarAction.tsx | 29 +- .../meteor/client/components/MarkdownText.tsx | 2 +- .../client/components/NotFoundState.tsx | 5 +- .../OmnichannelSortingDisclaimer.tsx | 7 +- .../client/components/Omnichannel/Tags.tsx | 5 +- .../Omnichannel/hooks/useAgentsList.ts | 5 +- .../Omnichannel/hooks/useDepartmentsList.ts | 5 +- .../Omnichannel/modals/CloseChatModal.tsx | 6 +- .../modals/EnterpriseDepartmentsModal.tsx | 5 +- .../Omnichannel/modals/ForwardChatModal.tsx | 7 +- .../client/components/Page/PageHeader.tsx | 7 +- .../RoomAutoComplete/RoomAutoComplete.tsx | 2 +- .../RoomAutoCompleteMultiple.tsx | 2 +- .../TwoFactorModal/TwoFactorEmailModal.tsx | 5 +- .../client/components/UrlChangeModal.tsx | 9 +- .../client/components/UserStatusMenu.tsx | 7 +- .../components/avatar/RoomAvatarEditor.tsx | 5 +- .../UserAvatarEditor/UserAvatarEditor.tsx | 5 +- .../ConnectionStatusBar.stories.tsx | 5 +- .../dashboards/DownloadDataButton.tsx | 5 +- .../components/message/StatusIndicators.tsx | 7 +- .../message/content/Attachments.tsx | 8 +- .../message/content/ThreadMetrics.tsx | 7 +- .../message/content/location/MapView.tsx | 2 +- .../message/content/reactions/Reaction.tsx | 5 +- .../content/urlPreviews/UrlPreview.tsx | 5 +- .../message/hooks/useNormalizedMessage.ts | 2 +- .../message/hooks/useOembedLayout.ts | 2 +- .../message/list/MessageListContext.tsx | 11 +- .../message/toolbar/MessageActionMenu.tsx | 25 +- .../toolbar/useWebDAVMessageAction.tsx | 6 +- .../variants/thread/ThreadMessageContent.tsx | 5 +- .../client/contexts/VideoConfContext.ts | 38 +- .../client/definitions/IRocketChatDesktop.ts | 2 +- apps/meteor/client/definitions/global.d.ts | 75 - .../roomActions/useWebRTCVideoRoomAction.ts | 2 +- .../client/hooks/useClipboardWithToast.ts | 5 +- .../client/hooks/useDevicesMenuOption.tsx | 5 +- apps/meteor/client/hooks/useDialModal.tsx | 5 +- .../hooks/useDownloadFromServiceWorker.ts | 3 +- apps/meteor/client/hooks/useEndpointAction.ts | 4 +- .../hooks/useFeaturePreviewEnableQuery.ts | 11 +- .../client/hooks/useFormatDateAndTime.ts | 2 +- apps/meteor/client/hooks/useFormatTime.ts | 2 +- .../hooks/useLicenseLimitsByBehavior.ts | 8 +- .../client/hooks/usePruneWarningMessage.ts | 6 +- apps/meteor/client/hooks/useTimeAgo.ts | 4 +- apps/meteor/client/hooks/useUpdateAvatar.ts | 5 +- apps/meteor/client/hooks/useVoipClient.ts | 2 +- apps/meteor/client/lib/VideoConfManager.ts | 16 +- .../lib/parseMessageTextToAstMarkdown.ts | 6 +- apps/meteor/client/lib/userData.ts | 4 +- apps/meteor/client/lib/utils/messageArgs.ts | 18 + apps/meteor/client/lib/utils/renderEmoji.ts | 2 +- .../client/lib/utils/renderMessageEmoji.ts | 2 +- apps/meteor/client/lib/voip/VoIPUser.ts | 5 +- .../client/meteorOverrides/login/google.ts | 13 +- apps/meteor/client/methods/index.ts | 1 + .../client/methods/setUserActiveStatus.ts | 9 + .../navbar/actions/NavbarHomeAction.tsx | 5 +- .../DepartmentBusinessHours.tsx | 5 +- .../additionalForms/DepartmentForwarding.tsx | 2 +- .../businessHours/BusinessHoursRow.tsx | 5 +- .../businessHours/BusinessHoursTable.tsx | 24 +- .../businessHours/useRemoveBusinessHour.tsx | 5 +- .../CannedResponseEditWithData.tsx | 5 +- .../cannedResponses/CannedResponsesPage.tsx | 5 +- .../CannedResponsesComposer.tsx | 5 +- .../components/cannedResponseForm.tsx | 5 +- .../CreateCannedResponseModal.tsx | 5 +- .../useRemoveCannedResponse.tsx | 5 +- .../hooks/useCannedResponseFilterOptions.ts | 5 +- .../hooks/useOmnichannelPrioritiesMenu.tsx | 5 +- .../omnichannel/monitors/MonitorsTable.tsx | 30 +- .../omnichannel/priorities/PrioritiesPage.tsx | 5 +- .../priorities/PriorityEditForm.tsx | 5 +- .../omnichannel/reports/ReportsPage.tsx | 5 +- .../reports/hooks/useAgentsSection.tsx | 5 +- .../reports/hooks/useChannelsSection.tsx | 12 +- .../reports/hooks/useDepartmentsSection.tsx | 5 +- .../reports/hooks/useStatusSection.tsx | 10 +- .../reports/hooks/useTagsSection.tsx | 5 +- .../reports/utils/formatPeriodDescription.tsx | 4 +- .../slaPolicies/RemoveSlaButton.tsx | 5 +- .../slaPolicies/SlaEditWithData.tsx | 5 +- .../omnichannel/slaPolicies/SlaPage.tsx | 5 +- .../omnichannel/slaPolicies/SlaTable.tsx | 24 +- .../client/omnichannel/tags/TagEdit.tsx | 5 +- .../omnichannel/tags/TagEditWithData.tsx | 5 +- .../client/omnichannel/tags/TagsTable.tsx | 26 +- .../client/omnichannel/tags/useRemoveTag.tsx | 5 +- .../omnichannel/units/UnitEditWithData.tsx | 5 +- .../client/omnichannel/units/UnitsTable.tsx | 31 +- .../providers/AppsProvider/AppsProvider.tsx | 1 - .../AuthenticationProvider.tsx | 4 +- .../hooks/useLDAPAndCrowdCollisionWarning.tsx | 4 +- .../providers/AuthorizationProvider.tsx | 11 +- .../EmojiPickerProvider.tsx | 2 +- .../client/providers/LayoutProvider.tsx | 2 +- .../client/providers/OmnichannelProvider.tsx | 26 +- .../client/providers/SettingsProvider.tsx | 2 +- .../client/providers/TranslationProvider.tsx | 6 +- .../client/providers/UserPresenceProvider.tsx | 2 +- .../hooks/useEmailVerificationWarning.tsx | 2 +- .../client/providers/VideoConfProvider.tsx | 21 +- .../client/sidebar/RoomList/RoomList.tsx | 5 +- .../client/sidebar/RoomList/RoomListRow.tsx | 4 +- .../RoomList/SideBarItemTemplateWithData.tsx | 14 +- .../RoomList/normalizeSidebarMessage.ts | 4 +- apps/meteor/client/sidebar/RoomMenu.tsx | 4 +- .../sidebar/footer/SidebarFooterDefault.tsx | 2 +- .../client/sidebar/footer/voip/VoipFooter.tsx | 2 +- .../client/sidebar/footer/voip/index.tsx | 5 +- .../CreateChannel/CreateChannelModal.tsx | 2 +- .../sidebar/header/CreateDirectMessage.tsx | 2 +- .../client/sidebar/header/HeaderUnstable.tsx | 5 +- .../FederatedRoomList.tsx | 5 +- .../MatrixFederationSearchModalContent.tsx | 5 +- .../sidebar/header/UserAvatarWithStatus.tsx | 2 +- .../client/sidebar/header/UserMenuHeader.tsx | 7 +- .../client/sidebar/header/actions/Login.tsx | 5 +- .../actions/hooks/useCreateRoomMenu.tsx | 5 +- .../actions/hooks/useGroupingListItems.tsx | 5 +- .../header/actions/hooks/useSortModeItems.tsx | 5 +- .../header/actions/hooks/useViewModeItems.tsx | 5 +- .../sidebar/header/hooks/useAccountItems.tsx | 5 +- .../hooks/useEncryptedRoomDescription.tsx | 5 +- .../sidebar/header/hooks/useStatusItems.tsx | 2 +- .../sidebar/header/hooks/useUserMenu.tsx | 5 +- .../client/sidebar/sections/BannerSection.tsx | 2 +- .../sidebar/sections/OmnichannelSection.tsx | 5 +- .../actions/OmnichannelLivechatToggle.tsx | 5 +- .../RoomList/SidebarItemTemplateWithData.tsx | 54 +- apps/meteor/client/sidebarv2/RoomMenu.tsx | 2 +- .../sidebarv2/footer/SidebarFooterDefault.tsx | 2 +- .../sidebarv2/footer/voip/VoipFooter.tsx | 2 +- .../client/sidebarv2/footer/voip/index.tsx | 5 +- .../sidebarv2/header/CreateChannelModal.tsx | 2 +- .../sidebarv2/header/CreateDirectMessage.tsx | 2 +- .../FederatedRoomList.tsx | 5 +- .../MatrixFederationSearchModalContent.tsx | 5 +- .../actions/hooks/useCreateRoomMenu.tsx | 5 +- .../actions/hooks/useGroupingListItems.tsx | 5 +- .../header/actions/hooks/useSortModeItems.tsx | 5 +- .../header/actions/hooks/useViewModeItems.tsx | 5 +- .../hooks/useEncryptedRoomDescription.tsx | 5 +- .../sidebarv2/hooks/useRoomList.spec.tsx | 251 - .../sidebarv2/hooks/useUnreadDisplay.spec.tsx | 237 - .../sidebarv2/hooks/useUnreadDisplay.ts | 15 - .../sidebarv2/sections/BannerSection.tsx | 2 +- .../startup/actionButtons/jumpToMessage.ts | 4 +- .../startup/actionButtons/jumpToPinMessage.ts | 4 +- .../actionButtons/jumpToSearchMessage.ts | 4 +- .../actionButtons/jumpToStarMessage.ts | 4 +- .../startup/actionButtons/permalinkPinned.ts | 4 +- .../startup/actionButtons/permalinkStar.ts | 4 +- .../startup/actionButtons/pinMessage.tsx | 4 +- .../startup/actionButtons/starMessage.ts | 5 +- .../startup/actionButtons/unpinMessage.ts | 4 +- .../startup/actionButtons/unstarMessage.ts | 5 +- apps/meteor/client/startup/iframeCommands.ts | 11 +- apps/meteor/client/startup/readReceipt.ts | 4 +- .../stories/contexts/ModalContextMock.tsx | 2 +- .../stories/contexts/ServerContextMock.tsx | 2 +- .../integrations/AccountIntegrationsPage.tsx | 5 +- .../integrations/AccountIntegrationsRoute.tsx | 2 +- .../OmnichannelPreferencesPage.tsx | 2 +- .../preferences/PreferencesGlobalSection.tsx | 5 +- .../PreferencesLocalizationSection.tsx | 5 +- .../preferences/PreferencesMyDataSection.tsx | 7 +- .../PreferencesNotificationsSection.tsx | 15 +- .../profile/useAccountProfileSettings.ts | 14 +- .../views/account/security/ChangePassword.tsx | 5 +- .../views/account/security/TwoFactorEmail.tsx | 5 +- .../views/account/security/TwoFactorTOTP.tsx | 5 +- .../AccountTokensTable/AccountTokensTable.tsx | 5 +- .../tokens/AccountTokensTable/AddToken.tsx | 5 +- .../views/admin/EditableSettingsContext.ts | 24 +- .../views/admin/customEmoji/CustomEmoji.tsx | 2 +- .../admin/customEmoji/CustomEmojiRoute.tsx | 5 +- .../admin/customEmoji/EditCustomEmoji.tsx | 5 +- .../customEmoji/EditCustomEmojiWithData.tsx | 5 +- .../admin/customSounds/AddCustomSound.tsx | 5 +- .../admin/customSounds/CustomSoundsPage.tsx | 5 +- .../CustomSoundsTable/CustomSoundRow.tsx | 5 +- .../CustomSoundsTable/CustomSoundsTable.tsx | 4 +- .../admin/customSounds/EditCustomSound.tsx | 5 +- .../views/admin/customSounds/EditSound.tsx | 5 +- .../CustomUserStatusFormWithData.tsx | 5 +- .../CustomUserStatusRoute.tsx | 2 +- .../CustomUserStatusService.tsx | 2 +- .../CustomUserStatusTable.tsx | 11 +- .../DeviceManagementAdminPage.tsx | 5 +- .../DeviceManagementAdminRoute.tsx | 5 +- .../DeviceManagementAdminRow.tsx | 5 +- .../DeviceManagementAdminTable.tsx | 2 +- .../DeviceManagementInfo.tsx | 5 +- .../views/admin/emailInbox/EmailInboxForm.tsx | 5 +- .../views/admin/emailInbox/EmailInboxPage.tsx | 5 +- .../views/admin/emailInbox/SendTestButton.tsx | 5 +- .../channels/useChannelsList.ts | 2 +- .../messages/useMessageOrigins.ts | 2 +- .../messages/useMessagesSent.ts | 2 +- .../messages/useTopFivePopularChannels.ts | 2 +- .../users/useActiveUsers.ts | 2 +- .../users/useHourlyChatActivity.ts | 2 +- .../engagementDashboard/users/useNewUsers.ts | 2 +- .../users/useUsersByTimeOfTheDay.ts | 2 +- .../users/useWeeklyChatActivity.ts | 2 +- .../federationDashboard/OverviewSection.tsx | 5 +- .../views/admin/import/ImportHistoryPage.tsx | 4 +- .../admin/import/ImportOperationSummary.tsx | 7 +- .../views/admin/import/ImportProgressPage.tsx | 9 +- .../views/admin/import/NewImportPage.tsx | 7 +- .../EditIntegrationsPageWithData.tsx | 5 +- .../admin/integrations/IntegrationsPage.tsx | 5 +- .../admin/integrations/IntegrationsTable.tsx | 2 +- .../hooks/useCreateIntegration.ts | 5 +- .../hooks/useDeleteIntegration.ts | 5 +- .../hooks/useUpdateIntegration.ts | 5 +- .../incoming/IncomingWebhookForm.tsx | 15 +- .../outgoing/OutgoingWebhookForm.tsx | 5 +- .../outgoing/history/HistoryItem.tsx | 5 +- .../client/views/admin/invites/InviteRow.tsx | 5 +- .../client/views/admin/mailer/MailerPage.tsx | 5 +- .../admin/moderation/MessageReportInfo.tsx | 7 +- .../moderation/ModerationConsoleTable.tsx | 2 +- .../views/admin/moderation/UserMessages.tsx | 5 +- .../UserReports/ModConsoleUsersTable.tsx | 29 +- .../moderation/UserReports/UserReportInfo.tsx | 5 +- .../moderation/helpers/ContextMessage.tsx | 7 +- .../moderation/helpers/ModerationFilter.tsx | 8 +- .../moderation/hooks/useDeleteMessage.tsx | 5 +- .../hooks/useDeleteMessagesAction.tsx | 5 +- .../hooks/useDismissMessageAction.tsx | 5 +- .../moderation/hooks/useResetAvatarAction.tsx | 5 +- .../admin/oauthApps/EditOauthAppWithData.tsx | 5 +- .../views/admin/oauthApps/OAuthAddApp.tsx | 5 +- .../views/admin/permissions/EditRolePage.tsx | 5 +- .../permissions/EditRolePageWithData.tsx | 5 +- .../UsersInRoleTable/UsersInRoleTable.tsx | 11 +- .../client/views/admin/rooms/EditRoom.tsx | 5 +- .../views/admin/rooms/EditRoomWithData.tsx | 5 +- .../client/views/admin/rooms/RoomRow.tsx | 5 +- .../client/views/admin/rooms/RoomsPage.tsx | 5 +- .../client/views/admin/rooms/RoomsTable.tsx | 11 +- .../settings/EditableSettingsProvider.tsx | 14 +- .../views/admin/settings/Setting/Setting.tsx | 18 +- .../Setting/inputs/ActionSettingInput.tsx | 7 +- .../admin/settings/SettingsGroupCard.tsx | 7 +- .../SettingsGroupPage/SettingsGroupPage.tsx | 7 +- .../SettingsGroupSelector.tsx | 4 +- .../views/admin/settings/SettingsPage.tsx | 5 +- .../admin/settings/groups/LDAPGroupPage.tsx | 5 +- .../OAuthGroupPage/CreateOAuthModal.spec.tsx | 51 - .../OAuthGroupPage/CreateOAuthModal.tsx | 57 +- .../VoipGroupPage/AssignAgentButton.tsx | 5 +- .../groups/VoipGroupPage/AssignAgentModal.tsx | 5 +- .../VoipGroupPage/RemoveAgentButton.tsx | 5 +- .../groups/VoipGroupPage/VoipGroupPage.tsx | 5 +- .../admin/settings/hooks/useSettingsGroups.ts | 5 +- .../subscription/hooks/useRemoveLicense.ts | 5 +- .../views/admin/users/AdminUserForm.tsx | 2 +- .../admin/users/AdminUserInfoActions.tsx | 5 +- .../AdminUserSetRandomPasswordContent.tsx | 11 +- .../views/admin/users/AdminUserUpgrade.tsx | 5 +- .../admin/users/UsersTable/UsersTable.tsx | 10 +- .../users/UsersTable/UsersTableFilters.tsx | 2 +- .../users/hooks/useChangeAdminStatusAction.ts | 2 +- .../users/hooks/useChangeUserStatusAction.ts | 2 +- .../admin/users/hooks/useDeleteUserAction.tsx | 2 +- .../users/hooks/useResetE2EEKeyAction.tsx | 2 +- .../admin/users/hooks/useResetTOTPAction.tsx | 2 +- .../hooks/useSendInvitationEmailMutation.ts | 5 +- .../hooks/useSendWelcomeEmailMutation.ts | 5 +- .../users/hooks/useVoipExtensionAction.tsx | 2 +- .../voip/hooks/useVoipExtensionAction.tsx | 2 +- .../voip/hooks/useVoipExtensionPermission.tsx | 2 +- .../views/admin/viewLogs/ServerLogs.tsx | 5 +- .../DeploymentCard/DeploymentCard.tsx | 5 +- .../UsersUploadsCard/UsersUploadsCard.tsx | 5 +- .../workspace/VersionCard/VersionCard.tsx | 8 +- .../modals/RegisterWorkspaceModal.tsx | 5 +- .../RegisterWorkspaceSetupStepOneModal.tsx | 6 +- .../RegisterWorkspaceSetupStepTwoModal.tsx | 9 +- .../modals/RegisterWorkspaceTokenModal.tsx | 8 +- .../modals/RegisteredWorkspaceModal.tsx | 5 +- .../views/admin/workspace/WorkspaceRoute.tsx | 5 +- .../components/forms/DateRangePicker.tsx | 2 +- .../client/views/banners/BannerRegion.tsx | 3 +- .../views/banners/hooks/useUserBanners.ts | 11 +- .../client/views/directory/DirectoryPage.tsx | 7 +- .../directory/hooks/useDirectoryQuery.ts | 24 +- .../channels/ChannelsTable/ChannelsTable.tsx | 8 +- .../tabs/teams/TeamsTable/TeamsTable.tsx | 9 +- .../tabs/users/UsersTable/UsersTable.tsx | 9 +- .../views/home/CustomHomePageContent.tsx | 2 +- .../client/views/home/DefaultHomePage.tsx | 4 +- .../client/views/home/HomePageHeader.tsx | 2 +- .../views/home/cards/CustomContentCard.tsx | 6 +- .../hooks/roomActions/useArchiveRoom.tsx | 5 +- .../views/hooks/roomActions/useDeleteRoom.tsx | 5 +- .../meteor/client/views/invite/InvitePage.tsx | 5 +- .../invite/hooks/useInviteTokenMutation.ts | 5 +- .../invite/hooks/useValidateInviteQuery.ts | 5 +- .../views/mailer/MailerUnsubscriptionPage.tsx | 5 +- .../AppDetailsPage/AppDetailsPageTabs.tsx | 5 +- .../tabs/AppDetails/AppDetailsAPIs.tsx | 5 +- .../tabs/AppReleases/AppReleases.tsx | 13 +- .../tabs/AppRequests/AppRequests.tsx | 5 +- .../tabs/AppStatus/AppStatus.tsx | 7 +- .../marketplace/AppsPage/AppsFilters.tsx | 4 +- .../marketplace/AppsPage/AppsPageContent.tsx | 6 +- .../components/BannerEnterpriseTrialEnded.tsx | 7 +- .../components/ScreenshotCarousel.tsx | 4 +- .../client/views/marketplace/helpers.ts | 8 +- .../views/marketplace/hooks/useAppInfo.ts | 2 +- apps/meteor/client/views/meet/CallPage.tsx | 44 +- apps/meteor/client/views/meet/MeetPage.tsx | 5 +- .../oauth/components/CurrentUserDisplay.tsx | 2 +- .../client/views/oauth/components/Layout.tsx | 2 +- .../omnichannel/ExternalFrameContainer.tsx | 4 +- .../omnichannel/agents/AgentEditWithData.tsx | 5 +- .../views/omnichannel/agents/AgentInfo.tsx | 7 +- .../views/omnichannel/agents/AgentsPage.tsx | 5 +- .../agents/AgentsTable/AddAgent.tsx | 5 +- .../agents/AgentsTable/AgentsTable.tsx | 15 +- .../agents/AgentsTable/AgentsTableRow.tsx | 5 +- .../omnichannel/agents/hooks/useQuery.ts | 24 +- .../omnichannel/analytics/AgentOverview.tsx | 11 +- .../analytics/InterchangeableChart.tsx | 5 +- .../views/omnichannel/analytics/Overview.tsx | 5 +- .../omnichannel/appearance/AppearancePage.tsx | 5 +- .../appearance/AppearancePageContainer.tsx | 5 +- .../BusinessHoursDisabledPage.tsx | 5 +- .../BusinessHoursMultiplePage.tsx | 5 +- .../EditBusinessHoursWithData.tsx | 5 +- .../omnichannel/components/CustomField.tsx | 5 +- .../ContactHistoryMessagesList.tsx | 2 +- .../currentChats/CurrentChatsPage.tsx | 5 +- .../omnichannel/currentChats/FilterByText.tsx | 5 +- .../currentChats/RemoveChatButton.tsx | 5 +- .../customFields/CustomFieldsPage.tsx | 5 +- .../customFields/CustomFieldsTable.tsx | 30 +- .../customFields/EditCustomFieldsWithData.tsx | 5 +- .../customFields/useRemoveCustomField.tsx | 5 +- .../DepartmentAgentsTable/AddAgent.tsx | 5 +- .../RemoveAgentButton.tsx | 5 +- .../DepartmentsTable/DepartmentsTable.tsx | 30 +- .../RemoveDepartmentModal.tsx | 5 +- .../EditDepartmentWithAllowedForwardData.tsx | 5 +- .../departments/EditDepartmentWithData.tsx | 5 +- .../omnichannel/departments/NewDepartment.tsx | 5 +- .../directory/CallsContextualBarDirectory.tsx | 5 +- .../directory/ChatsContextualBar.tsx | 5 +- .../directory/ContactContextualBar.tsx | 5 +- .../omnichannel/directory/calls/CallTable.tsx | 33 +- .../omnichannel/directory/chats/ChatTable.tsx | 4 +- .../chats/contextualBar/ChatInfo.tsx | 4 +- .../chats/contextualBar/ChatInfoDirectory.tsx | 9 +- .../contextualBar/ChatsContextualBar.tsx | 5 +- .../directory/components/AgentField.tsx | 5 +- .../directory/components/ContactField.tsx | 5 +- .../directory/contacts/ContactTable.tsx | 16 +- .../contacts/contextualBar/ContactNewEdit.tsx | 5 +- .../contextualBar/ContactsContextualBar.tsx | 5 +- .../omnichannel/installation/Installation.tsx | 8 +- .../views/omnichannel/managers/AddManager.tsx | 5 +- .../omnichannel/managers/ManagersRoute.tsx | 5 +- .../omnichannel/managers/ManagersTable.tsx | 11 +- .../managers/RemoveManagerButton.tsx | 5 +- .../realTimeMonitoring/counter/CounterRow.tsx | 2 +- .../omnichannel/triggers/EditTrigger.tsx | 5 +- .../triggers/EditTriggerWithData.tsx | 5 +- .../omnichannel/triggers/TriggersPage.tsx | 5 +- .../actions/ActionExternalServiceUrl.tsx | 5 +- .../webhooks/WebhooksPageContainer.tsx | 5 +- .../OutlookEventsList/OutlookEventItem.tsx | 9 +- .../OutlookEventsList/OutlookEventsList.tsx | 2 +- .../hooks/useOutlookAuthentication.ts | 5 +- .../views/room/Announcement/Announcement.tsx | 7 +- .../room/E2EESetup/RoomE2EENotAllowed.tsx | 5 +- .../client/views/room/Header/Header.tsx | 2 +- .../room/Header/Omnichannel/BackButton.tsx | 5 +- .../QuickActions/hooks/useQuickActions.tsx | 2 +- .../room/Header/RoomToolbox/RoomToolbox.tsx | 5 +- .../views/room/Header/icons/Encrypted.tsx | 5 +- .../views/room/Header/icons/Translate.tsx | 5 +- .../client/views/room/HeaderV2/Header.tsx | 2 +- .../room/HeaderV2/Omnichannel/BackButton.tsx | 5 +- .../QuickActions/hooks/useQuickActions.tsx | 2 +- .../room/HeaderV2/RoomToolbox/RoomToolbox.tsx | 5 +- .../views/room/HeaderV2/icons/Encrypted.tsx | 5 +- .../views/room/HeaderV2/icons/Translate.tsx | 8 +- .../views/room/MessageList/MessageList.tsx | 2 +- .../MessageList/hooks/useAutoLinkDomains.ts | 2 +- .../MessageList/hooks/useAutoTranslate.ts | 2 +- .../views/room/MessageList/hooks/useKatex.ts | 6 +- .../room/MessageList/hooks/useMessages.ts | 2 +- .../providers/MessageListProvider.tsx | 6 +- .../RoomAnnouncement/RoomAnnouncement.tsx | 11 +- .../meteor/client/views/room/RoomNotFound.tsx | 5 +- .../room/Sidepanel/hooks/useItemData.tsx | 24 +- .../views/room/UserCard/UserCardWithData.tsx | 12 +- .../client/views/room/body/RoomBody.tsx | 4 +- .../client/views/room/body/RoomBodyV2.tsx | 2 +- .../RoomForeword/RoomForewordUsernameList.tsx | 2 +- .../client/views/room/body/RoomTopic.tsx | 2 +- .../body/hooks/useFileUploadDropTarget.ts | 2 +- .../room/body/hooks/useGoToHomeOnRemoved.ts | 5 +- .../ComposerOmnichannelJoin.tsx | 5 +- .../views/room/composer/ComposerReadOnly.tsx | 5 +- .../room/composer/messageBox/MessageBox.tsx | 43 +- .../hooks/useAudioMessageAction.ts | 8 +- .../hooks/useCreateDiscussionAction.tsx | 2 +- .../hooks/useFileUploadAction.ts | 2 +- .../hooks/useShareLocationAction.tsx | 7 +- .../hooks/useVideoMessageAction.ts | 8 +- .../hooks/useWebdavActions.tsx | 4 +- .../composer/messageBox/MessageBoxReplies.tsx | 9 +- .../Discussions/DiscussionsList.tsx | 7 +- .../ExportMessages/MailExportForm.tsx | 2 +- .../ExportMessages/useRoomExportMutation.ts | 5 +- .../Info/EditRoomInfo/EditRoomInfo.tsx | 2 +- .../Info/hooks/useRoomActions.ts | 12 +- .../views/room/contextualBar/MentionsTab.tsx | 5 +- .../components/MessageSearch.tsx | 2 +- .../views/room/contextualBar/OTR/OTR.tsx | 4 +- .../room/contextualBar/PinnedMessagesTab.tsx | 5 +- .../PruneMessages/PruneMessagesWithData.tsx | 24 +- .../RoomFiles/hooks/useDeleteFile.tsx | 5 +- .../hooks/useMessageDeletionIsAllowed.ts | 2 +- .../AddMatrixUsers/AddMatrixUsersModal.tsx | 5 +- .../RoomMembers/AddUsers/AddUsers.tsx | 5 +- .../contextualBar/RoomMembers/RoomMembers.tsx | 2 +- .../room/contextualBar/StarredMessagesTab.tsx | 5 +- .../room/contextualBar/Threads/Thread.tsx | 4 +- .../room/contextualBar/Threads/ThreadList.tsx | 2 +- .../Threads/components/ThreadListItem.tsx | 2 +- .../Threads/components/ThreadMessageList.tsx | 7 +- .../UserInfo/UserInfoWithData.tsx | 5 +- .../VideoConfList/VideoConfList.tsx | 2 +- .../VideoConfList/VideoConfListItem.tsx | 2 +- .../hooks/useVideoConfOpenCall.spec.tsx | 2 +- .../client/views/room/hooks/useDateScroll.ts | 79 +- .../client/views/room/hooks/useOpenRoom.ts | 2 +- .../views/room/hooks/useRetentionPolicy.ts | 20 +- .../actions/useAddUserAction.tsx | 2 +- .../actions/useBlockUserAction.ts | 2 +- .../actions/useChangeLeaderAction.ts | 2 +- .../actions/useChangeModeratorAction.tsx | 2 +- .../actions/useChangeOwnerAction.tsx | 2 +- .../actions/useDirectMessageAction.ts | 2 +- .../actions/useIgnoreUserAction.ts | 2 +- .../actions/useMuteUserAction.tsx | 2 +- .../actions/useRedirectModerationConsole.ts | 5 +- .../actions/useRemoveUserAction.tsx | 2 +- .../actions/useReportUser.tsx | 2 +- .../actions/useVideoCallAction.tsx | 2 +- .../actions/useVoipCallAction.tsx | 2 +- .../FileUploadModal/FileUploadModal.tsx | 2 +- .../ReadReceiptsModal/ReadReceiptsModal.tsx | 5 +- .../room/providers/ComposerPopupProvider.tsx | 26 +- .../room/webdav/AddWebdavAccountModal.tsx | 5 +- .../views/room/webdav/SaveToWebdavModal.tsx | 5 +- .../WebdavFilePickerModal.tsx | 4 +- .../views/root/DocumentTitleWrapper.tsx | 2 +- .../root/MainLayout/AccessibilityShortcut.tsx | 5 +- .../views/root/MainLayout/LoginPage.tsx | 5 +- .../root/MainLayout/RegisterUsername.tsx | 2 +- .../views/root/MainLayout/UsernameCheck.tsx | 2 +- .../views/root/hooks/useEscapeKeyStroke.ts | 5 +- .../views/root/hooks/useGoogleTagManager.ts | 2 +- .../views/root/hooks/useUnreadMessages.ts | 5 +- .../views/setupWizard/SetupWizardRoute.tsx | 18 +- .../views/setupWizard/steps/AdminInfoStep.tsx | 25 +- .../steps/CloudAccountConfirmation.tsx | 23 +- .../steps/OrganizationInfoStep.tsx | 32 +- .../setupWizard/steps/RegisterServerStep.tsx | 21 +- .../TeamAutocomplete/TeamAutocomplete.tsx | 2 +- .../AddExistingModal/AddExistingModal.tsx | 5 +- .../channels/TeamsChannelItem.tsx | 5 +- .../channels/hooks/useRemoveRoomFromTeam.tsx | 5 +- .../channels/hooks/useToggleAutoJoin.tsx | 2 +- .../components/modals/WrapUpCallModal.tsx | 5 +- apps/meteor/definition/externals/global.d.ts | 24 + .../meteor/meteorhacks-inject-initial.d.ts | 1 - .../server/lib/canned-responses.js | 2 +- .../server/api/lib/tags.ts | 2 +- .../applySimultaneousChatsRestrictions.ts | 4 +- .../server/hooks/beforeRoutingChat.ts | 4 +- .../server/hooks/scheduleAutoTransfer.ts | 2 +- .../server/hooks/sendPdfTranscriptOnClose.ts | 2 +- .../storage/ConfigurableAppSourceStorage.ts | 5 +- .../apps/storage/{index.ts => index.js} | 0 .../server/configuration/videoConference.ts | 2 +- .../infrastructure/matrix/Bridge.ts | 5 +- .../local-services/instance/getLogger.ts | 2 +- apps/meteor/ee/server/models/raw/Users.ts | 2 +- apps/meteor/ee/server/sdk/index.ts | 6 +- apps/meteor/ee/server/services/Dockerfile | 4 +- apps/meteor/ee/server/services/package.json | 22 +- apps/meteor/lib/callbacks.ts | 21 +- apps/meteor/lib/getSubscriptionUnreadData.ts | 63 - apps/meteor/lib/utils/generatePath.ts | 8 +- apps/meteor/package.json | 168 +- apps/meteor/playwright.config.ts | 2 +- apps/meteor/server/email/IMAPInterceptor.ts | 42 +- .../server/features/EmailInbox/EmailInbox.ts | 2 +- .../EmailInbox/EmailInbox_Incoming.ts | 5 +- .../EmailInbox/EmailInbox_Outgoing.ts | 2 +- apps/meteor/server/lib/dataExport/sendFile.ts | 2 +- apps/meteor/server/lib/i18n.ts | 10 + apps/meteor/server/lib/logger/logPayloads.ts | 4 +- .../lib/moderation/deleteReportedMessages.ts | 17 +- apps/meteor/server/methods/browseChannels.ts | 6 +- apps/meteor/server/methods/removeRoomOwner.ts | 5 +- .../server/methods/removeUserFromRoom.ts | 6 +- .../server/methods/requestDataDownload.ts | 2 +- apps/meteor/server/models/raw/BaseRaw.ts | 15 +- .../server/models/raw/ExportOperations.ts | 9 - .../models/raw/LivechatDepartmentAgents.ts | 23 - .../server/models/raw/LivechatVisitors.ts | 8 +- apps/meteor/server/models/raw/Messages.ts | 21 +- .../server/models/raw/ModerationReports.ts | 2 +- apps/meteor/server/models/raw/NpsVote.ts | 13 - apps/meteor/server/models/raw/Roles.ts | 34 +- apps/meteor/server/models/raw/Rooms.ts | 63 +- apps/meteor/server/models/raw/ServerEvents.ts | 16 +- .../meteor/server/models/raw/Subscriptions.ts | 26 +- apps/meteor/server/models/raw/TeamMember.ts | 8 - apps/meteor/server/models/raw/Users.js | 55 +- .../server/models/raw/VideoConference.ts | 21 +- .../routes/avatar/{index.ts => index.js} | 4 +- .../server/routes/avatar/middlewares/auth.js | 22 + .../routes/avatar/middlewares/auth.spec.ts | 87 - .../server/routes/avatar/middlewares/auth.ts | 44 - .../{browserVersion.ts => browserVersion.js} | 31 +- .../avatar/middlewares/browserVersion.spec.ts | 87 - .../server/routes/avatar/middlewares/index.js | 7 + .../server/routes/avatar/middlewares/index.ts | 7 - apps/meteor/server/routes/avatar/room.js | 78 + apps/meteor/server/routes/avatar/room.spec.ts | 139 - apps/meteor/server/routes/avatar/room.ts | 61 - apps/meteor/server/routes/avatar/user.js | 84 + apps/meteor/server/routes/avatar/user.spec.ts | 155 - apps/meteor/server/routes/avatar/user.ts | 74 - .../routes/avatar/{utils.ts => utils.js} | 69 +- .../meteor/server/routes/avatar/utils.spec.ts | 231 - .../authorization/canAccessRoomLivechat.ts | 4 +- .../authorization/canAccessRoomVoip.ts | 4 +- .../infrastructure/matrix/Bridge.ts | 9 +- .../matrix/converters/room/RoomReceiver.ts | 12 +- .../infrastructure/matrix/handlers/Room.ts | 5 +- apps/meteor/server/services/nps/service.ts | 6 +- .../server/services/omnichannel/service.ts | 8 +- apps/meteor/server/services/team/service.ts | 29 +- apps/meteor/server/startup/initialData.js | 9 +- apps/meteor/server/startup/serverRunning.js | 2 +- apps/meteor/tests/data/permissions.helper.ts | 2 +- apps/meteor/tests/e2e/administration.spec.ts | 1 - apps/meteor/tests/e2e/feature-preview.spec.ts | 142 - apps/meteor/tests/e2e/fixtures/userStates.ts | 2 +- .../e2e/omnichannel/omnichannel-units.spec.ts | 23 +- .../tests/e2e/page-objects/account-profile.ts | 8 - .../page-objects/fragments/home-content.ts | 2 +- .../page-objects/fragments/home-sidenav.ts | 28 +- .../tests/e2e/page-objects/fragments/index.ts | 1 - .../e2e/page-objects/fragments/navbar.ts | 21 - .../tests/e2e/page-objects/home-channel.ts | 5 +- .../e2e/page-objects/omnichannel-livechat.ts | 5 +- .../e2e/page-objects/omnichannel-tags.ts | 4 +- .../meteor/tests/e2e/retention-policy.spec.ts | 25 +- .../tests/e2e/utils/omnichannel/rooms.ts | 6 +- apps/meteor/tests/end-to-end/api/chat.ts | 7 +- apps/meteor/tests/end-to-end/api/users.ts | 2 +- .../meteor/tests/mocks/server/BrokerMocked.ts | 4 + .../tests/unit/app/emoji/helpers.spec.ts | 51 - .../messageContainsHighlight.tests.ts | 69 - .../server/functions/settings.tests.ts | 43 +- .../unit/server/startup/initialData.tests.ts | 38 +- apps/uikit-playground/package.json | 22 +- .../src/Components/CodeEditor/index.tsx | 11 +- .../src/hooks/useFormatCodeMirrorValue.ts | 9 +- ee/apps/account-service/Dockerfile | 2 +- ee/apps/account-service/package.json | 12 +- ee/apps/authorization-service/Dockerfile | 2 +- ee/apps/authorization-service/package.json | 8 +- ee/apps/ddp-streamer/Dockerfile | 2 +- ee/apps/ddp-streamer/package.json | 17 +- ee/apps/ddp-streamer/src/Client.ts | 6 +- ee/apps/ddp-streamer/src/Publication.ts | 6 +- ee/apps/omnichannel-transcript/Dockerfile | 2 +- ee/apps/omnichannel-transcript/package.json | 10 +- ee/apps/presence-service/Dockerfile | 2 +- ee/apps/presence-service/package.json | 8 +- ee/apps/queue-worker/Dockerfile | 2 +- ee/apps/queue-worker/package.json | 10 +- ee/apps/stream-hub-service/Dockerfile | 2 +- ee/apps/stream-hub-service/package.json | 8 +- ee/apps/stream-hub-service/src/StreamHub.ts | 5 +- ee/packages/license/package.json | 6 +- ee/packages/license/src/license.ts | 6 +- ee/packages/license/src/v2/convertToV3.ts | 12 +- ee/packages/network-broker/package.json | 8 +- .../network-broker/src/EnterpriseCheck.ts | 37 +- .../network-broker/src/NetworkBroker.ts | 34 +- ee/packages/network-broker/src/index.ts | 2 +- ee/packages/omnichannel-services/package.json | 12 +- .../omnichannel-services/src/QueueWorker.ts | 7 +- ee/packages/pdf-worker/package.json | 10 +- .../src/strategies/ChatTranscript.ts | 2 +- ee/packages/presence/package.json | 10 +- ee/packages/ui-theming/package.json | 2 +- package.json | 13 +- packages/api-client/package.json | 2 +- packages/api-client/src/index.ts | 2 +- packages/apps-engine/package.json | 16 +- .../src/client/AppClientManager.ts | 5 +- packages/apps-engine/src/definition/App.ts | 6 +- .../src/definition/email/IEmail.ts | 5 +- .../src/definition/metadata/AppMethod.ts | 2 - .../metadata/RocketChatAssociations.ts | 5 +- .../slashcommands/SlashCommandContext.ts | 8 +- packages/apps-engine/src/server/ProxiedApp.ts | 11 +- .../src/server/accessors/ApiExtend.ts | 5 +- .../src/server/accessors/AppAccessors.ts | 5 +- .../server/accessors/CloudWorkspaceRead.ts | 5 +- .../src/server/accessors/EmailCreator.ts | 5 +- .../src/server/accessors/EnvironmentWrite.ts | 5 +- .../accessors/EnvironmentalVariableRead.ts | 5 +- .../accessors/ExternalComponentsExtend.ts | 5 +- .../src/server/accessors/LivechatCreator.ts | 5 +- .../src/server/accessors/LivechatRead.ts | 5 +- .../src/server/accessors/LivechatUpdater.ts | 5 +- .../src/server/accessors/MessageRead.ts | 5 +- .../src/server/accessors/ModerationModify.ts | 5 +- .../src/server/accessors/Modify.ts | 5 +- .../src/server/accessors/ModifyCreator.ts | 5 +- .../src/server/accessors/ModifyDeleter.ts | 5 +- .../src/server/accessors/ModifyExtender.ts | 5 +- .../src/server/accessors/ModifyUpdater.ts | 5 +- .../src/server/accessors/Notifier.ts | 6 +- .../src/server/accessors/OAuthAppsModify.ts | 5 +- .../src/server/accessors/OAuthAppsReader.ts | 5 +- .../src/server/accessors/Persistence.ts | 5 +- .../src/server/accessors/PersistenceRead.ts | 5 +- .../src/server/accessors/RoleRead.ts | 5 +- .../src/server/accessors/RoomRead.ts | 5 +- .../src/server/accessors/SchedulerExtend.ts | 5 +- .../src/server/accessors/SchedulerModify.ts | 5 +- .../src/server/accessors/ServerSettingRead.ts | 5 +- .../server/accessors/ServerSettingUpdater.ts | 5 +- .../server/accessors/ServerSettingsModify.ts | 5 +- .../src/server/accessors/SettingUpdater.ts | 5 +- .../server/accessors/SlashCommandsExtend.ts | 5 +- .../server/accessors/SlashCommandsModify.ts | 5 +- .../src/server/accessors/ThreadRead.ts | 5 +- .../src/server/accessors/UIController.ts | 5 +- .../src/server/accessors/UIExtend.ts | 5 +- .../src/server/accessors/UploadCreator.ts | 5 +- .../src/server/accessors/UploadRead.ts | 5 +- .../src/server/accessors/UserRead.ts | 5 +- .../src/server/accessors/UserUpdater.ts | 5 +- .../accessors/VideoConfProviderExtend.ts | 5 +- .../server/accessors/VideoConferenceRead.ts | 5 +- .../src/server/compiler/AppCompiler.ts | 2 +- .../apps-engine/src/server/managers/AppApi.ts | 6 +- .../src/server/managers/AppRuntimeManager.ts | 9 +- .../src/server/managers/AppSlashCommand.ts | 5 +- .../server/managers/AppVideoConfProvider.ts | 5 +- .../src/server/messages/Message.ts | 5 +- .../src/server/oauth2/OAuth2Client.ts | 5 +- .../server/runtime/AppsEngineNodeRuntime.ts | 5 +- .../runtime/deno/AppsEngineDenoRuntime.ts | 90 +- .../server/runtime/deno/LivenessManager.ts | 12 +- .../DenoRuntimeSubprocessController.spec.ts | 13 +- packages/base64/package.json | 4 +- packages/base64/src/base64.ts | 18 +- packages/cas-validate/package.json | 2 +- packages/core-services/package.json | 14 +- packages/core-services/src/LocalBroker.ts | 4 + packages/core-services/src/MeteorError.ts | 6 +- packages/core-services/src/index.ts | 76 +- packages/core-services/src/lib/Api.ts | 4 + packages/core-services/src/lib/proxify.ts | 29 +- .../core-services/src/types/IApiService.ts | 2 + packages/core-services/src/types/IBroker.ts | 1 + .../src/types/IOmnichannelService.ts | 3 +- packages/core-typings/package.json | 6 +- packages/core-typings/src/IRoom.ts | 8 +- packages/core-typings/src/ISetting.ts | 22 +- packages/core-typings/src/Serialized.ts | 27 +- packages/core-typings/src/utils.ts | 18 +- packages/ddp-client/package.json | 4 +- packages/ddp-client/src/ClientStream.ts | 5 +- packages/ddp-client/src/MinimalDDPClient.ts | 6 +- packages/ddp-client/src/TimeoutControl.ts | 5 +- .../src/legacy/RocketchatSDKLegacy.ts | 4 +- .../src/livechat/types/LivechatSDK.ts | 19 +- packages/ddp-client/src/types/streams.ts | 97 +- packages/eslint-config/package.json | 10 +- packages/freeswitch/package.json | 4 +- .../.storybook/DocsContainer.tsx | 2 +- packages/fuselage-ui-kit/package.json | 30 +- .../src/blocks/ActionsBlock.tsx | 8 +- .../src/blocks/CalloutBlock.tsx | 2 +- .../src/blocks/ContextBlock.tsx | 2 +- .../fuselage-ui-kit/src/blocks/ImageBlock.tsx | 2 +- .../fuselage-ui-kit/src/blocks/InputBlock.tsx | 6 +- .../src/blocks/PreviewBlock.tsx | 8 +- .../src/blocks/SectionBlock.tsx | 4 +- .../src/blocks/TabNavigationBlock.tsx | 2 +- .../VideoConferenceBlock.tsx | 6 +- .../hooks/useVideoConfData.ts | 2 +- .../hooks/useVideoConfDataStream.ts | 2 +- .../src/contexts/UiKitContext.ts | 4 +- .../ChannelsSelectElement.spec.tsx | 2 +- .../ChannelsSelectElement.tsx | 2 +- .../MultiChannelsSelectElement.spec.tsx | 2 +- .../MultiChannelsSelectElement.tsx | 2 +- .../hooks/useChannelsData.ts | 8 +- .../src/elements/CheckboxElement.tsx | 2 +- .../ContextElement/ContextElementItem.tsx | 2 +- .../src/elements/LinearScaleElement.tsx | 12 +- .../src/elements/MultiStaticSelectElement.tsx | 4 +- .../src/elements/OverflowElement.tsx | 6 +- .../src/elements/RadioButtonElement.tsx | 2 +- .../src/elements/StaticSelectElement.tsx | 4 +- .../src/elements/ToggleSwitchElement.tsx | 2 +- .../MultiUsersSelectElement.spec.tsx | 2 +- .../MultiUsersSelectElement.tsx | 2 +- .../UserSelectElement.spec.tsx | 2 +- .../UsersSelectElement/UsersSelectElement.tsx | 2 +- .../UsersSelectElement/hooks/useUsersData.ts | 4 +- .../src/hooks/useAppTranslation.spec.tsx | 2 +- .../src/hooks/useStringFromTextObject.ts | 2 +- .../src/hooks/useUiKitState.ts | 24 +- .../src/stories/Banner.stories.tsx | 30 +- .../src/stories/Message.stories.tsx | 24 +- .../src/stories/Modal.stories.tsx | 30 +- .../surfaces/ContextualBarSurfaceRenderer.tsx | 2 +- .../FuselageMessageSurfaceRenderer.tsx | 2 +- .../src/surfaces/FuselageSurfaceRenderer.tsx | 60 +- .../src/surfaces/createSurfaceRenderer.tsx | 6 +- .../fuselage-ui-kit/src/surfaces/index.ts | 4 +- .../utils/extractInitialStateFromLayout.ts | 4 +- .../src/utils/getInitialValue.ts | 10 +- .../fuselage-ui-kit/src/utils/hasElement.ts | 2 +- .../fuselage-ui-kit/src/utils/hasElements.ts | 2 +- packages/gazzodown/package.json | 29 +- packages/i18n/package.json | 1 + packages/i18n/src/locales/af.i18n.json | 2 + packages/i18n/src/locales/ar.i18n.json | 2 + packages/i18n/src/locales/az.i18n.json | 2 + packages/i18n/src/locales/be-BY.i18n.json | 2 + packages/i18n/src/locales/bg.i18n.json | 2 + packages/i18n/src/locales/bs.i18n.json | 2 + packages/i18n/src/locales/ca.i18n.json | 2 + packages/i18n/src/locales/cs.i18n.json | 2 + packages/i18n/src/locales/cy.i18n.json | 2 + packages/i18n/src/locales/da.i18n.json | 2 + packages/i18n/src/locales/de-AT.i18n.json | 2 + packages/i18n/src/locales/de-IN.i18n.json | 2 + packages/i18n/src/locales/de.i18n.json | 2 + packages/i18n/src/locales/el.i18n.json | 2 + packages/i18n/src/locales/en.i18n.json | 4 +- packages/i18n/src/locales/eo.i18n.json | 2 + packages/i18n/src/locales/es.i18n.json | 2 + packages/i18n/src/locales/fa.i18n.json | 2 + packages/i18n/src/locales/fi.i18n.json | 2 + packages/i18n/src/locales/fr.i18n.json | 2 + packages/i18n/src/locales/he.i18n.json | 2 + packages/i18n/src/locales/hi-IN.i18n.json | 2 + packages/i18n/src/locales/hr.i18n.json | 2 + packages/i18n/src/locales/hu.i18n.json | 2 + packages/i18n/src/locales/id.i18n.json | 2 + packages/i18n/src/locales/it.i18n.json | 2 + packages/i18n/src/locales/ja.i18n.json | 2 + packages/i18n/src/locales/ka-GE.i18n.json | 2 + packages/i18n/src/locales/km.i18n.json | 2 + packages/i18n/src/locales/ko.i18n.json | 2 + packages/i18n/src/locales/ku.i18n.json | 2 + packages/i18n/src/locales/lo.i18n.json | 2 + packages/i18n/src/locales/lt.i18n.json | 2 + packages/i18n/src/locales/lv.i18n.json | 2 + packages/i18n/src/locales/mn.i18n.json | 2 + packages/i18n/src/locales/ms-MY.i18n.json | 2 + packages/i18n/src/locales/nl.i18n.json | 2 + packages/i18n/src/locales/nn.i18n.json | 2 + packages/i18n/src/locales/no.i18n.json | 2 + packages/i18n/src/locales/pl.i18n.json | 2 + packages/i18n/src/locales/pt-BR.i18n.json | 2 + packages/i18n/src/locales/pt.i18n.json | 2 + packages/i18n/src/locales/ro.i18n.json | 2 + packages/i18n/src/locales/ru.i18n.json | 2 + packages/i18n/src/locales/se.i18n.json | 2 + packages/i18n/src/locales/sk-SK.i18n.json | 2 + packages/i18n/src/locales/sl-SI.i18n.json | 2 + packages/i18n/src/locales/sq.i18n.json | 2 + packages/i18n/src/locales/sv.i18n.json | 2 + packages/i18n/src/locales/ta-IN.i18n.json | 2 + packages/i18n/src/locales/th-TH.i18n.json | 2 + packages/i18n/src/locales/tr.i18n.json | 2 + packages/i18n/src/locales/ug.i18n.json | 2 + packages/i18n/src/locales/uk.i18n.json | 2 + packages/i18n/src/locales/vi-VN.i18n.json | 2 + packages/i18n/src/locales/zh-HK.i18n.json | 2 + packages/i18n/src/locales/zh-TW.i18n.json | 2 + packages/i18n/src/locales/zh.i18n.json | 2 + packages/instance-status/package.json | 2 +- packages/jest-presets/package.json | 10 +- packages/jwt/package.json | 2 +- packages/livechat/.storybook/helpers.tsx | 2 +- packages/livechat/package.json | 46 +- .../components/Form/CustomFields/index.tsx | 2 +- .../Messages/MessageText/markdown.ts | 1 + .../livechat/src/helpers/canRenderMessage.ts | 4 +- packages/livechat/src/helpers/formatAgent.ts | 2 +- .../src/hooks/useDeleteMessageSubscription.ts | 2 +- packages/livechat/src/lib/connection.ts | 6 +- packages/livechat/src/lib/triggers.js | 6 +- .../providers/ConnectionStatusProvider.tsx | 2 +- .../livechat/src/routes/Chat/connector.tsx | 2 +- packages/livechat/src/store/Store.ts | 49 +- packages/livechat/src/store/index.tsx | 2 +- packages/livechat/src/widget.ts | 8 +- packages/log-format/package.json | 4 +- packages/logger/package.json | 2 +- packages/logger/src/getPino.ts | 2 +- packages/message-parser/package.json | 18 +- packages/message-parser/src/guards.ts | 2 +- packages/message-parser/src/utils.ts | 41 +- packages/message-parser/tests/abuse.test.ts | 26 +- packages/message-parser/tests/katex.test.ts | 2 +- packages/message-parser/tests/link.test.ts | 46 +- packages/message-parser/tests/tasks.test.ts | 6 +- packages/message-parser/tests/url.test.ts | 26 +- packages/mock-providers/package.json | 4 +- .../src/MockedAppRootBuilder.tsx | 14 +- .../model-typings/src/models/IBaseModel.ts | 4 +- .../src/models/IExportOperationsModel.ts | 1 - .../models/ILivechatDepartmentAgentsModel.ts | 2 - .../src/models/ILivechatVisitorsModel.ts | 2 +- .../src/models/IMessagesModel.ts | 2 - .../model-typings/src/models/INpsVoteModel.ts | 2 - .../model-typings/src/models/IRolesModel.ts | 5 +- .../model-typings/src/models/IRoomsModel.ts | 9 - .../src/models/ISubscriptionsModel.ts | 1 - .../src/models/ITeamMemberModel.ts | 3 - .../model-typings/src/models/IUsersModel.ts | 4 - packages/models/package.json | 2 +- packages/password-policies/package.json | 2 +- packages/patch-injection/package.json | 2 +- packages/peggy-loader/package.json | 6 +- packages/peggy-loader/src/index.ts | 23 +- packages/random/package.json | 4 +- packages/release-action/package.json | 6 +- packages/release-changelog/package.json | 2 +- packages/rest-typings/package.json | 8 +- .../src/helpers/ReplacePlaceholders.ts | 8 +- packages/rest-typings/src/index.ts | 14 +- packages/server-fetch/package.json | 4 +- packages/server-fetch/src/index.ts | 15 +- packages/sha256/package.json | 4 +- packages/tools/package.json | 2 +- packages/tracing/package.json | 12 +- packages/ui-avatar/package.json | 4 +- packages/ui-client/package.json | 26 +- .../EmojiPicker/EmojiPickerContainer.tsx | 35 +- .../PasswordVerifiers.spec.tsx | 44 +- .../useDefaultSettingFeaturePreviewList.ts | 2 +- .../hooks/usePreferenceFeaturePreviewList.ts | 2 +- packages/ui-composer/package.json | 20 +- .../MessageFooterCalloutAction.tsx | 11 +- packages/ui-contexts/src/SettingsContext.ts | 12 +- .../ui-contexts/src/hooks/useAssetPath.ts | 4 +- .../src/hooks/useCurrentRoutePath.ts | 2 +- packages/ui-contexts/src/hooks/useEndpoint.ts | 10 +- packages/ui-contexts/src/hooks/useSetting.ts | 11 +- .../src/hooks/useSettingSetValue.ts | 4 +- .../src/hooks/useSettingStructure.ts | 4 +- .../src/hooks/useVerifyPassword.ts | 18 +- packages/ui-kit/package.json | 20 +- packages/ui-kit/src/rendering/ActionOf.ts | 64 +- packages/ui-video-conf/package.json | 24 +- packages/ui-voip/package.json | 26 +- .../VoipContactId/VoipContactId.spec.tsx | 79 +- .../__snapshots__/VoipContactId.spec.tsx.snap | 8 +- .../VoipDialPad/VoipDialPad.spec.tsx | 104 +- .../components/VoipPopup/VoipPopup.spec.tsx | 17 +- .../components/VoipPopupHeader.spec.tsx | 19 +- .../VoipPopup/views/VoipDialerView.spec.tsx | 21 +- .../VoipPopup/views/VoipErrorView.spec.tsx | 13 +- .../VoipPopup/views/VoipIncomingView.spec.tsx | 7 +- .../VoipPopup/views/VoipOngoingView.spec.tsx | 95 +- .../VoipPopup/views/VoipOutgoingView.spec.tsx | 5 +- .../components/VoipTimer/VoipTimer.spec.tsx | 48 +- .../VoipTransferModal.spec.tsx | 14 +- packages/ui-voip/src/hooks/useVoipClient.tsx | 2 +- packages/ui-voip/src/hooks/useVoipEffect.tsx | 22 +- .../ui-voip/src/providers/VoipProvider.tsx | 2 +- packages/web-ui-registration/package.json | 26 +- packages/web-ui-registration/src/CMSPage.tsx | 2 +- .../web-ui-registration/src/LoginForm.tsx | 8 +- .../src/LoginServicesButton.tsx | 5 +- .../web-ui-registration/src/RegisterForm.tsx | 12 +- .../src/RegisterFormDisabled.tsx | 2 +- .../src/RegisterSecretPageRouter.tsx | 2 +- .../src/RegisterTemplate.tsx | 2 +- .../src/ResetPassword/ResetPasswordPage.tsx | 6 +- .../src/components/LoginPoweredBy.tsx | 2 +- .../components/LoginSwitchLanguageFooter.tsx | 2 +- .../src/components/LoginTerms.tsx | 2 +- .../src/components/RegisterTitle.tsx | 4 +- .../src/hooks/useAssetPath.ts | 12 + .../src/template/HorizontalTemplate.tsx | 2 +- .../src/template/VerticalTemplate.tsx | 2 +- yarn.lock | 8940 ++++++++++------- 1084 files changed, 10601 insertions(+), 11794 deletions(-) delete mode 100644 .changeset/clean-flies-collect.md delete mode 100644 .changeset/curvy-flies-greet.md delete mode 100644 .changeset/fair-colts-remain.md delete mode 100644 .changeset/forty-gorillas-kneel.md delete mode 100644 .changeset/light-terms-ring.md delete mode 100644 .changeset/nervous-rivers-fry.md delete mode 100644 .changeset/old-coins-bow.md delete mode 100644 .changeset/real-jeans-worry.md delete mode 100644 .changeset/serious-mice-film.md delete mode 100644 .changeset/smart-radios-reflect.md delete mode 100644 .changeset/spicy-spiders-search.md delete mode 100644 .changeset/two-guests-tan.md delete mode 100644 .changeset/weak-trees-exercise.md create mode 100644 .github/actions/build-docker-image/action.yml rename apps/meteor/.docker/{Dockerfile.alpine => Dockerfile} (98%) rename apps/meteor/app/canned-responses/client/startup/{responses.ts => responses.js} (72%) rename apps/meteor/app/e2e/client/{events.ts => events.js} (83%) rename apps/meteor/app/e2e/client/{helper.ts => helper.js} (64%) rename apps/meteor/app/e2e/client/{rocketchat.e2e.room.ts => rocketchat.e2e.room.js} (88%) rename apps/meteor/app/emoji-custom/client/lib/{emojiCustom.ts => emojiCustom.js} (59%) create mode 100644 apps/meteor/app/emoji-custom/client/lib/function-isSet.js rename apps/meteor/app/emoji/client/{emojiParser.ts => emojiParser.js} (74%) rename apps/meteor/app/federation/server/handler/{index.ts => index.js} (80%) rename apps/meteor/app/irc/server/irc-bridge/localHandlers/{index.ts => index.js} (100%) rename apps/meteor/app/irc/server/irc-bridge/peerHandlers/{index.ts => index.js} (100%) rename apps/meteor/app/irc/server/servers/{index.ts => index.js} (100%) rename apps/meteor/app/lib/client/{OAuthProxy.ts => OAuthProxy.js} (86%) delete mode 100644 apps/meteor/app/lib/server/functions/notifications/messageContainsHighlight.ts create mode 100644 apps/meteor/app/lib/server/functions/saveUser.js delete mode 100644 apps/meteor/app/lib/server/functions/saveUser/handleBio.ts delete mode 100644 apps/meteor/app/lib/server/functions/saveUser/handleNickname.ts delete mode 100644 apps/meteor/app/lib/server/functions/saveUser/index.ts delete mode 100644 apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts delete mode 100644 apps/meteor/app/lib/server/functions/saveUser/saveUser.ts delete mode 100644 apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts delete mode 100644 apps/meteor/app/lib/server/functions/saveUser/validateUserData.ts delete mode 100644 apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts create mode 100644 apps/meteor/app/livechat/client/collections/LivechatInquiry.js delete mode 100644 apps/meteor/app/livechat/client/collections/LivechatInquiry.ts create mode 100644 apps/meteor/app/livechat/server/lib/Departments.ts delete mode 100644 apps/meteor/app/livechat/server/lib/departmentsLib.ts delete mode 100644 apps/meteor/app/livechat/server/lib/getOnlineAgents.ts delete mode 100644 apps/meteor/app/livechat/server/lib/getRoomMessages.ts delete mode 100644 apps/meteor/app/livechat/server/lib/messages.ts delete mode 100644 apps/meteor/app/livechat/server/lib/tracking.ts rename apps/meteor/app/slackbridge/client/{slackbridge_import.client.ts => slackbridge_import.client.js} (85%) rename apps/meteor/app/ui-master/server/{index.ts => index.js} (81%) rename apps/meteor/app/webrtc/client/{WebRTCClass.ts => WebRTCClass.js} (66%) create mode 100644 apps/meteor/app/webrtc/client/adapter.js delete mode 100644 apps/meteor/app/webrtc/client/adapter.ts rename apps/meteor/app/webrtc/client/{screenShare.ts => screenShare.js} (77%) create mode 100644 apps/meteor/client/lib/utils/messageArgs.ts create mode 100644 apps/meteor/client/methods/setUserActiveStatus.ts delete mode 100644 apps/meteor/client/sidebarv2/hooks/useRoomList.spec.tsx delete mode 100644 apps/meteor/client/sidebarv2/hooks/useUnreadDisplay.spec.tsx delete mode 100644 apps/meteor/client/sidebarv2/hooks/useUnreadDisplay.ts delete mode 100644 apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/CreateOAuthModal.spec.tsx rename apps/meteor/ee/server/apps/storage/{index.ts => index.js} (100%) delete mode 100644 apps/meteor/lib/getSubscriptionUnreadData.ts rename apps/meteor/server/routes/avatar/{index.ts => index.js} (59%) create mode 100644 apps/meteor/server/routes/avatar/middlewares/auth.js delete mode 100644 apps/meteor/server/routes/avatar/middlewares/auth.spec.ts delete mode 100644 apps/meteor/server/routes/avatar/middlewares/auth.ts rename apps/meteor/server/routes/avatar/middlewares/{browserVersion.ts => browserVersion.js} (81%) delete mode 100644 apps/meteor/server/routes/avatar/middlewares/browserVersion.spec.ts create mode 100644 apps/meteor/server/routes/avatar/middlewares/index.js delete mode 100644 apps/meteor/server/routes/avatar/middlewares/index.ts create mode 100644 apps/meteor/server/routes/avatar/room.js delete mode 100644 apps/meteor/server/routes/avatar/room.spec.ts delete mode 100644 apps/meteor/server/routes/avatar/room.ts create mode 100644 apps/meteor/server/routes/avatar/user.js delete mode 100644 apps/meteor/server/routes/avatar/user.spec.ts delete mode 100644 apps/meteor/server/routes/avatar/user.ts rename apps/meteor/server/routes/avatar/{utils.ts => utils.js} (56%) delete mode 100644 apps/meteor/server/routes/avatar/utils.spec.ts delete mode 100644 apps/meteor/tests/e2e/feature-preview.spec.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/fragments/navbar.ts delete mode 100644 apps/meteor/tests/unit/app/emoji/helpers.spec.ts delete mode 100644 apps/meteor/tests/unit/app/lib/server/functions/notifications/messageContainsHighlight.tests.ts create mode 100644 packages/web-ui-registration/src/hooks/useAssetPath.ts diff --git a/.changeset/clean-flies-collect.md b/.changeset/clean-flies-collect.md deleted file mode 100644 index 4921355b51e8a..0000000000000 --- a/.changeset/clean-flies-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fix user highlights not matching only whole words diff --git a/.changeset/curvy-flies-greet.md b/.changeset/curvy-flies-greet.md deleted file mode 100644 index aeac8382b152b..0000000000000 --- a/.changeset/curvy-flies-greet.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Remove unused client side `setUserActiveStatus` meteor method. diff --git a/.changeset/fair-colts-remain.md b/.changeset/fair-colts-remain.md deleted file mode 100644 index 7ce003e50fd53..0000000000000 --- a/.changeset/fair-colts-remain.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/account-service": patch -"@rocket.chat/authorization-service": patch -"@rocket.chat/ddp-streamer": patch -"@rocket.chat/omnichannel-transcript": patch -"@rocket.chat/presence-service": patch -"@rocket.chat/queue-worker": patch -"@rocket.chat/stream-hub-service": patch -"rocketchat-services": patch ---- - -Bump meteor to 3.0.4 and Node version to 20.18.0 diff --git a/.changeset/forty-gorillas-kneel.md b/.changeset/forty-gorillas-kneel.md deleted file mode 100644 index 42df0ed8c0e4e..0000000000000 --- a/.changeset/forty-gorillas-kneel.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/apps-engine": patch ---- - -Deprecated the `from` field in the apps email bridge and made it optional, using the server's settings when the field is omitted diff --git a/.changeset/light-terms-ring.md b/.changeset/light-terms-ring.md deleted file mode 100644 index 4437c5c4d596f..0000000000000 --- a/.changeset/light-terms-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes the issue where newly created teams are incorrectly displayed as channels on the sidebar when the DISABLE_DB_WATCHERS environment variable is enabled diff --git a/.changeset/nervous-rivers-fry.md b/.changeset/nervous-rivers-fry.md deleted file mode 100644 index 278259c53a86f..0000000000000 --- a/.changeset/nervous-rivers-fry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/apps-engine': patch -'@rocket.chat/meteor': patch ---- - -Fixed an issue that would grant network permission to app's processes in wrong cases diff --git a/.changeset/old-coins-bow.md b/.changeset/old-coins-bow.md deleted file mode 100644 index 1790cc205160a..0000000000000 --- a/.changeset/old-coins-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/apps-engine': patch ---- - -Fixes an issue that would cause apps to appear disabled after a subprocess restart diff --git a/.changeset/real-jeans-worry.md b/.changeset/real-jeans-worry.md deleted file mode 100644 index 9b16e7681a983..0000000000000 --- a/.changeset/real-jeans-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixes banner breaking the UI with specific payloads diff --git a/.changeset/serious-mice-film.md b/.changeset/serious-mice-film.md deleted file mode 100644 index 35a2d67040717..0000000000000 --- a/.changeset/serious-mice-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixes client-side updates for recent emoji list when custom emojis are modified. diff --git a/.changeset/smart-radios-reflect.md b/.changeset/smart-radios-reflect.md deleted file mode 100644 index 58ea7413e51c0..0000000000000 --- a/.changeset/smart-radios-reflect.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@rocket.chat/meteor": patch -"@rocket.chat/core-services": patch ---- - -stops calling an object through proxy calling getQueueWorker diff --git a/.changeset/spicy-spiders-search.md b/.changeset/spicy-spiders-search.md deleted file mode 100644 index d86bc93313c5c..0000000000000 --- a/.changeset/spicy-spiders-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/meteor': patch ---- - -Fixed an issue where the installed apps list would go stale without a refresh in some cases diff --git a/.changeset/two-guests-tan.md b/.changeset/two-guests-tan.md deleted file mode 100644 index ff44f0ef493ba..0000000000000 --- a/.changeset/two-guests-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/apps-engine': patch ---- - -Removed the 1 second timeout of `Pre` app events. Now they will follow the "global" configuration diff --git a/.changeset/weak-trees-exercise.md b/.changeset/weak-trees-exercise.md deleted file mode 100644 index 230c087ccd83a..0000000000000 --- a/.changeset/weak-trees-exercise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@rocket.chat/apps-engine': minor ---- - -Add support to configure apps runtime timeout via the APPS_ENGINE_RUNTIME_TIMEOUT environment variable diff --git a/.github/actions/build-docker-image/action.yml b/.github/actions/build-docker-image/action.yml new file mode 100644 index 0000000000000..fa0535d332c6c --- /dev/null +++ b/.github/actions/build-docker-image/action.yml @@ -0,0 +1,91 @@ +name: 'Build Docker image' +description: 'Build Rocket.Chat Docker image' + +inputs: + root-dir: + required: true + docker-tag: + required: true + release: + required: true + username: + required: false + password: + required: false + deno-version: + required: true + type: string + +outputs: + image-name: + value: ${{ steps.build-image.outputs.image-name }} + +runs: + using: composite + steps: + # - shell: bash + # name: Free disk space + # run: | + # sudo swapoff -a + # sudo rm -f /swapfile + # sudo apt clean + # docker rmi $(docker image ls -aq) + # df -h + + - shell: bash + id: build-image + run: | + cd ${{ inputs.root-dir }} + + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + + IMAGE_NAME_BASE="ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${{ inputs.docker-tag }}" + + IMAGE_NAME="${IMAGE_NAME_BASE}.${{ inputs.release }}" + + echo "Build Docker image ${IMAGE_NAME}" + + DOCKER_PATH="${GITHUB_WORKSPACE}/apps/meteor/.docker" + if [[ '${{ inputs.release }}' = 'preview' ]]; then + DOCKER_PATH="${DOCKER_PATH}-mongo" + fi; + + DOCKERFILE_PATH="${DOCKER_PATH}/Dockerfile" + if [[ '${{ inputs.release }}' = 'debian' ]]; then + DOCKERFILE_PATH="${DOCKERFILE_PATH}.${{ inputs.release }}" + fi; + + echo "Copy Dockerfile for release: ${{ inputs.release }}" + cp $DOCKERFILE_PATH ./Dockerfile + if [ -e ${DOCKER_PATH}/entrypoint.sh ]; then + cp ${DOCKER_PATH}/entrypoint.sh . + fi; + + echo "Build ${{ inputs.release }} Docker image" + docker build --build-arg DENO_VERSION=${{ inputs.deno-version }} -t $IMAGE_NAME . + + echo "image-name-base=${IMAGE_NAME_BASE}" >> $GITHUB_OUTPUT + echo "image-name=${IMAGE_NAME}" >> $GITHUB_OUTPUT + + - name: Login to GitHub Container Registry + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ inputs.username }} + password: ${{ inputs.password }} + + - name: Publish image + shell: bash + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' + run: | + echo "Push Docker image: ${{ steps.build-image.outputs.image-name }}" + + docker push ${{ steps.build-image.outputs.image-name }} + + if [[ '${{ inputs.release }}' = 'official' ]]; then + echo "Push release official without variant" + + docker tag ${{ steps.build-image.outputs.image-name }} ${{ steps.build-image.outputs.image-name-base }} + docker push ${{ steps.build-image.outputs.image-name-base }} + fi; diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index cb736c757cd26..2891d892c3599 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -16,7 +16,7 @@ inputs: platform: required: false description: 'Platform' - default: 'alpine' + type: string build-containers: required: false description: 'Containers to build along with Rocket.Chat' @@ -84,7 +84,7 @@ runs: - run: yarn build if: inputs.setup == 'true' shell: bash - - if: ${{ inputs.platform == 'alpine' }} + - if: ${{ inputs.platform == 'official' }} uses: actions/cache@v3 with: path: /tmp/build/matrix-sdk-crypto.linux-x64-musl.node diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index ce03f20383d26..6da5693303b48 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -17,7 +17,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.18.0 + node-version: 20.17.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index a6d6edf11a422..8f5d258ef1650 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -18,6 +18,12 @@ on: rc-docker-tag: required: true type: string + rc-dockerfile-debian: + required: true + type: string + rc-docker-tag-debian: + required: true + type: string gh-docker-tag: required: true type: string @@ -77,8 +83,8 @@ jobs: test: runs-on: ubuntu-20.04 env: - RC_DOCKERFILE: ${{ inputs.rc-dockerfile }}.${{ (matrix.mongodb-version == '7.0' && 'debian' && false) || 'alpine' }} - RC_DOCKER_TAG: ${{ inputs.rc-docker-tag }}.${{ (matrix.mongodb-version == '7.0' && 'debian' && false) || 'alpine' }} + RC_DOCKERFILE: ${{ matrix.mongodb-version == '7.0' && inputs.rc-dockerfile-debian || inputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ matrix.mongodb-version == '7.0' && inputs.rc-docker-tag-debian || inputs.rc-docker-tag }} strategy: fail-fast: false @@ -86,7 +92,7 @@ jobs: mongodb-version: ${{ fromJSON(inputs.mongodb-version) }} shard: ${{ fromJSON(inputs.shard) }} - name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'true' && ' [no watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }}) - ${{ (matrix.mongodb-version == '7.0' && 'Debian' && false) || 'Alpine (Official)' }} + name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'true' && ' [no watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }}) - ${{ matrix.mongodb-version == '7.0' && 'Debian' || 'Alpine (Official)' }} steps: - name: Collect Workflow Telemetry @@ -204,7 +210,7 @@ jobs: path: | ~/.cache/ms-playwright # This is the version of Playwright that we are using, if you are willing to upgrade, you should update this. - key: playwright-1.48.2 + key: playwright-1.40.1 - name: Install Playwright if: inputs.type == 'ui' && steps.cache-playwright.outputs.cache-hit != 'true' @@ -299,11 +305,7 @@ jobs: include-hidden-files: true - name: Show server logs if E2E test failed - if: failure() && inputs.release == 'ee' - run: docker compose -f docker-compose-ci.yml logs - - - name: Show server logs if E2E test failed - if: failure() && inputs.release != 'ee' + if: failure() run: docker compose -f docker-compose-ci.yml logs rocketchat - name: Extract e2e:ee:coverage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dabd5ac277383..07870e10e520d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,10 +31,11 @@ jobs: gh-docker-tag: ${{ steps.docker.outputs.gh-docker-tag }} lowercase-repo: ${{ steps.var.outputs.lowercase-repo }} rc-dockerfile: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile' - rc-docker-tag: '${{ steps.docker.outputs.gh-docker-tag }}' + rc-docker-tag: '${{ steps.docker.outputs.gh-docker-tag }}.official' + rc-dockerfile-debian: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile.debian' + rc-docker-tag-debian: '${{ steps.docker.outputs.gh-docker-tag }}.debian' node-version: ${{ steps.var.outputs.node-version }} deno-version: ${{ steps.var.outputs.deno-version }} - official-platform: 'alpine' # this is 100% intentional, secrets are not available for forks, so ee-tests will always fail # to avoid this, we are using a dummy license, expiring at 2025-06-31 enterprise-license: X/XumwIkgwQuld0alWKt37lVA90XjKOrfiMvMZ0/RtqsMtrdL9GoAk+4jXnaY1b2ePoG7XSzGhuxEDxFKIWJK3hIKGNTvrd980LgH5sM5+1T4P42ivSpd8UZi0bwjJkCFLIu9RozzYwslGG0IehMxe0S6VjcO0UYlUJtbMCBHuR2WmTAmO6YVU3ln+pZCbrPFaTPSS1RovhKaNCNkZwIx/CLWW8UTXUuFV/ML4PbKKVoa5nvvJwPeatgL7UCnlSD90lfCiiuikpzj/Y/JLkIL6velFbwNxsrxg9iRJ2k0sKheMMSmlTiGzSvZUm+na5WQq91aKGncih+DmaEZA7QGrjp4eoA0dqTk6OmItsy0fHmQhvZIOKNMeO7vNQiLbaSV6rqibrzu7WPpeIvsvL57T1h37USoCSB6+jDqkzdfoqIpz8BxTiJDj1d8xGPJFVrgxoqQqkj9qIP/gCaEz5DF39QFv5sovk4yK2O8fEQYod2d14V9yECYl4szZPMk1IBfCAC2w7czWGHHFonhL+CQGT403y5wmDmnsnjlCqMKF72odqfTPTI8XnCvJDriPMWohnQEAGtTTyciAhNokx/mjAVJ4NeZPcsbm4BjhvJvnjxx/BhYhBBTNWPaCSZzocfrGUj9Z+ZA7BEz+xAFQyGDx3xRzqIXfT0G7w8fvgYJMU= @@ -199,7 +200,7 @@ jobs: uses: ./.github/actions/setup-node if: github.event.action != 'closed' with: - node-version: 20.18.0 + node-version: 20.17.0 deno-version: 1.37.1 cache-modules: true install: true @@ -306,7 +307,7 @@ jobs: - if: steps.matrix-rust-sdk-crypto-nodejs.outputs.cache-hit != 'true' uses: actions/setup-node@v4 with: - node-version: '20.18.0' + node-version: '20' - if: steps.matrix-rust-sdk-crypto-nodejs.outputs.cache-hit != 'true' uses: dtolnay/rust-toolchain@stable @@ -334,15 +335,15 @@ jobs: runs-on: ubuntu-20.04 env: - RC_DOCKERFILE: ${{ needs.release-versions.outputs.rc-dockerfile }}.${{ matrix.platform }} - RC_DOCKER_TAG: ${{ needs.release-versions.outputs.rc-docker-tag }}.${{ matrix.platform }} + RC_DOCKERFILE: ${{ matrix.platform == 'debian' && needs.release-versions.outputs.rc-dockerfile-debian || needs.release-versions.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ matrix.platform == 'debian' && needs.release-versions.outputs.rc-docker-tag-debian || needs.release-versions.outputs.rc-docker-tag }} DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} strategy: fail-fast: false matrix: - platform: ['alpine'] + platform: ['official', 'debian'] steps: - uses: actions/checkout@v4 @@ -356,11 +357,11 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} deno-version: ${{ needs.release-versions.outputs.deno-version }} platform: ${{ matrix.platform }} - build-containers: ${{ matrix.platform == needs.release-versions.outputs.official-platform && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} + build-containers: ${{ matrix.platform == 'debian' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Make sure matrix bindings load - if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && matrix.platform == 'alpine' + if: ${{ matrix.platform == 'official' }} run: | docker run --rm -w /app/bundle/programs/server/npm/node_modules/matrix-appservice-bridge ghcr.io/rocketchat/rocket.chat:$RC_DOCKER_TAG -e 'require(".")' @@ -370,15 +371,15 @@ jobs: runs-on: ubuntu-20.04 env: - RC_DOCKERFILE: ${{ needs.release-versions.outputs.rc-dockerfile }}.${{ matrix.platform }} - RC_DOCKER_TAG: ${{ needs.release-versions.outputs.rc-docker-tag }}.${{ matrix.platform }} + RC_DOCKERFILE: ${{ matrix.platform == 'debian' && needs.release-versions.outputs.rc-dockerfile-debian || needs.release-versions.outputs.rc-dockerfile }} + RC_DOCKER_TAG: ${{ matrix.platform == 'debian' && needs.release-versions.outputs.rc-docker-tag-debian || needs.release-versions.outputs.rc-docker-tag }} DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} strategy: fail-fast: false matrix: - platform: ['alpine'] + platform: ['official', 'debian'] steps: - uses: actions/checkout@v4 @@ -390,22 +391,18 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} deno-version: ${{ needs.release-versions.outputs.deno-version }} platform: ${{ matrix.platform }} - build-containers: ${{ matrix.platform == needs.release-versions.outputs.official-platform && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} + build-containers: ${{ matrix.platform == 'debian' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Rename official Docker tag to GitHub Container Registry - if: matrix.platform == needs.release-versions.outputs.official-platform + if: matrix.platform == 'official' run: | IMAGE_NAME_BASE="ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${DOCKER_TAG}" echo "Push Docker image: ${IMAGE_NAME_BASE}" - docker tag ${IMAGE_NAME_BASE}.${{matrix.platform}} $IMAGE_NAME_BASE + docker tag ${IMAGE_NAME_BASE}.official $IMAGE_NAME_BASE docker push $IMAGE_NAME_BASE - echo "Push Docker image: ${IMAGE_NAME_BASE}.official" - docker tag ${IMAGE_NAME_BASE}.${{matrix.platform}} ${IMAGE_NAME_BASE}.official - docker push ${IMAGE_NAME_BASE}.official - checks: needs: [release-versions, packages-build] @@ -440,6 +437,8 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} + rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} + rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} secrets: CR_USER: ${{ secrets.CR_USER }} @@ -462,6 +461,8 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} + rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} + rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} secrets: @@ -488,6 +489,8 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} + rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} + rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} secrets: CR_USER: ${{ secrets.CR_USER }} @@ -511,6 +514,8 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} + rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} + rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} secrets: @@ -540,6 +545,8 @@ jobs: lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} + rc-dockerfile-debian: ${{ needs.release-versions.outputs.rc-dockerfile-debian }} + rc-docker-tag-debian: ${{ needs.release-versions.outputs.rc-docker-tag-debian }} gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} db-watcher-disabled: 'true' @@ -644,15 +651,48 @@ jobs: aws s3 cp $ROCKET_DEPLOY_DIR/ s3://download.rocket.chat/build/ --recursive + build-docker-preview: + name: 🚢 Build Docker Image (preview) + runs-on: ubuntu-20.04 + needs: [build, checks, release-versions] + if: github.event_name == 'release' || github.ref == 'refs/heads/develop' + steps: + - uses: actions/checkout@v4 + + - name: Restore build + uses: actions/download-artifact@v4 + with: + name: build + path: /tmp/build + + - name: Unpack build + run: | + cd /tmp/build + tar xzf Rocket.Chat.tar.gz + rm Rocket.Chat.tar.gz + + - name: Build Docker image + id: build-docker-image-preview + uses: ./.github/actions/build-docker-image + with: + root-dir: /tmp/build + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + release: preview + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + docker-image-publish: name: 🚀 Publish Docker Image (main) runs-on: ubuntu-20.04 - needs: [deploy, release-versions] + needs: [deploy, build-docker-preview, release-versions] strategy: matrix: # this is currently a mix of variants and different images - release: ['alpine'] + release: ['official', 'preview', 'debian'] + env: IMAGE_NAME: 'rocketchat/rocket.chat' diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index b3ead30e89668..cd6fc3dec3f2c 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -34,7 +34,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.18.0 + node-version: 20.17.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index 7a0d8650a97fc..66a8e4436a341 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -21,7 +21,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.18.0 + node-version: 20.17.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 2f803576ef51d..33f2de48feeba 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -24,7 +24,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.18.0 + node-version: 20.17.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index d2d22a3fc6c55..ad64416da04c4 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -15,7 +15,7 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/setup-node with: - node-version: 20.18.0 + node-version: 20.17.0 deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index 410734f94e2b9..bdf6b75e0f5bf 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -14,22 +14,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4.0.4 - with: - node-version: '20.18.0' + - name: Use Node.js + uses: actions/setup-node@v4.0.4 + with: + node-version: '20.17.0' - - name: Install dependencies - run: | - cd ./.github/actions/update-version-durability - npm install + - name: Install dependencies + run: | + cd ./.github/actions/update-version-durability + npm install - - name: Update Version Durability - uses: ./.github/actions/update-version-durability - with: - GH_TOKEN: ${{ secrets.CI_PAT }} - D360_TOKEN: ${{ secrets.D360_TOKEN }} - D360_ARTICLE_ID: 800f8d52-409d-478d-b560-f82a2c0eb7fb - PUBLISH: true + - name: Update Version Durability + uses: ./.github/actions/update-version-durability + with: + GH_TOKEN: ${{ secrets.CI_PAT }} + D360_TOKEN: ${{ secrets.D360_TOKEN }} + D360_ARTICLE_ID: 800f8d52-409d-478d-b560-f82a2c0eb7fb + PUBLISH: true diff --git a/README.md b/README.md index d4053587a5c79..564ca75d2b11f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Free for 30 days. Afterward, choose between continuing to host on our secure clo You can follow these instructions to setup a dev environment: -- Install **Node 20.x (LTS)** either [manually](https://nodejs.org/dist/latest-v20.x/) or using a tool like [nvm](https://github.com/creationix/nvm) or [volta](https://volta.sh/) (recommended) +- Install **Node 14.x (LTS)** either [manually](https://nodejs.org/dist/latest-v14.x/) or using a tool like [nvm](https://github.com/creationix/nvm) or [volta](https://volta.sh/) (recommended) - Install **Meteor** ([version here](apps/meteor/.meteor/release)): https://docs.meteor.com/about/install.html - Install **yarn**: https://yarnpkg.com/getting-started/install - Install **Deno 1.x**: https://docs.deno.com/runtime/fundamentals/installation/ @@ -88,7 +88,7 @@ yarn turbo run ms After initialized, you can access the server at http://localhost:4000 -> ⚠️ Check more detailed information in the [Rocket.Chat Environment Setup](https://developer.rocket.chat/docs/server-environment-setup) guide +> ⚠️ Check more detailed information in the [Rocket.Chat Environment Setup](https://developer.rocket.chat/rocket.chat/rocket-chat-environment-setup) guide # 💻 Installation diff --git a/apps/meteor/.docker-mongo/Dockerfile b/apps/meteor/.docker-mongo/Dockerfile index 560fde4a69dc6..17784e1769a33 100644 --- a/apps/meteor/.docker-mongo/Dockerfile +++ b/apps/meteor/.docker-mongo/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.18.0-bullseye-slim +FROM node:20.17.0-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile similarity index 98% rename from apps/meteor/.docker/Dockerfile.alpine rename to apps/meteor/.docker/Dockerfile index 0f1e170f9570b..7bff2a067c0a7 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.18.0-alpine3.20 +FROM node:20.17.0-alpine3.20 LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile.debian b/apps/meteor/.docker/Dockerfile.debian index 22134532ece00..7bbca15a91426 100644 --- a/apps/meteor/.docker/Dockerfile.debian +++ b/apps/meteor/.docker/Dockerfile.debian @@ -2,7 +2,7 @@ ARG DENO_VERSION="1.37.1" FROM denoland/deno:bin-${DENO_VERSION} as deno -FROM node:20.18.0-bullseye-slim +FROM node:20.17.0-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.eslintignore b/apps/meteor/.eslintignore index 9f66dc806f8b4..3ff422a3ed3f9 100644 --- a/apps/meteor/.eslintignore +++ b/apps/meteor/.eslintignore @@ -12,4 +12,3 @@ !/.storybook/ !/client/.eslintrc.js !/ee/client/.eslintrc.js -/storybook-static/ diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index ead1cc41fd09a..70518d252df9d 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -14,7 +14,7 @@ rocketchat:streamer rocketchat:version rocketchat:user-presence -accounts-base@3.0.3 +accounts-base@3.0.2 accounts-facebook@1.3.4 accounts-github@1.5.1 accounts-google@1.4.1 @@ -27,14 +27,14 @@ google-oauth@1.4.5 oauth@3.0.0 oauth2@1.3.3 -check@1.4.4 +check@1.4.2 ddp-rate-limiter@1.2.2 rate-limit@1.1.2 email@3.1.0 meteor-base@1.5.2 ddp-common@1.4.4 -webapp@2.0.3 +webapp@2.0.1 mongo@2.0.2 @@ -59,7 +59,7 @@ tracker@1.3.4 reactive-dict@1.3.2 reactive-var@1.0.13 -babel-compiler@7.11.1 +babel-compiler@7.11.0 standard-minifier-css@1.9.3 dynamic-import@0.7.4 ecmascript@0.16.9 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index b1e86a359f7c3..c41cba9c61a21 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@3.0.4 +METEOR@3.0.3 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 0f8f733b31480..c41710f5aa16e 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,4 +1,4 @@ -accounts-base@3.0.3 +accounts-base@3.0.2 accounts-facebook@1.3.4 accounts-github@1.5.1 accounts-google@1.4.1 @@ -8,24 +8,24 @@ accounts-password@3.0.2 accounts-twitter@1.5.2 allow-deny@2.0.0 autoupdate@2.0.0 -babel-compiler@7.11.1 +babel-compiler@7.11.0 babel-runtime@1.5.2 base64@1.0.13 binary-heap@1.0.12 boilerplate-generator@2.0.0 callback-hook@1.6.0 -check@1.4.4 +check@1.4.2 core-runtime@1.0.0 ddp@1.4.2 -ddp-client@3.0.2 +ddp-client@3.0.1 ddp-common@1.4.4 ddp-rate-limiter@1.2.2 -ddp-server@3.0.2 +ddp-server@3.0.1 diff-sequence@1.1.3 dispatch:run-as-user@1.1.1 dynamic-import@0.7.4 ecmascript@0.16.9 -ecmascript-runtime@0.8.3 +ecmascript-runtime@0.8.2 ecmascript-runtime-client@0.12.2 ecmascript-runtime-server@0.11.1 ejson@1.1.4 @@ -51,7 +51,7 @@ meteorhacks:inject-initial@1.0.5 minifier-css@2.0.0 minimongo@2.0.1 modern-browsers@0.1.11 -modules@0.20.2 +modules@0.20.1 modules-runtime@0.13.2 mongo@2.0.2 mongo-decimal@0.1.4-beta300.7 @@ -89,8 +89,8 @@ tracker@1.3.4 twitter-oauth@1.3.4 typescript@5.4.3 underscore@1.6.4 -url@1.3.4 -webapp@2.0.3 +url@1.3.3 +webapp@2.0.1 webapp-hashing@1.1.2 zodern:caching-minifier@0.5.0 zodern:standard-minifier-js@5.2.0 diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index aa092068a9b0c..fb5324f4c0fd7 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -32,7 +32,6 @@ module.exports = { 'tests/unit/app/**/*.tests.js', 'tests/unit/app/**/*.tests.ts', 'tests/unit/lib/**/*.tests.ts', - 'server/routes/avatar/**/*.spec.ts', 'tests/unit/lib/**/*.spec.ts', 'tests/unit/server/**/*.tests.ts', 'tests/unit/server/**/*.spec.ts', diff --git a/apps/meteor/.stylelintignore b/apps/meteor/.stylelintignore index 33637d3dd3e71..4bd9c6d458d65 100644 --- a/apps/meteor/.stylelintignore +++ b/apps/meteor/.stylelintignore @@ -1,4 +1,3 @@ app/theme/client/vendor/fontello/css/fontello.css app/meteor-autocomplete/client/autocomplete.css app/emoji-emojione/client/*.css -storybook-static diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 57be4f294d9be..7bb7f5af28ba7 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -189,13 +189,10 @@ export class APIClass extends Restivus { } public setLimitedCustomFields(customFields: string[]): void { - const nonPublicFieds = customFields.reduce( - (acc, customField) => { - acc[`customFields.${customField}`] = 0; - return acc; - }, - {} as Record, - ); + const nonPublicFieds = customFields.reduce((acc, customField) => { + acc[`customFields.${customField}`] = 0; + return acc; + }, {} as Record); this.limitedUserFieldsToExclude = { ...this.defaultLimitedUserFieldsToExclude, ...nonPublicFieds, diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index c15e0bc10d98f..acace73a82262 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -22,7 +22,7 @@ export type FailureResult } ? T : TOptions extends { validateParams: { GET: ValidateFunction } } - ? T - : Partial> & { offset?: number; count?: number } + ? T + : Partial> & { offset?: number; count?: number } : Record; // TODO make it unsafe readonly bodyParams: TMethod extends 'GET' ? Record : TOptions extends { validateParams: ValidateFunction } + ? T + : TOptions extends { validateParams: infer V } + ? V extends { [key in TMethod]: ValidateFunction } ? T - : TOptions extends { validateParams: infer V } - ? V extends { [key in TMethod]: ValidateFunction } - ? T - : Partial> - : // TODO remove the extra (optionals) params when all the endpoints that use these are typed correctly - Partial>; + : Partial> + : // TODO remove the extra (optionals) params when all the endpoints that use these are typed correctly + Partial>; readonly request: Request; readonly queryOperations: TOptions extends { queryOperations: infer T } ? T : never; @@ -163,18 +163,18 @@ export type ActionThis = | SuccessResult> diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index 20f6e38f35679..fc9bd273996d3 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -165,7 +165,9 @@ API.v1.addRoute( throw new Meteor.Error('error-role-protected', 'Cannot delete a protected role'); } - if ((await Roles.countUsersInRole(role._id)) > 0) { + const existingUsers = await Roles.findUsersInRole(role._id); + + if (existingUsers && (await existingUsers.count()) > 0) { throw new Meteor.Error('error-role-in-use', "Cannot delete role because it's in use"); } @@ -215,7 +217,7 @@ API.v1.addRoute( } if (role._id === 'admin') { - const adminCount = await Roles.countUsersInRole('admin'); + const adminCount = await (await Roles.findUsersInRole('admin')).count(); if (adminCount === 1) { throw new Meteor.Error('error-admin-required', 'You need to have at least one admin'); } diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index be0aaea4b4e1d..b92d9ba572fd8 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -35,7 +35,7 @@ API.v1.addRoute( ? { update: result, remove: [], - } + } : result, ); }, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 1dcbd3ab3c787..b7187ec8cd81d 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -155,7 +155,7 @@ API.v1.addRoute( : { twoFactorCode: userData.typedPassword, twoFactorMethod: 'password', - }; + }; await Meteor.callAsync('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions); @@ -512,7 +512,7 @@ API.v1.addRoute( { $limit: count, }, - ] + ] : []; const result = await Users.col diff --git a/apps/meteor/app/apps/server/bridges/email.ts b/apps/meteor/app/apps/server/bridges/email.ts index 6d75a45044832..4c9cb9a93ed63 100644 --- a/apps/meteor/app/apps/server/bridges/email.ts +++ b/apps/meteor/app/apps/server/bridges/email.ts @@ -3,7 +3,6 @@ import type { IEmail } from '@rocket.chat/apps-engine/definition/email'; import { EmailBridge } from '@rocket.chat/apps-engine/server/bridges'; import * as Mailer from '../../../mailer/server/api'; -import { settings } from '../../../settings/server'; export class AppEmailBridge extends EmailBridge { constructor(private readonly orch: IAppServerOrchestrator) { @@ -11,13 +10,7 @@ export class AppEmailBridge extends EmailBridge { } protected async sendEmail(email: IEmail, appId: string): Promise { - let { from } = email; - if (!from) { - this.orch.debugLog(`The app ${appId} didn't provide a from address, using the default one.`); - from = String(settings.get('From_Email')); - } - this.orch.debugLog(`The app ${appId} is sending an email.`); - await Mailer.send({ ...email, from }); + await Mailer.send(email); } } diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index f2521d2f8cf5d..821d1fdd60d53 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -10,9 +10,7 @@ import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@roc import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; -import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; -import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; +import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/apps/dist/converters/IAppMessagesConverter' { @@ -354,7 +352,7 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Could not get the message converter to process livechat room messages'); } - const livechatMessages = await getRoomMessages({ rid: roomId }); + const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId }); return Promise.all(await livechatMessages.map((message) => messageConverter.convertMessage(message, livechatMessages)).toArray()); } diff --git a/apps/meteor/app/apps/server/converters/transformMappedData.ts b/apps/meteor/app/apps/server/converters/transformMappedData.ts index 3fcd4db482a7f..df2f16138d731 100644 --- a/apps/meteor/app/apps/server/converters/transformMappedData.ts +++ b/apps/meteor/app/apps/server/converters/transformMappedData.ts @@ -67,8 +67,8 @@ export const transformMappedData = async < -readonly [p in keyof MapType]: MapType[p] extends keyof DataType ? DataType[MapType[p]] : MapType[p] extends (...args: any[]) => any - ? Awaited> - : never; + ? Awaited> + : never; }, DataType extends Record, MapType extends { [p in string]: string | ((data: DataType) => Promise) | ((data: DataType) => unknown) }, diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts index 8bb2e8277a9f4..ef307f00b30cf 100644 --- a/apps/meteor/app/assets/server/assets.ts +++ b/apps/meteor/app/assets/server/assets.ts @@ -374,12 +374,14 @@ export async function addAssetToSetting(asset: string, value: IRocketChatAsset, defaultUrl: value.defaultUrl, }, { - type: 'asset', - group: 'Assets', - fileConstraints: value.constraints, - i18nLabel: value.label, - asset, - public: true, + ...{ + type: 'asset', + group: 'Assets', + fileConstraints: value.constraints, + i18nLabel: value.label, + asset, + public: true, + }, ...options, }, ); diff --git a/apps/meteor/app/authorization/server/functions/getRoles.ts b/apps/meteor/app/authorization/server/functions/getRoles.ts index bee995f885d37..59ab1ef537328 100644 --- a/apps/meteor/app/authorization/server/functions/getRoles.ts +++ b/apps/meteor/app/authorization/server/functions/getRoles.ts @@ -2,6 +2,3 @@ import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; export const getRoles = async (): Promise => Roles.find().toArray(); - -export const getRoleIds = async (): Promise => - (await Roles.find({}, { projection: { _id: 1 } }).toArray()).map(({ _id }) => _id); diff --git a/apps/meteor/app/authorization/server/methods/deleteRole.ts b/apps/meteor/app/authorization/server/methods/deleteRole.ts index 512468f2d6d7f..140852e0f1ecf 100644 --- a/apps/meteor/app/authorization/server/methods/deleteRole.ts +++ b/apps/meteor/app/authorization/server/methods/deleteRole.ts @@ -56,7 +56,7 @@ Meteor.methods({ }); } - const users = await Roles.countUsersInRole(role._id); + const users = await (await Roles.findUsersInRole(role._id)).count(); if (users > 0) { throw new Meteor.Error('error-role-in-use', "Cannot delete role because it's in use", { diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index a720917ce175e..24cea2d8d28af 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; +import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { hasTranslationLanguageInAttachments, hasTranslationLanguageInMessage, @@ -24,7 +25,8 @@ Meteor.startup(() => { label: 'Translate', context: ['message', 'message-mobile', 'threads'], type: 'interaction', - action(_, { message }) { + action(_, props) { + const { message = messageArgs(this).msg } = props; const language = AutoTranslate.getLanguage(message.rid); if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { (AutoTranslate.messageIdsToWait as any)[message._id] = true; @@ -59,7 +61,7 @@ Meteor.startup(() => { context: ['message', 'message-mobile', 'threads'], type: 'interaction', action(_, props) { - const { message } = props; + const { message = messageArgs(this).msg } = props; const language = AutoTranslate.getLanguage(message.rid); if (!hasTranslationLanguageInMessage(message, language) && !hasTranslationLanguageInAttachments(message.attachments, language)) { (AutoTranslate.messageIdsToWait as any)[message._id] = true; diff --git a/apps/meteor/app/canned-responses/client/startup/responses.ts b/apps/meteor/app/canned-responses/client/startup/responses.js similarity index 72% rename from apps/meteor/app/canned-responses/client/startup/responses.ts rename to apps/meteor/app/canned-responses/client/startup/responses.js index 6d761adb890da..5959452832619 100644 --- a/apps/meteor/app/canned-responses/client/startup/responses.ts +++ b/apps/meteor/app/canned-responses/client/startup/responses.js @@ -6,6 +6,13 @@ import { settings } from '../../../settings/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; import { CannedResponse } from '../collections/CannedResponse'; +const events = { + changed: ({ type, ...response }) => { + CannedResponse.upsert({ _id: response._id }, response); + }, + removed: (response) => CannedResponse.remove({ _id: response._id }), +}; + Meteor.startup(() => { Tracker.autorun(async (c) => { if (!Meteor.userId()) { @@ -20,24 +27,12 @@ Meteor.startup(() => { Tracker.afterFlush(() => { try { // TODO: check options - sdk.stream('canned-responses', ['canned-responses'], (...[response, options]) => { + sdk.stream('canned-responses', ['canned-responses'], (response, options) => { const { agentsId } = options || {}; if (Array.isArray(agentsId) && !agentsId.includes(Meteor.userId())) { return; } - - switch (response.type) { - case 'changed': { - const { type, ...fields } = response; - CannedResponse.upsert({ _id: response._id }, fields); - break; - } - - case 'removed': { - CannedResponse.remove({ _id: response._id }); - break; - } - } + events[response.type](response); }); } catch (error) { console.log(error); diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts index 68e38baf5cc2d..f4334bd04d647 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -34,7 +34,7 @@ export const wrapPromise = ( } > => promise - .then((result) => ({ success: true, result }) as const) + .then((result) => ({ success: true, result } as const)) .catch((error) => ({ success: false, error, diff --git a/apps/meteor/app/custom-oauth/client/CustomOAuth.ts b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts index 7849498fa4b9a..1d57d1969d939 100644 --- a/apps/meteor/app/custom-oauth/client/CustomOAuth.ts +++ b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts @@ -27,10 +27,7 @@ export class CustomOAuth implements IOAuthProvider { public responseType: string; - constructor( - public readonly name: string, - options: OauthConfig, - ) { + constructor(public readonly name: string, options: OauthConfig) { this.name = name; if (!Match.test(this.name, String)) { throw new Meteor.Error('CustomOAuth: Name is required and must be String'); diff --git a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts index 14cf16fbe8fd8..ecf0142488308 100644 --- a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts +++ b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts @@ -4,6 +4,7 @@ import { Tracker } from 'meteor/tracker'; import CreateDiscussion from '../../../client/components/CreateDiscussion/CreateDiscussion'; import { imperativeModal } from '../../../client/lib/imperativeModal'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { hasPermission } from '../../authorization/client'; import { settings } from '../../settings/client'; import { MessageAction } from '../../ui-utils/client'; @@ -20,7 +21,9 @@ Meteor.startup(() => { label: 'Discussion_start', type: 'communication', context: ['message', 'message-mobile', 'videoconf'], - async action(_, { message, room }) { + async action(_, props) { + const { message = messageArgs(this).msg, room } = props; + imperativeModal.open({ component: CreateDiscussion, props: { diff --git a/apps/meteor/app/e2e/client/events.ts b/apps/meteor/app/e2e/client/events.js similarity index 83% rename from apps/meteor/app/e2e/client/events.ts rename to apps/meteor/app/e2e/client/events.js index 9ccef3d7b28dd..c59b20594b85f 100644 --- a/apps/meteor/app/e2e/client/events.ts +++ b/apps/meteor/app/e2e/client/events.js @@ -3,5 +3,5 @@ import { Accounts } from 'meteor/accounts-base'; import { e2e } from './rocketchat.e2e'; Accounts.onLogout(() => { - void e2e.stopClient(); + e2e.stopClient(); }); diff --git a/apps/meteor/app/e2e/client/helper.ts b/apps/meteor/app/e2e/client/helper.js similarity index 64% rename from apps/meteor/app/e2e/client/helper.ts rename to apps/meteor/app/e2e/client/helper.js index 66ca3bf1cc2eb..25d9e94078015 100644 --- a/apps/meteor/app/e2e/client/helper.ts +++ b/apps/meteor/app/e2e/client/helper.js @@ -1,20 +1,24 @@ import { Random } from '@rocket.chat/random'; import ByteBuffer from 'bytebuffer'; -export function toString(thing: any) { +// eslint-disable-next-line no-proto +const StaticArrayBufferProto = new ArrayBuffer().__proto__; + +export function toString(thing) { if (typeof thing === 'string') { return thing; } - - return ByteBuffer.wrap(thing).toString('binary'); + // eslint-disable-next-line new-cap + return new ByteBuffer.wrap(thing).toString('binary'); } -export function toArrayBuffer(thing: any) { +export function toArrayBuffer(thing) { if (thing === undefined) { return undefined; } - if (typeof thing === 'object') { - if (Object.getPrototypeOf(thing) === ArrayBuffer.prototype) { + if (thing === Object(thing)) { + // eslint-disable-next-line no-proto + if (thing.__proto__ === StaticArrayBufferProto) { return thing; } } @@ -22,11 +26,11 @@ export function toArrayBuffer(thing: any) { if (typeof thing !== 'string') { throw new Error(`Tried to convert a non-string of type ${typeof thing} to an array buffer`); } - - return ByteBuffer.wrap(thing, 'binary').toArrayBuffer(); + // eslint-disable-next-line new-cap + return new ByteBuffer.wrap(thing, 'binary').toArrayBuffer(); } -export function joinVectorAndEcryptedData(vector: any, encryptedData: any) { +export function joinVectorAndEcryptedData(vector, encryptedData) { const cipherText = new Uint8Array(encryptedData); const output = new Uint8Array(vector.length + cipherText.length); output.set(vector, 0); @@ -34,30 +38,30 @@ export function joinVectorAndEcryptedData(vector: any, encryptedData: any) { return output; } -export function splitVectorAndEcryptedData(cipherText: any) { +export function splitVectorAndEcryptedData(cipherText) { const vector = cipherText.slice(0, 16); const encryptedData = cipherText.slice(16); return [vector, encryptedData]; } -export async function encryptRSA(key: any, data: any) { +export async function encryptRSA(key, data) { return crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, data); } -export async function encryptAES(vector: any, key: any, data: any) { +export async function encryptAES(vector, key, data) { return crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data); } -export async function encryptAESCTR(vector: any, key: any, data: any) { +export async function encryptAESCTR(vector, key, data) { return crypto.subtle.encrypt({ name: 'AES-CTR', counter: vector, length: 64 }, key, data); } -export async function decryptRSA(key: any, data: any) { +export async function decryptRSA(key, data) { return crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data); } -export async function decryptAES(vector: any, key: any, data: any) { +export async function decryptAES(vector, key, data) { return crypto.subtle.decrypt({ name: 'AES-CBC', iv: vector }, key, data); } @@ -82,54 +86,54 @@ export async function generateRSAKey() { ); } -export async function exportJWKKey(key: any) { +export async function exportJWKKey(key) { return crypto.subtle.exportKey('jwk', key); } -export async function importRSAKey(keyData: any, keyUsages: ReadonlyArray = ['encrypt', 'decrypt']) { +export async function importRSAKey(keyData, keyUsages = ['encrypt', 'decrypt']) { return crypto.subtle.importKey( - 'jwk' as any, + 'jwk', keyData, { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: 'SHA-256' }, - } as any, + }, true, keyUsages, ); } -export async function importAESKey(keyData: any, keyUsages: ReadonlyArray = ['encrypt', 'decrypt']) { +export async function importAESKey(keyData, keyUsages = ['encrypt', 'decrypt']) { return crypto.subtle.importKey('jwk', keyData, { name: 'AES-CBC' }, true, keyUsages); } -export async function importRawKey(keyData: any, keyUsages: ReadonlyArray = ['deriveKey']) { +export async function importRawKey(keyData, keyUsages = ['deriveKey']) { return crypto.subtle.importKey('raw', keyData, { name: 'PBKDF2' }, false, keyUsages); } -export async function deriveKey(salt: any, baseKey: any, keyUsages: ReadonlyArray = ['encrypt', 'decrypt']) { +export async function deriveKey(salt, baseKey, keyUsages = ['encrypt', 'decrypt']) { const iterations = 1000; const hash = 'SHA-256'; return crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations, hash }, baseKey, { name: 'AES-CBC', length: 256 }, true, keyUsages); } -export async function readFileAsArrayBuffer(file: any) { - return new Promise((resolve, reject) => { +export async function readFileAsArrayBuffer(file) { + return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = (evt) => { - resolve(evt.target?.result); + reader.onload = function (evt) { + resolve(evt.target.result); }; - reader.onerror = (evt) => { + reader.onerror = function (evt) { reject(evt); }; reader.readAsArrayBuffer(file); }); } -export async function generateMnemonicPhrase(n: any, sep = ' ') { +export async function generateMnemonicPhrase(n, sep = ' ') { const { default: wordList } = await import('./wordList'); const result = new Array(n); let len = wordList.length; @@ -143,14 +147,14 @@ export async function generateMnemonicPhrase(n: any, sep = ' ') { return result.join(sep); } -export async function createSha256HashFromText(data: any) { +export async function createSha256HashFromText(data) { const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data)); return Array.from(new Uint8Array(hash)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } -export async function sha256HashFromArrayBuffer(arrayBuffer: any) { +export async function sha256HashFromArrayBuffer(arrayBuffer) { const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', arrayBuffer))); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js similarity index 88% rename from apps/meteor/app/e2e/client/rocketchat.e2e.room.ts rename to apps/meteor/app/e2e/client/rocketchat.e2e.room.js index ff1841a7ef86e..4c9de837dce03 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -34,7 +34,7 @@ import { e2e } from './rocketchat.e2e'; const KEY_ID = Symbol('keyID'); const PAUSED = Symbol('PAUSED'); -const permitedMutations: any = { +const permitedMutations = { [E2ERoomState.NOT_STARTED]: [E2ERoomState.ESTABLISHING, E2ERoomState.DISABLED, E2ERoomState.KEYS_RECEIVED], [E2ERoomState.READY]: [E2ERoomState.DISABLED, E2ERoomState.CREATING_KEYS, E2ERoomState.WAITING_KEYS], [E2ERoomState.ERROR]: [E2ERoomState.KEYS_RECEIVED, E2ERoomState.NOT_STARTED], @@ -49,7 +49,7 @@ const permitedMutations: any = { ], }; -const filterMutation = (currentState: any, nextState: any): any => { +const filterMutation = (currentState, nextState) => { if (currentState === nextState) { return nextState === E2ERoomState.ERROR; } @@ -66,29 +66,11 @@ const filterMutation = (currentState: any, nextState: any): any => { }; export class E2ERoom extends Emitter { - state: any = undefined; + state = undefined; - [PAUSED]: boolean | undefined = undefined; + [PAUSED] = undefined; - [KEY_ID]: any; - - userId: any; - - roomId: any; - - typeOfRoom: any; - - roomKeyId: any; - - groupSessionKey: any; - - oldKeys: any; - - sessionKeyExportedString: string | undefined; - - sessionKeyExported: any; - - constructor(userId: any, room: any) { + constructor(userId, room) { super(); this.userId = userId; @@ -111,11 +93,11 @@ export class E2ERoom extends Emitter { this.setState(E2ERoomState.NOT_STARTED); } - log(...msg: unknown[]) { + log(...msg) { log(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg); } - error(...msg: unknown[]) { + error(...msg) { logError(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg); } @@ -127,7 +109,7 @@ export class E2ERoom extends Emitter { return this.state; } - setState(requestedState: any) { + setState(requestedState) { const currentState = this.state; const nextState = filterMutation(currentState, requestedState); @@ -138,7 +120,7 @@ export class E2ERoom extends Emitter { this.state = nextState; this.log(currentState, '->', nextState); - this.emit('STATE_CHANGED', currentState); + this.emit('STATE_CHANGED', currentState, nextState, this); this.emit(nextState, this); } @@ -178,7 +160,7 @@ export class E2ERoom extends Emitter { this.setState(E2ERoomState.KEYS_RECEIVED); } - async shouldConvertSentMessages(message: any) { + async shouldConvertSentMessages(message) { if (!this.isReady() || this[PAUSED]) { return false; } @@ -215,7 +197,7 @@ export class E2ERoom extends Emitter { async decryptSubscription() { const subscription = Subscriptions.findOne({ rid: this.roomId }); - if (subscription?.lastMessage?.t !== 'e2e') { + if (subscription.lastMessage?.t !== 'e2e') { this.log('decryptSubscriptions nothing to do'); return; } @@ -263,7 +245,7 @@ export class E2ERoom extends Emitter { this.log('decryptOldRoomKeys Done'); } - async exportOldRoomKeys(oldKeys: any) { + async exportOldRoomKeys(oldKeys) { this.log('exportOldRoomKeys starting'); if (!oldKeys || oldKeys.length === 0) { this.log('exportOldRoomKeys nothing to do'); @@ -312,7 +294,7 @@ export class E2ERoom extends Emitter { this.setState(E2ERoomState.ESTABLISHING); try { - const groupKey = Subscriptions.findOne({ rid: this.roomId })?.E2EKey; + const groupKey = Subscriptions.findOne({ rid: this.roomId }).E2EKey; if (groupKey) { await this.importGroupKey(groupKey); this.setState(E2ERoomState.READY); @@ -325,7 +307,7 @@ export class E2ERoom extends Emitter { } try { - const room = ChatRoom.findOne({ _id: this.roomId })!; + const room = ChatRoom.findOne({ _id: this.roomId }); // Only room creator can set keys for room if (!room.e2eKeyId && this.userShouldCreateKeys(room)) { this.setState(E2ERoomState.CREATING_KEYS); @@ -343,7 +325,7 @@ export class E2ERoom extends Emitter { } } - userShouldCreateKeys(room: any) { + userShouldCreateKeys(room) { // On DMs, we'll allow any user to set the keys if (room.t === 'd') { return true; @@ -352,15 +334,15 @@ export class E2ERoom extends Emitter { return room.u._id === this.userId; } - isSupportedRoomType(type: any) { + isSupportedRoomType(type) { return roomCoordinator.getRoomDirectives(type).allowRoomSettingChange({}, RoomSettingsEnum.E2E); } - async decryptSessionKey(key: any) { + async decryptSessionKey(key) { return importAESKey(JSON.parse(await this.exportSessionKey(key))); } - async exportSessionKey(key: any) { + async exportSessionKey(key) { key = key.slice(12); key = Base64.decode(key); @@ -368,7 +350,7 @@ export class E2ERoom extends Emitter { return toString(decryptedKey); } - async importGroupKey(groupKey: any) { + async importGroupKey(groupKey) { this.log('Importing room key ->', this.roomId); // Get existing group key // const keyID = groupKey.slice(0, 12); @@ -392,7 +374,7 @@ export class E2ERoom extends Emitter { // Import session key for use. try { - const key = await importAESKey(JSON.parse(this.sessionKeyExportedString!)); + const key = await importAESKey(JSON.parse(this.sessionKeyExportedString)); // Key has been obtained. E2E is now in session. this.groupSessionKey = key; } catch (error) { @@ -420,8 +402,8 @@ export class E2ERoom extends Emitter { await sdk.rest.post('/v1/e2e.updateGroupKey', { rid: this.roomId, uid: this.userId, - key: await this.encryptGroupKeyForParticipant(e2e.publicKey!), - } as any); + key: await this.encryptGroupKeyForParticipant(e2e.publicKey), + }); await this.encryptKeyForOtherParticipants(); } catch (error) { this.error('Error exporting group key: ', error); @@ -452,7 +434,7 @@ export class E2ERoom extends Emitter { } } - onRoomKeyReset(keyID: any) { + onRoomKeyReset(keyID) { this.log(`Room keyID was reset. New keyID: ${keyID} Previous keyID: ${this.keyID}`); this.setState(E2ERoomState.WAITING_KEYS); this.keyID = keyID; @@ -473,10 +455,10 @@ export class E2ERoom extends Emitter { return; } - const usersSuggestedGroupKeys = { [this.roomId]: [] as any[] }; + const usersSuggestedGroupKeys = { [this.roomId]: [] }; for await (const user of users) { - const encryptedGroupKey = await this.encryptGroupKeyForParticipant(user.e2e!.public_key!); - const oldKeys = await this.encryptOldKeysForParticipant(user.e2e?.public_key, decryptedOldGroupKeys); + const encryptedGroupKey = await this.encryptGroupKeyForParticipant(user.e2e.public_key); + const oldKeys = await this.encryptOldKeysForParticipant(user.e2e.public_key, decryptedOldGroupKeys); usersSuggestedGroupKeys[this.roomId].push({ _id: user._id, key: encryptedGroupKey, ...(oldKeys && { oldKeys }) }); } @@ -487,7 +469,7 @@ export class E2ERoom extends Emitter { } } - async encryptOldKeysForParticipant(publicKey: any, oldRoomKeys: any) { + async encryptOldKeysForParticipant(public_key, oldRoomKeys) { if (!oldRoomKeys || oldRoomKeys.length === 0) { return; } @@ -495,7 +477,7 @@ export class E2ERoom extends Emitter { let userKey; try { - userKey = await importRSAKey(JSON.parse(publicKey), ['encrypt']); + userKey = await importRSAKey(JSON.parse(public_key), ['encrypt']); } catch (error) { return this.error('Error importing user key: ', error); } @@ -517,10 +499,10 @@ export class E2ERoom extends Emitter { } } - async encryptGroupKeyForParticipant(publicKey: string) { + async encryptGroupKeyForParticipant(public_key) { let userKey; try { - userKey = await importRSAKey(JSON.parse(publicKey), ['encrypt']); + userKey = await importRSAKey(JSON.parse(public_key), ['encrypt']); } catch (error) { return this.error('Error importing user key: ', error); } @@ -537,7 +519,7 @@ export class E2ERoom extends Emitter { } // Encrypts files before upload. I/O is in arraybuffers. - async encryptFile(file: any) { + async encryptFile(file) { // if (!this.isSupportedRoomType(this.typeOfRoom)) { // return; // } @@ -572,7 +554,7 @@ export class E2ERoom extends Emitter { } // Decrypt uploaded encrypted files. I/O is in arraybuffers. - async decryptFile(file: any, key: any, iv: any) { + async decryptFile(file, key, iv) { const ivArray = Base64.decode(iv); const cryptoKey = await window.crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']); @@ -580,7 +562,7 @@ export class E2ERoom extends Emitter { } // Encrypts messages - async encryptText(data: any) { + async encryptText(data) { const vector = crypto.getRandomValues(new Uint8Array(16)); try { @@ -593,7 +575,7 @@ export class E2ERoom extends Emitter { } // Helper function for encryption of content - async encryptMessageContent(contentToBeEncrypted: any) { + async encryptMessageContent(contentToBeEncrypted) { const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted)); return { @@ -603,7 +585,7 @@ export class E2ERoom extends Emitter { } // Helper function for encryption of content - async encryptMessage(message: any) { + async encryptMessage(message) { const { msg, attachments, ...rest } = message; const content = await this.encryptMessageContent({ msg, attachments }); @@ -617,7 +599,7 @@ export class E2ERoom extends Emitter { } // Helper function for encryption of messages - encrypt(message: any) { + encrypt(message) { if (!this.isSupportedRoomType(this.typeOfRoom)) { return; } @@ -628,7 +610,7 @@ export class E2ERoom extends Emitter { const ts = new Date(); - const data = new TextEncoder().encode( + const data = new TextEncoder('UTF-8').encode( EJSON.stringify({ _id: message._id, text: message.msg, @@ -640,7 +622,7 @@ export class E2ERoom extends Emitter { return this.encryptText(data); } - async decryptContent(data: any) { + async decryptContent(data) { if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') { const content = await this.decrypt(data.content.ciphertext); Object.assign(data, content); @@ -650,7 +632,7 @@ export class E2ERoom extends Emitter { } // Decrypt messages - async decryptMessage(message: any) { + async decryptMessage(message) { if (message.t !== 'e2e' || message.e2e === 'done') { return message; } @@ -671,12 +653,12 @@ export class E2ERoom extends Emitter { }; } - async doDecrypt(vector: any, key: any, cipherText: any) { + async doDecrypt(vector, key, cipherText) { const result = await decryptAES(vector, key, cipherText); return EJSON.parse(new TextDecoder('UTF-8').decode(new Uint8Array(result))); } - async decrypt(message: any) { + async decrypt(message) { const keyID = message.slice(0, 12); message = message.slice(12); @@ -684,7 +666,7 @@ export class E2ERoom extends Emitter { let oldKey = ''; if (keyID !== this.keyID) { - const oldRoomKey = this.oldKeys?.find((key: any) => key.e2eKeyId === keyID); + const oldRoomKey = this.oldKeys?.find((key) => key.e2eKeyId === keyID); // Messages already contain a keyID stored with them // That means that if we cannot find a keyID for the key the message has preppended to // The message is indecipherable. @@ -709,21 +691,21 @@ export class E2ERoom extends Emitter { } } - provideKeyToUser(keyId: any) { + provideKeyToUser(keyId) { if (this.keyID !== keyId) { return; } - void this.encryptKeyForOtherParticipants(); + this.encryptKeyForOtherParticipants(); this.setState(E2ERoomState.READY); } - onStateChange(cb: any) { + onStateChange(cb) { this.on('STATE_CHANGED', cb); return () => this.off('STATE_CHANGED', cb); } - async encryptGroupKeyForParticipantsWaitingForTheKeys(users: any[]) { + async encryptGroupKeyForParticipantsWaitingForTheKeys(users) { if (!this.isReady()) { return; } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 824afc3aa2d5c..3b2fd01621e47 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -44,7 +44,7 @@ import { import { log, logError } from './logger'; import { E2ERoom } from './rocketchat.e2e.room'; -import './events'; +import './events.js'; let failedToDecodeKey = false; diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js similarity index 59% rename from apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts rename to apps/meteor/app/emoji-custom/client/lib/emojiCustom.js index 3f4b876f12a76..64f1df9bd932c 100644 --- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts +++ b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js @@ -1,68 +1,57 @@ -import type { IEmoji } from '@rocket.chat/core-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; import { Session } from 'meteor/session'; -import { emoji, removeFromRecent, replaceEmojiInRecent } from '../../../emoji/client'; +import { emoji, updateRecent } from '../../../emoji/client'; import { CachedCollectionManager } from '../../../ui-cached-collection/client'; import { getURL } from '../../../utils/client'; import { sdk } from '../../../utils/client/lib/SDKClient'; +import { isSetNotNull } from './function-isSet'; -const isSetNotNull = (fn: () => unknown) => { - let value; - try { - value = fn(); - } catch (e) { - value = null; - } - return value !== null && value !== undefined; -}; - -const getEmojiUrlFromName = (name: string, extension: string) => { +export const getEmojiUrlFromName = function (name, extension) { if (name == null) { return; } - const key = `emoji_random_${name}` as const; + const key = `emoji_random_${name}`; - const random = (Session as unknown as { keys: Record }).keys[key] ?? 0; + const random = isSetNotNull(() => Session.keys[key]) ? Session.keys[key] : 0; return getURL(`/emoji-custom/${encodeURIComponent(name)}.${extension}?_dc=${random}`); }; -export const deleteEmojiCustom = (emojiData: IEmoji) => { +export const deleteEmojiCustom = function (emojiData) { delete emoji.list[`:${emojiData.name}:`]; const arrayIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(emojiData.name); if (arrayIndex !== -1) { emoji.packages.emojiCustom.emojisByCategory.rocket.splice(arrayIndex, 1); } - const arrayIndexList = emoji.packages.emojiCustom.list?.indexOf(`:${emojiData.name}:`) ?? -1; + const arrayIndexList = emoji.packages.emojiCustom.list.indexOf(`:${emojiData.name}:`); if (arrayIndexList !== -1) { - emoji.packages.emojiCustom.list?.splice(arrayIndexList, 1); + emoji.packages.emojiCustom.list.splice(arrayIndexList, 1); } - if (emojiData.aliases) { + if (isSetNotNull(() => emojiData.aliases)) { for (const alias of emojiData.aliases) { delete emoji.list[`:${alias}:`]; - const aliasIndex = emoji.packages.emojiCustom.list?.indexOf(`:${alias}:`) ?? -1; + const aliasIndex = emoji.packages.emojiCustom.list.indexOf(`:${alias}:`); if (aliasIndex !== -1) { - emoji.packages.emojiCustom.list?.splice(aliasIndex, 1); + emoji.packages.emojiCustom.list.splice(aliasIndex, 1); } } } - - removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent); + updateRecent('rocket'); }; -export const updateEmojiCustom = (emojiData: IEmoji) => { +export const updateEmojiCustom = function (emojiData) { const previousExists = isSetNotNull(() => emojiData.previousName); const currentAliases = isSetNotNull(() => emojiData.aliases); if (previousExists && isSetNotNull(() => emoji.list[`:${emojiData.previousName}:`].aliases)) { - for (const alias of emoji.list[`:${emojiData.previousName}:`].aliases ?? []) { + for (const alias of emoji.list[`:${emojiData.previousName}:`].aliases) { delete emoji.list[`:${alias}:`]; - const aliasIndex = emoji.packages.emojiCustom.list?.indexOf(`:${alias}:`) ?? -1; + const aliasIndex = emoji.packages.emojiCustom.list.indexOf(`:${alias}:`); if (aliasIndex !== -1) { - emoji.packages.emojiCustom.list?.splice(aliasIndex, 1); + emoji.packages.emojiCustom.list.splice(aliasIndex, 1); } } } @@ -72,9 +61,9 @@ export const updateEmojiCustom = (emojiData: IEmoji) => { if (arrayIndex !== -1) { emoji.packages.emojiCustom.emojisByCategory.rocket.splice(arrayIndex, 1); } - const arrayIndexList = emoji.packages.emojiCustom.list?.indexOf(`:${emojiData.previousName}:`) ?? -1; + const arrayIndexList = emoji.packages.emojiCustom.list.indexOf(`:${emojiData.previousName}:`); if (arrayIndexList !== -1) { - emoji.packages.emojiCustom.list?.splice(arrayIndexList, 1); + emoji.packages.emojiCustom.list.splice(arrayIndexList, 1); } delete emoji.list[`:${emojiData.previousName}:`]; } @@ -82,26 +71,23 @@ export const updateEmojiCustom = (emojiData: IEmoji) => { const categoryIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(`${emojiData.name}`); if (categoryIndex === -1) { emoji.packages.emojiCustom.emojisByCategory.rocket.push(`${emojiData.name}`); - emoji.packages.emojiCustom.list?.push(`:${emojiData.name}:`); + emoji.packages.emojiCustom.list.push(`:${emojiData.name}:`); } emoji.list[`:${emojiData.name}:`] = Object.assign({ emojiPackage: 'emojiCustom' }, emoji.list[`:${emojiData.name}:`], emojiData); if (currentAliases) { for (const alias of emojiData.aliases) { - emoji.packages.emojiCustom.list?.push(`:${alias}:`); - emoji.list[`:${alias}:`] = { - emojiPackage: 'emojiCustom', - aliasOf: emojiData.name, - }; + emoji.packages.emojiCustom.list.push(`:${alias}:`); + emoji.list[`:${alias}:`] = {}; + emoji.list[`:${alias}:`].emojiPackage = 'emojiCustom'; + emoji.list[`:${alias}:`].aliasOf = emojiData.name; } } - if (previousExists) { - replaceEmojiInRecent({ oldEmoji: emojiData.previousName, newEmoji: emojiData.name }); - } + updateRecent('rocket'); }; -const customRender = (html: string) => { - const emojisMatchGroup = emoji.packages.emojiCustom.list?.map(escapeRegExp).join('|'); +const customRender = (html) => { + const emojisMatchGroup = emoji.packages.emojiCustom.list.map(escapeRegExp).join('|'); if (emojisMatchGroup !== emoji.packages.emojiCustom._regexpSignature) { emoji.packages.emojiCustom._regexpSignature = emojisMatchGroup; emoji.packages.emojiCustom._regexp = new RegExp( @@ -110,22 +96,22 @@ const customRender = (html: string) => { ); } - html = html.replace(emoji.packages.emojiCustom._regexp!, (shortname) => { - if (typeof shortname === 'undefined' || shortname === '' || (emoji.packages.emojiCustom.list?.indexOf(shortname) ?? -1) === -1) { + html = html.replace(emoji.packages.emojiCustom._regexp, (shortname) => { + if (typeof shortname === 'undefined' || shortname === '' || emoji.packages.emojiCustom.list.indexOf(shortname) === -1) { return shortname; } let emojiAlias = shortname.replace(/:/g, ''); let dataCheck = emoji.list[shortname]; - if (dataCheck.aliasOf) { + if (dataCheck.hasOwnProperty('aliasOf')) { emojiAlias = dataCheck.aliasOf; dataCheck = emoji.list[`:${emojiAlias}:`]; } return `${shortname}`; }); @@ -139,7 +125,7 @@ emoji.packages.emojiCustom = { list: [], _regexpSignature: null, _regexp: null, - emojisByCategory: {}, + render: customRender, renderPicker: customRender, }; @@ -149,15 +135,16 @@ Meteor.startup(() => try { const { emojis: { update: emojis }, - } = await sdk.rest.get('/v1/emoji-custom.list', { query: '' }); + } = await sdk.rest.get('/v1/emoji-custom.list'); emoji.packages.emojiCustom.emojisByCategory = { rocket: [] }; for (const currentEmoji of emojis) { emoji.packages.emojiCustom.emojisByCategory.rocket.push(currentEmoji.name); - emoji.packages.emojiCustom.list?.push(`:${currentEmoji.name}:`); - emoji.list[`:${currentEmoji.name}:`] = { ...currentEmoji, emojiPackage: 'emojiCustom' } as any; + emoji.packages.emojiCustom.list.push(`:${currentEmoji.name}:`); + emoji.list[`:${currentEmoji.name}:`] = currentEmoji; + emoji.list[`:${currentEmoji.name}:`].emojiPackage = 'emojiCustom'; for (const alias of currentEmoji.aliases) { - emoji.packages.emojiCustom.list?.push(`:${alias}:`); + emoji.packages.emojiCustom.list.push(`:${alias}:`); emoji.list[`:${alias}:`] = { emojiPackage: 'emojiCustom', aliasOf: currentEmoji.name, diff --git a/apps/meteor/app/emoji-custom/client/lib/function-isSet.js b/apps/meteor/app/emoji-custom/client/lib/function-isSet.js new file mode 100644 index 0000000000000..0ccf1abe02ab0 --- /dev/null +++ b/apps/meteor/app/emoji-custom/client/lib/function-isSet.js @@ -0,0 +1,9 @@ +export const isSetNotNull = function (fn) { + let value; + try { + value = fn(); + } catch (e) { + value = null; + } + return value !== null && value !== undefined; +}; diff --git a/apps/meteor/app/emoji/client/emojiParser.ts b/apps/meteor/app/emoji/client/emojiParser.js similarity index 74% rename from apps/meteor/app/emoji/client/emojiParser.ts rename to apps/meteor/app/emoji/client/emojiParser.js index 08ec99b069586..0b3b722aaebdf 100644 --- a/apps/meteor/app/emoji/client/emojiParser.ts +++ b/apps/meteor/app/emoji/client/emojiParser.js @@ -3,8 +3,10 @@ import { emoji } from './lib'; /** * emojiParser is a function that will replace emojis + * @param {{ html: string }} message - The message object + * @return {{ html: string }} */ -export const emojiParser = (html: string) => { +export const emojiParser = ({ html }) => { html = html.trim(); // ' to apostrophe (') for emojis such as :') @@ -26,12 +28,8 @@ export const emojiParser = (html: string) => { let hasText = false; if (!isIE11) { - const isElement = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE; - - const isTextNode = (node: Node): node is Text => node.nodeType === Node.TEXT_NODE; - - const filter = (node: Node) => { - if (isElement(node) && (node.classList.contains('emojione') || node.classList.contains('emoji'))) { + const filter = (node) => { + if (node.nodeType === Node.ELEMENT_NODE && (node.classList.contains('emojione') || node.classList.contains('emoji'))) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; @@ -40,7 +38,7 @@ export const emojiParser = (html: string) => { const walker = document.createTreeWalker(checkEmojiOnly, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, filter); while (walker.nextNode()) { - if (isTextNode(walker.currentNode) && walker.currentNode.nodeValue.trim() !== '') { + if (walker.currentNode.nodeType === Node.TEXT_NODE && walker.currentNode.nodeValue.trim() !== '') { hasText = true; break; } @@ -62,5 +60,5 @@ export const emojiParser = (html: string) => { // line breaks '
' back to '
' html = html.replace(/
/g, '
'); - return html; + return { html }; }; diff --git a/apps/meteor/app/emoji/client/helpers.ts b/apps/meteor/app/emoji/client/helpers.ts index a203216640f5e..35badda26a732 100644 --- a/apps/meteor/app/emoji/client/helpers.ts +++ b/apps/meteor/app/emoji/client/helpers.ts @@ -138,7 +138,7 @@ export const getEmojisBySearchTerm = ( return emojis; }; -export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecentEmojis?: (emojis: string[]) => void) => { +export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void) => { const _emoji = emoji.replace(/(^:|:$)/g, ''); const pos = recentEmojis.indexOf(_emoji as never); @@ -146,7 +146,7 @@ export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecen return; } recentEmojis.splice(pos, 1); - setRecentEmojis?.(recentEmojis); + setRecentEmojis(recentEmojis); }; export const updateRecent = (recentList: string[]) => { @@ -156,15 +156,6 @@ export const updateRecent = (recentList: string[]) => { }); }; -export const replaceEmojiInRecent = ({ oldEmoji, newEmoji }: { oldEmoji: string; newEmoji: string }) => { - const recentPkgList: string[] = emoji.packages.base.emojisByCategory.recent; - const pos = recentPkgList.indexOf(oldEmoji); - - if (pos !== -1) { - recentPkgList[pos] = newEmoji; - } -}; - const getEmojiRender = (emojiName: string) => { const emojiPackageName = emoji.list[emojiName]?.emojiPackage; const emojiPackage = emoji.packages[emojiPackageName]; diff --git a/apps/meteor/app/emoji/lib/rocketchat.ts b/apps/meteor/app/emoji/lib/rocketchat.ts index f5d33cce3de08..49d6ffbe41aa3 100644 --- a/apps/meteor/app/emoji/lib/rocketchat.ts +++ b/apps/meteor/app/emoji/lib/rocketchat.ts @@ -9,9 +9,6 @@ export type EmojiPackage = { renderPicker: (emojiToRender: string) => string | undefined; ascii?: boolean; sprites?: unknown; - list?: string[]; - _regexpSignature?: string | null; - _regexp?: RegExp | null; }; export type EmojiPackages = { @@ -19,25 +16,14 @@ export type EmojiPackages = { [key: string]: EmojiPackage; }; list: { - [key: keyof NonNullable]: - | { - category: string; - emojiPackage: string; - shortnames: string[]; - uc_base: string; - uc_greedy: string; - uc_match: string; - uc_output: string; - aliases?: string[]; - aliasOf?: undefined; - extension?: string; - } - | { - emojiPackage: string; - aliasOf: string; - extension?: undefined; - aliases?: undefined; - shortnames?: undefined; - }; + [key: keyof NonNullable]: { + category: string; + emojiPackage: string; + shortnames: string[]; + uc_base: string; + uc_greedy: string; + uc_match: string; + uc_output: string; + }; }; }; diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts index 24224a599abf8..984561fe13cd6 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts @@ -110,7 +110,7 @@ process.on('unhandledRejection', (error) => { console.error('Future node.js versions will automatically exit the process'); console.error('================================='); - if (process.env.TEST_MODE || process.env.NODE_ENV === 'development' || process.env.EXIT_UNHANDLEDPROMISEREJECTION) { + if (process.env.NODE_ENV === 'development' || process.env.EXIT_UNHANDLEDPROMISEREJECTION) { process.exit(1); } }); @@ -125,8 +125,4 @@ process.on('uncaughtException', async (error) => { console.error('==========================='); void errorHandler.trackError(error.message, error.stack); - - if (process.env.TEST_MODE || process.env.NODE_ENV === 'development' || process.env.EXIT_UNHANDLEDPROMISEREJECTION) { - process.exit(1); - } }); diff --git a/apps/meteor/app/federation/server/functions/helpers.ts b/apps/meteor/app/federation/server/functions/helpers.ts index 54df33cdfa2d3..c684b7b8f74a3 100644 --- a/apps/meteor/app/federation/server/functions/helpers.ts +++ b/apps/meteor/app/federation/server/functions/helpers.ts @@ -57,13 +57,10 @@ export const getFederatedRoomData = async ( // Find all subscriptions of this room const s = await Subscriptions.findByRoomIdWhenUsernameExists(room._id).toArray(); - const subscriptions = s.reduce( - (acc, s) => { - acc[s.u._id] = s; - return acc; - }, - {} as { [k: string]: ISubscription }, - ); + const subscriptions = s.reduce((acc, s) => { + acc[s.u._id] = s; + return acc; + }, {} as { [k: string]: ISubscription }); // Get all user ids const userIds = Object.keys(subscriptions); diff --git a/apps/meteor/app/federation/server/handler/index.ts b/apps/meteor/app/federation/server/handler/index.js similarity index 80% rename from apps/meteor/app/federation/server/handler/index.ts rename to apps/meteor/app/federation/server/handler/index.js index f7a3ae53ec298..c5b19856f19f3 100644 --- a/apps/meteor/app/federation/server/handler/index.ts +++ b/apps/meteor/app/federation/server/handler/index.js @@ -5,7 +5,7 @@ import { federationRequestToPeer } from '../lib/http'; import { isFederationEnabled } from '../lib/isFederationEnabled'; import { clientLogger } from '../lib/logger'; -export async function federationSearchUsers(query: string) { +export async function federationSearchUsers(query) { if (!isFederationEnabled()) { throw disabled('client.searchUsers'); } @@ -23,7 +23,7 @@ export async function federationSearchUsers(query: string) { return users; } -export async function getUserByUsername(query: string) { +export async function getUserByUsername(query) { if (!isFederationEnabled()) { throw disabled('client.searchUsers'); } @@ -41,13 +41,7 @@ export async function getUserByUsername(query: string) { return user; } -export async function requestEventsFromLatest( - domain: string, - fromDomain: string, - contextType: unknown, - contextQuery: unknown, - latestEventIds: unknown, -) { +export async function requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) { if (!isFederationEnabled()) { throw disabled('client.requestEventsFromLatest'); } @@ -70,7 +64,7 @@ export async function requestEventsFromLatest( }); } -export async function dispatchEvents(domains: string[], events: unknown[]) { +export async function dispatchEvents(domains, events) { if (!isFederationEnabled()) { throw disabled('client.dispatchEvents'); } @@ -86,11 +80,11 @@ export async function dispatchEvents(domains: string[], events: unknown[]) { } } -export async function dispatchEvent(domains: string[], event: unknown) { +export async function dispatchEvent(domains, event) { await dispatchEvents([...new Set(domains)], [event]); } -export async function getUpload(domain: string, fileId: string) { +export async function getUpload(domain, fileId) { const { data: { upload, buffer }, } = await federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${qs.stringify({ upload_id: fileId })}`); diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index c7b82fe3e02c3..ac97923be41ed 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -384,7 +384,7 @@ export const FileUpload = { ? { width, height, - } + } : undefined, }; diff --git a/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts index f057da4a625d4..de37ba2002897 100644 --- a/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts +++ b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts @@ -10,7 +10,8 @@ export class PendingAvatarImporter extends Importer { this.logger.debug('start preparing import operation'); await super.updateProgress(ProgressStep.PREPARING_STARTED); - const fileCount = await Users.countAllUsersWithPendingAvatar(); + const users = Users.findAllUsersWithPendingAvatar(); + const fileCount = await users.count(); if (fileCount === 0) { await super.updateProgress(ProgressStep.DONE); diff --git a/apps/meteor/app/importer-slack/server/SlackImporter.ts b/apps/meteor/app/importer-slack/server/SlackImporter.ts index 59c07d26e9692..344db66565310 100644 --- a/apps/meteor/app/importer-slack/server/SlackImporter.ts +++ b/apps/meteor/app/importer-slack/server/SlackImporter.ts @@ -545,16 +545,13 @@ export class SlackImporter extends Importer { // Process the reactions if (message.reactions && message.reactions.length > 0) { - newMessage.reactions = message.reactions.reduce( - (newReactions, reaction) => { - const name = `:${reaction.name}:`; - return { - ...newReactions, - ...(reaction.users?.length ? { name: { name, users: this._replaceSlackUserIds(reaction.users) } } : {}), - }; - }, - {} as Required['reactions'], - ); + newMessage.reactions = message.reactions.reduce((newReactions, reaction) => { + const name = `:${reaction.name}:`; + return { + ...newReactions, + ...(reaction.users?.length ? { name: { name, users: this._replaceSlackUserIds(reaction.users) } } : {}), + }; + }, {} as Required['reactions']); } if (message.type === 'message') { diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index 52716dabef5a3..2d2bb7bad80aa 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -376,7 +376,7 @@ export class UserConverter extends RecordConverter({ script: integration.script, scriptEnabled: integration.scriptEnabled, scriptEngine, - }), + }), ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, }), diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 6ee39d81a5888..4449902437c98 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -91,7 +91,7 @@ Meteor.methods({ scriptEnabled: integration.scriptEnabled, scriptEngine, ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), - }), + }), triggerWords: integration.triggerWords, retryFailedCalls: integration.retryFailedCalls, retryCount: integration.retryCount, @@ -107,7 +107,7 @@ Meteor.methods({ $unset: { ...(integration.scriptCompiled ? { scriptError: 1 as const } : { scriptCompiled: 1 as const }), }, - }), + }), }, ); diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/index.ts b/apps/meteor/app/irc/server/irc-bridge/localHandlers/index.js similarity index 100% rename from apps/meteor/app/irc/server/irc-bridge/localHandlers/index.ts rename to apps/meteor/app/irc/server/irc-bridge/localHandlers/index.js diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/index.ts b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/index.js similarity index 100% rename from apps/meteor/app/irc/server/irc-bridge/peerHandlers/index.ts rename to apps/meteor/app/irc/server/irc-bridge/peerHandlers/index.js diff --git a/apps/meteor/app/irc/server/servers/index.ts b/apps/meteor/app/irc/server/servers/index.js similarity index 100% rename from apps/meteor/app/irc/server/servers/index.ts rename to apps/meteor/app/irc/server/servers/index.js diff --git a/apps/meteor/app/lib/client/OAuthProxy.ts b/apps/meteor/app/lib/client/OAuthProxy.js similarity index 86% rename from apps/meteor/app/lib/client/OAuthProxy.ts rename to apps/meteor/app/lib/client/OAuthProxy.js index ec9143528fc9b..a5035783c6764 100644 --- a/apps/meteor/app/lib/client/OAuthProxy.ts +++ b/apps/meteor/app/lib/client/OAuthProxy.js @@ -6,12 +6,12 @@ OAuth.launchLogin = ((func) => function (options) { const proxy = settings.get('Accounts_OAuth_Proxy_services').replace(/\s/g, '').split(','); if (proxy.includes(options.loginService)) { - const redirectUri = options.loginUrl.match(/(&redirect_uri=)([^&]+|$)/)?.[2]; + const redirect_uri = options.loginUrl.match(/(&redirect_uri=)([^&]+|$)/)[2]; options.loginUrl = options.loginUrl.replace( /(&redirect_uri=)([^&]+|$)/, `$1${encodeURIComponent(settings.get('Accounts_OAuth_Proxy_host'))}/oauth_redirect`, ); - options.loginUrl = options.loginUrl.replace(/(&state=)([^&]+|$)/, `$1${redirectUri}!$2`); + options.loginUrl = options.loginUrl.replace(/(&state=)([^&]+|$)/, `$1${redirect_uri}!$2`); options.loginUrl = `${settings.get('Accounts_OAuth_Proxy_host')}/redirect/${encodeURIComponent(options.loginUrl)}`; } diff --git a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts index 1e7672005867b..263b137ae00c6 100644 --- a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts +++ b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts @@ -3,8 +3,8 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped'; import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; -import type { CloseRoomParams } from '../../../livechat/server/lib/localTypes'; import { notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const closeLivechatRoom = async ( @@ -67,12 +67,12 @@ export const closeLivechatRoom = async ( requestedBy: user, }, }, - } + } : { emailTranscript: { sendToVisitor: false, }, - }), + }), }), }; diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index 5addbd896889b..fee7061cae963 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -49,7 +49,7 @@ export async function loadMessageHistory({ hiddenMessageTypes, options, showThreadMessages, - ).toArray() + ).toArray() : await Messages.findVisibleByRoomIdNotContainingTypes(rid, hiddenMessageTypes, options, showThreadMessages).toArray(); const messages = await normalizeMessagesForUser(records, userId); let unreadNotLoaded = 0; diff --git a/apps/meteor/app/lib/server/functions/notifications/index.ts b/apps/meteor/app/lib/server/functions/notifications/index.ts index 538ff2ad5ed91..54b18f502ae70 100644 --- a/apps/meteor/app/lib/server/functions/notifications/index.ts +++ b/apps/meteor/app/lib/server/functions/notifications/index.ts @@ -47,3 +47,22 @@ export function replaceMentionedUsernamesWithFullNames(message: string, mentions }); return message; } + +/** + * Checks if a message contains a user highlight + * + * @param {string} message + * @param {array|undefined} highlights + * + * @returns {boolean} + */ +export function messageContainsHighlight(message: Pick, highlights: string[] | undefined): boolean { + if (!highlights || highlights.length === 0) { + return false; + } + + return highlights.some((highlight: string) => { + const regexp = new RegExp(escapeRegExp(highlight), 'i'); + return regexp.test(message.msg); + }); +} diff --git a/apps/meteor/app/lib/server/functions/notifications/messageContainsHighlight.ts b/apps/meteor/app/lib/server/functions/notifications/messageContainsHighlight.ts deleted file mode 100644 index d749441ba0b4a..0000000000000 --- a/apps/meteor/app/lib/server/functions/notifications/messageContainsHighlight.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -/** - * Checks if a message contains a user highlight - * - * @param {string} message - * @param {array|undefined} highlights - * - * @returns {boolean} - */ -export function messageContainsHighlight(message: Pick, highlights: string[] | undefined): boolean { - if (!highlights || highlights.length === 0) { - return false; - } - - return highlights.some((highlight: string) => { - const hl = escapeRegExp(highlight); - const regexp = new RegExp(`(? { + Mailer.getTemplate('Accounts_UserAddedEmail_Email', (template) => { + html = template; + }); + + Mailer.getTemplate('Password_Changed_Email', (template) => { + passwordChangedHtml = template; + }); +}); + +async function _sendUserEmail(subject, html, userData) { + const email = { + to: userData.email, + from: settings.get('From_Email'), + subject, + html, + data: { + email: userData.email, + password: userData.password, + }, + }; + + if (typeof userData.name !== 'undefined') { + email.data.name = userData.name; + } + + try { + await Mailer.send(email); + } catch (error) { + throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, { + function: 'RocketChat.saveUser', + message: error.message, + }); + } +} + +async function validateUserData(userId, userData) { + const existingRoles = _.pluck(await getRoles(), '_id'); + + if (userData.verified && userData._id && userId === userData._id) { + throw new Meteor.Error('error-action-not-allowed', 'Editing email verification is not allowed', { + method: 'insertOrUpdateUser', + action: 'Editing_user', + }); + } + + if (userData._id && userId !== userData._id && !(await hasPermissionAsync(userId, 'edit-other-user-info'))) { + throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed', { + method: 'insertOrUpdateUser', + action: 'Editing_user', + }); + } + + if (!userData._id && !(await hasPermissionAsync(userId, 'create-user'))) { + throw new Meteor.Error('error-action-not-allowed', 'Adding user is not allowed', { + method: 'insertOrUpdateUser', + action: 'Adding_user', + }); + } + + if (userData.roles && _.difference(userData.roles, existingRoles).length > 0) { + throw new Meteor.Error('error-action-not-allowed', 'The field Roles consist invalid role id', { + method: 'insertOrUpdateUser', + action: 'Assign_role', + }); + } + + if (userData.roles && userData.roles.includes('admin') && !(await hasPermissionAsync(userId, 'assign-admin-role'))) { + throw new Meteor.Error('error-action-not-allowed', 'Assigning admin is not allowed', { + method: 'insertOrUpdateUser', + action: 'Assign_admin', + }); + } + + if (settings.get('Accounts_RequireNameForSignUp') && !userData._id && !trim(userData.name)) { + throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { + method: 'insertOrUpdateUser', + field: 'Name', + }); + } + + if (!userData._id && !trim(userData.username)) { + throw new Meteor.Error('error-the-field-is-required', 'The field Username is required', { + method: 'insertOrUpdateUser', + field: 'Username', + }); + } + + let nameValidation; + + try { + nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`); + } catch (e) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + + if (userData.username && !nameValidation.test(userData.username)) { + throw new Meteor.Error('error-input-is-not-a-valid-field', `${_.escape(userData.username)} is not a valid username`, { + method: 'insertOrUpdateUser', + input: userData.username, + field: 'Username', + }); + } + + if (!userData._id && !userData.password && !userData.setRandomPassword) { + throw new Meteor.Error('error-the-field-is-required', 'The field Password is required', { + method: 'insertOrUpdateUser', + field: 'Password', + }); + } + + if (!userData._id) { + if (!(await checkUsernameAvailability(userData.username))) { + throw new Meteor.Error('error-field-unavailable', `${_.escape(userData.username)} is already in use :(`, { + method: 'insertOrUpdateUser', + field: userData.username, + }); + } + + if (userData.email && !(await checkEmailAvailability(userData.email))) { + throw new Meteor.Error('error-field-unavailable', `${_.escape(userData.email)} is already in use :(`, { + method: 'insertOrUpdateUser', + field: userData.email, + }); + } + } +} + +/** + * Validate permissions to edit user fields + * + * @param {string} userId + * @param {{ _id: string, roles?: string[], username?: string, name?: string, statusText?: string, email?: string, password?: string}} userData + */ +export async function validateUserEditing(userId, userData) { + const editingMyself = userData._id && userId === userData._id; + + const canEditOtherUserInfo = await hasPermissionAsync(userId, 'edit-other-user-info'); + const canEditOtherUserPassword = await hasPermissionAsync(userId, 'edit-other-user-password'); + const user = await Users.findOneById(userData._id); + + const isEditingUserRoles = (previousRoles, newRoles) => + typeof newRoles !== 'undefined' && !_.isEqual(_.sortBy(previousRoles), _.sortBy(newRoles)); + const isEditingField = (previousValue, newValue) => typeof newValue !== 'undefined' && newValue !== previousValue; + + if (isEditingUserRoles(user.roles, userData.roles) && !(await hasPermissionAsync(userId, 'assign-roles'))) { + throw new Meteor.Error('error-action-not-allowed', 'Assign roles is not allowed', { + method: 'insertOrUpdateUser', + action: 'Assign_role', + }); + } + + if (!settings.get('Accounts_AllowUserProfileChange') && !canEditOtherUserInfo && !canEditOtherUserPassword) { + throw new Meteor.Error('error-action-not-allowed', 'Edit user profile is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if ( + isEditingField(user.username, userData.username) && + !settings.get('Accounts_AllowUsernameChange') && + (!canEditOtherUserInfo || editingMyself) + ) { + throw new Meteor.Error('error-action-not-allowed', 'Edit username is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if ( + isEditingField(user.statusText, userData.statusText) && + !settings.get('Accounts_AllowUserStatusMessageChange') && + (!canEditOtherUserInfo || editingMyself) + ) { + throw new Meteor.Error('error-action-not-allowed', 'Edit user status is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if ( + isEditingField(user.name, userData.name) && + !settings.get('Accounts_AllowRealNameChange') && + (!canEditOtherUserInfo || editingMyself) + ) { + throw new Meteor.Error('error-action-not-allowed', 'Edit user real name is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if ( + user.emails?.[0] && + isEditingField(user.emails[0].address, userData.email) && + !settings.get('Accounts_AllowEmailChange') && + (!canEditOtherUserInfo || editingMyself) + ) { + throw new Meteor.Error('error-action-not-allowed', 'Edit user email is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } + + if (userData.password && !settings.get('Accounts_AllowPasswordChange') && (!canEditOtherUserPassword || editingMyself)) { + throw new Meteor.Error('error-action-not-allowed', 'Edit user password is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } +} + +const handleBio = (updateUser, bio) => { + if (bio && bio.trim()) { + if (bio.length > MAX_BIO_LENGTH) { + throw new Meteor.Error('error-bio-size-exceeded', `Bio size exceeds ${MAX_BIO_LENGTH} characters`, { + method: 'saveUserProfile', + }); + } + updateUser.$set = updateUser.$set || {}; + updateUser.$set.bio = bio; + } else { + updateUser.$unset = updateUser.$unset || {}; + updateUser.$unset.bio = 1; + } +}; + +const handleNickname = (updateUser, nickname) => { + if (nickname && nickname.trim()) { + if (nickname.length > MAX_NICKNAME_LENGTH) { + throw new Meteor.Error('error-nickname-size-exceeded', `Nickname size exceeds ${MAX_NICKNAME_LENGTH} characters`, { + method: 'saveUserProfile', + }); + } + updateUser.$set = updateUser.$set || {}; + updateUser.$set.nickname = nickname; + } else { + updateUser.$unset = updateUser.$unset || {}; + updateUser.$unset.nickname = 1; + } +}; + +const saveNewUser = async function (userData, sendPassword) { + await validateEmailDomain(userData.email); + + const roles = (!!userData.roles && userData.roles.length > 0 && userData.roles) || getNewUserRoles(); + const isGuest = roles && roles.length === 1 && roles.includes('guest'); + + // insert user + const createUser = { + username: userData.username, + password: userData.password, + joinDefaultChannels: userData.joinDefaultChannels, + isGuest, + globalRoles: roles, + skipNewUserRolesSetting: true, + }; + if (userData.email) { + createUser.email = userData.email; + } + + const _id = await Accounts.createUserAsync(createUser); + + const updateUser = { + $set: { + ...(typeof userData.name !== 'undefined' && { name: userData.name }), + settings: userData.settings || {}, + }, + }; + + if (typeof userData.requirePasswordChange !== 'undefined') { + updateUser.$set.requirePasswordChange = userData.requirePasswordChange; + } + + if (typeof userData.verified === 'boolean') { + updateUser.$set['emails.0.verified'] = userData.verified; + } + + handleBio(updateUser, userData.bio); + handleNickname(updateUser, userData.nickname); + + await Users.updateOne({ _id }, updateUser); + + if (userData.sendWelcomeEmail) { + await _sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData); + } + + if (sendPassword) { + await _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); + } + + userData._id = _id; + + if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) { + const gravatarUrl = Gravatar.url(userData.email, { + default: '404', + size: '200', + protocol: 'https', + }); + + try { + await setUserAvatar(userData, gravatarUrl, '', 'url'); + } catch (e) { + // Ignore this error for now, as it not being successful isn't bad + } + } + + void notifyOnUserChangeById({ clientAction: 'inserted', id: _id }); + + return _id; +}; + +export const saveUser = async function (userId, userData) { + const oldUserData = userData._id && (await Users.findOneById(userData._id)); + if (oldUserData && isUserFederated(oldUserData)) { + throw new Meteor.Error('Edit_Federated_User_Not_Allowed', 'Not possible to edit a federated user'); + } + + await validateUserData(userId, userData); + + await callbacks.run('beforeSaveUser', { + user: userData, + oldUser: oldUserData, + }); + + let sendPassword = false; + + if (userData.hasOwnProperty('setRandomPassword')) { + if (userData.setRandomPassword) { + userData.password = generatePassword(); + userData.requirePasswordChange = true; + sendPassword = true; + } + + delete userData.setRandomPassword; + } + + if (!userData._id) { + return saveNewUser(userData, sendPassword); + } + + await validateUserEditing(userId, userData); + + // update user + if (userData.hasOwnProperty('username') || userData.hasOwnProperty('name')) { + if ( + !(await saveUserIdentity({ + _id: userData._id, + username: userData.username, + name: userData.name, + updateUsernameInBackground: true, + })) + ) { + throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', { + method: 'saveUser', + }); + } + } + + if (typeof userData.statusText === 'string') { + await setStatusText(userData._id, userData.statusText); + } + + if (userData.email) { + const shouldSendVerificationEmailToUser = userData.verified !== true; + await setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser); + } + + if ( + userData.password && + userData.password.trim() && + (await hasPermissionAsync(userId, 'edit-other-user-password')) && + passwordPolicy.validate(userData.password) + ) { + await Accounts.setPasswordAsync(userData._id, userData.password.trim()); + } else { + sendPassword = false; + } + + const updateUser = { + $set: {}, + $unset: {}, + }; + + handleBio(updateUser, userData.bio); + handleNickname(updateUser, userData.nickname); + + if (userData.roles) { + updateUser.$set.roles = userData.roles; + } + if (userData.settings) { + updateUser.$set.settings = { preferences: userData.settings.preferences }; + } + + if (userData.language) { + updateUser.$set.language = userData.language; + } + + if (typeof userData.requirePasswordChange !== 'undefined') { + updateUser.$set.requirePasswordChange = userData.requirePasswordChange; + if (!userData.requirePasswordChange) { + updateUser.$unset.requirePasswordChangeReason = 1; + } + } + + if (typeof userData.verified === 'boolean') { + updateUser.$set['emails.0.verified'] = userData.verified; + } + + await Users.updateOne({ _id: userData._id }, updateUser); + + // App IPostUserUpdated event hook + const userUpdated = await Users.findOneById(userData._id); + + await callbacks.run('afterSaveUser', { + user: userUpdated, + oldUser: oldUserData, + }); + + await Apps.self?.triggerEvent(AppEvents.IPostUserUpdated, { + user: userUpdated, + previousUser: oldUserData, + performedBy: await safeGetMeteorUser(), + }); + + if (sendPassword) { + await _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); + } + + if (typeof userData.verified === 'boolean') { + delete userData.verified; + } + void notifyOnUserChange({ + clientAction: 'updated', + id: userData._id, + diff: { + ...userData, + emails: userUpdated.emails, + }, + }); + + return true; +}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/handleBio.ts b/apps/meteor/app/lib/server/functions/saveUser/handleBio.ts deleted file mode 100644 index 1d2f572f5cd9d..0000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser/handleBio.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MeteorError } from '@rocket.chat/core-services'; -import type { DeepPartial, DeepWritable, IUser } from '@rocket.chat/core-typings'; -import type { UpdateFilter } from 'mongodb'; - -import type { SaveUserData } from './saveUser'; - -const MAX_BIO_LENGTH = 260; - -export const handleBio = (updateUser: DeepWritable>>, bio: SaveUserData['bio']) => { - if (bio?.trim()) { - if (bio.length > MAX_BIO_LENGTH) { - throw new MeteorError('error-bio-size-exceeded', `Bio size exceeds ${MAX_BIO_LENGTH} characters`, { - method: 'saveUserProfile', - }); - } - updateUser.$set = updateUser.$set || {}; - updateUser.$set.bio = bio; - } else { - updateUser.$unset = updateUser.$unset || {}; - updateUser.$unset.bio = 1; - } -}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/handleNickname.ts b/apps/meteor/app/lib/server/functions/saveUser/handleNickname.ts deleted file mode 100644 index 4a37ec9e1518f..0000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser/handleNickname.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MeteorError } from '@rocket.chat/core-services'; -import type { DeepPartial, DeepWritable, IUser } from '@rocket.chat/core-typings'; -import type { UpdateFilter } from 'mongodb'; - -import type { SaveUserData } from './saveUser'; - -const MAX_NICKNAME_LENGTH = 120; - -export const handleNickname = (updateUser: DeepWritable>>, nickname: SaveUserData['nickname']) => { - if (nickname?.trim()) { - if (nickname.length > MAX_NICKNAME_LENGTH) { - throw new MeteorError('error-nickname-size-exceeded', `Nickname size exceeds ${MAX_NICKNAME_LENGTH} characters`, { - method: 'saveUserProfile', - }); - } - updateUser.$set = updateUser.$set || {}; - updateUser.$set.nickname = nickname; - } else { - updateUser.$unset = updateUser.$unset || {}; - updateUser.$unset.nickname = 1; - } -}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/index.ts b/apps/meteor/app/lib/server/functions/saveUser/index.ts deleted file mode 100644 index 3fd0668e6e47f..0000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { saveUser } from './saveUser'; -export { validateUserEditing } from './validateUserEditing'; diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts deleted file mode 100644 index 18e2858e81c09..0000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { DeepPartial, DeepWritable, IUser, RequiredField } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import Gravatar from 'gravatar'; -import type { UpdateFilter } from 'mongodb'; - -import { getNewUserRoles } from '../../../../../server/services/user/lib/getNewUserRoles'; -import { settings } from '../../../../settings/server'; -import { notifyOnUserChangeById } from '../../lib/notifyListener'; -import { validateEmailDomain } from '../../lib/validateEmailDomain'; -import { setUserAvatar } from '../setUserAvatar'; -import { handleBio } from './handleBio'; -import { handleNickname } from './handleNickname'; -import type { SaveUserData } from './saveUser'; -import { sendPasswordEmail, sendWelcomeEmail } from './sendUserEmail'; - -export const saveNewUser = async function (userData: SaveUserData, sendPassword: boolean) { - await validateEmailDomain(userData.email); - - const roles = (!!userData.roles && userData.roles.length > 0 && userData.roles) || getNewUserRoles(); - const isGuest = roles && roles.length === 1 && roles.includes('guest'); - - // insert user - const createUser: Record = { - username: userData.username, - password: userData.password, - joinDefaultChannels: userData.joinDefaultChannels, - isGuest, - globalRoles: roles, - skipNewUserRolesSetting: true, - }; - if (userData.email) { - createUser.email = userData.email; - } - - const _id = await Accounts.createUserAsync(createUser); - - const updateUser: RequiredField>>, '$set'> = { - $set: { - ...(typeof userData.name !== 'undefined' && { name: userData.name }), - settings: userData.settings || {}, - }, - }; - - if (typeof userData.requirePasswordChange !== 'undefined') { - updateUser.$set.requirePasswordChange = userData.requirePasswordChange; - } - - if (typeof userData.verified === 'boolean') { - updateUser.$set['emails.0.verified'] = userData.verified; - } - - handleBio(updateUser, userData.bio); - handleNickname(updateUser, userData.nickname); - - await Users.updateOne({ _id }, updateUser as UpdateFilter); - - if (userData.sendWelcomeEmail) { - await sendWelcomeEmail(userData); - } - - if (sendPassword) { - await sendPasswordEmail(userData); - } - - userData._id = _id; - - if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) { - const gravatarUrl = Gravatar.url(userData.email, { - default: '404', - size: '200', - protocol: 'https', - }); - - try { - await setUserAvatar({ ...userData, _id }, gravatarUrl, '', 'url'); - } catch (e) { - // Ignore this error for now, as it not being successful isn't bad - } - } - - void notifyOnUserChangeById({ clientAction: 'inserted', id: _id }); - - return _id; -}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts deleted file mode 100644 index 047a417e94a12..0000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; -import type { DeepWritable, DeepPartial } from '@rocket.chat/core-typings'; -import { isUserFederated, type IUser, type IRole, type IUserSettings, type RequiredField } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; -import type { UpdateFilter } from 'mongodb'; - -import { callbacks } from '../../../../../lib/callbacks'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { safeGetMeteorUser } from '../../../../utils/server/functions/safeGetMeteorUser'; -import { generatePassword } from '../../lib/generatePassword'; -import { notifyOnUserChange } from '../../lib/notifyListener'; -import { passwordPolicy } from '../../lib/passwordPolicy'; -import { saveUserIdentity } from '../saveUserIdentity'; -import { setEmail } from '../setEmail'; -import { setStatusText } from '../setStatusText'; -import { handleBio } from './handleBio'; -import { handleNickname } from './handleNickname'; -import { saveNewUser } from './saveNewUser'; -import { sendPasswordEmail } from './sendUserEmail'; -import { validateUserData } from './validateUserData'; -import { validateUserEditing } from './validateUserEditing'; - -export type SaveUserData = { - _id?: IUser['_id']; - setRandomPassword?: boolean; - - password?: string; - requirePasswordChange?: boolean; - - username?: string; - name?: string; - - statusText?: string; - email?: string; - verified?: boolean; - - bio?: string; - nickname?: string; - - roles?: IRole['_id'][]; - settings?: Partial; - language?: string; - - joinDefaultChannels?: boolean; - sendWelcomeEmail?: boolean; -}; - -export const saveUser = async function (userId: IUser['_id'], userData: SaveUserData) { - const oldUserData = userData._id && (await Users.findOneById(userData._id)); - if (oldUserData && isUserFederated(oldUserData)) { - throw new Meteor.Error('Edit_Federated_User_Not_Allowed', 'Not possible to edit a federated user'); - } - - await validateUserData(userId, userData); - - await callbacks.run('beforeSaveUser', { - user: userData, - oldUser: oldUserData, - }); - - let sendPassword = false; - - if (userData.hasOwnProperty('setRandomPassword')) { - if (userData.setRandomPassword) { - userData.password = generatePassword(); - userData.requirePasswordChange = true; - sendPassword = true; - } - - delete userData.setRandomPassword; - } - - if (!userData._id) { - return saveNewUser(userData, sendPassword); - } - - await validateUserEditing(userId, userData as RequiredField); - - // update user - if (userData.hasOwnProperty('username') || userData.hasOwnProperty('name')) { - if ( - !(await saveUserIdentity({ - _id: userData._id, - username: userData.username, - name: userData.name, - updateUsernameInBackground: true, - })) - ) { - throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', { - method: 'saveUser', - }); - } - } - - if (typeof userData.statusText === 'string') { - await setStatusText(userData._id, userData.statusText); - } - - if (userData.email) { - const shouldSendVerificationEmailToUser = userData.verified !== true; - await setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser); - } - - if ( - userData.password?.trim() && - (await hasPermissionAsync(userId, 'edit-other-user-password')) && - passwordPolicy.validate(userData.password) - ) { - await Accounts.setPasswordAsync(userData._id, userData.password.trim()); - } else { - sendPassword = false; - } - - const updateUser: RequiredField>>, '$set' | '$unset'> = { - $set: {}, - $unset: {}, - }; - - handleBio(updateUser, userData.bio); - handleNickname(updateUser, userData.nickname); - - if (userData.roles) { - updateUser.$set.roles = userData.roles; - } - if (userData.settings) { - updateUser.$set.settings = { preferences: userData.settings.preferences }; - } - - if (userData.language) { - updateUser.$set.language = userData.language; - } - - if (typeof userData.requirePasswordChange !== 'undefined') { - updateUser.$set.requirePasswordChange = userData.requirePasswordChange; - if (!userData.requirePasswordChange) { - updateUser.$unset.requirePasswordChangeReason = 1; - } - } - - if (typeof userData.verified === 'boolean') { - updateUser.$set['emails.0.verified'] = userData.verified; - } - - await Users.updateOne({ _id: userData._id }, updateUser as UpdateFilter); - - // App IPostUserUpdated event hook - const userUpdated = await Users.findOneById(userData._id); - - await callbacks.run('afterSaveUser', { - user: userUpdated, - oldUser: oldUserData, - }); - - await Apps.self?.triggerEvent(AppEvents.IPostUserUpdated, { - user: userUpdated, - previousUser: oldUserData, - performedBy: await safeGetMeteorUser(), - }); - - if (sendPassword) { - await sendPasswordEmail(userData); - } - - if (typeof userData.verified === 'boolean') { - delete userData.verified; - } - void notifyOnUserChange({ - clientAction: 'updated', - id: userData._id, - diff: { - ...userData, - emails: userUpdated?.emails, - }, - }); - - return true; -}; diff --git a/apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts b/apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts deleted file mode 100644 index babe985dbd4b8..0000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { MeteorError } from '@rocket.chat/core-services'; - -import * as Mailer from '../../../../mailer/server/api'; -import { settings } from '../../../../settings/server'; -import type { SaveUserData } from './saveUser'; - -let html = ''; -let passwordChangedHtml = ''; -Meteor.startup(() => { - Mailer.getTemplate('Accounts_UserAddedEmail_Email', (template) => { - html = template; - }); - - Mailer.getTemplate('Password_Changed_Email', (template) => { - passwordChangedHtml = template; - }); -}); - -export async function sendUserEmail(subject: string, html: string, userData: SaveUserData): Promise { - if (!userData.email) { - return; - } - - const email = { - to: userData.email, - from: settings.get('From_Email'), - subject, - html, - data: { - email: userData.email, - password: userData.password, - ...(typeof userData.name !== 'undefined' ? { name: userData.name } : {}), - }, - }; - - try { - await Mailer.send(email); - } catch (error) { - const errorMessage = typeof error === 'object' && error && 'message' in error ? error.message : ''; - - throw new MeteorError('error-email-send-failed', `Error trying to send email: ${errorMessage}`, { - function: 'RocketChat.saveUser', - message: errorMessage, - }); - } -} - -export async function sendWelcomeEmail(userData: SaveUserData) { - return sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData); -} - -export async function sendPasswordEmail(userData: SaveUserData) { - return sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); -} diff --git a/apps/meteor/app/lib/server/functions/saveUser/validateUserData.ts b/apps/meteor/app/lib/server/functions/saveUser/validateUserData.ts deleted file mode 100644 index 52652a6c47b40..0000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser/validateUserData.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { MeteorError } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; -import { makeFunction } from '@rocket.chat/patch-injection'; -import escape from 'lodash.escape'; - -import { trim } from '../../../../../lib/utils/stringUtils'; -import { getRoleIds } from '../../../../authorization/server/functions/getRoles'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { settings } from '../../../../settings/server'; -import { checkEmailAvailability } from '../checkEmailAvailability'; -import { checkUsernameAvailability } from '../checkUsernameAvailability'; -import type { SaveUserData } from './saveUser'; - -export const validateUserData = makeFunction(async (userId: IUser['_id'], userData: SaveUserData): Promise => { - const existingRoles = await getRoleIds(); - - if (userData.verified && userData._id && userId === userData._id) { - throw new MeteorError('error-action-not-allowed', 'Editing email verification is not allowed', { - method: 'insertOrUpdateUser', - action: 'Editing_user', - }); - } - - if (userData._id && userId !== userData._id && !(await hasPermissionAsync(userId, 'edit-other-user-info'))) { - throw new MeteorError('error-action-not-allowed', 'Editing user is not allowed', { - method: 'insertOrUpdateUser', - action: 'Editing_user', - }); - } - - if (!userData._id && !(await hasPermissionAsync(userId, 'create-user'))) { - throw new MeteorError('error-action-not-allowed', 'Adding user is not allowed', { - method: 'insertOrUpdateUser', - action: 'Adding_user', - }); - } - - if (userData.roles) { - const newRoles = userData.roles.filter((roleId) => !existingRoles.includes(roleId)); - if (newRoles.length > 0) { - throw new MeteorError('error-action-not-allowed', 'The field Roles consist invalid role id', { - method: 'insertOrUpdateUser', - action: 'Assign_role', - }); - } - } - - if (userData.roles?.includes('admin') && !(await hasPermissionAsync(userId, 'assign-admin-role'))) { - throw new MeteorError('error-action-not-allowed', 'Assigning admin is not allowed', { - method: 'insertOrUpdateUser', - action: 'Assign_admin', - }); - } - - if (settings.get('Accounts_RequireNameForSignUp') && !userData._id && !trim(userData.name)) { - throw new MeteorError('error-the-field-is-required', 'The field Name is required', { - method: 'insertOrUpdateUser', - field: 'Name', - }); - } - - if (!userData._id && !trim(userData.username)) { - throw new MeteorError('error-the-field-is-required', 'The field Username is required', { - method: 'insertOrUpdateUser', - field: 'Username', - }); - } - - let nameValidation; - - try { - nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`); - } catch (e) { - nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); - } - - if (userData.username && !nameValidation.test(userData.username)) { - throw new MeteorError('error-input-is-not-a-valid-field', `${escape(userData.username)} is not a valid username`, { - method: 'insertOrUpdateUser', - input: userData.username, - field: 'Username', - }); - } - - if (!userData._id && !userData.password && !userData.setRandomPassword) { - throw new MeteorError('error-the-field-is-required', 'The field Password is required', { - method: 'insertOrUpdateUser', - field: 'Password', - }); - } - - if (!userData._id) { - if (userData.username && !(await checkUsernameAvailability(userData.username))) { - throw new MeteorError('error-field-unavailable', `${escape(userData.username)} is already in use :(`, { - method: 'insertOrUpdateUser', - field: userData.username, - }); - } - - if (userData.email && !(await checkEmailAvailability(userData.email))) { - throw new MeteorError('error-field-unavailable', `${escape(userData.email)} is already in use :(`, { - method: 'insertOrUpdateUser', - field: userData.email, - }); - } - } -}); diff --git a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts deleted file mode 100644 index 78b8910361cdd..0000000000000 --- a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { MeteorError } from '@rocket.chat/core-services'; -import type { IUser, RequiredField } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; - -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { settings } from '../../../../settings/server'; -import type { SaveUserData } from './saveUser'; - -const isEditingUserRoles = (previousRoles: IUser['roles'], newRoles?: IUser['roles']) => - newRoles !== undefined && - (newRoles.some((item) => !previousRoles.includes(item)) || previousRoles.some((item) => !newRoles.includes(item))); -const isEditingField = (previousValue?: string, newValue?: string) => typeof newValue !== 'undefined' && newValue !== previousValue; - -/** - * Validate permissions to edit user fields - * - * @param {string} userId - * @param {{ _id: string, roles?: string[], username?: string, name?: string, statusText?: string, email?: string, password?: string}} userData - */ -export async function validateUserEditing(userId: IUser['_id'], userData: RequiredField): Promise { - const editingMyself = userData._id && userId === userData._id; - - const canEditOtherUserInfo = await hasPermissionAsync(userId, 'edit-other-user-info'); - const canEditOtherUserPassword = await hasPermissionAsync(userId, 'edit-other-user-password'); - const user = await Users.findOneById(userData._id); - - if (!user) { - throw new MeteorError('error-invalid-user', 'Invalid user'); - } - - if (isEditingUserRoles(user.roles, userData.roles) && !(await hasPermissionAsync(userId, 'assign-roles'))) { - throw new MeteorError('error-action-not-allowed', 'Assign roles is not allowed', { - method: 'insertOrUpdateUser', - action: 'Assign_role', - }); - } - - if (!settings.get('Accounts_AllowUserProfileChange') && !canEditOtherUserInfo && !canEditOtherUserPassword) { - throw new MeteorError('error-action-not-allowed', 'Edit user profile is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if ( - isEditingField(user.username, userData.username) && - !settings.get('Accounts_AllowUsernameChange') && - (!canEditOtherUserInfo || editingMyself) - ) { - throw new MeteorError('error-action-not-allowed', 'Edit username is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if ( - isEditingField(user.statusText, userData.statusText) && - !settings.get('Accounts_AllowUserStatusMessageChange') && - (!canEditOtherUserInfo || editingMyself) - ) { - throw new MeteorError('error-action-not-allowed', 'Edit user status is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if ( - isEditingField(user.name, userData.name) && - !settings.get('Accounts_AllowRealNameChange') && - (!canEditOtherUserInfo || editingMyself) - ) { - throw new MeteorError('error-action-not-allowed', 'Edit user real name is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if ( - user.emails?.[0] && - isEditingField(user.emails[0].address, userData.email) && - !settings.get('Accounts_AllowEmailChange') && - (!canEditOtherUserInfo || editingMyself) - ) { - throw new MeteorError('error-action-not-allowed', 'Edit user email is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } - - if (userData.password && !settings.get('Accounts_AllowPasswordChange') && (!canEditOtherUserPassword || editingMyself)) { - throw new MeteorError('error-action-not-allowed', 'Edit user password is not allowed', { - method: 'insertOrUpdateUser', - action: 'Update_user', - }); - } -} diff --git a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts index ac204af51439c..feb26ce6a1b07 100644 --- a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts +++ b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts @@ -8,14 +8,14 @@ const getName = (members: IUser[]): string => members.map(({ username }) => user async function getUsersWhoAreInTheSameGroupDMsAs(user: IUser) { // add all users to single array so we can fetch details from them all at once - if ((await Rooms.countGroupDMsByUids([user._id])) === 0) { + const rooms = Rooms.findGroupDMsByUids([user._id], { projection: { uids: 1 } }); + if ((await rooms.count()) === 0) { return; } const userIds = new Set(); const users = new Map(); - const rooms = Rooms.findGroupDMsByUids([user._id], { projection: { uids: 1 } }); await rooms.forEach((room) => { if (!room.uids) { return; diff --git a/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js b/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js index 075dd055cad04..f9af0911bcc68 100644 --- a/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js +++ b/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js @@ -127,13 +127,10 @@ export class POP3Helper { start() { this.log('POP3 started'); - this.running = setInterval( - () => { - // get new emails and process - this.POP3 = new POP3Intercepter(); - }, - Math.max(this.frequency * 60 * 1000, 2 * 60 * 1000), - ); + this.running = setInterval(() => { + // get new emails and process + this.POP3 = new POP3Intercepter(); + }, Math.max(this.frequency * 60 * 1000, 2 * 60 * 1000)); } log(...args) { diff --git a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts index e729b9652cb67..fdb99c83207c9 100644 --- a/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts @@ -2,17 +2,26 @@ import type { IMessage, IRoom, IUser, RoomType } from '@rocket.chat/core-typings import { isEditedMessage } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; import { Subscriptions, Rooms } from '@rocket.chat/models'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; -import { messageContainsHighlight } from '../functions/notifications/messageContainsHighlight'; import { notifyOnSubscriptionChanged, notifyOnSubscriptionChangedByRoomIdAndUserId, notifyOnSubscriptionChangedByRoomIdAndUserIds, } from './notifyListener'; +function messageContainsHighlight(message: IMessage, highlights: string[]): boolean { + if (!highlights || highlights.length === 0) return false; + + return highlights.some((highlight: string) => { + const regexp = new RegExp(escapeRegExp(highlight), 'i'); + return regexp.test(message.msg); + }); +} + export async function getMentions(message: IMessage): Promise<{ toAll: boolean; toHere: boolean; mentionIds: string[] }> { const { mentions, diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts index fe65ebe5a5d24..94c25f4762220 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts @@ -17,10 +17,9 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Notification } from '../../../notification-queue/server/NotificationQueue'; import { settings } from '../../../settings/server'; -import { parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../functions/notifications'; +import { messageContainsHighlight, parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../functions/notifications'; import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop'; import { getEmailData, shouldNotifyEmail } from '../functions/notifications/email'; -import { messageContainsHighlight } from '../functions/notifications/messageContainsHighlight'; import { getPushData, shouldNotifyMobile } from '../functions/notifications/mobile'; import { getMentions } from './notifyUsersOnMessage'; diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index 5fcdc09e0eeba..8f1f4c5861418 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -89,7 +89,7 @@ Meteor.methods({ options, showThreadMessages, inclusive, - ).toArray() + ).toArray() : await Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( rid, oldest, @@ -98,7 +98,7 @@ Meteor.methods({ options, showThreadMessages, inclusive, - ).toArray(); + ).toArray(); const messages = await normalizeMessagesForUser(records, fromUserId); diff --git a/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts b/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts index 3c5f07f624b39..122b11172d57a 100644 --- a/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts +++ b/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts @@ -19,14 +19,12 @@ Meteor.methods({ check(userData, Object); - const userId = Meteor.userId(); - - if (!userId) { + if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'insertOrUpdateUser', }); } - return saveUser(userId, userData); + return saveUser(Meteor.userId(), userData); }), }); diff --git a/apps/meteor/app/lib/server/methods/leaveRoom.ts b/apps/meteor/app/lib/server/methods/leaveRoom.ts index 8ded8b56ce2b0..4fc85b35fd05c 100644 --- a/apps/meteor/app/lib/server/methods/leaveRoom.ts +++ b/apps/meteor/app/lib/server/methods/leaveRoom.ts @@ -45,7 +45,8 @@ export const leaveRoomMethod = async (user: IUser, rid: string): Promise = // If user is room owner, check if there are other owners. If there isn't anyone else, warn user to set a new owner. if (await hasRoleAsync(user._id, 'owner', room._id)) { - const numOwners = await Roles.countUsersInRole('owner', room._id); + const cursor = await Roles.findUsersInRole('owner', room._id); + const numOwners = await cursor.count(); if (numOwners === 1) { throw new Meteor.Error('error-you-are-last-owner', 'You are the last owner. Please set new owner before leaving the room.', { method: 'leaveRoom', diff --git a/apps/meteor/app/livechat/client/collections/LivechatInquiry.js b/apps/meteor/app/livechat/client/collections/LivechatInquiry.js new file mode 100644 index 0000000000000..c43a9cb31ca5a --- /dev/null +++ b/apps/meteor/app/livechat/client/collections/LivechatInquiry.js @@ -0,0 +1,3 @@ +import { Mongo } from 'meteor/mongo'; + +export const LivechatInquiry = new Mongo.Collection(null); diff --git a/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts b/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts deleted file mode 100644 index 16b9533d1649e..0000000000000 --- a/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; -import { Mongo } from 'meteor/mongo'; - -export const LivechatInquiry = new Mongo.Collection(null); diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index c6a671e2883d7..5ba2cf0d97919 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -30,12 +30,12 @@ const events = { const invalidateRoomQueries = async (rid: string) => { await queryClient.invalidateQueries(['rooms', { reference: rid, type: 'l' }]); - queryClient.removeQueries(['rooms', rid]); - queryClient.removeQueries(['/v1/rooms.info', rid]); + await queryClient.removeQueries(['rooms', rid]); + await queryClient.removeQueries(['/v1/rooms.info', rid]); }; const removeInquiry = async (inquiry: ILivechatInquiryRecord) => { - LivechatInquiry.remove(inquiry._id); + await LivechatInquiry.remove(inquiry._id); return queryClient.invalidateQueries(['rooms', { reference: inquiry.rid, type: 'l' }]); }; diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 966b7e6d6af4a..e56feeac2fa3c 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -14,13 +14,8 @@ import { findDepartmentAgents, findArchivedDepartments, } from '../../../server/api/lib/departments'; -import { - saveDepartment, - archiveDepartment, - unarchiveDepartment, - saveDepartmentAgents, - removeDepartment, -} from '../../../server/lib/departmentsLib'; +import { DepartmentHelper } from '../../../server/lib/Departments'; +import { Livechat as LivechatTs } from '../../../server/lib/LivechatTyped'; import { isDepartmentCreationAvailable } from '../../../server/lib/isDepartmentCreationAvailable'; API.v1.addRoute( @@ -67,7 +62,7 @@ API.v1.addRoute( const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; const { departmentUnit } = this.bodyParams; - const department = await saveDepartment( + const department = await LivechatTs.saveDepartment( this.userId, null, this.bodyParams.department as ILivechatDepartment, @@ -136,7 +131,7 @@ API.v1.addRoute( } const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {}; - await saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); + await LivechatTs.saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); return API.v1.success({ department: await LivechatDepartment.findOneById(_id), @@ -148,7 +143,7 @@ API.v1.addRoute( _id: String, }); - await removeDepartment(this.urlParams._id); + await DepartmentHelper.removeDepartment(this.urlParams._id); return API.v1.success(); }, @@ -196,7 +191,7 @@ API.v1.addRoute( }, { async post() { - await archiveDepartment(this.urlParams._id); + await LivechatTs.archiveDepartment(this.urlParams._id); return API.v1.success(); }, @@ -211,7 +206,7 @@ API.v1.addRoute( }, { async post() { - await unarchiveDepartment(this.urlParams._id); + await LivechatTs.unarchiveDepartment(this.urlParams._id); return API.v1.success(); }, }, @@ -276,7 +271,7 @@ API.v1.addRoute( remove: Array, }), ); - await saveDepartmentAgents(this.urlParams._id, this.bodyParams); + await LivechatTs.saveDepartmentAgents(this.urlParams._id, this.bodyParams); return API.v1.success(); }, diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 2f13fd01f2215..15f08cdc1e83b 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -1,11 +1,11 @@ import { OmnichannelIntegration } from '@rocket.chat/core-services'; import type { ILivechatVisitor, + IOmnichannelRoom, IUpload, MessageAttachment, ServiceData, FileAttachmentProps, - IOmnichannelRoomInfo, } from '@rocket.chat/core-typings'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; @@ -20,8 +20,8 @@ import { FileUpload } from '../../../../file-upload/server'; import { checkUrlForSsrf } from '../../../../lib/server/functions/checkUrlForSsrf'; import { settings } from '../../../../settings/server'; import { setCustomField } from '../../../server/api/lib/customFields'; +import type { ILivechatMessage } from '../../../server/lib/LivechatTyped'; import { Livechat as LivechatTyped } from '../../../server/lib/LivechatTyped'; -import type { ILivechatMessage } from '../../../server/lib/localTypes'; const logger = new Logger('SMS'); @@ -122,7 +122,10 @@ API.v1.addRoute('livechat/sms-incoming/:service', { return API.v1.success(SMSService.error(new Error('Invalid visitor'))); } - const roomInfo: IOmnichannelRoomInfo = { + const roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + } = { sms: { from: sms.to, }, @@ -241,7 +244,10 @@ API.v1.addRoute('livechat/sms-incoming/:service', { const sendMessage: { guest: ILivechatVisitor; message: ILivechatMessage; - roomInfo: IOmnichannelRoomInfo; + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }; } = { guest: visitor, roomInfo, diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts index 6c583e1b24db2..abc6163fe9c9a 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.ts +++ b/apps/meteor/app/livechat/server/api/v1/agent.ts @@ -7,7 +7,6 @@ import { API } from '../../../../api/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { RoutingManager } from '../../lib/RoutingManager'; -import { getRequiredDepartment } from '../../lib/departmentsLib'; import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; API.v1.addRoute('livechat/agent.info/:rid/:token', { @@ -44,7 +43,7 @@ API.v1.addRoute( let { department } = this.queryParams; if (!department) { - const requireDepartment = await getRequiredDepartment(); + const requireDepartment = await LivechatTyped.getRequiredDepartment(); if (requireDepartment) { department = requireDepartment._id; } diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts index f6fb29d174423..a367df7a04adc 100644 --- a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts +++ b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts @@ -2,7 +2,7 @@ import { isPOSTLivechatOfflineMessageParams } from '@rocket.chat/rest-typings'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; -import { sendOfflineMessage } from '../../lib/messages'; +import { Livechat } from '../../lib/LivechatTyped'; API.v1.addRoute( 'livechat/offline.message', @@ -14,7 +14,7 @@ API.v1.addRoute( async post() { const { name, email, message, department, host } = this.bodyParams; try { - await sendOfflineMessage({ name, email, message, department, host }); + await Livechat.sendOfflineMessage({ name, email, message, department, host }); return API.v1.success({ message: i18n.t('Livechat_offline_message_sent') }); } catch (e) { return API.v1.failure(i18n.t('Error_sending_livechat_offline_message')); diff --git a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts index a97b5278462d6..2688ad673af00 100644 --- a/apps/meteor/app/livechat/server/api/v1/pageVisited.ts +++ b/apps/meteor/app/livechat/server/api/v1/pageVisited.ts @@ -2,7 +2,7 @@ import type { IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; import { isPOSTLivechatPageVisitedParams } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; -import { savePageHistory } from '../../lib/tracking'; +import { Livechat } from '../../lib/LivechatTyped'; API.v1.addRoute( 'livechat/page.visited', @@ -11,7 +11,7 @@ API.v1.addRoute( async post() { const { token, rid, pageInfo } = this.bodyParams; - const message = await savePageHistory(token, rid, pageInfo); + const message = await Livechat.savePageHistory(token, rid, pageInfo); if (!message) { return API.v1.success(); } diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index b46a8c3e06630..7d52617e074ab 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -1,5 +1,5 @@ import { Omnichannel } from '@rocket.chat/core-services'; -import type { ILivechatAgent, IOmnichannelInquiryExtraData, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings'; import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings'; import { LivechatVisitors, Users, LivechatRooms, Messages } from '@rocket.chat/models'; import { @@ -23,8 +23,8 @@ import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivechatRoom'; import { settings as rcSettings } from '../../../../settings/server'; import { normalizeTransferredByData } from '../../lib/Helper'; +import type { CloseRoomParams } from '../../lib/LivechatTyped'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; -import type { CloseRoomParams } from '../../lib/localTypes'; import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); @@ -80,12 +80,7 @@ API.v1.addRoute( }, }; - const newRoom = await LivechatTyped.createRoom({ - visitor: guest, - roomInfo, - agent, - extraData: extraParams as IOmnichannelInquiryExtraData, - }); + const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams }); return API.v1.success({ room: newRoom, diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index 752bce901d314..2c718c058b004 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -31,7 +31,7 @@ export const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, a const department = room.departmentId ? await LivechatDepartment.findOneById>(room.departmentId, { projection: { businessHourId: 1 }, - }) + }) : null; if (department?.businessHourId) { const businessHour = await LivechatBusinessHours.findOneById(department.businessHourId); diff --git a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts index f0c445a78a788..f6a35f4dd7f9f 100644 --- a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts +++ b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts @@ -3,7 +3,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; -import type { CloseRoomParams } from '../lib/localTypes'; +import type { CloseRoomParams } from '../lib/LivechatTyped'; import { sendTranscript } from '../lib/sendTranscript'; type LivechatCloseCallbackParams = { diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts new file mode 100644 index 0000000000000..3dfa01e4f6b63 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/Departments.ts @@ -0,0 +1,69 @@ +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import { Logger } from '@rocket.chat/logger'; +import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatDepartmentAgentChanged } from '../../../lib/server/lib/notifyListener'; + +class DepartmentHelperClass { + logger = new Logger('Omnichannel:DepartmentHelper'); + + async removeDepartment(departmentId: string) { + this.logger.debug(`Removing department: ${departmentId}`); + + const department = await LivechatDepartment.findOneById>(departmentId, { + projection: { _id: 1, businessHourId: 1 }, + }); + if (!department) { + throw new Error('error-department-not-found'); + } + + const { _id } = department; + + const ret = await LivechatDepartment.removeById(_id); + if (ret.acknowledged !== true) { + throw new Error('error-failed-to-delete-department'); + } + + const removedAgents = await LivechatDepartmentAgents.findByDepartmentId(department._id, { projection: { agentId: 1 } }).toArray(); + + this.logger.debug( + `Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`, + ); + + const removeByDept = LivechatDepartmentAgents.removeByDepartmentId(_id); + + const promiseResponses = await Promise.allSettled([ + removeByDept, + LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id), + LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id), + ]); + + promiseResponses.forEach((response, index) => { + if (response.status === 'rejected') { + this.logger.error(`Error while performing post-department-removal actions: ${_id}. Action No: ${index}. Error:`, response.reason); + } + }); + + const { deletedCount } = await removeByDept; + + if (deletedCount > 0) { + removedAgents.forEach(({ _id: docId, agentId }) => { + void notifyOnLivechatDepartmentAgentChanged( + { + _id: docId, + agentId, + departmentId: _id, + }, + 'removed', + ); + }); + } + + await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); + + return ret; + } +} + +export const DepartmentHelper = new DepartmentHelperClass(); diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index cf11c8d12b2a8..a050e06c19433 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -13,9 +13,6 @@ import type { TransferByData, ILivechatAgent, ILivechatDepartment, - IOmnichannelRoomInfo, - IOmnichannelInquiryExtraData, - IOmnichannelRoomExtraData, } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus, OmnichannelSourceType, DEFAULT_SLA_CONFIG, UserStatus } from '@rocket.chat/core-typings'; import { LivechatPriorityWeight } from '@rocket.chat/core-typings/src/ILivechatPriority'; @@ -50,7 +47,6 @@ import { settings } from '../../../settings/server'; import { Livechat as LivechatTyped } from './LivechatTyped'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; import { RoutingManager } from './RoutingManager'; -import { getOnlineAgents } from './getOnlineAgents'; const logger = new Logger('LivechatHelper'); export const allowAgentSkipQueue = (agent: SelectedAgent) => { @@ -63,12 +59,18 @@ export const allowAgentSkipQueue = (agent: SelectedAgent) => { return hasRoleAsync(agent.agentId, 'bot'); }; -export const createLivechatRoom = async ( +export const createLivechatRoom = async < + E extends Record & { + sla?: string; + customFields?: Record; + source?: OmnichannelSourceType; + }, +>( rid: string, name: string, guest: ILivechatVisitor, - roomInfo: IOmnichannelRoomInfo = {}, - extraData?: IOmnichannelRoomExtraData, + roomInfo: Partial = {}, + extraData?: E, ) => { check(rid, String); check(name, String); @@ -160,7 +162,7 @@ export const createLivechatInquiry = async ({ guest?: Pick; message?: string; initialStatus?: LivechatInquiryStatus; - extraData?: IOmnichannelInquiryExtraData; + extraData?: Pick; }) => { check(rid, String); check(name, String); @@ -401,12 +403,13 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age await saveQueueInquiry(inquiry); // Alert only the online agents of the queued request - const onlineAgents = await getOnlineAgents(department, agent); + const onlineAgents = await LivechatTyped.getOnlineAgents(department, agent); if (!onlineAgents) { logger.debug('Cannot notify agents of queued inquiry. No online agents found'); return; } + logger.debug(`Notifying ${await onlineAgents.count()} agents of new inquiry`); const notificationUserName = v && (v.name || v.username); for await (const agent of onlineAgents) { diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index c522218f283e8..e521ac98fe711 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,9 +1,13 @@ +import dns from 'dns'; +import * as util from 'util'; + import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message, VideoConf, api, Omnichannel } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo, IUser, + MessageTypesValues, ILivechatVisitor, SelectedAgent, ILivechatAgent, @@ -11,12 +15,14 @@ import type { ILivechatDepartment, AtLeast, TransferData, + MessageAttachment, + IMessageInbox, IOmnichannelAgent, + ILivechatDepartmentAgents, + LivechatDepartmentDTO, ILivechatInquiryRecord, ILivechatContact, ILivechatContactChannel, - IOmnichannelRoomInfo, - IOmnichannelRoomExtraData, } from '@rocket.chat/core-typings'; import { OmnichannelSourceType, ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; @@ -37,7 +43,7 @@ import { import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { Filter, ClientSession, MongoError } from 'mongodb'; +import type { Filter, FindCursor, ClientSession, MongoError } from 'mongodb'; import UAParser from 'ua-parser-js'; import { callbacks } from '../../../../lib/callbacks'; @@ -59,9 +65,11 @@ import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByToken, notifyOnUserChange, + notifyOnLivechatDepartmentAgentChangedByDepartmentId, notifyOnSubscriptionChangedByRoomId, notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; +import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; @@ -69,8 +77,8 @@ import { createContact, createContactFromVisitor, isSingleContactEnabled } from import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; -import { getRequiredDepartment } from './departmentsLib'; -import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor, ILivechatMessage } from './localTypes'; +import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable'; +import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes'; import { parseTranscriptRequest } from './parseTranscriptRequest'; type RegisterGuestType = Partial> & { @@ -80,10 +88,44 @@ type RegisterGuestType = Partial = { [K in keyof T]?: T[K]; }; +type PageInfo = { title: string; location: { href: string }; change: string }; + type ICRMData = { _id: string; label?: string; @@ -111,6 +153,8 @@ const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomP const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor => (params as CloseRoomParamsByVisitor).visitor !== undefined; +const dnsResolveMx = util.promisify(dns.resolveMx); + class LivechatClass { logger: Logger; @@ -132,7 +176,7 @@ class LivechatClass { Livechat.logger.debug(`Fetching online bot agents for department ${department}`); const botAgents = await Livechat.getBotAgents(department); if (botAgents) { - const onlineBots = await Livechat.countBotAgents(department); + const onlineBots = await botAgents.count(); this.logger.debug(`Found ${onlineBots} online`); if (onlineBots > 0) { return true; @@ -145,6 +189,27 @@ class LivechatClass { return agentsOnline; } + async getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { + if (agent?.agentId) { + return Users.findOnlineAgents(agent.agentId); + } + + if (department) { + const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); + if (!departmentAgents) { + return; + } + + const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); + if (!agentIds.length) { + return; + } + + return Users.findByIds([...new Set(agentIds)]); + } + return Users.findOnlineAgents(); + } + async closeRoom(params: CloseRoomParams, attempts = 2): Promise { let newRoom: IOmnichannelRoom; let chatCloser: ChatCloser; @@ -332,6 +397,24 @@ class LivechatClass { return { room: newRoom, closedBy: closeData.closedBy, removedInquiry: inquiry }; } + async getRequiredDepartment(onlineRequired = true) { + const departments = LivechatDepartment.findEnabledWithAgents(); + + for await (const dept of departments) { + if (!dept.showOnRegistration) { + continue; + } + if (!onlineRequired) { + return dept; + } + + const onlineAgents = await LivechatDepartmentAgents.getOnlineForDepartment(dept._id); + if (onlineAgents && (await onlineAgents.count())) { + return dept; + } + } + } + async createRoom({ visitor, message, @@ -343,9 +426,12 @@ class LivechatClass { visitor: ILivechatVisitor; message?: string; rid?: string; - roomInfo: IOmnichannelRoomInfo; + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }; agent?: SelectedAgent; - extraData?: IOmnichannelRoomExtraData; + extraData?: Record; }) { if (!settings.get('Livechat_enabled')) { throw new Meteor.Error('error-omnichannel-is-disabled'); @@ -354,7 +440,7 @@ class LivechatClass { const defaultAgent = await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, visitor); // if no department selected verify if there is at least one active and pick the first if (!defaultAgent && !visitor.department) { - const department = await getRequiredDepartment(); + const department = await this.getRequiredDepartment(); Livechat.logger.debug(`No department or default agent selected for ${visitor._id}`); if (department) { @@ -431,12 +517,21 @@ class LivechatClass { return room; } - async getRoom( + async getRoom< + E extends Record & { + sla?: string; + customFields?: Record; + source?: OmnichannelSourceType; + }, + >( guest: ILivechatVisitor, message: Pick, - roomInfo: IOmnichannelRoomInfo, + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }, agent?: SelectedAgent, - extraData?: IOmnichannelRoomExtraData, + extraData?: E, ) { if (!settings.get('Livechat_enabled')) { throw new Meteor.Error('error-omnichannel-is-disabled'); @@ -487,6 +582,30 @@ class LivechatClass { return Users.checkOnlineAgents(); } + async setDepartmentForGuest({ token, department }: { token: string; department: string }) { + check(token, String); + check(department, String); + + Livechat.logger.debug(`Switching departments for user with token ${token} (to ${department})`); + + const updateUser = { + $set: { + department, + }, + }; + + const dep = await LivechatDepartment.findOneById>(department, { projection: { _id: 1 } }); + if (!dep) { + throw new Meteor.Error('invalid-department', 'Provided department does not exists'); + } + + const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + if (!visitor) { + throw new Meteor.Error('invalid-token', 'Provided token is invalid'); + } + await LivechatVisitors.updateById(visitor._id, updateUser); + } + async removeRoom(rid: string) { Livechat.logger.debug(`Deleting room ${rid}`); check(rid, String); @@ -632,14 +751,6 @@ class LivechatClass { return Users.findBotAgents(); } - private async countBotAgents(department?: string) { - if (department) { - return LivechatDepartmentAgents.countBotsForDepartment(department); - } - - return Users.countBotAgents(); - } - private async resolveChatTags( room: IOmnichannelRoom, options: CloseRoomParams['options'] = {}, @@ -704,6 +815,16 @@ class LivechatClass { }; } + private async sendEmail(from: string, to: string, replyTo: string, subject: string, html: string): Promise { + await Mailer.send({ + to, + from, + replyTo, + subject, + html, + }); + } + async sendRequest( postData: { type: string; @@ -842,6 +963,57 @@ class LivechatClass { }); } + async getRoomMessages({ rid }: { rid: string }) { + const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); + if (room?.t !== 'l') { + throw new Meteor.Error('invalid-room'); + } + + const ignoredMessageTypes: MessageTypesValues[] = [ + 'livechat_navigation_history', + 'livechat_transcript_history', + 'command', + 'livechat-close', + 'livechat-started', + 'livechat_video_call', + ]; + + return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { + sort: { ts: 1 }, + }); + } + + async archiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById>(_id, { + projection: { _id: 1, businessHourId: 1 }, + }); + + if (!department) { + throw new Error('department-not-found'); + } + + await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + + await callbacks.run('livechat.afterDepartmentArchived', department); + } + + async unarchiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found'); + } + + // TODO: these kind of actions should be on events instead of here + await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + + return true; + } + async updateMessage({ guest, message }: { guest: ILivechatVisitor; message: AtLeast }) { check(message, Match.ObjectIncluding({ _id: String })); @@ -977,6 +1149,69 @@ class LivechatClass { return rcSettings; } + async sendOfflineMessage(data: OfflineMessageData) { + if (!settings.get('Livechat_display_offline_form')) { + throw new Error('error-offline-form-disabled'); + } + + const { message, name, email, department, host } = data; + + if (!email) { + throw new Error('error-invalid-email'); + } + + const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); + + let html = '

New livechat message

'; + if (host && host !== '') { + html = html.concat(`

Sent from: ${host}

`); + } + html = html.concat(` +

Visitor name: ${name}

+

Visitor email: ${email}

+

Message:
${emailMessage}

`); + + const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); + + let from: string; + if (fromEmail) { + from = fromEmail[0]; + } else { + from = settings.get('From_Email'); + } + + if (settings.get('Livechat_validate_offline_email')) { + const emailDomain = email.substr(email.lastIndexOf('@') + 1); + + try { + await dnsResolveMx(emailDomain); + } catch (e) { + throw new Meteor.Error('error-invalid-email-address'); + } + } + + // TODO Block offline form if Livechat_offline_email is undefined + // (it does not make sense to have an offline form that does nothing) + // `this.sendEmail` will throw an error if the email is invalid + // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client + let emailTo = settings.get('Livechat_offline_email'); + if (department && department !== '') { + const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { email: 1 } }); + if (dep) { + emailTo = dep.email || emailTo; + } + } + + const fromText = `${name} - ${email} <${from}>`; + const replyTo = `${name} <${email}>`; + const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; + await this.sendEmail(fromText, emailTo, replyTo, subject, html); + + setImmediate(() => { + void callbacks.run('livechat.offlineMessage', data); + }); + } + async sendMessage({ guest, message, @@ -985,7 +1220,10 @@ class LivechatClass { }: { guest: ILivechatVisitor; message: ILivechatMessage; - roomInfo: IOmnichannelRoomInfo; + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }; agent?: SelectedAgent; }) { const { room, newRoom } = await this.getRoom(guest, message, roomInfo, agent); @@ -1270,6 +1508,53 @@ class LivechatClass { return true; } + async savePageHistory(token: string, roomId: string | undefined, pageInfo: PageInfo) { + this.logger.debug({ + msg: `Saving page movement history for visitor with token ${token}`, + pageInfo, + roomId, + }); + + if (pageInfo.change !== settings.get('Livechat_history_monitor_type')) { + return; + } + const user = await Users.findOneById('rocket.cat'); + + if (!user) { + throw new Error('error-invalid-user'); + } + + const pageTitle = pageInfo.title; + const pageUrl = pageInfo.location.href; + const extraData: { + navigation: { + page: PageInfo; + token: string; + }; + expireAt?: number; + _hidden?: boolean; + } = { + navigation: { + page: pageInfo, + token, + }, + }; + + if (!roomId) { + this.logger.warn(`Saving page history without room id for visitor with token ${token}`); + // keep history of unregistered visitors for 1 month + const keepHistoryMiliseconds = 2592000000; + extraData.expireAt = new Date().getTime() + keepHistoryMiliseconds; + } + + if (!settings.get('Livechat_Visitor_navigation_as_a_message')) { + extraData._hidden = true; + } + + // @ts-expect-error: Investigating on which case we won't receive a roomId and where that history is supposed to be stored + return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData); + } + async afterRemoveAgent(user: AtLeast) { await callbacks.run('livechat.afterAgentRemoved', { agent: user }); return true; @@ -1448,6 +1733,41 @@ class LivechatClass { return false; } + async saveDepartmentAgents( + _id: string, + departmentAgents: { + upsert?: Pick[]; + remove?: Pick[]; + }, + ) { + check(_id, String); + check(departmentAgents, { + upsert: Match.Maybe([ + Match.ObjectIncluding({ + agentId: String, + username: String, + count: Match.Maybe(Match.Integer), + order: Match.Maybe(Match.Integer), + }), + ]), + remove: Match.Maybe([ + Match.ObjectIncluding({ + agentId: String, + username: Match.Maybe(String), + count: Match.Maybe(Match.Integer), + order: Match.Maybe(Match.Integer), + }), + ]), + }); + + const department = await LivechatDepartment.findOneById>(_id, { projection: { enabled: 1 } }); + if (!department) { + throw new Meteor.Error('error-department-not-found', 'Department not found'); + } + + return updateDepartmentAgents(_id, departmentAgents, department.enabled); + } + async saveRoomInfo( roomData: { _id: string; @@ -1519,6 +1839,135 @@ class LivechatClass { return true; } + + /** + * @param {string|null} _id - The department id + * @param {Partial} departmentData + * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents + * @param {{_id?: string}} [departmentUnit] - The department's unit id + */ + async saveDepartment( + userId: string, + _id: string | null, + departmentData: LivechatDepartmentDTO, + departmentAgents?: { + upsert?: { agentId: string; count?: number; order?: number }[]; + remove?: { agentId: string; count?: number; order?: number }; + }, + departmentUnit?: { _id?: string }, + ) { + check(_id, Match.Maybe(String)); + if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') { + throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', { + method: 'livechat:saveDepartment', + }); + } + + const department = _id + ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } }) + : null; + + if (departmentUnit && !departmentUnit._id && department && department.parentId) { + const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1; + if (isLastDepartmentInUnit) { + throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", { + method: 'livechat:saveDepartment', + }); + } + } + + if (!department && !(await isDepartmentCreationAvailable())) { + throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { + method: 'livechat:saveDepartment', + }); + } + + if (department?.archived && departmentData.enabled) { + throw new Meteor.Error('error-archived-department-cant-be-enabled', 'Archived departments cant be enabled', { + method: 'livechat:saveDepartment', + }); + } + + const defaultValidations: Record | BooleanConstructor | StringConstructor> = { + enabled: Boolean, + name: String, + description: Match.Optional(String), + showOnRegistration: Boolean, + email: String, + showOnOfflineForm: Boolean, + requestTagBeforeClosingChat: Match.Optional(Boolean), + chatClosingTags: Match.Optional([String]), + fallbackForwardDepartment: Match.Optional(String), + departmentsAllowedToForward: Match.Optional([String]), + allowReceiveForwardOffline: Match.Optional(Boolean), + }; + + // The Livechat Form department support addition/custom fields, so those fields need to be added before validating + Object.keys(departmentData).forEach((field) => { + if (!defaultValidations.hasOwnProperty(field)) { + defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); + } + }); + + check(departmentData, defaultValidations); + check( + departmentAgents, + Match.Maybe({ + upsert: Match.Maybe(Array), + remove: Match.Maybe(Array), + }), + ); + + const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; + if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { + throw new Meteor.Error( + 'error-validating-department-chat-closing-tags', + 'At least one closing tag is required when the department requires tag(s) on closing conversations.', + { method: 'livechat:saveDepartment' }, + ); + } + + if (_id && !department) { + throw new Meteor.Error('error-department-not-found', 'Department not found', { + method: 'livechat:saveDepartment', + }); + } + + if (fallbackForwardDepartment === _id) { + throw new Meteor.Error( + 'error-fallback-department-circular', + 'Cannot save department. Circular reference between fallback department and department', + ); + } + + if (fallbackForwardDepartment) { + const fallbackDep = await LivechatDepartment.findOneById(fallbackForwardDepartment, { + projection: { _id: 1, fallbackForwardDepartment: 1 }, + }); + if (!fallbackDep) { + throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { + method: 'livechat:saveDepartment', + }); + } + } + + const departmentDB = await LivechatDepartment.createOrUpdateDepartment(_id, departmentData); + if (departmentDB && departmentAgents) { + await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); + } + + // Disable event + if (department?.enabled && !departmentDB?.enabled) { + await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); + } + + if (departmentUnit) { + await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id }); + } + + return departmentDB; + } } export const Livechat = new LivechatClass(); +export * from './localTypes'; diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 24be8d42b7a49..c6728d470870b 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -1,12 +1,13 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { Omnichannel } from '@rocket.chat/core-services'; -import type { ILivechatDepartment, IOmnichannelRoomInfo, IOmnichannelRoomExtraData } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus, type ILivechatInquiryRecord, type ILivechatVisitor, type IOmnichannelRoom, type SelectedAgent, + type OmnichannelSourceType, } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; @@ -27,7 +28,6 @@ import { i18n } from '../../../utils/lib/i18n'; import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue } from './Helper'; import { Livechat } from './LivechatTyped'; import { RoutingManager } from './RoutingManager'; -import { getOnlineAgents } from './getOnlineAgents'; import { getInquirySortMechanismSetting } from './settings'; const logger = new Logger('QueueManager'); @@ -145,20 +145,29 @@ export class QueueManager { await this.dispatchInquiryQueued(inquiry, room, defaultAgent); } - static async requestRoom({ + static async requestRoom< + E extends Record & { + sla?: string; + customFields?: Record; + source?: OmnichannelSourceType; + }, + >({ guest, rid = Random.id(), message, roomInfo, agent, - extraData: { customFields, ...extraData } = {}, + extraData: { customFields, ...extraData } = {} as E, }: { guest: ILivechatVisitor; rid?: string; message?: string; - roomInfo: IOmnichannelRoomInfo; + roomInfo: { + source?: IOmnichannelRoom['source']; + [key: string]: unknown; + }; agent?: SelectedAgent; - extraData?: IOmnichannelRoomExtraData; + extraData?: E; }) { logger.debug(`Requesting a room for guest ${guest._id}`); check( @@ -212,7 +221,7 @@ export class QueueManager { } } - const name = guest.name || guest.username; + const name = (roomInfo?.fname as string) || guest.name || guest.username; const room = await createLivechatRoom(rid, name, { ...guest, ...(department && { department }) }, roomInfo, { ...extraData, @@ -324,13 +333,14 @@ export class QueueManager { const { department, rid, v } = inquiry; // Alert only the online agents of the queued request - const onlineAgents = await getOnlineAgents(department, agent); + const onlineAgents = await Livechat.getOnlineAgents(department, agent); if (!onlineAgents) { logger.debug('Cannot notify agents of queued inquiry. No online agents found'); return; } + logger.debug(`Notifying ${await onlineAgents.count()} agents of new inquiry`); const notificationUserName = v && (v.name || v.username); for await (const agent of onlineAgents) { diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index 8781f675ebf98..28e5c72efc16b 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { Message } from '@rocket.chat/core-services'; +import { Message, Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatInquiryRecord, ILivechatVisitor, @@ -12,6 +12,7 @@ import type { TransferData, } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { LivechatInquiry, LivechatRooms, Subscriptions, Rooms, Users } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; @@ -35,6 +36,7 @@ const logger = new Logger('RoutingManager'); type Routing = { methods: Record; + startQueue(): Promise; isMethodSet(): boolean; registerMethod(name: string, Method: IRoutingMethodConstructor): void; getMethod(): IRoutingMethod; @@ -66,6 +68,16 @@ type Routing = { export const RoutingManager: Routing = { methods: {}, + async startQueue() { + const shouldPreventQueueStart = await License.shouldPreventAction('monthlyActiveContacts'); + + if (shouldPreventQueueStart) { + logger.error('Monthly Active Contacts limit reached. Queue will not start'); + return; + } + void (await Omnichannel.getQueueWorker()).shouldStart(); + }, + isMethodSet() { return settings.get('Livechat_Routing_Method') !== ''; }, diff --git a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts index cf5a0abd5d54f..dd0d54970065b 100644 --- a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts +++ b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts @@ -247,11 +247,11 @@ const getConversationsMetricsAsync = async ({ language: user.language || settings.get('Language') || 'en', })) || []; const metrics = ['Total_conversations', 'Open_conversations', 'On_Hold_conversations', 'Total_messages']; - const visitorsCount = await LivechatVisitors.countVisitorsBetweenDate({ + const visitorsCount = await LivechatVisitors.getVisitorsBetweenDate({ start: new Date(start), end: new Date(end), department: departmentId, - }); + }).count(); return { totalizers: [ ...totalizers.filter((metric: { title: string }) => metrics.includes(metric.title)), diff --git a/apps/meteor/app/livechat/server/lib/departmentsLib.ts b/apps/meteor/app/livechat/server/lib/departmentsLib.ts deleted file mode 100644 index fa66fdbfe5735..0000000000000 --- a/apps/meteor/app/livechat/server/lib/departmentsLib.ts +++ /dev/null @@ -1,304 +0,0 @@ -import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; -import { LivechatDepartment, LivechatDepartmentAgents, LivechatVisitors, LivechatRooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../lib/callbacks'; -import { - notifyOnLivechatDepartmentAgentChangedByDepartmentId, - notifyOnLivechatDepartmentAgentChanged, -} from '../../../lib/server/lib/notifyListener'; -import { updateDepartmentAgents } from './Helper'; -import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable'; -import { livechatLogger } from './logger'; -/** - * @param {string|null} _id - The department id - * @param {Partial} departmentData - * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents - * @param {{_id?: string}} [departmentUnit] - The department's unit id - */ -export async function saveDepartment( - userId: string, - _id: string | null, - departmentData: LivechatDepartmentDTO, - departmentAgents?: { - upsert?: { agentId: string; count?: number; order?: number }[]; - remove?: { agentId: string; count?: number; order?: number }; - }, - departmentUnit?: { _id?: string }, -) { - check(_id, Match.Maybe(String)); - if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') { - throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', { - method: 'livechat:saveDepartment', - }); - } - - const department = _id - ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } }) - : null; - - if (departmentUnit && !departmentUnit._id && department && department.parentId) { - const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1; - if (isLastDepartmentInUnit) { - throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", { - method: 'livechat:saveDepartment', - }); - } - } - - if (!department && !(await isDepartmentCreationAvailable())) { - throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { - method: 'livechat:saveDepartment', - }); - } - - if (department?.archived && departmentData.enabled) { - throw new Meteor.Error('error-archived-department-cant-be-enabled', 'Archived departments cant be enabled', { - method: 'livechat:saveDepartment', - }); - } - - const defaultValidations: Record | BooleanConstructor | StringConstructor> = { - enabled: Boolean, - name: String, - description: Match.Optional(String), - showOnRegistration: Boolean, - email: String, - showOnOfflineForm: Boolean, - requestTagBeforeClosingChat: Match.Optional(Boolean), - chatClosingTags: Match.Optional([String]), - fallbackForwardDepartment: Match.Optional(String), - departmentsAllowedToForward: Match.Optional([String]), - allowReceiveForwardOffline: Match.Optional(Boolean), - }; - - // The Livechat Form department support addition/custom fields, so those fields need to be added before validating - Object.keys(departmentData).forEach((field) => { - if (!defaultValidations.hasOwnProperty(field)) { - defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); - } - }); - - check(departmentData, defaultValidations); - check( - departmentAgents, - Match.Maybe({ - upsert: Match.Maybe(Array), - remove: Match.Maybe(Array), - }), - ); - - const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; - if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { - throw new Meteor.Error( - 'error-validating-department-chat-closing-tags', - 'At least one closing tag is required when the department requires tag(s) on closing conversations.', - { method: 'livechat:saveDepartment' }, - ); - } - - if (_id && !department) { - throw new Meteor.Error('error-department-not-found', 'Department not found', { - method: 'livechat:saveDepartment', - }); - } - - if (fallbackForwardDepartment === _id) { - throw new Meteor.Error( - 'error-fallback-department-circular', - 'Cannot save department. Circular reference between fallback department and department', - ); - } - - if (fallbackForwardDepartment) { - const fallbackDep = await LivechatDepartment.findOneById(fallbackForwardDepartment, { - projection: { _id: 1, fallbackForwardDepartment: 1 }, - }); - if (!fallbackDep) { - throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { - method: 'livechat:saveDepartment', - }); - } - } - - const departmentDB = await LivechatDepartment.createOrUpdateDepartment(_id, departmentData); - if (departmentDB && departmentAgents) { - await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); - } - - // Disable event - if (department?.enabled && !departmentDB?.enabled) { - await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); - } - - if (departmentUnit) { - await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id }); - } - - return departmentDB; -} - -export async function archiveDepartment(_id: string) { - const department = await LivechatDepartment.findOneById>(_id, { - projection: { _id: 1, businessHourId: 1 }, - }); - - if (!department) { - throw new Error('department-not-found'); - } - - await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); - - void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); - - await callbacks.run('livechat.afterDepartmentArchived', department); -} - -export async function unarchiveDepartment(_id: string) { - const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found'); - } - - // TODO: these kind of actions should be on events instead of here - await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); - - void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); - - return true; -} - -export async function saveDepartmentAgents( - _id: string, - departmentAgents: { - upsert?: Pick[]; - remove?: Pick[]; - }, -) { - check(_id, String); - check(departmentAgents, { - upsert: Match.Maybe([ - Match.ObjectIncluding({ - agentId: String, - username: String, - count: Match.Maybe(Match.Integer), - order: Match.Maybe(Match.Integer), - }), - ]), - remove: Match.Maybe([ - Match.ObjectIncluding({ - agentId: String, - username: Match.Maybe(String), - count: Match.Maybe(Match.Integer), - order: Match.Maybe(Match.Integer), - }), - ]), - }); - - const department = await LivechatDepartment.findOneById>(_id, { projection: { enabled: 1 } }); - if (!department) { - throw new Meteor.Error('error-department-not-found', 'Department not found'); - } - - return updateDepartmentAgents(_id, departmentAgents, department.enabled); -} - -export async function setDepartmentForGuest({ token, department }: { token: string; department: string }) { - check(token, String); - check(department, String); - - livechatLogger.debug(`Switching departments for user with token ${token} (to ${department})`); - - const updateUser = { - $set: { - department, - }, - }; - - const dep = await LivechatDepartment.findOneById>(department, { projection: { _id: 1 } }); - if (!dep) { - throw new Meteor.Error('invalid-department', 'Provided department does not exists'); - } - - const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); - if (!visitor) { - throw new Meteor.Error('invalid-token', 'Provided token is invalid'); - } - await LivechatVisitors.updateById(visitor._id, updateUser); -} - -export async function removeDepartment(departmentId: string) { - livechatLogger.debug(`Removing department: ${departmentId}`); - - const department = await LivechatDepartment.findOneById>(departmentId, { - projection: { _id: 1, businessHourId: 1 }, - }); - if (!department) { - throw new Error('error-department-not-found'); - } - - const { _id } = department; - - const ret = await LivechatDepartment.removeById(_id); - if (ret.acknowledged !== true) { - throw new Error('error-failed-to-delete-department'); - } - - const removedAgents = await LivechatDepartmentAgents.findByDepartmentId(department._id, { projection: { agentId: 1 } }).toArray(); - - livechatLogger.debug( - `Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`, - ); - - const removeByDept = LivechatDepartmentAgents.removeByDepartmentId(_id); - - const promiseResponses = await Promise.allSettled([ - removeByDept, - LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id), - LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id), - ]); - - promiseResponses.forEach((response, index) => { - if (response.status === 'rejected') { - livechatLogger.error(`Error while performing post-department-removal actions: ${_id}. Action No: ${index}. Error:`, response.reason); - } - }); - - const { deletedCount } = await removeByDept; - - if (deletedCount > 0) { - removedAgents.forEach(({ _id: docId, agentId }) => { - void notifyOnLivechatDepartmentAgentChanged( - { - _id: docId, - agentId, - departmentId: _id, - }, - 'removed', - ); - }); - } - - await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); - - return ret; -} - -export async function getRequiredDepartment(onlineRequired = true) { - const departments = LivechatDepartment.findEnabledWithAgents(); - - for await (const dept of departments) { - if (!dept.showOnRegistration) { - continue; - } - if (!onlineRequired) { - return dept; - } - - const onlineAgents = await LivechatDepartmentAgents.countOnlineForDepartment(dept._id); - if (onlineAgents) { - return dept; - } - } -} diff --git a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts deleted file mode 100644 index be92a7cfcc545..0000000000000 --- a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ILivechatAgent, SelectedAgent } from '@rocket.chat/core-typings'; -import { Users, LivechatDepartmentAgents } from '@rocket.chat/models'; -import type { FindCursor } from 'mongodb'; - -export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { - if (agent?.agentId) { - return Users.findOnlineAgents(agent.agentId); - } - - if (department) { - const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); - if (!departmentAgents) { - return; - } - - const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); - if (!agentIds.length) { - return; - } - - return Users.findByIds([...new Set(agentIds)]); - } - return Users.findOnlineAgents(); -} diff --git a/apps/meteor/app/livechat/server/lib/getRoomMessages.ts b/apps/meteor/app/livechat/server/lib/getRoomMessages.ts deleted file mode 100644 index 8a9af72b7e23f..0000000000000 --- a/apps/meteor/app/livechat/server/lib/getRoomMessages.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { MessageTypesValues, IRoom } from '@rocket.chat/core-typings'; -import { Rooms, Messages } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -export async function getRoomMessages({ rid }: { rid: string }) { - const room = await Rooms.findOneById>(rid, { projection: { t: 1 } }); - if (room?.t !== 'l') { - throw new Meteor.Error('invalid-room'); - } - - const ignoredMessageTypes: MessageTypesValues[] = [ - 'livechat_navigation_history', - 'livechat_transcript_history', - 'command', - 'livechat-close', - 'livechat-started', - 'livechat_video_call', - ]; - - return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { - sort: { ts: 1 }, - }); -} diff --git a/apps/meteor/app/livechat/server/lib/localTypes.ts b/apps/meteor/app/livechat/server/lib/localTypes.ts index d58cad8f50bc5..c6acbbc5bcbd6 100644 --- a/apps/meteor/app/livechat/server/lib/localTypes.ts +++ b/apps/meteor/app/livechat/server/lib/localTypes.ts @@ -1,4 +1,4 @@ -import type { IOmnichannelRoom, IUser, ILivechatVisitor, IMessage, MessageAttachment, IMessageInbox } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IUser, ILivechatVisitor } from '@rocket.chat/core-typings'; export type GenericCloseRoomParams = { room: IOmnichannelRoom; @@ -29,27 +29,3 @@ export type CloseRoomParamsByVisitor = { } & GenericCloseRoomParams; export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor; - -type UploadedFile = { - _id: string; - name?: string; - type?: string; - size?: number; - description?: string; - identify?: { size: { width: number; height: number } }; - format?: string; -}; - -export interface ILivechatMessage { - token: string; - _id: string; - rid: string; - msg: string; - file?: UploadedFile; - files?: UploadedFile[]; - attachments?: MessageAttachment[]; - alias?: string; - groupable?: boolean; - blocks?: IMessage['blocks']; - email?: IMessageInbox['email']; -} diff --git a/apps/meteor/app/livechat/server/lib/logger.ts b/apps/meteor/app/livechat/server/lib/logger.ts index 1e4781240c0c7..bb452cd4db043 100644 --- a/apps/meteor/app/livechat/server/lib/logger.ts +++ b/apps/meteor/app/livechat/server/lib/logger.ts @@ -2,4 +2,3 @@ import { Logger } from '@rocket.chat/logger'; export const callbackLogger = new Logger('[Omnichannel] Callback'); export const businessHourLogger = new Logger('Business Hour'); -export const livechatLogger = new Logger('Livechat'); diff --git a/apps/meteor/app/livechat/server/lib/messages.ts b/apps/meteor/app/livechat/server/lib/messages.ts deleted file mode 100644 index 0f5c460e3c288..0000000000000 --- a/apps/meteor/app/livechat/server/lib/messages.ts +++ /dev/null @@ -1,91 +0,0 @@ -import dns from 'dns'; -import * as util from 'util'; - -import { LivechatDepartment } from '@rocket.chat/models'; - -import { callbacks } from '../../../../lib/callbacks'; -import * as Mailer from '../../../mailer/server/api'; -import { settings } from '../../../settings/server'; - -const dnsResolveMx = util.promisify(dns.resolveMx); - -type OfflineMessageData = { - message: string; - name: string; - email: string; - department?: string; - host?: string; -}; - -export async function sendOfflineMessage(data: OfflineMessageData) { - if (!settings.get('Livechat_display_offline_form')) { - throw new Error('error-offline-form-disabled'); - } - - const { message, name, email, department, host } = data; - - if (!email) { - throw new Error('error-invalid-email'); - } - - const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); - - let html = '

New livechat message

'; - if (host && host !== '') { - html = html.concat(`

Sent from: ${host}

`); - } - html = html.concat(` -

Visitor name: ${name}

-

Visitor email: ${email}

-

Message:
${emailMessage}

`); - - const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); - - let from: string; - if (fromEmail) { - from = fromEmail[0]; - } else { - from = settings.get('From_Email'); - } - - if (settings.get('Livechat_validate_offline_email')) { - const emailDomain = email.substr(email.lastIndexOf('@') + 1); - - try { - await dnsResolveMx(emailDomain); - } catch (e) { - throw new Meteor.Error('error-invalid-email-address'); - } - } - - // TODO Block offline form if Livechat_offline_email is undefined - // (it does not make sense to have an offline form that does nothing) - // `this.sendEmail` will throw an error if the email is invalid - // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client - let emailTo = settings.get('Livechat_offline_email'); - if (department && department !== '') { - const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { email: 1 } }); - if (dep) { - emailTo = dep.email || emailTo; - } - } - - const fromText = `${name} - ${email} <${from}>`; - const replyTo = `${name} <${email}>`; - const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; - await sendEmail(fromText, emailTo, replyTo, subject, html); - - setImmediate(() => { - void callbacks.run('livechat.offlineMessage', data); - }); -} - -async function sendEmail(from: string, to: string, replyTo: string, subject: string, html: string): Promise { - await Mailer.send({ - to, - from, - replyTo, - subject, - html, - }); -} diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index e701e8f5b8639..bc7c06e0eaaed 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -108,7 +108,7 @@ export async function sendTranscript({ messageType.data ? { ...messageType.data(message), interpolation: { escapeValue: false } } : { interpolation: { escapeValue: false } }, - )}` + )}` : message.msg; let filesHTML = ''; diff --git a/apps/meteor/app/livechat/server/lib/tracking.ts b/apps/meteor/app/livechat/server/lib/tracking.ts deleted file mode 100644 index fb0e91a336681..0000000000000 --- a/apps/meteor/app/livechat/server/lib/tracking.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Message } from '@rocket.chat/core-services'; -import { Users } from '@rocket.chat/models'; - -import { settings } from '../../../settings/server'; -import { livechatLogger } from './logger'; - -type PageInfo = { title: string; location: { href: string }; change: string }; - -export async function savePageHistory(token: string, roomId: string | undefined, pageInfo: PageInfo) { - livechatLogger.debug({ - msg: `Saving page movement history for visitor with token ${token}`, - pageInfo, - roomId, - }); - - if (pageInfo.change !== settings.get('Livechat_history_monitor_type')) { - return; - } - const user = await Users.findOneById('rocket.cat'); - - if (!user) { - throw new Error('error-invalid-user'); - } - - const pageTitle = pageInfo.title; - const pageUrl = pageInfo.location.href; - const extraData: { - navigation: { - page: PageInfo; - token: string; - }; - expireAt?: number; - _hidden?: boolean; - } = { - navigation: { - page: pageInfo, - token, - }, - }; - - if (!roomId) { - livechatLogger.warn(`Saving page history without room id for visitor with token ${token}`); - // keep history of unregistered visitors for 1 month - const keepHistoryMiliseconds = 2592000000; - extraData.expireAt = new Date().getTime() + keepHistoryMiliseconds; - } - - if (!settings.get('Livechat_Visitor_navigation_as_a_message')) { - extraData._hidden = true; - } - - // @ts-expect-error: Investigating on which case we won't receive a roomId and where that history is supposed to be stored - return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData); -} diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts index 484cf275088d7..659f85f49945c 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts +++ b/apps/meteor/app/livechat/server/methods/saveDepartment.ts @@ -3,7 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { saveDepartment } from '../lib/departmentsLib'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -44,6 +44,6 @@ Meteor.methods({ }); } - return saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); + return Livechat.saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); }, }); diff --git a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts index 742631dcea904..6fac80397906f 100644 --- a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts +++ b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; import { Livechat } from '../lib/LivechatTyped'; -import type { ILivechatMessage } from '../lib/localTypes'; +import type { ILivechatMessage } from '../lib/LivechatTyped'; interface ILivechatMessageAgent { agentId: string; diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index e546e6967569a..32cf01d2e6404 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -15,6 +15,7 @@ import { settings } from '../../settings/server'; import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; import { Livechat as LivechatTyped } from './lib/LivechatTyped'; +import { RoutingManager } from './lib/RoutingManager'; import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; import './roomAccessValidator.internalService'; @@ -77,10 +78,14 @@ Meteor.startup(async () => { process.env.TEST_MODE === 'true' ? { debounce: 10, - } + } : undefined, ); + settings.watch('Livechat_Routing_Method', () => { + void RoutingManager.startQueue(); + }); + // Remove when accounts.onLogout is async Accounts.onLogout(({ user }: { user?: IUser }) => { if (!user?.roles?.includes('livechat-agent') || user?.roles?.includes('bot')) { diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index a7577af5843f3..7e0ad0380b119 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -44,7 +44,7 @@ export const replace = (str: string, data: { [key: string]: unknown } = {}): str ? { fname: strLeft(String(data.name), ' '), lname: strRightBack(String(data.name), ' '), - } + } : {}), ...data, }; diff --git a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts index 97cdf8a7a1ac0..e1e35d2160293 100644 --- a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts +++ b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; import { dispatchToastMessage } from '../../../client/lib/toast'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { router } from '../../../client/providers/RouterProvider'; import { ChatSubscription } from '../../models/client'; import { LegacyRoomManager, MessageAction } from '../../ui-utils/client'; @@ -14,7 +15,9 @@ Meteor.startup(() => { label: 'Mark_unread', context: ['message', 'message-mobile', 'threads'], type: 'interaction', - async action(_, { message }) { + async action(_, props) { + const { message = messageArgs(this).msg } = props; + try { const subscription = ChatSubscription.findOne({ rid: message.rid, diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts index 6c724b45282fa..760fbea7c232c 100644 --- a/apps/meteor/app/push/server/push.ts +++ b/apps/meteor/app/push/server/push.ts @@ -484,14 +484,14 @@ class PushClass { apn: { ...pick(options.apn, 'category'), }, - } + } : {}), ...(this.hasGcmOptions(options) ? { gcm: { ...pick(options.gcm, 'image', 'style'), }, - } + } : {}), }; diff --git a/apps/meteor/app/reactions/client/init.ts b/apps/meteor/app/reactions/client/init.ts index d9573fbfe8331..1943d7262939a 100644 --- a/apps/meteor/app/reactions/client/init.ts +++ b/apps/meteor/app/reactions/client/init.ts @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { MessageAction } from '../../ui-utils/client'; import { sdk } from '../../utils/client/lib/SDKClient'; @@ -10,7 +11,8 @@ Meteor.startup(() => { icon: 'add-reaction', label: 'Add_Reaction', context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - action(event, { message, chat }) { + action(event, props) { + const { message = messageArgs(this).msg, chat } = props; event?.stopPropagation(); chat?.emojiPicker.open(event?.currentTarget as Element, (emoji) => sdk.call('setReaction', `:${emoji}:`, message._id)); }, diff --git a/apps/meteor/app/slackbridge/client/slackbridge_import.client.ts b/apps/meteor/app/slackbridge/client/slackbridge_import.client.js similarity index 85% rename from apps/meteor/app/slackbridge/client/slackbridge_import.client.ts rename to apps/meteor/app/slackbridge/client/slackbridge_import.client.js index 2138fc2a35f90..eebc07ddb72d2 100644 --- a/apps/meteor/app/slackbridge/client/slackbridge_import.client.ts +++ b/apps/meteor/app/slackbridge/client/slackbridge_import.client.js @@ -1,7 +1,7 @@ import { settings } from '../../settings/client'; import { slashCommands } from '../../utils/client/slashCommand'; -settings.onload('SlackBridge_Enabled', (_key, value) => { +settings.onload('SlackBridge_Enabled', (key, value) => { if (value) { slashCommands.add({ command: 'slackbridge-import', diff --git a/apps/meteor/app/slackbridge/server/SlackAdapter.js b/apps/meteor/app/slackbridge/server/SlackAdapter.js index 31cbff69463de..0263d5369a4c5 100644 --- a/apps/meteor/app/slackbridge/server/SlackAdapter.js +++ b/apps/meteor/app/slackbridge/server/SlackAdapter.js @@ -987,7 +987,7 @@ export default class SlackAdapter { const rocketChannel = await this.rocket.getChannel(slackMessage); const rocketUser = slackMessage.previous_message.user ? (await this.rocket.findUser(slackMessage.previous_message.user)) || - (await this.rocket.addUser(slackMessage.previous_message.user)) + (await this.rocket.addUser(slackMessage.previous_message.user)) : null; const rocketMsgObj = { diff --git a/apps/meteor/app/slashcommands-hide/server/hide.ts b/apps/meteor/app/slashcommands-hide/server/hide.ts index 636974297914e..bc614b95e8eda 100644 --- a/apps/meteor/app/slashcommands-hide/server/hide.ts +++ b/apps/meteor/app/slashcommands-hide/server/hide.ts @@ -41,7 +41,7 @@ slashCommands.add({ : await Rooms.findOne({ t: 'd', usernames: { $all: [user.username, strippedRoom] }, - }); + }); if (!roomObject) { void api.broadcast('notify.ephemeralMessage', user._id, message.rid, { msg: i18n.t('Channel_doesnt_exist', { diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index 436d283d4b5a5..e74bb89899c28 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -64,20 +64,20 @@ function inviteAll(type: T): SlashCommand['callback'] { return; } + const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, { + projection: { 'u.username': 1 }, + }); + try { const APIsettings = settings.get('API_User_Limit'); if (!APIsettings) { return; } - if ((await Subscriptions.countByRoomIdWhenUsernameExists(baseChannel._id)) > APIsettings) { + if ((await cursor.count()) > APIsettings) { throw new Meteor.Error('error-user-limit-exceeded', 'User Limit Exceeded', { method: 'addAllToRoom', }); } - - const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, { - projection: { 'u.username': 1 }, - }); const users = (await cursor.toArray()).map((s: ISubscription) => s.u.username).filter(isTruthy); if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) { diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 65b363cad1252..84b8f23f77900 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -2,7 +2,7 @@ import { log } from 'console'; import os from 'os'; import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; -import type { IRoom, IStats, ISetting } from '@rocket.chat/core-typings'; +import type { IRoom, IStats } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import { NotificationQueue, @@ -92,7 +92,7 @@ export const statistics = { }; // Version - const uniqueID = await Settings.findOne>('uniqueID', { projection: { createdAt: 1 } }); + const uniqueID = await Settings.findOne('uniqueID'); statistics.uniqueId = settings.get('uniqueID'); if (uniqueID) { statistics.installedAt = uniqueID.createdAt.toISOString(); @@ -126,10 +126,10 @@ export const statistics = { // Room statistics statistics.totalRooms = await Rooms.col.countDocuments({}); - statistics.totalChannels = await Rooms.countByType('c'); - statistics.totalPrivateGroups = await Rooms.countByType('p'); - statistics.totalDirect = await Rooms.countByType('d'); - statistics.totalLivechat = await Rooms.countByType('l'); + statistics.totalChannels = await Rooms.findByType('c').count(); + statistics.totalPrivateGroups = await Rooms.findByType('p').count(); + statistics.totalDirect = await Rooms.findByType('d').count(); + statistics.totalLivechat = await Rooms.findByType('l').count(); statistics.totalDiscussions = await Rooms.countDiscussions(); statistics.totalThreads = await Messages.countThreads(); @@ -183,7 +183,7 @@ export const statistics = { // Number of triggers statsPms.push( - LivechatTrigger.estimatedDocumentCount().then((count) => { + LivechatTrigger.col.count().then((count) => { statistics.totalTriggers = count; }), ); @@ -205,13 +205,13 @@ export const statistics = { // Number of Email Inboxes statsPms.push( - EmailInbox.estimatedDocumentCount().then((count) => { + EmailInbox.col.count().then((count) => { statistics.emailInboxes = count; }), ); statsPms.push( - LivechatBusinessHours.estimatedDocumentCount().then((count) => { + LivechatBusinessHours.col.count().then((count) => { statistics.BusinessHours = { // Number of Business Hours total: count, @@ -326,7 +326,8 @@ export const statistics = { room: IRoom, ) { return num + (room.prid ? room.msgs : 0); - }, 0); + }, + 0); statistics.totalPrivateGroupMessages = (await privateGroups.reduce(function _countPrivateGroupMessages(num: number, room: IRoom) { return num + room.msgs; @@ -519,7 +520,7 @@ export const statistics = { ); statsPms.push( - NotificationQueue.estimatedDocumentCount().then((count) => { + NotificationQueue.col.estimatedDocumentCount().then((count) => { statistics.pushQueue = count; }), ); @@ -545,27 +546,27 @@ export const statistics = { statistics.messageAuditLoad = settings.get('Message_Auditing_Panel_Load_Count'); statistics.joinJitsiButton = settings.get('Jitsi_Click_To_Join_Count'); statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count'); - statistics.totalOTRRooms = await Rooms.countByCreatedOTR({ readPreference }); + statistics.totalOTRRooms = await Rooms.findByCreatedOTR().count(); statistics.totalOTR = settings.get('OTR_Count'); - statistics.totalBroadcastRooms = await Rooms.countByBroadcast({ readPreference }); + statistics.totalBroadcastRooms = await Rooms.findByBroadcast().count(); statistics.totalTriggeredEmails = settings.get('Triggered_Emails_Count'); statistics.totalRoomsWithStarred = await Messages.countRoomsWithStarredMessages({ readPreference }); statistics.totalRoomsWithPinned = await Messages.countRoomsWithPinnedMessages({ readPreference }); statistics.totalUserTOTP = await Users.countActiveUsersTOTPEnable({ readPreference }); statistics.totalUserEmail2fa = await Users.countActiveUsersEmail2faEnable({ readPreference }); - statistics.totalPinned = await Messages.countPinned({ readPreference }); - statistics.totalStarred = await Messages.countStarred({ readPreference }); - statistics.totalLinkInvitation = await Invites.estimatedDocumentCount(); + statistics.totalPinned = await Messages.findPinned({ readPreference }).count(); + statistics.totalStarred = await Messages.findStarred({ readPreference }).count(); + statistics.totalLinkInvitation = await Invites.find().count(); statistics.totalLinkInvitationUses = await Invites.countUses(); statistics.totalEmailInvitation = settings.get('Invitation_Email_Count'); - statistics.totalE2ERooms = await Rooms.countByE2E({ readPreference }); + statistics.totalE2ERooms = await Rooms.findByE2E({ readPreference }).count(); statistics.logoChange = Object.keys(settings.get('Assets_logo') || {}).includes('url'); statistics.showHomeButton = settings.get('Layout_Show_Home_Button'); statistics.totalEncryptedMessages = await Messages.countByType('e2e', { readPreference }); statistics.totalManuallyAddedUsers = settings.get('Manual_Entry_User_Count'); - statistics.totalSubscriptionRoles = await RolesRaw.countByScope('Subscriptions', { readPreference }); - statistics.totalUserRoles = await RolesRaw.countByScope('Users', { readPreference }); - statistics.totalCustomRoles = await RolesRaw.countCustomRoles({ readPreference }); + statistics.totalSubscriptionRoles = await RolesRaw.findByScope('Subscriptions').count(); + statistics.totalUserRoles = await RolesRaw.findByScope('Users').count(); + statistics.totalCustomRoles = await RolesRaw.findCustomRoles({ readPreference }).count(); statistics.totalWebRTCCalls = settings.get('WebRTC_Calls_Count'); statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count'); diff --git a/apps/meteor/app/threads/client/lib/normalizeThreadTitle.ts b/apps/meteor/app/threads/client/lib/normalizeThreadTitle.ts index c3d10b531b6bc..70a2a6008e56d 100644 --- a/apps/meteor/app/threads/client/lib/normalizeThreadTitle.ts +++ b/apps/meteor/app/threads/client/lib/normalizeThreadTitle.ts @@ -2,7 +2,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; -import { emojiParser } from '../../../emoji/client/emojiParser'; +import { emojiParser } from '../../../emoji/client/emojiParser.js'; import { filterMarkdown } from '../../../markdown/lib/markdown'; import { MentionsParser } from '../../../mentions/lib/MentionsParser'; import { Users } from '../../../models/client'; @@ -26,7 +26,7 @@ export function normalizeThreadTitle({ ...message }: Readonly) { userTemplate: ({ label }) => ` ${label} `, roomTemplate: ({ prefix, mention }) => `${prefix} ${mention} `, }); - const html = emojiParser(filteredMessage); + const { html } = emojiParser({ html: filteredMessage }); return instance.parse({ ...message, msg: filteredMessage, html }).html; } diff --git a/apps/meteor/app/threads/client/messageAction/replyInThread.ts b/apps/meteor/app/threads/client/messageAction/replyInThread.ts index 9d0ab8566e04c..01d007e0d9532 100644 --- a/apps/meteor/app/threads/client/messageAction/replyInThread.ts +++ b/apps/meteor/app/threads/client/messageAction/replyInThread.ts @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; +import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { router } from '../../../../client/providers/RouterProvider'; import { settings } from '../../../settings/client'; import { MessageAction } from '../../../ui-utils/client'; @@ -16,7 +17,8 @@ Meteor.startup(() => { icon: 'thread', label: 'Reply_in_thread', context: ['message', 'message-mobile', 'federated', 'videoconf'], - action(e, { message }) { + action(e, props) { + const { message = messageArgs(this).msg } = props; e?.stopPropagation(); router.navigate({ name: router.getRouteName()!, diff --git a/apps/meteor/app/ui-master/server/index.ts b/apps/meteor/app/ui-master/server/index.js similarity index 81% rename from apps/meteor/app/ui-master/server/index.ts rename to apps/meteor/app/ui-master/server/index.js index b4f15f211abc2..2d4f3cc7de56f 100644 --- a/apps/meteor/app/ui-master/server/index.ts +++ b/apps/meteor/app/ui-master/server/index.js @@ -1,4 +1,3 @@ -import type { ISettingColor } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; @@ -16,11 +15,11 @@ export * from './inject'; Meteor.startup(() => { Tracker.autorun(() => { - const injections = Object.values(headInjections.all()).filter((injection): injection is NonNullable => !!injection); + const injections = Object.values(headInjections.all()); Inject.rawModHtml('headInjections', applyHeadInjections(injections)); }); - settings.watch('Default_Referrer_Policy', (value) => { + settings.watch('Default_Referrer_Policy', (value) => { if (!value) { return injectIntoHead('noreferrer', ''); } @@ -41,7 +40,7 @@ Meteor.startup(() => { ); } - settings.watch('Assets_SvgFavicon_Enable', (value) => { + settings.watch('Assets_SvgFavicon_Enable', (value) => { const standardFavicons = ` `; @@ -57,7 +56,7 @@ Meteor.startup(() => { } }); - settings.watch('theme-color-sidebar-background', (value) => { + settings.watch('theme-color-sidebar-background', (value) => { const escapedValue = escapeHTML(value); injectIntoHead( 'theme-color-sidebar-background', @@ -65,7 +64,7 @@ Meteor.startup(() => { ); }); - settings.watch('Site_Name', (value = 'Rocket.Chat') => { + settings.watch('Site_Name', (value = 'Rocket.Chat') => { const escapedValue = escapeHTML(value); injectIntoHead( 'Site_Name', @@ -75,7 +74,7 @@ Meteor.startup(() => { ); }); - settings.watch('Meta_language', (value = '') => { + settings.watch('Meta_language', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead( 'Meta_language', @@ -83,27 +82,27 @@ Meteor.startup(() => { ); }); - settings.watch('Meta_robots', (value = '') => { + settings.watch('Meta_robots', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead('Meta_robots', ``); }); - settings.watch('Meta_msvalidate01', (value = '') => { + settings.watch('Meta_msvalidate01', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead('Meta_msvalidate01', ``); }); - settings.watch('Meta_google-site-verification', (value = '') => { + settings.watch('Meta_google-site-verification', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead('Meta_google-site-verification', ``); }); - settings.watch('Meta_fb_app_id', (value = '') => { + settings.watch('Meta_fb_app_id', (value = '') => { const escapedValue = escapeHTML(value); injectIntoHead('Meta_fb_app_id', ``); }); - settings.watch('Meta_custom', (value = '') => { + settings.watch('Meta_custom', (value = '') => { injectIntoHead('Meta_custom', value); }); @@ -128,7 +127,7 @@ const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => { // const variables = RocketChat.models.Settings.findOne({_id:'theme-custom-variables'}, {fields: { value: 1}}); const colors = await Settings.find({ _id: /theme-color-rc/i }, { projection: { value: 1, editor: 1 } }).toArray(); const css = colors - .filter((color): color is ISettingColor => !!color?.value) + .filter((color) => color && color.value) .map(({ _id, value, editor }) => { if (editor === 'expression') { return `--${_id.replace('theme-color-', '')}: var(--${value});`; @@ -139,7 +138,7 @@ const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => { injectIntoBody('dynamic-variables', ``); }); -await renderDynamicCssList(); +renderDynamicCssList(); settings.watchByRegex(/theme-color-rc/i, renderDynamicCssList); @@ -161,4 +160,4 @@ injectIntoBody( `, ); -injectIntoBody('icons', (await Assets.getTextAsync('public/icons.svg')) ?? ''); +injectIntoBody('icons', await Assets.getTextAsync('public/icons.svg')); diff --git a/apps/meteor/app/ui-master/server/inject.ts b/apps/meteor/app/ui-master/server/inject.ts index 47b63db4bb3f8..1e00a0e47433f 100644 --- a/apps/meteor/app/ui-master/server/inject.ts +++ b/apps/meteor/app/ui-master/server/inject.ts @@ -16,7 +16,7 @@ type Injection = tag: string; }; -export const headInjections = new ReactiveDict>(); +export const headInjections = new ReactiveDict(); const callback: NextHandleFunction = (req, res, next) => { if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') { @@ -32,7 +32,7 @@ const callback: NextHandleFunction = (req, res, next) => { return; } - const injection = headInjections.get(pathname.replace(/^\//, '').split('_')[0]); + const injection = headInjections.get(pathname.replace(/^\//, '').split('_')[0]) as Injection | undefined; if (!injection || typeof injection === 'string') { next(); diff --git a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts index 0a5483c4acaaa..c1f9590b98ee8 100644 --- a/apps/meteor/app/ui-utils/client/lib/MessageAction.ts +++ b/apps/meteor/app/ui-utils/client/lib/MessageAction.ts @@ -47,6 +47,7 @@ export type MessageActionConfig = { group?: MessageActionGroup | MessageActionGroup[]; context?: MessageActionContext[]; action: ( + this: any, e: Pick | undefined, { message, diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index ae2ff1cf80d29..2f2793f7493b9 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -7,6 +7,7 @@ import { getPermaLink } from '../../../../client/lib/getPermaLink'; import { imperativeModal } from '../../../../client/lib/imperativeModal'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; import { dispatchToastMessage } from '../../../../client/lib/toast'; +import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { router } from '../../../../client/providers/RouterProvider'; import ForwardMessageModal from '../../../../client/views/room/modals/ForwardMessageModal/ForwardMessageModal'; import ReactionListModal from '../../../../client/views/room/modals/ReactionListModal'; @@ -31,7 +32,8 @@ Meteor.startup(async () => { context: ['message', 'message-mobile', 'threads', 'federated'], role: 'link', type: 'communication', - action(_, { message }) { + action(_, props) { + const { message = messageArgs(this).msg } = props; roomCoordinator.openRouteLink( 'd', { name: message.u.username }, @@ -72,7 +74,8 @@ Meteor.startup(async () => { label: 'Forward_message', context: ['message', 'message-mobile', 'threads'], type: 'communication', - async action(_, { message }) { + async action(_, props) { + const { message = messageArgs(this).msg } = props; const permalink = await getPermaLink(message._id); imperativeModal.open({ component: ForwardMessageModal, @@ -97,7 +100,9 @@ Meteor.startup(async () => { icon: 'quote', label: 'Quote', context: ['message', 'message-mobile', 'threads', 'federated'], - async action(_, { message, chat, autoTranslateOptions }) { + async action(_, props) { + const { message = messageArgs(this).msg, chat, autoTranslateOptions } = props; + if (message && autoTranslateOptions?.autoTranslateEnabled && autoTranslateOptions.showAutoTranslate(message)) { message.msg = message.translations && autoTranslateOptions.autoTranslateLanguage @@ -125,8 +130,9 @@ Meteor.startup(async () => { // classes: 'clipboard', context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], type: 'duplication', - async action(_, { message }) { + async action(_, props) { try { + const { message = messageArgs(this).msg } = props; const permalink = await getPermaLink(message._id); await navigator.clipboard.writeText(permalink); dispatchToastMessage({ type: 'success', message: t('Copied') }); @@ -151,7 +157,8 @@ Meteor.startup(async () => { // classes: 'clipboard', context: ['message', 'message-mobile', 'threads', 'federated'], type: 'duplication', - async action(_, { message }) { + async action(_, props) { + const { message = messageArgs(this).msg } = props; const msgText = getMainMessageText(message).msg; await navigator.clipboard.writeText(msgText); dispatchToastMessage({ type: 'success', message: t('Copied') }); @@ -169,7 +176,8 @@ Meteor.startup(async () => { label: 'Edit', context: ['message', 'message-mobile', 'threads', 'federated'], type: 'management', - async action(_, { message, chat }) { + async action(_, props) { + const { message = messageArgs(this).msg, chat } = props; await chat?.messageEditing.editMessage(message); }, condition({ message, subscription, settings, room, user }) { @@ -212,7 +220,7 @@ Meteor.startup(async () => { context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], color: 'alert', type: 'management', - async action(_, { message, chat }) { + async action(this: unknown, _, { message = messageArgs(this).msg, chat }) { await chat?.flows.requestMessageDeletion(message); }, condition({ message, subscription, room, chat, user }) { @@ -240,7 +248,7 @@ Meteor.startup(async () => { context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], color: 'alert', type: 'management', - action(_, { message }) { + action(this: unknown, _, { message = messageArgs(this).msg }) { imperativeModal.open({ component: ReportMessageModal, props: { @@ -267,7 +275,7 @@ Meteor.startup(async () => { label: 'Reactions', context: ['message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'], type: 'interaction', - action(_, { message: { reactions = {} } }) { + action(this: unknown, _, { message: { reactions = {} } = messageArgs(this).msg }) { imperativeModal.open({ component: ReactionListModal, props: { reactions, onClose: imperativeModal.close }, diff --git a/apps/meteor/app/ui-utils/client/lib/messageBox.ts b/apps/meteor/app/ui-utils/client/lib/messageBox.ts index 9dd26f64c8e04..3418adef1c1cd 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageBox.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageBox.ts @@ -48,16 +48,13 @@ class MessageBoxActions { get(group?: TranslationKey) { if (!group) { - return [...this.actions.entries()].reduce>( - (ret, [group, actions]) => { - const filteredActions = actions.filter((action) => !action.condition || action.condition()); - if (filteredActions.length) { - ret[group] = filteredActions; - } - return ret; - }, - {} as Record, - ); + return [...this.actions.entries()].reduce>((ret, [group, actions]) => { + const filteredActions = actions.filter((action) => !action.condition || action.condition()); + if (filteredActions.length) { + ret[group] = filteredActions; + } + return ret; + }, {} as Record); } return this.actions.get(group)?.filter((action) => !action.condition || action.condition()); diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 681d93aab82c3..5d78fd1e1d983 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -24,7 +24,7 @@ type DeepWritable = T extends (...args: any) => any ? T : { -readonly [P in keyof T]: DeepWritable; - }; + }; export class ChatMessages implements ChatAPI { public uid: string | null; diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index 57cc34225d8af..b69fe6b305139 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -1,18 +1,9 @@ import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; -import type { TOptions } from 'i18next'; import i18next from 'i18next'; import sprintf from 'i18next-sprintf-postprocessor'; import { isObject } from '../../../lib/utils/isObject'; -declare module 'i18next' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface TFunction { - (key: RocketchatI18nKeys): string; - (key: RocketchatI18nKeys, options: TOptions): string; - } -} - export const i18n = i18next.use(sprintf); export const addSprinfToI18n = function (t: (typeof i18n)['t']) { diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 79b54edc4aad5..d56c898b14c1e 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.1.0-develop" + "version": "7.0.0" } diff --git a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts index 173592a982766..097bbc4e9eb83 100644 --- a/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts +++ b/apps/meteor/app/utils/server/functions/normalizeMessageFileUpload.ts @@ -20,7 +20,7 @@ export const normalizeMessageFileUpload = async (message: Omit void, onError: (error: any) => void): void; - }; - }; - - // eslint-disable-next-line @typescript-eslint/naming-convention - interface Window { - rocketchatscreenshare?: unknown; - audioContext?: AudioContext; - } -} - -type EventData, TType> = Extract< - StreamerCallbackArgs, - [type: TType, data: any] ->[1]; - -type StatusData = EventData<'notify-room', `${string}/webrtc`, 'status'>; -type CallData = EventData<'notify-room-users', `${string}/webrtc`, 'call'>; -type CandidateData = EventData<'notify-user', `${string}/webrtc`, 'candidate'>; -type DescriptionData = EventData<'notify-user', `${string}/webrtc`, 'description'>; -type JoinData = EventData<'notify-user', `${string}/webrtc`, 'join'>; - -type RemoteItem = { - id: string; - url: MediaStream; - state: RTCIceConnectionState; - stateText?: string; - connected?: boolean; -}; - -type RemoteConnection = { - id: string; - media: MediaStreamConstraints; -}; - -class WebRTCTransportClass extends Emitter<{ - status: StatusData; - call: CallData; - candidate: CandidateData; - description: DescriptionData; - join: JoinData; -}> { - public debug = false; - - constructor(public webrtcInstance: WebRTCClass) { +class WebRTCTransportClass extends Emitter { + constructor(webrtcInstance) { super(); + this.debug = false; + this.webrtcInstance = webrtcInstance; sdk.stream('notify-room', [`${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { this.log('WebRTCTransportClass - onRoom', type, data); this.emit(type, data); }); } - log(...args: unknown[]) { + log(...args) { if (this.debug === true) { console.log(...args); } } - onUserStream(type: 'candidate', data: CandidateData): void; - - onUserStream(type: 'description', data: DescriptionData): void; - - onUserStream(type: 'join', data: JoinData): void; - - onUserStream( - ...[type, data]: - | [type: 'candidate', data: CandidateData] - | [type: 'description', data: DescriptionData] - | [type: 'join', data: JoinData] - ) { + onUserStream(type, data) { if (data.room !== this.webrtcInstance.room) { return; } this.log('WebRTCTransportClass - onUser', type, data); - - switch (type) { - case 'candidate': - this.emit('candidate', data); - break; - case 'description': - this.emit('description', data); - break; - case 'join': - this.emit('join', data); - break; - } + this.emit(type, data); } - startCall(data: CallData) { + startCall(data) { this.log('WebRTCTransportClass - startCall', this.webrtcInstance.room, this.webrtcInstance.selfId); sdk.publish('notify-room-users', [ `${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`, @@ -150,7 +53,7 @@ class WebRTCTransportClass extends Emitter<{ ]); } - joinCall(data: JoinData) { + joinCall(data) { this.log('WebRTCTransportClass - joinCall', this.webrtcInstance.room, this.webrtcInstance.selfId); if (data.monitor === true) { sdk.publish('notify-user', [ @@ -177,109 +80,75 @@ class WebRTCTransportClass extends Emitter<{ } } - sendCandidate(data: CandidateData) { + sendCandidate(data) { data.from = this.webrtcInstance.selfId; data.room = this.webrtcInstance.room; this.log('WebRTCTransportClass - sendCandidate', data); sdk.publish('notify-user', [`${data.to}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.CANDIDATE, data]); } - sendDescription(data: DescriptionData) { + sendDescription(data) { data.from = this.webrtcInstance.selfId; data.room = this.webrtcInstance.room; this.log('WebRTCTransportClass - sendDescription', data); sdk.publish('notify-user', [`${data.to}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.DESCRIPTION, data]); } - sendStatus(data: StatusData) { + sendStatus(data) { this.log('WebRTCTransportClass - sendStatus', data, this.webrtcInstance.room); data.from = this.webrtcInstance.selfId; sdk.publish('notify-room', [`${this.webrtcInstance.room}/${WEB_RTC_EVENTS.WEB_RTC}`, WEB_RTC_EVENTS.STATUS, data]); } - onRemoteCall(fn: (data: CallData) => void) { + onRemoteCall(fn) { return this.on(WEB_RTC_EVENTS.CALL, fn); } - onRemoteJoin(fn: (data: JoinData) => void) { + onRemoteJoin(fn) { return this.on(WEB_RTC_EVENTS.JOIN, fn); } - onRemoteCandidate(fn: (data: CandidateData) => void) { + onRemoteCandidate(fn) { return this.on(WEB_RTC_EVENTS.CANDIDATE, fn); } - onRemoteDescription(fn: (data: DescriptionData) => void) { + onRemoteDescription(fn) { return this.on(WEB_RTC_EVENTS.DESCRIPTION, fn); } - onRemoteStatus(fn: (data: StatusData) => void) { + onRemoteStatus(fn) { return this.on(WEB_RTC_EVENTS.STATUS, fn); } } class WebRTCClass { - transport: WebRTCTransportClass; - - config: { iceServers: RTCIceServer[] }; - - debug: boolean; - - TransportClass: typeof WebRTCTransportClass; - - peerConnections: Record = {}; - - remoteItems: ReactiveVar; - - remoteItemsById: ReactiveVar>; - - callInProgress: ReactiveVar; - - audioEnabled: ReactiveVar; - - videoEnabled: ReactiveVar; - - overlayEnabled: ReactiveVar; - - screenShareEnabled: ReactiveVar; - - localUrl: ReactiveVar; + /* + @param seldId {String} + @param room {String} + */ - active: boolean; - - remoteMonitoring: boolean; - - monitor: boolean; - - navigator: string | undefined; - - screenShareAvailable: boolean; - - media: MediaStreamConstraints; - - constructor( - public selfId: string, - public room: string, - public autoAccept = false, - ) { + constructor(selfId, room, autoAccept = false) { this.config = { iceServers: [], }; this.debug = false; this.TransportClass = WebRTCTransportClass; - let servers = settings.get('WebRTC_Servers'); + this.selfId = selfId; + this.room = room; + let servers = settings.get('WebRTC_Servers'); if (servers && servers.trim() !== '') { servers = servers.replace(/\s/g, ''); + servers = servers.split(','); - servers.split(',').forEach((server) => { - const parts = server.split('@'); - const serverConfig: RTCIceServer = { - urls: parts.pop()!, + servers.forEach((server) => { + server = server.split('@'); + const serverConfig = { + urls: server.pop(), }; - if (parts.length === 1) { - const [username, credential] = parts[0].split(':'); - serverConfig.username = decodeURIComponent(username); - serverConfig.credential = decodeURIComponent(credential); + if (server.length === 1) { + server = server[0].split(':'); + serverConfig.username = decodeURIComponent(server[0]); + serverConfig.credential = decodeURIComponent(server[1]); } this.config.iceServers.push(serverConfig); }); @@ -292,10 +161,11 @@ class WebRTCClass { this.videoEnabled = new ReactiveVar(false); this.overlayEnabled = new ReactiveVar(false); this.screenShareEnabled = new ReactiveVar(false); - this.localUrl = new ReactiveVar(undefined); + this.localUrl = new ReactiveVar(); this.active = false; this.remoteMonitoring = false; this.monitor = false; + this.autoAccept = autoAccept; this.navigator = undefined; const userAgent = navigator.userAgent.toLocaleLowerCase(); @@ -309,7 +179,7 @@ class WebRTCClass { this.navigator = 'safari'; } - this.screenShareAvailable = ['chrome', 'firefox', 'electron'].includes(this.navigator!); + this.screenShareAvailable = ['chrome', 'firefox', 'electron'].includes(this.navigator); this.media = { video: true, audio: true, @@ -324,41 +194,18 @@ class WebRTCClass { setInterval(this.checkPeerConnections.bind(this), 1000); } - onUserStream(type: 'candidate', data: CandidateData): void; - - onUserStream(type: 'description', data: DescriptionData): void; - - onUserStream(type: 'join', data: JoinData): void; - - onUserStream( - ...[type, data]: - | [type: 'candidate', data: CandidateData] - | [type: 'description', data: DescriptionData] - | [type: 'join', data: JoinData] - ) { - switch (type) { - case 'candidate': - this.transport.onUserStream('candidate', data); - break; - - case 'description': - this.transport.onUserStream('description', data); - break; - - case 'join': - this.transport.onUserStream('join', data); - break; - } + onUserStream(...args) { + return this.transport.onUserStream(...args); } - log(...args: unknown[]) { + log(...args) { if (this.debug === true) { - console.log(...args); + console.log.apply(console, args); } } - onError(...args: unknown[]) { - console.error(...args); + onError(...args) { + console.error.apply(console, args); } checkPeerConnections() { @@ -374,13 +221,13 @@ class WebRTCClass { } updateRemoteItems() { - const items: RemoteItem[] = []; - const itemsById: Record = {}; + const items = []; + const itemsById = {}; const { peerConnections } = this; Object.entries(peerConnections).forEach(([id, peerConnection]) => { peerConnection.getRemoteStreams().forEach((remoteStream) => { - const item: RemoteItem = { + const item = { id, url: remoteStream, state: peerConnection.iceConnectionState, @@ -419,9 +266,9 @@ class WebRTCClass { if (this.active !== true || this.monitor === true || this.remoteMonitoring === true) { return; } - const remoteConnections: RemoteConnection[] = []; + const remoteConnections = []; const { peerConnections } = this; - Object.entries(peerConnections).forEach(([id, { remoteMedia: media }]) => { + Object.keys(peerConnections).entries(([id, { remoteMedia: media }]) => { remoteConnections.push({ id, media, @@ -434,9 +281,16 @@ class WebRTCClass { }); } - callInProgressTimeout: ReturnType | undefined = undefined; + /* + @param data {Object} + from {String} + media {Object} + remoteConnections {Array[Object]} + id {String} + media {Object} + */ - onRemoteStatus(data: StatusData) { + onRemoteStatus(data) { // this.log(onRemoteStatus, arguments); this.callInProgress.set(true); clearTimeout(this.callInProgressTimeout); @@ -446,7 +300,7 @@ class WebRTCClass { } const remoteConnections = [ { - id: data.from!, + id: data.from, media: data.media, }, ...data.remoteConnections, @@ -463,7 +317,11 @@ class WebRTCClass { }); } - getPeerConnection(id: string) { + /* + @param id {String} + */ + + getPeerConnection(id) { if (this.peerConnections[id] != null) { return this.peerConnections[id]; } @@ -528,10 +386,8 @@ class WebRTCClass { return peerConnection; } - audioContext: AudioContext | undefined; - - _getUserMedia(media: MediaStreamConstraints, onSuccess: (stream: MediaStream) => void, onError: (error?: any) => void) { - const onSuccessLocal = (stream: MediaStream) => { + _getUserMedia(media, onSuccess, onError) { + const onSuccessLocal = (stream) => { if (AudioContext && stream.getAudioTracks().length > 0) { const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(stream); @@ -547,24 +403,23 @@ class WebRTCClass { } onSuccess(stream); }; - - if (navigator.mediaDevices?.getUserMedia) { + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { return navigator.mediaDevices.getUserMedia(media).then(onSuccessLocal).catch(onError); } - navigator.getUserMedia?.(media, onSuccessLocal, onError); + navigator.getUserMedia(media, onSuccessLocal, onError); } - getUserMedia(media: MediaStreamConstraints, onSuccess: (stream: MediaStream) => void, onError: (error: any) => void = this.onError) { + getUserMedia(media, onSuccess, onError = this.onError) { if (media.desktop !== true) { - void this._getUserMedia(media, onSuccess, onError); + this._getUserMedia(media, onSuccess, onError); return; } if (this.screenShareAvailable !== true) { console.log('Screen share is not avaliable'); return; } - const getScreen = (audioStream?: MediaStream) => { + const getScreen = (audioStream) => { const refresh = function () { imperativeModal.open({ component: GenericModal, @@ -611,7 +466,7 @@ class WebRTCClass { return onError(false); } - const getScreenSuccess = (stream: MediaStream) => { + const getScreenSuccess = (stream) => { if (audioStream != null) { stream.addTrack(audioStream.getAudioTracks()[0]); } @@ -625,9 +480,9 @@ class WebRTCClass { mediaSource: 'window', }, }; - void this._getUserMedia(media, getScreenSuccess, onError); + this._getUserMedia(media, getScreenSuccess, onError); } else { - ChromeScreenShare.getSourceId(this.navigator!, (id) => { + ChromeScreenShare.getSourceId(this.navigator, (id) => { media = { audio: false, video: { @@ -639,21 +494,21 @@ class WebRTCClass { }, }, }; - void this._getUserMedia(media, getScreenSuccess, onError); + this._getUserMedia(media, getScreenSuccess, onError); }); } }; if (this.navigator === 'firefox' || media.audio == null || media.audio === false) { getScreen(); } else { - const getAudioSuccess = (audioStream: MediaStream) => { + const getAudioSuccess = (audioStream) => { getScreen(audioStream); }; const getAudioError = () => { getScreen(); }; - void this._getUserMedia( + this._getUserMedia( { audio: media.audio, }, @@ -663,29 +518,37 @@ class WebRTCClass { } } - getLocalUserMedia(callback: (...args: any[]) => void, ...args: unknown[]) { + /* + @param callback {Function} + */ + + getLocalUserMedia(callback, ...args) { this.log('getLocalUserMedia', [callback, ...args]); if (this.localStream != null) { return callback(null, this.localStream); } - const onSuccess = (stream: MediaStream) => { + const onSuccess = (stream) => { this.localStream = stream; !this.audioEnabled.get() && this.disableAudio(); !this.videoEnabled.get() && this.disableVideo(); this.localUrl.set(stream); const { peerConnections } = this; Object.entries(peerConnections).forEach(([, peerConnection]) => peerConnection.addStream(stream)); - document.querySelector('video#localVideo')!.srcObject = stream; + document.querySelector('video#localVideo').srcObject = stream; callback(null, this.localStream); }; - const onError = (error: any) => { + const onError = (error) => { callback(false); this.onError(error); }; this.getUserMedia(this.media, onSuccess, onError); } - stopPeerConnection = (id: string) => { + /* + @param id {String} + */ + + stopPeerConnection = (id) => { const peerConnection = this.peerConnections[id]; if (peerConnection == null) { return; @@ -700,7 +563,7 @@ class WebRTCClass { Object.keys(peerConnections).forEach(this.stopPeerConnection); - void window.audioContext?.close(); // FIXME: probably should be `this.audioContext` + window.audioContext && window.audioContext.close(); } setAudioEnabled(enabled = true) { @@ -727,8 +590,6 @@ class WebRTCClass { return this.enableAudio(); } - localStream: MediaStream | undefined; - setVideoEnabled(enabled = true) { if (this.localStream != null) { this.localStream.getVideoTracks().forEach((video) => { @@ -788,7 +649,13 @@ class WebRTCClass { this.stopAllPeerConnections(); } - startCall(media: MediaStreamConstraints = {}, ...args: unknown[]) { + /* + @param media {Object} + audio {Boolean} + video {Boolean} + */ + + startCall(media = {}, ...args) { this.log('startCall', [media, ...args]); this.media = media; this.getLocalUserMedia(() => { @@ -799,7 +666,7 @@ class WebRTCClass { }); } - startCallAsMonitor(media: MediaStreamConstraints = {}, ...args: unknown[]) { + startCallAsMonitor(media = {}, ...args) { this.log('startCallAsMonitor', [media, ...args]); this.media = media; this.active = true; @@ -810,7 +677,16 @@ class WebRTCClass { }); } - onRemoteCall(data: CallData) { + /* + @param data {Object} + from {String} + monitor {Boolean} + media {Object} + audio {Boolean} + video {Boolean} + */ + + onRemoteCall(data) { if (this.autoAccept === true) { setTimeout(() => { this.joinCall({ @@ -824,31 +700,31 @@ class WebRTCClass { const user = Meteor.users.findOne(data.from); let fromUsername = undefined; - if (user?.username) { + if (user && user.username) { fromUsername = user.username; } const subscription = ChatSubscription.findOne({ rid: data.room, - })!; + }); let icon; let title; if (data.monitor === true) { - icon = 'eye' as const; + icon = 'eye'; title = t('WebRTC_monitor_call_from_%s', fromUsername); } else if (subscription && subscription.t === 'd') { - if (data.media?.video) { - icon = 'video' as const; + if (data.media && data.media.video) { + icon = 'videocam'; title = t('WebRTC_direct_video_call_from_%s', fromUsername); } else { - icon = 'phone' as const; + icon = 'phone'; title = t('WebRTC_direct_audio_call_from_%s', fromUsername); } - } else if (data.media?.video) { - icon = 'video' as const; + } else if (data.media && data.media.video) { + icon = 'videocam'; title = t('WebRTC_group_video_call_from_%s', subscription.name); } else { - icon = 'phone' as const; + icon = 'phone'; title = t('WebRTC_group_audio_call_from_%s', subscription.name); } @@ -861,7 +737,7 @@ class WebRTCClass { cancelText: t('No'), children: t('Do_you_want_to_accept'), onConfirm: () => { - void goToRoomById(data.room!); + goToRoomById(data.room); return this.joinCall({ to: data.from, monitor: data.monitor, @@ -874,22 +750,32 @@ class WebRTCClass { }); } - joinCall(data: JoinData = {}, ...args: unknown[]) { + /* + @param data {Object} + to {String} + monitor {Boolean} + media {Object} + audio {Boolean} + video {Boolean} + desktop {Boolean} + */ + + joinCall(data = {}, ...args) { data.media = this.media; this.log('joinCall', [data, ...args]); this.getLocalUserMedia(() => { - this.remoteMonitoring = data.monitor!; + this.remoteMonitoring = data.monitor; this.active = true; this.transport.joinCall(data); }); } - onRemoteJoin(data: JoinData, ...args: unknown[]) { + onRemoteJoin(data, ...args) { if (this.active !== true) { return; } this.log('onRemoteJoin', [data, ...args]); - let peerConnection = this.getPeerConnection(data.from!); + let peerConnection = this.getPeerConnection(data.from); // needsRefresh = false // if peerConnection.iceConnectionState isnt 'new' @@ -899,18 +785,18 @@ class WebRTCClass { // # if peerConnection.signalingState is "have-local-offer" or needsRefresh - if ((peerConnection.signalingState as RTCSignalingState | 'checking') !== 'checking') { - this.stopPeerConnection(data.from!); - peerConnection = this.getPeerConnection(data.from!); + if (peerConnection.signalingState !== 'checking') { + this.stopPeerConnection(data.from); + peerConnection = this.getPeerConnection(data.from); } if (peerConnection.iceConnectionState !== 'new') { return; } - peerConnection.remoteMedia = data.media!; + peerConnection.remoteMedia = data.media; if (this.localStream) { peerConnection.addStream(this.localStream); } - const onOffer: RTCSessionDescriptionCallback = (offer) => { + const onOffer = (offer) => { const onLocalDescription = () => { this.transport.sendDescription({ to: data.from, @@ -924,39 +810,39 @@ class WebRTCClass { }); }; - void peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError); + peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError); }; if (data.monitor === true) { - void peerConnection.createOffer(onOffer, this.onError, { + peerConnection.createOffer(onOffer, this.onError, { mandatory: { - OfferToReceiveAudio: data.media?.audio, - OfferToReceiveVideo: data.media?.video, + OfferToReceiveAudio: data.media.audio, + OfferToReceiveVideo: data.media.video, }, }); } else { - void peerConnection.createOffer(onOffer, this.onError); + peerConnection.createOffer(onOffer, this.onError); } } - onRemoteOffer(data: Omit, ...args: unknown[]) { + onRemoteOffer(data, ...args) { if (this.active !== true) { return; } this.log('onRemoteOffer', [data, ...args]); - let peerConnection = this.getPeerConnection(data.from!); + let peerConnection = this.getPeerConnection(data.from); if (['have-local-offer', 'stable'].includes(peerConnection.signalingState) && peerConnection.createdAt < data.ts) { - this.stopPeerConnection(data.from!); - peerConnection = this.getPeerConnection(data.from!); + this.stopPeerConnection(data.from); + peerConnection = this.getPeerConnection(data.from); } if (peerConnection.iceConnectionState !== 'new') { return; } - void peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); + peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); try { if (this.localStream) { @@ -966,7 +852,7 @@ class WebRTCClass { console.log(error); } - const onAnswer: RTCSessionDescriptionCallback = (answer) => { + const onAnswer = (answer) => { const onLocalDescription = () => { this.transport.sendDescription({ to: data.from, @@ -979,13 +865,20 @@ class WebRTCClass { }); }; - void peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError); + peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError); }; - void peerConnection.createAnswer(onAnswer, this.onError); + peerConnection.createAnswer(onAnswer, this.onError); } - onRemoteCandidate(data: CandidateData, ...args: unknown[]) { + /* + @param data {Object} + to {String} + from {String} + candidate {RTCIceCandidate JSON encoded} + */ + + onRemoteCandidate(data, ...args) { if (this.active !== true) { return; } @@ -993,19 +886,32 @@ class WebRTCClass { return; } this.log('onRemoteCandidate', [data, ...args]); - const peerConnection = this.getPeerConnection(data.from!); + const peerConnection = this.getPeerConnection(data.from); if ( peerConnection.iceConnectionState !== 'closed' && peerConnection.iceConnectionState !== 'failed' && peerConnection.iceConnectionState !== 'disconnected' && peerConnection.iceConnectionState !== 'completed' ) { - void peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); + peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); } - document.querySelector('video#remoteVideo')!.srcObject = this.remoteItems.get()[0]?.url; + document.querySelector('video#remoteVideo').srcObject = this.remoteItems.get()[0]?.url; } - onRemoteDescription(data: DescriptionData, ...args: unknown[]) { + /* + @param data {Object} + to {String} + from {String} + type {String} [offer, answer] + description {RTCSessionDescription JSON encoded} + ts {Integer} + media {Object} + audio {Boolean} + video {Boolean} + desktop {Boolean} + */ + + onRemoteDescription(data, ...args) { if (this.active !== true) { return; } @@ -1013,7 +919,7 @@ class WebRTCClass { return; } this.log('onRemoteDescription', [data, ...args]); - const peerConnection = this.getPeerConnection(data.from!); + const peerConnection = this.getPeerConnection(data.from); if (data.type === 'offer') { peerConnection.remoteMedia = data.media; this.onRemoteOffer({ @@ -1022,19 +928,17 @@ class WebRTCClass { description: data.description, }); } else { - void peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); + peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); } } } const WebRTC = new (class { - instancesByRoomId: Record = {}; - constructor() { this.instancesByRoomId = {}; } - getInstanceByRoomId(rid: IRoom['_id'], visitorId: string | null = null) { + getInstanceByRoomId(rid, visitorId = null) { let enabled = false; if (!visitorId) { const subscription = ChatSubscription.findOne({ rid }); @@ -1052,17 +956,17 @@ const WebRTC = new (class { enabled = settings.get('WebRTC_Enable_Channel'); break; case 'l': - enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; + enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; } } else { - enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; + enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; } enabled = enabled && settings.get('WebRTC_Enabled'); if (enabled === false) { return; } if (this.instancesByRoomId[rid] == null) { - let uid = Meteor.userId()!; + let uid = Meteor.userId(); let autoAccept = false; if (visitorId) { uid = visitorId; @@ -1076,26 +980,13 @@ const WebRTC = new (class { Meteor.startup(() => { Tracker.autorun(() => { - const uid = Meteor.userId(); - - if (uid) { - sdk.stream('notify-user', [`${uid}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { + if (Meteor.userId()) { + sdk.stream('notify-user', [`${Meteor.userId()}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { if (data.room == null) { return; } const webrtc = WebRTC.getInstanceByRoomId(data.room); - - switch (type) { - case 'candidate': - webrtc?.onUserStream('candidate', data); - break; - case 'description': - webrtc?.onUserStream('description', data); - break; - case 'join': - webrtc?.onUserStream('join', data); - break; - } + webrtc.onUserStream(type, data); }); } }); diff --git a/apps/meteor/app/webrtc/client/adapter.js b/apps/meteor/app/webrtc/client/adapter.js new file mode 100644 index 0000000000000..972e68e09f3cb --- /dev/null +++ b/apps/meteor/app/webrtc/client/adapter.js @@ -0,0 +1,6 @@ +window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; +window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription; +window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate; +window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription; +window.AudioContext = window.AudioContext || window.mozAudioContext || window.webkitAudioContext; +navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia; diff --git a/apps/meteor/app/webrtc/client/adapter.ts b/apps/meteor/app/webrtc/client/adapter.ts deleted file mode 100644 index f98ae7815c051..0000000000000 --- a/apps/meteor/app/webrtc/client/adapter.ts +++ /dev/null @@ -1,7 +0,0 @@ -// FIXME: probably outdated -window.RTCPeerConnection = window.RTCPeerConnection ?? window.mozRTCPeerConnection ?? window.webkitRTCPeerConnection; -window.RTCSessionDescription = window.RTCSessionDescription ?? window.mozRTCSessionDescription ?? window.webkitRTCSessionDescription; -window.RTCIceCandidate = window.RTCIceCandidate ?? window.mozRTCIceCandidate ?? window.webkitRTCIceCandidate; -window.RTCSessionDescription = window.RTCSessionDescription ?? window.mozRTCSessionDescription ?? window.webkitRTCSessionDescription; -window.AudioContext = window.AudioContext ?? window.mozAudioContext ?? window.webkitAudioContext; -navigator.getUserMedia = navigator.getUserMedia ?? navigator.mozGetUserMedia ?? navigator.webkitGetUserMedia; diff --git a/apps/meteor/app/webrtc/client/screenShare.ts b/apps/meteor/app/webrtc/client/screenShare.js similarity index 77% rename from apps/meteor/app/webrtc/client/screenShare.ts rename to apps/meteor/app/webrtc/client/screenShare.js index 3fac4a05bfea7..ecb6f93a51d09 100644 --- a/apps/meteor/app/webrtc/client/screenShare.ts +++ b/apps/meteor/app/webrtc/client/screenShare.js @@ -1,20 +1,18 @@ import { fireGlobalEvent } from '../../../client/lib/utils/fireGlobalEvent'; export const ChromeScreenShare = { - callbacks: { - 'get-RocketChatScreenSharingExtensionVersion': (version: unknown) => { - if (version) { - ChromeScreenShare.installed = true; - } - }, - 'getSourceId': (_sourceId: string): void => undefined, - }, + callbacks: {}, installed: false, init() { + this.callbacks['get-RocketChatScreenSharingExtensionVersion'] = (version) => { + if (version) { + this.installed = true; + } + }; window.postMessage('get-RocketChatScreenSharingExtensionVersion', '*'); }, - getSourceId(navigator: string, callback: (sourceId: string) => void) { - if (!callback) { + getSourceId(navigator, callback) { + if (callback == null) { throw new Error('"callback" parameter is mandatory.'); } this.callbacks.getSourceId = callback; @@ -38,7 +36,8 @@ window.addEventListener('message', (e) => { throw new Error('PermissionDeniedError'); } if (e.data.version != null) { - ChromeScreenShare.callbacks['get-RocketChatScreenSharingExtensionVersion']?.(e.data.version); + ChromeScreenShare.callbacks['get-RocketChatScreenSharingExtensionVersion'] && + ChromeScreenShare.callbacks['get-RocketChatScreenSharingExtensionVersion'](e.data.version); } else if (e.data.sourceId != null) { return typeof ChromeScreenShare.callbacks.getSourceId === 'function' && ChromeScreenShare.callbacks.getSourceId(e.data.sourceId); } diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx index abf8ca432c28b..5bf174362e194 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx @@ -1,16 +1,15 @@ import { Sidebar } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useOmnichannelAgentAvailable } from '../../hooks/omnichannel/useOmnichannelAgentAvailable'; type NavBarItemOmnichannelLivechatToggleProps = Omit, 'icon'>; const NavBarItemOmnichannelLivechatToggle = (props: NavBarItemOmnichannelLivechatToggleProps): ReactElement => { - const { t } = useTranslation(); + const t = useTranslation(); const agentAvailable = useOmnichannelAgentAvailable(); const changeAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status'); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx index 7f47611f9bdb8..7c8a50338e7dc 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx @@ -1,16 +1,15 @@ import { NavBarItem } from '@rocket.chat/fuselage'; import { GenericMenu } from '@rocket.chat/ui-client'; -import { useCurrentRoutePath } from '@rocket.chat/ui-contexts'; +import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useAuditMenu } from './hooks/useAuditMenu'; type NavBarItemAuditMenuProps = Omit, 'is'>; const NavBarItemAuditMenu = (props: NavBarItemAuditMenuProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const sections = useAuditMenu(); const currentRoute = useCurrentRoutePath(); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx index 1e7fbdefb0831..85687bb12a2e1 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx @@ -1,16 +1,15 @@ import { NavBarItem } from '@rocket.chat/fuselage'; import { GenericMenu } from '@rocket.chat/ui-client'; -import { useCurrentRoutePath } from '@rocket.chat/ui-contexts'; +import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useMarketPlaceMenu } from './hooks/useMarketPlaceMenu'; type NavBarItemMarketPlaceMenuProps = Omit, 'is'>; const NavBarItemMarketPlaceMenu = (props: NavBarItemMarketPlaceMenuProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const sections = useMarketPlaceMenu(); const currentRoute = useCurrentRoutePath(); diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx index 7c0c36dc9f24a..97c8d7299497f 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx @@ -1,12 +1,11 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { usePermission, useRouter } from '@rocket.chat/ui-contexts'; -import { useTranslation } from 'react-i18next'; +import { usePermission, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; export const useAuditMenu = () => { const router = useRouter(); - const { t } = useTranslation(); + const t = useTranslation(); const hasAuditLicense = useHasLicenseModule('auditing') === true; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx index a17061050ce9b..8236eec030e89 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx @@ -1,16 +1,15 @@ import { NavBarItem } from '@rocket.chat/fuselage'; import { GenericMenu } from '@rocket.chat/ui-client'; -import { useCurrentRoutePath } from '@rocket.chat/ui-contexts'; +import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useAdministrationMenu } from './hooks/useAdministrationMenu'; type NavBarItemAdministrationMenuProps = Omit, 'is'>; const NavBarItemAdministrationMenu = (props: NavBarItemAdministrationMenuProps) => { - const { t } = useTranslation(); + const t = useTranslation(); const currentRoute = useCurrentRoutePath(); const sections = useAdministrationMenu(); diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemLoginPage.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemLoginPage.tsx index 1ef6f298fccd2..a02c17db0b9be 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemLoginPage.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemLoginPage.tsx @@ -1,14 +1,13 @@ import { Button } from '@rocket.chat/fuselage'; -import { useSessionDispatch } from '@rocket.chat/ui-contexts'; +import { useSessionDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; type NavBarItemLoginPageProps = Omit, 'is'>; const NavBarItemLoginPage = (props: NavBarItemLoginPageProps) => { const setForceLogin = useSessionDispatch('forceLogin'); - const { t } = useTranslation(); + const t = useTranslation(); return (