Skip to content

Commit

Permalink
feat(Omnichannel): System messages in transcripts (#32752)
Browse files Browse the repository at this point in the history
  • Loading branch information
yash-rajpal authored Jul 24, 2024
1 parent 0de8ee1 commit 03c8b06
Show file tree
Hide file tree
Showing 24 changed files with 829 additions and 74 deletions.
12 changes: 12 additions & 0 deletions .changeset/new-scissors-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@rocket.chat/omnichannel-services': minor
'@rocket.chat/pdf-worker': minor
'@rocket.chat/core-services': minor
'@rocket.chat/model-typings': minor
'@rocket.chat/i18n': minor
'@rocket.chat/meteor': minor
---

Added system messages support for Omnichannel PDF transcripts and email transcripts. Currently these transcripts don't render system messages and is shown as an empty message in PDF/email. This PR adds this support for all valid livechat system messages.

Also added a new setting under transcripts, to toggle the inclusion of system messages in email and PDF transcripts.
7 changes: 7 additions & 0 deletions .changeset/weak-pets-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rocket.chat/omnichannel-services': patch
'@rocket.chat/core-services': patch
'@rocket.chat/meteor': patch
---

Reduced time on generation of PDF transcripts. Earlier Rocket.Chat was fetching the required translations everytime a PDF transcript was requested, this process was async and was being unnecessarily being performed on every pdf transcript request. This PR improves this and now the translations are loaded at the start and kept in memory to process further pdf transcripts requests. This reduces the time of asynchronously fetching translations again and again.
17 changes: 16 additions & 1 deletion apps/meteor/app/livechat/server/lib/sendTranscript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { i18n } from '../../../../server/lib/i18n';
import { FileUpload } from '../../../file-upload/server';
import * as Mailer from '../../../mailer/server/api';
import { settings } from '../../../settings/server';
import { MessageTypes } from '../../../ui-utils/lib/MessageTypes';
import { getTimezone } from '../../../utils/server/lib/getTimezone';

const logger = new Logger('Livechat-SendTranscript');
Expand Down Expand Up @@ -63,6 +64,7 @@ export async function sendTranscript({
}

const showAgentInfo = settings.get<boolean>('Livechat_show_agent_info');
const showSystemMessages = settings.get<boolean>('Livechat_transcript_show_system_messages');
const closingMessage = await Messages.findLivechatClosingMessage(rid, { projection: { ts: 1 } });
const ignoredMessageTypes: MessageTypesValues[] = [
'livechat_navigation_history',
Expand All @@ -71,12 +73,14 @@ export async function sendTranscript({
'livechat-close',
'livechat-started',
'livechat_video_call',
'omnichannel_priority_change_history',
];
const acceptableImageMimeTypes = ['image/jpeg', 'image/png', 'image/jpg'];
const messages = await Messages.findVisibleByRoomIdNotContainingTypesBeforeTs(
rid,
ignoredMessageTypes,
closingMessage?.ts ? new Date(closingMessage.ts) : new Date(),
showSystemMessages,
{
sort: { ts: 1 },
},
Expand All @@ -98,7 +102,18 @@ export async function sendTranscript({
author = showAgentInfo ? message.u.name || message.u.username : i18n.t('Agent', { lng: userLanguage });
}

let messageContent = message.msg;
const isSystemMessage = MessageTypes.isSystemMessage(message);
const messageType = isSystemMessage && MessageTypes.getType(message);

let messageContent = messageType
? `<i>${i18n.t(
messageType.message,
messageType.data
? { ...messageType.data(message), interpolation: { escapeValue: false } }
: { interpolation: { escapeValue: false } },
)}</i>`
: message.msg;

let filesHTML = '';

if (message.attachments && message.attachments?.length > 0) {
Expand Down
32 changes: 24 additions & 8 deletions apps/meteor/server/models/raw/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel {
roomId: IRoom['_id'],
types: IMessage['t'][],
ts: Date,
showSystemMessages: boolean,
options?: FindOptions<IMessage>,
showThreadMessages = true,
): FindCursor<IMessage> {
Expand All @@ -389,6 +390,10 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel {
query.t = { $nin: types };
}

if (!showSystemMessages) {
query.t = { $exists: false };
}

return this.find(query, options);
}

Expand Down Expand Up @@ -424,14 +429,25 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel {
return this.find(query, options);
}

findLivechatMessagesWithoutClosing(rid: IRoom['_id'], options?: FindOptions<IMessage>): FindCursor<IMessage> {
return this.find(
{
rid,
t: { $exists: false },
},
options,
);
findLivechatMessagesWithoutTypes(
rid: IRoom['_id'],
ignoredTypes: IMessage['t'][],
showSystemMessages: boolean,
options?: FindOptions<IMessage>,
): FindCursor<IMessage> {
const query: Filter<IMessage> = {
rid,
};

if (ignoredTypes.length > 0) {
query.t = { $nin: ignoredTypes };
}

if (!showSystemMessages) {
query.t = { $exists: false };
}

return this.find(query, options);
}

async setBlocksById(_id: string, blocks: Required<IMessage>['blocks']): Promise<void> {
Expand Down
17 changes: 13 additions & 4 deletions apps/meteor/server/services/translation/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export class TranslationService extends ServiceClassInternal implements ITransla
}

// Use translateText when you already know the language, or want to translate to a predefined language
translateText(text: string, targetLanguage: string): Promise<string> {
return Promise.resolve(i18n.t(text, { lng: targetLanguage }));
translateText(text: string, targetLanguage: string, args?: Record<string, string>): Promise<string> {
return Promise.resolve(i18n.t(text, { lng: targetLanguage, ...args }));
}

// Use translate when you want to translate to the user's language, or server's as a fallback
Expand All @@ -28,9 +28,18 @@ export class TranslationService extends ServiceClassInternal implements ITransla
return this.translateText(text, language);
}

async translateToServerLanguage(text: string): Promise<string> {
async translateToServerLanguage(text: string, args?: Record<string, string>): Promise<string> {
const language = await this.getServerLanguageCached();

return this.translateText(text, language);
return this.translateText(text, language, args);
}

async translateMultipleToServerLanguage(keys: string[]): Promise<Array<{ key: string; value: string }>> {
const language = await this.getServerLanguageCached();

return keys.map((key) => ({
key,
value: i18n.t(key, { lng: language, fallbackLng: 'en' }),
}));
}
}
7 changes: 7 additions & 0 deletions apps/meteor/server/settings/omnichannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,13 @@ export const createOmniSettings = () =>
enableQuery: omnichannelEnabledQuery,
});

await this.add('Livechat_transcript_show_system_messages', false, {
type: 'boolean',
group: 'Omnichannel',
public: true,
enableQuery: omnichannelEnabledQuery,
});

await this.add('Livechat_transcript_message', '', {
type: 'string',
group: 'Omnichannel',
Expand Down
9 changes: 9 additions & 0 deletions ee/packages/omnichannel-services/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
preset: 'ts-jest',
errorOnDeprecated: true,
testEnvironment: 'jsdom',
modulePathIgnorePatterns: ['<rootDir>/dist/'],
moduleNameMapper: {
'\\.css$': 'identity-obj-proxy',
},
};
1 change: 1 addition & 0 deletions ee/packages/omnichannel-services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@rocket.chat/string-helpers": "~0.31.25",
"@rocket.chat/tools": "workspace:^",
"@types/node": "^14.18.63",
"date-fns": "^2.28.0",
"ejson": "^2.2.3",
"emoji-toolkit": "^7.0.1",
"eventemitter3": "^4.0.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { MessageTypesValues } from '@rocket.chat/core-typings';

export const validFile = { name: 'screenshot.png', buffer: Buffer.from([1, 2, 3]) };

export const invalidFile = { name: 'audio.mp3', buffer: null };

export const messages = [
{
msg: 'Hello, how can I help you today?',
ts: '2022-11-21T16:00:00.000Z',
u: {
_id: '123',
name: 'Juanito De Ponce',
username: 'juanito.ponce',
},
},
{
msg: 'I am having trouble with my account.',
ts: '2022-11-21T16:00:00.000Z',
u: {
_id: '321',
name: 'Christian Castro',
username: 'cristiano.castro',
},
md: [
{
type: 'UNORDERED_LIST',
value: [
{ type: 'LIST_ITEM', value: [{ type: 'PLAIN_TEXT', value: 'I am having trouble with my account;' }] },
{
type: 'LIST_ITEM',
value: [
{ type: 'PLAIN_TEXT', value: 'I am having trouble with my password. ' },
{ type: 'EMOJI', value: undefined, unicode: '🙂' },
],
},
],
},
],
},
{
msg: 'Can you please provide your account email?',
ts: '2022-11-21T16:00:00.000Z',
u: {
_id: '123',
name: 'Juanito De Ponce',
username: 'juanito.ponce',
},
},
];

export const validSystemMessage = {
ts: '2022-11-21T16:00:00.000Z',
u: {
_id: '123',
name: 'Juanito De Ponce',
username: 'juanito.ponce',
},
t: 'livechat-started' as MessageTypesValues,
};

export const invalidSystemMessage = {
ts: '2022-11-21T16:00:00.000Z',
u: {
_id: '123',
name: 'Juanito De Ponce',
username: 'juanito.ponce',
},
t: 'some-system-message' as MessageTypesValues,
};
119 changes: 119 additions & 0 deletions ee/packages/omnichannel-services/src/OmnichannelTranscript.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import '@testing-library/jest-dom';
import type { IMessage } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';

import { OmnichannelTranscript } from './OmnichannelTranscript';
import { invalidSystemMessage, messages, validSystemMessage } from './OmnichannelTranscript.fixtures';

jest.mock('@rocket.chat/pdf-worker', () => ({
PdfWorker: jest.fn().mockImplementation(() => ({
renderToStream: jest.fn().mockResolvedValue(Buffer.from('')),
isMimeTypeValid: jest.fn(() => true),
})),
}));

jest.mock('@rocket.chat/core-services', () => ({
ServiceClass: class {},
Upload: {
getFileBuffer: jest.fn().mockResolvedValue(Buffer.from('')),
uploadFile: jest.fn().mockResolvedValue({ _id: 'fileId', name: 'fileName' }),
sendFileMessage: jest.fn(),
},
Message: {
sendMessage: jest.fn(),
},
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'),
translateMultipleToServerLanguage: jest.fn((keys) => keys.map((key: any) => ({ key, value: key }))),
},
Settings: {
get: jest.fn().mockResolvedValue(''),
},
}));

jest.mock('@rocket.chat/models', () => ({
LivechatRooms: {
findOneById: jest.fn().mockResolvedValue({}),
setTranscriptRequestedPdfById: jest.fn(),
unsetTranscriptRequestedPdfById: jest.fn(),
setPdfTranscriptFileIdById: jest.fn(),
},
Messages: {
findLivechatMessagesWithoutTypes: jest.fn().mockReturnValue({
toArray: jest.fn().mockResolvedValue([]),
}),
},
Uploads: {
findOneById: jest.fn().mockResolvedValue({}),
},
Users: {
findOneById: jest.fn().mockResolvedValue({}),
findOneAgentById: jest.fn().mockResolvedValue({}),
},
LivechatVisitors: {
findOneEnabledById: jest.fn().mockResolvedValue({}),
},
}));

jest.mock('@rocket.chat/tools', () => ({
guessTimezone: jest.fn().mockReturnValue('UTC'),
guessTimezoneFromOffset: jest.fn().mockReturnValue('UTC'),
streamToBuffer: jest.fn().mockResolvedValue(Buffer.from('')),
}));

describe('OmnichannelTranscript', () => {
let omnichannelTranscript: OmnichannelTranscript;

beforeEach(() => {
omnichannelTranscript = new OmnichannelTranscript(Logger);
});

it('should return default timezone', async () => {
const timezone = await omnichannelTranscript.getTimezone();
expect(timezone).toBe('UTC');
});

it('should parse the messages', async () => {
const parsedMessages = await omnichannelTranscript.getMessagesData(messages as unknown as IMessage[]);
console.log(parsedMessages[0]);
expect(parsedMessages).toBeDefined();
expect(parsedMessages).toHaveLength(3);
expect(parsedMessages[0]).toHaveProperty('files');
expect(parsedMessages[0].files).toHaveLength(0);
expect(parsedMessages[0]).toHaveProperty('quotes');
expect(parsedMessages[0].quotes).toHaveLength(0);
});

it('should parse system message', async () => {
const parsedMessages = await omnichannelTranscript.getMessagesData([...messages, validSystemMessage] as unknown as IMessage[]);
const systemMessage = parsedMessages[3];
expect(parsedMessages).toBeDefined();
expect(parsedMessages).toHaveLength(4);
expect(systemMessage).toHaveProperty('t');
expect(systemMessage.t).toBe('livechat-started');
expect(systemMessage).toHaveProperty('msg');
expect(systemMessage.msg).toBe('Chat_started');
expect(systemMessage).toHaveProperty('files');
expect(systemMessage.files).toHaveLength(0);
expect(systemMessage).toHaveProperty('quotes');
expect(systemMessage.quotes).toHaveLength(0);
});

it('should parse an invalid system message', async () => {
const parsedMessages = await omnichannelTranscript.getMessagesData([...messages, invalidSystemMessage] as unknown as IMessage[]);
const systemMessage = parsedMessages[3];
console.log(parsedMessages[3]);
expect(parsedMessages).toBeDefined();
expect(parsedMessages).toHaveLength(4);
expect(systemMessage).toHaveProperty('t');
expect(systemMessage.t).toBe('some-system-message');
expect(systemMessage.msg).toBeUndefined();
});
});
Loading

0 comments on commit 03c8b06

Please sign in to comment.