diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts index 58b280083eca..19c1c450f5e6 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts @@ -1,8 +1,8 @@ -import { OmnichannelTranscript } from '@rocket.chat/core-services'; import { LivechatRooms } from '@rocket.chat/models'; import { API } from '../../../../../app/api/server'; import { canAccessRoomAsync } from '../../../../../app/authorization/server/functions/canAccessRoom'; +import { requestPdfTranscript } from '../lib/requestPdfTranscript'; API.v1.addRoute( 'omnichannel/:rid/request-transcript', @@ -19,17 +19,11 @@ API.v1.addRoute( } // Flow is as follows: - // 1. Call OmnichannelTranscript.requestTranscript() - // 2. OmnichannelTranscript.requestTranscript() calls QueueWorker.queueWork() - // 3. QueueWorker.queueWork() eventually calls OmnichannelTranscript.workOnPdf() - // 4. OmnichannelTranscript.workOnPdf() calls OmnichannelTranscript.pdfComplete() when processing ends - // 5. OmnichannelTranscript.pdfComplete() sends the messages to the user, and updates the room with the flags - await OmnichannelTranscript.requestTranscript({ - details: { - userId: this.userId, - rid: this.urlParams.rid, - }, - }); + // 1. On Test Mode, call Transcript.workOnPdf directly + // 2. On Normal Mode, call QueueWorker.queueWork to queue the work + // 3. OmnichannelTranscript.workOnPdf will be called by the worker to generate the transcript + // 4. We be happy :) + await requestPdfTranscript(room, this.userId); return API.v1.success(); }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts index ee3fa414433c..159945e57c7a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts @@ -1,9 +1,9 @@ -import { OmnichannelTranscript } from '@rocket.chat/core-services'; import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import type { CloseRoomParams } from '../../../../../app/livechat/server/lib/localTypes'; import { callbacks } from '../../../../../lib/callbacks'; +import { requestPdfTranscript } from '../lib/requestPdfTranscript'; type LivechatCloseCallbackParams = { room: IOmnichannelRoom; @@ -24,12 +24,7 @@ const sendPdfTranscriptOnClose = async (params: LivechatCloseCallbackParams): Pr const { requestedBy } = pdfTranscript; - await OmnichannelTranscript.requestTranscript({ - details: { - userId: requestedBy, - rid: room._id, - }, - }); + await requestPdfTranscript(room, requestedBy); return params; }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/requestPdfTranscript.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/requestPdfTranscript.ts new file mode 100644 index 000000000000..e0bb03e6d850 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/requestPdfTranscript.ts @@ -0,0 +1,40 @@ +import { OmnichannelTranscript, QueueWorker } from '@rocket.chat/core-services'; +import type { AtLeast, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms } from '@rocket.chat/models'; + +import { logger } from './logger'; + +const serviceName = 'omnichannel-transcript' as const; +export const requestPdfTranscript = async ( + room: AtLeast, + requestedBy: string, +): Promise => { + if (room.open) { + throw new Error('room-still-open'); + } + + if (!room.v) { + throw new Error('improper-room-state'); + } + + // Don't request a transcript if there's already one requested :) + if (room.pdfTranscriptRequested) { + // TODO: use logger + logger.info(`Transcript already requested for room ${room._id}`); + return; + } + + // TODO: change this with a timestamp, allowing users to request a transcript again after a while if the first one fails + await LivechatRooms.setTranscriptRequestedPdfById(room._id); + + const details = { details: { rid: room._id, userId: requestedBy, from: serviceName } }; + // Make the whole process sync when running on test mode + // This will prevent the usage of timeouts on the tests of this functionality :) + if (process.env.TEST_MODE) { + await OmnichannelTranscript.workOnPdf(details); + return; + } + + logger.info(`Queuing work for room ${room._id}`); + await QueueWorker.queueWork('work', `${serviceName}.workOnPdf`, details); +}; diff --git a/apps/meteor/tests/unit/server/livechat/lib/requestPdfTranscript.spec.ts b/apps/meteor/tests/unit/server/livechat/lib/requestPdfTranscript.spec.ts new file mode 100644 index 000000000000..706e06704b54 --- /dev/null +++ b/apps/meteor/tests/unit/server/livechat/lib/requestPdfTranscript.spec.ts @@ -0,0 +1,74 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach, after } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const setStub = sinon.stub(); +const workOnPdfStub = sinon.stub(); +const queueWorkStub = sinon.stub(); + +const { requestPdfTranscript } = proxyquire + .noCallThru() + .load('../../../../../ee/app/livechat-enterprise/server/lib/requestPdfTranscript.ts', { + '@rocket.chat/models': { + LivechatRooms: { + setTranscriptRequestedPdfById: setStub, + }, + }, + '@rocket.chat/core-services': { + OmnichannelTranscript: { + workOnPdf: workOnPdfStub, + }, + QueueWorker: { + queueWork: queueWorkStub, + }, + }, + }); + +describe('requestPdfTranscript', () => { + const currentTestModeValue = process.env.TEST_MODE; + + beforeEach(() => { + setStub.reset(); + workOnPdfStub.reset(); + queueWorkStub.reset(); + }); + + after(() => { + process.env.TEST_MODE = currentTestModeValue; + }); + + it('should throw an error if room is still open', async () => { + await expect(requestPdfTranscript({ open: true }, 'userId')).to.be.rejectedWith('room-still-open'); + }); + it('should throw an error if room doesnt have a v property', async () => { + await expect(requestPdfTranscript({}, 'userId')).to.be.rejectedWith('improper-room-state'); + }); + it('should not request a transcript if it was already requested', async () => { + await requestPdfTranscript({ v: 1, pdfTranscriptRequested: true }, 'userId'); + expect(setStub.callCount).to.equal(0); + expect(workOnPdfStub.callCount).to.equal(0); + expect(queueWorkStub.callCount).to.equal(0); + }); + it('should set pdfTranscriptRequested to true on room', async () => { + await requestPdfTranscript({ _id: 'roomId', v: {}, pdfTranscriptRequested: false }, 'userId'); + expect(setStub.calledWith('roomId')).to.be.true; + }); + it('should call workOnPdf if TEST_MODE is true', async () => { + process.env.TEST_MODE = 'true'; + await requestPdfTranscript({ _id: 'roomId', v: {} }, 'userId'); + expect(workOnPdfStub.getCall(0).calledWithExactly({ details: { rid: 'roomId', userId: 'userId', from: 'omnichannel-transcript' } })).to + .be.true; + expect(queueWorkStub.calledOnce).to.be.false; + }); + it('should queue work if TEST_MODE is not set', async () => { + delete process.env.TEST_MODE; + await requestPdfTranscript({ _id: 'roomId', v: {} }, 'userId'); + expect(workOnPdfStub.calledOnce).to.be.false; + expect( + queueWorkStub.getCall(0).calledWithExactly('work', 'omnichannel-transcript.workOnPdf', { + details: { rid: 'roomId', userId: 'userId', from: 'omnichannel-transcript' }, + }), + ).to.be.true; + }); +}); diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 15b57e56881a..31a6578463f0 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -43,5 +43,8 @@ "typings": "./dist/index.d.ts", "files": [ "/dist" - ] + ], + "volta": { + "extends": "../../../package.json" + } } diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.spec.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.spec.ts index 569e0b4eefe5..4620fea206d6 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.spec.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.spec.ts @@ -24,9 +24,6 @@ jest.mock('@rocket.chat/core-services', () => ({ Room: { createDirectMessage: jest.fn().mockResolvedValue({ rid: 'roomId' }), }, - QueueWorker: { - queueWork: jest.fn(), - }, Translation: { translate: jest.fn().mockResolvedValue('translated message'), translateToServerLanguage: jest.fn().mockResolvedValue('translated server message'), @@ -40,7 +37,6 @@ jest.mock('@rocket.chat/core-services', () => ({ jest.mock('@rocket.chat/models', () => ({ LivechatRooms: { findOneById: jest.fn().mockResolvedValue({}), - setTranscriptRequestedPdfById: jest.fn(), unsetTranscriptRequestedPdfById: jest.fn(), setPdfTranscriptFileIdById: jest.fn(), }, diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 3c7222e99a64..3700b456b067 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -5,7 +5,6 @@ import { Upload as uploadService, Message as messageService, Room as roomService, - QueueWorker as queueService, Translation as translationService, Settings as settingsService, } from '@rocket.chat/core-services'; @@ -17,7 +16,6 @@ import type { IUpload, ILivechatVisitor, ILivechatAgent, - IOmnichannelRoom, IOmnichannelSystemMessage, } from '@rocket.chat/core-typings'; import { isQuoteAttachment, isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings'; @@ -141,52 +139,11 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT comment: 1, priorityData: 1, slaData: 1, + rid: 1, }, }).toArray(); } - async requestTranscript({ details }: { details: WorkDetails }): Promise { - this.log.info(`Requesting transcript for room ${details.rid} by user ${details.userId}`); - const room = await LivechatRooms.findOneById>(details.rid, { - projection: { _id: 1, open: 1, v: 1, pdfTranscriptRequested: 1 }, - }); - - if (!room) { - throw new Error('room-not-found'); - } - - if (room.open) { - throw new Error('room-still-open'); - } - - if (!room.v) { - throw new Error('improper-room-state'); - } - - // Don't request a transcript if there's already one requested :) - if (room.pdfTranscriptRequested) { - // TODO: use logger - this.log.info(`Transcript already requested for room ${details.rid}`); - return; - } - - await LivechatRooms.setTranscriptRequestedPdfById(details.rid); - - // Make the whole process sync when running on test mode - // This will prevent the usage of timeouts on the tests of this functionality :) - if (process.env.TEST_MODE) { - await this.workOnPdf({ details: { ...details, from: this.name } }); - return; - } - - // Even when processing is done "in-house", we still need to queue the work - // to avoid blocking the request - this.log.info(`Queuing work for room ${details.rid}`); - await queueService.queueWork('work', `${this.name}.workOnPdf`, { - details: { ...details, from: this.name }, - }); - } - private getQuotesFromMessage(message: IMessage): Quote[] { const quotes: Quote[] = []; diff --git a/packages/core-services/src/types/IOmnichannelTranscriptService.ts b/packages/core-services/src/types/IOmnichannelTranscriptService.ts index 90cc4799bc94..b67f7efee59a 100644 --- a/packages/core-services/src/types/IOmnichannelTranscriptService.ts +++ b/packages/core-services/src/types/IOmnichannelTranscriptService.ts @@ -1,6 +1,14 @@ import type { IUser, IRoom } from '@rocket.chat/core-typings'; +type WorkDetails = { + rid: IRoom['_id']; + userId: IUser['_id']; +}; + +type WorkDetailsWithSource = WorkDetails & { + from: string; +}; + export interface IOmnichannelTranscriptService { - requestTranscript({ details }: { details: { userId: IUser['_id']; rid: IRoom['_id'] } }): Promise; - workOnPdf({ template, details }: { template: string; details: any }): Promise; + workOnPdf({ details }: { details: WorkDetailsWithSource }): Promise; }