chore: Improve Transcript service call chain (#34920)

pull/34486/head^2
Kevin Aleman 12 months ago committed by GitHub
parent f323946ec0
commit 1fb9f86a92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 18
      apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts
  2. 9
      apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts
  3. 40
      apps/meteor/ee/app/livechat-enterprise/server/lib/requestPdfTranscript.ts
  4. 74
      apps/meteor/tests/unit/server/livechat/lib/requestPdfTranscript.spec.ts
  5. 5
      ee/packages/omnichannel-services/package.json
  6. 4
      ee/packages/omnichannel-services/src/OmnichannelTranscript.spec.ts
  7. 45
      ee/packages/omnichannel-services/src/OmnichannelTranscript.ts
  8. 12
      packages/core-services/src/types/IOmnichannelTranscriptService.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();
},

@ -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;
};

@ -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<IOmnichannelRoom, '_id' | 'open' | 'v' | 'pdfTranscriptRequested'>,
requestedBy: string,
): Promise<void> => {
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);
};

@ -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;
});
});

@ -43,5 +43,8 @@
"typings": "./dist/index.d.ts",
"files": [
"/dist"
]
],
"volta": {
"extends": "../../../package.json"
}
}

@ -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(),
},

@ -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<void> {
this.log.info(`Requesting transcript for room ${details.rid} by user ${details.userId}`);
const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'open' | 'v' | 'pdfTranscriptRequested'>>(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[] = [];

@ -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<void>;
workOnPdf({ template, details }: { template: string; details: any }): Promise<void>;
workOnPdf({ details }: { details: WorkDetailsWithSource }): Promise<void>;
}

Loading…
Cancel
Save