From 85ddfb24baccdcbae56ffaf7a070b83128b4c7fb Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:46:51 -0300 Subject: [PATCH 1/8] fix: licenses.info endpoint only available for admins (#30644) --- apps/meteor/ee/server/api/licenses.ts | 7 ++- .../tests/end-to-end/api/20-licenses.js | 46 +++++++++++++++++++ .../license/src/definition/LicenseInfo.ts | 10 ++++ ee/packages/license/src/index.ts | 11 ++--- ee/packages/license/src/license.ts | 19 +++++--- packages/rest-typings/src/v1/licenses.ts | 8 +--- 6 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 ee/packages/license/src/definition/LicenseInfo.ts diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index ff5c3fcc3e47..b7ac3ba81e9c 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -25,10 +25,13 @@ API.v1.addRoute( API.v1.addRoute( 'licenses.info', - { authRequired: true, validateParams: isLicensesInfoProps, permissionsRequired: ['view-privileged-setting'] }, + { authRequired: true, validateParams: isLicensesInfoProps }, { async get() { - const data = await License.getInfo(Boolean(this.queryParams.loadValues)); + const unrestrictedAccess = await hasPermissionAsync(this.userId, 'view-privileged-setting'); + const loadCurrentValues = unrestrictedAccess && Boolean(this.queryParams.loadValues); + + const data = await License.getInfo({ limits: unrestrictedAccess, license: unrestrictedAccess, currentValues: loadCurrentValues }); return API.v1.success({ data }); }, diff --git a/apps/meteor/tests/end-to-end/api/20-licenses.js b/apps/meteor/tests/end-to-end/api/20-licenses.js index 993428d34409..302011addef9 100644 --- a/apps/meteor/tests/end-to-end/api/20-licenses.js +++ b/apps/meteor/tests/end-to-end/api/20-licenses.js @@ -105,6 +105,52 @@ describe('licenses', function () { }); }); + describe('[/licenses.info]', () => { + it('should fail if not logged in', (done) => { + request + .get(api('licenses.info')) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res) => { + expect(res.body).to.have.property('status', 'error'); + expect(res.body).to.have.property('message'); + }) + .end(done); + }); + + it('should return limited information if user is unauthorized', (done) => { + request + .get(api('licenses.info')) + .set(unauthorizedUserCredentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('data').and.to.be.an('object'); + expect(res.body.data).to.not.have.property('license'); + expect(res.body.data).to.have.property('tags').and.to.be.an('array'); + }) + .end(done); + }); + + it('should return unrestricted info if user is logged in and is authorized', (done) => { + request + .get(api('licenses.info')) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('data').and.to.be.an('object'); + if (process.env.IS_EE) { + expect(res.body.data).to.have.property('license').and.to.be.an('object'); + } + expect(res.body.data).to.have.property('tags').and.to.be.an('array'); + }) + + .end(done); + }); + }); + describe('[/licenses.isEnterprise]', () => { it('should fail if not logged in', (done) => { request diff --git a/ee/packages/license/src/definition/LicenseInfo.ts b/ee/packages/license/src/definition/LicenseInfo.ts new file mode 100644 index 000000000000..7de3c0cfbdd6 --- /dev/null +++ b/ee/packages/license/src/definition/LicenseInfo.ts @@ -0,0 +1,10 @@ +import type { ILicenseTag } from './ILicenseTag'; +import type { ILicenseV3, LicenseLimitKind } from './ILicenseV3'; +import type { LicenseModule } from './LicenseModule'; + +export type LicenseInfo = { + license?: ILicenseV3; + activeModules: LicenseModule[]; + limits: Record; + tags: ILicenseTag[]; +}; diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index 77e2976f156a..9707a41d96ab 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,5 +1,5 @@ -import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; -import type { LicenseModule } from './definition/LicenseModule'; +import type { LicenseLimitKind } from './definition/ILicenseV3'; +import type { LicenseInfo } from './definition/LicenseInfo'; import type { LimitContext } from './definition/LimitContext'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; @@ -24,6 +24,7 @@ export * from './definition/ILicenseTag'; export * from './definition/ILicenseV2'; export * from './definition/ILicenseV3'; export * from './definition/LicenseBehavior'; +export * from './definition/LicenseInfo'; export * from './definition/LicenseLimit'; export * from './definition/LicenseModule'; export * from './definition/LicensePeriod'; @@ -49,11 +50,7 @@ interface License { onBehaviorTriggered: typeof onBehaviorTriggered; revalidateLicense: () => Promise; - getInfo: (loadCurrentValues: boolean) => Promise<{ - license: ILicenseV3 | undefined; - activeModules: LicenseModule[]; - limits: Record; - }>; + getInfo: (info: { limits: boolean; currentValues: boolean; license: boolean }) => Promise; // Deprecated: onLicense: typeof onLicense; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 8449d4136810..d24d91287d1e 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -4,6 +4,7 @@ import { type ILicenseTag } from './definition/ILicenseTag'; import type { ILicenseV2 } from './definition/ILicenseV2'; import type { ILicenseV3, LicenseLimitKind } from './definition/ILicenseV3'; import type { BehaviorWithContext } from './definition/LicenseBehavior'; +import type { LicenseInfo } from './definition/LicenseInfo'; import type { LicenseModule } from './definition/LicenseModule'; import type { LicenseValidationOptions } from './definition/LicenseValidationOptions'; import type { LimitContext } from './definition/LimitContext'; @@ -291,17 +292,22 @@ export class LicenseManager extends Emitter { return isBehaviorsInResult(validationResult, ['prevent_action']); } - public async getInfo(loadCurrentValues = false): Promise<{ - license: ILicenseV3 | undefined; - activeModules: LicenseModule[]; - limits: Record; - }> { + public async getInfo({ + limits: includeLimits, + currentValues: loadCurrentValues, + license: includeLicense, + }: { + limits: boolean; + currentValues: boolean; + license: boolean; + }): Promise { const activeModules = getModules.call(this); const license = this.getLicense(); // Get all limits present in the license and their current value const limits = ( (license && + includeLimits && (await Promise.all( globalLimitKinds .map((limitKey) => ({ @@ -322,9 +328,10 @@ export class LicenseManager extends Emitter { ).reduce((prev, curr) => ({ ...prev, ...curr }), {}); return { - license, + license: (includeLicense && license) || undefined, activeModules, limits: limits as Record, + tags: license?.information.tags || [], }; } } diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts index 87c0106f6d3f..d229ca49f1fc 100644 --- a/packages/rest-typings/src/v1/licenses.ts +++ b/packages/rest-typings/src/v1/licenses.ts @@ -1,4 +1,4 @@ -import type { ILicenseV2, ILicenseV3, LicenseLimitKind } from '@rocket.chat/license'; +import type { ILicenseV2, ILicenseV3, LicenseInfo } from '@rocket.chat/license'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -45,11 +45,7 @@ export type LicensesEndpoints = { }; '/v1/licenses.info': { GET: (params: licensesInfoProps) => { - data: { - license: ILicenseV3 | undefined; - activeModules: string[]; - limits: Record; - }; + data: LicenseInfo; }; }; '/v1/licenses.add': { From ff2263a3c11d59f9e964c4f1f6b6926521f9283c Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:25:51 -0300 Subject: [PATCH 2/8] fix: Read receipts are not created on the first time a user reads a room (#30610) Co-authored-by: Heitor Tanoue <68477006+heitortanoue@users.noreply.github.com> --- .changeset/weak-cameras-pay.md | 5 +++++ apps/meteor/server/lib/readMessages.ts | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/weak-cameras-pay.md diff --git a/.changeset/weak-cameras-pay.md b/.changeset/weak-cameras-pay.md new file mode 100644 index 000000000000..724f3af69a29 --- /dev/null +++ b/.changeset/weak-cameras-pay.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with message read receipts not being created when accessing a room the first time diff --git a/apps/meteor/server/lib/readMessages.ts b/apps/meteor/server/lib/readMessages.ts index 00bf04bd3449..d7c8cf559288 100644 --- a/apps/meteor/server/lib/readMessages.ts +++ b/apps/meteor/server/lib/readMessages.ts @@ -6,7 +6,7 @@ import { callbacks } from '../../lib/callbacks'; export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThreads: boolean): Promise { await callbacks.run('beforeReadMessages', rid, uid); - const projection = { ls: 1, tunread: 1, alert: 1 }; + const projection = { ls: 1, tunread: 1, alert: 1, ts: 1 }; const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, { projection }); if (!sub) { throw new Error('error-invalid-subscription'); @@ -19,5 +19,6 @@ export async function readMessages(rid: IRoom['_id'], uid: IUser['_id'], readThr await NotificationQueue.clearQueueByUserId(uid); - callbacks.runAsync('afterReadMessages', rid, { uid, lastSeen: sub.ls }); + const lastSeen = sub.ls || sub.ts; + callbacks.runAsync('afterReadMessages', rid, { uid, lastSeen }); } From dd5b236895f754bbec857fe9c0d4f17ecaa28465 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:07:28 -0300 Subject: [PATCH 3/8] chore: remove license v3 public key envvar (#30646) --- ee/packages/license/babel.config.json | 11 ----- ee/packages/license/package.json | 9 +--- ee/packages/license/src/token.ts | 2 +- yarn.lock | 65 +++------------------------ 4 files changed, 7 insertions(+), 80 deletions(-) delete mode 100644 ee/packages/license/babel.config.json diff --git a/ee/packages/license/babel.config.json b/ee/packages/license/babel.config.json deleted file mode 100644 index e154c0813530..000000000000 --- a/ee/packages/license/babel.config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "presets": ["@babel/preset-typescript"], - "plugins": [ - [ - "transform-inline-environment-variables", - { - "include": ["LICENSE_PUBLIC_KEY_V3"] - } - ] - ] -} diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 6810f53e40dd..ec79532a9680 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -3,18 +3,11 @@ "version": "0.0.1", "private": true, "devDependencies": { - "@babel/cli": "^7.23.0", - "@babel/core": "^7.23.0", - "@babel/preset-env": "^7.22.20", - "@babel/preset-typescript": "^7.23.0", "@swc/core": "^1.3.66", "@swc/jest": "^0.2.26", - "@types/babel__core": "^7", - "@types/babel__preset-env": "^7", "@types/bcrypt": "^5.0.0", "@types/jest": "~29.5.3", "@types/ws": "^8.5.5", - "babel-plugin-transform-inline-environment-variables": "^0.4.4", "eslint": "~8.45.0", "jest": "~29.6.1", "jest-environment-jsdom": "~29.6.1", @@ -29,7 +22,7 @@ "testunit": "jest", "build": "npm run build:types && npm run build:js", "build:types": "tsc --emitDeclarationOnly", - "build:js": "babel src --out-dir dist --extensions \".ts,.tsx\" --source-maps inline", + "build:js": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, "main": "./dist/index.js", diff --git a/ee/packages/license/src/token.ts b/ee/packages/license/src/token.ts index 2a9836a48303..46daaef83974 100644 --- a/ee/packages/license/src/token.ts +++ b/ee/packages/license/src/token.ts @@ -7,7 +7,7 @@ import type { ILicenseV3 } from './definition/ILicenseV3'; const PUBLIC_LICENSE_KEY_V2 = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUFxV1Nza2Q5LzZ6Ung4a3lQY2ljcwpiMzJ3Mnd4VnV3N3lCVDk2clEvOEQreU1lQ01POXdTU3BIYS85bkZ5d293RXRpZ3B0L3dyb1BOK1ZHU3didHdQCkZYQmVxRWxCbmRHRkFsODZlNStFbGlIOEt6L2hHbkNtSk5tWHB4RUsyUkUwM1g0SXhzWVg3RERCN010eC9pcXMKY2pCL091dlNCa2ppU2xlUzdibE5JVC9kQTdLNC9DSjNvaXUwMmJMNEV4Y2xDSGVwenFOTWVQM3dVWmdweE9uZgpOT3VkOElYWUs3M3pTY3VFOEUxNTdZd3B6Q0twVmFIWDdaSmY4UXVOc09PNVcvYUlqS2wzTDYyNjkrZUlPRXJHCndPTm1hSG56Zmc5RkxwSmh6Z3BPMzhhVm43NnZENUtLakJhaldza1krNGEyZ1NRbUtOZUZxYXFPb3p5RUZNMGUKY0ZXWlZWWjNMZWg0dkVNb1lWUHlJeng5Nng4ZjIveW1QbmhJdXZRdjV3TjRmeWVwYTdFWTVVQ2NwNzF6OGtmUAo0RmNVelBBMElEV3lNaWhYUi9HNlhnUVFaNEdiL3FCQmh2cnZpSkNGemZZRGNKZ0w3RmVnRllIUDNQR0wwN1FnCnZMZXZNSytpUVpQcnhyYnh5U3FkUE9rZ3VyS2pWclhUVXI0QTlUZ2lMeUlYNVVsSnEzRS9SVjdtZk9xWm5MVGEKU0NWWEhCaHVQbG5DR1pSMDFUb1RDZktoTUcxdTBDRm5MMisxNWhDOWZxT21XdjlRa2U0M3FsSjBQZ0YzVkovWAp1eC9tVHBuazlnbmJHOUpIK21mSDM5Um9GdlROaW5Zd1NNdll6dXRWT242OXNPemR3aERsYTkwbDNBQ2g0eENWCks3Sk9YK3VIa29OdTNnMmlWeGlaVU0wQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo='; -const PUBLIC_LICENSE_KEY_V3 = process.env.PUBLIC_LICENSE_KEY_V3 || PUBLIC_LICENSE_KEY_V2; +const PUBLIC_LICENSE_KEY_V3 = PUBLIC_LICENSE_KEY_V2; let TEST_KEYS: [string, string] | undefined = undefined; diff --git a/yarn.lock b/yarn.lock index b4e4af200f30..ce6dc859fc3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,33 +966,6 @@ __metadata: languageName: node linkType: hard -"@babel/cli@npm:^7.23.0": - version: 7.23.0 - resolution: "@babel/cli@npm:7.23.0" - dependencies: - "@jridgewell/trace-mapping": ^0.3.17 - "@nicolo-ribaudo/chokidar-2": 2.1.8-no-fsevents.3 - chokidar: ^3.4.0 - commander: ^4.0.1 - convert-source-map: ^2.0.0 - fs-readdir-recursive: ^1.1.0 - glob: ^7.2.0 - make-dir: ^2.1.0 - slash: ^2.0.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - dependenciesMeta: - "@nicolo-ribaudo/chokidar-2": - optional: true - chokidar: - optional: true - bin: - babel: ./bin/babel.js - babel-external-helpers: ./bin/babel-external-helpers.js - checksum: beeb189560bf9c4ea951ef637eefa5214654678fb09c4aaa6695921037059c1e1553c610fe95fbd19a9cdfd9f5598a812fc13df40a6b9a9ea899e43fc6c42052 - languageName: node - linkType: hard - "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -1043,7 +1016,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.4, @babel/core@npm:^7.23.0, @babel/core@npm:^7.7.5": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.4, @babel/core@npm:^7.7.5": version: 7.23.0 resolution: "@babel/core@npm:7.23.0" dependencies: @@ -2513,7 +2486,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.22.20, @babel/preset-env@npm:~7.22.10, @babel/preset-env@npm:~7.22.9": +"@babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:~7.22.10, @babel/preset-env@npm:~7.22.9": version: 7.22.20 resolution: "@babel/preset-env@npm:7.22.20" dependencies: @@ -2645,7 +2618,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:^7.12.7, @babel/preset-typescript@npm:^7.23.0": +"@babel/preset-typescript@npm:^7.12.7": version: 7.23.0 resolution: "@babel/preset-typescript@npm:7.23.0" dependencies: @@ -4588,13 +4561,6 @@ __metadata: languageName: node linkType: hard -"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3": - version: 2.1.8-no-fsevents.3 - resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3" - checksum: ee55cc9241aeea7eb94b8a8551bfa4246c56c53bc71ecda0a2104018fcc328ba5723b33686bdf9cc65d4df4ae65e8016b89e0bbdeb94e0309fe91bb9ced42344 - languageName: node - linkType: hard - "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -8474,21 +8440,14 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/license@workspace:ee/packages/license" dependencies: - "@babel/cli": ^7.23.0 - "@babel/core": ^7.23.0 - "@babel/preset-env": ^7.22.20 - "@babel/preset-typescript": ^7.23.0 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/jwt": "workspace:^" "@rocket.chat/logger": "workspace:^" "@swc/core": ^1.3.66 "@swc/jest": ^0.2.26 - "@types/babel__core": ^7 - "@types/babel__preset-env": ^7 "@types/bcrypt": ^5.0.0 "@types/jest": ~29.5.3 "@types/ws": ^8.5.5 - babel-plugin-transform-inline-environment-variables: ^0.4.4 bcrypt: ^5.0.1 eslint: ~8.45.0 jest: ~29.6.1 @@ -15433,13 +15392,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-transform-inline-environment-variables@npm:^0.4.4": - version: 0.4.4 - resolution: "babel-plugin-transform-inline-environment-variables@npm:0.4.4" - checksum: fa361287411301237fd8ce332aff4f8e8ccb8db30e87a2ddc7224c8bf7cd792eda47aca24dc2e09e70bce4c027bc8cbe22f4999056be37a25d2472945df21ef5 - languageName: node - linkType: hard - "babel-polyfill@npm:^6.2.0": version: 6.26.0 resolution: "babel-polyfill@npm:6.26.0" @@ -16964,7 +16916,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -17513,7 +17465,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^4.0.0, commander@npm:^4.0.1, commander@npm:^4.1.1": +"commander@npm:^4.0.0, commander@npm:^4.1.1": version: 4.1.1 resolution: "commander@npm:4.1.1" checksum: d7b9913ff92cae20cb577a4ac6fcc121bd6223319e54a40f51a14740a681ad5c574fd29a57da478a5f234a6fa6c52cbf0b7c641353e03c648b1ae85ba670b977 @@ -22120,13 +22072,6 @@ __metadata: languageName: node linkType: hard -"fs-readdir-recursive@npm:^1.1.0": - version: 1.1.0 - resolution: "fs-readdir-recursive@npm:1.1.0" - checksum: 29d50f3d2128391c7fc9fd051c8b7ea45bcc8aa84daf31ef52b17218e20bfd2bd34d02382742801954cc8d1905832b68227f6b680a666ce525d8b6b75068ad1e - languageName: node - linkType: hard - "fs-write-stream-atomic@npm:^1.0.8": version: 1.0.10 resolution: "fs-write-stream-atomic@npm:1.0.10" From d6fa895e84007a898e410691292f4647dcc3acb6 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 17 Oct 2023 13:55:56 -0600 Subject: [PATCH 4/8] refactor: Move functions out of `Livechat.js` (#30650) --- .../app/apps/server/bridges/livechat.ts | 5 +- .../app/livechat/server/api/lib/livechat.ts | 3 +- .../app/livechat/server/api/v1/message.ts | 4 +- .../livechat/server/api/v1/offlineMessage.ts | 2 +- .../meteor/app/livechat/server/api/v1/room.ts | 14 +- apps/meteor/app/livechat/server/lib/Helper.ts | 3 + .../app/livechat/server/lib/Livechat.js | 213 ------------------ .../app/livechat/server/lib/LivechatTyped.ts | 208 ++++++++++++++++- .../livechat/server/lib/stream/agentStatus.ts | 2 +- .../server/methods/sendOfflineMessage.ts | 2 +- .../server/methods/setDepartmentForVisitor.ts | 2 +- .../app/livechat/server/methods/transfer.ts | 6 +- .../core-typings/src/omnichannel/routing.ts | 4 +- 13 files changed, 236 insertions(+), 232 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 0ace08bb8446..5b6c76257667 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -74,7 +74,8 @@ export class AppLivechatBridge extends LivechatBridge { message: await this.orch.getConverters()?.get('messages').convertAppMessage(message), }; - await Livechat.updateMessage(data); + // @ts-expect-error IVisitor vs ILivechatVisitor :( + await LivechatTyped.updateMessage(data); } protected async createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise { @@ -208,7 +209,7 @@ export class AppLivechatBridge extends LivechatBridge { userId = transferredTo._id; } - return Livechat.transfer( + return LivechatTyped.transfer( await this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom), this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), { userId, departmentId, transferredBy, transferredTo }, diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index 7bb608090557..2b72065345d6 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -13,7 +13,6 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { normalizeAgent } from '../../lib/Helper'; -import { Livechat } from '../../lib/Livechat'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; export function online(department: string, skipSettingCheck = false, skipFallbackCheck = false): Promise { @@ -139,7 +138,7 @@ export function normalizeHttpHeaderData(headers: Record> { // Putting this ugly conversion while we type the livechat service - const initSettings = (await Livechat.getInitSettings()) as unknown as Record; + const initSettings = await LivechatTyped.getInitSettings(); const triggers = await findTriggers(); const departments = await findDepartments(businessUnit); const sound = `${Meteor.absoluteUrl()}sounds/chime.mp3`; diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 104e2ece94d5..0d5a22b90d89 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -134,9 +134,9 @@ API.v1.addRoute( throw new Error('invalid-message'); } - const result = await Livechat.updateMessage({ + const result = await LivechatTyped.updateMessage({ guest, - message: { _id: msg._id, msg: this.bodyParams.msg }, + message: { _id: msg._id, msg: this.bodyParams.msg, rid: msg.rid }, }); if (!result) { return API.v1.failure(); diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts index b01e60d2265f..6acd6ab98ea1 100644 --- a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts +++ b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts @@ -2,7 +2,7 @@ import { isPOSTLivechatOfflineMessageParams } from '@rocket.chat/rest-typings'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; -import { Livechat } from '../../lib/Livechat'; +import { Livechat } from '../../lib/LivechatTyped'; API.v1.addRoute( 'livechat/offline.message', diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 8f6151797463..4f3b4eb6234d 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -251,7 +251,7 @@ API.v1.addRoute( const { _id, username, name } = guest; const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); - if (!(await Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferredBy }))) { + if (!(await LivechatTyped.transfer(room, guest, { departmentId: department, transferredBy }))) { return API.v1.failure(); } @@ -312,10 +312,10 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'], validateParams: isLiveChatRoomForwardProps }, { async post() { - const transferData: typeof this.bodyParams & { - transferredBy?: unknown; + const transferData = this.bodyParams as typeof this.bodyParams & { + transferredBy: TransferByData; transferredTo?: { _id: string; username?: string; name?: string }; - } = this.bodyParams; + }; const room = await LivechatRooms.findOneById(this.bodyParams.roomId); if (!room || room.t !== 'l') { @@ -327,6 +327,10 @@ API.v1.addRoute( } const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); + if (!guest) { + throw new Error('error-invalid-visitor'); + } + const transferedBy = this.user satisfies TransferByData; transferData.transferredBy = normalizeTransferredByData(transferedBy, room); if (transferData.userId) { @@ -340,7 +344,7 @@ API.v1.addRoute( } } - const chatForwardedResult = await Livechat.transfer(room, guest, transferData); + const chatForwardedResult = await LivechatTyped.transfer(room, guest, transferData); if (!chatForwardedResult) { throw new Error('error-forwarding-chat'); } diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 75722e709b17..63cbbd6998ef 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -402,6 +402,9 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T logger.debug(`Forwarding room ${room._id} to agent ${transferData.userId}`); const { userId: agentId, clientAction } = transferData; + if (!agentId) { + throw new Error('error-invalid-agent'); + } const user = await Users.findOneOnlineAgentById(agentId); if (!user) { logger.debug(`Agent ${agentId} is offline. Cannot forward`); diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index e1d6626c7ddb..837a8eb7309b 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -1,21 +1,15 @@ // Note: Please don't add any new methods to this file, since its still in js and we are migrating to ts // Please add new methods to LivechatTyped.ts - -import dns from 'dns'; -import util from 'util'; - import { Message } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { LivechatVisitors, LivechatCustomField, - Settings, LivechatRooms, LivechatInquiry, Subscriptions, Messages, LivechatDepartment as LivechatDepartmentRaw, - LivechatDepartmentAgents, Rooms, Users, ReadReceipts, @@ -34,7 +28,6 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP import { FileUpload } from '../../../file-upload/server'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; -import { updateMessage } from '../../../lib/server/functions/updateMessage'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; @@ -45,8 +38,6 @@ import { RoutingManager } from './RoutingManager'; const logger = new Logger('Livechat'); -const dnsResolveMx = util.promisify(dns.resolveMx); - export const Livechat = { Analytics, @@ -63,28 +54,6 @@ export const Livechat = { }); }, - async updateMessage({ guest, message }) { - check(message, Match.ObjectIncluding({ _id: String })); - - const originalMessage = await Messages.findOneById(message._id); - if (!originalMessage || !originalMessage._id) { - return; - } - - const editAllowed = settings.get('Message_AllowEditing'); - const editOwn = originalMessage.u && originalMessage.u._id === guest._id; - - if (!editAllowed || !editOwn) { - throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', { - method: 'livechatUpdateMessage', - }); - } - - await updateMessage(message, guest); - - return true; - }, - async deleteMessage({ guest, message }) { Livechat.logger.debug(`Attempting to delete a message by visitor ${guest._id}`); check(message, Match.ObjectIncluding({ _id: String })); @@ -188,50 +157,6 @@ export const Livechat = { return 0; }, - async getInitSettings() { - const rcSettings = {}; - - await Settings.findNotHiddenPublic([ - 'Livechat_title', - 'Livechat_title_color', - 'Livechat_enable_message_character_limit', - 'Livechat_message_character_limit', - 'Message_MaxAllowedSize', - 'Livechat_enabled', - 'Livechat_registration_form', - 'Livechat_allow_switching_departments', - 'Livechat_offline_title', - 'Livechat_offline_title_color', - 'Livechat_offline_message', - 'Livechat_offline_success_message', - 'Livechat_offline_form_unavailable', - 'Livechat_display_offline_form', - 'Omnichannel_call_provider', - 'Language', - 'Livechat_enable_transcript', - 'Livechat_transcript_message', - 'Livechat_fileupload_enabled', - 'FileUpload_Enabled', - 'Livechat_conversation_finished_message', - 'Livechat_conversation_finished_text', - 'Livechat_name_field_registration_form', - 'Livechat_email_field_registration_form', - 'Livechat_registration_form_message', - 'Livechat_force_accept_data_processing_consent', - 'Livechat_data_processing_consent_text', - 'Livechat_show_agent_info', - 'Livechat_clear_local_storage_when_chat_ended', - ]).forEach((setting) => { - rcSettings[setting._id] = setting.value; - }); - - rcSettings.Livechat_history_monitor_type = settings.get('Livechat_history_monitor_type'); - - rcSettings.Livechat_Show_Connecting = this.showConnecting(); - - return rcSettings; - }, - async saveRoomInfo(roomData, guestData, userId) { Livechat.logger.debug(`Saving room information on room ${roomData._id}`); const { livechatData = {} } = roomData; @@ -280,35 +205,6 @@ export const Livechat = { } }, - async closeOpenChats(userId, comment) { - Livechat.logger.debug(`Closing open chats for user ${userId}`); - const user = await Users.findOneById(userId); - - const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); - const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); - const promises = []; - await openChats.forEach((room) => { - promises.push(LivechatTyped.closeRoom({ user, room, comment })); - }); - - await Promise.all(promises); - }, - - async forwardOpenChats(userId) { - Livechat.logger.debug(`Transferring open chats for user ${userId}`); - for await (const room of LivechatRooms.findOpenByAgent(userId)) { - const guest = await LivechatVisitors.findOneEnabledById(room.v._id); - const user = await Users.findOneById(userId); - const { _id, username, name } = user; - const transferredBy = normalizeTransferredByData({ _id, username, name }, room); - await this.transfer(room, guest, { - roomId: room._id, - transferredBy, - departmentId: guest.department, - }); - } - }, - async savePageHistory(token, roomId, pageInfo) { Livechat.logger.debug(`Saving page movement history for visitor with token ${token}`); if (pageInfo.change !== settings.get('Livechat_history_monitor_type')) { @@ -387,23 +283,6 @@ export const Livechat = { await sendMessage(transferredBy, transferMessage, room); }, - async transfer(room, guest, transferData) { - Livechat.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); - if (room.onHold) { - Livechat.logger.debug('Cannot transfer. Room is on hold'); - throw new Error('error-room-onHold'); - } - - if (transferData.departmentId) { - transferData.department = await LivechatDepartmentRaw.findOneById(transferData.departmentId, { - projection: { name: 1 }, - }); - Livechat.logger.debug(`Transfering room ${room._id} to department ${transferData.department?._id}`); - } - - return RoutingManager.transferRoom(room, guest, transferData); - }, - async returnRoomAsInquiry(rid, departmentId, overrideTransferData = {}) { Livechat.logger.debug(`Transfering room ${rid} to ${departmentId ? 'department' : ''} queue`); const room = await LivechatRooms.findOneById(rid); @@ -682,41 +561,6 @@ export const Livechat = { return updateDepartmentAgents(_id, departmentAgents, department.enabled); }, - /* - * @deprecated - Use the equivalent from DepartmentHelpers class - */ - async removeDepartment(_id) { - check(_id, String); - - const departmentRemovalEnabled = settings.get('Omnichannel_enable_department_removal'); - - if (!departmentRemovalEnabled) { - throw new Meteor.Error('department-removal-disabled', 'Department removal is disabled', { - method: 'livechat:removeDepartment', - }); - } - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - const ret = (await LivechatDepartmentRaw.removeById(_id)).deletedCount; - const agentsIds = (await LivechatDepartmentAgents.findByDepartmentId(_id, { projection: { agentId: 1 } }).toArray()).map( - (agent) => agent.agentId, - ); - await LivechatDepartmentAgents.removeByDepartmentId(_id); - await LivechatDepartmentRaw.unsetFallbackDepartmentByDepartmentId(_id); - if (ret) { - setImmediate(() => { - callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); - }); - } - return ret; - }, - showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; @@ -778,63 +622,6 @@ export const Livechat = { await LivechatRooms.updateVisitorStatus(token, status); }, - async sendOfflineMessage(data = {}) { - if (!settings.get('Livechat_display_offline_form')) { - throw new Error('error-offline-form-disabled'); - } - - const { message, name, email, department, host } = data; - const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); - - let html = '

New livechat message

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

Sent from: ${host}

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

Visitor name: ${name}

-

Visitor email: ${email}

-

Message:
${emailMessage}

`); - - let fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); - - if (fromEmail) { - fromEmail = fromEmail[0]; - } else { - fromEmail = settings.get('From_Email'); - } - - if (settings.get('Livechat_validate_offline_email')) { - const emailDomain = email.substr(email.lastIndexOf('@') + 1); - - try { - await dnsResolveMx(emailDomain); - } catch (e) { - throw new Meteor.Error('error-invalid-email-address', 'Invalid email address', { - method: 'livechat:sendOfflineMessage', - }); - } - } - - // TODO Block offline form if Livechat_offline_email is undefined - // (it does not make sense to have an offline form that does nothing) - // `this.sendEmail` will throw an error if the email is invalid - // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client - let emailTo = settings.get('Livechat_offline_email'); - if (department && department !== '') { - const dep = await LivechatDepartmentRaw.findOneByIdOrName(department); - emailTo = dep.email || emailTo; - } - - const from = `${name} - ${email} <${fromEmail}>`; - const replyTo = `${name} <${email}>`; - const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; - await this.sendEmail(from, emailTo, replyTo, subject, html); - - setImmediate(() => { - callbacks.run('livechat.offlineMessage', data); - }); - }, - async allowAgentChangeServiceStatus(statusLivechat, agentId) { if (statusLivechat !== 'available') { return true; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index afb649488300..293b15e8d63c 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,3 +1,6 @@ +import dns from 'dns'; +import * as util from 'util'; + import { Message, VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, @@ -10,6 +13,8 @@ import type { ILivechatAgent, IMessage, ILivechatDepartment, + AtLeast, + TransferData, } from '@rocket.chat/core-typings'; import { UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; @@ -24,6 +29,7 @@ import { LivechatDepartmentAgents, ReadReceipts, Rooms, + Settings, } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -41,7 +47,7 @@ import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { getTimezone } from '../../../utils/server/lib/getTimezone'; -import { updateDepartmentAgents, validateEmail } from './Helper'; +import { updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; @@ -75,6 +81,16 @@ export type CloseRoomParamsByVisitor = { export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor; +type OfflineMessageData = { + message: string; + name: string; + email: string; + department?: string; + host?: string; +}; + +const dnsResolveMx = util.promisify(dns.resolveMx); + class LivechatClass { logger: Logger; @@ -917,6 +933,196 @@ class LivechatClass { await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); return true; } + + async updateMessage({ guest, message }: { guest: ILivechatVisitor; message: AtLeast }) { + check(message, Match.ObjectIncluding({ _id: String })); + + const originalMessage = await Messages.findOneById>(message._id, { projection: { u: 1 } }); + if (!originalMessage?._id) { + return; + } + + const editAllowed = settings.get('Message_AllowEditing'); + const editOwn = originalMessage.u && originalMessage.u._id === guest._id; + + if (!editAllowed || !editOwn) { + throw new Error('error-action-not-allowed'); + } + + // TODO: Apps sends an `any` object and apparently we just check for _id being present + // while updateMessage expects AtLeast + await updateMessage(message, guest as unknown as IUser); + + return true; + } + + async closeOpenChats(userId: string, comment?: string) { + this.logger.debug(`Closing open chats for user ${userId}`); + const user = await Users.findOneById(userId); + + const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId }); + const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery); + const promises: Promise[] = []; + await openChats.forEach((room) => { + promises.push(this.closeRoom({ user, room, comment })); + }); + + await Promise.all(promises); + } + + async transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) { + this.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); + if (room.onHold) { + throw new Error('error-room-onHold'); + } + + if (transferData.departmentId) { + const department = await LivechatDepartment.findOneById(transferData.departmentId, { + projection: { name: 1 }, + }); + if (!department) { + throw new Error('error-invalid-department'); + } + + transferData.department = department; + this.logger.debug(`Transfering room ${room._id} to department ${transferData.department?._id}`); + } + + return RoutingManager.transferRoom(room, guest, transferData); + } + + async forwardOpenChats(userId: string) { + this.logger.debug(`Transferring open chats for user ${userId}`); + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('error-invalid-user'); + } + + const { _id, username, name } = user; + for await (const room of LivechatRooms.findOpenByAgent(userId)) { + const guest = await LivechatVisitors.findOneEnabledById(room.v._id); + if (!guest) { + continue; + } + + const transferredBy = normalizeTransferredByData({ _id, username, name }, room); + await this.transfer(room, guest, { + transferredBy, + departmentId: guest.department, + }); + } + } + + showConnecting() { + return RoutingManager.getConfig()?.showConnecting || false; + } + + async getInitSettings() { + const rcSettings: Record = {}; + + await Settings.findNotHiddenPublic([ + 'Livechat_title', + 'Livechat_title_color', + 'Livechat_enable_message_character_limit', + 'Livechat_message_character_limit', + 'Message_MaxAllowedSize', + 'Livechat_enabled', + 'Livechat_registration_form', + 'Livechat_allow_switching_departments', + 'Livechat_offline_title', + 'Livechat_offline_title_color', + 'Livechat_offline_message', + 'Livechat_offline_success_message', + 'Livechat_offline_form_unavailable', + 'Livechat_display_offline_form', + 'Omnichannel_call_provider', + 'Language', + 'Livechat_enable_transcript', + 'Livechat_transcript_message', + 'Livechat_fileupload_enabled', + 'FileUpload_Enabled', + 'Livechat_conversation_finished_message', + 'Livechat_conversation_finished_text', + 'Livechat_name_field_registration_form', + 'Livechat_email_field_registration_form', + 'Livechat_registration_form_message', + 'Livechat_force_accept_data_processing_consent', + 'Livechat_data_processing_consent_text', + 'Livechat_show_agent_info', + 'Livechat_clear_local_storage_when_chat_ended', + ]).forEach((setting) => { + rcSettings[setting._id] = setting.value; + }); + + rcSettings.Livechat_history_monitor_type = settings.get('Livechat_history_monitor_type'); + + rcSettings.Livechat_Show_Connecting = this.showConnecting(); + + return rcSettings; + } + + async sendOfflineMessage(data: OfflineMessageData) { + if (!settings.get('Livechat_display_offline_form')) { + throw new Error('error-offline-form-disabled'); + } + + const { message, name, email, department, host } = data; + + if (!email) { + throw new Error('error-invalid-email'); + } + + const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); + + let html = '

New livechat message

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

Sent from: ${host}

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

Visitor name: ${name}

+

Visitor email: ${email}

+

Message:
${emailMessage}

`); + + const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); + + let from: string; + if (fromEmail) { + from = fromEmail[0]; + } else { + from = settings.get('From_Email'); + } + + if (settings.get('Livechat_validate_offline_email')) { + const emailDomain = email.substr(email.lastIndexOf('@') + 1); + + try { + await dnsResolveMx(emailDomain); + } catch (e) { + throw new Meteor.Error('error-invalid-email-address'); + } + } + + // TODO Block offline form if Livechat_offline_email is undefined + // (it does not make sense to have an offline form that does nothing) + // `this.sendEmail` will throw an error if the email is invalid + // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client + let emailTo = settings.get('Livechat_offline_email'); + if (department && department !== '') { + const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { email: 1 } }); + if (dep) { + emailTo = dep.email || emailTo; + } + } + + const fromText = `${name} - ${email} <${from}>`; + const replyTo = `${name} <${email}>`; + const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; + await this.sendEmail(fromText, emailTo, replyTo, subject, html); + + setImmediate(() => { + void callbacks.run('livechat.offlineMessage', data); + }); + } } export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts index bbce5d16efb4..5ddd25e90bd2 100644 --- a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts +++ b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts @@ -1,7 +1,7 @@ import { Logger } from '@rocket.chat/logger'; import { settings } from '../../../../settings/server'; -import { Livechat } from '../Livechat'; +import { Livechat } from '../LivechatTyped'; const logger = new Logger('AgentStatusWatcher'); diff --git a/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts b/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts index 9a475de5e32d..c3b5537f31be 100644 --- a/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts +++ b/apps/meteor/app/livechat/server/methods/sendOfflineMessage.ts @@ -4,7 +4,7 @@ import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.ts b/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.ts index 61e6b21267da..a14933ed8d47 100644 --- a/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.ts +++ b/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.ts @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { normalizeTransferredByData } from '../lib/Helper'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/meteor/app/livechat/server/methods/transfer.ts b/apps/meteor/app/livechat/server/methods/transfer.ts index 3817b10bf42b..16ee1abc6191 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.ts +++ b/apps/meteor/app/livechat/server/methods/transfer.ts @@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { normalizeTransferredByData } from '../lib/Helper'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -60,6 +60,10 @@ Meteor.methods({ const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); + if (!guest) { + throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor', { method: 'livechat:transfer' }); + } + const user = await Meteor.userAsync(); if (!user) { diff --git a/packages/core-typings/src/omnichannel/routing.ts b/packages/core-typings/src/omnichannel/routing.ts index eed6dd6f1a19..43ca0c08f5d2 100644 --- a/packages/core-typings/src/omnichannel/routing.ts +++ b/packages/core-typings/src/omnichannel/routing.ts @@ -24,7 +24,7 @@ export interface IRoutingMethod { } export type TransferData = { - userId: string; + userId?: string; departmentId?: string; department?: Pick; transferredBy: { @@ -36,7 +36,7 @@ export type TransferData = { name?: string; }; clientAction?: boolean; - scope: 'agent' | 'department' | 'queue' | 'autoTransferUnansweredChatsToAgent' | 'autoTransferUnansweredChatsToQueue'; + scope?: 'agent' | 'department' | 'queue' | 'autoTransferUnansweredChatsToAgent' | 'autoTransferUnansweredChatsToQueue'; comment?: string; }; From 3b5310cf2350b93ab5f171d5d547434e6a9f46c5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 18 Oct 2023 05:48:04 -0700 Subject: [PATCH 5/8] regression: Restore default limits to community apps (#30611) Co-authored-by: Rodrigo Nascimento --- ee/packages/license/src/deprecated.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/packages/license/src/deprecated.ts b/ee/packages/license/src/deprecated.ts index 65851a79c7eb..0a4a6b0f1bb3 100644 --- a/ee/packages/license/src/deprecated.ts +++ b/ee/packages/license/src/deprecated.ts @@ -23,8 +23,8 @@ export function getMaxActiveUsers(this: LicenseManager) { export function getAppsConfig(this: LicenseManager) { return { - maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps') ?? -1, - maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps') ?? -1, + maxPrivateApps: getLicenseLimit(this.getLicense(), 'privateApps') ?? 3, + maxMarketplaceApps: getLicenseLimit(this.getLicense(), 'marketplaceApps') ?? 5, }; } From 343ba56f44c35ea7044c73f3f9e9af8debd19a79 Mon Sep 17 00:00:00 2001 From: Rafael Tapia Date: Wed, 18 Oct 2023 10:34:02 -0300 Subject: [PATCH 6/8] test: wait for the name update finish (#30663) --- apps/meteor/tests/end-to-end/api/09-rooms.js | 47 ++++++++++---------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index ed3c7eefb15b..10d576c316a2 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -1570,29 +1570,30 @@ describe('[Rooms]', function () { }); }); - it('should update group name if user changes name', (done) => { - updateSetting('UI_Use_Real_Name', true).then(() => { - request - .post(api('users.update')) - .set(credentials) - .send({ - userId: testUser._id, - data: { - name: `changed.name.${testUser.username}`, - }, - }) - .end(() => { - request - .get(api('subscriptions.getOne')) - .set(credentials) - .query({ roomId }) - .end((err, res) => { - const { subscription } = res.body; - expect(subscription.fname).to.equal(`changed.name.${testUser.username}, Rocket.Cat`); - done(); - }); - }); - }); + it('should update group name if user changes name', async () => { + await updateSetting('UI_Use_Real_Name', true); + await request + .post(api('users.update')) + .set(credentials) + .send({ + userId: testUser._id, + data: { + name: `changed.name.${testUser.username}`, + }, + }); + + // need to wait for the name update finish + await sleep(300); + + await request + .get(api('subscriptions.getOne')) + .set(credentials) + .query({ roomId }) + .send() + .expect((res) => { + const { subscription } = res.body; + expect(subscription.fname).to.equal(`changed.name.${testUser.username}, Rocket.Cat`); + }); }); }); From 049b921bc337d1ec802d60f1493414ccac5b4972 Mon Sep 17 00:00:00 2001 From: Noach Magedman Date: Wed, 18 Oct 2023 17:10:01 +0300 Subject: [PATCH 7/8] fix: Handle AWS S3 Re-Authentication via s3.getSignedUrlPromise (#30642) --- apps/meteor/app/file-upload/ufs/AmazonS3/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts index b9f0807b6112..d6b69faf75fa 100644 --- a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts +++ b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts @@ -80,7 +80,7 @@ class AmazonS3Store extends UploadFS.Store { ResponseContentDisposition: `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(file.name || '')}"`, }; - return s3.getSignedUrl('getObject', params); + return s3.getSignedUrlPromise('getObject', params); }; /** From e24d071675c720d8dc947193b180ee2c81cde95b Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 18 Oct 2023 13:17:28 -0300 Subject: [PATCH 8/8] fix: inconsistent behavior when removing subscriptions and inquiries (#30572) --- .changeset/long-cars-dream.md | 5 +++ .../client/lib/stream/queueManager.ts | 26 ++++++++++--- .../client/views/room/hooks/useOpenRoom.ts | 10 +++++ .../views/room/providers/RoomProvider.tsx | 39 +------------------ 4 files changed, 38 insertions(+), 42 deletions(-) create mode 100644 .changeset/long-cars-dream.md diff --git a/.changeset/long-cars-dream.md b/.changeset/long-cars-dream.md new file mode 100644 index 000000000000..95f226d6dfb4 --- /dev/null +++ b/.changeset/long-cars-dream.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed intermittent errors caused by the removal of subscriptions and inquiries when lacking permissions. diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 28d09958535a..906ace402bb9 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -8,18 +8,34 @@ import { LivechatInquiry } from '../../collections/LivechatInquiry'; const departments = new Set(); const events = { - added: (inquiry: ILivechatInquiryRecord) => { - departments.has(inquiry.department) && LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); + added: async (inquiry: ILivechatInquiryRecord) => { + if (!departments.has(inquiry.department)) { + return; + } + + LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); + await invalidateRoomQueries(inquiry.rid); }, changed: async (inquiry: ILivechatInquiryRecord) => { if (inquiry.status !== 'queued' || (inquiry.department && !departments.has(inquiry.department))) { - return LivechatInquiry.remove(inquiry._id); + return removeInquiry(inquiry); } LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); - await queryClient.invalidateQueries(['/v1/rooms.info', inquiry.rid]); + await invalidateRoomQueries(inquiry.rid); }, - removed: (inquiry: ILivechatInquiryRecord) => LivechatInquiry.remove(inquiry._id), + removed: (inquiry: ILivechatInquiryRecord) => removeInquiry(inquiry), +}; + +const invalidateRoomQueries = async (rid: string) => { + await queryClient.invalidateQueries(['rooms', { reference: rid, type: 'l' }]); + await queryClient.removeQueries(['rooms', rid]); + await queryClient.removeQueries(['/v1/rooms.info', rid]); +}; + +const removeInquiry = async (inquiry: ILivechatInquiryRecord) => { + await LivechatInquiry.remove(inquiry._id); + return queryClient.invalidateQueries(['rooms', { reference: inquiry.rid, type: 'l' }]); }; const getInquiriesFromAPI = async () => { diff --git a/apps/meteor/client/views/room/hooks/useOpenRoom.ts b/apps/meteor/client/views/room/hooks/useOpenRoom.ts index c2b694414002..d529145aaf17 100644 --- a/apps/meteor/client/views/room/hooks/useOpenRoom.ts +++ b/apps/meteor/client/views/room/hooks/useOpenRoom.ts @@ -8,6 +8,7 @@ import { omit } from '../../../../lib/utils/omit'; import { NotAuthorizedError } from '../../../lib/errors/NotAuthorizedError'; import { OldUrlRoomError } from '../../../lib/errors/OldUrlRoomError'; import { RoomNotFoundError } from '../../../lib/errors/RoomNotFoundError'; +import { queryClient } from '../../../lib/queryClient'; export function useOpenRoom({ type, reference }: { type: RoomType; reference: string }) { const user = useUser(); @@ -102,6 +103,15 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st }, { retry: 0, + onError: async (error) => { + if (['l', 'v'].includes(type) && error instanceof RoomNotFoundError) { + const { ChatRoom } = await import('../../../../app/models/client'); + + ChatRoom.remove(reference); + queryClient.removeQueries(['rooms', reference]); + queryClient.removeQueries(['/v1/rooms.info', reference]); + } + }, }, ); } diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index e19fa8136f59..82c66c6f5d8d 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -1,10 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { usePermission, useStream, useUserId, useRouter } from '@rocket.chat/ui-contexts'; -import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from '@rocket.chat/ui-contexts'; import type { ReactNode, ContextType, ReactElement } from 'react'; import React, { useMemo, memo, useEffect, useCallback } from 'react'; -import { ChatRoom, ChatSubscription } from '../../../../app/models/client'; +import { ChatSubscription } from '../../../../app/models/client'; import { RoomHistoryManager } from '../../../../app/ui-utils/client'; import { UserAction } from '../../../../app/ui/client/lib/UserAction'; import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; @@ -29,24 +28,6 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { const { data: room, isSuccess } = useRoomQuery(rid); - const subscribeToRoom = useStream('room-data'); - - const queryClient = useQueryClient(); - const userId = useUserId(); - const isLivechatAdmin = usePermission('view-livechat-rooms'); - const { t: roomType } = room ?? {}; - - // TODO: move this to omnichannel context only - useEffect(() => { - if (roomType !== 'l') { - return; - } - - return subscribeToRoom(rid, (room) => { - queryClient.setQueryData(['rooms', rid], room); - }); - }, [subscribeToRoom, rid, queryClient, roomType]); - // TODO: the following effect is a workaround while we don't have a general and definitive solution for it const router = useRouter(); useEffect(() => { @@ -55,22 +36,6 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { } }, [isSuccess, room, router]); - const { _id: servedById } = room?.servedBy ?? {}; - - // TODO: Review the necessity of this effect when we move away from cached collections - useEffect(() => { - if (roomType !== 'l' || !servedById) { - return; - } - - if (!isLivechatAdmin && servedById !== userId) { - ChatRoom.remove(rid); - queryClient.removeQueries(['rooms', rid]); - queryClient.removeQueries(['rooms', { reference: rid, type: 'l' }]); - queryClient.removeQueries(['/v1/rooms.info', rid]); - } - }, [isLivechatAdmin, queryClient, userId, rid, roomType, servedById]); - const subscriptionQuery = useReactiveQuery(['subscriptions', { rid }], () => ChatSubscription.findOne({ rid }) ?? null); const pseudoRoom = useMemo(() => {