From 92e02f24c9684ebd2c2d8fdae73bcc720aaea2d6 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 27 Oct 2020 23:01:06 -0300 Subject: [PATCH] [NEW][Enterprise] Micro services (#19000) Co-authored-by: Rodrigo Nascimento Co-authored-by: Alan Sikora --- .eslintignore | 1 + .github/workflows/build_and_test.yml | 37 + .meteorignore | 1 + .scripts/start.js | 13 +- app/api/server/v1/settings.js | 2 + app/apps/server/bridges/messages.js | 20 +- app/apps/server/communication/websockets.js | 15 +- .../server/lib/restrictLoginAttempts.ts | 4 +- app/authorization/lib/AuthorizationUtils.ts | 14 +- .../server/functions/canAccessRoom.js | 40 - .../server/functions/canAccessRoom.ts | 8 + .../server/functions/hasPermission.js | 63 +- app/authorization/server/index.js | 2 - app/authorization/server/lib/streamer.js | 5 - .../server/methods/addUserToRole.js | 4 +- .../server/methods/deleteRole.js | 10 +- .../server/methods/removeUserFromRole.js | 4 +- app/authorization/server/methods/saveRole.js | 9 +- app/authorization/server/startup.js | 9 - .../server/streamer/permissions/emitter.js | 45 - .../server/streamer/permissions/index.js | 1 - app/discussion/server/authorization.js | 10 - app/discussion/server/index.js | 1 - .../server/methods/deleteEmojiCustom.js | 11 +- .../server/methods/insertOrUpdateEmoji.js | 6 +- .../server/methods/uploadEmojiCustom.js | 4 +- app/google-vision/server/googlevision.js | 8 +- .../server/classes/ImporterWebsocket.js | 7 +- app/integrations/server/index.js | 1 - app/integrations/server/lib/triggerHandler.js | 22 +- .../server/methods/clearIntegrationHistory.js | 4 +- app/integrations/server/streamer.js | 30 - app/ldap/server/sync.js | 8 +- app/lib/server/functions/deleteUser.js | 4 +- app/lib/server/functions/setRealName.js | 4 +- app/lib/server/functions/setRoomAvatar.js | 6 +- app/lib/server/functions/setStatusText.js | 30 +- app/lib/server/functions/setUserAvatar.js | 4 +- app/lib/server/functions/setUsername.js | 4 +- app/lib/server/index.js | 1 - app/lib/server/lib/msgStream.js | 58 +- app/lib/server/methods/addUsersToRoom.js | 8 +- app/lib/server/methods/filterATAllTag.js | 8 +- app/lib/server/methods/filterATHereTag.js | 8 +- app/lib/server/methods/sendMessage.js | 8 +- app/lib/server/startup/userDataStream.js | 104 - app/livechat/server/index.js | 2 - app/livechat/server/lib/Helper.js | 3 +- app/livechat/server/lib/Livechat.js | 21 +- app/livechat/server/lib/stream/agentStatus.ts | 38 +- .../server/lib/stream/departmentAgents.js | 29 - .../server/lib/stream/queueManager.js | 52 - .../roomAccessValidator.compatibility.js | 51 + .../roomAccessValidator.internalService.ts | 22 + app/livechat/server/roomType.js | 4 - app/livechat/server/startup.js | 62 +- app/logger/server/streamer.js | 18 +- app/mentions/server/server.js | 9 +- app/meteor-accounts-saml/server/lib/Utils.ts | 2 +- app/models/server/models/LivechatRooms.js | 2 +- app/models/server/models/Subscriptions.js | 4 +- app/models/server/models/Users.js | 2 +- app/models/server/models/_BaseDb.js | 10 +- app/models/server/models/_oplogHandle.ts | 46 +- app/models/server/raw/BaseRaw.js | 52 - app/models/server/raw/BaseRaw.ts | 94 + app/models/server/raw/InstanceStatus.ts | 4 + app/models/server/raw/IntegrationHistory.ts | 4 + .../server/raw/LivechatBusinessHours.ts | 4 +- .../server/raw/LoginServiceConfiguration.ts | 4 + app/models/server/raw/NotificationQueue.ts | 2 +- app/models/server/raw/OmnichannelQueue.ts | 8 +- app/models/server/raw/Permissions.js | 4 - app/models/server/raw/Permissions.ts | 5 + app/models/server/raw/Roles.js | 10 +- app/models/server/raw/ServerEvents.ts | 2 +- app/models/server/raw/Settings.js | 37 - app/models/server/raw/Settings.ts | 53 + app/models/server/raw/Subscriptions.js | 57 - app/models/server/raw/Subscriptions.ts | 72 + app/models/server/raw/Users.js | 18 + app/models/server/raw/UsersSessions.ts | 4 + app/models/server/raw/index.ts | 114 +- app/notifications/server/lib/Notifications.js | 212 - app/notifications/server/lib/Notifications.ts | 54 + app/reactions/server/setReaction.js | 8 +- app/search/server/events/events.js | 41 +- app/search/server/index.js | 1 + app/search/server/search.internalService.ts | 45 + app/settings/client/lib/settings.ts | 3 +- app/settings/lib/settings.ts | 5 +- app/settings/server/functions/settings.ts | 65 +- app/settings/server/raw.js | 2 +- .../server/server.js | 18 +- app/slashcommands-create/server/server.js | 8 +- app/slashcommands-help/server/server.js | 8 +- app/slashcommands-hide/server/hide.js | 18 +- app/slashcommands-invite/server/server.js | 23 +- app/slashcommands-inviteall/server/server.js | 23 +- app/slashcommands-join/server/server.js | 8 +- app/slashcommands-kick/server/server.js | 13 +- app/slashcommands-leave/server/leave.js | 8 +- app/slashcommands-msg/server/server.js | 12 +- app/slashcommands-mute/server/mute.js | 13 +- app/slashcommands-mute/server/unmute.js | 13 +- app/slashcommands-status/lib/status.js | 13 +- .../server/server.js | 18 +- app/sms/server/services/twilio.js | 8 +- app/sms/server/services/voxtelesys.js | 8 +- .../roomAccessValidator.compatibility.js | 18 + .../roomAccessValidator.internalService.ts | 22 + app/tokenpass/server/startup.js | 23 +- .../server/methods/deleteCustomUserStatus.js | 15 +- .../methods/insertOrUpdateUserStatus.js | 6 +- app/utils/lib/RoomTypeConfig.js | 4 - client/admin/sidebar/AdminSidebarSettings.tsx | 6 +- client/contexts/SettingsContext.ts | 2 +- .../omnichannel/appearance/AppearancePage.tsx | 2 +- definition/IBaseData.ts | 3 + definition/IEmoji.ts | 3 + definition/IInquiry.ts | 5 + definition/IInstanceStatus.ts | 6 + definition/IIntegration.ts | 5 + definition/IIntegrationHistory.ts | 14 + definition/ILivechatDepartmentAgents.ts | 7 + definition/ILoginServiceConfiguration.ts | 6 + definition/IMessage.ts | 10 +- definition/IPermission.ts | 12 + definition/IRole.ts | 5 + definition/IRoom.ts | 10 + definition/IRoutingManagerConfig.ts | 9 + definition/ISetting.ts | 53 +- definition/ISubscription.ts | 4 +- definition/IUser.ts | 9 +- definition/IUserSession.ts | 14 + definition/IUserStatus.ts | 3 + definition/UserStatus.ts | 6 + .../server/hooks/onRemoveAgentDepartment.ts | 4 +- .../server/hooks/onSaveAgentDepartment.ts | 4 +- .../server/methods/removeCannedResponse.js | 4 +- .../server/methods/saveCannedResponse.js | 4 +- ee/app/canned-responses/server/streamer.js | 11 - ee/app/license/server/bundles.ts | 1 + ee/app/license/server/index.ts | 1 + .../license/server/license.internalService.ts | 51 + ee/app/license/server/license.ts | 19 +- ee/app/license/server/startup.js | 2 - .../server/business-hour/Custom.ts | 10 +- .../server/business-hour/Helper.ts | 9 +- .../server/business-hour/Multiple.ts | 18 +- .../livechat-enterprise/server/lib/Helper.js | 4 +- .../server/raw/LivechatDepartmentAgents.ts | 3 - ee/app/settings/server/index.js | 1 + .../server/settings.internalService.ts | 15 + ee/app/settings/server/settings.ts | 27 +- ee/server/broker.ts | 299 ++ ee/server/index.js | 3 + .../dashboards/json-exports/docker_rev2.json | 1007 ++++ .../json-exports/moleculer-metrics.json | 1349 +++++ .../json-exports/reverse-proxy_rev1.json | 1303 +++++ .../json-exports/rocketchat-metrics.json | 4725 +++++++++++++++++ .../dashboards/provider/prometheus.yml | 11 + .../provisioning/datasources/prometheus.yml | 9 + .../.config/prometheus/prometheus.yml | 25 + .../services/.config/services/service.env | 5 + .../.config/traefik/servers/localhost.yml | 20 + ee/server/services/.env | 1 + ee/server/services/.gitignore | 3 + ee/server/services/Dockerfile | 30 + ee/server/services/README.md | 76 + ee/server/services/account/Account.ts | 89 + ee/server/services/account/lib/utils.ts | 62 + ee/server/services/account/service.ts | 6 + ee/server/services/authorization/service.ts | 9 + ee/server/services/ddp-streamer/Client.ts | 177 + .../services/ddp-streamer/DDPStreamer.ts | 147 + .../services/ddp-streamer/Publication.ts | 61 + ee/server/services/ddp-streamer/Server.ts | 157 + ee/server/services/ddp-streamer/Streamer.ts | 68 + .../services/ddp-streamer/configureServer.ts | 171 + ee/server/services/ddp-streamer/constants.ts | 47 + ee/server/services/ddp-streamer/lib/utils.ts | 9 + ee/server/services/ddp-streamer/service.ts | 6 + .../services/ddp-streamer/streams/index.ts | 25 + .../services/ddp-streamer/types/IPacket.ts | 9 + ee/server/services/ddp-streamer/types/ws.d.ts | 18 + ee/server/services/docker-compose.yml | 167 + ee/server/services/ecosystem.config.js | 23 + ee/server/services/mongo.ts | 39 + ee/server/services/package-lock.json | 2354 ++++++++ ee/server/services/package.json | 40 + ee/server/services/presence/Presence.ts | 55 + .../presence/actions/newConnection.ts | 46 + .../presence/actions/removeConnection.ts | 23 + .../presence/actions/removeLostConnections.ts | 68 + .../services/presence/actions/setStatus.ts | 64 + .../presence/actions/updateUserPresence.ts | 39 + .../presence/lib/processConnectionStatus.ts | 25 + ee/server/services/presence/service.ts | 6 + ee/server/services/stream-hub/StreamHub.ts | 114 + ee/server/services/stream-hub/service.ts | 6 + ee/server/services/tsconfig.json | 49 + imports/users-presence/server/activeUsers.js | 9 +- package-lock.json | 179 +- package.json | 5 + server/lib/markRoomAsRead.ts | 2 +- server/main.d.ts | 51 + server/main.js | 5 +- server/methods/addRoomLeader.js | 4 +- server/methods/addRoomModerator.js | 4 +- server/methods/addRoomOwner.js | 4 +- server/methods/removeRoomLeader.js | 4 +- server/methods/removeRoomModerator.js | 4 +- server/methods/removeRoomOwner.js | 4 +- server/methods/resetAvatar.js | 7 +- server/modules/listeners/listeners.module.ts | 259 + .../notifications/notifications.module.ts | 422 ++ server/modules/streamer/streamer.module.ts | 373 ++ server/modules/watchers/publishFields.ts | 94 + server/modules/watchers/watchers.module.ts | 333 ++ server/publications/room/emitter.js | 42 - server/publications/room/index.js | 61 +- server/publications/settings/emitter.js | 53 - server/publications/settings/index.js | 1 - server/publications/spotlight.js | 2 +- server/publications/subscription/emitter.js | 37 - server/publications/subscription/index.js | 44 +- server/sdk/api.ts | 3 + server/sdk/index.ts | 23 + server/sdk/lib/Api.ts | 50 + server/sdk/lib/Events.ts | 53 + server/sdk/lib/LocalBroker.ts | 71 + server/sdk/lib/proxify.ts | 27 + server/sdk/types/IAccount.ts | 12 + server/sdk/types/IAuthorization.ts | 12 + server/sdk/types/IAuthorizationLivechat.ts | 5 + server/sdk/types/IAuthorizationTokenpass.ts | 5 + server/sdk/types/IBroker.ts | 30 + server/sdk/types/IEnterpriseSettings.ts | 6 + server/sdk/types/ILicense.ts | 9 + server/sdk/types/IMeteor.ts | 24 + server/sdk/types/IPresence.ts | 11 + server/sdk/types/ServiceClass.ts | 68 + .../services/authorization/canAccessRoom.ts | 59 + .../authorization/canAccessRoomLivechat.ts | 14 + .../authorization/canAccessRoomTokenpass.ts | 14 + server/services/authorization/service.ts | 116 + server/services/meteor/service.ts | 263 + server/services/startup.ts | 8 + server/startup/presence.js | 32 +- server/startup/serverRunning.js | 2 +- server/stream/messages/emitter.js | 39 - server/stream/messages/index.js | 51 - server/stream/rooms/index.js | 33 - server/stream/streamBroadcast.js | 32 +- typings.d.ts | 14 + 256 files changed, 16817 insertions(+), 2026 deletions(-) create mode 100644 .meteorignore delete mode 100644 app/authorization/server/functions/canAccessRoom.js create mode 100644 app/authorization/server/functions/canAccessRoom.ts delete mode 100644 app/authorization/server/lib/streamer.js delete mode 100644 app/authorization/server/streamer/permissions/emitter.js delete mode 100644 app/discussion/server/authorization.js delete mode 100644 app/integrations/server/streamer.js delete mode 100644 app/lib/server/startup/userDataStream.js delete mode 100644 app/livechat/server/lib/stream/departmentAgents.js delete mode 100644 app/livechat/server/lib/stream/queueManager.js create mode 100644 app/livechat/server/roomAccessValidator.compatibility.js create mode 100644 app/livechat/server/roomAccessValidator.internalService.ts delete mode 100644 app/models/server/raw/BaseRaw.js create mode 100644 app/models/server/raw/BaseRaw.ts create mode 100644 app/models/server/raw/InstanceStatus.ts create mode 100644 app/models/server/raw/IntegrationHistory.ts create mode 100644 app/models/server/raw/LoginServiceConfiguration.ts delete mode 100644 app/models/server/raw/Permissions.js create mode 100644 app/models/server/raw/Permissions.ts delete mode 100644 app/models/server/raw/Settings.js create mode 100644 app/models/server/raw/Settings.ts delete mode 100644 app/models/server/raw/Subscriptions.js create mode 100644 app/models/server/raw/Subscriptions.ts create mode 100644 app/models/server/raw/UsersSessions.ts delete mode 100644 app/notifications/server/lib/Notifications.js create mode 100644 app/notifications/server/lib/Notifications.ts create mode 100644 app/search/server/search.internalService.ts create mode 100644 app/tokenpass/server/roomAccessValidator.compatibility.js create mode 100644 app/tokenpass/server/roomAccessValidator.internalService.ts create mode 100644 definition/IBaseData.ts create mode 100644 definition/IEmoji.ts create mode 100644 definition/IInquiry.ts create mode 100644 definition/IInstanceStatus.ts create mode 100644 definition/IIntegration.ts create mode 100644 definition/IIntegrationHistory.ts create mode 100644 definition/ILivechatDepartmentAgents.ts create mode 100644 definition/ILoginServiceConfiguration.ts create mode 100644 definition/IPermission.ts create mode 100644 definition/IRole.ts create mode 100644 definition/IRoutingManagerConfig.ts create mode 100644 definition/IUserSession.ts create mode 100644 definition/IUserStatus.ts create mode 100644 definition/UserStatus.ts delete mode 100644 ee/app/canned-responses/server/streamer.js create mode 100644 ee/app/license/server/license.internalService.ts create mode 100644 ee/app/settings/server/settings.internalService.ts create mode 100644 ee/server/broker.ts create mode 100644 ee/server/services/.config/grafana/provisioning/dashboards/json-exports/docker_rev2.json create mode 100644 ee/server/services/.config/grafana/provisioning/dashboards/json-exports/moleculer-metrics.json create mode 100644 ee/server/services/.config/grafana/provisioning/dashboards/json-exports/reverse-proxy_rev1.json create mode 100644 ee/server/services/.config/grafana/provisioning/dashboards/json-exports/rocketchat-metrics.json create mode 100644 ee/server/services/.config/grafana/provisioning/dashboards/provider/prometheus.yml create mode 100644 ee/server/services/.config/grafana/provisioning/datasources/prometheus.yml create mode 100644 ee/server/services/.config/prometheus/prometheus.yml create mode 100644 ee/server/services/.config/services/service.env create mode 100644 ee/server/services/.config/traefik/servers/localhost.yml create mode 100644 ee/server/services/.env create mode 100644 ee/server/services/.gitignore create mode 100644 ee/server/services/Dockerfile create mode 100644 ee/server/services/README.md create mode 100644 ee/server/services/account/Account.ts create mode 100644 ee/server/services/account/lib/utils.ts create mode 100644 ee/server/services/account/service.ts create mode 100644 ee/server/services/authorization/service.ts create mode 100644 ee/server/services/ddp-streamer/Client.ts create mode 100644 ee/server/services/ddp-streamer/DDPStreamer.ts create mode 100644 ee/server/services/ddp-streamer/Publication.ts create mode 100644 ee/server/services/ddp-streamer/Server.ts create mode 100644 ee/server/services/ddp-streamer/Streamer.ts create mode 100644 ee/server/services/ddp-streamer/configureServer.ts create mode 100644 ee/server/services/ddp-streamer/constants.ts create mode 100644 ee/server/services/ddp-streamer/lib/utils.ts create mode 100755 ee/server/services/ddp-streamer/service.ts create mode 100644 ee/server/services/ddp-streamer/streams/index.ts create mode 100644 ee/server/services/ddp-streamer/types/IPacket.ts create mode 100644 ee/server/services/ddp-streamer/types/ws.d.ts create mode 100644 ee/server/services/docker-compose.yml create mode 100644 ee/server/services/ecosystem.config.js create mode 100644 ee/server/services/mongo.ts create mode 100644 ee/server/services/package-lock.json create mode 100644 ee/server/services/package.json create mode 100755 ee/server/services/presence/Presence.ts create mode 100755 ee/server/services/presence/actions/newConnection.ts create mode 100755 ee/server/services/presence/actions/removeConnection.ts create mode 100755 ee/server/services/presence/actions/removeLostConnections.ts create mode 100755 ee/server/services/presence/actions/setStatus.ts create mode 100755 ee/server/services/presence/actions/updateUserPresence.ts create mode 100644 ee/server/services/presence/lib/processConnectionStatus.ts create mode 100755 ee/server/services/presence/service.ts create mode 100755 ee/server/services/stream-hub/StreamHub.ts create mode 100755 ee/server/services/stream-hub/service.ts create mode 100644 ee/server/services/tsconfig.json create mode 100644 server/modules/listeners/listeners.module.ts create mode 100644 server/modules/notifications/notifications.module.ts create mode 100644 server/modules/streamer/streamer.module.ts create mode 100644 server/modules/watchers/publishFields.ts create mode 100644 server/modules/watchers/watchers.module.ts delete mode 100644 server/publications/room/emitter.js delete mode 100644 server/publications/settings/emitter.js delete mode 100644 server/publications/subscription/emitter.js create mode 100644 server/sdk/api.ts create mode 100644 server/sdk/index.ts create mode 100644 server/sdk/lib/Api.ts create mode 100644 server/sdk/lib/Events.ts create mode 100644 server/sdk/lib/LocalBroker.ts create mode 100644 server/sdk/lib/proxify.ts create mode 100644 server/sdk/types/IAccount.ts create mode 100644 server/sdk/types/IAuthorization.ts create mode 100644 server/sdk/types/IAuthorizationLivechat.ts create mode 100644 server/sdk/types/IAuthorizationTokenpass.ts create mode 100644 server/sdk/types/IBroker.ts create mode 100644 server/sdk/types/IEnterpriseSettings.ts create mode 100644 server/sdk/types/ILicense.ts create mode 100644 server/sdk/types/IMeteor.ts create mode 100644 server/sdk/types/IPresence.ts create mode 100644 server/sdk/types/ServiceClass.ts create mode 100644 server/services/authorization/canAccessRoom.ts create mode 100644 server/services/authorization/canAccessRoomLivechat.ts create mode 100644 server/services/authorization/canAccessRoomTokenpass.ts create mode 100644 server/services/authorization/service.ts create mode 100644 server/services/meteor/service.ts create mode 100644 server/services/startup.ts delete mode 100644 server/stream/messages/emitter.js delete mode 100644 server/stream/messages/index.js delete mode 100644 server/stream/rooms/index.js diff --git a/.eslintignore b/.eslintignore index ebdb084375a..2a39e35ae2b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,3 +18,4 @@ public/pdf.worker.min.js public/workers/**/* imports/client/ !/.storybook/ +ee/server/services/dist/** diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 7fc7c5486bb..be0e9131ce3 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -487,3 +487,40 @@ jobs: docker build -t ${IMAGE}:develop . docker push ${IMAGE}:develop + + services-image-build: + runs-on: ubuntu-latest + needs: deploy + if: github.event.pull_request.head.repo.full_name == github.repository + + strategy: + matrix: + service: ["account", "authorization", "ddp-streamer", "presence", "stream-hub"] + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js 12.18.4 + uses: actions/setup-node@v1 + with: + node-version: "12.18.4" + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASS }} + + - name: Build Docker images + run: | + npm i + + cd ./ee/server/services + npm i + npm run build + + echo "Building Docker image for service: ${{ matrix.service }}" + + docker build --build-arg SERVICE=${{ matrix.service }} -t rocketchat/${{ matrix.service }}-service . + + docker push rocketchat/${{ matrix.service }}-service diff --git a/.meteorignore b/.meteorignore new file mode 100644 index 00000000000..35dd55fee16 --- /dev/null +++ b/.meteorignore @@ -0,0 +1 @@ +ee/server/services diff --git a/.scripts/start.js b/.scripts/start.js index 1159290bbbb..8caf131893a 100644 --- a/.scripts/start.js +++ b/.scripts/start.js @@ -19,13 +19,17 @@ const isPortTaken = (port) => new Promise((resolve, reject) => { .listen(port); }); -const waitPortRelease = (port) => new Promise((resolve, reject) => { +const waitPortRelease = (port, count = 0) => new Promise((resolve, reject) => { isPortTaken(port).then((taken) => { if (!taken) { return resolve(); } + if (count > 60) { + return reject(); + } + console.log('Port', port, 'not release, waiting 1s...'); setTimeout(() => { - waitPortRelease(port).then(resolve).catch(reject); + waitPortRelease(port, ++count).then(resolve).catch(reject); }, 1000); }); }); @@ -77,7 +81,10 @@ function startProcess(opts, callback) { processes.splice(processes.indexOf(proc), 1); - processes.forEach((p) => p.kill()); + processes.forEach((p) => { + console.log('Killing process', p.pid); + p.kill(); + }); if (processes.length === 0) { waitPortRelease(appOptions.env.PORT).then(() => { diff --git a/app/api/server/v1/settings.js b/app/api/server/v1/settings.js index cf926f44d47..c7a7ca33c97 100644 --- a/app/api/server/v1/settings.js +++ b/app/api/server/v1/settings.js @@ -7,6 +7,7 @@ import { Settings } from '../../../models/server'; import { hasPermission } from '../../../authorization'; import { API } from '../api'; import { SettingsEvents, settings } from '../../../settings/server'; +import { setValue } from '../../../settings/server/raw'; const fetchSettings = (query, sort, offset, count, fields) => { const settings = Settings.find(query, { @@ -150,6 +151,7 @@ API.v1.addRoute('settings/:_id', { authRequired: true }, { _id: this.urlParams._id, value: this.bodyParams.value, }); + setValue(this.urlParams._id, this.bodyParams.value); return API.v1.success(); } diff --git a/app/apps/server/bridges/messages.js b/app/apps/server/bridges/messages.js index 15e5c445fd0..2e65ff8033b 100644 --- a/app/apps/server/bridges/messages.js +++ b/app/apps/server/bridges/messages.js @@ -1,9 +1,8 @@ -import { Random } from 'meteor/random'; - import { Messages, Users, Subscriptions } from '../../../models/server'; -import { Notifications } from '../../../notifications'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; +import { api } from '../../../../server/sdk/api'; +import notifications from '../../../notifications/server/lib/Notifications'; export class AppMessageBridge { constructor(orch) { @@ -52,10 +51,8 @@ export class AppMessageBridge { return; } - Notifications.notifyUser(user.id, 'message', { + api.broadcast('stream.ephemeralMessage', user.id, msg.rid, { ...msg, - _id: Random.id(), - ts: new Date(), }); } @@ -67,11 +64,6 @@ export class AppMessageBridge { } const msg = this.orch.getConverters().get('messages').convertAppMessage(message); - const rmsg = Object.assign(msg, { - _id: Random.id(), - rid: room.id, - ts: new Date(), - }); const users = Subscriptions.findByRoomIdWhenUserIdExists(room.id, { fields: { 'u._id': 1 } }) .fetch() @@ -80,14 +72,16 @@ export class AppMessageBridge { Users.findByIds(users, { fields: { _id: 1 } }) .fetch() .forEach(({ _id }) => - Notifications.notifyUser(_id, 'message', rmsg), + api.broadcast('stream.ephemeralMessage', _id, room.id, { + ...msg, + }), ); } async typing({ scope, id, username, isTyping }) { switch (scope) { case 'room': - Notifications.notifyRoom(id, 'typing', username, isTyping); + notifications.notifyRoom(id, 'typing', username, isTyping); return; default: throw new Error('Unrecognized typing scope provided'); diff --git a/app/apps/server/communication/websockets.js b/app/apps/server/communication/websockets.js index e5615c0e2d9..ead46d67e59 100644 --- a/app/apps/server/communication/websockets.js +++ b/app/apps/server/communication/websockets.js @@ -1,6 +1,7 @@ -import { Meteor } from 'meteor/meteor'; import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import notifications from '../../../notifications/server/lib/Notifications'; + export const AppEvents = Object.freeze({ APP_ADDED: 'app/added', APP_REMOVED: 'app/removed', @@ -101,18 +102,10 @@ export class AppServerListener { export class AppServerNotifier { constructor(orch) { - this.engineStreamer = new Meteor.Streamer('apps-engine', { retransmit: false }); - this.engineStreamer.serverOnly = true; - this.engineStreamer.allowRead('none'); - this.engineStreamer.allowEmit('all'); - this.engineStreamer.allowWrite('none'); + this.engineStreamer = notifications.streamAppsEngine; // This is used to broadcast to the web clients - this.clientStreamer = new Meteor.Streamer('apps', { retransmit: false }); - this.clientStreamer.serverOnly = true; - this.clientStreamer.allowRead('all'); - this.clientStreamer.allowEmit('all'); - this.clientStreamer.allowWrite('none'); + this.clientStreamer = notifications.streamApps; this.received = new Map(); this.listener = new AppServerListener(orch, this.engineStreamer, this.clientStreamer, this.received); diff --git a/app/authentication/server/lib/restrictLoginAttempts.ts b/app/authentication/server/lib/restrictLoginAttempts.ts index 0e1fa7ac237..ef3b9368816 100644 --- a/app/authentication/server/lib/restrictLoginAttempts.ts +++ b/app/authentication/server/lib/restrictLoginAttempts.ts @@ -18,10 +18,10 @@ export const isValidLoginAttemptByIp = async (ip: string): Promise => { return true; } - const lastLogin = await Sessions.findLastLoginByIp(ip); + const lastLogin = await Sessions.findLastLoginByIp(ip) as {loginAt?: Date} | undefined; let failedAttemptsSinceLastLogin; - if (!lastLogin) { + if (!lastLogin || !lastLogin.loginAt) { failedAttemptsSinceLastLogin = await ServerEvents.countFailedAttemptsByIp(ip); } else { failedAttemptsSinceLastLogin = await ServerEvents.countFailedAttemptsByIpSince(ip, new Date(lastLogin.loginAt)); diff --git a/app/authorization/lib/AuthorizationUtils.ts b/app/authorization/lib/AuthorizationUtils.ts index daa19828305..41c96bd6fd3 100644 --- a/app/authorization/lib/AuthorizationUtils.ts +++ b/app/authorization/lib/AuthorizationUtils.ts @@ -1,15 +1,13 @@ -import { Meteor } from 'meteor/meteor'; - const restrictedRolePermissions = new Map(); export const AuthorizationUtils = class { - static addRolePermissionWhiteList(roleId: string, list: [string]): void { + static addRolePermissionWhiteList(roleId: string, list: string[]): void { if (!roleId) { - throw new Meteor.Error('invalid-param'); + throw new Error('invalid-param'); } if (!list) { - throw new Meteor.Error('invalid-param'); + throw new Error('invalid-param'); } if (!restrictedRolePermissions.has(roleId)) { @@ -25,7 +23,7 @@ export const AuthorizationUtils = class { static isPermissionRestrictedForRole(permissionId: string, roleId: string): boolean { if (!roleId || !permissionId) { - throw new Meteor.Error('invalid-param'); + throw new Error('invalid-param'); } if (!restrictedRolePermissions.has(roleId)) { @@ -40,9 +38,9 @@ export const AuthorizationUtils = class { return !rules.has(permissionId); } - static isPermissionRestrictedForRoleList(permissionId: string, roleList: [string]): boolean { + static isPermissionRestrictedForRoleList(permissionId: string, roleList: string[]): boolean { if (!roleList || !permissionId) { - throw new Meteor.Error('invalid-param'); + throw new Error('invalid-param'); } for (const roleId of roleList) { diff --git a/app/authorization/server/functions/canAccessRoom.js b/app/authorization/server/functions/canAccessRoom.js deleted file mode 100644 index abfa3e70c63..00000000000 --- a/app/authorization/server/functions/canAccessRoom.js +++ /dev/null @@ -1,40 +0,0 @@ -import { hasPermissionAsync } from './hasPermission'; -import { Subscriptions } from '../../../models/server/raw'; -import { getValue } from '../../../settings/server/raw'; - -export const roomAccessValidators = [ - async function(room, user = {}) { - if (room && room.t === 'c') { - const anonymous = await getValue('Accounts_AllowAnonymousRead'); - if (!user._id && anonymous === true) { - return true; - } - - return hasPermissionAsync(user._id, 'view-c-room'); - } - }, - async function(room, user) { - if (!room || !user) { - return; - } - - const exists = await Subscriptions.countByRoomIdAndUserId(room._id, user._id); - if (exists) { - return true; - } - }, -]; - -export const canAccessRoomAsync = async (room, user, extraData) => { - for (let i = 0, total = roomAccessValidators.length; i < total; i++) { - // eslint-disable-next-line no-await-in-loop - const permitted = await roomAccessValidators[i](room, user, extraData); - if (permitted) { - return true; - } - } -}; - -export const canAccessRoom = (room, user, extraData) => Promise.await(canAccessRoomAsync(room, user, extraData)); - -export const addRoomAccessValidator = (validator) => roomAccessValidators.push(validator.bind(this)); diff --git a/app/authorization/server/functions/canAccessRoom.ts b/app/authorization/server/functions/canAccessRoom.ts new file mode 100644 index 00000000000..5a943ec031a --- /dev/null +++ b/app/authorization/server/functions/canAccessRoom.ts @@ -0,0 +1,8 @@ +import { Promise } from 'meteor/promise'; + +import { Authorization } from '../../../../server/sdk'; +import { IAuthorization } from '../../../../server/sdk/types/IAuthorization'; + +export const canAccessRoomAsync = Authorization.canAccessRoom; + +export const canAccessRoom = (...args: Parameters): boolean => Promise.await(canAccessRoomAsync(...args)); diff --git a/app/authorization/server/functions/hasPermission.js b/app/authorization/server/functions/hasPermission.js index eb3d9019160..e14bf7f869a 100644 --- a/app/authorization/server/functions/hasPermission.js +++ b/app/authorization/server/functions/hasPermission.js @@ -1,63 +1,8 @@ -import mem from 'mem'; +import { Authorization } from '../../../../server/sdk'; -import { Permissions, Users, Subscriptions } from '../../../models/server/raw'; -import { AuthorizationUtils } from '../../lib/AuthorizationUtils'; - -const rolesHasPermission = mem(async (permission, roles) => { - if (AuthorizationUtils.isPermissionRestrictedForRoleList(permission, roles)) { - return false; - } - - const result = await Permissions.findOne({ _id: permission, roles: { $in: roles } }, { projection: { _id: 1 } }); - return !!result; -}, { - cacheKey: JSON.stringify, - ...process.env.TEST_MODE === 'true' && { maxAge: 1 }, -}); - -const getRoles = mem(async (uid, scope) => { - const { roles: userRoles = [] } = await Users.findOne({ _id: uid }, { projection: { roles: 1 } }); - const { roles: subscriptionsRoles = [] } = (scope && await Subscriptions.findOne({ rid: scope, 'u._id': uid }, { projection: { roles: 1 } })) || {}; - return [...userRoles, ...subscriptionsRoles].sort((a, b) => a.localeCompare(b)); -}, { maxAge: 1000, cacheKey: JSON.stringify }); - -export const clearCache = () => { - mem.clear(getRoles); - mem.clear(rolesHasPermission); -}; - -async function atLeastOne(uid, permissions = [], scope) { - const sortedRoles = await getRoles(uid, scope); - for (const permission of permissions) { - if (await rolesHasPermission(permission, sortedRoles)) { // eslint-disable-line - return true; - } - } - - return false; -} - -async function all(uid, permissions = [], scope) { - const sortedRoles = await getRoles(uid, scope); - for (const permission of permissions) { - if (!await rolesHasPermission(permission, sortedRoles)) { // eslint-disable-line - return false; - } - } - - return true; -} - -function _hasPermission(userId, permissions, scope, strategy) { - if (!userId) { - return false; - } - return strategy(userId, [].concat(permissions), scope); -} - -export const hasAllPermissionAsync = async (userId, permissions, scope) => _hasPermission(userId, permissions, scope, all); -export const hasPermissionAsync = async (userId, permissionId, scope) => _hasPermission(userId, permissionId, scope, all); -export const hasAtLeastOnePermissionAsync = async (userId, permissions, scope) => _hasPermission(userId, permissions, scope, atLeastOne); +export const hasAllPermissionAsync = async (userId, permissions, scope) => Authorization.hasAllPermission(userId, permissions, scope); +export const hasPermissionAsync = async (userId, permissionId, scope) => Authorization.hasPermission(userId, permissionId, scope); +export const hasAtLeastOnePermissionAsync = async (userId, permissions, scope) => Authorization.hasAtLeastOnePermission(userId, permissions, scope); export const hasAllPermission = (userId, permissions, scope) => Promise.await(hasAllPermissionAsync(userId, permissions, scope)); export const hasPermission = (userId, permissionId, scope) => Promise.await(hasPermissionAsync(userId, permissionId, scope)); diff --git a/app/authorization/server/index.js b/app/authorization/server/index.js index c4ceb42c9a3..c248f1c6e0e 100644 --- a/app/authorization/server/index.js +++ b/app/authorization/server/index.js @@ -1,6 +1,5 @@ import { addUserRoles } from './functions/addUserRoles'; import { - addRoomAccessValidator, canAccessRoom, roomAccessValidators, } from './functions/canAccessRoom'; @@ -32,7 +31,6 @@ export { removeUserFromRoles, canSendMessage, validateRoomMessagePermissions, - addRoomAccessValidator, roomAccessValidators, addUserRoles, canAccessRoom, diff --git a/app/authorization/server/lib/streamer.js b/app/authorization/server/lib/streamer.js deleted file mode 100644 index f00103832be..00000000000 --- a/app/authorization/server/lib/streamer.js +++ /dev/null @@ -1,5 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -export const rolesStreamer = new Meteor.Streamer('roles'); -rolesStreamer.allowWrite('none'); -rolesStreamer.allowRead('logged'); diff --git a/app/authorization/server/methods/addUserToRole.js b/app/authorization/server/methods/addUserToRole.js index b9d8302bc0b..a7fdd21ec24 100644 --- a/app/authorization/server/methods/addUserToRole.js +++ b/app/authorization/server/methods/addUserToRole.js @@ -3,8 +3,8 @@ import _ from 'underscore'; import { Users, Roles } from '../../../models/server'; import { settings } from '../../../settings/server'; -import { Notifications } from '../../../notifications/server'; import { hasPermission } from '../functions/hasPermission'; +import { api } from '../../../../server/sdk/api'; Meteor.methods({ 'authorization:addUserToRole'(roleName, username, scope) { @@ -50,7 +50,7 @@ Meteor.methods({ const add = Roles.addUserRoles(user._id, roleName, scope); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'added', _id: roleName, u: { diff --git a/app/authorization/server/methods/deleteRole.js b/app/authorization/server/methods/deleteRole.js index 56ab719fe98..8613e1761b0 100644 --- a/app/authorization/server/methods/deleteRole.js +++ b/app/authorization/server/methods/deleteRole.js @@ -2,7 +2,6 @@ import { Meteor } from 'meteor/meteor'; import * as Models from '../../../models/server'; import { hasPermission } from '../functions/hasPermission'; -import { rolesStreamer } from '../lib/streamer'; Meteor.methods({ 'authorization:deleteRole'(roleName) { @@ -36,13 +35,6 @@ Meteor.methods({ }); } - const removed = Models.Roles.remove(role.name); - if (removed) { - rolesStreamer.emit('roles', { - type: 'removed', - name: roleName, - }); - } - return removed; + return Models.Roles.remove(role.name); }, }); diff --git a/app/authorization/server/methods/removeUserFromRole.js b/app/authorization/server/methods/removeUserFromRole.js index 4ccec8b2a7c..9a36a889587 100644 --- a/app/authorization/server/methods/removeUserFromRole.js +++ b/app/authorization/server/methods/removeUserFromRole.js @@ -3,8 +3,8 @@ import _ from 'underscore'; import { Roles } from '../../../models/server'; import { settings } from '../../../settings/server'; -import { Notifications } from '../../../notifications/server'; import { hasPermission } from '../functions/hasPermission'; +import { api } from '../../../../server/sdk/api'; Meteor.methods({ 'authorization:removeUserFromRole'(roleName, username, scope) { @@ -55,7 +55,7 @@ Meteor.methods({ const remove = Roles.removeUserRoles(user._id, roleName, scope); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'removed', _id: roleName, u: { diff --git a/app/authorization/server/methods/saveRole.js b/app/authorization/server/methods/saveRole.js index 64cb3437c9d..5e09f211240 100644 --- a/app/authorization/server/methods/saveRole.js +++ b/app/authorization/server/methods/saveRole.js @@ -2,9 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Roles } from '../../../models/server'; import { settings } from '../../../settings/server'; -import { Notifications } from '../../../notifications/server'; import { hasPermission } from '../functions/hasPermission'; -import { rolesStreamer } from '../lib/streamer'; +import { api } from '../../../../server/sdk/api'; Meteor.methods({ 'authorization:saveRole'(roleData) { @@ -27,15 +26,11 @@ Meteor.methods({ const update = Roles.createOrUpdate(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'changed', _id: roleData.name, }); } - rolesStreamer.emit('roles', { - type: 'changed', - ...roleData, - }); return update; }, }); diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index 1cada0ba7a5..2ca73f01092 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -4,7 +4,6 @@ import { Meteor } from 'meteor/meteor'; import { Roles, Permissions, Settings } from '../../models/server'; import { settings } from '../../settings/server'; import { getSettingPermissionId, CONSTANTS } from '../lib'; -import { clearCache } from './functions/hasPermission'; Meteor.startup(function() { // Note: @@ -224,12 +223,4 @@ Meteor.startup(function() { }; settings.onload('*', createPermissionForAddedSetting); - - Roles.on('change', ({ diff }) => { - if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { - // avoid useless changes - return; - } - clearCache(); - }); }); diff --git a/app/authorization/server/streamer/permissions/emitter.js b/app/authorization/server/streamer/permissions/emitter.js deleted file mode 100644 index aafdaf27418..00000000000 --- a/app/authorization/server/streamer/permissions/emitter.js +++ /dev/null @@ -1,45 +0,0 @@ -import Settings from '../../../../models/server/models/Settings'; -import { Notifications } from '../../../../notifications/server'; -import { CONSTANTS } from '../../../lib'; -import Permissions from '../../../../models/server/models/Permissions'; -import { clearCache } from '../../functions/hasPermission'; - -Permissions.on('change', ({ clientAction, id, data, diff }) => { - if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { - // avoid useless changes - return; - } - switch (clientAction) { - case 'updated': - case 'inserted': - data = data ?? Permissions.findOneById(id); - break; - - case 'removed': - data = { _id: id }; - break; - } - - clearCache(); - - Notifications.notifyLoggedInThisInstance( - 'permissions-changed', - clientAction, - data, - ); - - if (data.level && data.level === CONSTANTS.SETTINGS_LEVEL) { - // if the permission changes, the effect on the visible settings depends on the role affected. - // The selected-settings-based consumers have to react accordingly and either add or remove the - // setting from the user's collection - const setting = Settings.findOneNotHiddenById(data.settingId); - if (!setting) { - return; - } - Notifications.notifyLoggedInThisInstance( - 'private-settings-changed', - 'updated', - setting, - ); - } -}); diff --git a/app/authorization/server/streamer/permissions/index.js b/app/authorization/server/streamer/permissions/index.js index 09e7ccffc6c..edffbdfe3e7 100644 --- a/app/authorization/server/streamer/permissions/index.js +++ b/app/authorization/server/streamer/permissions/index.js @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import Permissions from '../../../../models/server/models/Permissions'; -import './emitter'; Meteor.methods({ 'permissions/get'(updatedAt) { diff --git a/app/discussion/server/authorization.js b/app/discussion/server/authorization.js deleted file mode 100644 index 6835305ed82..00000000000 --- a/app/discussion/server/authorization.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { addRoomAccessValidator, canAccessRoom } from '../../authorization'; -import { Rooms } from '../../models'; - -Meteor.startup(() => { - addRoomAccessValidator(function(room, user) { - return room && room.prid && canAccessRoom(Rooms.findOne(room.prid), user); - }); -}); diff --git a/app/discussion/server/index.js b/app/discussion/server/index.js index 32b341422bf..92e47178450 100644 --- a/app/discussion/server/index.js +++ b/app/discussion/server/index.js @@ -1,5 +1,4 @@ import './config'; -import './authorization'; import './permissions'; import './hooks/propagateDiscussionMetadata'; diff --git a/app/emoji-custom/server/methods/deleteEmojiCustom.js b/app/emoji-custom/server/methods/deleteEmojiCustom.js index 0e5b383f718..7393f245b45 100644 --- a/app/emoji-custom/server/methods/deleteEmojiCustom.js +++ b/app/emoji-custom/server/methods/deleteEmojiCustom.js @@ -1,27 +1,24 @@ import { Meteor } from 'meteor/meteor'; +import { api } from '../../../../server/sdk/api'; import { hasPermission } from '../../../authorization'; import { EmojiCustom } from '../../../models'; -import { Notifications } from '../../../notifications'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; Meteor.methods({ deleteEmojiCustom(emojiID) { - let emoji = null; - - if (hasPermission(this.userId, 'manage-emoji')) { - emoji = EmojiCustom.findOneById(emojiID); - } else { + if (!hasPermission(this.userId, 'manage-emoji')) { throw new Meteor.Error('not_authorized'); } + const emoji = EmojiCustom.findOneById(emojiID); if (emoji == null) { throw new Meteor.Error('Custom_Emoji_Error_Invalid_Emoji', 'Invalid emoji', { method: 'deleteEmojiCustom' }); } RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emoji.name }.${ emoji.extension }`)); EmojiCustom.removeById(emojiID); - Notifications.notifyLogged('deleteEmojiCustom', { emojiData: emoji }); + api.broadcast('emoji.deleteCustom', emoji); return true; }, diff --git a/app/emoji-custom/server/methods/insertOrUpdateEmoji.js b/app/emoji-custom/server/methods/insertOrUpdateEmoji.js index 0e633ecb83c..72a0a6a0694 100644 --- a/app/emoji-custom/server/methods/insertOrUpdateEmoji.js +++ b/app/emoji-custom/server/methods/insertOrUpdateEmoji.js @@ -4,9 +4,9 @@ import s from 'underscore.string'; import limax from 'limax'; import { hasPermission } from '../../../authorization'; -import { Notifications } from '../../../notifications'; import { EmojiCustom } from '../../../models'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; +import { api } from '../../../../server/sdk/api'; Meteor.methods({ insertOrUpdateEmoji(emojiData) { @@ -73,7 +73,7 @@ Meteor.methods({ const _id = EmojiCustom.create(createEmoji); - Notifications.notifyLogged('updateEmojiCustom', { emojiData: createEmoji }); + api.broadcast('emoji.updateCustom', createEmoji); return _id; } @@ -107,7 +107,7 @@ Meteor.methods({ EmojiCustom.setAliases(emojiData._id, []); } - Notifications.notifyLogged('updateEmojiCustom', { emojiData }); + api.broadcast('emoji.updateCustom', emojiData); return true; }, diff --git a/app/emoji-custom/server/methods/uploadEmojiCustom.js b/app/emoji-custom/server/methods/uploadEmojiCustom.js index 3dc49bf3157..1f3d4cf4d1c 100644 --- a/app/emoji-custom/server/methods/uploadEmojiCustom.js +++ b/app/emoji-custom/server/methods/uploadEmojiCustom.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import limax from 'limax'; -import { Notifications } from '../../../notifications'; import { hasPermission } from '../../../authorization'; import { RocketChatFile } from '../../../file'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; +import { api } from '../../../../server/sdk/api'; Meteor.methods({ uploadEmojiCustom(binaryContent, contentType, emojiData) { @@ -21,7 +21,7 @@ Meteor.methods({ RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emojiData.name }.${ emojiData.extension }`)); const ws = RocketChatFileEmojiCustomInstance.createWriteStream(encodeURIComponent(`${ emojiData.name }.${ emojiData.extension }`), contentType); ws.on('end', Meteor.bindEnvironment(() => - Meteor.setTimeout(() => Notifications.notifyLogged('updateEmojiCustom', { emojiData }), 500), + Meteor.setTimeout(() => api.broadcast('emoji.updateCustom', emojiData), 500), )); rs.pipe(ws); diff --git a/app/google-vision/server/googlevision.js b/app/google-vision/server/googlevision.js index 33494b398c1..69729c6c1dd 100644 --- a/app/google-vision/server/googlevision.js +++ b/app/google-vision/server/googlevision.js @@ -1,12 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../settings'; import { callbacks } from '../../callbacks'; -import { Notifications } from '../../notifications'; import { Uploads, Settings, Users, Messages } from '../../models'; import { FileUpload } from '../../file-upload'; +import { api } from '../../../server/sdk/api'; class GoogleVision { constructor() { @@ -67,10 +66,7 @@ class GoogleVision { FileUpload.getStore('Uploads').deleteById(file._id); const user = Users.findOneById(message.u && message.u._id); if (user) { - Notifications.notifyUser(user._id, 'message', { - _id: Random.id(), - rid: message.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', user._id, message.rid, { msg: TAPi18n.__('Adult_images_are_not_allowed', {}, user.language), }); } diff --git a/app/importer/server/classes/ImporterWebsocket.js b/app/importer/server/classes/ImporterWebsocket.js index 07e23cb77e5..ba067fda386 100644 --- a/app/importer/server/classes/ImporterWebsocket.js +++ b/app/importer/server/classes/ImporterWebsocket.js @@ -1,11 +1,8 @@ -import { Meteor } from 'meteor/meteor'; +import notifications from '../../../notifications/server/lib/Notifications'; class ImporterWebsocketDef { constructor() { - this.streamer = new Meteor.Streamer('importers', { retransmit: false }); - this.streamer.allowRead('all'); - this.streamer.allowEmit('all'); - this.streamer.allowWrite('none'); + this.streamer = notifications.streamImporters; } /** diff --git a/app/integrations/server/index.js b/app/integrations/server/index.js index b022e3034fe..4e826bf4fbd 100644 --- a/app/integrations/server/index.js +++ b/app/integrations/server/index.js @@ -11,7 +11,6 @@ import './methods/outgoing/deleteOutgoingIntegration'; import './methods/clearIntegrationHistory'; import './api/api'; import './lib/triggerHandler'; -import './streamer'; import './triggers'; export { mountIntegrationQueryBasedOnPermissions, mountIntegrationHistoryQueryBasedOnPermissions } from './lib/mountQueriesBasedOnPermission'; diff --git a/app/integrations/server/lib/triggerHandler.js b/app/integrations/server/lib/triggerHandler.js index 0541855cbf6..3763178e129 100644 --- a/app/integrations/server/lib/triggerHandler.js +++ b/app/integrations/server/lib/triggerHandler.js @@ -23,26 +23,6 @@ integrations.triggerHandler = new class RocketChatIntegrationHandler { this.triggers = {}; Models.Integrations.find({ type: 'webhook-outgoing' }).fetch().forEach((data) => this.addIntegration(data)); - - Models.Integrations.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'inserted': - if (data.type === 'webhook-outgoing') { - this.addIntegration(data); - } - break; - case 'updated': - data = data ?? Models.Integrations.findOneById(id); - if (data.type === 'webhook-outgoing') { - this.removeIntegration(data); - this.addIntegration(data); - } - break; - case 'removed': - this.removeIntegration({ _id: id }); - break; - } - }); } addIntegration(record) { @@ -826,3 +806,5 @@ integrations.triggerHandler = new class RocketChatIntegrationHandler { this.executeTriggerUrl(history.url, integration, { event, message, room, owner, user }); } }(); + +export { integrations }; diff --git a/app/integrations/server/methods/clearIntegrationHistory.js b/app/integrations/server/methods/clearIntegrationHistory.js index 4bd3f2a5cdc..87eec581e37 100644 --- a/app/integrations/server/methods/clearIntegrationHistory.js +++ b/app/integrations/server/methods/clearIntegrationHistory.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; import { IntegrationHistory, Integrations } from '../../../models'; -import { integrationHistoryStreamer } from '../streamer'; +import notifications from '../../../notifications/server/lib/Notifications'; Meteor.methods({ clearIntegrationHistory(integrationId) { @@ -22,7 +22,7 @@ Meteor.methods({ IntegrationHistory.removeByIntegrationId(integrationId); - integrationHistoryStreamer.emit(integrationId, { type: 'removed' }); + notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed' }); return true; }, diff --git a/app/integrations/server/streamer.js b/app/integrations/server/streamer.js deleted file mode 100644 index 557c4f871ff..00000000000 --- a/app/integrations/server/streamer.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasAtLeastOnePermission } from '../../authorization/server'; -import { IntegrationHistory } from '../../models/server'; - -export const integrationHistoryStreamer = new Meteor.Streamer('integrationHistory'); -integrationHistoryStreamer.allowWrite('none'); -integrationHistoryStreamer.allowRead(function() { - return this.userId && hasAtLeastOnePermission(this.userId, [ - 'manage-outgoing-integrations', - 'manage-own-outgoing-integrations', - ]); -}); - -IntegrationHistory.on('change', ({ clientAction, id, data, diff }) => { - switch (clientAction) { - case 'updated': { - const history = IntegrationHistory.findOneById(id, { fields: { 'integration._id': 1 } }); - if (!history && !history.integration) { - return; - } - integrationHistoryStreamer.emit(history.integration._id, { id, diff, type: clientAction }); - break; - } - case 'inserted': { - integrationHistoryStreamer.emit(data.integration._id, { data, type: clientAction }); - break; - } - } -}); diff --git a/app/ldap/server/sync.js b/app/ldap/server/sync.js index 0c3db0eca37..9993fe54549 100644 --- a/app/ldap/server/sync.js +++ b/app/ldap/server/sync.js @@ -8,13 +8,13 @@ import LDAP from './ldap'; import { callbacks } from '../../callbacks/server'; import { RocketChatFile } from '../../file'; import { settings } from '../../settings'; -import { Notifications } from '../../notifications'; import { Users, Roles, Rooms, Subscriptions } from '../../models'; import { Logger } from '../../logger'; import { _setRealName, _setUsername } from '../../lib'; import { templateVarHandler } from '../../utils'; import { FileUpload } from '../../file-upload'; import { addUserToRoom, removeUserFromRoom, createRoom } from '../../lib/server/functions'; +import { api } from '../../../server/sdk/api'; export const logger = new Logger('LDAPSync', {}); @@ -264,7 +264,7 @@ export function mapLdapGroupsToUserRoles(ldap, ldapUser, user) { const del = Roles.removeUserRoles(user._id, roleName); if (settings.get('UI_DisplayRoles') && del) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'removed', _id: roleName, u: { @@ -365,7 +365,7 @@ function syncUserAvatar(user, ldapUser) { fileStore.insert(file, rs, (err, result) => { Meteor.setTimeout(function() { Users.setAvatarData(user._id, 'ldap', result.etag); - Notifications.notifyLogged('updateAvatar', { username: user.username, etag: result.etag }); + api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: result.etag }); }, 500); }); }); @@ -412,7 +412,7 @@ export function syncUserData(user, ldapUser, ldap) { for (const roleName of userRoles) { const add = Roles.addUserRoles(user._id, roleName); if (settings.get('UI_DisplayRoles') && add) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'added', _id: roleName, u: { diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js index 6f4f4b16f5e..32146767639 100644 --- a/app/lib/server/functions/deleteUser.js +++ b/app/lib/server/functions/deleteUser.js @@ -4,11 +4,11 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FileUpload } from '../../../file-upload/server'; import { Users, Subscriptions, Messages, Rooms, Integrations, FederationServers } from '../../../models/server'; import { settings } from '../../../settings/server'; -import { Notifications } from '../../../notifications/server'; import { updateGroupDMsName } from './updateGroupDMsName'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; +import { api } from '../../../../server/sdk/api'; export const deleteUser = function(userId, confirmRelinquish = false) { const user = Users.findOneById(userId, { @@ -65,7 +65,7 @@ export const deleteUser = function(userId, confirmRelinquish = false) { } Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. - Notifications.notifyLogged('Users:Deleted', { userId }); + api.broadcast('user.deleted', user); } // Remove user from users database diff --git a/app/lib/server/functions/setRealName.js b/app/lib/server/functions/setRealName.js index 190fc1b739b..3a7051d68ad 100644 --- a/app/lib/server/functions/setRealName.js +++ b/app/lib/server/functions/setRealName.js @@ -3,9 +3,9 @@ import s from 'underscore.string'; import { Users } from '../../../models/server'; import { settings } from '../../../settings'; -import { Notifications } from '../../../notifications'; import { hasPermission } from '../../../authorization'; import { RateLimiter } from '../lib'; +import { api } from '../../../../server/sdk/api'; export const _setRealName = function(userId, name, fullUser) { name = s.trim(name); @@ -30,7 +30,7 @@ export const _setRealName = function(userId, name, fullUser) { user.name = name; if (settings.get('UI_Use_Real_Name') === true) { - Notifications.notifyLogged('Users:NameChanged', { + api.broadcast('user.nameChanged', { _id: user._id, name: user.name, username: user.username, diff --git a/app/lib/server/functions/setRoomAvatar.js b/app/lib/server/functions/setRoomAvatar.js index a63a583d505..dd81ef8e19a 100644 --- a/app/lib/server/functions/setRoomAvatar.js +++ b/app/lib/server/functions/setRoomAvatar.js @@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { RocketChatFile } from '../../../file'; import { FileUpload } from '../../../file-upload'; -import { Notifications } from '../../../notifications'; import { Rooms, Avatars, Messages } from '../../../models/server'; +import { api } from '../../../../server/sdk/api'; export const setRoomAvatar = function(rid, dataURI, user) { const fileStore = FileUpload.getStore('Avatars'); @@ -13,7 +13,7 @@ export const setRoomAvatar = function(rid, dataURI, user) { if (!dataURI) { fileStore.deleteByRoomId(rid); Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_avatar', rid, '', user); - Notifications.notifyLogged('updateAvatar', { rid }); + api.broadcast('room.avatarUpdate', { rid }); return Rooms.unsetAvatarData(rid); } @@ -40,7 +40,7 @@ export const setRoomAvatar = function(rid, dataURI, user) { } Rooms.setAvatarData(rid, 'upload', result.etag); Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_avatar', rid, '', user); - Notifications.notifyLogged('updateAvatar', { rid, etag: result.etag }); + api.broadcast('room.avatarUpdate', { rid, avatarETag: result.etag }); }, 500); }); }; diff --git a/app/lib/server/functions/setStatusText.js b/app/lib/server/functions/setStatusText.js index 1db1020bc18..d6705468ab3 100644 --- a/app/lib/server/functions/setStatusText.js +++ b/app/lib/server/functions/setStatusText.js @@ -1,19 +1,11 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; import { Users as UsersRaw } from '../../../models/server/raw'; -import { Notifications } from '../../../notifications'; -import { hasPermission } from '../../../authorization'; +import { hasPermission } from '../../../authorization/server'; import { RateLimiter } from '../lib'; - -// mirror of object in /imports/startup/client/listenActiveUsers.js - keep updated -const STATUS_MAP = { - offline: 0, - online: 1, - away: 2, - busy: 3, -}; +import { api } from '../../../../server/sdk/api'; export const _setStatusTextPromise = async function(userId, statusText) { if (!userId) { return false; } @@ -28,12 +20,8 @@ export const _setStatusTextPromise = async function(userId, statusText) { await UsersRaw.updateStatusText(user._id, statusText); - Notifications.notifyLogged('user-status', [ - user._id, - user.username, - STATUS_MAP[user.status], - statusText, - ]); + const { _id, username, status } = user; + api.broadcast('userpresence', { user: { _id, username, status, statusText } }); return true; }; @@ -59,12 +47,8 @@ export const _setStatusText = function(userId, statusText) { Users.updateStatusText(user._id, statusText); user.statusText = statusText; - Notifications.notifyLogged('user-status', [ - user._id, - user.username, - STATUS_MAP[user.status], - statusText, - ]); + const { _id, username, status } = user; + api.broadcast('userpresence', { user: { _id, username, status, statusText } }); return true; }; diff --git a/app/lib/server/functions/setUserAvatar.js b/app/lib/server/functions/setUserAvatar.js index ae6c54fe1ef..7229d5a294e 100644 --- a/app/lib/server/functions/setUserAvatar.js +++ b/app/lib/server/functions/setUserAvatar.js @@ -4,7 +4,7 @@ import { HTTP } from 'meteor/http'; import { RocketChatFile } from '../../../file'; import { FileUpload } from '../../../file-upload'; import { Users } from '../../../models'; -import { Notifications } from '../../../notifications'; +import { api } from '../../../../server/sdk/api'; export const setUserAvatar = function(user, dataURI, contentType, service) { let encoding; @@ -64,7 +64,7 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { fileStore.insert(file, buffer, (err, result) => { Meteor.setTimeout(function() { Users.setAvatarData(user._id, service, result.etag); - Notifications.notifyLogged('updateAvatar', { username: user.username, etag: result.etag }); + api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: result.etag }); }, 500); }); }; diff --git a/app/lib/server/functions/setUsername.js b/app/lib/server/functions/setUsername.js index 6e5f08cf399..35b46122cbc 100644 --- a/app/lib/server/functions/setUsername.js +++ b/app/lib/server/functions/setUsername.js @@ -6,8 +6,8 @@ import { settings } from '../../../settings'; import { Users, Invites } from '../../../models/server'; import { hasPermission } from '../../../authorization'; import { RateLimiter } from '../lib'; -import { Notifications } from '../../../notifications/server'; import { addUserToRoom } from './addUserToRoom'; +import { api } from '../../../../server/sdk/api'; import { checkUsernameAvailability, setUserAvatar, getAvatarSuggestionForUser } from '.'; @@ -76,7 +76,7 @@ export const _setUsername = function(userId, u, fullUser) { } } - Notifications.notifyLogged('Users:NameChanged', { + api.broadcast('user.nameChanged', { _id: user._id, name: user.name, username: user.username, diff --git a/app/lib/server/index.js b/app/lib/server/index.js index 56ffb23f199..aa0f7c468bf 100644 --- a/app/lib/server/index.js +++ b/app/lib/server/index.js @@ -6,7 +6,6 @@ import './startup/settings'; import './startup/settingsOnLoadCdnPrefix'; import './startup/settingsOnLoadDirectReply'; import './startup/settingsOnLoadSMTP'; -import './startup/userDataStream'; import '../lib/MessageTypes'; import '../startup'; import '../startup/defaultRoomTypes'; diff --git a/app/lib/server/lib/msgStream.js b/app/lib/server/lib/msgStream.js index 94f657a2691..208c3ab14f5 100644 --- a/app/lib/server/lib/msgStream.js +++ b/app/lib/server/lib/msgStream.js @@ -1,57 +1,3 @@ -import { Meteor } from 'meteor/meteor'; -import { DDPCommon } from 'meteor/ddp-common'; +import notifications from '../../../notifications/server/lib/Notifications'; -const changedPayload = function(collection, id, fields) { - return DDPCommon.stringifyDDP({ - msg: 'changed', - collection, - id, - fields, - }); -}; - -const send = function(self, msg) { - if (!self.socket) { - return; - } - self.socket.send(msg); -}; - -class MessageStream extends Meteor.Streamer { - getSubscriptionByUserIdAndRoomId(userId, rid) { - return this.subscriptions.find((sub) => sub.eventName === rid && sub.subscription.userId === userId); - } - - _publish(publication, eventName, options) { - super._publish(publication, eventName, options); - const uid = Meteor.userId(); - - const userEvent = (clientAction, { rid }) => { - switch (clientAction) { - case 'removed': - this.removeListener(uid, userEvent); - this.removeSubscription(this.getSubscriptionByUserIdAndRoomId(uid, rid), eventName); - break; - } - }; - this.on(uid, userEvent); - } - - mymessage = (eventName, args) => { - const subscriptions = this.subscriptionsByEventName[eventName]; - if (!Array.isArray(subscriptions)) { - return; - } - subscriptions.forEach(({ subscription }) => { - const options = this.isEmitAllowed(subscription, eventName, args); - if (options) { - send(subscription._session, changedPayload(this.subscriptionName, 'id', { - eventName, - args: [args, options], - })); - } - }); - } -} - -export const msgStream = new MessageStream('room-messages'); +export const msgStream = notifications.streamRoomMessage; diff --git a/app/lib/server/methods/addUsersToRoom.js b/app/lib/server/methods/addUsersToRoom.js index 63eaa7594a8..46556b76371 100644 --- a/app/lib/server/methods/addUsersToRoom.js +++ b/app/lib/server/methods/addUsersToRoom.js @@ -1,12 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Rooms, Subscriptions, Users } from '../../../models'; import { hasPermission } from '../../../authorization'; import { addUserToRoom } from '../functions'; -import { Notifications } from '../../../notifications'; +import { api } from '../../../../server/sdk/api'; Meteor.methods({ addUsersToRoom(data = {}) { @@ -73,10 +72,7 @@ Meteor.methods({ if (!subscription) { addUserToRoom(data.rid, newUser, user); } else { - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: data.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, data.rid, { msg: TAPi18n.__('Username_is_already_in_here', { postProcess: 'sprintf', sprintf: [newUser.username], diff --git a/app/lib/server/methods/filterATAllTag.js b/app/lib/server/methods/filterATAllTag.js index 9ada82426f3..183a5075f48 100644 --- a/app/lib/server/methods/filterATAllTag.js +++ b/app/lib/server/methods/filterATAllTag.js @@ -1,13 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; import moment from 'moment'; import { hasPermission } from '../../../authorization'; import { callbacks } from '../../../callbacks'; -import { Notifications } from '../../../notifications'; import { Users } from '../../../models'; +import { api } from '../../../../server/sdk/api'; callbacks.add('beforeSaveMessage', function(message) { // If the message was edited, or is older than 60 seconds (imported) @@ -27,10 +26,7 @@ callbacks.add('beforeSaveMessage', function(message) { // Add a notification to the chat, informing the user that this // action is not allowed. - Notifications.notifyUser(message.u._id, 'message', { - _id: Random.id(), - rid: message.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', message.u._id, message.rid, { msg: TAPi18n.__('error-action-not-allowed', { action }, language), }); diff --git a/app/lib/server/methods/filterATHereTag.js b/app/lib/server/methods/filterATHereTag.js index 79198e1aaf5..e67a0f76c6c 100644 --- a/app/lib/server/methods/filterATHereTag.js +++ b/app/lib/server/methods/filterATHereTag.js @@ -1,13 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; import moment from 'moment'; import { hasPermission } from '../../../authorization'; import { callbacks } from '../../../callbacks'; -import { Notifications } from '../../../notifications'; import { Users } from '../../../models'; +import { api } from '../../../../server/sdk/api'; callbacks.add('beforeSaveMessage', function(message) { // If the message was edited, or is older than 60 seconds (imported) @@ -26,10 +25,7 @@ callbacks.add('beforeSaveMessage', function(message) { // Add a notification to the chat, informing the user that this // action is not allowed. - Notifications.notifyUser(message.u._id, 'message', { - _id: Random.id(), - rid: message.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', message.u._id, message.rid, { msg: TAPi18n.__('error-action-not-allowed', { action }, language), }); diff --git a/app/lib/server/methods/sendMessage.js b/app/lib/server/methods/sendMessage.js index 22d30108fcb..ae996bc6cfe 100644 --- a/app/lib/server/methods/sendMessage.js +++ b/app/lib/server/methods/sendMessage.js @@ -1,19 +1,18 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import moment from 'moment'; import { hasPermission } from '../../../authorization'; import { metrics } from '../../../metrics'; import { settings } from '../../../settings'; -import { Notifications } from '../../../notifications'; import { messageProperties } from '../../../ui-utils'; import { Users, Messages } from '../../../models'; import { sendMessage } from '../functions'; import { RateLimiter } from '../lib'; import { canSendMessage } from '../../../authorization/server'; import { SystemLogger } from '../../../logger/server'; +import { api } from '../../../../server/sdk/api'; export function executeSendMessage(uid, message) { if (message.tshow && !message.tmid) { @@ -81,10 +80,7 @@ export function executeSendMessage(uid, message) { SystemLogger.error('Error sending message:', error); const errorMessage = typeof error === 'string' ? error : error.error || error.message; - Notifications.notifyUser(uid, 'message', { - _id: Random.id(), - rid: message.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', uid, message.rid, { msg: TAPi18n.__(errorMessage, {}, user.language), }); diff --git a/app/lib/server/startup/userDataStream.js b/app/lib/server/startup/userDataStream.js deleted file mode 100644 index 9d0de862d01..00000000000 --- a/app/lib/server/startup/userDataStream.js +++ /dev/null @@ -1,104 +0,0 @@ -import { MongoInternals } from 'meteor/mongo'; - -import { Users } from '../../../models/server'; -import { Notifications } from '../../../notifications/server'; -import loginServiceConfiguration from '../../../models/server/models/LoginServiceConfiguration'; - -let processOnChange; -// eslint-disable-next-line no-undef -const disableOplog = Package['disable-oplog']; - -if (disableOplog) { - // Stores the callbacks for the disconnection reactivity bellow - const userCallbacks = new Map(); - const serviceConfigCallbacks = new Set(); - - // Overrides the native observe changes to prevent database polling and stores the callbacks - // for the users' tokens to re-implement the reactivity based on our database listeners - const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); - MongoInternals.Connection.prototype._observeChanges = function({ collectionName, selector, options = {} }, _ordered, callbacks) { - // console.error('Connection.Collection.prototype._observeChanges', collectionName, selector, options); - let cbs; - if (callbacks?.added) { - const records = Promise.await(mongo.rawCollection(collectionName).find(selector, { projection: options.fields }).toArray()); - for (const { _id, ...fields } of records) { - callbacks.added(_id, fields); - } - - if (collectionName === 'users' && selector['services.resume.loginTokens.hashedToken']) { - cbs = userCallbacks.get(selector._id) || new Set(); - cbs.add({ - hashedToken: selector['services.resume.loginTokens.hashedToken'], - callbacks, - }); - userCallbacks.set(selector._id, cbs); - } - } - - if (collectionName === 'meteor_accounts_loginServiceConfiguration') { - serviceConfigCallbacks.add(callbacks); - } - - return { - stop() { - if (cbs) { - cbs.delete(callbacks); - } - serviceConfigCallbacks.delete(callbacks); - }, - }; - }; - - // Re-implement meteor's reactivity that uses observe to disconnect sessions when the token - // associated was removed - processOnChange = (diff, id) => { - const loginTokens = diff['services.resume.loginTokens']; - if (loginTokens) { - const tokens = loginTokens.map(({ hashedToken }) => hashedToken); - - const cbs = userCallbacks.get(id); - if (cbs) { - [...cbs].filter(({ hashedToken }) => !tokens.includes(hashedToken)).forEach((item) => { - item.callbacks.removed(id); - cbs.delete(item); - }); - } - } - }; - - loginServiceConfiguration.on('change', ({ clientAction, id, data, diff }) => { - switch (clientAction) { - case 'inserted': - case 'updated': - const record = { ...data || diff }; - delete record.secret; - serviceConfigCallbacks.forEach((callbacks) => { - callbacks[clientAction === 'inserted' ? 'added' : 'changed']?.(id, record); - }); - break; - case 'removed': - serviceConfigCallbacks.forEach((callbacks) => { - callbacks.removed?.(id); - }); - } - }); -} - -Users.on('change', ({ clientAction, id, data, diff, unset }) => { - switch (clientAction) { - case 'updated': - Notifications.notifyUserInThisInstance(id, 'userData', { diff, unset, type: clientAction }); - - if (disableOplog) { - processOnChange(diff, id); - } - - break; - case 'inserted': - Notifications.notifyUserInThisInstance(id, 'userData', { data, type: clientAction }); - break; - case 'removed': - Notifications.notifyUserInThisInstance(id, 'userData', { id, type: clientAction }); - break; - } -}); diff --git a/app/livechat/server/index.js b/app/livechat/server/index.js index dea202128cf..ad8f2ca1c2d 100644 --- a/app/livechat/server/index.js +++ b/app/livechat/server/index.js @@ -80,8 +80,6 @@ import './lib/routing/External'; import './lib/routing/ManualSelection'; import './lib/routing/AutoSelection'; import './lib/stream/agentStatus'; -import './lib/stream/departmentAgents'; -import './lib/stream/queueManager'; import './sendMessageBySMS'; import './api'; import './api/rest'; diff --git a/app/livechat/server/lib/Helper.js b/app/livechat/server/lib/Helper.js index d124c3b04d7..d1e04d9ff27 100644 --- a/app/livechat/server/lib/Helper.js +++ b/app/livechat/server/lib/Helper.js @@ -8,6 +8,7 @@ import { RoutingManager } from './RoutingManager'; import { callbacks } from '../../../callbacks/server'; import { settings } from '../../../settings'; import { Apps, AppEvents } from '../../../apps/server'; +import notifications from '../../../notifications/server/lib/Notifications'; export const createLivechatRoom = (rid, name, guest, roomInfo = {}, extraData = {}) => { check(rid, String); @@ -192,7 +193,7 @@ export const normalizeAgent = (agentId) => { export const dispatchAgentDelegated = (rid, agentId) => { const agent = normalizeAgent(agentId); - Livechat.stream.emit(rid, { + notifications.streamLivechatRoom.emit(rid, { type: 'agentData', data: agent, }); diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index bdcb69c15cb..d5fd07399eb 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -38,6 +38,7 @@ import { FileUpload } from '../../../file-upload/server'; import { normalizeTransferredByData, parseAgentCustomFields, updateDepartmentAgents } from './Helper'; import { Apps, AppEvents } from '../../../apps/server'; import { businessHourManager } from '../business-hour'; +import notifications from '../../../notifications/server/lib/Notifications'; export const Livechat = { Analytics, @@ -1107,7 +1108,7 @@ export const Livechat = { } LivechatRooms.findOpenByAgent(userId).forEach((room) => { - Livechat.stream.emit(room._id, { + notifications.streamLivechatRoom.emit(room._id, { type: 'agentStatus', status, }); @@ -1123,7 +1124,7 @@ export const Livechat = { }, notifyRoomVisitorChange(roomId, visitor) { - Livechat.stream.emit(roomId, { + notifications.streamLivechatRoom.emit(roomId, { type: 'visitorData', visitor, }); @@ -1156,22 +1157,6 @@ export const Livechat = { }, }; -Livechat.stream = new Meteor.Streamer('livechat-room'); - -Livechat.stream.allowRead((roomId, extraData) => { - const room = LivechatRooms.findOneById(roomId); - - if (!room) { - console.warn(`Invalid eventName: "${ roomId }"`); - return false; - } - - if (room.t === 'l' && extraData && extraData.visitorToken && room.v.token === extraData.visitorToken) { - return true; - } - return false; -}); - settings.get('Livechat_history_monitor_type', (key, value) => { Livechat.historyMonitorType = value; }); diff --git a/app/livechat/server/lib/stream/agentStatus.ts b/app/livechat/server/lib/stream/agentStatus.ts index 4d875e95ebc..c04cd48d5e8 100644 --- a/app/livechat/server/lib/stream/agentStatus.ts +++ b/app/livechat/server/lib/stream/agentStatus.ts @@ -2,9 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Livechat } from '../Livechat'; import { settings } from '../../../../settings/server'; -import { Users } from '../../../../models/server'; -let monitorAgents = false; +export let monitorAgents = false; let actionTimeout = 60000; let action = 'none'; let comment = ''; @@ -28,7 +27,7 @@ settings.get('Livechat_agent_leave_comment', (_key, value) => { comment = value; }); -const onlineAgents = { +export const onlineAgents = { users: new Set(), queue: new Map(), @@ -74,36 +73,3 @@ const onlineAgents = { } }), }; - -Users.on('change', ({ clientAction, id, diff }) => { - if (!monitorAgents) { - return; - } - - if (clientAction !== 'removed' && diff && !diff.status && !diff.statusLivechat) { - return; - } - - switch (clientAction) { - case 'updated': - case 'inserted': - const agent = Users.findOneAgentById(id, { - fields: { - status: 1, - statusLivechat: 1, - }, - }); - const serviceOnline = agent && agent.status !== 'offline' && agent.statusLivechat === 'available'; - - if (serviceOnline) { - return onlineAgents.add(id); - } - - onlineAgents.remove(id); - - break; - case 'removed': - onlineAgents.remove(id); - break; - } -}); diff --git a/app/livechat/server/lib/stream/departmentAgents.js b/app/livechat/server/lib/stream/departmentAgents.js deleted file mode 100644 index a6c6fab7512..00000000000 --- a/app/livechat/server/lib/stream/departmentAgents.js +++ /dev/null @@ -1,29 +0,0 @@ -import { LivechatDepartmentAgents } from '../../../../models/server'; -import { Notifications } from '../../../../notifications'; - -const fields = { agentId: 1, departmentId: 1 }; - -const emitNotification = (action, payload = {}) => { - const { agentId = null } = payload; - if (!agentId) { - return; - } - - Notifications.notifyUserInThisInstance(agentId, 'departmentAgentData', { - action, - ...payload, - }); -}; - -LivechatDepartmentAgents.on('change', ({ clientAction, id }) => { - switch (clientAction) { - case 'inserted': - case 'updated': - emitNotification(clientAction, LivechatDepartmentAgents.findOneById(id, { fields })); - break; - - case 'removed': - emitNotification(clientAction, LivechatDepartmentAgents.trashFindOneById(id, { fields })); - break; - } -}); diff --git a/app/livechat/server/lib/stream/queueManager.js b/app/livechat/server/lib/stream/queueManager.js deleted file mode 100644 index bf30b8397ff..00000000000 --- a/app/livechat/server/lib/stream/queueManager.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../../authorization/server'; -import { LivechatInquiry } from '../../../../models/server'; -import { LIVECHAT_INQUIRY_QUEUE_STREAM_OBSERVER } from '../../../lib/stream/constants'; -import { RoutingManager } from '../RoutingManager'; - -const queueDataStreamer = new Meteor.Streamer(LIVECHAT_INQUIRY_QUEUE_STREAM_OBSERVER); -queueDataStreamer.allowWrite('none'); -queueDataStreamer.allowRead(function() { - return this.userId ? hasPermission(this.userId, 'view-l-room') : false; -}); - -const emitQueueDataEvent = (event, data) => queueDataStreamer.emitWithoutBroadcast(event, data); -const mountDataToEmit = (type, data) => ({ type, ...data }); - -LivechatInquiry.on('change', ({ clientAction, id: _id, data: record }) => { - if (RoutingManager.getConfig().autoAssignAgent) { - return; - } - - switch (clientAction) { - case 'inserted': - emitQueueDataEvent(_id, { ...record, clientAction }); - if (record && record.department) { - return emitQueueDataEvent(`department/${ record.department }`, mountDataToEmit('added', record)); - } - emitQueueDataEvent('public', mountDataToEmit('added', record)); - break; - case 'updated': - const isUpdatingDepartment = record && record.department; - const updatedRecord = LivechatInquiry.findOneById(_id); - emitQueueDataEvent(_id, { ...updatedRecord, clientAction }); - if (updatedRecord && !updatedRecord.department) { - return emitQueueDataEvent('public', mountDataToEmit('changed', updatedRecord)); - } - if (isUpdatingDepartment) { - emitQueueDataEvent('public', mountDataToEmit('changed', updatedRecord)); - } - emitQueueDataEvent(`department/${ updatedRecord.department }`, mountDataToEmit('changed', updatedRecord)); - break; - - case 'removed': - const removedRecord = LivechatInquiry.trashFindOneById(_id); - emitQueueDataEvent(_id, { _id, clientAction }); - if (removedRecord && removedRecord.department) { - return emitQueueDataEvent(`department/${ removedRecord.department }`, mountDataToEmit('removed', { _id })); - } - emitQueueDataEvent('public', mountDataToEmit('removed', { _id })); - break; - } -}); diff --git a/app/livechat/server/roomAccessValidator.compatibility.js b/app/livechat/server/roomAccessValidator.compatibility.js new file mode 100644 index 00000000000..a2092302d01 --- /dev/null +++ b/app/livechat/server/roomAccessValidator.compatibility.js @@ -0,0 +1,51 @@ +import { LivechatRooms } from '../../models'; +import { hasPermission, hasRole } from '../../authorization'; +import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry } from '../../models/server'; +import { RoutingManager } from './lib/RoutingManager'; + +export const validators = [ + function(room, user) { + return hasPermission(user._id, 'view-livechat-rooms'); + }, + function(room, user) { + const { _id: userId } = user; + const { servedBy: { _id: agentId } = {} } = room; + return userId === agentId || (!room.open && hasPermission(user._id, 'view-livechat-room-closed-by-another-agent')); + }, + function(room, user, extraData) { + if (extraData && extraData.rid) { + room = LivechatRooms.findOneById(extraData.rid); + } + return extraData && extraData.visitorToken && room.v && room.v.token === extraData.visitorToken; + }, + function(room, user) { + const { previewRoom } = RoutingManager.getConfig(); + if (!previewRoom) { + return; + } + + let departmentIds; + if (!hasRole(user._id, 'livechat-manager')) { + const departmentAgents = LivechatDepartmentAgents.findByAgentId(user._id).fetch().map((d) => d.departmentId); + departmentIds = LivechatDepartment.find({ _id: { $in: departmentAgents }, enabled: true }).fetch().map((d) => d._id); + } + + const filter = { + rid: room._id, + ...departmentIds && departmentIds.length > 0 && { department: { $in: departmentIds } }, + }; + + const inquiry = LivechatInquiry.findOne(filter, { fields: { status: 1 } }); + return inquiry && inquiry.status === 'queued'; + }, + function(room, user) { + if (!room.departmentId || room.open) { + return; + } + const agentOfDepartment = LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(user._id, room.departmentId); + if (!agentOfDepartment) { + return; + } + return hasPermission(user._id, 'view-livechat-room-closed-same-department'); + }, +]; diff --git a/app/livechat/server/roomAccessValidator.internalService.ts b/app/livechat/server/roomAccessValidator.internalService.ts new file mode 100644 index 00000000000..9a7c8c79396 --- /dev/null +++ b/app/livechat/server/roomAccessValidator.internalService.ts @@ -0,0 +1,22 @@ +import { ServiceClass } from '../../../server/sdk/types/ServiceClass'; +import { IAuthorizationLivechat } from '../../../server/sdk/types/IAuthorizationLivechat'; +import { validators } from './roomAccessValidator.compatibility'; +import { api } from '../../../server/sdk/api'; +import { IRoom } from '../../../definition/IRoom'; +import { IUser } from '../../../definition/IUser'; + +class AuthorizationLivechat extends ServiceClass implements IAuthorizationLivechat { + protected name = 'authorization-livechat'; + + async canAccessRoom(room: Partial, user: Pick, extraData?: object): Promise { + for (const validator of validators) { + if (validator(room, user, extraData)) { + return true; + } + } + + return false; + } +} + +api.registerService(new AuthorizationLivechat()); diff --git a/app/livechat/server/roomType.js b/app/livechat/server/roomType.js index 9d8cb17af7b..35f34b77408 100644 --- a/app/livechat/server/roomType.js +++ b/app/livechat/server/roomType.js @@ -31,10 +31,6 @@ class LivechatRoomTypeServer extends LivechatRoomType { const { token } = message; return { token }; } - - isEmitAllowed() { - return true; - } } roomTypes.add(new LivechatRoomTypeServer()); diff --git a/app/livechat/server/startup.js b/app/livechat/server/startup.js index 589f3b90a8b..719a545de2b 100644 --- a/app/livechat/server/startup.js +++ b/app/livechat/server/startup.js @@ -3,76 +3,18 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { roomTypes } from '../../utils'; import { LivechatRooms } from '../../models'; -import { hasPermission, hasRole, addRoomAccessValidator } from '../../authorization'; import { callbacks } from '../../callbacks'; import { settings } from '../../settings'; -import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry } from '../../models/server'; -import { RoutingManager } from './lib/RoutingManager'; import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; +import { hasPermission } from '../../authorization/server'; -function allowAccessClosedRoomOfSameDepartment(room, user) { - if (!room || !user || room.t !== 'l' || !room.departmentId || room.open) { - return; - } - const agentOfDepartment = LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(user._id, room.departmentId); - if (!agentOfDepartment) { - return; - } - return hasPermission(user._id, 'view-livechat-room-closed-same-department'); -} +import './roomAccessValidator.internalService'; Meteor.startup(async () => { roomTypes.setRoomFind('l', (_id) => LivechatRooms.findOneById(_id)); - addRoomAccessValidator(function(room, user) { - return room && room.t === 'l' && user && hasPermission(user._id, 'view-livechat-rooms'); - }); - - addRoomAccessValidator(function(room, user) { - if (!room || !user || room.t !== 'l') { - return; - } - const { _id: userId } = user; - const { servedBy: { _id: agentId } = {} } = room; - return userId === agentId || (!room.open && hasPermission(user._id, 'view-livechat-room-closed-by-another-agent')); - }); - - addRoomAccessValidator(function(room, user, extraData) { - if (!room && extraData && extraData.rid) { - room = LivechatRooms.findOneById(extraData.rid); - } - return room && room.t === 'l' && extraData && extraData.visitorToken && room.v && room.v.token === extraData.visitorToken; - }); - - addRoomAccessValidator(function(room, user) { - const { previewRoom } = RoutingManager.getConfig(); - if (!previewRoom) { - return; - } - - if (!user || !room || room.t !== 'l') { - return; - } - - let departmentIds; - if (!hasRole(user._id, 'livechat-manager')) { - const departmentAgents = LivechatDepartmentAgents.findByAgentId(user._id).fetch().map((d) => d.departmentId); - departmentIds = LivechatDepartment.find({ _id: { $in: departmentAgents }, enabled: true }).fetch().map((d) => d._id); - } - - const filter = { - rid: room._id, - ...departmentIds && departmentIds.length > 0 && { department: { $in: departmentIds } }, - }; - - const inquiry = LivechatInquiry.findOne(filter, { fields: { status: 1 } }); - return inquiry && inquiry.status === 'queued'; - }); - - addRoomAccessValidator(allowAccessClosedRoomOfSameDepartment); - callbacks.add('beforeLeaveRoom', function(user, room) { if (room.t !== 'l') { return user; diff --git a/app/logger/server/streamer.js b/app/logger/server/streamer.js index 475c7a183ef..3bff9032aaf 100644 --- a/app/logger/server/streamer.js +++ b/app/logger/server/streamer.js @@ -6,7 +6,7 @@ import { EJSON } from 'meteor/ejson'; import { Log } from 'meteor/logging'; import { settings } from '../../settings'; -import { hasPermission } from '../../authorization/server'; +import notifications from '../../notifications/server/lib/Notifications'; export const processString = function(string, date) { let obj; @@ -52,17 +52,17 @@ export const StdOut = new class extends EventEmitter { } }(); -const stdoutStreamer = new Meteor.Streamer('stdout'); -stdoutStreamer.allowWrite('none'); -stdoutStreamer.allowRead(function() { - return this.userId ? hasPermission(this.userId, 'view-logs') : false; -}); - Meteor.startup(() => { const handler = (string, item) => { - stdoutStreamer.emitWithoutBroadcast('stdout', { + // TODO having this as 'emitWithoutBroadcast' will not sent this data to ddp-streamer, so this data + // won't be available when using micro services. + notifications.streamStdout.emitWithoutBroadcast('stdout', { ...item, }); }; - StdOut.on('write', handler); + + // do not emit to StdOut if moleculer log level set to debug because it creates an infinite loop + if (String(process.env.MOLECULER_LOG_LEVEL).toLowerCase() !== 'debug') { + StdOut.on('write', handler); + } }); diff --git a/app/mentions/server/server.js b/app/mentions/server/server.js index 9c515e4ecde..62c54e25248 100644 --- a/app/mentions/server/server.js +++ b/app/mentions/server/server.js @@ -1,13 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; import MentionsServer from './Mentions'; import { settings } from '../../settings'; import { callbacks } from '../../callbacks'; -import { Notifications } from '../../notifications'; import { Users, Subscriptions, Rooms } from '../../models'; +import { api } from '../../../server/sdk/api'; const mention = new MentionsServer({ pattern: () => settings.get('UTF8_Names_Validation'), @@ -21,12 +20,8 @@ const mention = new MentionsServer({ const { language } = this.getUser(sender._id); const msg = TAPi18n.__('Group_mentions_disabled_x_members', { total: this.messageMaxAll }, language); - Notifications.notifyUser(sender._id, 'message', { - _id: Random.id(), - rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', sender._id, rid, { msg, - groupable: false, }); // Also throw to stop propagation of 'sendMessage'. diff --git a/app/meteor-accounts-saml/server/lib/Utils.ts b/app/meteor-accounts-saml/server/lib/Utils.ts index 3a76a4e6c65..d31421d45c2 100644 --- a/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/app/meteor-accounts-saml/server/lib/Utils.ts @@ -393,7 +393,7 @@ export class SAMLUtils { return mainValue; } - public static convertArrayBufferToString(buffer: ArrayBuffer, encoding = 'utf8'): string { + public static convertArrayBufferToString(buffer: ArrayBuffer, encoding: BufferEncoding = 'utf8'): string { return Buffer.from(buffer).toString(encoding); } diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index 8a114d68c26..dcaeff7d772 100644 --- a/app/models/server/models/LivechatRooms.js +++ b/app/models/server/models/LivechatRooms.js @@ -137,7 +137,7 @@ export class LivechatRooms extends Base { return this.find(query, options); } - findOneById(_id, fields) { + findOneById(_id, fields = {}) { const options = {}; if (fields) { diff --git a/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js index f93cd86afde..70efe48b1e3 100644 --- a/app/models/server/models/Subscriptions.js +++ b/app/models/server/models/Subscriptions.js @@ -453,7 +453,7 @@ export class Subscriptions extends Base { } // FIND ONE - findOneByRoomIdAndUserId(roomId, userId, options) { + findOneByRoomIdAndUserId(roomId, userId, options = {}) { const query = { rid: roomId, 'u._id': userId, @@ -561,7 +561,7 @@ export class Subscriptions extends Base { return this.find(query, options); } - findByRoomIdAndNotUserId(roomId, userId, options) { + findByRoomIdAndNotUserId(roomId, userId, options = {}) { const query = { rid: roomId, 'u._id': { diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index 8feef74f6ed..26cdb48b682 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -608,7 +608,7 @@ export class Users extends Base { return this.findOne(query, options); } - findOneById(userId, options) { + findOneById(userId, options = {}) { const query = { _id: userId }; return this.findOne(query, options); diff --git a/app/models/server/models/_BaseDb.js b/app/models/server/models/_BaseDb.js index e4c37904ecb..516ed06edbb 100644 --- a/app/models/server/models/_BaseDb.js +++ b/app/models/server/models/_BaseDb.js @@ -48,6 +48,14 @@ export class BaseDb extends EventEmitter { this.wrapModel(); + if (!process.env.DISABLE_DB_WATCH) { + this.initDbWatch(); + } + + this.tryEnsureIndex({ _updatedAt: 1 }, options._updatedAtIndexOptions); + } + + initDbWatch() { const _oplogHandle = Promise.await(getOplogHandle()); // When someone start listening for changes we start oplog if available @@ -85,8 +93,6 @@ export class BaseDb extends EventEmitter { if (_oplogHandle) { this.on('newListener', handleListener); } - - this.tryEnsureIndex({ _updatedAt: 1 }, options._updatedAtIndexOptions); } get baseName() { diff --git a/app/models/server/models/_oplogHandle.ts b/app/models/server/models/_oplogHandle.ts index 811a8fb9db3..e5cae5affdf 100644 --- a/app/models/server/models/_oplogHandle.ts +++ b/app/models/server/models/_oplogHandle.ts @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { Promise } from 'meteor/promise'; -import { MongoInternals } from 'meteor/mongo'; +import { MongoInternals, OplogHandle } from 'meteor/mongo'; import semver from 'semver'; import s from 'underscore.string'; import { MongoClient, Cursor, Timestamp, Db } from 'mongodb'; import { urlParser } from './_oplogUrlParser'; -class OplogHandle { +class CustomOplogHandle { dbName: string; client: MongoClient; @@ -43,7 +43,7 @@ class OplogHandle { return true; } - async start(): Promise { + async start(): Promise { this.usingChangeStream = await this.isChangeStreamAvailable(); const oplogUrl = this.usingChangeStream ? process.env.MONGO_URL : process.env.MONGO_OPLOG_URL; @@ -70,7 +70,7 @@ class OplogHandle { this.client = new MongoClient(oplogUrl, { useUnifiedTopology: true, useNewUrlParser: true, - ...!this.usingChangeStream && { poolSize: 1 }, + poolSize: this.usingChangeStream ? 15 : 1, }); await this.client.connect(); @@ -83,6 +83,10 @@ class OplogHandle { return this; } + async stop(): Promise { + return this.client?.close(); + } + async startOplog(): Promise { const isMasterDoc = await this.db.admin().command({ ismaster: 1 }); if (!isMasterDoc || !isMasterDoc.setName) { @@ -152,7 +156,10 @@ class OplogHandle { // o: event.fullDocument, o: { $set: event.updateDescription.updatedFields, - $unset: event.updateDescription.removedFields, + $unset: event.updateDescription.removedFields.reduce((obj, field) => { + obj[field as string] = true; + return obj; + }, {} as Record), }, }, }); @@ -174,25 +181,32 @@ class OplogHandle { } } -let oplogHandle: Promise; +let oplogHandle: Promise; -// @ts-ignore -// eslint-disable-next-line no-undef -if (Package['disable-oplog']) { - try { - oplogHandle = Promise.await(new OplogHandle().start()); - } catch (e) { - console.error(e.message); +if (!process.env.DISABLE_DB_WATCH) { + // @ts-ignore + // eslint-disable-next-line no-undef + if (Package['disable-oplog']) { + try { + oplogHandle = Promise.await(new CustomOplogHandle().start()); + } catch (e) { + console.error(e.message); + } } } -export const getOplogHandle = async (): Promise => { +export const getOplogHandle = async (): Promise => { + if (process.env.DISABLE_DB_WATCH) { + return; + } + if (oplogHandle) { return oplogHandle; } const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); - if (mongo._oplogHandle?.onOplogEntry) { - return mongo._oplogHandle; + if (!mongo._oplogHandle?.onOplogEntry) { + return; } + return mongo._oplogHandle; }; diff --git a/app/models/server/raw/BaseRaw.js b/app/models/server/raw/BaseRaw.js deleted file mode 100644 index 312ff9df244..00000000000 --- a/app/models/server/raw/BaseRaw.js +++ /dev/null @@ -1,52 +0,0 @@ -export class BaseRaw { - constructor(col) { - this.col = col; - } - - _ensureDefaultFields(options) { - if (!this.defaultFields) { - return options; - } - - if (!options) { - return { projection: this.defaultFields }; - } - - // TODO: change all places using "fields" for raw models and remove the additional condition here - if ((options.projection != null && Object.keys(options.projection).length > 0) - || (options.fields != null && Object.keys(options.fields).length > 0)) { - return options; - } - - return { - ...options, - projection: this.defaultFields, - }; - } - - findOneById(_id, options = {}) { - return this.findOne({ _id }, options); - } - - findOne(query = {}, options = {}) { - const optionsDef = this._ensureDefaultFields(options); - return this.col.findOne(query, optionsDef); - } - - findUsersInRoles() { - throw new Error('overwrite-function', 'You must overwrite this function in the extended classes'); - } - - find(query = {}, options = {}) { - const optionsDef = this._ensureDefaultFields(options); - return this.col.find(query, optionsDef); - } - - update(...args) { - return this.col.update(...args); - } - - removeById(_id) { - return this.col.deleteOne({ _id }); - } -} diff --git a/app/models/server/raw/BaseRaw.ts b/app/models/server/raw/BaseRaw.ts new file mode 100644 index 00000000000..f602de361df --- /dev/null +++ b/app/models/server/raw/BaseRaw.ts @@ -0,0 +1,94 @@ +import { Collection, FindOneOptions, Cursor, WriteOpResult, DeleteWriteOpResultObject, FilterQuery, UpdateQuery, UpdateOneOptions } from 'mongodb'; + +interface ITrash { + __collection__: string; +} + +export interface IBaseRaw { + col: Collection; +} + +const baseName = 'rocketchat_'; + +export class BaseRaw implements IBaseRaw { + public defaultFields?: Record; + + protected name: string; + + constructor( + public readonly col: Collection, + public readonly trash?: Collection, + ) { + this.name = this.col.collectionName.replace(baseName, ''); + } + + _ensureDefaultFields(options: FindOneOptions): FindOneOptions { + if (!this.defaultFields) { + return options; + } + + if (!options) { + return { projection: this.defaultFields }; + } + + // TODO: change all places using "fields" for raw models and remove the additional condition here + if ((options.projection != null && Object.keys(options.projection).length > 0) + || (options.fields != null && Object.keys(options.fields).length > 0)) { + return options; + } + + return { + ...options, + projection: this.defaultFields, + }; + } + + async findOneById(_id: string, options: FindOneOptions = {}): Promise { + return this.findOne({ _id }, options); + } + + async findOne(query = {}, options: FindOneOptions = {}): Promise { + const optionsDef = this._ensureDefaultFields(options); + + if (typeof query === 'string') { + return this.findOneById(query, options); + } + + return await this.col.findOne(query, optionsDef) ?? undefined; + } + + findUsersInRoles(): void { + throw new Error('[overwrite-function] You must overwrite this function in the extended classes'); + } + + find(query = {}, options: FindOneOptions = {}): Cursor { + const optionsDef = this._ensureDefaultFields(options); + return this.col.find(query, optionsDef); + } + + update(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateOneOptions & { multi?: boolean }): Promise { + return this.col.update(filter, update, options); + } + + removeById(_id: string): Promise { + const query: object = { _id }; + return this.col.deleteOne(query); + } + + // Trash + trashFind(query: FilterQuery, options: FindOneOptions): Cursor | undefined { + return this.trash?.find({ + __collection__: this.name, + ...query, + }, options); + } + + async trashFindOneById(_id: string, options: FindOneOptions = {}): Promise { + const query: object = { + _id, + __collection__: this.name, + }; + + return await this.trash?.findOne(query, options) ?? undefined; + } +} diff --git a/app/models/server/raw/InstanceStatus.ts b/app/models/server/raw/InstanceStatus.ts new file mode 100644 index 00000000000..775c17becbb --- /dev/null +++ b/app/models/server/raw/InstanceStatus.ts @@ -0,0 +1,4 @@ +import { BaseRaw } from './BaseRaw'; +import { IInstanceStatus } from '../../../../definition/IInstanceStatus'; + +export class InstanceStatusRaw extends BaseRaw {} diff --git a/app/models/server/raw/IntegrationHistory.ts b/app/models/server/raw/IntegrationHistory.ts new file mode 100644 index 00000000000..53f7167db79 --- /dev/null +++ b/app/models/server/raw/IntegrationHistory.ts @@ -0,0 +1,4 @@ +import { BaseRaw } from './BaseRaw'; +import { IIntegrationHistory } from '../../../../definition/IIntegrationHistory'; + +export class IntegrationHistoryRaw extends BaseRaw {} diff --git a/app/models/server/raw/LivechatBusinessHours.ts b/app/models/server/raw/LivechatBusinessHours.ts index 1c16328cf16..09993b1697f 100644 --- a/app/models/server/raw/LivechatBusinessHours.ts +++ b/app/models/server/raw/LivechatBusinessHours.ts @@ -17,10 +17,10 @@ export interface IWorkHoursCronJobsWrapper { finish: IWorkHoursCronJobsItem[]; } -export class LivechatBusinessHoursRaw extends BaseRaw { +export class LivechatBusinessHoursRaw extends BaseRaw { public readonly col!: Collection; - findOneDefaultBusinessHour(options?: any): Promise { + findOneDefaultBusinessHour(options?: any): Promise { return this.findOne({ type: LivechatBusinessHourTypes.DEFAULT }, options); } diff --git a/app/models/server/raw/LoginServiceConfiguration.ts b/app/models/server/raw/LoginServiceConfiguration.ts new file mode 100644 index 00000000000..1653b8c2b5c --- /dev/null +++ b/app/models/server/raw/LoginServiceConfiguration.ts @@ -0,0 +1,4 @@ +import { BaseRaw } from './BaseRaw'; +import { ILoginServiceConfiguration } from '../../../../definition/ILoginServiceConfiguration'; + +export class LoginServiceConfigurationRaw extends BaseRaw {} diff --git a/app/models/server/raw/NotificationQueue.ts b/app/models/server/raw/NotificationQueue.ts index 44a34a21be7..9aedb968090 100644 --- a/app/models/server/raw/NotificationQueue.ts +++ b/app/models/server/raw/NotificationQueue.ts @@ -7,7 +7,7 @@ import { import { BaseRaw } from './BaseRaw'; import { INotification } from '../../../../definition/INotification'; -export class NotificationQueueRaw extends BaseRaw { +export class NotificationQueueRaw extends BaseRaw { public readonly col!: Collection; unsetSendingById(_id: string) { diff --git a/app/models/server/raw/OmnichannelQueue.ts b/app/models/server/raw/OmnichannelQueue.ts index dc75499f38b..74a2e1d9f80 100644 --- a/app/models/server/raw/OmnichannelQueue.ts +++ b/app/models/server/raw/OmnichannelQueue.ts @@ -1,15 +1,9 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { - Collection, -} from 'mongodb'; - import { BaseRaw } from './BaseRaw'; import { IOmnichannelQueueStatus } from '../../../../definition/IOmnichannel'; const UNIQUE_QUEUE_ID = 'queue'; -export class OmnichannelQueueRaw extends BaseRaw { - public readonly col!: Collection; - +export class OmnichannelQueueRaw extends BaseRaw { initQueue() { return this.col.updateOne({ _id: UNIQUE_QUEUE_ID, diff --git a/app/models/server/raw/Permissions.js b/app/models/server/raw/Permissions.js deleted file mode 100644 index 3c5c1aaf6e6..00000000000 --- a/app/models/server/raw/Permissions.js +++ /dev/null @@ -1,4 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class PermissionsRaw extends BaseRaw { -} diff --git a/app/models/server/raw/Permissions.ts b/app/models/server/raw/Permissions.ts new file mode 100644 index 00000000000..d5321c82c80 --- /dev/null +++ b/app/models/server/raw/Permissions.ts @@ -0,0 +1,5 @@ +import { BaseRaw } from './BaseRaw'; +import { IPermission } from '../../../../definition/IPermission'; + +export class PermissionsRaw extends BaseRaw { +} diff --git a/app/models/server/raw/Roles.js b/app/models/server/raw/Roles.js index bda23eea6b5..523aa968057 100644 --- a/app/models/server/raw/Roles.js +++ b/app/models/server/raw/Roles.js @@ -1,8 +1,12 @@ import { BaseRaw } from './BaseRaw'; -import * as Models from './index'; - export class RolesRaw extends BaseRaw { + constructor(col, trash, models) { + super(col, trash); + + this.models = models; + } + async isUserInRoles(userId, roles, scope) { roles = [].concat(roles); @@ -12,7 +16,7 @@ export class RolesRaw extends BaseRaw { // eslint-disable-next-line no-await-in-loop const role = await this.findOne({ _id: roleName }); const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; + const model = this.models[roleScope]; // eslint-disable-next-line no-await-in-loop const permitted = await (model && model.isUserInRole && model.isUserInRole(userId, roleName, scope)); diff --git a/app/models/server/raw/ServerEvents.ts b/app/models/server/raw/ServerEvents.ts index d906f41edce..f36b44983e1 100644 --- a/app/models/server/raw/ServerEvents.ts +++ b/app/models/server/raw/ServerEvents.ts @@ -4,7 +4,7 @@ import { BaseRaw } from './BaseRaw'; import { IServerEvent, IServerEventType } from '../../../../definition/IServerEvent'; import { IUser } from '../../../../definition/IUser'; -export class ServerEventsRaw extends BaseRaw { +export class ServerEventsRaw extends BaseRaw { public readonly col!: Collection; async insertOne(data: Omit): Promise { diff --git a/app/models/server/raw/Settings.js b/app/models/server/raw/Settings.js deleted file mode 100644 index 7bfeb5be325..00000000000 --- a/app/models/server/raw/Settings.js +++ /dev/null @@ -1,37 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class SettingsRaw extends BaseRaw { - async getValueById(_id) { - const setting = await this.col.findOne({ _id }, { projection: { value: 1 } }); - - return setting.value; - } - - findByIds(_id = []) { - _id = [].concat(_id); - - const query = { - _id: { - $in: _id, - }, - }; - - return this.find(query); - } - - updateValueById(_id, value) { - const query = { - blocked: { $ne: true }, - value: { $ne: value }, - _id, - }; - - const update = { - $set: { - value, - }, - }; - - return this.col.update(query, update); - } -} diff --git a/app/models/server/raw/Settings.ts b/app/models/server/raw/Settings.ts new file mode 100644 index 00000000000..7f25b049284 --- /dev/null +++ b/app/models/server/raw/Settings.ts @@ -0,0 +1,53 @@ +import { Cursor, WriteOpResult } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { ISetting } from '../../../../definition/ISetting'; + +type T = ISetting; + +export class SettingsRaw extends BaseRaw { + async getValueById(_id: string): Promise { + const setting = await this.findOne({ _id }, { projection: { value: 1 } }); + + return setting?.value; + } + + findOneNotHiddenById(_id: string): Promise { + const query = { + _id, + hidden: { $ne: true }, + }; + + return this.findOne(query); + } + + findByIds(_id: string[] | string = []): Cursor { + if (typeof _id === 'string') { + _id = [_id]; + } + + const query = { + _id: { + $in: _id, + }, + }; + + return this.find(query); + } + + updateValueById(_id: string, value: any): Promise { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id, + }; + + const update = { + $set: { + value, + }, + }; + + return this.update(query, update); + } +} diff --git a/app/models/server/raw/Subscriptions.js b/app/models/server/raw/Subscriptions.js deleted file mode 100644 index 8860ebc2132..00000000000 --- a/app/models/server/raw/Subscriptions.js +++ /dev/null @@ -1,57 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class SubscriptionsRaw extends BaseRaw { - findOneByRoomIdAndUserId(rid, uid, options) { - const query = { - rid, - 'u._id': uid, - }; - - return this.col.findOne(query, options); - } - - countByRoomIdAndUserId(rid, uid) { - const query = { - rid, - 'u._id': uid, - }; - - const cursor = this.col.find(query); - - return cursor.count(); - } - - isUserInRole(uid, roleName, rid) { - if (rid == null) { - return; - } - - const query = { - 'u._id': uid, - rid, - roles: roleName, - }; - - return this.findOne(query, { fields: { roles: 1 } }); - } - - setAsReadByRoomIdAndUserId(rid, uid, alert = false) { - const query = { - rid, - 'u._id': uid, - }; - - const update = { - $set: { - open: true, - alert, - unread: 0, - userMentions: 0, - groupMentions: 0, - ls: new Date(), - }, - }; - - return this.col.update(query, update); - } -} diff --git a/app/models/server/raw/Subscriptions.ts b/app/models/server/raw/Subscriptions.ts new file mode 100644 index 00000000000..1cd61692b69 --- /dev/null +++ b/app/models/server/raw/Subscriptions.ts @@ -0,0 +1,72 @@ +import { FindOneOptions, Cursor, UpdateQuery, FilterQuery } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { ISubscription } from '../../../../definition/ISubscription'; + +type T = ISubscription; +export class SubscriptionsRaw extends BaseRaw { + findOneByRoomIdAndUserId(rid: string, uid: string, options: FindOneOptions = {}): Promise { + const query = { + rid, + 'u._id': uid, + }; + + return this.findOne(query, options); + } + + findByRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions = {}): Cursor { + const query = { + rid: roomId, + 'u._id': { + $ne: userId, + }, + }; + + return this.find(query, options); + } + + countByRoomIdAndUserId(rid: string, uid: string): Promise { + const query = { + rid, + 'u._id': uid, + }; + + const cursor = this.find(query, { projection: { _id: 0 } }); + + return cursor.count(); + } + + async isUserInRole(uid: string, roleName: string, rid: string): Promise { + if (rid == null) { + return; + } + + const query = { + 'u._id': uid, + rid, + roles: roleName, + }; + + return this.findOne(query, { projection: { roles: 1 } }); + } + + setAsReadByRoomIdAndUserId(rid: string, uid: string, alert = false, options: FindOneOptions = {}): ReturnType['update']> { + const query: FilterQuery = { + rid, + 'u._id': uid, + }; + + const update: UpdateQuery = { + $set: { + open: true, + alert, + unread: 0, + userMentions: 0, + groupMentions: 0, + ls: new Date(), + }, + }; + + return this.update(query, update, options); + } +} diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index 62c15e0ba62..9d293bb7348 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -27,6 +27,15 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } + findOneAgentById(_id, options) { + const query = { + _id, + roles: 'livechat-agent', + }; + + return this.findOne(query, options); + } + findUsersInRolesWithQuery(roles, query, options) { roles = [].concat(roles); @@ -48,6 +57,15 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } + findOneByIdAndLoginHashedToken(_id, token, options = {}) { + const query = { + _id, + 'services.resume.loginTokens.hashedToken': token, + }; + + return this.findOne(query, options); + } + findByActiveUsersExcept(searchTerm, exceptions, options, searchFields, extraQuery = [], { startsWith = false, endsWith = false } = {}) { if (exceptions == null) { exceptions = []; } if (options == null) { options = {}; } diff --git a/app/models/server/raw/UsersSessions.ts b/app/models/server/raw/UsersSessions.ts new file mode 100644 index 00000000000..b89feaa9deb --- /dev/null +++ b/app/models/server/raw/UsersSessions.ts @@ -0,0 +1,4 @@ +import { BaseRaw } from './BaseRaw'; +import { IUserSession } from '../../../../definition/IUserSession'; + +export class UsersSessionsRaw extends BaseRaw {} diff --git a/app/models/server/raw/index.ts b/app/models/server/raw/index.ts index 40104d2dc62..f161d4523fd 100644 --- a/app/models/server/raw/index.ts +++ b/app/models/server/raw/index.ts @@ -51,35 +51,93 @@ import { NotificationQueueRaw } from './NotificationQueue'; import LivechatBusinessHoursModel from '../models/LivechatBusinessHours'; import { LivechatBusinessHoursRaw } from './LivechatBusinessHours'; import ServerEventModel from '../models/ServerEvents'; +import { UsersSessionsRaw } from './UsersSessions'; +import UsersSessionsModel from '../models/UsersSessions'; import { ServerEventsRaw } from './ServerEvents'; +import { trash } from '../models/_BaseDb'; +import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; +import LoginServiceConfigurationModel from '../models/LoginServiceConfiguration'; +import { LoginServiceConfigurationRaw } from './LoginServiceConfiguration'; +import { InstanceStatusRaw } from './InstanceStatus'; +import InstanceStatusModel from '../models/InstanceStatus'; +import { IntegrationHistoryRaw } from './IntegrationHistory'; +import IntegrationHistoryModel from '../models/IntegrationHistory'; import OmnichannelQueueModel from '../models/OmnichannelQueue'; import { OmnichannelQueueRaw } from './OmnichannelQueue'; -export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection()); -export const Roles = new RolesRaw(RolesModel.model.rawCollection()); -export const Subscriptions = new SubscriptionsRaw(SubscriptionsModel.model.rawCollection()); -export const Settings = new SettingsRaw(SettingsModel.model.rawCollection()); -export const Users = new UsersRaw(UsersModel.model.rawCollection()); -export const Sessions = new SessionsRaw(SessionsModel.model.rawCollection()); -export const Rooms = new RoomsRaw(RoomsModel.model.rawCollection()); -export const LivechatCustomField = new LivechatCustomFieldRaw(LivechatCustomFieldModel.model.rawCollection()); -export const LivechatTrigger = new LivechatTriggerRaw(LivechatTriggerModel.model.rawCollection()); -export const LivechatDepartment = new LivechatDepartmentRaw(LivechatDepartmentModel.model.rawCollection()); -export const LivechatDepartmentAgents = new LivechatDepartmentAgentsRaw(LivechatDepartmentAgentsModel.model.rawCollection()); -export const LivechatRooms = new LivechatRoomsRaw(LivechatRoomsModel.model.rawCollection()); -export const Messages = new MessagesRaw(MessagesModel.model.rawCollection()); -export const LivechatExternalMessage = new LivechatExternalMessageRaw(LivechatExternalMessagesModel.model.rawCollection()); -export const LivechatVisitors = new LivechatVisitorsRaw(LivechatVisitorsModel.model.rawCollection()); -export const LivechatInquiry = new LivechatInquiryRaw(LivechatInquiryModel.model.rawCollection()); -export const Integrations = new IntegrationsRaw(IntegrationsModel.model.rawCollection()); -export const EmojiCustom = new EmojiCustomRaw(EmojiCustomModel.model.rawCollection()); -export const WebdavAccounts = new WebdavAccountsRaw(WebdavAccountsModel.model.rawCollection()); -export const OAuthApps = new OAuthAppsRaw(OAuthAppsModel.model.rawCollection()); -export const CustomSounds = new CustomSoundsRaw(CustomSoundsModel.model.rawCollection()); -export const CustomUserStatus = new CustomUserStatusRaw(CustomUserStatusModel.model.rawCollection()); -export const LivechatAgentActivity = new LivechatAgentActivityRaw(LivechatAgentActivityModel.model.rawCollection()); -export const Statistics = new StatisticsRaw(StatisticsModel.model.rawCollection()); -export const NotificationQueue = new NotificationQueueRaw(NotificationQueueModel.model.rawCollection()); -export const LivechatBusinessHours = new LivechatBusinessHoursRaw(LivechatBusinessHoursModel.model.rawCollection()); -export const ServerEvents = new ServerEventsRaw(ServerEventModel.model.rawCollection()); -export const OmnichannelQueue = new OmnichannelQueueRaw(OmnichannelQueueModel.model.rawCollection()); +const trashCollection = trash.rawCollection(); + +export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection(), trashCollection); +export const Subscriptions = new SubscriptionsRaw(SubscriptionsModel.model.rawCollection(), trashCollection); +export const Settings = new SettingsRaw(SettingsModel.model.rawCollection(), trashCollection); +export const Users = new UsersRaw(UsersModel.model.rawCollection(), trashCollection); +export const Rooms = new RoomsRaw(RoomsModel.model.rawCollection(), trashCollection); +export const LivechatCustomField = new LivechatCustomFieldRaw(LivechatCustomFieldModel.model.rawCollection(), trashCollection); +export const LivechatTrigger = new LivechatTriggerRaw(LivechatTriggerModel.model.rawCollection(), trashCollection); +export const LivechatDepartment = new LivechatDepartmentRaw(LivechatDepartmentModel.model.rawCollection(), trashCollection); +export const LivechatDepartmentAgents = new LivechatDepartmentAgentsRaw(LivechatDepartmentAgentsModel.model.rawCollection(), trashCollection); +export const LivechatRooms = new LivechatRoomsRaw(LivechatRoomsModel.model.rawCollection(), trashCollection); +export const Messages = new MessagesRaw(MessagesModel.model.rawCollection(), trashCollection); +export const LivechatExternalMessage = new LivechatExternalMessageRaw(LivechatExternalMessagesModel.model.rawCollection(), trashCollection); +export const LivechatVisitors = new LivechatVisitorsRaw(LivechatVisitorsModel.model.rawCollection(), trashCollection); +export const LivechatInquiry = new LivechatInquiryRaw(LivechatInquiryModel.model.rawCollection(), trashCollection); +export const Integrations = new IntegrationsRaw(IntegrationsModel.model.rawCollection(), trashCollection); +export const EmojiCustom = new EmojiCustomRaw(EmojiCustomModel.model.rawCollection(), trashCollection); +export const WebdavAccounts = new WebdavAccountsRaw(WebdavAccountsModel.model.rawCollection(), trashCollection); +export const OAuthApps = new OAuthAppsRaw(OAuthAppsModel.model.rawCollection(), trashCollection); +export const CustomSounds = new CustomSoundsRaw(CustomSoundsModel.model.rawCollection(), trashCollection); +export const CustomUserStatus = new CustomUserStatusRaw(CustomUserStatusModel.model.rawCollection(), trashCollection); +export const LivechatAgentActivity = new LivechatAgentActivityRaw(LivechatAgentActivityModel.model.rawCollection(), trashCollection); +export const Statistics = new StatisticsRaw(StatisticsModel.model.rawCollection(), trashCollection); +export const NotificationQueue = new NotificationQueueRaw(NotificationQueueModel.model.rawCollection(), trashCollection); +export const LivechatBusinessHours = new LivechatBusinessHoursRaw(LivechatBusinessHoursModel.model.rawCollection(), trashCollection); +export const ServerEvents = new ServerEventsRaw(ServerEventModel.model.rawCollection(), trashCollection); +export const Roles = new RolesRaw(RolesModel.model.rawCollection(), trashCollection, { Users, Subscriptions }); +export const UsersSessions = new UsersSessionsRaw(UsersSessionsModel.model.rawCollection(), trashCollection); +export const LoginServiceConfiguration = new LoginServiceConfigurationRaw(LoginServiceConfigurationModel.model.rawCollection(), trashCollection); +export const InstanceStatus = new InstanceStatusRaw(InstanceStatusModel.model.rawCollection(), trashCollection); +export const IntegrationHistory = new IntegrationHistoryRaw(IntegrationHistoryModel.model.rawCollection(), trashCollection); +export const Sessions = new SessionsRaw(SessionsModel.model.rawCollection(), trashCollection); +export const OmnichannelQueue = new OmnichannelQueueRaw(OmnichannelQueueModel.model.rawCollection(), trashCollection); + +const map = { + [Messages.col.collectionName]: MessagesModel, + [Users.col.collectionName]: UsersModel, + [Subscriptions.col.collectionName]: SubscriptionsModel, + [Settings.col.collectionName]: SettingsModel, + [Roles.col.collectionName]: RolesModel, + [Permissions.col.collectionName]: PermissionsModel, + [LivechatInquiry.col.collectionName]: LivechatInquiryModel, + [LivechatDepartmentAgents.col.collectionName]: LivechatDepartmentAgentsModel, + [UsersSessions.col.collectionName]: UsersSessionsModel, + [Rooms.col.collectionName]: RoomsModel, + [LoginServiceConfiguration.col.collectionName]: LoginServiceConfigurationModel, + [InstanceStatus.col.collectionName]: InstanceStatusModel, + [IntegrationHistory.col.collectionName]: IntegrationHistoryModel, + [Integrations.col.collectionName]: IntegrationsModel, +}; + +!process.env.DISABLE_DB_WATCH && initWatchers({ + Messages, + Users, + Subscriptions, + Settings, + LivechatInquiry, + LivechatDepartmentAgents, + UsersSessions, + Permissions, + Roles, + Rooms, + LoginServiceConfiguration, + InstanceStatus, + IntegrationHistory, + Integrations, +}, (model, fn) => { + const meteorModel = map[model.col.collectionName]; + + if (!meteorModel) { + return; + } + + meteorModel.on('change', fn); +}); diff --git a/app/notifications/server/lib/Notifications.js b/app/notifications/server/lib/Notifications.js deleted file mode 100644 index 003db8f22f1..00000000000 --- a/app/notifications/server/lib/Notifications.js +++ /dev/null @@ -1,212 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { DDPCommon } from 'meteor/ddp-common'; - -import { WEB_RTC_EVENTS } from '../../../webrtc'; -import { Subscriptions, Rooms } from '../../../models/server'; -import { settings } from '../../../settings/server'; - -const changedPayload = function(collection, id, fields) { - return DDPCommon.stringifyDDP({ - msg: 'changed', - collection, - id, - fields, - }); -}; -const send = function(self, msg) { - if (!self.socket) { - return; - } - self.socket.send(msg); -}; -class RoomStreamer extends Meteor.Streamer { - _publish(publication, eventName, options) { - super._publish(publication, eventName, options); - const uid = Meteor.userId(); - if (/rooms-changed/.test(eventName)) { - const roomEvent = (...args) => send(publication._session, changedPayload(this.subscriptionName, 'id', { - eventName: `${ uid }/rooms-changed`, - args, - })); - const rooms = Subscriptions.find({ 'u._id': uid }, { fields: { rid: 1 } }).fetch(); - rooms.forEach(({ rid }) => { - this.on(rid, roomEvent); - }); - - const userEvent = (clientAction, { rid }) => { - switch (clientAction) { - case 'inserted': - rooms.push({ rid }); - this.on(rid, roomEvent); - - // after a subscription is added need to emit the room again - roomEvent('inserted', Rooms.findOneById(rid)); - break; - - case 'removed': - this.removeListener(rid, roomEvent); - break; - } - }; - this.on(uid, userEvent); - - publication.onStop(() => { - this.removeListener(uid, userEvent); - rooms.forEach(({ rid }) => this.removeListener(rid, roomEvent)); - }); - } - } -} - -class Notifications { - constructor() { - const self = this; - this.debug = false; - this.notifyUser = this.notifyUser.bind(this); - this.streamAll = new Meteor.Streamer('notify-all'); - this.streamLogged = new Meteor.Streamer('notify-logged'); - this.streamRoom = new Meteor.Streamer('notify-room'); - this.streamRoomUsers = new Meteor.Streamer('notify-room-users'); - this.streamUser = new RoomStreamer('notify-user'); - this.streamAll.allowWrite('none'); - this.streamLogged.allowWrite('none'); - this.streamRoom.allowWrite('none'); - this.streamRoomUsers.allowWrite(function(eventName, ...args) { - const [roomId, e] = eventName.split('/'); - // const user = Meteor.users.findOne(this.userId, { - // fields: { - // username: 1 - // } - // }); - if (Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId) != null) { - const subscriptions = Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId).fetch(); - subscriptions.forEach((subscription) => self.notifyUser(subscription.u._id, e, ...args)); - } - return false; - }); - this.streamUser.allowWrite('logged'); - this.streamAll.allowRead('all'); - this.streamLogged.allowRead('logged'); - this.streamRoom.allowRead(function(eventName, extraData) { - const [roomId] = eventName.split('/'); - const room = Rooms.findOneById(roomId); - if (!room) { - console.warn(`Invalid streamRoom eventName: "${ eventName }"`); - return false; - } - if (room.t === 'l' && extraData && extraData.token && room.v.token === extraData.token) { - return true; - } - if (this.userId == null) { - return false; - } - const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { fields: { _id: 1 } }); - return subscription != null; - }); - this.streamRoomUsers.allowRead('none'); - this.streamUser.allowRead(function(eventName) { - const [userId] = eventName.split('/'); - return (this.userId != null) && this.userId === userId; - }); - } - - notifyAll(eventName, ...args) { - if (this.debug === true) { - console.log('notifyAll', [eventName, ...args]); - } - args.unshift(eventName); - return this.streamAll.emit.apply(this.streamAll, args); - } - - notifyLogged(eventName, ...args) { - if (this.debug === true) { - console.log('notifyLogged', [eventName, ...args]); - } - args.unshift(eventName); - return this.streamLogged.emit.apply(this.streamLogged, args); - } - - notifyRoom(room, eventName, ...args) { - if (this.debug === true) { - console.log('notifyRoom', [room, eventName, ...args]); - } - args.unshift(`${ room }/${ eventName }`); - return this.streamRoom.emit.apply(this.streamRoom, args); - } - - notifyUser(userId, eventName, ...args) { - if (this.debug === true) { - console.log('notifyUser', [userId, eventName, ...args]); - } - args.unshift(`${ userId }/${ eventName }`); - return this.streamUser.emit.apply(this.streamUser, args); - } - - notifyAllInThisInstance(eventName, ...args) { - if (this.debug === true) { - console.log('notifyAll', [eventName, ...args]); - } - args.unshift(eventName); - return this.streamAll.emitWithoutBroadcast.apply(this.streamAll, args); - } - - notifyLoggedInThisInstance(eventName, ...args) { - if (this.debug === true) { - console.log('notifyLogged', [eventName, ...args]); - } - args.unshift(eventName); - return this.streamLogged.emitWithoutBroadcast.apply(this.streamLogged, args); - } - - notifyRoomInThisInstance(room, eventName, ...args) { - if (this.debug === true) { - console.log('notifyRoomAndBroadcast', [room, eventName, ...args]); - } - args.unshift(`${ room }/${ eventName }`); - return this.streamRoom.emitWithoutBroadcast.apply(this.streamRoom, args); - } - - notifyUserInThisInstance(userId, eventName, ...args) { - if (this.debug === true) { - console.log('notifyUserAndBroadcast', [userId, eventName, ...args]); - } - args.unshift(`${ userId }/${ eventName }`); - return this.streamUser.emitWithoutBroadcast.apply(this.streamUser, args); - } -} - -const notifications = new Notifications(); - -notifications.streamRoom.allowWrite(function(eventName, username, typing, extraData) { - const [roomId, e] = eventName.split('/'); - - if (isNaN(e) ? e === WEB_RTC_EVENTS.WEB_RTC : parseFloat(e) === WEB_RTC_EVENTS.WEB_RTC) { - return true; - } - - if (e === 'typing') { - const key = settings.get('UI_Use_Real_Name') ? 'name' : 'username'; - // typing from livechat widget - if (extraData && extraData.token) { - const room = Rooms.findOneById(roomId); - if (room && room.t === 'l' && room.v.token === extraData.token) { - return true; - } - } - - const user = Meteor.users.findOne(this.userId, { - fields: { - [key]: 1, - }, - }); - - if (!user) { - return false; - } - - return user[key] === username; - } - return false; -}); - -export default notifications; diff --git a/app/notifications/server/lib/Notifications.ts b/app/notifications/server/lib/Notifications.ts new file mode 100644 index 00000000000..a84a8f2d4d3 --- /dev/null +++ b/app/notifications/server/lib/Notifications.ts @@ -0,0 +1,54 @@ +import { Meteor } from 'meteor/meteor'; +import { Promise } from 'meteor/promise'; +import { DDPCommon } from 'meteor/ddp-common'; + +import { NotificationsModule } from '../../../../server/modules/notifications/notifications.module'; +import { Streamer, StreamerCentral } from '../../../../server/modules/streamer/streamer.module'; +import { api } from '../../../../server/sdk/api'; +import { + Subscriptions as SubscriptionsRaw, + Rooms as RoomsRaw, + Users as UsersRaw, + Settings as SettingsRaw, +} from '../../../models/server/raw'; + +// TODO: Replace this in favor of the api.broadcast +StreamerCentral.on('broadcast', (name, eventName, args) => { + api.broadcast('stream', [ + name, + eventName, + args, + ]); +}); + +export class Stream extends Streamer { + registerPublication(name: string, fn: (eventName: string, options: boolean | {useCollection?: boolean; args?: any}) => void): void { + Meteor.publish(name, function(eventName, options) { + return Promise.await(fn.call(this, eventName, options)); + }); + } + + registerMethod(methods: Record any>): void { + Meteor.methods(methods); + } + + changedPayload(collection: string, id: string, fields: Record): string | false { + return DDPCommon.stringifyDDP({ + msg: 'changed', + collection, + id, + fields, + }); + } +} + +const notifications = new NotificationsModule(Stream); + +notifications.configure({ + Rooms: RoomsRaw, + Subscriptions: SubscriptionsRaw, + Users: UsersRaw, + Settings: SettingsRaw, +}); + +export default notifications; diff --git a/app/reactions/server/setReaction.js b/app/reactions/server/setReaction.js index 6729baf00ce..53de3fe70c1 100644 --- a/app/reactions/server/setReaction.js +++ b/app/reactions/server/setReaction.js @@ -1,14 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; import { Messages, EmojiCustom, Rooms } from '../../models'; -import { Notifications } from '../../notifications'; import { callbacks } from '../../callbacks'; import { emoji } from '../../emoji'; import { isTheLastMessage, msgStream } from '../../lib'; import { hasPermission } from '../../authorization/server/functions/hasPermission'; +import { api } from '../../../server/sdk/api'; const removeUserReaction = (message, reaction, username) => { message.reactions[reaction].usernames.splice(message.reactions[reaction].usernames.indexOf(username), 1); @@ -112,10 +111,7 @@ Meteor.methods({ return Promise.await(executeSetReaction(reaction, messageId, shouldReact)); } catch (e) { if (e.error === 'error-not-allowed' && e.reason && e.details && e.details.rid) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: e.details.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), e.details.rid, { msg: e.reason, }); diff --git a/app/search/server/events/events.js b/app/search/server/events/events.js index 41a1e217b45..cbfc53a0d4c 100644 --- a/app/search/server/events/events.js +++ b/app/search/server/events/events.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../callbacks/server'; -import { Users, Rooms } from '../../../models/server'; import { searchProviderService } from '../service/providerService'; import SearchLogger from '../logger/logger'; @@ -19,61 +18,27 @@ class EventService { } } -const eventService = new EventService(); +export const searchEventService = new EventService(); /** * Listen to message changes via Hooks */ function afterSaveMessage(m) { - eventService.promoteEvent('message.save', m._id, m); + searchEventService.promoteEvent('message.save', m._id, m); return m; } function afterDeleteMessage(m) { - eventService.promoteEvent('message.delete', m._id); + searchEventService.promoteEvent('message.delete', m._id); return m; } -/** - * Listen to user and room changes via cursor - */ -function onUsersChange({ clientAction, id, data }) { - switch (clientAction) { - case 'updated': - case 'inserted': - const user = data ?? Users.findOneById(id); - eventService.promoteEvent('user.save', id, user); - break; - - case 'removed': - eventService.promoteEvent('user.delete', id); - break; - } -} - -function onRoomsChange({ clientAction, id, data }) { - switch (clientAction) { - case 'updated': - case 'inserted': - const room = data ?? Rooms.findOneById(id); - eventService.promoteEvent('room.save', id, room); - break; - - case 'removed': - eventService.promoteEvent('room.delete', id); - break; - } -} settings.get('Search.Provider', _.debounce(() => { if (searchProviderService.activeProvider?.on) { - Users.on('change', onUsersChange); - Rooms.on('change', onRoomsChange); callbacks.add('afterSaveMessage', afterSaveMessage, callbacks.priority.MEDIUM, 'search-events'); callbacks.add('afterDeleteMessage', afterDeleteMessage, callbacks.priority.MEDIUM, 'search-events-delete'); } else { - Users.removeListener('change', onUsersChange); - Rooms.removeListener('change', onRoomsChange); callbacks.remove('afterSaveMessage', 'search-events'); callbacks.remove('afterDeleteMessage', 'search-events-delete'); } diff --git a/app/search/server/index.js b/app/search/server/index.js index e0fd6273a18..ae4aa30fa61 100644 --- a/app/search/server/index.js +++ b/app/search/server/index.js @@ -3,6 +3,7 @@ import { searchProviderService } from './service/providerService.js'; import './service/validationService.js'; import './events/events.js'; import './provider/defaultProvider.js'; +import './search.internalService'; export { diff --git a/app/search/server/search.internalService.ts b/app/search/server/search.internalService.ts new file mode 100644 index 00000000000..bc247103dc9 --- /dev/null +++ b/app/search/server/search.internalService.ts @@ -0,0 +1,45 @@ +import _ from 'underscore'; + +import { Users } from '../../models/server'; +import { settings } from '../../settings/server'; +import { searchProviderService } from './service/providerService'; +import { ServiceClass } from '../../../server/sdk/types/ServiceClass'; +import { api } from '../../../server/sdk/api'; +import { searchEventService } from './events/events'; + +class Search extends ServiceClass { + protected name = 'search'; + + constructor() { + super(); + + this.onEvent('watch.users', async ({ clientAction, data, id }) => { + if (clientAction === 'removed') { + searchEventService.promoteEvent('user.delete', id, undefined); + return; + } + + const user = data ?? Users.findOneById(id); + searchEventService.promoteEvent('user.save', id, user); + }); + + this.onEvent('watch.rooms', async ({ clientAction, room }) => { + if (clientAction === 'removed') { + searchEventService.promoteEvent('room.delete', room._id, undefined); + return; + } + + searchEventService.promoteEvent('room.save', room._id, room); + }); + } +} + +const service = new Search(); + +settings.get('Search.Provider', _.debounce(() => { + if (searchProviderService.activeProvider?.on) { + api.registerService(service); + } else { + api.destroyService(service); + } +}, 1000)); diff --git a/app/settings/client/lib/settings.ts b/app/settings/client/lib/settings.ts index c94664ae4d9..ae5610677f0 100644 --- a/app/settings/client/lib/settings.ts +++ b/app/settings/client/lib/settings.ts @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveDict } from 'meteor/reactive-dict'; import { PublicSettingsCachedCollection } from '../../../../client/lib/settings/PublicSettingsCachedCollection'; -import { SettingsBase, SettingValue } from '../../lib/settings'; +import { SettingsBase } from '../../lib/settings'; +import { SettingValue } from '../../../../definition/ISetting'; class Settings extends SettingsBase { cachedCollection = PublicSettingsCachedCollection.get() diff --git a/app/settings/lib/settings.ts b/app/settings/lib/settings.ts index 4bc180c9941..ae4bb6a7b2f 100644 --- a/app/settings/lib/settings.ts +++ b/app/settings/lib/settings.ts @@ -1,9 +1,8 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -export type SettingValueMultiSelect = Array<{key: string; i18nLabel: string}> -export type SettingValueRoomPick = Array<{_id: string; name: string}> | string -export type SettingValue = string | boolean | number | SettingValueMultiSelect | undefined; +import { SettingValue } from '../../../definition/ISetting'; + export type SettingComposedValue = {key: string; value: SettingValue}; export type SettingCallback = (key: string, value: SettingValue, initialLoad?: boolean) => void; diff --git a/app/settings/server/functions/settings.ts b/app/settings/server/functions/settings.ts index 3bff85f1f1b..3c79ea20bec 100644 --- a/app/settings/server/functions/settings.ts +++ b/app/settings/server/functions/settings.ts @@ -3,9 +3,10 @@ import { EventEmitter } from 'events'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { SettingsBase, SettingValue } from '../../lib/settings'; +import { SettingsBase } from '../../lib/settings'; import SettingsModel from '../../../models/server/models/Settings'; -import { setValue, updateValue } from '../raw'; +import { updateValue } from '../raw'; +import { ISetting, SettingValue } from '../../../../definition/ISetting'; const blockedSettings = new Set(); const hiddenSettings = new Set(); @@ -64,44 +65,8 @@ const overrideSetting = (_id: string, value: SettingValue, options: ISettingAddO return value; }; -export interface ISettingAddOptions { - _id?: string; - type?: 'group' | 'boolean' | 'int' | 'string' | 'asset' | 'code' | 'select' | 'password' | 'action' | 'relativeUrl' | 'language' | 'date' | 'color' | 'font' | 'roomPick' | 'multiSelect'; - editor?: string; - packageEditor?: string; - packageValue?: SettingValue; - valueSource?: string; - hidden?: boolean; - blocked?: boolean; - requiredOnWizard?: boolean; - secret?: boolean; - sorter?: number; - i18nLabel?: string; - i18nDescription?: string; - autocomplete?: boolean; +export interface ISettingAddOptions extends Partial { force?: boolean; - group?: string; - section?: string; - enableQuery?: any; - processEnvValue?: SettingValue; - meteorSettingsValue?: SettingValue; - value?: SettingValue; - ts?: Date; - multiline?: boolean; - values?: Array; - public?: boolean; - enterprise?: boolean; - modules?: Array; - invalidValue?: SettingValue; -} -export interface ISettingSelectOption { - key: string; - i18nLabel: string; -} -export interface ISettingRecord extends ISettingAddOptions { - _id: string; - env: boolean; - value: SettingValue; } export interface ISettingAddGroupOptions { @@ -362,7 +327,7 @@ class Settings extends SettingsBase { /* * Change a setting value on the Meteor.settings object */ - storeSettingValue(record: ISettingRecord, initialLoad: boolean): void { + storeSettingValue(record: ISetting, initialLoad: boolean): void { const newData = { value: record.value, }; @@ -380,7 +345,7 @@ class Settings extends SettingsBase { /* * Remove a setting value on the Meteor.settings object */ - removeSettingValue(record: ISettingRecord, initialLoad: boolean): void { + removeSettingValue(record: ISetting, initialLoad: boolean): void { SettingsEvents.emit('remove-setting-value', record); delete Meteor.settings[record._id]; @@ -396,28 +361,12 @@ class Settings extends SettingsBase { */ init(): void { this.initialLoad = true; - SettingsModel.find().fetch().forEach((record: ISettingRecord) => { + SettingsModel.find().fetch().forEach((record: ISetting) => { this.storeSettingValue(record, this.initialLoad); updateValue(record._id, { value: record.value }); }); this.initialLoad = false; this.afterInitialLoad.forEach((fn) => fn(Meteor.settings)); - - SettingsModel.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'inserted': - case 'updated': - data = data ?? SettingsModel.findOneById(id); - this.storeSettingValue(data, this.initialLoad); - updateValue(id, { value: data.value }); - break; - case 'removed': - data = SettingsModel.trashFindOneById(id); - this.removeSettingValue(data, this.initialLoad); - setValue(id, undefined); - break; - } - }); } onAfterInitialLoad(fn: (settings: Meteor.Settings) => void): void { diff --git a/app/settings/server/raw.js b/app/settings/server/raw.js index 436643cbae4..d458f7a5561 100644 --- a/app/settings/server/raw.js +++ b/app/settings/server/raw.js @@ -1,4 +1,4 @@ -import { Settings } from '../../models/server/models/Settings'; +import Settings from '../../models/server/models/Settings'; const cache = new Map(); diff --git a/app/slashcommands-archiveroom/server/server.js b/app/slashcommands-archiveroom/server/server.js index 6085a259742..6e10f24f6de 100644 --- a/app/slashcommands-archiveroom/server/server.js +++ b/app/slashcommands-archiveroom/server/server.js @@ -1,11 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Rooms, Messages } from '../../models'; import { slashCommands } from '../../utils'; -import { Notifications } from '../../notifications'; +import { api } from '../../../server/sdk/api'; function Archive(command, params, item) { if (command !== 'archive' || !Match.test(params, String)) { @@ -26,10 +25,7 @@ function Archive(command, params, item) { const user = Meteor.users.findOne(Meteor.userId()); if (!room) { - return Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Channel_doesnt_exist', { postProcess: 'sprintf', sprintf: [channel], @@ -43,10 +39,7 @@ function Archive(command, params, item) { } if (room.archived) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Duplicate_archived_channel_name', { postProcess: 'sprintf', sprintf: [channel], @@ -57,10 +50,7 @@ function Archive(command, params, item) { Meteor.call('archiveRoom', room._id); Messages.createRoomArchivedByRoomIdAndUser(room._id, Meteor.user()); - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Channel_Archived', { postProcess: 'sprintf', sprintf: [channel], diff --git a/app/slashcommands-create/server/server.js b/app/slashcommands-create/server/server.js index 898d44bdaac..21c26dac750 100644 --- a/app/slashcommands-create/server/server.js +++ b/app/slashcommands-create/server/server.js @@ -1,12 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../settings'; -import { Notifications } from '../../notifications'; import { Rooms } from '../../models'; import { slashCommands } from '../../utils'; +import { api } from '../../../server/sdk/api'; function Create(command, params, item) { function getParams(str) { @@ -36,10 +35,7 @@ function Create(command, params, item) { const user = Meteor.users.findOne(Meteor.userId()); const room = Rooms.findOneByName(channel); if (room != null) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Channel_already_exist', { postProcess: 'sprintf', sprintf: [channel], diff --git a/app/slashcommands-help/server/server.js b/app/slashcommands-help/server/server.js index ba76736dc9a..5b4eaec7140 100644 --- a/app/slashcommands-help/server/server.js +++ b/app/slashcommands-help/server/server.js @@ -1,9 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { slashCommands } from '../../utils'; -import { Notifications } from '../../notifications'; +import { api } from '../../../server/sdk/api'; /* * Help is a named function that will replace /join commands @@ -39,10 +38,7 @@ slashCommands.add('help', function Help(command, params, item) { }, ]; keys.forEach((key) => { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__(Object.keys(key)[0], { postProcess: 'sprintf', sprintf: [key[Object.keys(key)[0]]], diff --git a/app/slashcommands-hide/server/hide.js b/app/slashcommands-hide/server/hide.js index 2a316417d4d..a42041849b6 100644 --- a/app/slashcommands-hide/server/hide.js +++ b/app/slashcommands-hide/server/hide.js @@ -1,11 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Rooms, Subscriptions } from '../../models'; -import { Notifications } from '../../notifications'; import { slashCommands } from '../../utils'; +import { api } from '../../../server/sdk/api'; /* * Hide is a named function that will replace /hide commands @@ -29,10 +28,7 @@ function Hide(command, param, item) { }); if (!roomObject) { - return Notifications.notifyUser(user._id, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', user._id, item.rid, { msg: TAPi18n.__('Channel_doesnt_exist', { postProcess: 'sprintf', sprintf: [room], @@ -41,10 +37,7 @@ function Hide(command, param, item) { } if (!Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { fields: { _id: 1 } })) { - return Notifications.notifyUser(user._id, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', user._id, item.rid, { msg: TAPi18n.__('error-logged-user-not-in-room', { postProcess: 'sprintf', sprintf: [room], @@ -56,10 +49,7 @@ function Hide(command, param, item) { Meteor.call('hideRoom', rid, (error) => { if (error) { - return Notifications.notifyUser(user._id, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', user._id, item.rid, { msg: TAPi18n.__(error, null, user.language), }); } diff --git a/app/slashcommands-invite/server/server.js b/app/slashcommands-invite/server/server.js index eb804329cc5..7c3a953abb5 100644 --- a/app/slashcommands-invite/server/server.js +++ b/app/slashcommands-invite/server/server.js @@ -1,11 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Notifications } from '../../notifications'; import { slashCommands } from '../../utils'; import { Subscriptions } from '../../models'; +import { api } from '../../../server/sdk/api'; /* * Invite is a named function that will replace /invite commands @@ -30,10 +29,7 @@ function Invite(command, params, item) { const userId = Meteor.userId(); const currentUser = Meteor.users.findOne(userId); if (users.count() === 0) { - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__('User_doesnt_exist', { postProcess: 'sprintf', sprintf: [usernames.join(' @')], @@ -46,10 +42,7 @@ function Invite(command, params, item) { if (subscription == null) { return true; } - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__('Username_is_already_in_here', { postProcess: 'sprintf', sprintf: [user.username], @@ -66,17 +59,11 @@ function Invite(command, params, item) { }); } catch ({ error }) { if (error === 'cant-invite-for-direct-room') { - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__('Cannot_invite_users_to_direct_rooms', null, currentUser.language), }); } else { - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__(error, null, currentUser.language), }); } diff --git a/app/slashcommands-inviteall/server/server.js b/app/slashcommands-inviteall/server/server.js index d5481f70b11..c98a7098483 100644 --- a/app/slashcommands-inviteall/server/server.js +++ b/app/slashcommands-inviteall/server/server.js @@ -4,13 +4,12 @@ */ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Rooms, Subscriptions } from '../../models'; import { slashCommands } from '../../utils'; import { settings } from '../../settings'; -import { Notifications } from '../../notifications'; +import { api } from '../../../server/sdk/api'; function inviteAll(type) { return function inviteAll(command, params, item) { @@ -30,10 +29,7 @@ function inviteAll(type) { const targetChannel = type === 'from' ? Rooms.findOneById(item.rid) : Rooms.findOneByName(channel); if (!baseChannel) { - return Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__('Channel_doesnt_exist', { postProcess: 'sprintf', sprintf: [channel], @@ -52,10 +48,7 @@ function inviteAll(type) { if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) { Meteor.call(baseChannel.t === 'c' ? 'createChannel' : 'createPrivateGroup', channel, users); - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__('Channel_created', { postProcess: 'sprintf', sprintf: [channel], @@ -67,18 +60,12 @@ function inviteAll(type) { users, }); } - return Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__('Users_added', null, currentUser.language), }); } catch (e) { const msg = e.error === 'cant-invite-for-direct-room' ? 'Cannot_invite_users_to_direct_rooms' : e.error; - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__(msg, null, currentUser.language), }); } diff --git a/app/slashcommands-join/server/server.js b/app/slashcommands-join/server/server.js index 1b1802da704..0443c2300c7 100644 --- a/app/slashcommands-join/server/server.js +++ b/app/slashcommands-join/server/server.js @@ -1,11 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Rooms, Subscriptions } from '../../models'; -import { Notifications } from '../../notifications'; import { slashCommands } from '../../utils'; +import { api } from '../../../server/sdk/api'; function Join(command, params, item) { if (command !== 'join' || !Match.test(params, String)) { @@ -19,10 +18,7 @@ function Join(command, params, item) { const user = Meteor.users.findOne(Meteor.userId()); const room = Rooms.findOneByNameAndType(channel, 'c'); if (!room) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Channel_doesnt_exist', { postProcess: 'sprintf', sprintf: [channel], diff --git a/app/slashcommands-kick/server/server.js b/app/slashcommands-kick/server/server.js index 5bc785cc821..9808e86731c 100644 --- a/app/slashcommands-kick/server/server.js +++ b/app/slashcommands-kick/server/server.js @@ -2,12 +2,11 @@ // Kick is a named function that will replace /kick commands import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Notifications } from '../../notifications'; import { Users, Subscriptions } from '../../models'; import { slashCommands } from '../../utils'; +import { api } from '../../../server/sdk/api'; const Kick = function(command, params, { rid }) { if (command !== 'kick' || !Match.test(params, String)) { @@ -22,10 +21,7 @@ const Kick = function(command, params, { rid }) { const kickedUser = Users.findOneByUsernameIgnoringCase(username); if (kickedUser == null) { - return Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', userId, rid, { msg: TAPi18n.__('Username_doesnt_exist', { postProcess: 'sprintf', sprintf: [username], @@ -35,10 +31,7 @@ const Kick = function(command, params, { rid }) { const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, user._id, { fields: { _id: 1 } }); if (!subscription) { - return Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', userId, rid, { msg: TAPi18n.__('Username_is_not_in_this_room', { postProcess: 'sprintf', sprintf: [username], diff --git a/app/slashcommands-leave/server/leave.js b/app/slashcommands-leave/server/leave.js index ce6fc32fb1e..31436a2346a 100644 --- a/app/slashcommands-leave/server/leave.js +++ b/app/slashcommands-leave/server/leave.js @@ -1,9 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Notifications } from '../../notifications'; import { slashCommands } from '../../utils'; +import { api } from '../../../server/sdk/api'; /* * Leave is a named function that will replace /leave commands @@ -17,10 +16,7 @@ function Leave(command, params, item) { try { Meteor.call('leaveRoom', item.rid); } catch ({ error }) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__(error, null, Meteor.user().language), }); } diff --git a/app/slashcommands-msg/server/server.js b/app/slashcommands-msg/server/server.js index 5fa33fe901e..74cec48799f 100644 --- a/app/slashcommands-msg/server/server.js +++ b/app/slashcommands-msg/server/server.js @@ -4,8 +4,8 @@ import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { slashCommands } from '../../utils'; -import { Notifications } from '../../notifications'; import { Users } from '../../models'; +import { api } from '../../../server/sdk/api'; /* * Msg is a named function that will replace /msg commands @@ -19,10 +19,7 @@ function Msg(command, params, item) { const separator = trimmedParams.indexOf(' '); const user = Meteor.users.findOne(Meteor.userId()); if (separator === -1) { - return Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Username_and_message_must_not_be_empty', null, user.language), }); } @@ -31,10 +28,7 @@ function Msg(command, params, item) { const targetUsername = targetUsernameOrig.replace('@', ''); const targetUser = Users.findOneByUsernameIgnoringCase(targetUsername); if (targetUser == null) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Username_doesnt_exist', { postProcess: 'sprintf', sprintf: [targetUsernameOrig], diff --git a/app/slashcommands-mute/server/mute.js b/app/slashcommands-mute/server/mute.js index 306edfecde3..8321e6434b1 100644 --- a/app/slashcommands-mute/server/mute.js +++ b/app/slashcommands-mute/server/mute.js @@ -1,11 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { slashCommands } from '../../utils'; import { Users, Subscriptions } from '../../models'; -import { Notifications } from '../../notifications'; +import { api } from '../../../server/sdk/api'; /* * Mute is a named function that will replace /mute commands @@ -23,10 +22,7 @@ slashCommands.add('mute', function Mute(command, params, item) { const user = Meteor.users.findOne(userId); const mutedUser = Users.findOneByUsernameIgnoringCase(username); if (mutedUser == null) { - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__('Username_doesnt_exist', { postProcess: 'sprintf', sprintf: [username], @@ -37,10 +33,7 @@ slashCommands.add('mute', function Mute(command, params, item) { const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, mutedUser._id, { fields: { _id: 1 } }); if (!subscription) { - Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { msg: TAPi18n.__('Username_is_not_in_this_room', { postProcess: 'sprintf', sprintf: [username], diff --git a/app/slashcommands-mute/server/unmute.js b/app/slashcommands-mute/server/unmute.js index a84c518e48b..e962bf2979f 100644 --- a/app/slashcommands-mute/server/unmute.js +++ b/app/slashcommands-mute/server/unmute.js @@ -1,11 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { slashCommands } from '../../utils'; import { Users, Subscriptions } from '../../models'; -import { Notifications } from '../../notifications'; +import { api } from '../../../server/sdk/api'; /* * Unmute is a named function that will replace /unmute commands @@ -22,10 +21,7 @@ slashCommands.add('unmute', function Unmute(command, params, item) { const user = Meteor.users.findOne(Meteor.userId()); const unmutedUser = Users.findOneByUsernameIgnoringCase(username); if (unmutedUser == null) { - return Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Username_doesnt_exist', { postProcess: 'sprintf', sprintf: [username], @@ -35,10 +31,7 @@ slashCommands.add('unmute', function Unmute(command, params, item) { const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, unmutedUser._id, { fields: { _id: 1 } }); if (!subscription) { - return Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Username_is_not_in_this_room', { postProcess: 'sprintf', sprintf: [username], diff --git a/app/slashcommands-status/lib/status.js b/app/slashcommands-status/lib/status.js index 0141fdc1b1b..9f7a4658846 100644 --- a/app/slashcommands-status/lib/status.js +++ b/app/slashcommands-status/lib/status.js @@ -1,9 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Random } from 'meteor/random'; import { handleError, slashCommands } from '../../utils'; -import { Notifications } from '../../notifications'; +import { api } from '../../../server/sdk/api'; function Status(command, params, item) { if (command === 'status') { @@ -16,20 +15,14 @@ function Status(command, params, item) { } if (err.error === 'error-not-allowed') { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('StatusMessage_Change_Disabled', null, user.language), }); } throw err; } else { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('StatusMessage_Changed_Successfully', null, user.language), }); } diff --git a/app/slashcommands-unarchiveroom/server/server.js b/app/slashcommands-unarchiveroom/server/server.js index be2c56e47cb..07096dc8ec7 100644 --- a/app/slashcommands-unarchiveroom/server/server.js +++ b/app/slashcommands-unarchiveroom/server/server.js @@ -1,12 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Rooms, Messages } from '../../models'; import { slashCommands } from '../../utils'; -import { Notifications } from '../../notifications'; import { roomTypes, RoomMemberActions } from '../../utils/server'; +import { api } from '../../../server/sdk/api'; function Unarchive(command, params, item) { if (command !== 'unarchive' || !Match.test(params, String)) { @@ -27,10 +26,7 @@ function Unarchive(command, params, item) { const user = Meteor.users.findOne(Meteor.userId()); if (!room) { - return Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + return api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Channel_doesnt_exist', { postProcess: 'sprintf', sprintf: [channel], @@ -44,10 +40,7 @@ function Unarchive(command, params, item) { } if (!room.archived) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Channel_already_Unarchived', { postProcess: 'sprintf', sprintf: [channel], @@ -59,10 +52,7 @@ function Unarchive(command, params, item) { Meteor.call('unarchiveRoom', room._id); Messages.createRoomUnarchivedByRoomIdAndUser(room._id, Meteor.user()); - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: item.rid, - ts: new Date(), + api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { msg: TAPi18n.__('Channel_Unarchived', { postProcess: 'sprintf', sprintf: [channel], diff --git a/app/sms/server/services/twilio.js b/app/sms/server/services/twilio.js index c928ed8a447..bf52dcd5491 100644 --- a/app/sms/server/services/twilio.js +++ b/app/sms/server/services/twilio.js @@ -1,20 +1,16 @@ import { Meteor } from 'meteor/meteor'; import twilio from 'twilio'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import filesize from 'filesize'; import { settings } from '../../../settings'; import { SMS } from '../SMS'; -import { Notifications } from '../../../notifications'; import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions'; +import { api } from '../../../../server/sdk/api'; const MAX_FILE_SIZE = 5242880; -const notifyAgent = (userId, rid, msg) => Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid, - ts: new Date(), +const notifyAgent = (userId, rid, msg) => api.broadcast('notify.ephemeralMessage', userId, rid, { msg, }); diff --git a/app/sms/server/services/voxtelesys.js b/app/sms/server/services/voxtelesys.js index 7e0afc37eb3..3b0c7dd0ae9 100644 --- a/app/sms/server/services/voxtelesys.js +++ b/app/sms/server/services/voxtelesys.js @@ -1,21 +1,17 @@ import { HTTP } from 'meteor/http'; import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import filesize from 'filesize'; import { settings } from '../../../settings'; import { SMS } from '../SMS'; -import { Notifications } from '../../../notifications'; import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions'; import { mime } from '../../../utils/lib/mimeTypes'; +import { api } from '../../../../server/sdk/api'; const MAX_FILE_SIZE = 5242880; -const notifyAgent = (userId, rid, msg) => Notifications.notifyUser(userId, 'message', { - _id: Random.id(), - rid, - ts: new Date(), +const notifyAgent = (userId, rid, msg) => api.broadcast('notify.ephemeralMessage', userId, rid, { msg, }); diff --git a/app/tokenpass/server/roomAccessValidator.compatibility.js b/app/tokenpass/server/roomAccessValidator.compatibility.js new file mode 100644 index 00000000000..051227f1eaa --- /dev/null +++ b/app/tokenpass/server/roomAccessValidator.compatibility.js @@ -0,0 +1,18 @@ +import { Tokenpass } from './Tokenpass'; +import { Users } from '../../models'; + +export function validateTokenAccess(userData, roomData) { + if (!userData || !userData.services || !userData.services.tokenpass || !userData.services.tokenpass.tcaBalances) { + return false; + } + + return Tokenpass.validateAccess(roomData.tokenpass, userData.services.tokenpass.tcaBalances); +} + +export const validators = [ + function(room, user) { + const userData = Users.getTokenBalancesByUserId(user._id); + + return validateTokenAccess(userData, room); + }, +]; diff --git a/app/tokenpass/server/roomAccessValidator.internalService.ts b/app/tokenpass/server/roomAccessValidator.internalService.ts new file mode 100644 index 00000000000..0b5b364040c --- /dev/null +++ b/app/tokenpass/server/roomAccessValidator.internalService.ts @@ -0,0 +1,22 @@ +import { ServiceClass } from '../../../server/sdk/types/ServiceClass'; +import { validators } from './roomAccessValidator.compatibility'; +import { api } from '../../../server/sdk/api'; +import { IAuthorizationTokenpass } from '../../../server/sdk/types/IAuthorizationTokenpass'; +import { IRoom } from '../../../definition/IRoom'; +import { IUser } from '../../../definition/IUser'; + +class AuthorizationTokenpass extends ServiceClass implements IAuthorizationTokenpass { + protected name = 'authorization-tokenpass'; + + async canAccessRoom(room: Partial, user: Pick): Promise { + for (const validator of validators) { + if (validator(room, user)) { + return true; + } + } + + return false; + } +} + +api.registerService(new AuthorizationTokenpass()); diff --git a/app/tokenpass/server/startup.js b/app/tokenpass/server/startup.js index 4ed4a907431..6ff1ae50ce8 100644 --- a/app/tokenpass/server/startup.js +++ b/app/tokenpass/server/startup.js @@ -2,11 +2,10 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances'; -import { Tokenpass } from './Tokenpass'; import { settings } from '../../settings'; -import { addRoomAccessValidator } from '../../authorization'; -import { Users } from '../../models'; import { callbacks } from '../../callbacks'; +import { validateTokenAccess } from './roomAccessValidator.compatibility'; +import './roomAccessValidator.internalService'; settings.addGroup('OAuth', function() { this.section('Tokenpass', function() { @@ -23,25 +22,7 @@ settings.addGroup('OAuth', function() { }); }); -function validateTokenAccess(userData, roomData) { - if (!userData || !userData.services || !userData.services.tokenpass || !userData.services.tokenpass.tcaBalances) { - return false; - } - - return Tokenpass.validateAccess(roomData.tokenpass, userData.services.tokenpass.tcaBalances); -} - Meteor.startup(function() { - addRoomAccessValidator(function(room, user) { - if (!room || !room.tokenpass || !user) { - return false; - } - - const userData = Users.getTokenBalancesByUserId(user._id); - - return validateTokenAccess(userData, room); - }); - callbacks.add('beforeJoinRoom', function(user, room) { if (room.tokenpass && !validateTokenAccess(user, room)) { throw new Meteor.Error('error-not-allowed', 'Token required', { method: 'joinRoom' }); diff --git a/app/user-status/server/methods/deleteCustomUserStatus.js b/app/user-status/server/methods/deleteCustomUserStatus.js index 4d947ff5732..e81a8140d71 100644 --- a/app/user-status/server/methods/deleteCustomUserStatus.js +++ b/app/user-status/server/methods/deleteCustomUserStatus.js @@ -1,25 +1,22 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../authorization'; -import { Notifications } from '../../../notifications'; -import { CustomUserStatus } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { CustomUserStatus } from '../../../models/server'; +import { api } from '../../../../server/sdk/api'; Meteor.methods({ deleteCustomUserStatus(userStatusID) { - let userStatus = null; - - if (hasPermission(this.userId, 'manage-user-status')) { - userStatus = CustomUserStatus.findOneById(userStatusID); - } else { + if (!hasPermission(this.userId, 'manage-user-status')) { throw new Meteor.Error('not_authorized'); } + const userStatus = CustomUserStatus.findOneById(userStatusID); if (userStatus == null) { throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' }); } CustomUserStatus.removeById(userStatusID); - Notifications.notifyLogged('deleteCustomUserStatus', { userStatusData: userStatus }); + api.broadcast('user.deleteCustomStatus', userStatus); return true; }, diff --git a/app/user-status/server/methods/insertOrUpdateUserStatus.js b/app/user-status/server/methods/insertOrUpdateUserStatus.js index 07b4631173b..a01cc751c52 100644 --- a/app/user-status/server/methods/insertOrUpdateUserStatus.js +++ b/app/user-status/server/methods/insertOrUpdateUserStatus.js @@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import { hasPermission } from '../../../authorization'; -import { Notifications } from '../../../notifications'; import { CustomUserStatus } from '../../../models'; +import { api } from '../../../../server/sdk/api'; Meteor.methods({ insertOrUpdateUserStatus(userStatusData) { @@ -49,7 +49,7 @@ Meteor.methods({ const _id = CustomUserStatus.create(createUserStatus); - Notifications.notifyLogged('updateCustomUserStatus', { userStatusData: createUserStatus }); + api.broadcast('user.updateCustomStatus', createUserStatus); return _id; } @@ -63,7 +63,7 @@ Meteor.methods({ CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); } - Notifications.notifyLogged('updateCustomUserStatus', { userStatusData }); + api.broadcast('user.updateCustomStatus', userStatusData); return true; }, diff --git a/app/utils/lib/RoomTypeConfig.js b/app/utils/lib/RoomTypeConfig.js index bc40944c6b3..2e67d8f9c01 100644 --- a/app/utils/lib/RoomTypeConfig.js +++ b/app/utils/lib/RoomTypeConfig.js @@ -233,10 +233,6 @@ export class RoomTypeConfig { return false; } - isEmitAllowed() { - return false; - } - /** * Returns a text which can be used in generic UIs. * @param context The role of the text in the UI-Element diff --git a/client/admin/sidebar/AdminSidebarSettings.tsx b/client/admin/sidebar/AdminSidebarSettings.tsx index fd88b0554d3..f348ca9618c 100644 --- a/client/admin/sidebar/AdminSidebarSettings.tsx +++ b/client/admin/sidebar/AdminSidebarSettings.tsx @@ -2,7 +2,7 @@ import { Box, Icon, SearchInput, Skeleton } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import React, { useCallback, useState, useMemo, FC } from 'react'; -import { ISetting, SettingType } from '../../../definition/ISetting'; +import { ISetting } from '../../../definition/ISetting'; import { useSettings } from '../../contexts/SettingsContext'; import { useTranslation } from '../../contexts/TranslationContext'; import Sidebar from '../../components/basic/Sidebar'; @@ -38,7 +38,7 @@ const useSettingsGroups = (filter: string): ISetting[] => { settings .filter(filterPredicate) .map((setting) => { - if (setting.type === SettingType.GROUP) { + if (setting.type === 'group') { return setting._id; } @@ -47,7 +47,7 @@ const useSettingsGroups = (filter: string): ISetting[] => { )); return settings - .filter(({ type, group, _id }) => type === SettingType.GROUP && groupIds.includes(group || _id)) + .filter(({ type, group, _id }) => type === 'group' && groupIds.includes(group || _id)) .sort((a, b) => t(a.i18nLabel || a._id).localeCompare(t(b.i18nLabel || b._id))); }, [settings, filterPredicate, t]); }; diff --git a/client/contexts/SettingsContext.ts b/client/contexts/SettingsContext.ts index 2e4e817dca8..0f105034215 100644 --- a/client/contexts/SettingsContext.ts +++ b/client/contexts/SettingsContext.ts @@ -60,7 +60,7 @@ export const useSettings = (query?: SettingsContextQuery): ISetting[] => { export const useSettingsDispatch = (): ((changes: Partial[]) => Promise) => useContext(SettingsContext).dispatch; -export const useSettingSetValue = (_id: SettingId): ((value: T) => Promise) => { +export const useSettingSetValue = (_id: SettingId): ((value: T) => Promise) => { const dispatch = useSettingsDispatch(); return useCallback((value: T) => dispatch([{ _id, value }]), [dispatch, _id]); }; diff --git a/client/omnichannel/appearance/AppearancePage.tsx b/client/omnichannel/appearance/AppearancePage.tsx index 22dbe959cf5..1bc01150a90 100644 --- a/client/omnichannel/appearance/AppearancePage.tsx +++ b/client/omnichannel/appearance/AppearancePage.tsx @@ -86,7 +86,7 @@ const AppearancePage: FC = ({ settings }) => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const save: (settings: Pick[]) => Promise = useMethod('livechat:saveAppearance'); + const save: (settings: Pick[] & {value: unknown}[]) => Promise = useMethod('livechat:saveAppearance'); const { values, handlers, commit, reset, hasUnsavedChanges } = useForm(reduceAppearance(settings)); diff --git a/definition/IBaseData.ts b/definition/IBaseData.ts new file mode 100644 index 00000000000..16227f92a1e --- /dev/null +++ b/definition/IBaseData.ts @@ -0,0 +1,3 @@ +export interface IBaseData { + _id: string; +} diff --git a/definition/IEmoji.ts b/definition/IEmoji.ts new file mode 100644 index 00000000000..af9cb8d8fc7 --- /dev/null +++ b/definition/IEmoji.ts @@ -0,0 +1,3 @@ +export interface IEmoji { + [x: string]: any; +} diff --git a/definition/IInquiry.ts b/definition/IInquiry.ts new file mode 100644 index 00000000000..8cc531ef1f4 --- /dev/null +++ b/definition/IInquiry.ts @@ -0,0 +1,5 @@ +export interface IInquiry { + _id: string; + _updatedAt?: Date; + department?: string; +} diff --git a/definition/IInstanceStatus.ts b/definition/IInstanceStatus.ts new file mode 100644 index 00000000000..c4d6cd2f471 --- /dev/null +++ b/definition/IInstanceStatus.ts @@ -0,0 +1,6 @@ +export interface IInstanceStatus { + _id: string; + extraInformation?: { + port?: number; + }; +} diff --git a/definition/IIntegration.ts b/definition/IIntegration.ts new file mode 100644 index 00000000000..5ad55faab93 --- /dev/null +++ b/definition/IIntegration.ts @@ -0,0 +1,5 @@ +export interface IIntegration { + _id: string; + type: string; + enabled: boolean; +} diff --git a/definition/IIntegrationHistory.ts b/definition/IIntegrationHistory.ts new file mode 100644 index 00000000000..2149aff521b --- /dev/null +++ b/definition/IIntegrationHistory.ts @@ -0,0 +1,14 @@ +export interface IIntegrationHistory { + _id: string; + type: string; + step: string; + integration: { + _id: string; + }; + event: string; + _createdAt: Date; + _updatedAt: Date; + // "data" : + ranPrepareScript: boolean; + finished: boolean; +} diff --git a/definition/ILivechatDepartmentAgents.ts b/definition/ILivechatDepartmentAgents.ts new file mode 100644 index 00000000000..3c221e1325f --- /dev/null +++ b/definition/ILivechatDepartmentAgents.ts @@ -0,0 +1,7 @@ +export interface ILivechatDepartmentAgents { + _id: string; + departmentId: string; + departmentEnabled: boolean; + agentId: string; + username: string; +} diff --git a/definition/ILoginServiceConfiguration.ts b/definition/ILoginServiceConfiguration.ts new file mode 100644 index 00000000000..0d082b43a32 --- /dev/null +++ b/definition/ILoginServiceConfiguration.ts @@ -0,0 +1,6 @@ +export interface ILoginServiceConfiguration { + _id: string; + service: string; + clientId: string; + secret: string; +} diff --git a/definition/IMessage.ts b/definition/IMessage.ts index 62b798881c7..177b8d25e28 100644 --- a/definition/IMessage.ts +++ b/definition/IMessage.ts @@ -1,12 +1,18 @@ import { IRocketChatRecord } from './IRocketChatRecord'; -import { IUser, Username } from './IUser'; +import { IUser } from './IUser'; import { ChannelName, RoomID } from './IRoom'; export interface IMessage extends IRocketChatRecord { rid: RoomID; msg: string; ts: Date; - mentions?: Array; + mentions?: { + _id: string; + name?: string; + }[]; channels?: Array; u: Pick; + + _hidden?: boolean; + imported?: boolean; } diff --git a/definition/IPermission.ts b/definition/IPermission.ts new file mode 100644 index 00000000000..be119d7bace --- /dev/null +++ b/definition/IPermission.ts @@ -0,0 +1,12 @@ +export interface IPermission { + _id: string; + _updatedAt?: Date; + roles: string[]; + group?: string; + groupPermissionId?: string; + level?: 'settings'; + section?: string; + sectionPermissionId?: string; + settingId?: string; + sorter?: number; +} diff --git a/definition/IRole.ts b/definition/IRole.ts new file mode 100644 index 00000000000..04af0654b28 --- /dev/null +++ b/definition/IRole.ts @@ -0,0 +1,5 @@ +export interface IRole { + _id: string; + name: string; + _updatedAt?: Date; +} diff --git a/definition/IRoom.ts b/definition/IRoom.ts index 19fd7f47ef1..7c66c00e20a 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -21,6 +21,16 @@ export interface IRoom extends IRocketChatRecord { lm?: Date; usersCount: number; // "jitsiTimeout" : Date + + prid: string; + avatarETag?: string; + tokenpass?: { + require: string; + tokens: { + token: string; + balance: number; + }[]; + }; } export interface IDirectMessageRoom extends Omit { diff --git a/definition/IRoutingManagerConfig.ts b/definition/IRoutingManagerConfig.ts new file mode 100644 index 00000000000..70c76c499a8 --- /dev/null +++ b/definition/IRoutingManagerConfig.ts @@ -0,0 +1,9 @@ +export interface IRoutingManagerConfig { + previewRoom: boolean; + showConnecting: boolean; + showQueue: boolean; + showQueueLink: boolean; + returnQueue: boolean; + enableTriggerAction: boolean; + autoAssignAgent: boolean; +} diff --git a/definition/ISetting.ts b/definition/ISetting.ts index a89556b5f44..16cf30f1b3f 100644 --- a/definition/ISetting.ts +++ b/definition/ISetting.ts @@ -1,42 +1,51 @@ +import { FilterQuery } from 'mongodb'; + export type SettingId = string; export type GroupId = SettingId; export type SectionName = string; -export enum SettingType { - BOOLEAN = 'boolean', - STRING = 'string', - RELATIVE_URL = 'relativeUrl', - PASSWORD = 'password', - INT = 'int', - SELECT = 'select', - MULTI_SELECT = 'multiSelect', - LANGUAGE = 'language', - COLOR = 'color', - FONT = 'font', - CODE = 'code', - ACTION = 'action', - ASSET = 'asset', - ROOM_PICK = 'roomPick', - GROUP = 'group', -} - export enum SettingEditor { COLOR = 'color', EXPRESSION = 'expression' } +export type SettingValueMultiSelect = Array<{key: string; i18nLabel: string}> +export type SettingValueRoomPick = Array<{_id: string; name: string}> | string +export type SettingValue = string | boolean | number | SettingValueMultiSelect | undefined; + +export interface ISettingSelectOption { + key: string; + i18nLabel: string; +} + export interface ISetting { _id: SettingId; - type: SettingType; + type: 'boolean' | 'string' | 'relativeUrl' | 'password' | 'int' | 'select' | 'multiSelect' | 'language' | 'color' | 'font' | 'code' | 'action' | 'asset' | 'roomPick' | 'group' | 'date'; public: boolean; + env: boolean; group?: GroupId; section?: SectionName; i18nLabel: string; - value: unknown; - packageValue: unknown; + value: SettingValue; + packageValue: SettingValue; editor?: SettingEditor; packageEditor?: SettingEditor; blocked: boolean; - enableQuery?: string | Mongo.ObjectID | Mongo.Query | Mongo.QueryWithModifiers; + enableQuery?: string | FilterQuery | FilterQuery[]; sorter?: number; + properties?: unknown; + enterprise?: boolean; + requiredOnWizard?: boolean; + hidden?: boolean; + modules?: Array; + invalidValue?: SettingValue; + valueSource?: string; + secret?: boolean; + i18nDescription?: string; + autocomplete?: boolean; + processEnvValue?: SettingValue; + meteorSettingsValue?: SettingValue; + ts?: Date; + multiline?: boolean; + values?: Array; } diff --git a/definition/ISubscription.ts b/definition/ISubscription.ts index 1d63becc7fb..268475735fa 100644 --- a/definition/ISubscription.ts +++ b/definition/ISubscription.ts @@ -13,7 +13,7 @@ export interface ISubscription extends IRocketChatRecord { name: string; - alert?: true; + alert?: boolean; unread: number; t: RoomType; ls: Date; @@ -29,6 +29,8 @@ export interface ISubscription extends IRocketChatRecord { tunreadUser: Array; prid?: RoomID; + + roles?: string[]; } export interface ISubscriptionDirectMessage extends Omit { diff --git a/definition/IUser.ts b/definition/IUser.ts index 89d7090348d..92dd97bd9dd 100644 --- a/definition/IUser.ts +++ b/definition/IUser.ts @@ -1,3 +1,5 @@ +import { USER_STATUS } from './UserStatus'; + export interface ILoginToken { hashedToken: string; twoFactorAuthorizedUntil?: Date; @@ -27,7 +29,7 @@ export interface IUserEmailCode { expire: Date; } -type LoginToken = ILoginToken & IPersonalAccessToken; +type LoginToken = IMeteorLoginToken & IPersonalAccessToken; export type Username = string; export interface IUserServices { @@ -85,7 +87,6 @@ export interface IRole { export interface IUser { _id: string; - avatarETag: string; createdAt: Date; roles: string[]; type: string; @@ -98,9 +99,11 @@ export interface IUser { statusConnection?: string; lastLogin?: Date; avatarOrigin?: string; + avatarETag?: string; utcOffset?: number; language?: string; - statusDefault?: string; + statusDefault?: USER_STATUS; + statusText?: string; oauth?: { authorizedClients: string[]; }; diff --git a/definition/IUserSession.ts b/definition/IUserSession.ts new file mode 100644 index 00000000000..4d98e73a8e7 --- /dev/null +++ b/definition/IUserSession.ts @@ -0,0 +1,14 @@ +import { USER_STATUS } from './UserStatus'; + +export interface IUserSessionConnection { + id: string; + instanceId: string; + status: USER_STATUS; + _createdAt: Date; + _updatedAt: Date; +} + +export interface IUserSession { + _id: string; + connections: IUserSessionConnection[]; +} diff --git a/definition/IUserStatus.ts b/definition/IUserStatus.ts new file mode 100644 index 00000000000..26b3610bde8 --- /dev/null +++ b/definition/IUserStatus.ts @@ -0,0 +1,3 @@ +export interface IUserStatus { + [x: string]: any; +} diff --git a/definition/UserStatus.ts b/definition/UserStatus.ts new file mode 100644 index 00000000000..115d0b3d0c0 --- /dev/null +++ b/definition/UserStatus.ts @@ -0,0 +1,6 @@ +export enum USER_STATUS { + ONLINE = 'online', + AWAY = 'away', + OFFLINE = 'offline', + BUSY = 'busy', +} diff --git a/ee/app/canned-responses/server/hooks/onRemoveAgentDepartment.ts b/ee/app/canned-responses/server/hooks/onRemoveAgentDepartment.ts index e08e23a115c..a56acd98254 100644 --- a/ee/app/canned-responses/server/hooks/onRemoveAgentDepartment.ts +++ b/ee/app/canned-responses/server/hooks/onRemoveAgentDepartment.ts @@ -1,12 +1,12 @@ import { callbacks } from '../../../../../app/callbacks/server'; import CannedResponse from '../../../models/server/models/CannedResponse'; -import { cannedResponsesStreamer } from '../streamer'; +import notifications from '../../../../../app/notifications/server/lib/Notifications'; callbacks.add('livechat.removeAgentDepartment', async (options: Record): Promise => { const { departmentId, agentsId } = options; CannedResponse.findByDepartmentId(departmentId, { fields: { _id: 1 } }).forEach((response: any) => { const { _id } = response; - cannedResponsesStreamer.emit('canned-responses', { type: 'removed', _id }, { agentsId }); + notifications.streamCannedResponses.emit('canned-responses', { type: 'removed', _id }, { agentsId }); }); return options; diff --git a/ee/app/canned-responses/server/hooks/onSaveAgentDepartment.ts b/ee/app/canned-responses/server/hooks/onSaveAgentDepartment.ts index d74f80f582c..e301b12c954 100644 --- a/ee/app/canned-responses/server/hooks/onSaveAgentDepartment.ts +++ b/ee/app/canned-responses/server/hooks/onSaveAgentDepartment.ts @@ -1,11 +1,11 @@ import { callbacks } from '../../../../../app/callbacks/server'; import CannedResponse from '../../../models/server/models/CannedResponse'; -import { cannedResponsesStreamer } from '../streamer'; +import notifications from '../../../../../app/notifications/server/lib/Notifications'; callbacks.add('livechat.saveAgentDepartment', async (options: Record): Promise => { const { departmentId, agentsId } = options; CannedResponse.findByDepartmentId(departmentId, {}).forEach((response: any) => { - cannedResponsesStreamer.emit('canned-responses', { type: 'changed', ...response }, { agentsId }); + notifications.streamCannedResponses.emit('canned-responses', { type: 'changed', ...response }, { agentsId }); }); return options; diff --git a/ee/app/canned-responses/server/methods/removeCannedResponse.js b/ee/app/canned-responses/server/methods/removeCannedResponse.js index 6fbab487a1f..c7c4329fcfc 100644 --- a/ee/app/canned-responses/server/methods/removeCannedResponse.js +++ b/ee/app/canned-responses/server/methods/removeCannedResponse.js @@ -3,7 +3,7 @@ import { check } from 'meteor/check'; import { hasPermission } from '../../../../../app/authorization'; import CannedResponse from '../../../models/server/models/CannedResponse'; -import { cannedResponsesStreamer } from '../streamer'; +import notifications from '../../../../../app/notifications/server/lib/Notifications'; Meteor.methods({ 'removeCannedResponse'(_id) { @@ -18,7 +18,7 @@ Meteor.methods({ throw new Meteor.Error('error-canned-response-not-found', 'Canned Response not found', { method: 'removeCannedResponse' }); } - cannedResponsesStreamer.emit('canned-responses', { type: 'removed', _id }); + notifications.streamCannedResponses.emit('canned-responses', { type: 'removed', _id }); return CannedResponse.removeById(_id); }, diff --git a/ee/app/canned-responses/server/methods/saveCannedResponse.js b/ee/app/canned-responses/server/methods/saveCannedResponse.js index 6d04c467bb7..cca82b4eba9 100644 --- a/ee/app/canned-responses/server/methods/saveCannedResponse.js +++ b/ee/app/canned-responses/server/methods/saveCannedResponse.js @@ -4,7 +4,7 @@ import { Match, check } from 'meteor/check'; import { hasPermission } from '../../../../../app/authorization'; import CannedResponse from '../../../models/server/models/CannedResponse'; import { Users } from '../../../../../app/models'; -import { cannedResponsesStreamer } from '../streamer'; +import notifications from '../../../../../app/notifications/server/lib/Notifications'; Meteor.methods({ saveCannedResponse(_id, responseData) { @@ -42,7 +42,7 @@ Meteor.methods({ responseData.createdBy = user.username; } const createdCannedResponse = CannedResponse.createOrUpdateCannedResponse(_id, responseData); - cannedResponsesStreamer.emit('canned-responses', { type: 'changed', ...createdCannedResponse }); + notifications.streamCannedResponses.emit('canned-responses', { type: 'changed', ...createdCannedResponse }); return createdCannedResponse; }, diff --git a/ee/app/canned-responses/server/streamer.js b/ee/app/canned-responses/server/streamer.js deleted file mode 100644 index ee34be29ed5..00000000000 --- a/ee/app/canned-responses/server/streamer.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../../app/authorization'; -import { settings } from '../../../../app/settings'; - -export const cannedResponsesStreamer = new Meteor.Streamer('canned-responses'); - -cannedResponsesStreamer.allowWrite('none'); -cannedResponsesStreamer.allowRead(function() { - return this.userId && settings.get('Canned_Responses_Enable') && hasPermission(this.userId, 'view-canned-responses'); -}); diff --git a/ee/app/license/server/bundles.ts b/ee/app/license/server/bundles.ts index 84b55a6f31b..67ea0be970a 100644 --- a/ee/app/license/server/bundles.ts +++ b/ee/app/license/server/bundles.ts @@ -11,6 +11,7 @@ const bundles: IBundle = { 'omnichannel-mobile-enterprise', 'engagement-dashboard', 'push-privacy', + 'scalability', ], pro: [ ], diff --git a/ee/app/license/server/index.ts b/ee/app/license/server/index.ts index 83763620a84..3de56316694 100644 --- a/ee/app/license/server/index.ts +++ b/ee/app/license/server/index.ts @@ -1,3 +1,4 @@ +import './license.internalService'; import './settings'; import './methods'; import './startup'; diff --git a/ee/app/license/server/license.internalService.ts b/ee/app/license/server/license.internalService.ts new file mode 100644 index 00000000000..f03d771aa51 --- /dev/null +++ b/ee/app/license/server/license.internalService.ts @@ -0,0 +1,51 @@ +import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; +import { api } from '../../../../server/sdk/api'; +import { ILicense } from '../../../../server/sdk/types/ILicense'; +import { hasLicense, isEnterprise, getModules, onValidateLicenses, onModule } from './license'; +import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions'; +import { Authorization } from '../../../../server/sdk'; +import { guestPermissions } from '../../authorization/lib/guestPermissions'; + +class LicenseService extends ServiceClass implements ILicense { + protected name = 'license'; + + constructor() { + super(); + + onValidateLicenses((): void => { + if (!isEnterprise()) { + return; + } + + Authorization.addRoleRestrictions('guest', guestPermissions); + resetEnterprisePermissions(); + }); + + onModule((licenseModule) => { + api.broadcast('license.module', licenseModule); + }); + } + + async started(): Promise { + if (!isEnterprise()) { + return; + } + + Authorization.addRoleRestrictions('guest', guestPermissions); + resetEnterprisePermissions(); + } + + hasLicense(feature: string): boolean { + return hasLicense(feature); + } + + isEnterprise(): boolean { + return isEnterprise(); + } + + getModules(): string[] { + return getModules(); + } +} + +api.registerService(new LicenseService()); diff --git a/ee/app/license/server/license.ts b/ee/app/license/server/license.ts index 09027ae2fe9..b092cb137e8 100644 --- a/ee/app/license/server/license.ts +++ b/ee/app/license/server/license.ts @@ -1,8 +1,6 @@ import { EventEmitter } from 'events'; import { Users } from '../../../../app/models/server'; -import { addRoleRestrictions } from '../../authorization/lib/addRoleRestrictions'; -import { resetEnterprisePermissions } from '../../authorization/server/resetEnterprisePermissions'; import { getBundleModules, isBundle, getBundleFromModule } from './bundles'; import decrypt from './decrypt'; import { getTagColor } from './getTagColor'; @@ -30,7 +28,6 @@ export interface IValidLicense { } let maxGuestUsers = 0; -let addedRoleRestrictions = false; class LicenseClass { private url: string|null = null; @@ -62,6 +59,7 @@ class LicenseClass { modules.forEach((module) => { this.modules.add(module); + EnterpriseLicenses.emit('module', { module, valid: true }); EnterpriseLicenses.emit(`valid:${ module }`); }); }); @@ -73,7 +71,10 @@ class LicenseClass { ? getBundleModules(licenseModule) : [licenseModule]; - modules.forEach((module) => EnterpriseLicenses.emit(`invalid:${ module }`)); + modules.forEach((module) => { + EnterpriseLicenses.emit('module', { module, valid: false }); + EnterpriseLicenses.emit(`invalid:${ module }`); + }); }); } @@ -112,12 +113,6 @@ class LicenseClass { }); this.validate(); - - if (!addedRoleRestrictions && this.hasAnyValidLicense()) { - addRoleRestrictions(); - resetEnterprisePermissions(); - addedRoleRestrictions = true; - } } hasModule(module: string): boolean { @@ -272,6 +267,10 @@ export function onLicense(feature: string, cb: (...args: any[]) => void): void { EnterpriseLicenses.once(`valid:${ feature }`, cb); } +export function onModule(cb: (...args: any[]) => void): void { + EnterpriseLicenses.on('module', cb); +} + export function onValidateLicenses(cb: (...args: any[]) => void): void { EnterpriseLicenses.on('validate', cb); } diff --git a/ee/app/license/server/startup.js b/ee/app/license/server/startup.js index 8c9c23cf09e..af6384fc715 100644 --- a/ee/app/license/server/startup.js +++ b/ee/app/license/server/startup.js @@ -1,8 +1,6 @@ import { settings } from '../../../../app/settings/server'; import { callbacks } from '../../../../app/callbacks'; import { addLicense, setURL } from './license'; -import './settings'; -import './methods'; settings.get('Site_Url', (key, value) => { if (value) { diff --git a/ee/app/livechat-enterprise/server/business-hour/Custom.ts b/ee/app/livechat-enterprise/server/business-hour/Custom.ts index 9dd3fe08a68..1bea445a566 100644 --- a/ee/app/livechat-enterprise/server/business-hour/Custom.ts +++ b/ee/app/livechat-enterprise/server/business-hour/Custom.ts @@ -4,9 +4,9 @@ import { } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour'; import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../../definition/ILivechatBusinessHour'; import { LivechatDepartmentRaw } from '../../../../../app/models/server/raw/LivechatDepartment'; -import { LivechatDepartment } from '../../../../../app/models/server/raw'; +import { LivechatDepartmentAgentsRaw } from '../../../../../app/models/server/raw/LivechatDepartmentAgents'; +import { LivechatDepartment, LivechatDepartmentAgents } from '../../../../../app/models/server/raw'; import { businessHourManager } from '../../../../../app/livechat/server/business-hour'; -import LivechatDepartmentAgents, { LivechatDepartmentAgentsRaw } from '../../../models/server/raw/LivechatDepartmentAgents'; export interface IBusinessHoursExtraProperties extends ILivechatBusinessHour { timezoneName: string; @@ -25,7 +25,7 @@ class CustomBusinessHour extends AbstractBusinessHourType implements IBusinessHo return; } - const businessHour: ILivechatBusinessHour = await this.BusinessHourRepository.findOneById(id); + const businessHour = await this.BusinessHourRepository.findOneById(id); if (!businessHour) { return; } @@ -62,13 +62,13 @@ class CustomBusinessHour extends AbstractBusinessHourType implements IBusinessHo await this.BusinessHourRepository.removeById(businessHourId); await this.removeBusinessHourFromAgents(businessHourId); await this.DepartmentsRepository.removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId); - return this.UsersRepository.updateLivechatStatusBasedOnBusinessHours(); + this.UsersRepository.updateLivechatStatusBasedOnBusinessHours(); } private async removeBusinessHourFromAgents(businessHourId: string): Promise { const departmentIds = (await this.DepartmentsRepository.findByBusinessHourId(businessHourId, { fields: { _id: 1 } }).toArray()).map((dept: any) => dept._id); const agentIds = (await this.DepartmentsAgentsRepository.findByDepartmentIds(departmentIds, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId); - return this.UsersRepository.removeBusinessHourByAgentIds(agentIds, businessHourId); + this.UsersRepository.removeBusinessHourByAgentIds(agentIds, businessHourId); } private async removeBusinessHourFromDepartmentsIfNeeded(businessHourId: string, departmentsToRemove: string[]): Promise { diff --git a/ee/app/livechat-enterprise/server/business-hour/Helper.ts b/ee/app/livechat-enterprise/server/business-hour/Helper.ts index 3fe0b4e7603..47dc2a5be80 100644 --- a/ee/app/livechat-enterprise/server/business-hour/Helper.ts +++ b/ee/app/livechat-enterprise/server/business-hour/Helper.ts @@ -28,13 +28,13 @@ const getAgentIdsToHandle = async (businessHour: Record): Promise): Promise => { const agentIds: string[] = await getAgentIdsToHandle(businessHour); await Users.addBusinessHourByAgentIds(agentIds, businessHour._id); - return Users.updateLivechatStatusBasedOnBusinessHours(); + Users.updateLivechatStatusBasedOnBusinessHours(); }; export const closeBusinessHour = async (businessHour: Record): Promise => { const agentIds: string[] = await getAgentIdsToHandle(businessHour); await Users.removeBusinessHourByAgentIds(agentIds, businessHour._id); - return Users.updateLivechatStatusBasedOnBusinessHours(); + Users.updateLivechatStatusBasedOnBusinessHours(); }; export const removeBusinessHourByAgentIds = async (agentIds: string[], businessHourId: string): Promise => { @@ -42,7 +42,7 @@ export const removeBusinessHourByAgentIds = async (agentIds: string[], businessH return; } await Users.removeBusinessHourByAgentIds(agentIds, businessHourId); - return Users.updateLivechatStatusBasedOnBusinessHours(); + Users.updateLivechatStatusBasedOnBusinessHours(); }; export const resetDefaultBusinessHourIfNeeded = async (): Promise => { @@ -54,6 +54,9 @@ export const resetDefaultBusinessHourIfNeeded = async (): Promise => { return; } const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour({ fields: { _id: 1 } }); + if (!defaultBusinessHour) { + return; + } LivechatBusinessHours.update({ _id: defaultBusinessHour._id }, { $set: { timezone: { diff --git a/ee/app/livechat-enterprise/server/business-hour/Multiple.ts b/ee/app/livechat-enterprise/server/business-hour/Multiple.ts index 1e030e1b043..35b11777124 100644 --- a/ee/app/livechat-enterprise/server/business-hour/Multiple.ts +++ b/ee/app/livechat-enterprise/server/business-hour/Multiple.ts @@ -5,10 +5,9 @@ import { IBusinessHourBehavior, } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour'; import { ILivechatBusinessHour } from '../../../../../definition/ILivechatBusinessHour'; -import { LivechatDepartment } from '../../../../../app/models/server'; -import { LivechatDepartment as Raw } from '../../../../../app/models/server/raw'; import { LivechatDepartmentRaw } from '../../../../../app/models/server/raw/LivechatDepartment'; -import LivechatDepartmentAgents, { LivechatDepartmentAgentsRaw } from '../../../models/server/raw/LivechatDepartmentAgents'; +import { LivechatDepartmentAgentsRaw } from '../../../models/server/raw/LivechatDepartmentAgents'; +import { LivechatDepartment, LivechatDepartmentAgents } from '../../../../../app/models/server/raw'; import { filterBusinessHoursThatMustBeOpened } from '../../../../../app/livechat/server/business-hour/Helper'; import { closeBusinessHour, openBusinessHour, removeBusinessHourByAgentIds } from './Helper'; @@ -18,9 +17,9 @@ interface IBusinessHoursExtraProperties extends ILivechatBusinessHour { } export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { - private DepartmentsRepository: LivechatDepartmentRaw = Raw; + private DepartmentsRepository: LivechatDepartmentRaw = LivechatDepartment; - private DepartmentsAgentsRepository: LivechatDepartmentAgentsRaw = LivechatDepartmentAgents; + private DepartmentsAgentsRepository = LivechatDepartmentAgents as LivechatDepartmentAgentsRaw; constructor() { super(); @@ -78,6 +77,9 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior const toRemove = [...(currentDepartments || []).filter((dept: Record) => !departments.includes(dept._id))]; await this.removeBusinessHourFromRemovedDepartmentsUsersIfNeeded(businessHourData._id, toRemove); const businessHour = await this.BusinessHourRepository.findOneById(businessHourData._id); + if (!businessHour) { + return; + } const businessHourIdToOpen = (await filterBusinessHoursThatMustBeOpened([businessHour])).map((businessHour) => businessHour._id); if (!businessHourIdToOpen.length) { return closeBusinessHour(businessHour); @@ -92,6 +94,9 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior return options; } const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour(); + if (!defaultBusinessHour) { + return options; + } await removeBusinessHourByAgentIds(agentsId, defaultBusinessHour._id); if (!department.businessHourId) { return options; @@ -144,6 +149,9 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior return options; } const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour(); + if (!defaultBusinessHour) { + return options; + } const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); if (!businessHourToOpen.length) { return options; diff --git a/ee/app/livechat-enterprise/server/lib/Helper.js b/ee/app/livechat-enterprise/server/lib/Helper.js index b6b0e6a7b81..3a843835bce 100644 --- a/ee/app/livechat-enterprise/server/lib/Helper.js +++ b/ee/app/livechat-enterprise/server/lib/Helper.js @@ -13,9 +13,9 @@ import { } from '../../../../../app/models/server'; import { Rooms as RoomRaw } from '../../../../../app/models/server/raw'; import { settings } from '../../../../../app/settings'; -import { Livechat } from '../../../../../app/livechat/server/lib/Livechat'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper'; +import notifications from '../../../../../app/notifications/server/lib/Notifications'; export const getMaxNumberSimultaneousChat = ({ agentId, departmentId }) => { if (agentId) { @@ -82,7 +82,7 @@ export const dispatchInquiryPosition = async (inquiry, queueInfo) => { const { position, department } = inquiry; const data = await normalizeQueueInfo({ position, queueInfo, department }); const propagateInquiryPosition = Meteor.bindEnvironment((inquiry) => { - Livechat.stream.emit(inquiry.rid, { + notifications.streamLivechatRoom.emit(inquiry.rid, { type: 'queueData', data, }); diff --git a/ee/app/models/server/raw/LivechatDepartmentAgents.ts b/ee/app/models/server/raw/LivechatDepartmentAgents.ts index 51f4c343056..c64046c4f4b 100644 --- a/ee/app/models/server/raw/LivechatDepartmentAgents.ts +++ b/ee/app/models/server/raw/LivechatDepartmentAgents.ts @@ -1,5 +1,4 @@ import { LivechatDepartmentAgentsRaw as Raw } from '../../../../../app/models/server/raw/LivechatDepartmentAgents'; -import { LivechatDepartmentAgents } from '../../../../../app/models/server'; export class LivechatDepartmentAgentsRaw extends Raw { findAgentsByAgentIdAndBusinessHourId(agentId: string, businessHourId: string): Promise> { @@ -25,5 +24,3 @@ export class LivechatDepartmentAgentsRaw extends Raw { return this.col.aggregate([match, lookup, unwind, withBusinessHourId, project]).toArray(); } } - -export default new LivechatDepartmentAgentsRaw(LivechatDepartmentAgents.model.rawCollection()); diff --git a/ee/app/settings/server/index.js b/ee/app/settings/server/index.js index 97097791afd..d3510e7884f 100644 --- a/ee/app/settings/server/index.js +++ b/ee/app/settings/server/index.js @@ -1 +1,2 @@ import './settings'; +import './settings.internalService'; diff --git a/ee/app/settings/server/settings.internalService.ts b/ee/app/settings/server/settings.internalService.ts new file mode 100644 index 00000000000..8dbe604a13a --- /dev/null +++ b/ee/app/settings/server/settings.internalService.ts @@ -0,0 +1,15 @@ +import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; +import { api } from '../../../../server/sdk/api'; +import { IEnterpriseSettings } from '../../../../server/sdk/types/IEnterpriseSettings'; +import { changeSettingValue } from './settings'; +import { ISetting } from '../../../../definition/ISetting'; + +class EnterpriseSettings extends ServiceClass implements IEnterpriseSettings { + protected name = 'ee-settings'; + + changeSettingValue(record: ISetting): undefined | { value: ISetting['value'] } { + return changeSettingValue(record); + } +} + +api.registerService(new EnterpriseSettings()); diff --git a/ee/app/settings/server/settings.ts b/ee/app/settings/server/settings.ts index 560d057c5f3..0f1a63ddb94 100644 --- a/ee/app/settings/server/settings.ts +++ b/ee/app/settings/server/settings.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import { SettingsEvents, settings, ISettingRecord } from '../../../../app/settings/server/functions/settings'; -import { SettingValue } from '../../../../app/settings/lib/settings'; +import { SettingsEvents, settings } from '../../../../app/settings/server/functions/settings'; import { isEnterprise, hasLicense, onValidateLicenses } from '../../license/server/license'; import SettingsModel from '../../../../app/models/server/models/Settings'; +import { ISetting, SettingValue } from '../../../../definition/ISetting'; -function changeSettingValue(record: ISettingRecord): undefined | { value: SettingValue } { +export function changeSettingValue(record: ISetting): undefined | { value: SettingValue } { if (!record.enterprise) { return; } @@ -25,14 +25,14 @@ function changeSettingValue(record: ISettingRecord): undefined | { value: Settin } } -SettingsEvents.on('store-setting-value', (record: ISettingRecord, newRecord: { value: SettingValue }) => { +SettingsEvents.on('store-setting-value', (record: ISetting, newRecord: { value: SettingValue }) => { const changedValue = changeSettingValue(record); if (changedValue) { newRecord.value = changedValue.value; } }); -SettingsEvents.on('fetch-settings', (settings: Array): void => { +SettingsEvents.on('fetch-settings', (settings: Array): void => { for (const setting of settings) { const changedValue = changeSettingValue(setting); if (changedValue) { @@ -41,25 +41,10 @@ SettingsEvents.on('fetch-settings', (settings: Array): void => { } }); -type ISettingNotificationValue = { - _id: string; - value: SettingValue; - editor: string; - properties: string; - enterprise: boolean; -}; - -SettingsEvents.on('change-setting', (record: ISettingRecord, value: ISettingNotificationValue): void => { - const changedValue = changeSettingValue(record); - if (changedValue) { - value.value = changedValue.value; - } -}); - function updateSettings(): void { const enterpriseSettings = SettingsModel.findEnterpriseSettings(); - enterpriseSettings.forEach((record: ISettingRecord) => settings.storeSettingValue(record, false)); + enterpriseSettings.forEach((record: ISetting) => settings.storeSettingValue(record, false)); } diff --git a/ee/server/broker.ts b/ee/server/broker.ts new file mode 100644 index 00000000000..3246768f4ff --- /dev/null +++ b/ee/server/broker.ts @@ -0,0 +1,299 @@ +import { ServiceBroker, Context, ServiceSchema, Serializers } from 'moleculer'; +import EJSON from 'ejson'; + +import { asyncLocalStorage, License } from '../../server/sdk'; +import { api } from '../../server/sdk/api'; +import { IBroker, IBrokerNode } from '../../server/sdk/types/IBroker'; +import { ServiceClass } from '../../server/sdk/types/ServiceClass'; +import { EventSignatures } from '../../server/sdk/lib/Events'; +import { LocalBroker } from '../../server/sdk/lib/LocalBroker'; + +const events: {[k: string]: string} = { + onNodeConnected: '$node.connected', + onNodeUpdated: '$node.updated', + onNodeDisconnected: '$node.disconnected', +}; + +const lifecycle: {[k: string]: string} = { + created: 'created', + started: 'started', + stopped: 'stopped', +}; + +class NetworkBroker implements IBroker { + private broker: ServiceBroker; + + private localBroker = new LocalBroker(); + + private allowed: Promise; + + private started: Promise; + + private whitelist = { + events: ['license.module'], + actions: ['license.hasLicense'], + } + + constructor(broker: ServiceBroker) { + this.broker = broker; + + api.setBroker(this); + + this.started = this.broker.start(); + + this.allowed = License.hasLicense('scalability'); + } + + async call(method: string, data: any): Promise { + await this.started; + + if (!(this.whitelist.actions.includes(method) || await this.allowed)) { + return this.localBroker.call(method, data); + } + + const context = asyncLocalStorage.getStore(); + if (context?.ctx?.call) { + return context.ctx.call(method, data); + } + + const services: {name: string}[] = await this.broker.call('$node.services', { onlyAvailable: true }); + if (!services.find((service) => service.name === method.split('.')[0])) { + return new Error('method-not-available'); + } + return this.broker.call(method, data); + } + + async waitAndCall(method: string, data: any): Promise { + await this.started; + + if (!(this.whitelist.actions.includes(method) || await this.allowed)) { + return this.localBroker.call(method, data); + } + + await this.broker.waitForServices(method.split('.')[0]); + + const context = asyncLocalStorage.getStore(); + if (context?.ctx?.call) { + return context.ctx.call(method, data); + } + + return this.broker.call(method, data); + } + + destroyService(instance: ServiceClass): void { + this.localBroker.destroyService(instance); + + this.broker.destroyService(instance.getName()); + } + + createService(instance: ServiceClass): void { + this.localBroker.createService(instance); + + const name = instance.getName(); + + // Listen for module license + instance.onEvent('license.module', async ({ module, valid }) => { + if (module === 'scalability') { + // Should we believe on the event only? Could it be a call from the CE version? + this.allowed = valid ? License.hasLicense('scalability') : Promise.resolve(false); + // console.log('on license.module', { allowed: this.allowed }); + } + }); + + const service: ServiceSchema = { + name, + actions: {}, + events: instance.getEvents().reduce>((map, eventName) => { + if (this.whitelist.events.includes(eventName)) { + map[eventName] = (data: Parameters): void => instance.emit(eventName, ...data); + return map; + } + + map[eventName] = (data: Parameters): any => { + if (this.allowed) { + return instance.emit(eventName, ...data); + } + }; + + return map; + }, {}), + }; + + if (!service.events || !service.actions) { + return; + } + + const methods = instance.constructor?.name === 'Object' ? Object.getOwnPropertyNames(instance) : Object.getOwnPropertyNames(Object.getPrototypeOf(instance)); + for (const method of methods) { + if (method === 'constructor') { + continue; + } + + const i = instance as any; + + if (method.match(/^on[A-Z]/)) { + service.events[events[method]] = i[method].bind(i); + continue; + } + + if (lifecycle[method]) { + service[method] = (): void => asyncLocalStorage.run({ + id: '', + nodeID: this.broker.nodeID, + requestID: null, + broker: this, + }, i[method].bind(i)); + continue; + } + + service.actions[method] = async (ctx: Context<[]>): Promise => asyncLocalStorage.run({ + id: ctx.id, + nodeID: ctx.nodeID, + requestID: ctx.requestID, + broker: this, + ctx, + }, async (): Promise => { + if (this.whitelist.actions.includes(`${ name }.${ method }`) || await this.allowed) { + return i[method](...ctx.params); + } + }); + } + + this.broker.createService(service); + } + + async broadcast(event: T, ...args: Parameters): Promise { + if (!(this.whitelist.events.includes(event) || await this.allowed)) { + return this.localBroker.broadcast(event, ...args); + } + return this.broker.broadcast(event, args); + } + + async nodeList(): Promise { + return this.broker.call('$node.list'); + } +} + +const Base = Serializers.Base as unknown as new () => {}; + +class EJSONSerializer extends Base { + serialize(obj: {}): Buffer { + return Buffer.from(EJSON.stringify(obj)); + } + + deserialize(buf: Buffer): any { + return EJSON.parse(buf.toString()); + } +} + +const { + TRANSPORTER = 'TCP', + CACHE = 'Memory', + // SERIALIZER = 'MsgPack', + SERIALIZER = 'EJSON', + MOLECULER_LOG_LEVEL = 'error', + BALANCE_STRATEGY = 'RoundRobin', + BALANCE_PREFER_LOCAL = 'false', + RETRY_FACTOR = '2', + RETRY_MAX_DELAY = '1000', + RETRY_DELAY = '100', + RETRY_RETRIES = '5', + RETRY_ENABLED = 'false', + REQUEST_TIMEOUT = '10', + HEARTBEAT_INTERVAL = '10', + HEARTBEAT_TIMEOUT = '30', + BULKHEAD_ENABLED = 'false', + BULKHEAD_CONCURRENCY = '10', + BULKHEAD_MAX_QUEUE_SIZE = '10000', + MS_METRICS = 'false', + MS_METRICS_PORT = '9458', + TRACING_ENABLED = 'false', +} = process.env; + +const network = new ServiceBroker({ + // TODO: Reevaluate, without this setting it was preventing the process to stop + skipProcessEventRegistration: true, + transporter: TRANSPORTER, + metrics: { + enabled: MS_METRICS === 'true', + reporter: [{ + type: 'Prometheus', + options: { + port: MS_METRICS_PORT, + }, + }], + }, + cacher: CACHE, + serializer: SERIALIZER === 'EJSON' ? new EJSONSerializer() : SERIALIZER, + logLevel: MOLECULER_LOG_LEVEL as any, + // logLevel: { + // // "TRACING": "trace", + // // "TRANS*": "warn", + // BROKER: 'debug', + // TRANSIT: 'debug', + // '**': 'info', + // }, + logger: { + type: 'Console', + options: { + formatter: 'short', + }, + }, + registry: { + strategy: BALANCE_STRATEGY, + preferLocal: BALANCE_PREFER_LOCAL !== 'false', + }, + + requestTimeout: parseInt(REQUEST_TIMEOUT) * 1000, + retryPolicy: { + enabled: RETRY_ENABLED === 'true', + retries: parseInt(RETRY_RETRIES), + delay: parseInt(RETRY_DELAY), + maxDelay: parseInt(RETRY_MAX_DELAY), + factor: parseInt(RETRY_FACTOR), + check: (err: any): boolean => err && !!err.retryable, + }, + + maxCallLevel: 100, + heartbeatInterval: parseInt(HEARTBEAT_INTERVAL), + heartbeatTimeout: parseInt(HEARTBEAT_TIMEOUT), + + // circuitBreaker: { + // enabled: false, + // threshold: 0.5, + // windowTime: 60, + // minRequestCount: 20, + // halfOpenTime: 10 * 1000, + // check: (err: any): boolean => err && err.code >= 500, + // }, + + bulkhead: { + enabled: BULKHEAD_ENABLED === 'true', + concurrency: parseInt(BULKHEAD_CONCURRENCY), + maxQueueSize: parseInt(BULKHEAD_MAX_QUEUE_SIZE), + }, + + tracing: { + enabled: TRACING_ENABLED === 'true', + exporter: { + type: 'Jaeger', + options: { + endpoint: null, + host: 'jaeger', + port: 6832, + sampler: { + // Sampler type. More info: https://www.jaegertracing.io/docs/1.14/sampling/#client-sampling-configuration + type: 'Const', + // Sampler specific options. + options: {}, + }, + // Additional options for `Jaeger.Tracer` + tracerOptions: {}, + // Default tags. They will be added into all span tags. + defaultTags: null, + }, + }, + }, +}); + +new NetworkBroker(network); diff --git a/ee/server/index.js b/ee/server/index.js index b7aa74f4c07..1c61f8ca5d8 100644 --- a/ee/server/index.js +++ b/ee/server/index.js @@ -1,4 +1,7 @@ +import './broker'; + import '../app/models'; +import '../app/license/server/index'; import '../app/api-enterprise/server/index'; import '../app/auditing/server/index'; import '../app/authorization/server/index'; diff --git a/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/docker_rev2.json b/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/docker_rev2.json new file mode 100644 index 00000000000..40d89fc5726 --- /dev/null +++ b/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/docker_rev2.json @@ -0,0 +1,1007 @@ +{ + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "6.4.3" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "singlestat", + "name": "Singlestat", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "cadvisor:v0.32.0", + "editable": true, + "gnetId": 11277, + "graphTooltip": 1, + "id": null, + "iteration": 1586447178129, + "links": [ + { + "icon": "external link", + "tags": [], + "targetBlank": true, + "title": "http://$instance/metrics", + "tooltip": "", + "type": "link", + "url": "http://$instance/metrics" + } + ], + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 0, + "y": 0 + }, + "height": "20", + "id": 7, + "interval": null, + "isNew": true, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "count(container_last_seen{project=~\"$project\",instance=~\"$instance\",image!=\"\"})", + "intervalFactor": 2, + "legendFormat": "", + "metric": "container_last_seen", + "refId": "A", + "step": 240 + } + ], + "thresholds": "", + "title": "Running containers", + "transparent": true, + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "editable": true, + "error": false, + "format": "mbytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 8, + "y": 0 + }, + "height": "20", + "id": 5, + "interval": null, + "isNew": true, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(container_memory_usage_bytes{project=~\"$project\",instance=~\"$instance\",image!=\"\"})/1024/1024", + "intervalFactor": 2, + "legendFormat": "", + "metric": "container_memory_usage_bytes", + "refId": "A", + "step": 240 + } + ], + "thresholds": "", + "title": "Total Memory Usage", + "transparent": true, + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 16, + "y": 0 + }, + "height": "20", + "id": 6, + "interval": null, + "isNew": true, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(rate(container_cpu_user_seconds_total{project=~\"$project\",instance=~\"$instance\",image!=\"\"}[5m]) * 100)", + "intervalFactor": 2, + "legendFormat": "", + "metric": "container_memory_usage_bytes", + "refId": "A", + "step": 240 + } + ], + "thresholds": "", + "title": "Total CPU Usage", + "transparent": true, + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 2, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(container_cpu_user_seconds_total{project=~\"$project\",instance=~\"$instance\",image!=\"\"}[5m]) * 100", + "intervalFactor": 2, + "legendFormat": "{{name}}", + "metric": "cpu", + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "CPU Usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 1, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "container_memory_usage_bytes{project=~\"$project\",instance=~\"$instance\",image!=\"\"}", + "hide": false, + "intervalFactor": 2, + "legendFormat": "{{name}}", + "metric": "container_memory_usage_bytes", + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Memory Usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 3, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "irate(container_network_receive_bytes_total{project=~\"$project\",instance=~\"$instance\",image!=\"\"}[5m])", + "intervalFactor": 2, + "legendFormat": "{{name}}", + "metric": "container_network_receive_bytes_total", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Network Rx", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 17 + }, + "id": 4, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "irate(container_network_transmit_bytes_total{project=~\"$project\",instance=~\"$instance\",image!=\"\"}[5m])", + "intervalFactor": 2, + "legendFormat": "{{name}}", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Network Tx", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 8, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "irate(container_fs_reads_bytes_total{project=~\"$project\",instance=~\"$instance\",image!=\"\"}[5m])", + "intervalFactor": 2, + "legendFormat": "{{name}}", + "metric": "container_fs_reads_bytes_total", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "I/O Rx", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 9, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "irate(container_fs_writes_bytes_total{project=~\"$project\",instance=~\"$instance\",image!=\"\"}[5m])", + "intervalFactor": 2, + "legendFormat": "{{name}}", + "metric": "container_fs_writes_bytes_total", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "I/O Tx", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "10s", + "schemaVersion": 20, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "Prometheus", + "definition": "label_values(container_last_seen, project)", + "hide": 0, + "includeAll": false, + "label": "项目", + "multi": false, + "name": "project", + "options": [], + "query": "label_values(container_last_seen, project)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": {}, + "datasource": "Prometheus", + "definition": "label_values(container_last_seen{project=~\"$project\"}, server)", + "hide": 0, + "includeAll": false, + "label": "主机", + "multi": false, + "name": "server", + "options": [], + "query": "label_values(container_last_seen{project=~\"$project\"}, server)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": {}, + "datasource": "Prometheus", + "definition": "label_values(container_last_seen{project=~\"$project\",server=~\"$server\"}, instance)", + "hide": 2, + "includeAll": false, + "label": "instance", + "multi": false, + "name": "instance", + "options": [], + "query": "label_values(container_last_seen{project=~\"$project\",server=~\"$server\"}, instance)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-12h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "docker", + "uid": "docker", + "version": 1 +} diff --git a/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/moleculer-metrics.json b/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/moleculer-metrics.json new file mode 100644 index 00000000000..bf1479fb39f --- /dev/null +++ b/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/moleculer-metrics.json @@ -0,0 +1,1349 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "hideTimeOverride": true, + "id": 9, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "avg(moleculer_registry_nodes_total)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "", + "timeFrom": "1s", + "title": "Nodes", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 0 + }, + "hideTimeOverride": true, + "id": 10, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "avg(moleculer_registry_services_total)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "", + "timeFrom": "1s", + "title": "Services", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 0 + }, + "hideTimeOverride": true, + "id": 11, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "avg(moleculer_registry_actions_total)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "", + "timeFrom": "1s", + "title": "Actions", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 0 + }, + "hideTimeOverride": true, + "id": 12, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "avg(moleculer_registry_events_total)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "", + "timeFrom": "1s", + "title": "Events", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "decimals": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "short", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 3 + }, + "hideTimeOverride": true, + "id": 2, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(moleculer_request_total)", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "", + "timeFrom": "1s", + "title": "Total calls", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "total" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(247, 55, 55, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "short", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 3 + }, + "hideTimeOverride": true, + "id": 3, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(moleculer_request_error_total)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "", + "timeFrom": "1s", + "title": "Total errors", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 3 + }, + "hideTimeOverride": true, + "id": 4, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "avg(irate(moleculer_request_time_rate[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "50, 100", + "timeFrom": "10s", + "title": "Response time", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 3 + }, + "hideTimeOverride": true, + "id": 5, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(irate(moleculer_request_total[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 2 + } + ], + "thresholds": "", + "timeFrom": "10s", + "title": "Rps", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 200 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "1m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "0m", + "frequency": "10s", + "handler": 1, + "name": "Response time ( >200ms )", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "hiddenSeries": false, + "id": 1, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.5", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(irate(moleculer_request_time_rate[1m]))", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "Total", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 200 + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Response time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.5", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(irate(moleculer_request_total_rate[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "Calls", + "refId": "A", + "step": 2 + }, + { + "expr": "sum(irate(moleculer_request_error_total_rate[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "Errors", + "refId": "B", + "step": 2 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Action calls", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 14 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.5", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (errorName) (irate(moleculer_request_error_total_rate[1m]))", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{errorName}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Error types", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 21 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.5", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(moleculer_request_total) by (action)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{action}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Actions", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "dashboardFilter": "", + "dashboardTags": [], + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 8, + "limit": 10, + "links": [], + "nameFilter": "", + "onlyAlertsOnDashboard": true, + "show": "current", + "sortOrder": 1, + "stateFilter": [], + "title": "Alerts", + "type": "alertlist" + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 28 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.5", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(moleculer_registry_nodes_total)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Available nodes", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 26, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Moleculer Prometheus demo", + "uid": "xtcSMfkmz", + "version": 1 + } diff --git a/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/reverse-proxy_rev1.json b/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/reverse-proxy_rev1.json new file mode 100644 index 00000000000..0479361a1d4 --- /dev/null +++ b/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/reverse-proxy_rev1.json @@ -0,0 +1,1303 @@ +{ + "__inputs": [ + { + "name": "DS_SWB", + "label": "Prometheus", + "description": "Traefik2 prometheus metric endpoint", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "6.3.6" + }, + { + "type": "panel", + "id": "grafana-piechart-panel", + "name": "Pie Chart", + "version": "1.3.9" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "singlestat", + "name": "Singlestat", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": 10906, + "graphTooltip": 0, + "id": null, + "iteration": 1569328089102, + "links": [ + { + "icon": "external link", + "tags": [ + "link" + ], + "type": "dashboards" + } + ], + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 0, + "y": 0 + }, + "id": 22, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "time() - process_start_time_seconds{job=\"$job\"}", + "format": "time_series", + "intervalFactor": 2, + "refId": "A" + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(26, 206, 22, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "Prometheus", + "decimals": 0, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 3, + "y": 0 + }, + "id": 26, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "200%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(increase(traefik_service_requests_total{code=\"404\",method=\"GET\",protocol=~\"$protocol\"}[$interval]))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "traefik_requests_total", + "refId": "A", + "step": 60 + } + ], + "thresholds": "0,1", + "title": "404 Error Count last $interval", + "type": "singlestat", + "valueFontSize": "200%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "max" + }, + { + "aliasColors": {}, + "breakPoint": "50%", + "cacheTimeout": null, + "combine": { + "label": "Others", + "threshold": 0 + }, + "datasource": "Prometheus", + "fontSize": "80%", + "format": "short", + "gridPos": { + "h": 6, + "w": 7, + "x": 6, + "y": 0 + }, + "id": 18, + "interval": null, + "legend": { + "percentage": true, + "show": true, + "sort": null, + "sortDesc": null, + "values": true + }, + "legendType": "Right side", + "links": [], + "maxDataPoints": 3, + "nullPointMode": "connected", + "options": {}, + "pieType": "pie", + "strokeWidth": 1, + "targets": [ + { + "expr": "traefik_service_requests_total{protocol=~\"$protocol\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{method}} : {{code}}", + "refId": "A" + } + ], + "title": "$protocol return code", + "type": "grafana-piechart-panel", + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 4, + "x": 13, + "y": 0 + }, + "id": 20, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(traefik_entrypoint_request_duration_seconds_sum) / sum(traefik_entrypoint_requests_total) * 1000", + "format": "time_series", + "intervalFactor": 2, + "refId": "A" + } + ], + "thresholds": "", + "title": "Average response time", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 7, + "x": 17, + "y": 0 + }, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(traefik_service_request_duration_seconds_sum{protocol=~\"$protocol\"}) / sum(traefik_entrypoint_requests_total{protocol=~\"$protocol\"}) * 1000", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Average response time (ms)", + "refId": "A", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average response time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 10, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "decimals": 0, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 10, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(traefik_service_requests_total{code=\"404\",method=\"GET\",protocol=~\"$protocol\"}[$interval])) by (service)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{service}} ", + "refId": "A", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Bad Status Code Count $interval", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 6 + }, + "hideTimeOverride": false, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(traefik_service_requests_total[$interval]))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Total requests", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Total requests over $interval", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": 0, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 10, + "x": 0, + "y": 12 + }, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null as zero", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "process_open_fds{job=~\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{ instance }}", + "refId": "A", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Used sockets", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "decimals": 0, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 14, + "x": 10, + "y": 12 + }, + "id": 24, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(traefik_service_requests_total{protocol=~\"http|https\",code=\"200\"}[$interval])) by (service)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{service}} {{method}} {{code}}", + "refId": "A", + "step": 240 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Access to backends", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 7, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 30, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(traefik_entrypoint_open_connections) by (method)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ method }}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "ENTRYPOINT - Open Connections", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 7, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 18 + }, + "id": 28, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(traefik_service_open_connections) by (method)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ method }}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "BACKEND - Open Connections", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "decimals": 0, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 12, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "/^[^234].*/", + "transform": "negative-Y" + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(traefik_service_requests_total{protocol=~\"$protocol\"}[$interval])) by (code)", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{code}}", + "refId": "A", + "step": 120 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Status Code Count per $interval", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "10s", + "schemaVersion": 19, + "style": "dark", + "tags": [ + "traefik", + "load-balancer", + "docker", + "prometheus", + "link" + ], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Job:", + "multi": false, + "name": "job", + "options": [], + "query": "label_values(job)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 2, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": "traefik", + "current": {}, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Node:", + "multi": false, + "name": "node", + "options": [], + "query": "label_values(process_start_time_seconds, instance)", + "refresh": 1, + "regex": "/([^:]+):.*/", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": {}, + "datasource": "Prometheus", + "definition": "label_values(traefik_service_requests_total, protocol)", + "hide": 0, + "includeAll": true, + "label": "Service:", + "multi": true, + "name": "protocol", + "options": [], + "query": "label_values(traefik_service_requests_total, protocol)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": true, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "30m", + "value": "30m" + }, + "hide": 0, + "label": "Interval", + "name": "interval", + "options": [ + { + "selected": false, + "text": "auto", + "value": "$__auto_interval_interval" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": true, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Traefik2", + "uid": "3ipsWfViz", + "version": 13, + "description": "Dashboard for Traefik2/Prometheus, based upon the one by ichasco. Updated for Traefik2 metric names." +} diff --git a/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/rocketchat-metrics.json b/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/rocketchat-metrics.json new file mode 100644 index 00000000000..4f1dfba53fb --- /dev/null +++ b/ee/server/services/.config/grafana/provisioning/dashboards/json-exports/rocketchat-metrics.json @@ -0,0 +1,4725 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "limit": 100, + "matchAny": true, + "name": "Annotations & Alerts", + "showIn": 0, + "tags": [ + "metrics", + "microservices" + ], + "type": "tags" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 10, + "iteration": 1587383367925, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 43, + "panels": [], + "title": "Totals", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": true, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 30, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(rocketchat_users_online{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "10000000", + "title": "Users Online", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": true, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 31, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(rocketchat_users_away{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "", + "title": "Users Away", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 32, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(rocketchat_ddp_connected_users{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "title": "DDP Users", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 0, + "y": 4 + }, + "id": 26, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(rocketchat_users_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "", + "title": "Users", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 8, + "y": 4 + }, + "id": 27, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(rocketchat_rooms_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "", + "title": "Rooms", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 16, + "y": 4 + }, + "id": 29, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(rocketchat_messages_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "", + "title": "Messages", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 0, + "y": 7 + }, + "id": 36, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(rocketchat_users_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "", + "title": "Users Diff", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "diff" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 8, + "y": 7 + }, + "id": 37, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(rocketchat_rooms_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "", + "title": "Rooms Diff", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "diff" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Prometheus", + "format": "locale", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 38, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "max(rocketchat_messages_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "", + "title": "Messages Diff", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "diff" + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 69, + "panels": [], + "title": "Metrics", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 11 + }, + "hiddenSeries": false, + "id": 71, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "increase(rocketchat_metrics_requests{DeploymentName=\"$deployment\"}[5m])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Metrics Requests", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 11 + }, + "hiddenSeries": false, + "id": 72, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rocketchat_metrics_size{DeploymentName=\"$deployment\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Metrics Size", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 23, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 19 + }, + "hiddenSeries": false, + "id": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "nodejs_active_handles_total{DeploymentName=\"$deployment\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Active Handles", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 19 + }, + "hiddenSeries": false, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "nodejs_active_requests_total{DeploymentName=\"$deployment\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Active Requests", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 26 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "nodejs_eventloop_lag_seconds{DeploymentName=\"$deployment\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Event loop lag", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 26 + }, + "hiddenSeries": false, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum by (space) (nodejs_heap_size_used_bytes{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{space}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Heap used", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 33 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "nodejs_heap_size_used_bytes{DeploymentName=\"$deployment\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "{{instance}} [{{space}}]", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Per pod heap", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 40 + }, + "hiddenSeries": false, + "id": 66, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(nodejs_gc_pause_seconds_total{DeploymentName=\"$deployment\"}[5m])) by (gctype)", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "{{gctype}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "GC - Seconds Running", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 40 + }, + "hiddenSeries": false, + "id": 67, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(nodejs_gc_reclaimed_bytes_total{DeploymentName=\"$deployment\"}[5m])) by (instance)", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "GC - Bytes Freed", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "NodeJS", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 63, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 48 + }, + "hiddenSeries": false, + "id": 58, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_ddp_rate_limit_exceeded{DeploymentName=\"$deployment\", user_id!=\"null\"}[5m])) by (limit_name)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{limit_name}} (not null)", + "refId": "A" + }, + { + "expr": "sum(increase(rocketchat_ddp_rate_limit_exceeded{DeploymentName=\"$deployment\", user_id=\"null\"}[5m])) by (limit_name)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{limit_name}} (null)", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Limit by type", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 48 + }, + "hiddenSeries": false, + "id": 59, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_ddp_rate_limit_exceeded{DeploymentName=\"$deployment\", user_id!=\"null\"}[5m])) by (type,name)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{type}}: {{name}} (not null)", + "refId": "A" + }, + { + "expr": "sum(increase(rocketchat_ddp_rate_limit_exceeded{DeploymentName=\"$deployment\", user_id=\"null\"}[5m])) by (type,name)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{type}}: {{name}} (null)", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Limit by method", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 54 + }, + "hiddenSeries": false, + "id": 61, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_ddp_rate_limit_exceeded{DeploymentName=\"$deployment\"}[5m])) by (user_id)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{user_id}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Limit by userId", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 54 + }, + "hiddenSeries": false, + "id": 60, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_ddp_rate_limit_exceeded{DeploymentName=\"$deployment\", user_id!=\"null\"}[5m])) by (connection_id)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{connection_id}} (not null)", + "refId": "A" + }, + { + "expr": "sum(increase(rocketchat_ddp_rate_limit_exceeded{DeploymentName=\"$deployment\", user_id=\"null\"}[5m])) by (connection_id)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{connection_id}} (null)", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Limit by connectionId", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "DDP Rate Limiter", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 21, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 21 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "increase(rocketchat_message_sent{DeploymentName=\"$deployment\"}[5m])", + "format": "time_series", + "intervalFactor": 10, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Messages Sent (5m)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 28 + }, + "hiddenSeries": false, + "id": 35, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "max(rocketchat_messages_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 10, + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Messages Sent Total", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 35 + }, + "hiddenSeries": false, + "id": 10, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "Sessions", + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "avg(rocketchat_users_online{DeploymentName=\"$deployment\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Online", + "refId": "A" + }, + { + "expr": "avg(rocketchat_users_away{DeploymentName=\"$deployment\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Away", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Users Presence", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 44 + }, + "hiddenSeries": false, + "id": 64, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "Sessions", + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_ddp_connected_users{DeploymentName=\"$deployment\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Connected Users", + "refId": "A" + }, + { + "expr": "sum(rocketchat_ddp_sessions_count{DeploymentName=\"$deployment\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "All Sessions", + "refId": "B" + }, + { + "expr": "sum(rocketchat_ddp_sessions_auth{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Authenticated Sessions ", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Users & Sessions", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 53 + }, + "hiddenSeries": false, + "id": 56, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_meteor_stream{DeploymentName=\"$deployment\"}[5m])) by (stream,eventName)", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{stream}}/{{eventName}}", + "refId": "A" + }, + { + "expr": "sum(increase(rocketchat_meteor_streamer_broadcast{DeploymentName=\"$deployment\"}[5m])) by (stream,eventName)", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "broadcast ({{stream}}/{{eventName}})", + "refId": "B" + }, + { + "expr": "sum(increase(rocketchat_oplog{DeploymentName=\"$deployment\"}[5m])) by (collection,op)", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{collection}}/{{op}}", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Oplog", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 62 + }, + "hiddenSeries": false, + "id": 65, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 1, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rocketchat_oplog_queue{DeploymentName=\"$deployment\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{instance}}", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Oplog Queue", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 71 + }, + "hiddenSeries": false, + "id": 73, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 1, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rocketchat_push_queue{DeploymentName=\"$deployment\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{instance}}", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Push Queue", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 80 + }, + "hiddenSeries": false, + "id": 74, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 1, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_meteor_facts{DeploymentName=\"$deployment\"}) by (pkg, fact)", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{pkg}} {{fact}}", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Meteor Facts", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 89 + }, + "hiddenSeries": false, + "id": 11, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "Sessions", + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "max(rocketchat_users_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Users", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Total of Users", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "locale", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 89 + }, + "hiddenSeries": false, + "id": 12, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "Sessions", + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "max(rocketchat_rooms_total{DeploymentName=\"$deployment\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Rooms", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Total of Rooms", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "locale", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 97 + }, + "hiddenSeries": false, + "id": 39, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "Sessions", + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_notification_sent{DeploymentName=\"$deployment\"}[5m])) by (notification_type)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{notification_type}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Notifications / Minute", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "locale", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "RocketChat Data", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 45, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "decimals": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 50 + }, + "hiddenSeries": false, + "id": 40, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": null, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_meteor_methods{DeploymentName=\"$deployment\"}) by (method) * sum(increase(rocketchat_meteor_methods_count{DeploymentName=\"$deployment\"}[5m])) by (method)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Meteor Methods Total Time", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 57 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": null, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_meteor_methods{DeploymentName=\"$deployment\"}) by (method)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Meteor Methods Time", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 57 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_meteor_methods_count{DeploymentName=\"$deployment\"}[5m])) by (method)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{method}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Meteor Methods Calls / Minute", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Methods", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 47, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 51 + }, + "hiddenSeries": false, + "id": 41, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_meteor_subscriptions{DeploymentName=\"$deployment\"}) by (subscription) * sum(increase(rocketchat_meteor_subscriptions_count{DeploymentName=\"$deployment\"}[5m])) by (subscription)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{subscription}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Meteor Subscription Total Time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 59 + }, + "hiddenSeries": false, + "id": 9, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_meteor_subscriptions{DeploymentName=\"$deployment\"}) by (subscription)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{subscription}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Meteor Subscription Times", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 59 + }, + "hiddenSeries": false, + "id": 15, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_meteor_subscriptions_count{DeploymentName=\"$deployment\"}[5m])) by (subscription)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{subscription}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Meteor Subscription Calls / Minute", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Subscriptions", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 49, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 52 + }, + "hiddenSeries": false, + "id": 53, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_callbacks{DeploymentName=\"$deployment\"}) by (callback) * sum(increase(rocketchat_callbacks_count{DeploymentName=\"$deployment\"}[5m])) by (callback)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{callback}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Callbacks Total Times", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 60 + }, + "hiddenSeries": false, + "id": 16, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(irate(rocketchat_callbacks{DeploymentName=\"$deployment\"}[5m])) by (callback)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{callback}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Callbacks Times", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 60 + }, + "hiddenSeries": false, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_callbacks_count{DeploymentName=\"$deployment\"}[5m])) by (callback)", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{callback}}", + "refId": "A" + }, + { + "expr": "sum(rocketchat_callbacks_count{DeploymentName=\"$deployment\"}) by (hook)", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{hook}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Callbacks Calls / Minute", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 68 + }, + "hiddenSeries": false, + "id": 54, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_hooks{DeploymentName=\"$deployment\"}) by (hook) * sum(increase(rocketchat_hooks_count{DeploymentName=\"$deployment\"}[5m])) by (hook)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{hook}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Hooks Total Times", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 76 + }, + "hiddenSeries": false, + "id": 34, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(irate(rocketchat_hooks{DeploymentName=\"$deployment\"}[5m])) by (hook)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{hook}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Hooks Times", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 76 + }, + "hiddenSeries": false, + "id": 33, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_hooks_count{DeploymentName=\"$deployment\"}[5m])) by (hook)", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{hook}}", + "refId": "A" + }, + { + "expr": "sum(rocketchat_callbacks_count{DeploymentName=\"$deployment\"}) by (hook)", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{hook}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Hooks Calls / Minute", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Callbacks & Hooks", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 51, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 25 + }, + "hiddenSeries": false, + "id": 52, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rocketchat_rest_api{DeploymentName=\"$deployment\"}) by (entrypoint) * sum(increase(rocketchat_rest_api_count{DeploymentName=\"$deployment\"}[5m])) by (entrypoint)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{entrypoint}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "REST API Total Times", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "hiddenSeries": false, + "id": 18, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "sideWidth": null, + "sort": "max", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(irate(rocketchat_rest_api{DeploymentName=\"$deployment\"}[5m])) by (entrypoint)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{entrypoint}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "REST API Times", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "hiddenSeries": false, + "id": 19, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "sideWidth": null, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(increase(rocketchat_rest_api_count{DeploymentName=\"$deployment\"}[5m])) by (entrypoint)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{entrypoint}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "REST API Calls / Minute", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "REST API", + "type": "row" + } + ], + "refresh": "1m", + "schemaVersion": 22, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 2, + "includeAll": false, + "label": null, + "multi": false, + "name": "deployment", + "options": [], + "query": "label_values(nodejs_active_handles_total,DeploymentName)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Rocket.Chat Metrics", + "uid": "Ee8-FY7ml", + "version": 3 +} diff --git a/ee/server/services/.config/grafana/provisioning/dashboards/provider/prometheus.yml b/ee/server/services/.config/grafana/provisioning/dashboards/provider/prometheus.yml new file mode 100644 index 00000000000..04139c4342d --- /dev/null +++ b/ee/server/services/.config/grafana/provisioning/dashboards/provider/prometheus.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: +- name: 'Prometheus' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/ diff --git a/ee/server/services/.config/grafana/provisioning/datasources/prometheus.yml b/ee/server/services/.config/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 00000000000..e4e3850d84e --- /dev/null +++ b/ee/server/services/.config/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: +- name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + version: 1 + editable: false diff --git a/ee/server/services/.config/prometheus/prometheus.yml b/ee/server/services/.config/prometheus/prometheus.yml new file mode 100644 index 00000000000..c5d3d43d89e --- /dev/null +++ b/ee/server/services/.config/prometheus/prometheus.yml @@ -0,0 +1,25 @@ +scrape_configs: +# if you just have single or multiple, but static instances - you +# can use the static configuration below. +- job_name: rocketchat_static + static_configs: + - targets: + - cadvisor:8080 + + +# If you use a Docker-based setup make sure to use the DNS +# discovery to always include all available application instances +# of Rocket.Chat +- job_name: rocketchat_docker + scrape_interval: 5s + dns_sd_configs: + - names: [ + "authorization-service", + "account-service", + "presence-service", + "ddp-streamer-service", + "stream-hub-service", + "traefik", + ] + type: A + port: 9458 diff --git a/ee/server/services/.config/services/service.env b/ee/server/services/.config/services/service.env new file mode 100644 index 00000000000..78aa22d519c --- /dev/null +++ b/ee/server/services/.config/services/service.env @@ -0,0 +1,5 @@ +MS_METRICS=true +TRACING_ENABLED=true +TRANSPORTER=nats://nats:4222 +MONGO_URL=mongodb://host.docker.internal:3001/meteor +MOLECULER_LOG_LEVEL=info diff --git a/ee/server/services/.config/traefik/servers/localhost.yml b/ee/server/services/.config/traefik/servers/localhost.yml new file mode 100644 index 00000000000..6ea37a941d6 --- /dev/null +++ b/ee/server/services/.config/traefik/servers/localhost.yml @@ -0,0 +1,20 @@ +http: + routers: + router2: + rule: Host(`localhost`) && PathPrefix(`/sockjs/`, `/websocket/`) + service: ddp-streamer-service@docker + priority: 2 + entryPoints: + - web + router1: + rule: Host(`localhost`) + service: service1 + priority: 1 + entryPoints: + - web + + services: + service1: + loadBalancer: + servers: + - url: http://host.docker.internal:3000 diff --git a/ee/server/services/.env b/ee/server/services/.env new file mode 100644 index 00000000000..6dcc49f9bae --- /dev/null +++ b/ee/server/services/.env @@ -0,0 +1 @@ +HOST_NAME=localhost diff --git a/ee/server/services/.gitignore b/ee/server/services/.gitignore new file mode 100644 index 00000000000..a244e0ddd3a --- /dev/null +++ b/ee/server/services/.gitignore @@ -0,0 +1,3 @@ +dist +.config/data +!.env diff --git a/ee/server/services/Dockerfile b/ee/server/services/Dockerfile new file mode 100644 index 00000000000..bf8a4cf1d4a --- /dev/null +++ b/ee/server/services/Dockerfile @@ -0,0 +1,30 @@ +FROM node:12 as build + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y build-essential git + +ADD ./package.json . + +RUN npm install --production + +FROM node:12-alpine + +ARG SERVICE + +WORKDIR /app + +COPY --from=build /app . + +# add dist/ folder from tsc so we don't need to add all rocket.chat repo +ADD ./dist . + +ENV NODE_ENV=production \ + PORT=3000 + +WORKDIR /app/ee/server/services/${SERVICE} + +EXPOSE 3000 9458 + +CMD ["node", "service.js"] diff --git a/ee/server/services/README.md b/ee/server/services/README.md new file mode 100644 index 00000000000..8ab18a7d26a --- /dev/null +++ b/ee/server/services/README.md @@ -0,0 +1,76 @@ +# Rocket.Chat Micro-Services + +## PM2 + +This usually suits better for development purposes. + +Start NATS first, you can it via Docker: + +``` +docker run --rm -d -p 4222:4222 nats +``` + +Then run Rocket.Chat as usual with an additional `TRANSPORTER` and `DISABLE_DB_WATCH` env vars: + +``` +TRANSPORTER=nats://localhost:4222 MOLECULER_LOG_LEVEL=debug DISABLE_DB_WATCH=true meteor +``` + +Set up an Enterprise license going to Admin > Enterprise. + +Then you can spin up micro services. From this folder, first install dependencies: + +``` +meteor npm i +``` + +You can run on `dev` to have hot reloading: + +``` +MONGO_URL=mongodb://localhost:3001/meteor \ +MOLECULER_LOG_LEVEL=debug \ +TRANSPORTER=nats://localhost:4222 \ +meteor npm run dev +``` + +To see process logs, do: + +``` +meteor npm run pm2 -- logs +``` + +## Docker Compose + +The `.env` file defines the HTTP address to be used, default to `localhost`. + +It requires meteor to be running, and the config at `services/.config/services/service.env` uses the default meteor mongodb. To run it: + +``` +TRANSPORTER=nats://localhost:4222 MOLECULER_LOG_LEVEL=debug meteor +``` + +The `docker-compose.yml` file contais a setup of the micro-services plus some extra tools: + +### Traefik +It's used to route the the domain on port 80 to the **DDP Streamer Service** and expose these addresses +* `traefik.localhost` as Traefik admin +* `prometheus.localhost` as Prometheus admin +* `grafana.localhost` as Grafana dashboards + +### Prometheus +Used to collect metrics from the micro-services + +### Grafana +Used to expose metrics from prometheus as dashboards + +### Nats +Used for the communication of the microservices + +## Build containers +`npm run build-containers` will build the typescript files and generate the containers + +## Running with docker-compose +`docker-compose up --remove-orphans` will run all the micro-services, still need to run MongoDB and Rocket.Chat Core separated + +### Running rocket.chat core +`MONGO_URL=mongodb://localhost:27017/rocketchat MONGO_OPLOG_URL=mongodb://localhost:27017/local TRANSPORTER=nats://localhost:4222 MOLECULER_LOG_LEVEL=debug meteor` diff --git a/ee/server/services/account/Account.ts b/ee/server/services/account/Account.ts new file mode 100644 index 00000000000..6971f9fd0ee --- /dev/null +++ b/ee/server/services/account/Account.ts @@ -0,0 +1,89 @@ +import { + IStampedToken, + _generateStampedLoginToken, + _hashStampedToken, + _hashLoginToken, + _tokenExpiration, + validatePassword, +} from './lib/utils'; +import { getCollection, Collections } from '../mongo'; +import { IUser } from '../../../../definition/IUser'; +import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; +import { IAccount, ILoginResult } from '../../../../server/sdk/types/IAccount'; + + +const saveSession = async (uid: string, newToken: IStampedToken): Promise => { + const Users = await getCollection(Collections.User); + await Users.updateOne({ _id: uid }, { + $push: { + 'services.resume.loginTokens': _hashStampedToken(newToken), + }, + }); +}; + +const loginViaResume = async (resume: string): Promise => { + const Users = await getCollection(Collections.User); + const hashedToken = _hashLoginToken(resume); + + const user = await Users.findOne({ + 'services.resume.loginTokens.hashedToken': hashedToken, + }, { + projection: { + 'services.resume.loginTokens': 1, + }, + }); + if (!user) { + return false; + } + + const { when } = user.services?.resume?.loginTokens?.find((token) => + token.hashedToken === hashedToken, + ) || {}; + + return { + uid: user._id, + token: resume, + tokenExpires: when ? _tokenExpiration(when) : undefined, + type: 'resume', + }; +}; + +const loginViaUsername = async ({ username }: {username: string}, password: string): Promise => { + const Users = await getCollection(Collections.User); + const user = await Users.findOne({ username }, { projection: { 'services.password.bcrypt': 1 } }); + if (!user) { + return false; + } + + const valid = user.services?.password?.bcrypt && validatePassword(password, user.services.password.bcrypt); + if (!valid) { + return false; + } + + const newToken = _generateStampedLoginToken(); + + await saveSession(user._id, newToken); + + return { + uid: user._id, + token: newToken.token, + tokenExpires: _tokenExpiration(newToken.when), + type: 'password', + }; +}; + +export class Account extends ServiceClass implements IAccount { + protected name = 'accounts'; + + async login({ resume, user, password }: {resume: string; user: {username: string}; password: string}): Promise { + if (resume) { + return loginViaResume(resume); + } + + if (user && password) { + return loginViaUsername(user, password); + } + + return false; + } +} diff --git a/ee/server/services/account/lib/utils.ts b/ee/server/services/account/lib/utils.ts new file mode 100644 index 00000000000..af8e4df00c2 --- /dev/null +++ b/ee/server/services/account/lib/utils.ts @@ -0,0 +1,62 @@ +import crypto from 'crypto'; + +import bcrypt from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; + +export interface IStampedToken { + token: string; + when: Date; + [key: string]: any; +} + +export interface IHashedStampedToken { + when: Date; + hashedToken: string; +} + +type Password = string | { + digest: string; +} + +export const getPassword = (password: Password): string => { + if (typeof password === 'string') { + return crypto.createHash('sha256').update(password).digest('hex'); // lgtm [js/insufficient-password-hash] + } + if (typeof password.digest === 'undefined') { + throw new Error('invalid password'); + } + return password.digest; +}; + +// https://github.com/meteor/meteor/blob/c5b51b0fc2a8cef498b9390ebcb4925e02de83e8/packages/accounts-base/accounts_server.js#L934 +export const _generateStampedLoginToken = (): IStampedToken => ({ + token: uuidv4(), + when: new Date(), +}); + +// https://github.com/meteor/meteor/blob/c5b51b0fc2a8cef498b9390ebcb4925e02de83e8/packages/accounts-base/accounts_server.js#L780 +export const _hashLoginToken = (loginToken: string): string => { + const hash = crypto.createHash('sha256'); + hash.update(loginToken); + return hash.digest('base64'); +}; + +// https://github.com/meteor/meteor/blob/c5b51b0fc2a8cef498b9390ebcb4925e02de83e8/packages/accounts-base/accounts_server.js#L787 +export const _hashStampedToken = (stampedToken: IStampedToken): IHashedStampedToken => { + const hashedStampedToken = Object.keys(stampedToken).reduce( + (prev, key) => (key === 'token' + ? prev + : { ...prev, [key]: stampedToken[key] }), + {}, + ); + + return { + ...hashedStampedToken, + hashedToken: _hashLoginToken(stampedToken.token), + } as IHashedStampedToken; +}; + +export const validatePassword = (password: string, bcryptPassword: string): Promise => bcrypt.compare(getPassword(password), bcryptPassword); + +const expiryDaysInMS = 15 * 60 * 60 * 24 * 1000; +export const _tokenExpiration = (when: string | Date): Date => new Date(new Date(when).getTime() + expiryDaysInMS); diff --git a/ee/server/services/account/service.ts b/ee/server/services/account/service.ts new file mode 100644 index 00000000000..2b0a7811496 --- /dev/null +++ b/ee/server/services/account/service.ts @@ -0,0 +1,6 @@ +import '../../broker'; + +import { api } from '../../../../server/sdk/api'; +import { Account } from './Account'; + +api.registerService(new Account()); diff --git a/ee/server/services/authorization/service.ts b/ee/server/services/authorization/service.ts new file mode 100644 index 00000000000..4a1975fabfb --- /dev/null +++ b/ee/server/services/authorization/service.ts @@ -0,0 +1,9 @@ +import '../../broker'; + +import { api } from '../../../../server/sdk/api'; +import { Authorization } from '../../../../server/services/authorization/service'; +import { getConnection } from '../mongo'; + +getConnection().then((db) => { + api.registerService(new Authorization(db)); +}); diff --git a/ee/server/services/ddp-streamer/Client.ts b/ee/server/services/ddp-streamer/Client.ts new file mode 100644 index 00000000000..24d93c74833 --- /dev/null +++ b/ee/server/services/ddp-streamer/Client.ts @@ -0,0 +1,177 @@ +import { EventEmitter } from 'events'; + +import { v1 as uuidv1 } from 'uuid'; +import WebSocket from 'ws'; + +import { DDP_EVENTS, WS_ERRORS, WS_ERRORS_MESSAGES, TIMEOUT } from './constants'; +import { SERVER_ID } from './Server'; +import { server } from './configureServer'; +import { IPacket } from './types/IPacket'; + +interface IConnection { + livechatToken?: string; + onClose(fn: (...args: any[]) => void): void; +} + +export class Client extends EventEmitter { + private chain = Promise.resolve(); + + protected timeout: NodeJS.Timeout; + + public readonly session = uuidv1(); + + public subscriptions = new Map(); + + public connection: IConnection; + + public wait = false; + + public userId: string; + + public userToken: string; + + constructor( + public ws: WebSocket, + public meteorClient = false, + ) { + super(); + + this.connection = { + onClose: (fn): void => { + this.on('close', fn); + }, + }; + + this.renewTimeout(TIMEOUT / 1000); + this.ws.on('message', this.handler); + this.ws.on('close', (...args) => { + server.emit(DDP_EVENTS.DISCONNECTED, this); + this.emit('close', ...args); + this.subscriptions.clear(); + clearTimeout(this.timeout); + }); + + this.setMaxListeners(50); + + this.greeting(); + + server.emit(DDP_EVENTS.CONNECTED, this); + + this.ws.on('message', () => this.renewTimeout(TIMEOUT)); + + this.once('message', ({ msg }) => { + if (msg !== DDP_EVENTS.CONNECT) { + return this.ws.close(WS_ERRORS.CLOSE_PROTOCOL_ERROR, WS_ERRORS_MESSAGES.CLOSE_PROTOCOL_ERROR); + } + return this.send( + server.serialize({ [DDP_EVENTS.MSG]: DDP_EVENTS.CONNECTED, session: this.session }), + ); + }); + + this.send(SERVER_ID); + } + + greeting(): void { + // no greeting by default + if (this.meteorClient) { + return this.ws.send('o'); + } + } + + async callMethod(packet: IPacket): Promise { + this.chain = this.chain.then(() => server.call(this, packet)).catch(); + } + + async callSubscribe(packet: IPacket): Promise { + this.chain = this.chain.then(() => server.subscribe(this, packet)).catch(); + } + + process(action: string, packet: IPacket): void { + switch (action) { + case DDP_EVENTS.PING: + this.pong(packet.id); + break; + case DDP_EVENTS.METHOD: + if (!packet.method) { + return this.ws.close(WS_ERRORS.CLOSE_PROTOCOL_ERROR); + } + if (!packet.id) { + return this.ws.close(WS_ERRORS.CLOSE_PROTOCOL_ERROR); + } + this.callMethod(packet); + break; + case DDP_EVENTS.SUBSCRIBE: + if (!packet.name) { + return this.ws.close(WS_ERRORS.CLOSE_PROTOCOL_ERROR); + } + if (!packet.id) { + return this.ws.close(WS_ERRORS.CLOSE_PROTOCOL_ERROR); + } + this.callSubscribe(packet); + break; + case DDP_EVENTS.UNSUBSCRIBE: + if (!packet.id) { + return this.ws.close(WS_ERRORS.CLOSE_PROTOCOL_ERROR); + } + const subscription = this.subscriptions.get(packet.id); + if (!subscription) { + return; + } + subscription.stop(); + break; + } + } + + closeTimeout = (): void => { + this.ws.close(WS_ERRORS.TIMEOUT, WS_ERRORS_MESSAGES.TIMEOUT); + }; + + ping(id?: string): void { + this.send(server.serialize({ [DDP_EVENTS.MSG]: DDP_EVENTS.PING, ...id && { [DDP_EVENTS.ID]: id } })); + } + + pong(id?: string): void { + this.send(server.serialize({ [DDP_EVENTS.MSG]: DDP_EVENTS.PONG, ...id && { [DDP_EVENTS.ID]: id } })); + } + + handleIdle = (): void => { + this.ping(); + this.timeout = setTimeout(this.closeTimeout, TIMEOUT); + }; + + renewTimeout(timeout = TIMEOUT): void { + clearTimeout(this.timeout); + this.timeout = setTimeout(this.handleIdle, timeout); + } + + handler = async (payload: string): Promise => { + try { + const packet = server.parse(payload); + this.emit('message', packet); + if (this.wait) { + return new Promise((resolve) => this.once(DDP_EVENTS.LOGGED, () => resolve(this.process(packet.msg, packet)))); + } + this.process(packet.msg, packet); + } catch (err) { + console.error(err); + return this.ws.close( + WS_ERRORS.UNSUPPORTED_DATA, + WS_ERRORS_MESSAGES.UNSUPPORTED_DATA, + ); + } + }; + + encodePayload(payload: string): string { + if (this.meteorClient) { + return `a${ JSON.stringify([payload]) }`; + } + return payload; + } + + send(payload: string): void { + return this.ws.send(this.encodePayload(payload)); + } +} + +// TODO implement meteor errors +// a["{\"msg\":\"result\",\"id\":\"12\",\"error\":{\"isClientSafe\":true,\"error\":403,\"reason\":\"User has no password set\",\"message\":\"User has no password set [403]\",\"errorType\":\"Meteor.Error\"}}"] diff --git a/ee/server/services/ddp-streamer/DDPStreamer.ts b/ee/server/services/ddp-streamer/DDPStreamer.ts new file mode 100644 index 00000000000..bd0952b9223 --- /dev/null +++ b/ee/server/services/ddp-streamer/DDPStreamer.ts @@ -0,0 +1,147 @@ +import http, { RequestOptions, IncomingMessage, ServerResponse } from 'http'; +import url from 'url'; + +import WebSocket from 'ws'; +// import PromService from 'moleculer-prometheus'; + +import { Client } from './Client'; +// import { STREAMER_EVENTS, STREAM_NAMES } from './constants'; +import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; +import { events } from './configureServer'; +import notifications from './streams/index'; +import { StreamerCentral } from '../../../../server/modules/streamer/streamer.module'; +import { ListenersModule } from '../../../../server/modules/listeners/listeners.module'; + +const { + PORT: port = 4000, +// PROMETHEUS_PORT = 9100, +} = process.env; + +const proxy = function(req: IncomingMessage, res: ServerResponse): void { + // console.log(`request ${ req.url }`); + req.pause(); + const options: RequestOptions = url.parse(req.url || ''); + options.headers = req.headers; + options.method = req.method; + options.agent = false; + options.hostname = 'localhost'; + options.port = 3000; + + const connector = http.request(options, function(serverResponse) { + serverResponse.pause(); + if (serverResponse.statusCode) { + res.writeHead(serverResponse.statusCode, serverResponse.headers); + } + serverResponse.pipe(res); + serverResponse.resume(); + }); + req.pipe(connector); + req.resume(); +}; + +const httpServer = http.createServer((req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + + if (process.env.NODE_ENV !== 'production' && !/^\/sockjs\/info\?cb=/.test(req.url || '')) { + return proxy(req, res); + + // res.writeHead(404); + // return res.end(); + } + + res.writeHead(200, { 'Content-Type': 'text/plain' }); + + res.end('{"websocket":true,"origins":["*:*"],"cookie_needed":false,"entropy":666}'); +}); + +httpServer.listen(port); + +const wss = new WebSocket.Server({ server: httpServer }); + +wss.on('connection', (ws, req) => new Client(ws, req.url !== '/websocket')); + +// export default { +// name: 'streamer', +// events: { +// ...events, +// stream({ stream, eventName, args }) { +// return ( +// Streamer[STREAM_NAMES[stream]] +// && Streamer[stream].emit(eventName, ...args) +// ); +// }, +// stream_internal: { +// handler({ stream, eventName, args }) { +// return ( +// Streamer[STREAM_NAMES[stream]] +// && Streamer[stream].internal.emit(eventName, ...args) +// ); +// }, +// }, +// }, +// }; + +// broker.createService({ +// settings: { +// port: PROMETHEUS_PORT, +// metrics: { +// streamer_users_connected: { +// type: 'Gauge', +// labelNames: ['nodeID'], +// help: 'Users connecteds by streamer', +// }, +// streamer_users_logged: { +// type: 'Gauge', +// labelNames: ['nodeID'], +// help: 'Users logged by streamer', +// }, +// }, +// }, +// mixins: PROMETHEUS_PORT !== 'false' ? [PromService] : [], + +export class DDPStreamer extends ServiceClass { + protected name = 'streamer'; + + constructor() { + super(); + + new ListenersModule(this, notifications); + + // [STREAMER_EVENTS.STREAM]([streamer, eventName, payload]) { + this.onEvent('stream', ([streamer, eventName, args]): void => { + const stream = StreamerCentral.instances[streamer]; + return stream && stream.emitWithoutBroadcast(eventName, ...args); + }); + + this.onEvent('watch.loginServiceConfiguration', ({ clientAction, id, data }) => { + if (clientAction === 'removed') { + events.emit('meteor.loginServiceConfiguration', 'removed', { + _id: id, + }); + return; + } + + events.emit( + 'meteor.loginServiceConfiguration', + clientAction === 'inserted' ? 'added' : 'changed', + data, + ); + }); + + this.onEvent('meteor.autoUpdateClientVersionChanged', ({ record }): void => { + events.emit('meteor.autoUpdateClientVersionChanged', record); + }); + + this.onEvent('stream.ephemeralMessage', (uid, rid, message): void => { + notifications.notifyUser(uid, 'message', { + groupable: false, + ...message, + _id: String(Date.now()), + rid, + ts: new Date(), + }); + }); + } +} + +// broker.start(); diff --git a/ee/server/services/ddp-streamer/Publication.ts b/ee/server/services/ddp-streamer/Publication.ts new file mode 100644 index 00000000000..e4136140dde --- /dev/null +++ b/ee/server/services/ddp-streamer/Publication.ts @@ -0,0 +1,61 @@ +import { EventEmitter } from 'events'; + +import { Server } from './Server'; +import { Client } from './Client'; +import { IPacket } from './types/IPacket'; +import { IPublication } from '../../../../server/modules/streamer/streamer.module'; + +export class Publication extends EventEmitter implements IPublication { + _session: IPublication['_session']; + + connection: IPublication['connection']; + + constructor( + public client: Client, + private packet: IPacket, + private server: Server, + ) { + super(); + this.packet = packet; + client.subscriptions.set(packet.id, this); + client.once('close', () => this.emit('stop', this.client, this.packet)); + this.once('stop', () => client.subscriptions.delete(packet.id)); + + this._session = { + sendAdded: this.added, + socket: client, + userId: client.userId, + }; + + this.connection = {}; + } + + ready(): void { + return this.server.ready(this.client, this.packet); + } + + stop(): void { + this.server.nosub(this.client, this.packet); + this.emit('stop', this.client, this.packet); + } + + onStop(fn: (...args: any[]) => void): void { + this.once('stop', fn); + } + + added(collection: string, id: string, fields: any): void { + this.server.added(this.client, collection, id, fields); + } + + changed(collection: string, id: string, fields: any): void { + this.server.changed(this.client, collection, id, fields); + } + + removed(collection: string, id: string): void { + this.server.removed(this.client, collection, id); + } + + get userId(): string { + return this.client.userId; + } +} diff --git a/ee/server/services/ddp-streamer/Server.ts b/ee/server/services/ddp-streamer/Server.ts new file mode 100644 index 00000000000..536bc0fcc93 --- /dev/null +++ b/ee/server/services/ddp-streamer/Server.ts @@ -0,0 +1,157 @@ +import { EventEmitter } from 'events'; + +import ejson from 'ejson'; + +import { DDP_EVENTS } from './constants'; +import { Publication } from './Publication'; +import { Client } from './Client'; +import { IPacket } from './types/IPacket'; +import { MeteorService } from '../../../../server/sdk'; + +type SubscriptionFn = (this: Publication, eventName: string, options: object) => void; +type MethodFn = (this: Client, ...args: any[]) => any; +type Methods = { + [k: string]: MethodFn; +} + +// eslint-disable-next-line @typescript-eslint/camelcase +export const SERVER_ID = ejson.stringify({ server_id: '0' }); + +export class Server extends EventEmitter { + private _subscriptions = new Map(); + + private _methods = new Map(); + + serialize = ejson.stringify; + + parse = (packet: string): IPacket => { + const payload = packet.startsWith('[') ? JSON.parse(packet)[0] : packet; + return ejson.parse(payload); + } + + async call(client: Client, packet: IPacket): Promise { + try { + if (!this._methods.has(packet.method)) { + const result = await MeteorService.callMethodWithToken(client.userId, client.userToken, packet.method, packet.params); + if (result?.result) { + return this.result(client, packet, result.result); + } + + throw new Error(`Method '${ packet.method }' doesn't exist`); + } + const fn = this._methods.get(packet.method); + + if (!fn) { + throw Error('method not found'); + } + + const result = await fn.apply(client, packet.params); + return this.result(client, packet, result); + } catch (error) { + return this.result(client, packet, null, error.toString()); + } + } + + methods(obj: Methods): void { + Object.entries(obj).forEach(([name, fn]) => { + if (this._methods.has(name)) { + return; + } + this._methods.set(name, fn); + }); + } + + async subscribe(client: Client, packet: IPacket): Promise { + try { + if (!this._subscriptions.has(packet.name)) { + throw new Error(`Subscription '${ packet.name }' doesn't exist`); + } + const fn = this._subscriptions.get(packet.name); + if (!fn) { + throw new Error('subscription not found'); + } + + const publication = new Publication(client, packet, this); + const [eventName, options] = packet.params; + await fn.call(publication, eventName, options); + } catch (error) { + this.nosub(client, packet, error.toString()); + } + } + + publish(name: string, fn: SubscriptionFn): void { + if (this._subscriptions.has(name)) { + return; + } + this._subscriptions.set(name, fn); + } + + stream(stream: string, fn: SubscriptionFn): void { + return this.publish(`stream-${ stream }`, fn); + } + + result(client: Client, { id }: IPacket, result?: any, error?: string): void { + client.send( + this.serialize({ + [DDP_EVENTS.MSG]: DDP_EVENTS.RESULT, + id, + ...result && { result }, + ...error && { error }, + }), + ); + return client.send( + this.serialize({ + [DDP_EVENTS.MSG]: DDP_EVENTS.UPDATED, + [DDP_EVENTS.METHODS]: [id], + }), + ); + } + + nosub(client: Client, { id }: IPacket, error?: string): void { + return client.send( + this.serialize({ + [DDP_EVENTS.MSG]: DDP_EVENTS.NO_SUBSCRIBE, + id, + ...error && { error }, + }), + ); + } + + ready(client: Client, packet: IPacket): void { + return client.send( + this.serialize({ [DDP_EVENTS.MSG]: DDP_EVENTS.READY, [DDP_EVENTS.SUBSCRIPTIONS]: [packet.id] }), + ); + } + + added(client: Client, collection: string, id: string, fields: any): void { + return client.send( + this.serialize({ + [DDP_EVENTS.MSG]: DDP_EVENTS.ADDED, + [DDP_EVENTS.COLLECTION]: collection, + [DDP_EVENTS.ID]: id, + [DDP_EVENTS.FIELDS]: fields, + }), + ); + } + + changed(client: Client, collection: string, id: string, fields: any): void { + return client.send( + this.serialize({ + [DDP_EVENTS.MSG]: DDP_EVENTS.CHANGED, + [DDP_EVENTS.COLLECTION]: collection, + [DDP_EVENTS.ID]: id, + [DDP_EVENTS.FIELDS]: fields, + }), + ); + } + + removed(client: Client, collection: string, id: string): void { + return client.send( + this.serialize({ + [DDP_EVENTS.MSG]: DDP_EVENTS.REMOVED, + [DDP_EVENTS.COLLECTION]: collection, + [DDP_EVENTS.ID]: id, + }), + ); + } +} diff --git a/ee/server/services/ddp-streamer/Streamer.ts b/ee/server/services/ddp-streamer/Streamer.ts new file mode 100644 index 00000000000..2932b52a091 --- /dev/null +++ b/ee/server/services/ddp-streamer/Streamer.ts @@ -0,0 +1,68 @@ +import WebSocket from 'ws'; + +import { server } from './configureServer'; +import { DDP_EVENTS } from './constants'; +import { isEmpty } from './lib/utils'; +import { Streamer, DDPSubscription, Connection, StreamerCentral, TransformMessage } from '../../../../server/modules/streamer/streamer.module'; +import { api } from '../../../../server/sdk/api'; + +StreamerCentral.on('broadcast', (name, eventName, args) => { + api.broadcast('stream', [ + name, + eventName, + args, + ]); +}); + +export class Stream extends Streamer { + registerPublication(name: string, fn: (eventName: string, options: boolean | {useCollection?: boolean; args?: any}) => void): void { + server.publish(name, fn); + } + + registerMethod(methods: Record any>): void { + server.methods(methods); + } + + changedPayload(collection: string, id: string, fields: Record): string | false { + return !isEmpty(fields) && server.serialize({ + [DDP_EVENTS.MSG]: DDP_EVENTS.CHANGED, + [DDP_EVENTS.COLLECTION]: collection, + [DDP_EVENTS.ID]: id, + [DDP_EVENTS.FIELDS]: fields, + }); + } + + async sendToManySubscriptions(subscriptions: Set, origin: Connection | undefined, eventName: string, args: any[], getMsg: string | TransformMessage): Promise { + if (typeof getMsg === 'function') { + return super.sendToManySubscriptions(subscriptions, origin, eventName, args, getMsg); + } + + const options = { + fin: true, // sending a single fragment message + rsv1: false, // don"t set rsv1 bit (no compression) + opcode: 1, // opcode for a text frame + mask: false, // set false for client-side + readOnly: false, // the data can be modified as needed + }; + + const data = { + meteor: [Buffer.concat(WebSocket.Sender.frame(Buffer.from(`a${ JSON.stringify([getMsg]) }`), options))], + normal: [Buffer.concat(WebSocket.Sender.frame(Buffer.from(getMsg), options))], + }; + + for await (const { subscription } of subscriptions) { + if (this.retransmitToSelf === false && origin && origin === subscription.connection) { + return; + } + + if (await this.isEmitAllowed(subscription, eventName, ...args)) { + await new Promise((resolve) => { + subscription.client.ws._sender.sendFrame( + data[subscription.client.meteorClient ? 'meteor' : 'normal'], + resolve, + ); + }); + } + } + } +} diff --git a/ee/server/services/ddp-streamer/configureServer.ts b/ee/server/services/ddp-streamer/configureServer.ts new file mode 100644 index 00000000000..8e544864e2f --- /dev/null +++ b/ee/server/services/ddp-streamer/configureServer.ts @@ -0,0 +1,171 @@ +import { EventEmitter } from 'events'; + +import { DDP_EVENTS } from './constants'; +import { Account, Presence, MeteorService } from '../../../../server/sdk'; +import { USER_STATUS } from '../../../../definition/UserStatus'; +import { Server } from './Server'; +import { AutoUpdateRecord } from '../../../../server/sdk/types/IMeteor'; + +export const server = new Server(); + +export const events = new EventEmitter(); + +const loginServiceConfigurationCollection = 'meteor_accounts_loginServiceConfiguration'; +const loginServiceConfigurationPublication = 'meteor.loginServiceConfiguration'; +const loginServices = new Map(); + +MeteorService.getLoginServiceConfiguration().then((records) => records.forEach((record) => loginServices.set(record._id, record))); + +server.publish(loginServiceConfigurationPublication, async function() { + loginServices.forEach((record) => this.added(loginServiceConfigurationCollection, record._id, record)); + + const fn = (action: string, record: any): void => { + switch (action) { + case 'added': + case 'changed': + loginServices.set(record._id, record); + this[action](loginServiceConfigurationCollection, record._id, record); + break; + case 'removed': + loginServices.delete(record._id); + this[action](loginServiceConfigurationCollection, record._id); + } + }; + + events.on(loginServiceConfigurationPublication, fn); + + this.onStop(() => { + events.removeListener(loginServiceConfigurationPublication, fn); + }); + + this.ready(); +}); + +const autoUpdateRecords = new Map(); + +MeteorService.getLastAutoUpdateClientVersions().then((records) => { + records.forEach((record) => autoUpdateRecords.set(record._id, record)); +}); + +const autoUpdateCollection = 'meteor_autoupdate_clientVersions'; +server.publish(autoUpdateCollection, function() { + autoUpdateRecords.forEach((record) => this.added(autoUpdateCollection, record._id, record)); + + const fn = (record: any): void => { + autoUpdateRecords.set(record._id, record); + this.changed(autoUpdateCollection, record._id, record); + }; + + events.on('meteor.autoUpdateClientVersionChanged', fn); + + this.onStop(() => { + events.removeListener('meteor.autoUpdateClientVersionChanged', fn); + }); + + this.ready(); +}); + +server.methods({ + async login({ resume, user, password }: {resume: string; user: {username: string}; password: string}) { + const result = await Account.login({ resume, user, password }); + if (!result) { + throw new Error('login error'); + } + + this.userId = result.uid; + this.userToken = result.token; + + this.emit(DDP_EVENTS.LOGGED); + + server.emit(DDP_EVENTS.LOGGED, this); + + return { + id: result.uid, + token: result.token, + tokenExpires: result.tokenExpires, + type: result.type, + }; + }, + 'UserPresence:setDefaultStatus'(status) { + const { userId } = this; + return Presence.setStatus(userId, status); + }, + 'UserPresence:online'() { + const { userId, session } = this; + return Presence.setConnectionStatus(userId, USER_STATUS.ONLINE, session); + }, + 'UserPresence:away'() { + const { userId, session } = this; + return Presence.setConnectionStatus(userId, USER_STATUS.AWAY, session); + }, + 'setUserStatus'(status, statusText) { + const { userId } = this; + return Presence.setStatus(userId, status, statusText); + }, + // Copied from /app/livechat/server/methods/setUpConnection.js + 'livechat:setUpConnection'(data = {}) { + const { token } = data; + + if (typeof token !== 'string') { + return new Error('Token must be string'); + } + + if (!this.connection.livechatToken) { + this.connection.livechatToken = token; + this.connection.onClose(() => { + MeteorService.notifyGuestStatusChanged(token, 'offline'); + }); + } + }, +}); + +server.on(DDP_EVENTS.LOGGED, ({ userId, session }) => { + Presence.newConnection(userId, session); +}); + +server.on(DDP_EVENTS.DISCONNECTED, ({ userId, session }) => { + if (!userId) { + return; + } + Presence.removeConnection(userId, session); +}); + +// TODO: resolve metrics +// server.on(DDP_EVENTS.CONNECTED, () => { +// broker.emit('metrics.update', { +// name: 'streamer_users_connected', +// method: 'inc', +// labels: { +// nodeID: broker.nodeID, +// }, +// }); +// }); + +// server.on(DDP_EVENTS.LOGGED, (/* client*/) => { +// broker.emit('metrics.update', { +// name: 'streamer_users_logged', +// method: 'inc', +// labels: { +// nodeID: broker.nodeID, +// }, +// }); +// }); + +// server.on(DDP_EVENTS.DISCONNECTED, ({ userId }) => { +// broker.emit('metrics.update', { +// name: 'streamer_users_connected', +// method: 'dec', +// labels: { +// nodeID: broker.nodeID, +// }, +// }); +// if (userId) { +// broker.emit('metrics.update', { +// name: 'streamer_users_logged', +// method: 'dec', +// labels: { +// nodeID: broker.nodeID, +// }, +// }); +// } +// }); diff --git a/ee/server/services/ddp-streamer/constants.ts b/ee/server/services/ddp-streamer/constants.ts new file mode 100644 index 00000000000..f3f92d05f9b --- /dev/null +++ b/ee/server/services/ddp-streamer/constants.ts @@ -0,0 +1,47 @@ +export const STREAMER_EVENTS = { + STREAM: 'stream', + USER_CHANGED: 'user-changed', +}; + +export const DDP_EVENTS = { + ID: 'id', + FIELDS: 'fields', + COLLECTION: 'collection', + CLEARED: 'cleared', + METHODS: 'methods', + + MSG: 'msg', + READY: 'ready', + ADDED: 'added', + CHANGED: 'changed', + REMOVED: 'removed', + RESUME: 'resume', + RESULT: 'result', + METHOD: 'method', + UPDATED: 'updated', + PING: 'ping', + PONG: 'pong', + SUBSCRIBE: 'sub', + CONNECT: 'connect', + CONNECTED: 'connected', + SUBSCRIPTIONS: 'subs', + NO_SUBSCRIBE: 'nosub', + UNSUBSCRIBE: 'unsub', + DISCONNECTED: 'disconnected', + LOGGED: 'logged', +}; + +export const WS_ERRORS = { + CLOSE_PROTOCOL_ERROR: 1002, + UNSUPPORTED_DATA: 1007, + + TIMEOUT: 4000, +}; + +export const WS_ERRORS_MESSAGES = { + CLOSE_PROTOCOL_ERROR: 'CLOSE_PROTOCOL_ERROR', + UNSUPPORTED_DATA: 'UNSUPPORTED_DATA', + TIMEOUT: 'TIMEOUT', +}; + +export const TIMEOUT = 1000 * 30; // 30 seconds diff --git a/ee/server/services/ddp-streamer/lib/utils.ts b/ee/server/services/ddp-streamer/lib/utils.ts new file mode 100644 index 00000000000..e6efcfb4bcb --- /dev/null +++ b/ee/server/services/ddp-streamer/lib/utils.ts @@ -0,0 +1,9 @@ +export const isEmpty = function(obj: object | any[] | string): boolean { + if (obj == null) { + return true; + } + if (Array.isArray(obj) || typeof obj === 'string') { + return obj.length === 0; + } + return !Object.values(obj).some((value) => value !== null); +}; diff --git a/ee/server/services/ddp-streamer/service.ts b/ee/server/services/ddp-streamer/service.ts new file mode 100755 index 00000000000..2dcaaadea5e --- /dev/null +++ b/ee/server/services/ddp-streamer/service.ts @@ -0,0 +1,6 @@ +import '../../broker'; + +import { api } from '../../../../server/sdk/api'; +import { DDPStreamer } from './DDPStreamer'; + +api.registerService(new DDPStreamer()); diff --git a/ee/server/services/ddp-streamer/streams/index.ts b/ee/server/services/ddp-streamer/streams/index.ts new file mode 100644 index 00000000000..a34fd2757ee --- /dev/null +++ b/ee/server/services/ddp-streamer/streams/index.ts @@ -0,0 +1,25 @@ +import { Stream } from '../Streamer'; +import { NotificationsModule } from '../../../../../server/modules/notifications/notifications.module'; +import { ISubscription } from '../../../../../definition/ISubscription'; +import { IRoom } from '../../../../../definition/IRoom'; +import { IUser } from '../../../../../definition/IUser'; +import { ISetting } from '../../../../../definition/ISetting'; +import { Collections, getConnection } from '../../mongo'; +import { RoomsRaw } from '../../../../../app/models/server/raw/Rooms'; +import { SubscriptionsRaw } from '../../../../../app/models/server/raw/Subscriptions'; +import { UsersRaw } from '../../../../../app/models/server/raw/Users'; +import { SettingsRaw } from '../../../../../app/models/server/raw/Settings'; + +const notifications = new NotificationsModule(Stream); + +getConnection() + .then((db) => { + notifications.configure({ + Rooms: new RoomsRaw(db.collection(Collections.Rooms)), + Subscriptions: new SubscriptionsRaw(db.collection(Collections.Subscriptions)), + Users: new UsersRaw(db.collection(Collections.User)), + Settings: new SettingsRaw(db.collection(Collections.Settings)), + }); + }); + +export default notifications; diff --git a/ee/server/services/ddp-streamer/types/IPacket.ts b/ee/server/services/ddp-streamer/types/IPacket.ts new file mode 100644 index 00000000000..5a2ebd086b3 --- /dev/null +++ b/ee/server/services/ddp-streamer/types/IPacket.ts @@ -0,0 +1,9 @@ +export interface IPacket { + name: string; + id: string; + method: string; + msg: string; + version: string; + support: string[]; + params: any[]; +} diff --git a/ee/server/services/ddp-streamer/types/ws.d.ts b/ee/server/services/ddp-streamer/types/ws.d.ts new file mode 100644 index 00000000000..b71218b7214 --- /dev/null +++ b/ee/server/services/ddp-streamer/types/ws.d.ts @@ -0,0 +1,18 @@ +/* eslint-disable no-redeclare */ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import 'ws'; + +type FrameOptions = { + opcode: number; // The opcode + readOnly: boolean; // Specifies whether `data` can be modified + fin: boolean; // Specifies whether or not to set the FIN bit + mask: boolean; // Specifies whether or not to mask `data` + rsv1: boolean; // Specifies whether or not to set the RSV1 bit +} + +declare module 'ws' { + class Sender { + static frame(data: Buffer, options: FrameOptions): Buffer[]; + } +} diff --git a/ee/server/services/docker-compose.yml b/ee/server/services/docker-compose.yml new file mode 100644 index 00000000000..a288a705477 --- /dev/null +++ b/ee/server/services/docker-compose.yml @@ -0,0 +1,167 @@ +version: '3.1' + +services: + authorization-service: + container_name: authorization-service + build: + context: . + args: + SERVICE: authorization + image: rocketchat/authorization-service:latest + env_file: .config/services/service.env + depends_on: + - nats + + account-service: + container_name: account-service + build: + context: . + args: + SERVICE: account + image: rocketchat/account-service:latest + env_file: .config/services/service.env + depends_on: + - nats + + presence-service: + container_name: presence-service + build: + context: . + args: + SERVICE: presence + image: rocketchat/presence-service:latest + env_file: .config/services/service.env + depends_on: + - nats + + ddp-streamer-service: + container_name: ddp-streamer-service + build: + context: . + args: + SERVICE: ddp-streamer + image: rocketchat/ddp-streamer-service:latest + env_file: .config/services/service.env + depends_on: + - nats + - traefik + labels: + traefik.enable: true + traefik.http.services.ddp-streamer-service.loadbalancer.server.port: 3000 + traefik.http.routers.ddp-streamer-service.service: ddp-streamer-service + + stream-hub-service: + container_name: stream-hub-service + build: + context: . + args: + SERVICE: stream-hub + image: rocketchat/stream-hub-service:latest + env_file: .config/services/service.env + depends_on: + - nats + + nats: + image: nats + ports: + - "4222:4222" + + # tracing container for all tracing reporting + jaeger: + image: jaegertracing/all-in-one:latest + container_name: jaeger + ports: + - "16686:16686" + - "5775:5775/udp" + - "6831:6831/udp" + - "6832:6832/udp" + - "5778:5778" + - "14268:14268" + - "9411:9411" + depends_on: + - traefik + # environment: + # - QUERY_BASE_PATH=/jaeger + labels: + traefik.enable: true + traefik.http.routers.jaeger.rule: Host(`jaeger.${HOST_NAME}`) + traefik.http.routers.jaeger.entrypoints: web + traefik.http.services.jaeger.loadbalancer.server.port: 16686 + # traefik.http.routers.jaeger.service: jaeger + + grafana: + image: grafana/grafana + container_name: grafana + restart: unless-stopped + volumes: + - ./.config/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro + - ./.config/grafana/provisioning/dashboards/provider:/etc/grafana/provisioning/dashboards:ro + - ./.config/grafana/provisioning/dashboards/json-exports:/var/lib/grafana/dashboards:ro + depends_on: + - prometheus + - traefik + labels: + traefik.enable: true + traefik.http.routers.grafana.rule: Host(`grafana.${HOST_NAME}`) + traefik.http.routers.grafana.entrypoints: web + traefik.http.services.grafana.loadbalancer.server.port: 3000 + + prometheus: + image: quay.io/prometheus/prometheus + container_name: prometheus + restart: unless-stopped + command: + - --config.file=/etc/prometheus/prometheus.yml + - '--storage.tsdb.retention.time=12w' + - '--storage.tsdb.path=/prometheus' + volumes: + - ./.config/data/prometheus:/prometheus + - ./.config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + depends_on: + - traefik + ports: + - 9090:9090 + labels: + traefik.enable: true + traefik.http.routers.prometheus.rule: Host(`prometheus.${HOST_NAME}`) + traefik.http.routers.prometheus.entrypoints: web + traefik.http.services.prometheus.loadbalancer.server.port: 9090 + + traefik: + image: traefik + container_name: traefik + command: + - --entrypoints.web.address=:80 + # - --entrypoints.websecure.address=:443 + - --api=true + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --providers.file.directory=/servers/ + ports: + - 80:80 + # - 443:443 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./.config/traefik/servers/:/servers/ + # - ./.config/traefik/acme.json:/acme.json + labels: + traefik.enable: true + # Dashboard + traefik.http.routers.traefik.rule: Host(`traefik.${HOST_NAME}`) + traefik.http.routers.traefik.service: api@internal + traefik.http.routers.traefik.entrypoints: web + + + cadvisor: + image: google/cadvisor + container_name: cadvisor + volumes: + # -/:/rootfs:ro + # -/var/run:/var/run:ro + # -/sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + # -/dev/disk/:/dev/disk:ro + ports: + - 8080:8080 + # --privileged \ + # --device=/dev/kmsg \ diff --git a/ee/server/services/ecosystem.config.js b/ee/server/services/ecosystem.config.js new file mode 100644 index 00000000000..e7da5b8e973 --- /dev/null +++ b/ee/server/services/ecosystem.config.js @@ -0,0 +1,23 @@ +const watch = ['.', '../broker.ts', '../../../server/sdk']; + +module.exports = { + apps: [{ + name: 'authorization', + watch: [...watch, '../../../server/services/authorization'], + }, { + name: 'presence', + }, { + name: 'account', + }, { + name: 'stream-hub', + }, { + name: 'ddp-streamer', + }].map((app) => Object.assign(app, { + script: app.script || `ts-node --files ${ app.name }/service.ts`, + watch: app.watch || ['.', '../broker.ts', '../../../server/sdk', '../../../server/modules'], + instances: 1, + env: { + MOLECULER_LOG_LEVEL: 'info', + }, + })), +}; diff --git a/ee/server/services/mongo.ts b/ee/server/services/mongo.ts new file mode 100644 index 00000000000..840d2b64d79 --- /dev/null +++ b/ee/server/services/mongo.ts @@ -0,0 +1,39 @@ + +import { MongoClient, Db, Collection } from 'mongodb'; + +const { + MONGO_URL = 'mongodb://localhost:27017/rocketchat', +} = process.env; + +const name = /^mongodb:\/\/.*?(?::[0-9]+)?\/([^?]*)/.exec(MONGO_URL)?.[1]; + +export enum Collections { + Subscriptions = 'rocketchat_subscription', + UserSession = 'usersSessions', + User = 'users', + Trash = 'rocketchat_trash', + Messages = 'rocketchat_message', + Rooms = 'rocketchat_room', + Settings = 'rocketchat_settings', +} + +let db: Db; +export async function getConnection(poolSize = 5): Promise { + if (!db) { + const client = new MongoClient(MONGO_URL, { + useUnifiedTopology: true, + useNewUrlParser: true, + poolSize, + }); + + await client.connect(); + db = client.db(name); + } + + return db; +} + +export async function getCollection(name: Collections): Promise> { + await getConnection(); + return db.collection(name); +} diff --git a/ee/server/services/package-lock.json b/ee/server/services/package-lock.json new file mode 100644 index 00000000000..0ca4fac5f30 --- /dev/null +++ b/ee/server/services/package-lock.json @@ -0,0 +1,2354 @@ +{ + "name": "rocketchat-authorization", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@opencensus/core": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.9.tgz", + "integrity": "sha512-31Q4VWtbzXpVUd2m9JS6HEaPjlKvNMOiF7lWKNmXF84yUcgfAFL5re7/hjDmdyQbOp32oGc+RFV78jXIldVz6Q==", + "dev": true, + "requires": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^5.5.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "@opencensus/propagation-b3": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@opencensus/propagation-b3/-/propagation-b3-0.0.8.tgz", + "integrity": "sha512-PffXX2AL8Sh0VHQ52jJC4u3T0H6wDK6N/4bg7xh4ngMYOIi13aR1kzVvX1sVDBgfGwDOkMbl4c54Xm3tlPx/+A==", + "dev": true, + "requires": { + "@opencensus/core": "^0.0.8", + "uuid": "^3.2.1" + }, + "dependencies": { + "@opencensus/core": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.8.tgz", + "integrity": "sha512-yUFT59SFhGMYQgX0PhoTR0LBff2BEhPrD9io1jWfF/VDbakRfs6Pq60rjv0Z7iaTav5gQlttJCX2+VPxFWCuoQ==", + "dev": true, + "requires": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^5.5.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "@pm2/agent": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-1.0.4.tgz", + "integrity": "sha512-cZLwaoLa45FRuetKCcoI3kHnnQ7VMLpZnmVom04MoK0cpY/RxcSarkCHSCu9V+pdARwxx96QrWdrtAJdw97dng==", + "dev": true, + "requires": { + "async": "~3.2.0", + "chalk": "~3.0.0", + "dayjs": "~1.8.24", + "debug": "~4.1.1", + "eventemitter2": "~5.0.1", + "fclone": "~1.0.11", + "nssocket": "0.6.0", + "pm2-axon": "^3.2.0", + "pm2-axon-rpc": "^0.5.0", + "proxy-agent": "~3.1.1", + "semver": "~7.2.0", + "ws": "~7.2.0" + }, + "dependencies": { + "semver": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.2.3.tgz", + "integrity": "sha512-utbW9Z7ZxVvwiIWkdOMLOR9G/NFXh2aRucghkVrEMJWuC++r3lCkBC3LwqBinyHzGMAJxY5tn6VakZGHObq5ig==", + "dev": true + } + } + }, + "@pm2/agent-node": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@pm2/agent-node/-/agent-node-1.1.10.tgz", + "integrity": "sha512-xRcrk7OEwhS3d/227/kKGvxgmbIi6Yyp27FzGlFNermEKhgddmFaRnmd7GRLIsBM/KB28NrwflBZulzk/mma6g==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "eventemitter2": "^5.0.1", + "proxy-agent": "^3.0.3", + "ws": "^6.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "@pm2/io": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@pm2/io/-/io-4.3.5.tgz", + "integrity": "sha512-CY/a6Nw72vrlp/FPx38l4jfEHp4gNEbo8i+WlSJ2cnWO6VE6CKmnC1zb4yQLvdP8f3EuzzoOBZVq6aGN20M82Q==", + "dev": true, + "requires": { + "@opencensus/core": "0.0.9", + "@opencensus/propagation-b3": "0.0.8", + "@pm2/agent-node": "^1.1.10", + "async": "~2.6.1", + "debug": "4.1.1", + "eventemitter2": "^6.3.1", + "require-in-the-middle": "^5.0.0", + "semver": "6.3.0", + "shimmer": "^1.2.0", + "signal-exit": "^3.0.3", + "tslib": "1.9.3" + }, + "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "eventemitter2": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz", + "integrity": "sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + } + } + }, + "@pm2/js-api": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.6.0.tgz", + "integrity": "sha512-ZgM/0yI8s3FRyxP01wI5UzDrVTecS/SmD98z25C9fsHo2Wz3JB1DtS4uIBlPopq2/R5HIQynTUJPDNn4qo1d/Q==", + "dev": true, + "requires": { + "async": "^2.6.3", + "axios": "^0.19.0", + "debug": "~3.2.6", + "eventemitter2": "^6.3.1", + "ws": "^7.0.0" + }, + "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eventemitter2": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz", + "integrity": "sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ==", + "dev": true + } + } + }, + "@pm2/pm2-version-check": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.3.tgz", + "integrity": "sha512-SBuYsh+o35knItbRW97vl5/5nEc5c5DYP7PxjyPLOfmm9bMaDsVeATXjXMBy6+KLlyrYWHZxGbfXe003NnHClg==", + "dev": true, + "requires": { + "debug": "^4.1.1" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/ejson": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/ejson/-/ejson-2.1.2.tgz", + "integrity": "sha1-oMuiYNUAYxDch3kFRj2D1fPN8RI=", + "dev": true + }, + "@types/node": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz", + "integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ==", + "dev": true + }, + "@types/ws": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz", + "integrity": "sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha1-at+NWKdPNh6CwfqNOJwHnhOfxH0=", + "dev": true + }, + "amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha1-p48cmJlQh602GSpBKY5NtJ49/EU=", + "dev": true, + "requires": { + "amp": "0.3.1" + } + }, + "ansi-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz", + "integrity": "sha1-PnXAN0dSF1RO12Oo21cJ+prlv5o=" + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + } + } + }, + "args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "requires": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "ast-types": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.1.tgz", + "integrity": "sha512-pfSiukbt23P1qMhNnsozLzhMLBs7EEeXqPyvPmnuZM+RMfwfqwDbSVKYflgGuVI7/VehR4oMks0igzdNAg4VeQ==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "dev": true, + "requires": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + } + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "dev": true, + "requires": { + "follow-redirects": "1.5.10" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-4.0.1.tgz", + "integrity": "sha512-hSIZHkUxIDS5zA2o00Kf2O5RfVbQ888n54xQoF/eIaquU4uaLxK8vhhBdktd0B3n2MjkcAWzv4mnhogykBKOUQ==", + "requires": { + "node-addon-api": "^2.0.0", + "node-pre-gyp": "0.14.0" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha1-+WLWh+wsNpVwrnGvhDJW5tDKESk=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "bson": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", + "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "bufrw": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bufrw/-/bufrw-1.3.0.tgz", + "integrity": "sha512-jzQnSbdJqhIltU9O5KUiTtljP9ccw2u5ix59McQy4pV2xGhVLhRZIndY8GIrgh5HjXa6+QJ9AQhOd2QWQizJFQ==", + "requires": { + "ansi-color": "^0.2.1", + "error": "^7.0.0", + "hexer": "^1.5.0", + "xtend": "^4.0.0" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "charm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz", + "integrity": "sha1-BsIe7RobBq62dVPNxT4jJ0usIpY=", + "dev": true + }, + "chokidar": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "cli-tableau": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", + "integrity": "sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==", + "dev": true, + "requires": { + "chalk": "3.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "dev": true, + "requires": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cron": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz", + "integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==", + "dev": true, + "requires": { + "moment-timezone": "^0.5.x" + } + }, + "data-uri-to-buffer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", + "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==", + "dev": true + }, + "dayjs": { + "version": "1.8.35", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.35.tgz", + "integrity": "sha512-isAbIEenO4ilm6f8cpqvgjZCsuerDAz2Kb7ri201AiNn58aqXuaLJEnCtfIMdCvERZHNGRY5lDMTr/jdAnKSWQ==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "degenerator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", + "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=", + "dev": true, + "requires": { + "ast-types": "0.x.x", + "escodegen": "1.x.x", + "esprima": "3.x.x" + } + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "ejson": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ejson/-/ejson-2.2.0.tgz", + "integrity": "sha512-kWa0AKAxDhmr4t6c4pgQqk6yL52/M67xOMh60HRnAeydzo5QIxOitN5bE1+e0rbdnxfly7FTB9e2Ny0ypLMbag==" + }, + "emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "dev": true, + "requires": { + "shimmer": "^1.2.0" + } + }, + "enquirer": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.5.tgz", + "integrity": "sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA==", + "dev": true, + "requires": { + "ansi-colors": "^3.2.1" + } + }, + "error": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", + "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", + "requires": { + "string-template": "~0.2.1", + "xtend": "~4.0.0" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escape-regexp": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/escape-regexp/-/escape-regexp-0.0.1.tgz", + "integrity": "sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "eventemitter2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", + "integrity": "sha1-YZegldX7a1folC9v1+qtY6CclFI=", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastest-validator": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/fastest-validator/-/fastest-validator-1.7.0.tgz", + "integrity": "sha512-g8G9gNYHVH2EdsIOAnQ60xTeRtf+yitBudR1/5ZpGCIUyXpYv4H9oVT4mwocUOAi/JH9X3kC/R3P6tiukItbvg==" + }, + "fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha1-EOhdo4v+p/xZk0HClu4ddyZu5kA=", + "dev": true + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fn-args": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fn-args/-/fn-args-5.0.0.tgz", + "integrity": "sha512-CtbfI3oFFc3nbdIoHycrfbrxiGgxXBXXuyOl49h47JawM1mYrqpiRqnH5CB2mBatdXvHHOUO6a+RiAuuvKt0lw==" + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dev": true, + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "dev": true, + "requires": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "get-uri": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", + "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", + "dev": true, + "requires": { + "data-uri-to-buffer": "1", + "debug": "2", + "extend": "~3.0.2", + "file-uri-to-path": "1", + "ftp": "~0.3.10", + "readable-stream": "2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "hexer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/hexer/-/hexer-1.5.0.tgz", + "integrity": "sha1-uGzoCFmOip0YksVx887dhvyfBlM=", + "requires": { + "ansi-color": "^0.2.1", + "minimist": "^1.1.0", + "process": "^0.10.0", + "xtend": "^4.0.0" + } + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dev": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ipaddr.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.0.tgz", + "integrity": "sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "jaeger-client": { + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/jaeger-client/-/jaeger-client-3.18.1.tgz", + "integrity": "sha512-eZLM2U6rJvYo0XbzQYFeMYfp29gQix7SKlmDReorp9hJkUwXZtTyxW81AcKdmFCjLHO5tFysTX+394BnjEnUZg==", + "requires": { + "node-int64": "^0.4.0", + "opentracing": "^0.14.4", + "thriftrw": "^3.5.0", + "uuid": "^3.2.1", + "xorshift": "^0.2.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, + "kleur": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.3.tgz", + "integrity": "sha512-H1tr8QP2PxFTNwAFM74Mui2b6ovcY9FoxJefgrwxY+OCJcq01k5nvhf4M/KnizzrJvLRap5STUy7dgDV35iUBw==" + }, + "lazy": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", + "integrity": "sha1-2qBoIGKCVCwIgojpdcKXwa53tpA=", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, + "long": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", + "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-6.1.1.tgz", + "integrity": "sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==", + "requires": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.0.0" + } + }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is=", + "dev": true + }, + "moleculer": { + "version": "0.14.11", + "resolved": "https://registry.npmjs.org/moleculer/-/moleculer-0.14.11.tgz", + "integrity": "sha512-u8uO5PTMgyW1ZD1rKkIOCwthOfSyR9z1FfcpflGO20IKI9zqJxy5VRQ0BTGfHcrTD/+o/ux5eAFV1i2CwidpxQ==", + "requires": { + "args": "^5.0.1", + "es6-error": "^4.1.1", + "eventemitter2": "^6.4.3", + "fastest-validator": "^1.7.0", + "fn-args": "^5.0.0", + "glob": "^7.1.6", + "ipaddr.js": "^2.0.0", + "kleur": "^4.1.2", + "lodash": "^4.17.20", + "lru-cache": "^6.0.0", + "node-fetch": "^2.6.1" + }, + "dependencies": { + "eventemitter2": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz", + "integrity": "sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "moment": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==", + "dev": true + }, + "moment-timezone": { + "version": "0.5.31", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", + "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", + "dev": true, + "requires": { + "moment": ">= 2.9.0" + } + }, + "mongodb": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.1.tgz", + "integrity": "sha512-uH76Zzr5wPptnjEKJRQnwTsomtFOU/kQEU8a9hKHr2M7y9qVk7Q4Pkv0EQVp88742z9+RwvsdTw6dRjDZCNu1g==", + "requires": { + "bl": "^2.2.0", + "bson": "^1.1.4", + "denque": "^1.4.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "nats": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/nats/-/nats-1.4.12.tgz", + "integrity": "sha512-Jf4qesEF0Ay0D4AMw3OZnKMRTQm+6oZ5q8/m4gpy5bTmiDiK6wCXbZpzEslmezGpE93LV3RojNEG6dpK/mysLQ==", + "requires": { + "nuid": "^1.1.4", + "ts-nkeys": "^1.0.16" + } + }, + "needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "netmask": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=", + "dev": true + }, + "node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + }, + "node-pre-gyp": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", + "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nssocket": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nssocket/-/nssocket-0.6.0.tgz", + "integrity": "sha1-Wflvb/MhVm8zxw99vu7N/cBxVPo=", + "dev": true, + "requires": { + "eventemitter2": "~0.4.14", + "lazy": "~1.0.11" + }, + "dependencies": { + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + } + } + }, + "nuid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/nuid/-/nuid-1.1.4.tgz", + "integrity": "sha512-PXiYyHhGfrq8H4g5HyC8enO1lz6SBe5z6x1yx/JG4tmADzDGJVQy3l1sRf3VtEvPsN8dGn9hRFRwDKWL62x0BA==" + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "opentracing": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.4.tgz", + "integrity": "sha512-nNnZDkUNExBwEpb7LZaeMeQgvrlO8l4bgY/LvGNZCR0xG/dGWqHqjKrAmR5GUoYo0FIz38kxasvA1aevxWs2CA==" + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + }, + "pac-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", + "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", + "dev": true, + "requires": { + "agent-base": "^4.2.0", + "debug": "^4.1.1", + "get-uri": "^2.0.0", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "pac-resolver": "^3.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "^4.0.1" + } + }, + "pac-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", + "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", + "dev": true, + "requires": { + "co": "^4.6.0", + "degenerator": "^1.0.4", + "ip": "^1.1.5", + "netmask": "^1.0.6", + "thunkify": "^2.1.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pidusage": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.18.tgz", + "integrity": "sha512-Y/VfKfh3poHjMEINxU+gJTeVOBjiThQeFAmzR7z56HSNiMx+etl+yBhk42nRPciPYt/VZl8DQLVXNC6P5vH11A==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2" + } + }, + "pm2": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-4.4.1.tgz", + "integrity": "sha512-ece2zqVvSg29tIGdznKqk07IwVaO4mX7zLBMVxbJQMfvxpQUotLLERDW0v/RYMHtzj1bX8MLsDUsmPiIbEszKg==", + "dev": true, + "requires": { + "@pm2/agent": "~1.0.2", + "@pm2/io": "~4.3.5", + "@pm2/js-api": "~0.6.0", + "@pm2/pm2-version-check": "^1.0.3", + "async": "~3.2.0", + "blessed": "0.1.81", + "chalk": "3.0.0", + "chokidar": "^3.3.0", + "cli-tableau": "^2.0.0", + "commander": "2.15.1", + "cron": "1.8.2", + "dayjs": "~1.8.25", + "debug": "4.1.1", + "enquirer": "2.3.5", + "eventemitter2": "5.0.1", + "fclone": "1.0.11", + "mkdirp": "1.0.4", + "needle": "2.4.0", + "pidusage": "2.0.18", + "pm2-axon": "3.3.0", + "pm2-axon-rpc": "0.5.1", + "pm2-deploy": "~1.0.2", + "pm2-multimeter": "^0.1.2", + "promptly": "^2", + "ps-list": "6.3.0", + "semver": "^7.2", + "source-map-support": "0.5.16", + "sprintf-js": "1.1.2", + "systeminformation": "^4.23.3", + "vizion": "0.2.13", + "yamljs": "0.3.0" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "pm2-axon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pm2-axon/-/pm2-axon-3.3.0.tgz", + "integrity": "sha512-dAFlFYRuFbFjX7oAk41zT+dx86EuaFX/TgOp5QpUKRKwxb946IM6ydnoH5sSTkdI2pHSVZ+3Am8n/l0ocr7jdQ==", + "dev": true, + "requires": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "debug": "^3.0", + "escape-regexp": "0.0.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "pm2-axon-rpc": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/pm2-axon-rpc/-/pm2-axon-rpc-0.5.1.tgz", + "integrity": "sha512-hT8gN3/j05895QLXpwg+Ws8PjO4AVID6Uf9StWpud9HB2homjc1KKCcI0vg9BNOt56FmrqKDT1NQgheIz35+sA==", + "dev": true, + "requires": { + "debug": "^3.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "pm2-deploy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-1.0.2.tgz", + "integrity": "sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==", + "dev": true, + "requires": { + "run-series": "^1.1.8", + "tv4": "^1.3.0" + } + }, + "pm2-multimeter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pm2-multimeter/-/pm2-multimeter-0.1.2.tgz", + "integrity": "sha1-Gh5VFT1BoFU0zqI8/oYKuqDrSs4=", + "dev": true, + "requires": { + "charm": "~0.1.1" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/process/-/process-0.10.1.tgz", + "integrity": "sha1-hCRXzFHP7XLcd1r+6vuMYDQ3JyU=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "promptly": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", + "integrity": "sha1-KhP6BjaIoqWYOxYf/wEIoH0m/HQ=", + "dev": true, + "requires": { + "read": "^1.0.4" + } + }, + "proxy-agent": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", + "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", + "dev": true, + "requires": { + "agent-base": "^4.2.0", + "debug": "4", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^3.0.1", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^4.0.1" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "ps-list": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-6.3.0.tgz", + "integrity": "sha512-qau0czUSB0fzSlBOQt0bo+I2v6R+xiQdj78e1BR/Qjfl5OHWJ/urXi8+ilw1eHe+5hSeDI1wrwVTgDp2wst4oA==", + "dev": true + }, + "raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true, + "requires": { + "mute-stream": "~0.0.4" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-in-the-middle": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.0.3.tgz", + "integrity": "sha512-p/ICV8uMlqC4tjOYabLMxAWCIKa0YUQgZZ6KDM0xgXJNgdGQ1WmL2A07TwmrZw+wi6ITUFKzH5v3n+ENEyXVkA==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.12.0" + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "run-series": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.8.tgz", + "integrity": "sha512-+GztYEPRpIsQoCSraWHDBs9WVy4eVME16zhOtDB4H9J4xN0XRhknnmLOl+4gRgZtu8dpp9N/utSPjKH/xmDzXg==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", + "dev": true + }, + "socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "dev": true, + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "dev": true, + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "systeminformation": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-4.27.3.tgz", + "integrity": "sha512-0Nc8AYEK818h7FI+bbe/kj7xXsMD5zOHvO9alUqQH/G4MHXu5tHQfWqC/bzWOk4JtoQPhnyLgxMYncDA2eeSBw==", + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + } + } + }, + "thriftrw": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/thriftrw/-/thriftrw-3.12.0.tgz", + "integrity": "sha512-4YZvR4DPEI41n4Opwr4jmrLGG4hndxr7387kzRFIIzxHQjarPusH4lGXrugvgb7TtPrfZVTpZCVe44/xUxowEw==", + "requires": { + "bufrw": "^1.3.0", + "error": "7.0.2", + "long": "^2.4.0" + } + }, + "thunkify": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", + "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "ts-nkeys": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/ts-nkeys/-/ts-nkeys-1.0.16.tgz", + "integrity": "sha512-1qrhAlavbm36wtW+7NtKOgxpzl+70NTF8xlz9mEhiA5zHMlMxjj3sEVKWm3pGZhHXE0Q3ykjrj+OSRVaYw+Dqg==", + "requires": { + "tweetnacl": "^1.0.3" + } + }, + "ts-node": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM=", + "dev": true + }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + }, + "underscore.string": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", + "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", + "requires": { + "sprintf-js": "^1.0.3", + "util-deprecate": "^1.0.2" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + }, + "vizion": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/vizion/-/vizion-0.2.13.tgz", + "integrity": "sha1-ExTN7is0EW+fWxJIU2+V2/zW718=", + "dev": true, + "requires": { + "async": "1.5" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz", + "integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==" + }, + "xorshift": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xorshift/-/xorshift-0.2.1.tgz", + "integrity": "sha1-/NgiZ+k1HBPw+5xzMH8lMx0pxjo=" + }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/ee/server/services/package.json b/ee/server/services/package.json new file mode 100644 index 00000000000..c3575f1a26c --- /dev/null +++ b/ee/server/services/package.json @@ -0,0 +1,40 @@ +{ + "name": "rocketchat-authorization", + "version": "1.0.0", + "description": "Rocket.Chat Authorization service", + "main": "index.js", + "scripts": { + "dev": "pm2 start ecosystem.config.js", + "pm2": "pm2", + "start": "ts-node index.ts", + "typecheck": "tsc --noEmit --skipLibCheck", + "build": "tsc", + "build-containers": "npm run build && docker-compose build && rm -rf ./dist", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "license": "MIT", + "dependencies": { + "bcrypt": "^4.0.1", + "ejson": "^2.2.0", + "jaeger-client": "^3.18.1", + "mem": "^6.1.0", + "moleculer": "^0.14.10", + "mongodb": "^3.6.1", + "nats": "^1.4.8", + "underscore.string": "^3.3.5", + "uuid": "^7.0.3", + "ws": "^7.2.3" + }, + "devDependencies": { + "@types/ejson": "^2.1.2", + "@types/node": "^14.6.4", + "@types/ws": "^7.2.6", + "pm2": "^4.4.1", + "ts-node": "^9.0.0", + "typescript": "^3.9.7" + } +} diff --git a/ee/server/services/presence/Presence.ts b/ee/server/services/presence/Presence.ts new file mode 100755 index 00000000000..80c942aae48 --- /dev/null +++ b/ee/server/services/presence/Presence.ts @@ -0,0 +1,55 @@ +import { newConnection } from './actions/newConnection'; +import { removeConnection } from './actions/removeConnection'; +import { removeLostConnections } from './actions/removeLostConnections'; +import { setStatus, setConnectionStatus } from './actions/setStatus'; +import { updateUserPresence } from './actions/updateUserPresence'; +import { ServiceClass } from '../../../../server/sdk/types/ServiceClass'; +import { IPresence } from '../../../../server/sdk/types/IPresence'; +import { USER_STATUS } from '../../../../definition/UserStatus'; +import { IBrokerNode } from '../../../../server/sdk/types/IBroker'; + +export class Presence extends ServiceClass implements IPresence { + protected name = 'presence'; + + async onNodeDisconnected({ node }: {node: IBrokerNode}): Promise { + const affectedUsers = await this.removeLostConnections(node.id); + return affectedUsers.forEach((uid) => this.updateUserPresence(uid)); + } + + async started(): Promise { + setTimeout(async () => { + const affectedUsers = await this.removeLostConnections(); + return affectedUsers.forEach((uid) => this.updateUserPresence(uid)); + }, 100); + } + + async newConnection(uid: string, session: string): Promise<{uid: string; connectionId: string} | undefined> { + const result = await newConnection(uid, session, this.context); + await updateUserPresence(uid); + return result; + } + + async removeConnection(uid: string, session: string): Promise<{uid: string; session: string}> { + const result = await removeConnection(uid, session); + await updateUserPresence(uid); + return result; + } + + async removeLostConnections(nodeID?: string): Promise { + return removeLostConnections(nodeID, this.context); + } + + async setStatus(uid: string, status: USER_STATUS, statusText?: string): Promise { + return setStatus(uid, status, statusText); + } + + async setConnectionStatus(uid: string, status: USER_STATUS, session: string): Promise { + const result = await setConnectionStatus(uid, status, session); + await updateUserPresence(uid); + return result; + } + + async updateUserPresence(uid: string): Promise { + return updateUserPresence(uid); + } +} diff --git a/ee/server/services/presence/actions/newConnection.ts b/ee/server/services/presence/actions/newConnection.ts new file mode 100755 index 00000000000..342553bbc75 --- /dev/null +++ b/ee/server/services/presence/actions/newConnection.ts @@ -0,0 +1,46 @@ +import { getCollection, Collections } from '../../mongo'; +import { IServiceContext } from '../../../../../server/sdk/types/ServiceClass'; + +const status = 'online'; + +export async function newConnection(uid: string, session: string, context?: IServiceContext): Promise<{uid: string; connectionId: string} | undefined> { + const instanceId = context?.nodeID; + + if (!instanceId) { + return; + } + + const query = { + _id: uid, + }; + + const now = new Date(); + + + const update = { + $push: { + connections: { + id: session, + instanceId, + status, + _createdAt: now, + _updatedAt: now, + }, + }, + }; + + // if (metadata) { + // update.$set = { + // metadata: metadata + // }; + // connection.metadata = metadata; + // } + + const UserSession = await getCollection(Collections.UserSession); + await UserSession.updateOne(query, update, { upsert: true }); + + return { + uid, + connectionId: session, + }; +} diff --git a/ee/server/services/presence/actions/removeConnection.ts b/ee/server/services/presence/actions/removeConnection.ts new file mode 100755 index 00000000000..1f11aa6add7 --- /dev/null +++ b/ee/server/services/presence/actions/removeConnection.ts @@ -0,0 +1,23 @@ +import { getCollection, Collections } from '../../mongo'; + +export async function removeConnection(uid: string, session: string): Promise<{uid: string; session: string}> { + const query = { + 'connections.id': session, + }; + + const update = { + $pull: { + connections: { + id: session, + }, + }, + }; + + const UserSession = await getCollection(Collections.UserSession); + await UserSession.updateMany(query, update); + + return { + uid, + session, + }; +} diff --git a/ee/server/services/presence/actions/removeLostConnections.ts b/ee/server/services/presence/actions/removeLostConnections.ts new file mode 100755 index 00000000000..ae6a9fbacc6 --- /dev/null +++ b/ee/server/services/presence/actions/removeLostConnections.ts @@ -0,0 +1,68 @@ +import { Collection } from 'mongodb'; + +import { getCollection, Collections } from '../../mongo'; +import { IServiceContext } from '../../../../../server/sdk/types/ServiceClass'; + +async function getAffectedUsers(model: Collection, query: object): Promise { + const list = await model.find<{_id: string}>(query, { projection: { _id: 1 } }).toArray(); + return list.map(({ _id }) => _id); +} + +// TODO: Change this to use find and modify +export async function removeLostConnections(nodeID?: string, context?: IServiceContext): Promise { + const UserSession = await getCollection(Collections.UserSession); + + if (nodeID) { + const query = { + 'connections.instanceId': nodeID, + }; + const update = { + $pull: { + connections: { + instanceId: nodeID, + }, + }, + }; + const affectedUsers = await getAffectedUsers(UserSession, query); + + const { modifiedCount } = await UserSession.updateMany(query, update); + + if (modifiedCount === 0) { + return []; + } + + return affectedUsers; + } + + if (!context) { + return []; + } + + const nodes = await context.broker.nodeList(); + + const ids = nodes.filter((node) => node.available).map(({ id }) => id); + + const affectedUsers = await getAffectedUsers(UserSession, { + 'connections.instanceId': { + $exists: true, + $nin: ids, + }, + }); + + const update = { + $pull: { + connections: { + instanceId: { + $nin: ids, + }, + }, + }, + }; + const { modifiedCount } = await UserSession.updateMany({}, update); + + if (modifiedCount === 0) { + return []; + } + + return affectedUsers; +} diff --git a/ee/server/services/presence/actions/setStatus.ts b/ee/server/services/presence/actions/setStatus.ts new file mode 100755 index 00000000000..0396e28043c --- /dev/null +++ b/ee/server/services/presence/actions/setStatus.ts @@ -0,0 +1,64 @@ +import { processPresenceAndStatus } from '../lib/processConnectionStatus'; +import { getCollection, Collections } from '../../mongo'; +import { IUser } from '../../../../../definition/IUser'; +import { USER_STATUS } from '../../../../../definition/UserStatus'; +import { IUserSession } from '../../../../../definition/IUserSession'; +import { api } from '../../../../../server/sdk/api'; + +export async function setStatus(uid: string, statusDefault: USER_STATUS, statusText?: string): Promise { + const query = { _id: uid }; + + const UserSession = await getCollection(Collections.UserSession); + const userSessions = await UserSession.findOne(query) || { connections: [] }; + + const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); + + const update = { + statusDefault, + status, + statusConnection, + ...typeof statusText !== 'undefined' ? { + // TODO logic duplicated from Rocket.Chat core + statusText: String(statusText || '').trim().substr(0, 120), + } : {}, + }; + + const User = await getCollection(Collections.User); + const result = await User.updateOne( + query, + { + $set: update, + }, + ); + + if (result.modifiedCount > 0) { + const user = await User.findOne(query, { projection: { username: 1 } }); + api.broadcast('userpresence', { + action: 'updated', + user: { _id: uid, username: user?.username, status, statusText }, + }); + } + + return !!result.modifiedCount; +} + +export async function setConnectionStatus(uid: string, status: USER_STATUS, session: string): Promise { + const query = { + _id: uid, + 'connections.id': session, + }; + + const now = new Date(); + + const update = { + $set: { + 'connections.$.status': status, + 'connections.$._updatedAt': now, + }, + }; + + const UserSession = await getCollection(Collections.UserSession); + const result = await UserSession.updateOne(query, update); + + return !!result.modifiedCount; +} diff --git a/ee/server/services/presence/actions/updateUserPresence.ts b/ee/server/services/presence/actions/updateUserPresence.ts new file mode 100755 index 00000000000..805de4742dd --- /dev/null +++ b/ee/server/services/presence/actions/updateUserPresence.ts @@ -0,0 +1,39 @@ +// import { afterAll } from '../hooks'; +import { processPresenceAndStatus } from '../lib/processConnectionStatus'; +import { getCollection, Collections } from '../../mongo'; +import { IUserSession } from '../../../../../definition/IUserSession'; +import { IUser } from '../../../../../definition/IUser'; +import { USER_STATUS } from '../../../../../definition/UserStatus'; +import { api } from '../../../../../server/sdk/api'; + +const projection = { + projection: { + username: 1, + statusDefault: 1, + statusText: 1, + }, +}; + +export async function updateUserPresence(uid: string): Promise { + const query = { _id: uid }; + + const UserSession = await getCollection(Collections.UserSession); + const User = await getCollection(Collections.User); + + const user = await User.findOne(query, projection); + if (!user) { return; } + + const userSessions = await UserSession.findOne(query) || { connections: [] }; + const { statusDefault = USER_STATUS.OFFLINE } = user; + const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); + const result = await User.updateOne(query, { + $set: { status, statusConnection }, + }); + + if (result.modifiedCount > 0) { + api.broadcast('userpresence', { + action: 'updated', + user: { _id: uid, username: user.username, status, statusText: user.statusText }, + }); + } +} diff --git a/ee/server/services/presence/lib/processConnectionStatus.ts b/ee/server/services/presence/lib/processConnectionStatus.ts new file mode 100644 index 00000000000..88e7b2c9111 --- /dev/null +++ b/ee/server/services/presence/lib/processConnectionStatus.ts @@ -0,0 +1,25 @@ +import { IUserSessionConnection } from '../../../../../definition/IUserSession'; +import { USER_STATUS } from '../../../../../definition/UserStatus'; + +export const processConnectionStatus = (current: USER_STATUS, status: USER_STATUS): USER_STATUS => { + if (status === USER_STATUS.ONLINE) { + return USER_STATUS.ONLINE; + } + if (status !== USER_STATUS.OFFLINE) { + return status; + } + return current; +}; + +export const processStatus = (statusConnection: USER_STATUS, statusDefault: USER_STATUS): USER_STATUS => ( + statusConnection !== USER_STATUS.OFFLINE ? statusDefault : statusConnection +); + +export const processPresenceAndStatus = (userSessions: IUserSessionConnection[] = [], statusDefault = USER_STATUS.ONLINE): {status: USER_STATUS; statusConnection: USER_STATUS} => { + const statusConnection = userSessions.map((s) => s.status).reduce(processConnectionStatus, USER_STATUS.OFFLINE); + const status = processStatus(statusConnection, statusDefault); + return { + status, + statusConnection, + }; +}; diff --git a/ee/server/services/presence/service.ts b/ee/server/services/presence/service.ts new file mode 100755 index 00000000000..f6ae2f810ef --- /dev/null +++ b/ee/server/services/presence/service.ts @@ -0,0 +1,6 @@ +import '../../broker'; + +import { api } from '../../../../server/sdk/api'; +import { Presence } from './Presence'; + +api.registerService(new Presence()); diff --git a/ee/server/services/stream-hub/StreamHub.ts b/ee/server/services/stream-hub/StreamHub.ts new file mode 100755 index 00000000000..b364fe1c182 --- /dev/null +++ b/ee/server/services/stream-hub/StreamHub.ts @@ -0,0 +1,114 @@ +import { getConnection } from '../mongo'; +import { ServiceClass, IServiceClass } from '../../../../server/sdk/types/ServiceClass'; +import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; +import { MessagesRaw } from '../../../../app/models/server/raw/Messages'; +import { UsersRaw } from '../../../../app/models/server/raw/Users'; +import { SubscriptionsRaw } from '../../../../app/models/server/raw/Subscriptions'; +import { SettingsRaw } from '../../../../app/models/server/raw/Settings'; +import { RolesRaw } from '../../../../app/models/server/raw/Roles'; +import { LivechatInquiryRaw } from '../../../../app/models/server/raw/LivechatInquiry'; +import { UsersSessionsRaw } from '../../../../app/models/server/raw/UsersSessions'; +import { RoomsRaw } from '../../../../app/models/server/raw/Rooms'; +import { LoginServiceConfigurationRaw } from '../../../../app/models/server/raw/LoginServiceConfiguration'; +import { InstanceStatusRaw } from '../../../../app/models/server/raw/InstanceStatus'; +import { IntegrationHistoryRaw } from '../../../../app/models/server/raw/IntegrationHistory'; +import { LivechatDepartmentAgentsRaw } from '../../../app/models/server/raw/LivechatDepartmentAgents'; +import { IntegrationsRaw } from '../../../../app/models/server/raw/Integrations'; +import { PermissionsRaw } from '../../../../app/models/server/raw/Permissions'; + +export class StreamHub extends ServiceClass implements IServiceClass { + protected name = 'hub'; + + async created(): Promise { + const db = await getConnection(15); + + const Trash = db.collection('rocketchat__trash'); + + const UsersCol = db.collection('users'); + + const Rooms = new RoomsRaw(db.collection('rocketchat_room'), Trash); + const Settings = new SettingsRaw(db.collection('rocketchat_settings'), Trash); + const Users = new UsersRaw(UsersCol, Trash); + const UsersSessions = new UsersSessionsRaw(db.collection('usersSessions'), Trash); + const Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription'), Trash); + const LivechatInquiry = new LivechatInquiryRaw(db.collection('rocketchat_livechat_inquiry'), Trash); + const LivechatDepartmentAgents = new LivechatDepartmentAgentsRaw(db.collection('rocketchat_livechat_department_agents'), Trash); + const Messages = new MessagesRaw(db.collection('rocketchat_message'), Trash); + const Permissions = new PermissionsRaw(db.collection('rocketchat_permissions'), Trash); + const Roles = new RolesRaw(db.collection('rocketchat_roles'), Trash, { Users, Subscriptions }); + const LoginServiceConfiguration = new LoginServiceConfigurationRaw(db.collection('meteor_accounts_loginServiceConfiguration'), Trash); + const InstanceStatus = new InstanceStatusRaw(db.collection('instances'), Trash); + const IntegrationHistory = new IntegrationHistoryRaw(db.collection('rocketchat_integration_history'), Trash); + const Integrations = new IntegrationsRaw(db.collection('rocketchat_integrations'), Trash); + + const models = { + Messages, + Users, + UsersSessions, + Subscriptions, + Permissions, + LivechatInquiry, + LivechatDepartmentAgents, + Settings, + Roles, + Rooms, + LoginServiceConfiguration, + InstanceStatus, + IntegrationHistory, + Integrations, + }; + + initWatchers(models, (model, fn) => { + model.col.watch([]).on('change', (event) => { + switch (event.operationType) { + case 'insert': + fn({ + action: 'insert', + clientAction: 'inserted', + id: event.documentKey._id, + data: event.fullDocument, + }); + + break; + case 'update': + const diff: Record = {}; + + if (event.updateDescription.updatedFields) { + for (const key in event.updateDescription.updatedFields) { + if (event.updateDescription.updatedFields.hasOwnProperty(key)) { + diff[key] = event.updateDescription.updatedFields[key]; + } + } + } + + const unset: Record = {}; + if (event.updateDescription.removedFields) { + for (const key in event.updateDescription.removedFields) { + if (event.updateDescription.removedFields.hasOwnProperty(key)) { + diff[key] = undefined; + unset[key] = 1; + } + } + } + + fn({ + action: 'update', + clientAction: 'updated', + id: event.documentKey._id, + diff, + unset, + }); + + break; + case 'delete': + fn({ + action: 'remove', + clientAction: 'removed', + id: event.documentKey._id, + }); + break; + } + }); + }); + } +} diff --git a/ee/server/services/stream-hub/service.ts b/ee/server/services/stream-hub/service.ts new file mode 100755 index 00000000000..ee9f9cdbf05 --- /dev/null +++ b/ee/server/services/stream-hub/service.ts @@ -0,0 +1,6 @@ +import '../../broker'; + +import { api } from '../../../../server/sdk/api'; +import { StreamHub } from './StreamHub'; + +api.registerService(new StreamHub()); diff --git a/ee/server/services/tsconfig.json b/ee/server/services/tsconfig.json new file mode 100644 index 00000000000..c9da0893969 --- /dev/null +++ b/ee/server/services/tsconfig.json @@ -0,0 +1,49 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "es2018", + "lib": ["esnext", "dom"], + "types" : ["node"], + + "allowJs": true, + "checkJs": false, + "jsx": "react", + // "incremental": true, + // "noEmit": true, + + /* Strict Type-Checking Options */ + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + + /* Module Resolution Options */ + "baseUrl": ".", + "paths": { + /* Support absolute /imports/* with a leading '/' */ + "/*": ["*"] + }, + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "preserveSymlinks": true, + + "outDir": "./dist", + + // "sourceMap": true, + // "declaration": true, + // "removeComments": false, + // "emitDecoratorMetadata": true, + // "experimentalDecorators": true, + }, + "exclude": [ + "./dist", + "./ecosystem.config.js" + ] +} diff --git a/imports/users-presence/server/activeUsers.js b/imports/users-presence/server/activeUsers.js index 53c54300908..f3225b933b8 100644 --- a/imports/users-presence/server/activeUsers.js +++ b/imports/users-presence/server/activeUsers.js @@ -1,7 +1,7 @@ import { UserPresenceEvents } from 'meteor/konecty:user-presence'; -import { Notifications } from '../../../app/notifications/server'; import { settings } from '../../../app/settings/server'; +import { api } from '../../../server/sdk/api'; // mirror of object in /imports/startup/client/listenActiveUsers.js - keep updated export const STATUS_MAP = { @@ -20,12 +20,7 @@ export const setUserStatus = (user, status/* , statusConnection*/) => { // since this callback can be called by only one instance in the cluster // we need to broadcast the change to all instances - Notifications.notifyLogged('user-status', [ - _id, - username, - STATUS_MAP[status], - statusText, - ]); + api.broadcast('userpresence', { user: { status, _id, username, statusText } }); // remove username }; let TroubleshootDisablePresenceBroadcast; diff --git a/package-lock.json b/package-lock.json index 24b92d95d79..8f29596875b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8237,6 +8237,12 @@ "integrity": "sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==", "dev": true }, + "@types/ejson": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/ejson/-/ejson-2.1.2.tgz", + "integrity": "sha1-oMuiYNUAYxDch3kFRj2D1fPN8RI=", + "dev": true + }, "@types/elliptic": { "version": "6.4.12", "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.12.tgz", @@ -8442,9 +8448,9 @@ } }, "@types/node": { - "version": "9.6.40", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.40.tgz", - "integrity": "sha512-M3HHoXXndsho/sTbQML2BJr7/uwNhMg8P0D4lb+UsM65JQZx268faiz9hKpY4FpocWqpwlLwa8vevw8hLtKjOw==" + "version": "10.12.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.14.tgz", + "integrity": "sha512-0rVcFRhM93kRGAU88ASCjX9Y3FWDCh+33G5Z5evpKOea4xcpLqDGwmo64+DjgaSezTN5j9KdnUzvxhOw7fNciQ==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -9684,6 +9690,55 @@ } } }, + "args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "requires": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -16006,6 +16061,11 @@ "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.9.tgz", "integrity": "sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ==" }, + "ejson": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ejson/-/ejson-2.2.0.tgz", + "integrity": "sha512-kWa0AKAxDhmr4t6c4pgQqk6yL52/M67xOMh60HRnAeydzo5QIxOitN5bE1+e0rbdnxfly7FTB9e2Ny0ypLMbag==" + }, "electron-to-chromium": { "version": "1.3.540", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.540.tgz", @@ -16279,6 +16339,11 @@ "integrity": "sha512-7SwlpL+2JpymWTt8sNLuC2zdhhc+wrfe5cMPI2j0o6WsPdfAiPwmFy2f0AocPB4RQVBOZ9kNTgi5YF7TdhkvEg==", "dev": true }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -16868,8 +16933,7 @@ "eventemitter2": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz", - "integrity": "sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ==", - "dev": true + "integrity": "sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ==" }, "eventemitter3": { "version": "3.1.2", @@ -17294,6 +17358,11 @@ "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" }, + "fastest-validator": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/fastest-validator/-/fastest-validator-1.7.0.tgz", + "integrity": "sha512-g8G9gNYHVH2EdsIOAnQ60xTeRtf+yitBudR1/5ZpGCIUyXpYv4H9oVT4mwocUOAi/JH9X3kC/R3P6tiukItbvg==" + }, "fastq": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", @@ -17699,6 +17768,11 @@ "resolved": "https://registry.npmjs.org/flushwritable/-/flushwritable-1.0.0.tgz", "integrity": "sha1-PjKNj95BKtR+c44751C00pAENJg=" }, + "fn-args": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fn-args/-/fn-args-5.0.0.tgz", + "integrity": "sha512-CtbfI3oFFc3nbdIoHycrfbrxiGgxXBXXuyOl49h47JawM1mYrqpiRqnH5CB2mBatdXvHHOUO6a+RiAuuvKt0lw==" + }, "focus-lock": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.6.8.tgz", @@ -22179,6 +22253,11 @@ "graceful-fs": "^4.1.9" } }, + "kleur": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.1.tgz", + "integrity": "sha512-BsNhM6T/yTWFG580CRnYhT3LfUuPK7Hwrm+W2H0G8lK/nogalP5Nsrh/cHjxVVkzl0sFm7z8b8rNcZCfKxeoxA==" + }, "known-css-properties": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.19.0.tgz", @@ -24873,6 +24952,62 @@ "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", "dev": true }, + "moleculer": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/moleculer/-/moleculer-0.14.10.tgz", + "integrity": "sha512-3HS8cBAIzhCX8x+R/9yS31NlNRp9u9iyN3lWZbmVv1MrlXqkqPCZXLNNCyaB+iSC6Ncuh97MCsv0dUosA7qpKA==", + "requires": { + "args": "^5.0.1", + "es6-error": "^4.1.1", + "eventemitter2": "^6.4.3", + "fastest-validator": "^1.6.1", + "fn-args": "^5.0.0", + "glob": "^7.1.6", + "ipaddr.js": "^2.0.0", + "kleur": "^4.1.1", + "lodash": "^4.17.20", + "lru-cache": "^6.0.0", + "node-fetch": "^2.6.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "ipaddr.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.0.tgz", + "integrity": "sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w==" + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "moment": { "version": "2.27.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", @@ -25236,6 +25371,11 @@ } } }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -25294,6 +25434,15 @@ "integrity": "sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==", "optional": true }, + "nats": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/nats/-/nats-1.4.12.tgz", + "integrity": "sha512-Jf4qesEF0Ay0D4AMw3OZnKMRTQm+6oZ5q8/m4gpy5bTmiDiK6wCXbZpzEslmezGpE93LV3RojNEG6dpK/mysLQ==", + "requires": { + "nuid": "^1.1.4", + "ts-nkeys": "^1.0.16" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -25773,6 +25922,11 @@ "boolbase": "~1.0.0" } }, + "nuid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/nuid/-/nuid-1.1.4.tgz", + "integrity": "sha512-PXiYyHhGfrq8H4g5HyC8enO1lz6SBe5z6x1yx/JG4tmADzDGJVQy3l1sRf3VtEvPsN8dGn9hRFRwDKWL62x0BA==" + }, "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", @@ -33095,6 +33249,21 @@ } } }, + "ts-nkeys": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/ts-nkeys/-/ts-nkeys-1.0.16.tgz", + "integrity": "sha512-1qrhAlavbm36wtW+7NtKOgxpzl+70NTF8xlz9mEhiA5zHMlMxjj3sEVKWm3pGZhHXE0Q3ykjrj+OSRVaYw+Dqg==", + "requires": { + "tweetnacl": "^1.0.3" + }, + "dependencies": { + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + } + } + }, "ts-node": { "version": "8.10.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", diff --git a/package.json b/package.json index ccd60ef48fc..ff8a09120ba 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,13 @@ "@types/chai": "^4.2.12", "@types/chai-spies": "^1.0.1", "@types/clipboard": "^2.0.1", + "@types/ejson": "^2.1.2", "@types/meteor": "^1.4.49", "@types/mocha": "^8.0.3", "@types/mock-require": "^2.0.0", "@types/moment-timezone": "^0.5.30", "@types/mongodb": "^3.5.26", + "@types/node": "^10.12.14", "@types/react-dom": "^16.9.8", "@types/semver": "^7.3.3", "@types/toastr": "^2.1.38", @@ -178,6 +180,7 @@ "core-js": "^2.6.11", "cors": "^2.8.5", "csv-parse": "^4.12.0", + "ejson": "^2.2.0", "emailreplyparser": "^0.0.5", "emojione": "^4.5.0", "eslint-plugin-import": "^2.22.0", @@ -217,9 +220,11 @@ "mime-db": "^1.44.0", "mime-type": "^3.1.0", "mkdirp": "^0.5.5", + "moleculer": "^0.14.10", "moment": "^2.27.0", "moment-timezone": "^0.5.31", "mongodb": "^3.6.0", + "nats": "^1.4.8", "node-dogstatsd": "^0.0.7", "node-gcm": "0.14.4", "node-rsa": "^1.1.1", diff --git a/server/lib/markRoomAsRead.ts b/server/lib/markRoomAsRead.ts index 99c89c82eac..f419631cbae 100644 --- a/server/lib/markRoomAsRead.ts +++ b/server/lib/markRoomAsRead.ts @@ -11,7 +11,7 @@ export async function markRoomAsRead(rid: string, uid: string): Promise { } // do not mark room as read if there are still unread threads - const alert = sub.alert && sub.tunread?.length > 0; + const alert = sub.alert && sub.tunread && sub.tunread.length > 0; await Subscriptions.setAsReadByRoomIdAndUserId(rid, uid, alert); diff --git a/server/main.d.ts b/server/main.d.ts index b9bf69c0e03..5e2c3fb103d 100644 --- a/server/main.d.ts +++ b/server/main.d.ts @@ -1,4 +1,7 @@ import { EJSON } from 'meteor/ejson'; +import { Db, Collection } from 'mongodb'; + +import { IStreamerConstructor } from './modules/streamer/streamer.module'; /* eslint-disable @typescript-eslint/interface-name-prefix */ declare module 'meteor/random' { @@ -37,8 +40,12 @@ declare module 'meteor/meteor' { details?: string | undefined | Record; } + const Streamer: IStreamerConstructor; + const server: any; + const runAsUser: (userId: string, scope: Function) => any; + interface MethodThisType { twoFactorChecked: boolean | undefined; } @@ -81,3 +88,47 @@ declare module 'meteor/littledata:synced-cron' { function remove(name: string): string; } } + +declare module 'meteor/mongo' { + interface RemoteCollectionDriver { + mongo: MongoConnection; + } + interface OplogHandle { + stop(): void; + onOplogEntry(trigger: Record, callback: Function): void; + onSkippedEntries(callback: Function): void; + waitUntilCaughtUp(): void; + _defineTooFarBehind(value: number): void; + } + interface MongoConnection { + db: Db; + _oplogHandle: OplogHandle; + rawCollection(name: string): Collection; + } + + namespace MongoInternals { + function defaultRemoteCollectionDriver(): RemoteCollectionDriver; + + class ConnectionClass {} + + function Connection(): ConnectionClass; + } +} + +declare module 'async_hooks' { + export class AsyncLocalStorage { + disable(): void; + + getStore(): T | undefined; + + run(store: T, callback: (...args: any[]) => void, ...args: any[]): void; + + exit(callback: (...args: any[]) => void, ...args: any[]): void; + + runSyncAndReturn(store: T, callback: (...args: any[]) => R, ...args: any[]): R; + + exitSyncAndReturn(callback: (...args: any[]) => R, ...args: any[]): R; + + enterWith(store: T): void; + } +} diff --git a/server/main.js b/server/main.js index 8b62fe259bd..86e6f4215d6 100644 --- a/server/main.js +++ b/server/main.js @@ -1,8 +1,11 @@ +import '../ee/server/broker'; import './importPackages'; import '../imports/startup/server'; import '../lib/RegExp'; +import './services/startup'; + import '../ee/server'; import './lib/pushConfig'; import './startup/migrations'; @@ -72,6 +75,4 @@ import './publications/settings'; import './publications/spotlight'; import './publications/subscription'; import './routes/avatar'; -import './stream/messages'; -import './stream/rooms'; import './stream/streamBroadcast'; diff --git a/server/methods/addRoomLeader.js b/server/methods/addRoomLeader.js index 548abf36a98..b602c75f984 100644 --- a/server/methods/addRoomLeader.js +++ b/server/methods/addRoomLeader.js @@ -4,7 +4,7 @@ import { check } from 'meteor/check'; import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; -import { Notifications } from '../../app/notifications'; +import { api } from '../sdk/api'; Meteor.methods({ addRoomLeader(rid, userId) { @@ -58,7 +58,7 @@ Meteor.methods({ }); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'added', _id: 'leader', u: { diff --git a/server/methods/addRoomModerator.js b/server/methods/addRoomModerator.js index 04a593768e2..8b430cb667a 100644 --- a/server/methods/addRoomModerator.js +++ b/server/methods/addRoomModerator.js @@ -4,7 +4,7 @@ import { check } from 'meteor/check'; import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; -import { Notifications } from '../../app/notifications'; +import { api } from '../sdk/api'; Meteor.methods({ addRoomModerator(rid, userId) { @@ -58,7 +58,7 @@ Meteor.methods({ }); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'added', _id: 'moderator', u: { diff --git a/server/methods/addRoomOwner.js b/server/methods/addRoomOwner.js index b6e0080f3a1..91473caef90 100644 --- a/server/methods/addRoomOwner.js +++ b/server/methods/addRoomOwner.js @@ -4,7 +4,7 @@ import { check } from 'meteor/check'; import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; -import { Notifications } from '../../app/notifications'; +import { api } from '../sdk/api'; Meteor.methods({ addRoomOwner(rid, userId) { @@ -58,7 +58,7 @@ Meteor.methods({ }); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'added', _id: 'owner', u: { diff --git a/server/methods/removeRoomLeader.js b/server/methods/removeRoomLeader.js index b0576c3d207..b88a6a7729d 100644 --- a/server/methods/removeRoomLeader.js +++ b/server/methods/removeRoomLeader.js @@ -4,7 +4,7 @@ import { check } from 'meteor/check'; import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; -import { Notifications } from '../../app/notifications'; +import { api } from '../sdk/api'; Meteor.methods({ removeRoomLeader(rid, userId) { @@ -58,7 +58,7 @@ Meteor.methods({ }); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'removed', _id: 'leader', u: { diff --git a/server/methods/removeRoomModerator.js b/server/methods/removeRoomModerator.js index ae00b1aeb2f..ef86a9c176b 100644 --- a/server/methods/removeRoomModerator.js +++ b/server/methods/removeRoomModerator.js @@ -4,7 +4,7 @@ import { check } from 'meteor/check'; import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; -import { Notifications } from '../../app/notifications'; +import { api } from '../sdk/api'; Meteor.methods({ removeRoomModerator(rid, userId) { @@ -58,7 +58,7 @@ Meteor.methods({ }); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'removed', _id: 'moderator', u: { diff --git a/server/methods/removeRoomOwner.js b/server/methods/removeRoomOwner.js index 2dbd82e6cb8..b4dcf08ca26 100644 --- a/server/methods/removeRoomOwner.js +++ b/server/methods/removeRoomOwner.js @@ -4,7 +4,7 @@ import { check } from 'meteor/check'; import { hasPermission, getUsersInRole } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; -import { Notifications } from '../../app/notifications'; +import { api } from '../sdk/api'; Meteor.methods({ removeRoomOwner(rid, userId) { @@ -65,7 +65,7 @@ Meteor.methods({ }); if (settings.get('UI_DisplayRoles')) { - Notifications.notifyLogged('roles-change', { + api.broadcast('user.roleUpdate', { type: 'removed', _id: 'owner', u: { diff --git a/server/methods/resetAvatar.js b/server/methods/resetAvatar.js index 48a6c3b8e3c..3ac38003a72 100644 --- a/server/methods/resetAvatar.js +++ b/server/methods/resetAvatar.js @@ -4,8 +4,8 @@ import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { FileUpload } from '../../app/file-upload'; import { Users } from '../../app/models/server'; import { settings } from '../../app/settings'; -import { Notifications } from '../../app/notifications'; import { hasPermission } from '../../app/authorization/server'; +import { api } from '../sdk/api'; Meteor.methods({ resetAvatar(userId) { @@ -43,10 +43,7 @@ Meteor.methods({ FileUpload.getStore('Avatars').deleteByName(user.username); Users.unsetAvatarData(user._id); - Notifications.notifyLogged('updateAvatar', { - username: user.username, - etag: null, - }); + api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: null }); }, }); diff --git a/server/modules/listeners/listeners.module.ts b/server/modules/listeners/listeners.module.ts new file mode 100644 index 00000000000..a83989810ed --- /dev/null +++ b/server/modules/listeners/listeners.module.ts @@ -0,0 +1,259 @@ +import { IServiceClass } from '../../sdk/types/ServiceClass'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { EnterpriseSettings, MeteorService } from '../../sdk/index'; +import { IRoutingManagerConfig } from '../../../definition/IRoutingManagerConfig'; + +const STATUS_MAP: {[k: string]: number} = { + offline: 0, + online: 1, + away: 2, + busy: 3, +}; + +export const minimongoChangeMap: Record = { inserted: 'added', updated: 'changed', removed: 'removed' }; + +export class ListenersModule { + constructor( + service: IServiceClass, + notifications: NotificationsModule, + ) { + service.onEvent('emoji.deleteCustom', (emoji) => { + notifications.notifyLogged('deleteEmojiCustom', { + emojiData: emoji, + }); + }); + + service.onEvent('emoji.updateCustom', (emoji) => { + notifications.notifyLogged('updateEmojiCustom', { + emojiData: emoji, + }); + }); + + service.onEvent('notify.ephemeralMessage', (uid, rid, message) => { + notifications.notifyLogged(`${ uid }/message`, { + groupable: false, + ...message, + _id: String(Date.now()), + rid, + ts: new Date(), + }); + }); + + service.onEvent('permission.changed', ({ clientAction, data }) => { + notifications.notifyLogged('permissions-changed', clientAction, data); + }); + + service.onEvent('room.avatarUpdate', ({ _id: rid, avatarETag: etag }) => { + notifications.notifyLogged('updateAvatar', { + rid, + etag, + }); + }); + + service.onEvent('user.avatarUpdate', ({ username, avatarETag: etag }) => { + notifications.notifyLogged('updateAvatar', { + username, + etag, + }); + }); + + service.onEvent('user.deleted', ({ _id: userId }) => { + notifications.notifyLogged('Users:Deleted', { + userId, + }); + }); + + service.onEvent('user.deleteCustomStatus', (userStatus) => { + notifications.notifyLogged('deleteCustomUserStatus', { + userStatusData: userStatus, + }); + }); + + service.onEvent('user.nameChanged', (user) => { + notifications.notifyLogged('Users:NameChanged', user); + }); + + service.onEvent('user.roleUpdate', (update) => { + notifications.notifyLogged('roles-change', update); + }); + + service.onEvent('userpresence', ({ user }) => { + const { + _id, username, status, statusText, + } = user; + if (!status) { + return; + } + + notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText]); + }); + + service.onEvent('user.updateCustomStatus', (userStatus) => { + notifications.notifyLogged('updateCustomUserStatus', { + userStatusData: userStatus, + }); + }); + + service.onEvent('watch.messages', ({ message }) => { + if (!message.rid) { + return; + } + + notifications.streamRoomMessage._emit('__my_messages__', [message], undefined, false, (streamer, _sub, eventName, args, allowed) => streamer.changedPayload(streamer.subscriptionName, 'id', { + eventName, + args: [args, allowed], + })); + + notifications.streamRoomMessage.emitWithoutBroadcast(message.rid, message); + }); + + service.onEvent('watch.subscriptions', ({ clientAction, subscription }) => { + if (!subscription.u?._id) { + return; + } + + // emit a removed event on msg stream to remove the user's stream-room-messages subscription when the user is removed from room + if (clientAction === 'removed') { + notifications.streamRoomMessage.__emit(subscription.u._id, clientAction, subscription); + } + + notifications.streamUser.__emit(subscription.u._id, clientAction, subscription); + + notifications.notifyUserInThisInstance( + subscription.u._id, + 'subscriptions-changed', + clientAction, + subscription, + ); + }); + + service.onEvent('watch.roles', ({ clientAction, role }): void => { + const payload = { + type: clientAction, + ...role, + }; + notifications.streamRoles.emit('roles', payload); + }); + + let autoAssignAgent: IRoutingManagerConfig | undefined; + async function getRoutingManagerConfig(): Promise { + if (!autoAssignAgent) { + autoAssignAgent = await MeteorService.getRoutingManagerConfig(); + } + + return autoAssignAgent; + } + + service.onEvent('watch.inquiries', async ({ clientAction, inquiry, diff }): Promise => { + const config = await getRoutingManagerConfig(); + if (config.autoAssignAgent) { + return; + } + + const type = minimongoChangeMap[clientAction]; + if (clientAction === 'removed') { + notifications.streamLivechatQueueData.emitWithoutBroadcast(inquiry._id, { _id: inquiry._id, clientAction }); + + if (inquiry.department) { + return notifications.streamLivechatQueueData.emitWithoutBroadcast(`department/${ inquiry.department }`, { type, ...inquiry }); + } + + return notifications.streamLivechatQueueData.emitWithoutBroadcast('public', { type, ...inquiry }); + } + + notifications.streamLivechatQueueData.emitWithoutBroadcast(inquiry._id, { ...inquiry, clientAction }); + + if (!inquiry.department) { + return notifications.streamLivechatQueueData.emitWithoutBroadcast('public', { type, ...inquiry }); + } + + notifications.streamLivechatQueueData.emitWithoutBroadcast(`department/${ inquiry.department }`, { type, ...inquiry }); + + if (clientAction === 'updated' && !diff?.department) { + notifications.streamLivechatQueueData.emitWithoutBroadcast('public', { type, ...inquiry }); + } + }); + + service.onEvent('watch.settings', async ({ clientAction, setting }): Promise => { + if (setting._id === 'Livechat_Routing_Method') { + autoAssignAgent = undefined; + } + + if (clientAction !== 'removed') { + const result = await EnterpriseSettings.changeSettingValue(setting); + if (!(result instanceof Error)) { + setting.value = result?.value; + } + } + + if (setting.hidden) { + return; + } + + + const value = { + _id: setting._id, + value: setting.value, + editor: setting.editor, + properties: setting.properties, + enterprise: setting.enterprise, + requiredOnWizard: setting.requiredOnWizard, + }; + + if (setting.public === true) { + notifications.notifyAllInThisInstance('public-settings-changed', clientAction, value); + } + + notifications.notifyLogged('private-settings-changed', clientAction, value); + }); + + service.onEvent('watch.rooms', ({ clientAction, room }): void => { + // this emit will cause the user to receive a 'rooms-changed' event + notifications.streamUser.__emit(room._id, clientAction, room); + + notifications.streamRoomData.emitWithoutBroadcast(room._id, clientAction, room); + }); + + service.onEvent('watch.users', ({ clientAction, data, diff, unset, id }): void => { + switch (clientAction) { + case 'updated': + notifications.notifyUserInThisInstance(id, 'userData', { diff, unset, type: clientAction }); + break; + case 'inserted': + notifications.notifyUserInThisInstance(id, 'userData', { data, type: clientAction }); + break; + case 'removed': + notifications.notifyUserInThisInstance(id, 'userData', { id, type: clientAction }); + break; + } + }); + + service.onEvent('watch.integrationHistory', ({ clientAction, data, diff, id }): void => { + if (!data?.integration?._id) { + return; + } + switch (clientAction) { + case 'updated': { + notifications.streamIntegrationHistory.emit(data.integration._id, { id, diff, type: clientAction }); + break; + } + case 'inserted': { + notifications.streamIntegrationHistory.emit(data.integration._id, { data, type: clientAction }); + break; + } + } + }); + + service.onEvent('watch.livechatDepartmentAgents', ({ clientAction, data }): void => { + const { agentId } = data; + if (!agentId) { + return; + } + + notifications.notifyUserInThisInstance(agentId, 'departmentAgentData', { + action: clientAction, + ...data, + }); + }); + } +} diff --git a/server/modules/notifications/notifications.module.ts b/server/modules/notifications/notifications.module.ts new file mode 100644 index 00000000000..06d35a5dcc1 --- /dev/null +++ b/server/modules/notifications/notifications.module.ts @@ -0,0 +1,422 @@ +import { IStreamer, IStreamerConstructor, IPublication } from '../streamer/streamer.module'; +import { Authorization } from '../../sdk'; +import { RoomsRaw } from '../../../app/models/server/raw/Rooms'; +import { SubscriptionsRaw } from '../../../app/models/server/raw/Subscriptions'; +import { ISubscription } from '../../../definition/ISubscription'; +import { UsersRaw } from '../../../app/models/server/raw/Users'; +import { SettingsRaw } from '../../../app/models/server/raw/Settings'; + +interface IModelsParam { + Rooms: RoomsRaw; + Subscriptions: SubscriptionsRaw; + Users: UsersRaw; + Settings: SettingsRaw; +} + +export class NotificationsModule { + private debug = false + + public readonly streamLogged: IStreamer; + + public readonly streamAll: IStreamer; + + public readonly streamRoom: IStreamer; + + public readonly streamRoomUsers: IStreamer; + + public readonly streamUser: IStreamer; + + public readonly streamRoomMessage: IStreamer; + + public readonly streamImporters: IStreamer; + + public readonly streamRoles: IStreamer; + + public readonly streamApps: IStreamer; + + public readonly streamAppsEngine: IStreamer; + + public readonly streamCannedResponses: IStreamer; + + public readonly streamIntegrationHistory: IStreamer; + + public readonly streamLivechatRoom: IStreamer; + + public readonly streamLivechatQueueData: IStreamer; + + public readonly streamStdout: IStreamer; + + public readonly streamRoomData: IStreamer; + + constructor( + private Streamer: IStreamerConstructor, + ) { + this.streamAll = new this.Streamer('notify-all'); + this.streamLogged = new this.Streamer('notify-logged'); + this.streamRoom = new this.Streamer('notify-room'); + this.streamRoomUsers = new this.Streamer('notify-room-users'); + this.streamImporters = new this.Streamer('importers', { retransmit: false }); + this.streamRoles = new this.Streamer('roles'); + this.streamApps = new this.Streamer('apps', { retransmit: false }); + this.streamAppsEngine = new this.Streamer('apps-engine', { retransmit: false }); + this.streamCannedResponses = new this.Streamer('canned-responses'); + this.streamIntegrationHistory = new this.Streamer('integrationHistory'); + this.streamLivechatRoom = new this.Streamer('livechat-room'); + this.streamLivechatQueueData = new this.Streamer('livechat-inquiry-queue-observer'); + this.streamStdout = new this.Streamer('stdout'); + this.streamRoomData = new this.Streamer('room-data'); + + this.streamRoomMessage = new this.Streamer('room-messages'); + + this.streamRoomMessage.on('_afterPublish', async (streamer: IStreamer, publication: IPublication, eventName: string): Promise => { + const { userId } = publication._session; + if (!userId) { + return; + } + + const userEvent = (clientAction: string, { rid }: {rid: string}): void => { + switch (clientAction) { + case 'removed': + streamer.removeListener(userId, userEvent); + const sub = [...streamer.subscriptions].find((sub) => sub.eventName === rid && sub.subscription.userId === userId); + sub && streamer.removeSubscription(sub, eventName); + break; + } + }; + + streamer.on(userId, userEvent); + }); + + this.streamUser = new this.Streamer('notify-user'); + } + + async configure({ Rooms, Subscriptions, Users, Settings }: IModelsParam): Promise { + const notifyUser = this.notifyUser.bind(this); + + this.streamRoomMessage.allowWrite('none'); + this.streamRoomMessage.allowRead(async function(eventName /* , args*/) { + const room = await Rooms.findOneById(eventName); + if (!room) { + return false; + } + + const canAccess = await Authorization.canAccessRoom(room, { _id: this.userId }); + if (!canAccess) { + // verify if can preview messages from public channels + if (room.t === 'c') { + return Authorization.hasPermission(this.userId, 'preview-c-room'); + } + return false; + } + + return true; + }); + + this.streamRoomMessage.allowRead('__my_messages__', 'all'); + this.streamRoomMessage.allowEmit('__my_messages__', async function(_eventName, { rid }) { + try { + const room = await Rooms.findOneById(rid); + if (!room) { + return false; + } + + const canAccess = await Authorization.canAccessRoom(room, { _id: this.userId }); + if (!canAccess) { + return false; + } + + const roomParticipant = await Subscriptions.countByRoomIdAndUserId(room._id, this.userId); + + return { + roomParticipant: roomParticipant > 0, + roomType: room.t, + roomName: room.name, + }; + } catch (error) { + /* error*/ + return false; + } + }); + + this.streamAll.allowWrite('none'); + this.streamAll.allowRead('all'); + this.streamAll.allowRead('private-settings-changed', async function() { + if (this.userId == null) { + return false; + } + return Authorization.hasAtLeastOnePermission(this.userId, ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']); + }); + + this.streamLogged.allowWrite('none'); + this.streamLogged.allowRead('logged'); + + this.streamRoom.allowRead(async function(eventName, extraData) { + if (!this.userId) { + return false; + } + + const [rid] = eventName.split('/'); + + // typing from livechat widget + if (extraData?.token) { + // TODO improve this to make a query 'v.token' + const room = await Rooms.findOneById(rid, { projection: { t: 1, 'v.token': 1 } }); + return room && room.t === 'l' && room.v.token === extraData.token; + } + + const subsCount = await Subscriptions.countByRoomIdAndUserId(rid, this.userId); + return subsCount > 0; + }); + + this.streamRoom.allowWrite(async function(eventName, username, _typing, extraData) { + const [rid, e] = eventName.split('/'); + + // TODO should this use WEB_RTC_EVENTS enum? + if (e === 'webrtc') { + return true; + } + + if (e !== 'typing') { + return false; + } + + try { + // TODO consider using something to cache settings + const key = await Settings.getValueById('UI_Use_Real_Name') ? 'name' : 'username'; + + // typing from livechat widget + if (extraData?.token) { + // TODO improve this to make a query 'v.token' + const room = await Rooms.findOneById(rid, { projection: { t: 1, 'v.token': 1 } }); + return room && room.t === 'l' && room.v.token === extraData.token; + } + + const user = await Users.findOneById(this.userId, { + projection: { + [key]: 1, + }, + }); + if (!user) { + return false; + } + + return user[key] === username; + } catch (e) { + console.error(e); + return false; + } + }); + + this.streamRoomUsers.allowRead('none'); + this.streamRoomUsers.allowWrite(async function(eventName, ...args) { + const [roomId, e] = eventName.split('/'); + if (await Subscriptions.countByRoomIdAndUserId(roomId, this.userId) > 0) { + const subscriptions: ISubscription[] = await Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId, { projection: { 'u._id': 1, _id: 0 } }).toArray(); + subscriptions.forEach((subscription) => notifyUser(subscription.u._id, e, ...args)); + } + return false; + }); + + this.streamUser.allowWrite('logged'); + this.streamUser.allowRead(async function(eventName) { + const [userId] = eventName.split('/'); + return (this.userId != null) && this.userId === userId; + }); + + this.streamImporters.allowRead('all'); + this.streamImporters.allowEmit('all'); + this.streamImporters.allowWrite('none'); + + this.streamApps.serverOnly = true; + this.streamApps.allowRead('all'); + this.streamApps.allowEmit('all'); + this.streamApps.allowWrite('none'); + + this.streamAppsEngine.serverOnly = true; + this.streamAppsEngine.allowRead('none'); + this.streamAppsEngine.allowEmit('all'); + this.streamAppsEngine.allowWrite('none'); + + this.streamCannedResponses.allowWrite('none'); + this.streamCannedResponses.allowRead(async function() { + return !!this.userId && !!await Settings.getValueById('Canned_Responses_Enable') && Authorization.hasPermission(this.userId, 'view-canned-responses'); + }); + + this.streamIntegrationHistory.allowWrite('none'); + this.streamIntegrationHistory.allowRead(async function() { + if (!this.userId) { + return false; + } + return Authorization.hasAtLeastOnePermission(this.userId, [ + 'manage-outgoing-integrations', + 'manage-own-outgoing-integrations', + ]); + }); + + this.streamLivechatRoom.allowRead(async function(roomId, extraData) { + const room = await Rooms.findOneById(roomId, { projection: { _id: 0, t: 1, v: 1 } }); + + if (!room) { + console.warn(`Invalid eventName: "${ roomId }"`); + return false; + } + + if (room.t === 'l' && extraData?.visitorToken && room.v.token === extraData.visitorToken) { + return true; + } + return false; + }); + + this.streamLivechatQueueData.allowWrite('none'); + this.streamLivechatQueueData.allowRead(async function() { + return this.userId ? Authorization.hasPermission(this.userId, 'view-l-room') : false; + }); + + this.streamStdout.allowWrite('none'); + this.streamStdout.allowRead(async function() { + if (!this.userId) { + return false; + } + return Authorization.hasPermission(this.userId, 'view-logs'); + }); + + this.streamRoomData.allowWrite('none'); + this.streamRoomData.allowRead(async function(rid) { + try { + const room = await Rooms.findOneById(rid); + if (!room) { + return false; + } + + const canAccess = await Authorization.canAccessRoom(room, { _id: this.userId }); + if (!canAccess) { + return false; + } + + return true; + } catch (error) { + return false; + } + }); + + this.streamRoles.allowWrite('none'); + this.streamRoles.allowRead('logged'); + + this.streamUser.on('_afterPublish', async (streamer: IStreamer, publication: IPublication, eventName: string): Promise => { + const { userId } = publication._session; + if (!userId) { + return; + } + + if (/rooms-changed/.test(eventName)) { + // TODO: change this to serialize only once + const roomEvent = (...args: any[]): void => { + // TODO if receive a removed event could do => streamer.removeListener(rid, roomEvent); + const payload = streamer.changedPayload(streamer.subscriptionName, 'id', { + eventName: `${ userId }/rooms-changed`, + args, + }); + + payload && publication._session.socket?.send( + payload, + ); + }; + + const subscriptions: Pick[] = await Subscriptions.find( + { 'u._id': userId }, + { projection: { rid: 1 } }, + ).toArray(); + + subscriptions.forEach(({ rid }) => { + streamer.on(rid, roomEvent); + }); + + const userEvent = async (clientAction: string, { rid }: Partial = {}): Promise => { + if (!rid) { + return; + } + + switch (clientAction) { + case 'inserted': + subscriptions.push({ rid }); + streamer.on(rid, roomEvent); + + // after a subscription is added need to emit the room again + roomEvent('inserted', await Rooms.findOneById(rid)); + break; + + case 'removed': + streamer.removeListener(rid, roomEvent); + break; + } + }; + streamer.on(userId, userEvent); + + publication.onStop(() => { + streamer.removeListener(userId, userEvent); + subscriptions.forEach(({ rid }) => streamer.removeListener(rid, roomEvent)); + }); + } + }); + } + + notifyAll(eventName: string, ...args: any[]): void { + if (this.debug === true) { + console.log('notifyAll', [eventName, ...args]); + } + return this.streamAll.emit(eventName, ...args); + } + + notifyLogged(eventName: string, ...args: any[]): void { + if (this.debug === true) { + console.log('notifyLogged', [eventName, ...args]); + } + return this.streamLogged.emit(eventName, ...args); + } + + notifyRoom(room: string, eventName: string, ...args: any[]): void { + if (this.debug === true) { + console.log('notifyRoom', [room, eventName, ...args]); + } + return this.streamRoom.emit(`${ room }/${ eventName }`, ...args); + } + + notifyUser(userId: string, eventName: string, ...args: any[]): void { + if (this.debug === true) { + console.log('notifyUser', [userId, eventName, ...args]); + } + return this.streamUser.emit(`${ userId }/${ eventName }`, ...args); + } + + notifyAllInThisInstance(eventName: string, ...args: any[]): void { + if (this.debug === true) { + console.log('notifyAll', [eventName, ...args]); + } + return this.streamAll.emitWithoutBroadcast(eventName, ...args); + } + + notifyLoggedInThisInstance(eventName: string, ...args: any[]): void { + if (this.debug === true) { + console.log('notifyLogged', [eventName, ...args]); + } + return this.streamLogged.emitWithoutBroadcast(eventName, ...args); + } + + notifyRoomInThisInstance(room: string, eventName: string, ...args: any[]): void { + if (this.debug === true) { + console.log('notifyRoomAndBroadcast', [room, eventName, ...args]); + } + return this.streamRoom.emitWithoutBroadcast(`${ room }/${ eventName }`, ...args); + } + + notifyUserInThisInstance(userId: string, eventName: string, ...args: any[]): void { + if (this.debug === true) { + console.log('notifyUserAndBroadcast', [userId, eventName, ...args]); + } + return this.streamUser.emitWithoutBroadcast(`${ userId }/${ eventName }`, ...args); + } + + progressUpdated(progress: {rate: number}): void { + this.streamImporters.emit('progress', progress); + } +} diff --git a/server/modules/streamer/streamer.module.ts b/server/modules/streamer/streamer.module.ts new file mode 100644 index 00000000000..7c2538f5429 --- /dev/null +++ b/server/modules/streamer/streamer.module.ts @@ -0,0 +1,373 @@ +import { EventEmitter } from 'events'; + +class StreamerCentralClass extends EventEmitter { + public instances: Record = {}; + + constructor() { + super(); + } +} + +export const StreamerCentral = new StreamerCentralClass(); + +export type Client = { + meteorClient: boolean; + ws: any; + userId: string; + send: Function; +} + +export interface IPublication { + onStop: Function; + stop: Function; + connection: Connection; + _session: { + sendAdded(publicationName: string, id: string, fields: Record): void; + userId: string; + socket?: { + send: Function; + }; + }; + ready: Function; + userId: string; + client: Client; +} + +type Rule = (this: IPublication, eventName: string, ...args: any) => Promise; + +interface IRules { + [k: string]: Rule; +} + +export type Connection = any; + +export type DDPSubscription = { + eventName: string; + subscription: IPublication; +} + +export interface IStreamer { + serverOnly: boolean; + + subscriptions: Set; + + subscriptionName: string; + + allowEmit(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + + allowWrite(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + + allowRead(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + + emit(event: string, ...data: any[]): void; + + on(event: string, fn: (...data: any[]) => void): void; + + removeSubscription(subscription: DDPSubscription, eventName: string): void; + + removeListener(event: string, fn: (...data: any[]) => void): void; + + __emit(...data: any[]): void; + + _emit(eventName: string, args: any[], origin: Connection | undefined, broadcast: boolean, transform?: TransformMessage): boolean; + + emitWithoutBroadcast(event: string, ...data: any[]): void; + + changedPayload(collection: string, id: string, fields: Record): string | false; +} + +export interface IStreamerConstructor { + // eslint-disable-next-line @typescript-eslint/no-misused-new + new(name: string, options?: {retransmit?: boolean; retransmitToSelf?: boolean}): IStreamer; +} + +export type TransformMessage = (streamer: Streamer, subscription: DDPSubscription, eventName: string, args: any[], allowed: boolean | object) => string | false; + +export abstract class Streamer extends EventEmitter implements IStreamer { + public subscriptions = new Set(); + + protected subscriptionsByEventName = new Map>(); + + public retransmit = true; + + public retransmitToSelf = false; + + public serverOnly = false; + + private _allowRead: IRules = {}; + + private _allowWrite: IRules = {}; + + private _allowEmit: IRules = {}; + + constructor( + public name: string, + { retransmit = true, retransmitToSelf = false }: {retransmit?: boolean; retransmitToSelf?: boolean } = { }, + ) { + super(); + + if (StreamerCentral.instances[name]) { + console.warn('Streamer instance already exists:', name); + return StreamerCentral.instances[name]; + } + + StreamerCentral.instances[name] = this; + + this.retransmit = retransmit; + this.retransmitToSelf = retransmitToSelf; + + this.iniPublication(); + // DDPStreamer doesn't have this + this.initMethod(); + + this.allowRead('none'); + this.allowEmit('all'); + this.allowWrite('none'); + } + + get subscriptionName(): string { + return `stream-${ this.name }`; + } + + private allow(rules: IRules, name: string) { + return (eventName: string | boolean | Rule, fn?: string | boolean | Rule): void => { + if (fn === undefined) { + fn = eventName; + eventName = '__all__'; + } + + if (typeof eventName !== 'string') { + return; + } + + if (typeof fn === 'function') { + rules[eventName] = fn; + return; + } + + if (typeof fn === 'string' && ['all', 'none', 'logged'].indexOf(fn) === -1) { + console.error(`${ name } shortcut '${ fn }' is invalid`); + } + + if (fn === 'all' || fn === true) { + rules[eventName] = async function(): Promise { + return true; + }; + return; + } + + if (fn === 'none' || fn === false) { + rules[eventName] = async function(): Promise { + return false; + }; + return; + } + + if (fn === 'logged') { + rules[eventName] = async function(): Promise { + return Boolean(this.userId); + }; + } + }; + } + + allowRead(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void { + this.allow(this._allowRead, 'allowRead')(eventName, fn); + } + + allowWrite(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void { + this.allow(this._allowWrite, 'allowWrite')(eventName, fn); + } + + allowEmit(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void { + this.allow(this._allowEmit, 'allowEmit')(eventName, fn); + } + + private isAllowed(rules: IRules) { + return async (scope: IPublication, eventName: string, args: any): Promise => { + if (rules[eventName]) { + return rules[eventName].call(scope, eventName, ...args); + } + + return rules.__all__.call(scope, eventName, ...args); + }; + } + + async isReadAllowed(scope: IPublication, eventName: string, args: any): Promise { + return this.isAllowed(this._allowRead)(scope, eventName, args); + } + + async isEmitAllowed(scope: IPublication, eventName: string, ...args: any[]): Promise { + return this.isAllowed(this._allowEmit)(scope, eventName, args); + } + + async isWriteAllowed(scope: IPublication, eventName: string, args: any): Promise { + return this.isAllowed(this._allowWrite)(scope, eventName, args); + } + + addSubscription(subscription: DDPSubscription, eventName: string): void { + this.subscriptions.add(subscription); + + const subByEventName = this.subscriptionsByEventName.get(eventName) || new Set(); + subByEventName.add(subscription); + + this.subscriptionsByEventName.set(eventName, subByEventName); + } + + removeSubscription(subscription: DDPSubscription, eventName: string): void { + this.subscriptions.delete(subscription); + + const subByEventName = this.subscriptionsByEventName.get(eventName); + if (subByEventName) { + subByEventName.delete(subscription); + } + } + + async _publish(publication: IPublication, eventName: string, options: boolean | {useCollection?: boolean; args?: any} = false): Promise { + let useCollection; + let args = []; + + if (typeof options === 'boolean') { + useCollection = options; + } else { + if (options.useCollection) { + useCollection = options.useCollection; + } + + if (options.args) { + args = options.args; + } + } + + if (eventName.length === 0) { + publication.stop(); + throw new Error('invalid-event-name'); + } + + if (await this.isReadAllowed(publication, eventName, args) !== true) { + publication.stop(); + throw new Error('not-allowed'); + } + + const subscription = { + subscription: publication, + eventName, + }; + + this.addSubscription(subscription, eventName); + + publication.onStop(() => { + this.removeSubscription(subscription, eventName); + }); + + // DDPStreamer doesn't have this + if (useCollection === true) { + // Collection compatibility + publication._session.sendAdded(this.subscriptionName, 'id', { + eventName, + }); + } + + publication.ready(); + + super.emit('_afterPublish', this, publication, eventName, options); + } + + abstract registerPublication(name: string, fn: (eventName: string, options: boolean | {useCollection?: boolean; args?: any}) => Promise): void; + + iniPublication(): void { + const _publish = this._publish.bind(this); + this.registerPublication(this.subscriptionName, async function(this: IPublication, eventName: string, options: boolean | {useCollection?: boolean; args?: any}) { + return _publish(this, eventName, options); + }); + } + + abstract registerMethod(methods: Record any>): void; + + initMethod(): void { + const isWriteAllowed = this.isWriteAllowed.bind(this); + const __emit = this.__emit.bind(this); + const _emit = this._emit.bind(this); + const { retransmit } = this; + + const method: Record any> = { + async [this.subscriptionName](this: IPublication, eventName, ...args): Promise { + if (await isWriteAllowed(this, eventName, args) !== true) { + return; + } + + __emit(eventName, ...args); + + if (retransmit === true) { + _emit(eventName, args, this.connection, true); + } + }, + }; + + try { + this.registerMethod(method); + } catch (e) { + console.error(e); + } + } + + abstract changedPayload(collection: string, id: string, fields: Record): string | false; + + _emit(eventName: string, args: any[], origin: Connection | undefined, broadcast: boolean, transform?: TransformMessage): boolean { + if (broadcast === true) { + StreamerCentral.emit('broadcast', this.name, eventName, args); + } + + const subscriptions = this.subscriptionsByEventName.get(eventName); + if (!subscriptions || !subscriptions.size) { + return false; + } + + if (transform) { + this.sendToManySubscriptions(subscriptions, origin, eventName, args, transform); + + return true; + } + + const msg = this.changedPayload(this.subscriptionName, 'id', { + eventName, + args, + }); + + if (!msg) { + return false; + } + + this.sendToManySubscriptions(subscriptions, origin, eventName, args, msg); + + return true; + } + + async sendToManySubscriptions(subscriptions: Set, origin: Connection | undefined, eventName: string, args: any[], getMsg: string | TransformMessage): Promise { + subscriptions.forEach(async (subscription) => { + if (this.retransmitToSelf === false && origin && origin === subscription.subscription.connection) { + return; + } + + const allowed = await this.isEmitAllowed(subscription.subscription, eventName, ...args); + if (allowed) { + const msg = typeof getMsg === 'string' ? getMsg : getMsg(this, subscription, eventName, args, allowed); + if (msg) { + subscription.subscription._session.socket?.send(msg); + } + } + }); + } + + emit(eventName: string, ...args: any[]): boolean { + return this._emit(eventName, args, undefined, true); + } + + __emit(eventName: string, ...args: any[]): boolean { + return super.emit(eventName, ...args); + } + + emitWithoutBroadcast(eventName: string, ...args: any[]): void { + this._emit(eventName, args, undefined, false); + } +} diff --git a/server/modules/watchers/publishFields.ts b/server/modules/watchers/publishFields.ts new file mode 100644 index 00000000000..1f7f0f90991 --- /dev/null +++ b/server/modules/watchers/publishFields.ts @@ -0,0 +1,94 @@ +export const subscriptionFields = { + t: 1, + ts: 1, + ls: 1, + lr: 1, + name: 1, + fname: 1, + rid: 1, + code: 1, + f: 1, + u: 1, + open: 1, + alert: 1, + roles: 1, + unread: 1, + prid: 1, + userMentions: 1, + groupMentions: 1, + archived: 1, + audioNotifications: 1, + audioNotificationValue: 1, + desktopNotifications: 1, + mobilePushNotifications: 1, + emailNotifications: 1, + unreadAlert: 1, + _updatedAt: 1, + blocked: 1, + blocker: 1, + autoTranslate: 1, + autoTranslateLanguage: 1, + disableNotifications: 1, + hideUnreadStatus: 1, + muteGroupMentions: 1, + ignored: 1, + E2EKey: 1, + tunread: 1, + tunreadGroup: 1, + tunreadUser: 1, +}; + +export const roomFields = { + _id: 1, + name: 1, + fname: 1, + t: 1, + cl: 1, + u: 1, + lm: 1, + // usernames: 1, + topic: 1, + announcement: 1, + announcementDetails: 1, + muted: 1, + unmuted: 1, + _updatedAt: 1, + archived: 1, + jitsiTimeout: 1, + description: 1, + default: 1, + customFields: 1, + lastMessage: 1, + retention: 1, + prid: 1, + avatarETag: 1, + usersCount: 1, + + // @TODO create an API to register this fields based on room type + livechatData: 1, + tags: 1, + sms: 1, + facebook: 1, + code: 1, + joinCodeRequired: 1, + open: 1, + v: 1, + label: 1, + ro: 1, + reactWhenReadOnly: 1, + sysMes: 1, + sentiment: 1, + tokenpass: 1, + streamingOptions: 1, + broadcast: 1, + encrypted: 1, + e2eKeyId: 1, + departmentId: 1, + servedBy: 1, + priorityId: 1, + transcriptRequest: 1, + + // fields used by DMs + usernames: 1, + uids: 1, +}; diff --git a/server/modules/watchers/watchers.module.ts b/server/modules/watchers/watchers.module.ts new file mode 100644 index 00000000000..038a50f1292 --- /dev/null +++ b/server/modules/watchers/watchers.module.ts @@ -0,0 +1,333 @@ +import { SubscriptionsRaw } from '../../../app/models/server/raw/Subscriptions'; +import { UsersRaw } from '../../../app/models/server/raw/Users'; +import { SettingsRaw } from '../../../app/models/server/raw/Settings'; +import { PermissionsRaw } from '../../../app/models/server/raw/Permissions'; +import { MessagesRaw } from '../../../app/models/server/raw/Messages'; +import { RolesRaw } from '../../../app/models/server/raw/Roles'; +import { RoomsRaw } from '../../../app/models/server/raw/Rooms'; +import { IMessage } from '../../../definition/IMessage'; +import { ISubscription } from '../../../definition/ISubscription'; +import { IRole } from '../../../definition/IRole'; +import { IRoom } from '../../../definition/IRoom'; +import { IBaseRaw } from '../../../app/models/server/raw/BaseRaw'; +import { LivechatInquiryRaw } from '../../../app/models/server/raw/LivechatInquiry'; +import { api } from '../../sdk/api'; +import { IBaseData } from '../../../definition/IBaseData'; +import { IPermission } from '../../../definition/IPermission'; +import { ISetting } from '../../../definition/ISetting'; +import { IInquiry } from '../../../definition/IInquiry'; +import { UsersSessionsRaw } from '../../../app/models/server/raw/UsersSessions'; +import { IUserSession } from '../../../definition/IUserSession'; +import { subscriptionFields, roomFields } from './publishFields'; +import { IUser } from '../../../definition/IUser'; +import { LoginServiceConfigurationRaw } from '../../../app/models/server/raw/LoginServiceConfiguration'; +import { ILoginServiceConfiguration } from '../../../definition/ILoginServiceConfiguration'; +import { IInstanceStatus } from '../../../definition/IInstanceStatus'; +import { InstanceStatusRaw } from '../../../app/models/server/raw/InstanceStatus'; +import { IntegrationHistoryRaw } from '../../../app/models/server/raw/IntegrationHistory'; +import { IIntegrationHistory } from '../../../definition/IIntegrationHistory'; +import { LivechatDepartmentAgentsRaw } from '../../../app/models/server/raw/LivechatDepartmentAgents'; +import { ILivechatDepartmentAgents } from '../../../definition/ILivechatDepartmentAgents'; +import { IIntegration } from '../../../definition/IIntegration'; +import { IntegrationsRaw } from '../../../app/models/server/raw/Integrations'; + +interface IModelsParam { + Subscriptions: SubscriptionsRaw; + Permissions: PermissionsRaw; + Users: UsersRaw; + Settings: SettingsRaw; + Messages: MessagesRaw; + LivechatInquiry: LivechatInquiryRaw; + LivechatDepartmentAgents: LivechatDepartmentAgentsRaw; + UsersSessions: UsersSessionsRaw; + Roles: RolesRaw; + Rooms: RoomsRaw; + LoginServiceConfiguration: LoginServiceConfigurationRaw; + InstanceStatus: InstanceStatusRaw; + IntegrationHistory: IntegrationHistoryRaw; + Integrations: IntegrationsRaw; +} + +interface IChange { + action: 'insert' | 'update' | 'remove'; + clientAction: 'inserted' | 'updated' | 'removed'; + id: string; + data?: T; + diff?: Record; + unset?: Record; +} + +type Watcher = (model: IBaseRaw, fn: (event: IChange) => void) => void; + +export function initWatchers({ + Messages, + Users, + Settings, + Subscriptions, + UsersSessions, + Roles, + Permissions, + LivechatInquiry, + LivechatDepartmentAgents, + Rooms, + LoginServiceConfiguration, + InstanceStatus, + IntegrationHistory, + Integrations, +}: IModelsParam, watch: Watcher): void { + watch(Messages, async ({ clientAction, id, data }) => { + switch (clientAction) { + case 'inserted': + case 'updated': + const message: IMessage | undefined = data ?? await Messages.findOne({ _id: id }); + if (!message) { + return; + } + + if (message._hidden !== true && message.imported == null) { + const UseRealName = await Settings.getValueById('UI_Use_Real_Name') === true; + + if (UseRealName) { + if (message.u?._id) { + const user = await Users.findOneById(message.u._id); + message.u.name = user?.name; + } + + if (message.mentions?.length) { + for await (const mention of message.mentions) { + const user = await Users.findOneById(mention._id); + mention.name = user?.name; + } + } + } + + api.broadcast('watch.messages', { clientAction, message }); + } + break; + } + }); + + watch(Subscriptions, async ({ clientAction, id }) => { + switch (clientAction) { + case 'inserted': + case 'updated': { + // Override data cuz we do not publish all fields + const subscription = await Subscriptions.findOneById(id, { projection: subscriptionFields }); + if (!subscription) { + return; + } + api.broadcast('watch.subscriptions', { clientAction, subscription }); + break; + } + + case 'removed': { + const trash = await Subscriptions.trashFindOneById(id, { projection: { u: 1, rid: 1 } }); + const subscription = trash || { _id: id }; + api.broadcast('watch.subscriptions', { clientAction, subscription }); + break; + } + } + }); + + watch(Roles, async ({ clientAction, id, data, diff }) => { + if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { + // avoid useless changes + return; + } + + const role = clientAction === 'removed' + ? { _id: id, name: id } + : data || await Roles.findOneById(id); + + if (!role) { + return; + } + + api.broadcast('watch.roles', { + clientAction: clientAction !== 'removed' ? 'changed' : clientAction, + role, + }); + }); + + watch(UsersSessions, async ({ clientAction, id, data }) => { + switch (clientAction) { + case 'inserted': + case 'updated': + data = data ?? await UsersSessions.findOneById(id); + if (!data) { + return; + } + + api.broadcast('watch.userSessions', { clientAction, userSession: data }); + break; + case 'removed': + api.broadcast('watch.userSessions', { clientAction, userSession: { _id: id } }); + break; + } + }); + + watch(LivechatInquiry, async ({ clientAction, id, data, diff }) => { + switch (clientAction) { + case 'inserted': + case 'updated': + data = data ?? await LivechatInquiry.findOneById(id); + break; + + case 'removed': + data = await LivechatInquiry.trashFindOneById(id); + break; + } + + if (!data) { + return; + } + + api.broadcast('watch.inquiries', { clientAction, inquiry: data, diff }); + }); + + watch(LivechatDepartmentAgents, async ({ clientAction, id, data, diff }) => { + if (clientAction === 'removed') { + data = await LivechatDepartmentAgents.trashFindOneById(id, { projection: { agentId: 1, departmentId: 1 } }); + if (!data) { + return; + } + api.broadcast('watch.livechatDepartmentAgents', { clientAction, id, data, diff }); + return; + } + + data = await LivechatDepartmentAgents.findOneById(id, { projection: { agentId: 1, departmentId: 1 } }); + if (!data) { + return; + } + api.broadcast('watch.livechatDepartmentAgents', { clientAction, id, data, diff }); + }); + + + watch(Permissions, async ({ clientAction, id, data, diff }) => { + if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { + // avoid useless changes + return; + } + switch (clientAction) { + case 'updated': + case 'inserted': + data = data ?? await Permissions.findOneById(id); + break; + + case 'removed': + data = { _id: id, roles: [] }; + break; + } + + if (!data) { + return; + } + + api.broadcast('permission.changed', { clientAction, data }); + + if (data.level === 'settings' && data.settingId) { + // if the permission changes, the effect on the visible settings depends on the role affected. + // The selected-settings-based consumers have to react accordingly and either add or remove the + // setting from the user's collection + const setting = await Settings.findOneNotHiddenById(data.settingId); + if (!setting) { + return; + } + api.broadcast('watch.settings', { clientAction: 'updated', setting }); + } + }); + + watch(Settings, async ({ clientAction, id, data, diff }) => { + if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { // avoid useless changes + return; + } + + let setting; + switch (clientAction) { + case 'updated': + case 'inserted': { + setting = data ?? await Settings.findOneById(id); + break; + } + + case 'removed': { + setting = data ?? await Settings.trashFindOneById(id); + break; + } + } + + if (!setting) { + return; + } + + api.broadcast('watch.settings', { clientAction, setting }); + }); + + watch(Rooms, async ({ clientAction, id, data }) => { + if (clientAction === 'removed') { + api.broadcast('watch.rooms', { clientAction, room: { _id: id } }); + return; + } + + const room = data ?? await Rooms.findOneById(id, { projection: roomFields }); + if (!room) { + return; + } + + api.broadcast('watch.rooms', { clientAction, room }); + }); + + // TODO: Prevent flood from database on username change, what causes changes on all past messages from that user + // and most of those messages are not loaded by the clients. + watch(Users, ({ clientAction, id, data, diff, unset }) => { + api.broadcast('watch.users', { clientAction, data, diff, unset, id }); + }); + + watch(LoginServiceConfiguration, async ({ clientAction, id }) => { + const data = await InstanceStatus.findOneById(id, { projection: { secret: 0 } }); + if (!data) { + return; + } + + api.broadcast('watch.loginServiceConfiguration', { clientAction, data, id }); + }); + + watch(InstanceStatus, ({ clientAction, id, data, diff }) => { + api.broadcast('watch.instanceStatus', { clientAction, data, diff, id }); + }); + + watch(IntegrationHistory, async ({ clientAction, id, data, diff }) => { + switch (clientAction) { + case 'updated': { + const history = await IntegrationHistory.findOneById(id, { projection: { 'integration._id': 1 } }); + if (!history || !history.integration) { + return; + } + data = history; + api.broadcast('watch.integrationHistory', { clientAction, data, diff, id }); + break; + } + case 'inserted': { + if (!data) { + return; + } + api.broadcast('watch.integrationHistory', { clientAction, data, diff, id }); + break; + } + } + }); + + watch(Integrations, async ({ clientAction, id, data }) => { + if (clientAction === 'removed') { + api.broadcast('watch.integrations', { clientAction, id, data: { _id: id } }); + return; + } + + data = data ?? await Integrations.findOneById(id); + if (!data) { + return; + } + + api.broadcast('watch.integrations', { clientAction, data, id }); + }); +} diff --git a/server/publications/room/emitter.js b/server/publications/room/emitter.js deleted file mode 100644 index ad5dabb524f..00000000000 --- a/server/publications/room/emitter.js +++ /dev/null @@ -1,42 +0,0 @@ -import { emitRoomDataEvent } from '../../stream/rooms'; -import { Rooms, Subscriptions } from '../../../app/models'; -import { Notifications } from '../../../app/notifications'; - -import { fields } from '.'; - -const getSubscriptions = (id) => { - const fields = { 'u._id': 1 }; - return Subscriptions.trashFind({ rid: id }, { fields }); -}; - -Rooms.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'updated': - case 'inserted': - // Override data cuz we do not publish all fields - data = Rooms.findOneById(id, { fields }); - break; - - case 'removed': - data = { _id: id }; - break; - } - - if (!data) { - return; - } - if (clientAction === 'removed') { - getSubscriptions(id).forEach(({ u }) => { - Notifications.notifyUserInThisInstance( - u._id, - 'rooms-changed', - clientAction, - data, - ); - }); - } - - Notifications.streamUser.__emit(id, clientAction, data); - - emitRoomDataEvent(id, data); -}); diff --git a/server/publications/room/index.js b/server/publications/room/index.js index 8063bd89888..ee7451885d9 100644 --- a/server/publications/room/index.js +++ b/server/publications/room/index.js @@ -5,73 +5,18 @@ import { roomTypes } from '../../../app/utils'; import { hasPermission } from '../../../app/authorization'; import { Rooms } from '../../../app/models'; import { settings } from '../../../app/settings'; -import './emitter'; - -export const fields = { - _id: 1, - name: 1, - fname: 1, - t: 1, - cl: 1, - u: 1, - lm: 1, - // usernames: 1, - topic: 1, - announcement: 1, - announcementDetails: 1, - muted: 1, - unmuted: 1, - _updatedAt: 1, - archived: 1, - jitsiTimeout: 1, - description: 1, - default: 1, - customFields: 1, - lastMessage: 1, - retention: 1, - prid: 1, - avatarETag: 1, - usersCount: 1, - - // @TODO create an API to register this fields based on room type - livechatData: 1, - tags: 1, - sms: 1, - facebook: 1, - code: 1, - joinCodeRequired: 1, - open: 1, - v: 1, - label: 1, - ro: 1, - reactWhenReadOnly: 1, - sysMes: 1, - sentiment: 1, - tokenpass: 1, - streamingOptions: 1, - broadcast: 1, - encrypted: 1, - e2eKeyId: 1, - departmentId: 1, - servedBy: 1, - priorityId: 1, - transcriptRequest: 1, - - // fields used by DMs - usernames: 1, - uids: 1, -}; +import { roomFields } from '../../modules/watchers/publishFields'; const roomMap = (record) => { if (record) { - return _.pick(record, ...Object.keys(fields)); + return _.pick(record, ...Object.keys(roomFields)); } return {}; }; Meteor.methods({ 'rooms/get'(updatedAt) { - const options = { fields }; + const options = { fields: roomFields }; if (!Meteor.userId()) { if (settings.get('Accounts_AllowAnonymousRead') === true) { diff --git a/server/publications/settings/emitter.js b/server/publications/settings/emitter.js deleted file mode 100644 index ecd3cd05a11..00000000000 --- a/server/publications/settings/emitter.js +++ /dev/null @@ -1,53 +0,0 @@ -import { Settings } from '../../../app/models/server'; -import { Notifications } from '../../../app/notifications/server'; -import { hasAtLeastOnePermission } from '../../../app/authorization/server'; -import { SettingsEvents } from '../../../app/settings/server/functions/settings'; - -Settings.on('change', ({ clientAction, id, data, diff }) => { - if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { // avoid useless changes - return; - } - switch (clientAction) { - case 'updated': - case 'inserted': { - const setting = data ?? Settings.findOneById(id); - const value = { - _id: setting._id, - value: setting.value, - editor: setting.editor, - properties: setting.properties, - enterprise: setting.enterprise, - requiredOnWizard: setting.requiredOnWizard, - }; - - SettingsEvents.emit('change-setting', setting, value); - - if (setting.hidden) { - return; - } - - if (setting.public === true) { - Notifications.notifyAllInThisInstance('public-settings-changed', clientAction, value); - } - Notifications.notifyLoggedInThisInstance('private-settings-changed', clientAction, setting); - break; - } - - case 'removed': { - const setting = data || Settings.findOneById(id, { fields: { public: 1 } }); - - if (setting && setting.public === true) { - Notifications.notifyAllInThisInstance('public-settings-changed', clientAction, { _id: id }); - } - Notifications.notifyLoggedInThisInstance('private-settings-changed', clientAction, { _id: id }); - break; - } - } -}); - -Notifications.streamAll.allowRead('private-settings-changed', function() { - if (this.userId == null) { - return false; - } - return hasAtLeastOnePermission(this.userId, ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']); -}); diff --git a/server/publications/settings/index.js b/server/publications/settings/index.js index d0afdc93a98..e14f2079ca3 100644 --- a/server/publications/settings/index.js +++ b/server/publications/settings/index.js @@ -4,7 +4,6 @@ import { Settings } from '../../../app/models/server'; import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/server'; import { getSettingPermissionId } from '../../../app/authorization/lib'; import { SettingsEvents } from '../../../app/settings/server/functions/settings'; -import './emitter'; Meteor.methods({ 'public-settings/get'(updatedAt) { diff --git a/server/publications/spotlight.js b/server/publications/spotlight.js index 238b9881c9b..a86b2c26934 100644 --- a/server/publications/spotlight.js +++ b/server/publications/spotlight.js @@ -130,7 +130,7 @@ function searchUsers({ userId, rid, text, usernames }) { return users; } - const canListOutsiders = hasAllPermission(userId, 'view-outside-room', 'view-d-room'); + const canListOutsiders = hasAllPermission(userId, ['view-outside-room', 'view-d-room']); const canListInsiders = canListOutsiders || (rid && canAccessRoom(room, { _id: userId })); // If can't list outsiders and, wither, the rid was not passed or the user has no access to the room, return diff --git a/server/publications/subscription/emitter.js b/server/publications/subscription/emitter.js deleted file mode 100644 index ba93668f488..00000000000 --- a/server/publications/subscription/emitter.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Notifications } from '../../../app/notifications'; -import { Subscriptions } from '../../../app/models'; -import { msgStream } from '../../../app/lib/server/lib/msgStream'; - -import { fields } from '.'; - -Subscriptions.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'inserted': - case 'updated': - // Override data cuz we do not publish all fields - data = Subscriptions.findOneById(id, { fields }); - break; - - case 'removed': - data = Subscriptions.trashFindOneById(id, { fields: { u: 1, rid: 1 } }); - if (!data) { - return; - } - // emit a removed event on msg stream to remove the user's stream-room-messages subscription when the user is removed from room - msgStream.__emit(data.u._id, clientAction, data); - break; - } - - if (!data) { - return; - } - - Notifications.streamUser.__emit(data.u._id, clientAction, data); - - Notifications.notifyUserInThisInstance( - data.u._id, - 'subscriptions-changed', - clientAction, - data, - ); -}); diff --git a/server/publications/subscription/index.js b/server/publications/subscription/index.js index e01534f5604..4142d990e9f 100644 --- a/server/publications/subscription/index.js +++ b/server/publications/subscription/index.js @@ -1,47 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Subscriptions } from '../../../app/models'; -import './emitter'; - -export const fields = { - t: 1, - ts: 1, - ls: 1, - lr: 1, - name: 1, - fname: 1, - rid: 1, - code: 1, - f: 1, - u: 1, - open: 1, - alert: 1, - roles: 1, - unread: 1, - prid: 1, - userMentions: 1, - groupMentions: 1, - archived: 1, - audioNotifications: 1, - audioNotificationValue: 1, - desktopNotifications: 1, - mobilePushNotifications: 1, - emailNotifications: 1, - unreadAlert: 1, - _updatedAt: 1, - blocked: 1, - blocker: 1, - autoTranslate: 1, - autoTranslateLanguage: 1, - disableNotifications: 1, - hideUnreadStatus: 1, - muteGroupMentions: 1, - ignored: 1, - E2EKey: 1, - tunread: 1, - tunreadGroup: 1, - tunreadUser: 1, -}; +import { subscriptionFields } from '../../modules/watchers/watchers.module'; Meteor.methods({ 'subscriptions/get'(updatedAt) { @@ -49,7 +9,7 @@ Meteor.methods({ return []; } - const options = { fields }; + const options = { fields: subscriptionFields }; const records = Subscriptions.findByUserId(Meteor.userId(), options).fetch(); diff --git a/server/sdk/api.ts b/server/sdk/api.ts new file mode 100644 index 00000000000..7255bd074d8 --- /dev/null +++ b/server/sdk/api.ts @@ -0,0 +1,3 @@ +import { Api } from './lib/Api'; + +export const api = new Api(); diff --git a/server/sdk/index.ts b/server/sdk/index.ts new file mode 100644 index 00000000000..d697f7722bf --- /dev/null +++ b/server/sdk/index.ts @@ -0,0 +1,23 @@ +import { AsyncLocalStorage } from 'async_hooks'; + +import { proxify, proxifyWithWait } from './lib/proxify'; +import { IAuthorization } from './types/IAuthorization'; +import { IServiceContext } from './types/ServiceClass'; +import { IPresence } from './types/IPresence'; +import { IAccount } from './types/IAccount'; +import { ILicense } from './types/ILicense'; +import { IMeteor } from './types/IMeteor'; +import { IEnterpriseSettings } from './types/IEnterpriseSettings'; + +// TODO think in a way to not have to pass the service name to proxify here as well +export const Authorization = proxifyWithWait('authorization'); +export const Presence = proxifyWithWait('presence'); +export const Account = proxifyWithWait('accounts'); +export const License = proxifyWithWait('license'); +export const MeteorService = proxifyWithWait('meteor'); + +// Calls without wait. Means that the service is optional and the result may be an error +// of service/method not available +export const EnterpriseSettings = proxify('ee-settings'); + +export const asyncLocalStorage = new AsyncLocalStorage(); diff --git a/server/sdk/lib/Api.ts b/server/sdk/lib/Api.ts new file mode 100644 index 00000000000..9b646f479e5 --- /dev/null +++ b/server/sdk/lib/Api.ts @@ -0,0 +1,50 @@ +// import { BaseBroker } from './BaseBroker'; +import { IBroker } from '../types/IBroker'; +import { ServiceClass } from '../types/ServiceClass'; +import { EventSignatures } from './Events'; +import { LocalBroker } from './LocalBroker'; + +export class Api { + private services = new Set(); + + private broker: IBroker = new LocalBroker(); + + // set a broker for the API and registers all services in the broker + setBroker(broker: IBroker): void { + this.broker = broker; + + this.services.forEach((service) => this.broker.createService(service)); + } + + destroyService(instance: ServiceClass): void { + if (!this.services.has(instance)) { + return; + } + + if (this.broker) { + this.broker.destroyService(instance); + } + + this.services.delete(instance); + } + + registerService(instance: ServiceClass): void { + this.services.add(instance); + + if (this.broker) { + this.broker.createService(instance); + } + } + + async call(method: string, data: any): Promise { + return this.broker.call(method, data); + } + + async waitAndCall(method: string, data: any): Promise { + return this.broker.waitAndCall(method, data); + } + + async broadcast(event: T, ...args: Parameters): Promise { + return this.broker.broadcast(event, ...args); + } +} diff --git a/server/sdk/lib/Events.ts b/server/sdk/lib/Events.ts new file mode 100644 index 00000000000..579efa31fe3 --- /dev/null +++ b/server/sdk/lib/Events.ts @@ -0,0 +1,53 @@ +import { IInquiry } from '../../../definition/IInquiry'; +import { IMessage } from '../../../definition/IMessage'; +import { IRole } from '../../../definition/IRole'; +import { IRoom } from '../../../definition/IRoom'; +import { ISetting } from '../../../definition/ISetting'; +import { ISubscription } from '../../../definition/ISubscription'; +import { IUser } from '../../../definition/IUser'; +import { AutoUpdateRecord } from '../types/IMeteor'; +import { IEmoji } from '../../../definition/IEmoji'; +import { IUserStatus } from '../../../definition/IUserStatus'; +import { IUserSession } from '../../../definition/IUserSession'; +import { ILoginServiceConfiguration } from '../../../definition/ILoginServiceConfiguration'; +import { IInstanceStatus } from '../../../definition/IInstanceStatus'; +import { IIntegrationHistory } from '../../../definition/IIntegrationHistory'; +import { ILivechatDepartmentAgents } from '../../../definition/ILivechatDepartmentAgents'; +import { IIntegration } from '../../../definition/IIntegration'; + +export type EventSignatures = { + 'emoji.deleteCustom'(emoji: IEmoji): void; + 'emoji.updateCustom'(emoji: IEmoji): void; + 'license.module'(data: { module: string; valid: boolean }): void; + 'livechat-inquiry-queue-observer'(data: { action: string; inquiry: IInquiry }): void; + 'message'(data: { action: string; message: IMessage }): void; + 'meteor.autoUpdateClientVersionChanged'(data: {record: AutoUpdateRecord }): void; + 'notify.ephemeralMessage'(uid: string, rid: string, message: Partial): void; + 'permission.changed'(data: { clientAction: string; data: any }): void; + 'room'(data: { action: string; room: Partial }): void; + 'room.avatarUpdate'(room: Partial): void; + 'setting'(data: { action: string; setting: Partial }): void; + 'stream'([streamer, eventName, payload]: [string, string, string]): void; + 'stream.ephemeralMessage'(uid: string, rid: string, message: Partial): void; + 'subscription'(data: { action: string; subscription: Partial }): void; + 'user.avatarUpdate'(user: Partial): void; + 'user.deleted'(user: Partial): void; + 'user.deleteCustomStatus'(userStatus: IUserStatus): void; + 'user.nameChanged'(user: Partial): void; + 'user.roleUpdate'(update: Record): void; + 'user.updateCustomStatus'(userStatus: IUserStatus): void; + 'userpresence'(data: { action: string; user: Partial }): void; + 'watch.messages'(data: { clientAction: string; message: Partial }): void; + 'watch.roles'(data: { clientAction: string; role: Partial }): void; + 'watch.rooms'(data: { clientAction: string; room: Pick & Partial }): void; + 'watch.subscriptions'(data: { clientAction: string; subscription: Partial }): void; + 'watch.userSessions'(data: { clientAction: string; userSession: Partial }): void; + 'watch.inquiries'(data: { clientAction: string; inquiry: IInquiry; diff?: Record }): void; + 'watch.settings'(data: { clientAction: string; setting: ISetting }): void; + 'watch.users'(data: { clientAction: string; data?: Partial; diff?: Record; unset?: Record; id: string }): void; + 'watch.loginServiceConfiguration'(data: { clientAction: string; data: Partial; id: string }): void; + 'watch.instanceStatus'(data: { clientAction: string; data?: Partial; diff?: Record; id: string }): void; + 'watch.integrationHistory'(data: { clientAction: string; data: Partial; diff?: Record; id: string }): void; + 'watch.integrations'(data: { clientAction: string; data: Partial; id: string }): void; + 'watch.livechatDepartmentAgents'(data: { clientAction: string; data: Partial; diff?: Record; id: string }): void; +} diff --git a/server/sdk/lib/LocalBroker.ts b/server/sdk/lib/LocalBroker.ts new file mode 100644 index 00000000000..636e81e51f2 --- /dev/null +++ b/server/sdk/lib/LocalBroker.ts @@ -0,0 +1,71 @@ +import { EventEmitter } from 'events'; + +import { IBroker, IBrokerNode } from '../types/IBroker'; +import { ServiceClass } from '../types/ServiceClass'; +import { asyncLocalStorage } from '..'; +import { EventSignatures } from './Events'; + +export class LocalBroker implements IBroker { + private methods = new Map(); + + private events = new EventEmitter(); + + async call(method: string, data: any): Promise { + const result = await asyncLocalStorage.run({ + id: 'ctx.id', + nodeID: 'ctx.nodeID', + requestID: 'ctx.requestID', + broker: this, + }, (): any => this.methods.get(method)?.(...data)); + + return result; + } + + async waitAndCall(method: string, data: any): Promise { + return this.call(method, data); + } + + destroyService(instance: ServiceClass): void { + const namespace = instance.getName(); + + instance.getEvents().forEach((eventName) => { + this.events.removeListener(eventName, instance.emit); + }); + + const methods = instance.constructor?.name === 'Object' ? Object.getOwnPropertyNames(instance) : Object.getOwnPropertyNames(Object.getPrototypeOf(instance)); + for (const method of methods) { + if (method === 'constructor') { + continue; + } + + this.methods.delete(`${ namespace }.${ method }`); + } + } + + createService(instance: ServiceClass): void { + const namespace = instance.getName(); + + instance.getEvents().forEach((eventName) => { + this.events.on(eventName, instance.emit); + }); + + const methods = instance.constructor?.name === 'Object' ? Object.getOwnPropertyNames(instance) : Object.getOwnPropertyNames(Object.getPrototypeOf(instance)); + for (const method of methods) { + if (method === 'constructor') { + continue; + } + const i = instance as any; + + this.methods.set(`${ namespace }.${ method }`, i[method].bind(i)); + } + } + + async broadcast(event: T, ...args: Parameters): Promise { + // Pass the event name twice to forward it to the instance's emit method + this.events.emit(event, event, ...args); + } + + async nodeList(): Promise { + return []; + } +} diff --git a/server/sdk/lib/proxify.ts b/server/sdk/lib/proxify.ts new file mode 100644 index 00000000000..99b30750a17 --- /dev/null +++ b/server/sdk/lib/proxify.ts @@ -0,0 +1,27 @@ +import { api } from '../api'; + +type FunctionPropertyNames = { + [K in keyof T]: T[K] extends Function ? K : never; +}[keyof T]; + +type Prom = { + [K in FunctionPropertyNames]: ReturnType extends Promise ? T[K] : (...params: Parameters) => Promise>; +} + +type PromOrError = { + [K in FunctionPropertyNames]: ReturnType extends Promise ? (...params: Parameters) => ReturnType | Promise : (...params: Parameters) => Promise | Error>; +} + +function handler(namespace: string, waitService: boolean): ProxyHandler { + return { + get: (_target: T, prop: string): any => (...params: any): Promise => api[waitService ? 'waitAndCall' : 'call'](`${ namespace }.${ prop }`, params), + }; +} + +export function proxifyWithWait(namespace: string): Prom { + return new Proxy({}, handler(namespace, true)) as unknown as Prom; +} + +export function proxify(namespace: string): PromOrError { + return new Proxy({}, handler(namespace, false)) as unknown as Prom; +} diff --git a/server/sdk/types/IAccount.ts b/server/sdk/types/IAccount.ts new file mode 100644 index 00000000000..c75ad9fc6ec --- /dev/null +++ b/server/sdk/types/IAccount.ts @@ -0,0 +1,12 @@ +import { IServiceClass } from './ServiceClass'; + +export interface ILoginResult { + uid: string; + token: string; + tokenExpires?: Date; + type: 'resume' | 'password'; +} + +export interface IAccount extends IServiceClass { + login({ resume, user, password }: {resume: string; user: {username: string}; password: string}): Promise; +} diff --git a/server/sdk/types/IAuthorization.ts b/server/sdk/types/IAuthorization.ts new file mode 100644 index 00000000000..eaad0b59cf7 --- /dev/null +++ b/server/sdk/types/IAuthorization.ts @@ -0,0 +1,12 @@ +import { IRoom } from '../../../definition/IRoom'; +import { IUser } from '../../../definition/IUser'; + +export type RoomAccessValidator = (room: Partial, user: Pick, extraData?: object) => Promise; + +export interface IAuthorization { + hasAllPermission(userId: string, permissions: string[], scope?: string): Promise; + hasPermission(userId: string, permissionId: string, scope?: string): Promise; + hasAtLeastOnePermission(userId: string, permissions: string[], scope?: string): Promise; + addRoleRestrictions(role: string, permissions: string[]): Promise; + canAccessRoom: RoomAccessValidator; +} diff --git a/server/sdk/types/IAuthorizationLivechat.ts b/server/sdk/types/IAuthorizationLivechat.ts new file mode 100644 index 00000000000..be7718c408e --- /dev/null +++ b/server/sdk/types/IAuthorizationLivechat.ts @@ -0,0 +1,5 @@ +import { RoomAccessValidator } from './IAuthorization'; + +export interface IAuthorizationLivechat { + canAccessRoom: RoomAccessValidator; +} diff --git a/server/sdk/types/IAuthorizationTokenpass.ts b/server/sdk/types/IAuthorizationTokenpass.ts new file mode 100644 index 00000000000..14860dff49e --- /dev/null +++ b/server/sdk/types/IAuthorizationTokenpass.ts @@ -0,0 +1,5 @@ +import { RoomAccessValidator } from './IAuthorization'; + +export interface IAuthorizationTokenpass { + canAccessRoom: RoomAccessValidator; +} diff --git a/server/sdk/types/IBroker.ts b/server/sdk/types/IBroker.ts new file mode 100644 index 00000000000..4f4d21f7c72 --- /dev/null +++ b/server/sdk/types/IBroker.ts @@ -0,0 +1,30 @@ +import { ServiceClass } from './ServiceClass'; +import { EventSignatures } from '../lib/Events'; + +export interface IBrokerNode { + id: string; + instanceID: string; + available: boolean; + local: boolean; + // lastHeartbeatTime: 16, + // config: {}, + // client: { type: 'nodejs', version: '0.14.10', langVersion: 'v12.18.3' }, + // metadata: {}, + // ipList: [ '192.168.0.100', '192.168.1.25' ], + // port: 59989, + // hostname: 'RocketChats-MacBook-Pro-Rodrigo-Nascimento.local', + // udpAddress: null, + // cpu: 25, + // cpuSeq: 1, + // seq: 3, + // offlineSince: null +} + +export interface IBroker { + destroyService(service: ServiceClass): void; + createService(service: ServiceClass): void; + call(method: string, data: any): Promise; + waitAndCall(method: string, data: any): Promise; + broadcast(event: T, ...args: Parameters): Promise; + nodeList(): Promise; +} diff --git a/server/sdk/types/IEnterpriseSettings.ts b/server/sdk/types/IEnterpriseSettings.ts new file mode 100644 index 00000000000..5fa2144c2d9 --- /dev/null +++ b/server/sdk/types/IEnterpriseSettings.ts @@ -0,0 +1,6 @@ +import { IServiceClass } from './ServiceClass'; +import { ISetting } from '../../../definition/ISetting'; + +export interface IEnterpriseSettings extends IServiceClass { + changeSettingValue(record: ISetting): undefined | { value: ISetting['value'] }; +} diff --git a/server/sdk/types/ILicense.ts b/server/sdk/types/ILicense.ts new file mode 100644 index 00000000000..b6bcc6ed1b2 --- /dev/null +++ b/server/sdk/types/ILicense.ts @@ -0,0 +1,9 @@ +import { IServiceClass } from './ServiceClass'; + +export interface ILicense extends IServiceClass { + hasLicense(feature: string): boolean; + + isEnterprise(): boolean; + + getModules(): string[]; +} diff --git a/server/sdk/types/IMeteor.ts b/server/sdk/types/IMeteor.ts new file mode 100644 index 00000000000..176081546f9 --- /dev/null +++ b/server/sdk/types/IMeteor.ts @@ -0,0 +1,24 @@ +import { IServiceClass } from './ServiceClass'; +import { IRoutingManagerConfig } from '../../../definition/IRoutingManagerConfig'; + +export type AutoUpdateRecord = { + _id: string; + version: string; + versionRefreshable?: string; + versionNonRefreshable?: string; + assets?: [{ + url: string; + }]; +} + +export interface IMeteor extends IServiceClass { + getLastAutoUpdateClientVersions(): Promise; + + getLoginServiceConfiguration(): Promise; + + callMethodWithToken(userId: string, token: string, method: string, args: any[]): Promise; + + notifyGuestStatusChanged(token: string, status: string): Promise; + + getRoutingManagerConfig(): IRoutingManagerConfig; +} diff --git a/server/sdk/types/IPresence.ts b/server/sdk/types/IPresence.ts new file mode 100644 index 00000000000..54b9e9c5a21 --- /dev/null +++ b/server/sdk/types/IPresence.ts @@ -0,0 +1,11 @@ +import { USER_STATUS } from '../../../definition/UserStatus'; +import { IServiceClass } from './ServiceClass'; + +export interface IPresence extends IServiceClass { + newConnection(uid: string, session: string): Promise<{uid: string; connectionId: string} | undefined>; + removeConnection(uid: string, session: string): Promise<{uid: string; session: string}>; + removeLostConnections(nodeID: string): Promise; + setStatus(uid: string, status: USER_STATUS, statusText?: string): Promise; + setConnectionStatus(uid: string, status: USER_STATUS, session: string): Promise; + updateUserPresence(uid: string): Promise; +} diff --git a/server/sdk/types/ServiceClass.ts b/server/sdk/types/ServiceClass.ts new file mode 100644 index 00000000000..5b28aedac3f --- /dev/null +++ b/server/sdk/types/ServiceClass.ts @@ -0,0 +1,68 @@ +import { EventEmitter } from 'events'; + +import { asyncLocalStorage } from '..'; +import { IBroker, IBrokerNode } from './IBroker'; +import { EventSignatures } from '../lib/Events'; + +export interface IServiceContext { + id: string; // Context ID + broker: IBroker; // Instance of the broker. + nodeID: string | null; // The caller or target Node ID. + // action: Object; // Instance of action definition. + // event: Object; // Instance of event definition. + // eventName: Object; // The emitted event name. + // eventType: String; // Type of event (“emit” or “broadcast”). + // eventGroups: Array; // String> Groups of event. + // caller: String; // Service full name of the caller. E.g.: v3.myService + requestID: string | null; // Request ID. If you make nested-calls, it will be the same ID. + // parentID: String; // Parent context ID (in nested-calls). + // params: Any; // Request params. Second argument from broker.call. + // meta: Any; // Request metadata. It will be also transferred to nested-calls. + // locals: any; // Local data. + // level: Number; // Request level (in nested-calls). The first level is 1. + // span: Span; // Current active span. + ctx?: any; +} + +export interface IServiceClass { + getName(): string; + onNodeConnected?({ node, reconnected }: {node: IBrokerNode; reconnected: boolean}): void; + onNodeUpdated?({ node }: {node: IBrokerNode }): void; + onNodeDisconnected?({ node, unexpected }: {node: IBrokerNode; unexpected: boolean}): Promise; + + onEvent(event: T, handler: EventSignatures[T]): void; + + created?(): Promise; + started?(): Promise; + stopped?(): Promise; +} + +export abstract class ServiceClass implements IServiceClass { + protected name: string; + + protected events = new EventEmitter(); + + constructor() { + this.emit = this.emit.bind(this); + } + + getEvents(): Array { + return this.events.eventNames() as unknown as Array; + } + + getName(): string { + return this.name; + } + + get context(): IServiceContext | undefined { + return asyncLocalStorage.getStore(); + } + + public onEvent(event: T, handler: EventSignatures[T]): void { + this.events.on(event, handler); + } + + public emit(event: T, ...args: Parameters): void { + this.events.emit(event, ...args); + } +} diff --git a/server/services/authorization/canAccessRoom.ts b/server/services/authorization/canAccessRoom.ts new file mode 100644 index 00000000000..b8428e70d99 --- /dev/null +++ b/server/services/authorization/canAccessRoom.ts @@ -0,0 +1,59 @@ + +import { Authorization } from '../../sdk'; +import { RoomAccessValidator } from '../../sdk/types/IAuthorization'; +import { canAccessRoomLivechat } from './canAccessRoomLivechat'; +import { canAccessRoomTokenpass } from './canAccessRoomTokenpass'; +import { Subscriptions, Rooms, Settings } from './service'; + +const roomAccessValidators: RoomAccessValidator[] = [ + async function(room, user): Promise { + if (room.t === 'c') { + // TODO: it was using cached version from /app/settings/server/raw.js + const anonymous = await Settings.getValueById('Accounts_AllowAnonymousRead'); + if (!user._id && anonymous === true) { + return true; + } + + return Authorization.hasPermission(user._id, 'view-c-room'); + } + + return false; + }, + + async function(room, user): Promise { + if (!room._id) { + return false; + } + if (await Subscriptions.countByRoomIdAndUserId(room._id, user._id)) { + return true; + } + return false; + }, + + async function(room, user): Promise { + if (!room.prid) { + return false; + } + room = await Rooms.findOne(room.prid); + if (!room) { + return false; + } + return Authorization.canAccessRoom(room, user); + }, + canAccessRoomLivechat, + canAccessRoomTokenpass, +]; + +export const canAccessRoom: RoomAccessValidator = async (room, user, extraData): Promise => { + if (!room || !user) { + return false; + } + + for await (const roomAccessValidator of roomAccessValidators) { + if (await roomAccessValidator(room, user, extraData)) { + return true; + } + } + + return false; +}; diff --git a/server/services/authorization/canAccessRoomLivechat.ts b/server/services/authorization/canAccessRoomLivechat.ts new file mode 100644 index 00000000000..ab3e38686b9 --- /dev/null +++ b/server/services/authorization/canAccessRoomLivechat.ts @@ -0,0 +1,14 @@ +import { IAuthorizationLivechat } from '../../sdk/types/IAuthorizationLivechat'; +import { proxifyWithWait } from '../../sdk/lib/proxify'; +import { RoomAccessValidator } from '../../sdk/types/IAuthorization'; + +export const AuthorizationLivechat = proxifyWithWait('authorization-livechat'); + +export const canAccessRoomLivechat: RoomAccessValidator = async (room, user): Promise => { + if (room.t !== 'l') { + return false; + } + + // Call back core temporarily + return AuthorizationLivechat.canAccessRoom(room, user); +}; diff --git a/server/services/authorization/canAccessRoomTokenpass.ts b/server/services/authorization/canAccessRoomTokenpass.ts new file mode 100644 index 00000000000..df701ed7ec5 --- /dev/null +++ b/server/services/authorization/canAccessRoomTokenpass.ts @@ -0,0 +1,14 @@ +import { IAuthorizationTokenpass } from '../../sdk/types/IAuthorizationTokenpass'; +import { proxifyWithWait } from '../../sdk/lib/proxify'; +import { RoomAccessValidator } from '../../sdk/types/IAuthorization'; + +export const AuthorizationTokenpass = proxifyWithWait('authorization-tokenpass'); + +export const canAccessRoomTokenpass: RoomAccessValidator = async (room, user): Promise => { + if (!room.tokenpass) { + return false; + } + + // Call back core temporarily + return AuthorizationTokenpass.canAccessRoom(room, user); +}; diff --git a/server/services/authorization/service.ts b/server/services/authorization/service.ts new file mode 100644 index 00000000000..25c7180b242 --- /dev/null +++ b/server/services/authorization/service.ts @@ -0,0 +1,116 @@ +import { Db, Collection } from 'mongodb'; +import mem from 'mem'; + +import { IAuthorization, RoomAccessValidator } from '../../sdk/types/IAuthorization'; +import { ServiceClass } from '../../sdk/types/ServiceClass'; +import { AuthorizationUtils } from '../../../app/authorization/lib/AuthorizationUtils'; +import { IUser } from '../../../definition/IUser'; +import { canAccessRoom } from './canAccessRoom'; +import { SubscriptionsRaw } from '../../../app/models/server/raw/Subscriptions'; +import { SettingsRaw } from '../../../app/models/server/raw/Settings'; +import { RoomsRaw } from '../../../app/models/server/raw/Rooms'; + +import './canAccessRoomLivechat'; +import './canAccessRoomTokenpass'; + +export let Subscriptions: SubscriptionsRaw; +export let Settings: SettingsRaw; +export let Rooms: RoomsRaw; + +// Register as class +export class Authorization extends ServiceClass implements IAuthorization { + protected name = 'authorization'; + + private Permissions: Collection; + + private Users: Collection; + + private getRolesCached = mem(this.getRoles.bind(this), { maxAge: 1000, cacheKey: JSON.stringify }) + + private rolesHasPermissionCached = mem(this.rolesHasPermission.bind(this), { cacheKey: JSON.stringify, ...process.env.TEST_MODE === 'true' && { maxAge: 1 } }) + + constructor(db: Db) { + super(); + + this.Permissions = db.collection('rocketchat_permissions'); + this.Users = db.collection('users'); + + Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription')); + Settings = new SettingsRaw(db.collection('rocketchat_settings')); + Rooms = new RoomsRaw(db.collection('rocketchat_room')); + + const clearCache = (): void => { + mem.clear(this.getRolesCached); + mem.clear(this.rolesHasPermissionCached); + }; + + this.onEvent('watch.roles', clearCache); + this.onEvent('permission.changed', clearCache); + } + + async hasAllPermission(userId: string, permissions: string[], scope?: string): Promise { + if (!userId) { + return false; + } + return this.all(userId, permissions, scope); + } + + async hasPermission(userId: string, permissionId: string, scope?: string): Promise { + if (!userId) { + return false; + } + return this.all(userId, [permissionId], scope); + } + + async hasAtLeastOnePermission(userId: string, permissions: string[], scope?: string): Promise { + if (!userId) { + return false; + } + return this.atLeastOne(userId, permissions, scope); + } + + async canAccessRoom(...args: Parameters): Promise { + return canAccessRoom(...args); + } + + async addRoleRestrictions(role: string, permissions: string[]): Promise { + AuthorizationUtils.addRolePermissionWhiteList(role, permissions); + } + + private async rolesHasPermission(permission: string, roles: string[]): Promise { + if (AuthorizationUtils.isPermissionRestrictedForRoleList(permission, roles)) { + return false; + } + + const result = await this.Permissions.findOne({ _id: permission, roles: { $in: roles } }, { projection: { _id: 1 } }); + return !!result; + } + + private async getRoles(uid: string, scope?: string): Promise { + const { roles: userRoles = [] } = await this.Users.findOne({ _id: uid }, { projection: { roles: 1 } }) || {}; + const { roles: subscriptionsRoles = [] } = (scope && await Subscriptions.findOne({ rid: scope, 'u._id': uid }, { projection: { roles: 1 } })) || {}; + return [...userRoles, ...subscriptionsRoles].sort((a, b) => a.localeCompare(b)); + } + + private async atLeastOne(uid: string, permissions: string[] = [], scope?: string): Promise { + const sortedRoles = await this.getRolesCached(uid, scope); + for (const permission of permissions) { + if (await this.rolesHasPermissionCached(permission, sortedRoles)) { // eslint-disable-line + return true; + } + } + + return false; + } + + private async all(uid: string, permissions: string[] = [], scope?: string): Promise { + const sortedRoles = await this.getRolesCached(uid, scope); + for (const permission of permissions) { + if (!await this.rolesHasPermissionCached(permission, sortedRoles)) { // eslint-disable-line + return false; + } + } + + return true; + } +} diff --git a/server/services/meteor/service.ts b/server/services/meteor/service.ts new file mode 100644 index 00000000000..ccb09b96c99 --- /dev/null +++ b/server/services/meteor/service.ts @@ -0,0 +1,263 @@ +import { Meteor } from 'meteor/meteor'; +import { Promise } from 'meteor/promise'; +import { ServiceConfiguration } from 'meteor/service-configuration'; +import { UserPresenceMonitor, UserPresence } from 'meteor/konecty:user-presence'; +import { MongoInternals } from 'meteor/mongo'; + +import { ServiceClass } from '../../sdk/types/ServiceClass'; +import { IMeteor, AutoUpdateRecord } from '../../sdk/types/IMeteor'; +import { api } from '../../sdk/api'; +import { Users } from '../../../app/models/server/raw/index'; +import { Livechat } from '../../../app/livechat/server'; +import { settings } from '../../../app/settings/server/functions/settings'; +import { setValue, updateValue } from '../../../app/settings/server/raw'; +import { IRoutingManagerConfig } from '../../../definition/IRoutingManagerConfig'; +import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; +import { onlineAgents, monitorAgents } from '../../../app/livechat/server/lib/stream/agentStatus'; +import { IUser } from '../../../definition/IUser'; +import { matrixBroadCastActions } from '../../stream/streamBroadcast'; +import { integrations } from '../../../app/integrations/server/lib/triggerHandler'; +import { ListenersModule, minimongoChangeMap } from '../../modules/listeners/listeners.module'; +import notifications from '../../../app/notifications/server/lib/Notifications'; + + +const autoUpdateRecords = new Map(); + +Meteor.server.publish_handlers.meteor_autoupdate_clientVersions.call({ + added(_collection: string, id: string, version: AutoUpdateRecord) { + autoUpdateRecords.set(id, version); + }, + changed(_collection: string, id: string, version: AutoUpdateRecord) { + autoUpdateRecords.set(id, version); + api.broadcast('meteor.autoUpdateClientVersionChanged', { record: version }); + }, + onStop() { + // + }, + ready() { + // + }, +}); + +type Callbacks = { + added(id: string, record: object): void; + changed(id: string, record: object): void; + removed(id: string): void; +} + +let processOnChange: (diff: Record, id: string) => void; +// eslint-disable-next-line no-undef +const disableOplog = Package['disable-oplog']; +const serviceConfigCallbacks = new Set(); + +if (disableOplog) { + // Stores the callbacks for the disconnection reactivity bellow + const userCallbacks = new Map(); + + // Overrides the native observe changes to prevent database polling and stores the callbacks + // for the users' tokens to re-implement the reactivity based on our database listeners + const { mongo } = MongoInternals.defaultRemoteCollectionDriver(); + MongoInternals.Connection.prototype._observeChanges = function({ collectionName, selector, options = {} }: {collectionName: string; selector: Record; options?: {fields?: Record}}, _ordered: boolean, callbacks: Callbacks): any { + // console.error('Connection.Collection.prototype._observeChanges', collectionName, selector, options); + let cbs: Set<{hashedToken: string; callbacks: Callbacks}>; + let data: {hashedToken: string; callbacks: Callbacks}; + if (callbacks?.added) { + const records = Promise.await(mongo.rawCollection(collectionName).find(selector, { projection: options.fields }).toArray()); + for (const { _id, ...fields } of records) { + callbacks.added(_id, fields); + } + + if (collectionName === 'users' && selector['services.resume.loginTokens.hashedToken']) { + cbs = userCallbacks.get(selector._id) || new Set(); + data = { + hashedToken: selector['services.resume.loginTokens.hashedToken'], + callbacks, + }; + + cbs.add(data); + userCallbacks.set(selector._id, cbs); + } + } + + if (collectionName === 'meteor_accounts_loginServiceConfiguration') { + serviceConfigCallbacks.add(callbacks); + } + + return { + stop(): void { + if (cbs) { + cbs.delete(data); + } + serviceConfigCallbacks.delete(callbacks); + }, + }; + }; + + // Re-implement meteor's reactivity that uses observe to disconnect sessions when the token + // associated was removed + processOnChange = (diff: Record, id: string): void => { + const loginTokens: undefined | {hashedToken: string}[] = diff['services.resume.loginTokens']; + if (loginTokens) { + const tokens = loginTokens.map(({ hashedToken }) => hashedToken); + + const cbs = userCallbacks.get(id); + if (cbs) { + [...cbs].filter(({ hashedToken }) => !tokens.includes(hashedToken)).forEach((item) => { + item.callbacks.removed(id); + cbs.delete(item); + }); + } + } + }; +} + +export class MeteorService extends ServiceClass implements IMeteor { + protected name = 'meteor'; + + constructor() { + super(); + + new ListenersModule(this, notifications); + + this.onEvent('watch.settings', async ({ clientAction, setting }): Promise => { + if (clientAction !== 'removed') { + settings.storeSettingValue(setting, false); + updateValue(setting._id, { value: setting.value }); + return; + } + + settings.removeSettingValue(setting, false); + setValue(setting._id, undefined); + }); + + // TODO: May need to merge with https://github.com/RocketChat/Rocket.Chat/blob/0ddc2831baf8340cbbbc432f88fc2cb97be70e9b/ee/server/services/Presence/Presence.ts#L28 + this.onEvent('watch.userSessions', async ({ clientAction, userSession }): Promise => { + if (clientAction === 'removed') { + UserPresenceMonitor.processUserSession({ + _id: userSession._id, + connections: [{ + fake: true, + }], + }, 'removed'); + } + + UserPresenceMonitor.processUserSession(userSession, minimongoChangeMap[clientAction]); + }); + + this.onEvent('watch.instanceStatus', async ({ clientAction, id, data }): Promise => { + if (clientAction === 'removed') { + UserPresence.removeConnectionsByInstanceId(id); + matrixBroadCastActions?.removed?.(id); + return; + } + + if (clientAction === 'inserted') { + if (data?.extraInformation?.port) { + matrixBroadCastActions?.added?.(data); + } + } + }); + + if (disableOplog) { + this.onEvent('watch.loginServiceConfiguration', ({ clientAction, id, data }) => { + if (clientAction === 'removed') { + serviceConfigCallbacks.forEach((callbacks) => { + callbacks.removed?.(id); + }); + return; + } + + serviceConfigCallbacks.forEach((callbacks) => { + callbacks[clientAction === 'inserted' ? 'added' : 'changed']?.(id, data); + }); + }); + } + + this.onEvent('watch.users', async ({ clientAction, id, diff }) => { + if (disableOplog) { + if (clientAction === 'updated' && diff) { + processOnChange(diff, id); + } + } + + if (!monitorAgents) { + return; + } + + if (clientAction !== 'removed' && diff && !diff.status && !diff.statusLivechat) { + return; + } + + switch (clientAction) { + case 'updated': + case 'inserted': + const agent: IUser | undefined = await Users.findOneAgentById(id, { + projection: { + status: 1, + statusLivechat: 1, + }, + }); + const serviceOnline = agent && agent.status !== 'offline' && agent.statusLivechat === 'available'; + + if (serviceOnline) { + return onlineAgents.add(id); + } + + onlineAgents.remove(id); + + break; + case 'removed': + onlineAgents.remove(id); + break; + } + }); + + this.onEvent('watch.integrations', async ({ clientAction, id, data }) => { + switch (clientAction) { + case 'inserted': + if (data.type === 'webhook-outgoing') { + integrations.triggerHandler.addIntegration(data); + } + break; + case 'updated': + if (data.type === 'webhook-outgoing') { + integrations.triggerHandler.removeIntegration(data); + integrations.triggerHandler.addIntegration(data); + } + break; + case 'removed': + integrations.triggerHandler.removeIntegration({ _id: id }); + break; + } + }); + } + + async getLastAutoUpdateClientVersions(): Promise { + return [...autoUpdateRecords.values()]; + } + + async getLoginServiceConfiguration(): Promise { + return ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(); + } + + async callMethodWithToken(userId: string, token: string, method: string, args: any[]): Promise { + const user = await Users.findOneByIdAndLoginHashedToken(userId, token, { projection: { _id: 1 } }); + if (!user) { + return { + result: Meteor.call(method, ...args), + }; + } + + return { + result: Meteor.runAsUser(userId, () => Meteor.call(method, ...args)), + }; + } + + async notifyGuestStatusChanged(token: string, status: string): Promise { + return Livechat.notifyGuestStatusChanged(token, status); + } + + getRoutingManagerConfig(): IRoutingManagerConfig { + return RoutingManager.getConfig(); + } +} diff --git a/server/services/startup.ts b/server/services/startup.ts new file mode 100644 index 00000000000..bca10b8b45d --- /dev/null +++ b/server/services/startup.ts @@ -0,0 +1,8 @@ +import { MongoInternals } from 'meteor/mongo'; + +import { api } from '../sdk/api'; +import { Authorization } from './authorization/service'; +import { MeteorService } from './meteor/service'; + +api.registerService(new Authorization(MongoInternals.defaultRemoteCollectionDriver().mongo.db)); +api.registerService(new MeteorService()); diff --git a/server/startup/presence.js b/server/startup/presence.js index d28c69906ae..193dfc3bb11 100644 --- a/server/startup/presence.js +++ b/server/startup/presence.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; -import { UserPresence, UserPresenceMonitor } from 'meteor/konecty:user-presence'; +import { UserPresence } from 'meteor/konecty:user-presence'; import InstanceStatusModel from '../../app/models/server/models/InstanceStatus'; import UsersSessionsModel from '../../app/models/server/models/UsersSessions'; @@ -37,35 +37,5 @@ Meteor.startup(function() { }, }; UsersSessionsModel.update({}, update, { multi: true }); - - InstanceStatusModel.on('change', ({ clientAction, id }) => { - switch (clientAction) { - case 'removed': - UserPresence.removeConnectionsByInstanceId(id); - break; - } - }); - - UsersSessionsModel.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'inserted': - UserPresenceMonitor.processUserSession(data, 'added'); - break; - case 'updated': - data = data ?? UsersSessionsModel.findOneById(id); - if (data) { - UserPresenceMonitor.processUserSession(data, 'changed'); - } - break; - case 'removed': - UserPresenceMonitor.processUserSession({ - _id: id, - connections: [{ - fake: true, - }], - }, 'removed'); - break; - } - }); } }); diff --git a/server/startup/serverRunning.js b/server/startup/serverRunning.js index 9b7611951c5..67ed3ee4a62 100644 --- a/server/startup/serverRunning.js +++ b/server/startup/serverRunning.js @@ -47,7 +47,7 @@ Meteor.startup(function() { msg = msg.join('\n'); - if (!oplogEnabled) { + if (!process.env.DISABLE_DB_WATCH && !oplogEnabled) { msg += ['', '', 'OPLOG / REPLICASET IS REQUIRED TO RUN ROCKET.CHAT, MORE INFORMATION AT:', 'https://go.rocket.chat/i/oplog-required'].join('\n'); SystemLogger.error_box(msg, 'SERVER ERROR'); diff --git a/server/stream/messages/emitter.js b/server/stream/messages/emitter.js deleted file mode 100644 index d86dea2639f..00000000000 --- a/server/stream/messages/emitter.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../app/settings'; -import { Users, Messages } from '../../../app/models'; -import { msgStream } from '../../../app/lib/server'; - -import { MY_MESSAGE } from '.'; - -Meteor.startup(function() { - function publishMessage(type, record) { - if (record._hidden !== true && (record.imported == null)) { - const UI_Use_Real_Name = settings.get('UI_Use_Real_Name') === true; - - if (record.u && record.u._id && UI_Use_Real_Name) { - const user = Users.findOneById(record.u._id); - record.u.name = user && user.name; - } - - if (record.mentions && record.mentions.length && UI_Use_Real_Name) { - record.mentions.forEach((mention) => { - const user = Users.findOneById(mention._id); - mention.name = user && user.name; - }); - } - msgStream.mymessage(MY_MESSAGE, record); - msgStream.emitWithoutBroadcast(record.rid, record); - } - } - - return Messages.on('change', function({ clientAction, id, data/* , oplog*/ }) { - switch (clientAction) { - case 'inserted': - case 'updated': - const message = data ?? Messages.findOne({ _id: id }); - publishMessage(clientAction, message); - break; - } - }); -}); diff --git a/server/stream/messages/index.js b/server/stream/messages/index.js deleted file mode 100644 index 9c5e839cf1c..00000000000 --- a/server/stream/messages/index.js +++ /dev/null @@ -1,51 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../app/authorization'; -import { Subscriptions } from '../../../app/models'; -import { msgStream } from '../../../app/lib/server'; -import './emitter'; - - -export const MY_MESSAGE = '__my_messages__'; - -msgStream.allowWrite('none'); - -msgStream.allowRead(function(eventName, args) { - try { - const room = Meteor.call('canAccessRoom', eventName, this.userId, args); - - if (!room) { - return false; - } - - if (room.t === 'c' && !hasPermission(this.userId, 'preview-c-room') && !Subscriptions.findOneByRoomIdAndUserId(room._id, this.userId, { fields: { _id: 1 } })) { - return false; - } - - return true; - } catch (error) { - /* error*/ - return false; - } -}); - -msgStream.allowRead(MY_MESSAGE, 'all'); - -msgStream.allowEmit(MY_MESSAGE, function(eventName, msg) { - try { - const room = Meteor.call('canAccessRoom', msg.rid, this.userId); - - if (!room) { - return false; - } - - return { - roomParticipant: Subscriptions.findOneByRoomIdAndUserId(room._id, this.userId, { fields: { _id: 1 } }) != null, - roomType: room.t, - roomName: room.name, - }; - } catch (error) { - /* error*/ - return false; - } -}); diff --git a/server/stream/rooms/index.js b/server/stream/rooms/index.js deleted file mode 100644 index 7ea683f290a..00000000000 --- a/server/stream/rooms/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { roomTypes } from '../../../app/utils'; -import { ROOM_DATA_STREAM } from '../../../app/utils/stream/constants'; - -export const roomDataStream = new Meteor.Streamer(ROOM_DATA_STREAM); - -roomDataStream.allowWrite('none'); - -roomDataStream.allowRead(function(rid) { - try { - const room = Meteor.call('canAccessRoom', rid, this.userId); - if (!room) { - return false; - } - - return roomTypes.getConfig(room.t).isEmitAllowed(); - } catch (error) { - return false; - } -}); - -export function emitRoomDataEvent(id, data) { - if (!data || !data.t) { - return; - } - - if (!roomTypes.getConfig(data.t).isEmitAllowed()) { - return; - } - - roomDataStream.emitWithoutBroadcast(id, data); -} diff --git a/server/stream/streamBroadcast.js b/server/stream/streamBroadcast.js index b615e1b5d68..990a907d4e3 100644 --- a/server/stream/streamBroadcast.js +++ b/server/stream/streamBroadcast.js @@ -12,6 +12,7 @@ import { settings } from '../../app/settings'; import { isDocker, getURL } from '../../app/utils'; import { Users } from '../../app/models/server'; import InstanceStatusModel from '../../app/models/server/models/InstanceStatus'; +import { StreamerCentral } from '../modules/streamer/streamer.module'; process.env.PORT = String(process.env.PORT).trim(); process.env.INSTANCE_IP = String(process.env.INSTANCE_IP).trim(); @@ -59,12 +60,13 @@ function authorizeConnection(instance) { const cache = new Map(); const originalSetDefaultStatus = UserPresence.setDefaultStatus; +export let matrixBroadCastActions; function startMatrixBroadcast() { if (!startMonitor) { UserPresence.setDefaultStatus = originalSetDefaultStatus; } - const actions = { + matrixBroadCastActions = { added(record) { cache.set(record._id, record); @@ -142,19 +144,7 @@ function startMatrixBroadcast() { }, }; - InstanceStatusModel.find(query, options).fetch().forEach(actions.added); - return InstanceStatusModel.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'inserted': - if (data.extraInformation?.port) { - actions.added(data); - } - break; - case 'removed': - actions.removed(id); - break; - } - }); + InstanceStatusModel.find(query, options).fetch().forEach(matrixBroadCastActions.added); } Meteor.methods({ @@ -182,7 +172,7 @@ Meteor.methods({ return 'not-authorized'; } - const instance = Meteor.StreamerCentral.instances[streamName]; + const instance = StreamerCentral.instances[streamName]; if (!instance) { return 'stream-not-exists'; } @@ -190,7 +180,7 @@ Meteor.methods({ if (instance.serverOnly) { instance.__emit(eventName, ...args); } else { - Meteor.StreamerCentral.instances[streamName]._emit(eventName, args); + StreamerCentral.instances[streamName]._emit(eventName, args); } }, }); @@ -233,7 +223,7 @@ function startStreamCastBroadcast(value) { return 'not-authorized'; } - const instance = Meteor.StreamerCentral.instances[streamName]; + const instance = StreamerCentral.instances[streamName]; if (!instance) { return 'stream-not-exists'; } @@ -309,9 +299,7 @@ function startStreamBroadcast() { return results; } - const onBroadcast = function(streamName, eventName, args) { - return broadcast(streamName, eventName, args); - }; + const onBroadcast = Meteor.bindEnvironment(broadcast); let TroubleshootDisableInstanceBroadcast; settings.get('Troubleshoot_Disable_Instance_Broadcast', (key, value) => { @@ -319,10 +307,10 @@ function startStreamBroadcast() { TroubleshootDisableInstanceBroadcast = value; if (value) { - return Meteor.StreamerCentral.removeListener('broadcast', onBroadcast); + return StreamerCentral.removeListener('broadcast', onBroadcast); } - Meteor.StreamerCentral.on('broadcast', onBroadcast); + StreamerCentral.on('broadcast', onBroadcast); }); } diff --git a/typings.d.ts b/typings.d.ts index 1362f561642..312f9003079 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -5,3 +5,17 @@ declare module 'meteor/ddp-common'; declare module 'meteor/routepolicy'; declare module 'xml-encryption'; declare module 'webdav'; + +declare module 'meteor/konecty:user-presence' { + namespace UserPresenceMonitor { + function processUserSession(userSession: any, event: string): void; + } + + namespace UserPresence { + function removeConnectionsByInstanceId(id: string): void; + } +} + +declare const Package: { + 'disable-oplog': object; +};