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

feat(Omnichannel): System messages in transcripts #32752

Merged
merged 38 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4eeb8d1
WIP: OC PDF transcripts system messages
yash-rajpal Jul 10, 2024
bc8335e
add setting for system messages in transcripts
yash-rajpal Jul 10, 2024
a87167e
system messages in email transcripts
yash-rajpal Jul 10, 2024
8a9aeb8
apply settings for email transcripts
yash-rajpal Jul 11, 2024
f3ef59a
setting on pdf transcripts
yash-rajpal Jul 11, 2024
08db96f
add more livechat message types
yash-rajpal Jul 11, 2024
2bf0f64
fix breaking unit tests
yash-rajpal Jul 11, 2024
b90c4da
add unit tests for system messages
yash-rajpal Jul 11, 2024
44017ad
remove logs
yash-rajpal Jul 11, 2024
5b59e23
remove unusable code
yash-rajpal Jul 11, 2024
15a9086
Merge branch 'develop' into feat/oc-pdf-system-messages
yash-rajpal Jul 11, 2024
6a0da1d
move system message parsing to oc-services package
yash-rajpal Jul 16, 2024
05411e0
revert async parseTemplateData
yash-rajpal Jul 16, 2024
ff3d33b
remove livechatSystemMessages from pdf package
yash-rajpal Jul 16, 2024
04504c6
revert packages deps
yash-rajpal Jul 16, 2024
701aec4
make translations handling synchronous
yash-rajpal Jul 17, 2024
14fe7d4
oops
yash-rajpal Jul 17, 2024
1e9ea76
remove async function
yash-rajpal Jul 17, 2024
3512cea
add left message
yash-rajpal Jul 18, 2024
393225e
fix types and requested changes
yash-rajpal Jul 18, 2024
9846c26
add unit tests for OmnichannelTranscripts
yash-rajpal Jul 18, 2024
6558ba3
test more props
yash-rajpal Jul 18, 2024
ee0b211
remove comments
yash-rajpal Jul 18, 2024
f65eca7
add cs
yash-rajpal Jul 18, 2024
85c93ad
add another changeset for improvements
yash-rajpal Jul 18, 2024
155b233
Merge branch 'develop' into feat/oc-pdf-system-messages
yash-rajpal Jul 19, 2024
47e5840
maybe should fix typecheck on CI
yash-rajpal Jul 19, 2024
b99afeb
make translations sync
yash-rajpal Jul 19, 2024
fc01795
remove messageType render prop
yash-rajpal Jul 19, 2024
ae73358
fix confs
yash-rajpal Jul 19, 2024
d5e9ee6
fix deps
yash-rajpal Jul 19, 2024
096d3a8
escape special characters encoding
yash-rajpal Jul 19, 2024
861172d
merge develop
yash-rajpal Jul 19, 2024
0a50655
add setting to cs
yash-rajpal Jul 19, 2024
47cb427
add more system messages and fallback language
yash-rajpal Jul 19, 2024
7b6217e
ignore priority system message
yash-rajpal Jul 19, 2024
bcb3993
Merge branch 'develop' into feat/oc-pdf-system-messages
kodiakhq[bot] Jul 23, 2024
aeabfa9
fix confs
yash-rajpal Jul 24, 2024
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
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,11 +17,12 @@
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');

export async function sendTranscript({

Check warning on line 25 in apps/meteor/app/livechat/server/lib/sendTranscript.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Async function 'sendTranscript' has a complexity of 35. Maximum allowed is 31
token,
rid,
email,
Expand Down Expand Up @@ -63,6 +64,7 @@
}

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 @@
'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 @@
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) {
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
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(
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
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) {
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
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, {
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
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
Loading