snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
262
apps/meteor/server/lib/omnichannel/sendTranscript.ts
Normal file
262
apps/meteor/server/lib/omnichannel/sendTranscript.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { Message, Omnichannel } from '@rocket.chat/core-services';
|
||||
import {
|
||||
type IUser,
|
||||
type MessageTypesValues,
|
||||
type IOmnichannelSystemMessage,
|
||||
type ILivechatVisitor,
|
||||
type IOmnichannelRoom,
|
||||
isFileAttachment,
|
||||
isFileImageAttachment,
|
||||
type AtLeast,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import colors from '@rocket.chat/fuselage-tokens/dist/colors.json';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { MessageTypes } from '@rocket.chat/message-types';
|
||||
import { LivechatRooms, Messages, Uploads, Users } from '@rocket.chat/models';
|
||||
import createDOMPurify from 'dompurify';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { settings } from '../../settings';
|
||||
import { callbacks } from '../callbacks';
|
||||
import { i18n } from '../i18n';
|
||||
import { FileUpload } from '../media/file-upload';
|
||||
import * as Mailer from '../notifications/email/api';
|
||||
import { getTimezone } from '../utils/lib/getTimezone';
|
||||
|
||||
const logger = new Logger('Livechat-SendTranscript');
|
||||
|
||||
const DOMPurify = createDOMPurify(new JSDOM('').window);
|
||||
|
||||
export async function sendTranscript({
|
||||
token,
|
||||
rid,
|
||||
email,
|
||||
subject,
|
||||
user,
|
||||
}: {
|
||||
token: string;
|
||||
rid: string;
|
||||
email: string;
|
||||
subject?: string;
|
||||
user?: Pick<IUser, '_id' | 'name' | 'username' | 'utcOffset'> | null;
|
||||
}): Promise<boolean> {
|
||||
logger.debug({ msg: 'Sending conversation transcript', rid, token });
|
||||
|
||||
const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'v'>>(rid, { projection: { _id: 1, v: 1 } });
|
||||
if (!room) {
|
||||
throw new Error('error-invalid-room');
|
||||
}
|
||||
|
||||
const visitor = room?.v as ILivechatVisitor;
|
||||
if (token !== visitor?.token) {
|
||||
throw new Error('error-invalid-visitor');
|
||||
}
|
||||
|
||||
const userLanguage = settings.get<string>('Language') || 'en';
|
||||
const timezone = getTimezone(user);
|
||||
logger.debug({ msg: 'Transcript will be sent using timezone', timezone });
|
||||
|
||||
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',
|
||||
'livechat_transcript_history',
|
||||
'command',
|
||||
'livechat-close',
|
||||
'livechat-started',
|
||||
'livechat_video_call',
|
||||
'omnichannel_priority_change_history',
|
||||
];
|
||||
const messages = Messages.findVisibleByRoomIdNotContainingTypesBeforeTs(
|
||||
rid,
|
||||
ignoredMessageTypes,
|
||||
closingMessage?.ts ? new Date(closingMessage.ts) : new Date(),
|
||||
showSystemMessages,
|
||||
{
|
||||
sort: { ts: 1 },
|
||||
},
|
||||
);
|
||||
let html = '<div> <hr>';
|
||||
const InvalidFileMessage = `<div style="background-color: ${colors.n100}; text-align: center; border-color: ${
|
||||
colors.n250
|
||||
}; border-width: 1px; border-style: solid; border-radius: 4px; padding-top: 8px; padding-bottom: 8px; margin-top: 4px;">${i18n.t(
|
||||
'This_attachment_is_not_supported',
|
||||
{ lng: userLanguage },
|
||||
)}</div>`;
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
if (typeof str !== 'string') return '';
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/');
|
||||
}
|
||||
|
||||
for await (const message of messages) {
|
||||
let author;
|
||||
if (message.u._id === visitor._id) {
|
||||
author = i18n.t('You', { lng: userLanguage });
|
||||
} else {
|
||||
author = showAgentInfo ? message.u.name || message.u.username : i18n.t('Agent', { lng: userLanguage });
|
||||
}
|
||||
|
||||
const messageType = MessageTypes.getType(message);
|
||||
|
||||
let messageContent = messageType?.system
|
||||
? DOMPurify.sanitize(`
|
||||
<i>${messageType.text(i18n.cloneInstance({ interpolation: { escapeValue: false } }).t, message)}}</i>`)
|
||||
: escapeHtml(message.msg);
|
||||
|
||||
let filesHTML = '';
|
||||
|
||||
if (message.attachments && message.attachments?.length > 0) {
|
||||
messageContent = message.attachments[0].description || '';
|
||||
escapeHtml(messageContent);
|
||||
|
||||
for await (const attachment of message.attachments) {
|
||||
if (!isFileAttachment(attachment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isFileImageAttachment(attachment)) {
|
||||
filesHTML += `<div>${escapeHtml(attachment.title || '')}${InvalidFileMessage}</div>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const file = message.files?.find((file) => file.name === attachment.title);
|
||||
|
||||
if (!file) {
|
||||
filesHTML += `<div>${escapeHtml(attachment.title || '')}${InvalidFileMessage}</div>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uploadedFile = await Uploads.findOneById(file._id);
|
||||
|
||||
if (!uploadedFile) {
|
||||
filesHTML += `<div>${escapeHtml(file.name)}${InvalidFileMessage}</div>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uploadedFileBuffer = await FileUpload.getBuffer(uploadedFile);
|
||||
filesHTML += `<div style="color: ${colors.n700}; margin-top: 4px; flex-direction: column;">
|
||||
<p>${escapeHtml(file.name)}</p>
|
||||
<img src="data:${attachment.image_type};base64,${uploadedFileBuffer.toString('base64')}" style="width: 400px; max-height: 240px; object-fit: contain; object-position: 0;" />
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
const datetime = moment.tz(message.ts, timezone).locale(userLanguage).format('LLL');
|
||||
const singleMessage = `
|
||||
<p><strong>${escapeHtml(author)}</strong> <em>${escapeHtml(datetime)}</em></p>
|
||||
<p>${messageContent}</p>
|
||||
<p>${filesHTML}</p>
|
||||
`;
|
||||
html += singleMessage;
|
||||
}
|
||||
|
||||
html = `${html}</div>`;
|
||||
|
||||
const fromEmail = settings.get<string>('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i);
|
||||
let emailFromRegexp = '';
|
||||
if (fromEmail) {
|
||||
emailFromRegexp = fromEmail[0];
|
||||
} else {
|
||||
emailFromRegexp = settings.get<string>('From_Email');
|
||||
}
|
||||
|
||||
// Some endpoints allow the caller to pass a different `subject` via parameter.
|
||||
// IF subject is passed, we'll use that one and treat it as an override
|
||||
// IF no subject is passed, we fallback to the setting `Livechat_transcript_email_subject`
|
||||
// IF that is not configured, we fallback to 'Transcript of your livechat conversation', which is the default value
|
||||
// As subject and setting value are user input, we don't translate them
|
||||
const mailSubject =
|
||||
subject ||
|
||||
settings.get<string>('Livechat_transcript_email_subject') ||
|
||||
i18n.t('Transcript_of_your_livechat_conversation', { lng: userLanguage });
|
||||
|
||||
await Mailer.send({
|
||||
to: email,
|
||||
from: emailFromRegexp,
|
||||
replyTo: emailFromRegexp,
|
||||
subject: mailSubject,
|
||||
html,
|
||||
});
|
||||
|
||||
setImmediate(() => {
|
||||
void callbacks.run('livechat.sendTranscript', messages, email);
|
||||
});
|
||||
|
||||
const requestData: IOmnichannelSystemMessage['requestData'] = {
|
||||
type: 'user',
|
||||
visitor,
|
||||
user,
|
||||
};
|
||||
|
||||
if (!user?.username) {
|
||||
const cat = await Users.findOneById('rocket.cat', { projection: { _id: 1, username: 1, name: 1 } });
|
||||
if (cat) {
|
||||
requestData.user = cat;
|
||||
requestData.type = 'visitor';
|
||||
}
|
||||
}
|
||||
|
||||
if (!requestData.user) {
|
||||
logger.error('rocket.cat user not found');
|
||||
throw new Error('No user provided and rocket.cat not found');
|
||||
}
|
||||
|
||||
await Message.saveSystemMessage<IOmnichannelSystemMessage>('livechat_transcript_history', room._id, '', requestData.user, {
|
||||
requestData,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function requestTranscript({
|
||||
rid,
|
||||
email,
|
||||
subject,
|
||||
user,
|
||||
}: {
|
||||
rid: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
user: AtLeast<IUser, '_id' | 'username' | 'utcOffset' | 'name'>;
|
||||
}) {
|
||||
// `v` is required by the MAC-limit check below
|
||||
const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, transcriptRequest: 1, v: 1 } });
|
||||
|
||||
if (!room?.open) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room');
|
||||
}
|
||||
|
||||
if (room.transcriptRequest) {
|
||||
throw new Meteor.Error('error-transcript-already-requested', 'Transcript already requested');
|
||||
}
|
||||
|
||||
if (!(await Omnichannel.isWithinMACLimit(room))) {
|
||||
throw new Error('error-mac-limit-reached');
|
||||
}
|
||||
|
||||
const { _id, username, name, utcOffset } = user;
|
||||
const transcriptRequest = {
|
||||
requestedAt: new Date(),
|
||||
requestedBy: {
|
||||
_id,
|
||||
username,
|
||||
name,
|
||||
utcOffset,
|
||||
},
|
||||
email,
|
||||
subject,
|
||||
};
|
||||
|
||||
await LivechatRooms.setEmailTranscriptRequestedByRoomId(rid, transcriptRequest);
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user