Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Improve Transcript service call chain #34920

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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();
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
@@ -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;
});
});
5 changes: 4 additions & 1 deletion ee/packages/omnichannel-services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@
"typings": "./dist/index.d.ts",
"files": [
"/dist"
]
],
"volta": {
"extends": "../../../package.json"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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(),
},
Expand Down
45 changes: 1 addition & 44 deletions ee/packages/omnichannel-services/src/OmnichannelTranscript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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[] = [];

Expand Down
12 changes: 10 additions & 2 deletions packages/core-services/src/types/IOmnichannelTranscriptService.ts
Original file line number Diff line number Diff line change
@@ -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