Skip to content

Commit

Permalink
Regression: Remove unnecessary messages from Email transcript (#28165)
Browse files Browse the repository at this point in the history
  • Loading branch information
KevLehman committed Feb 28, 2023
1 parent f0ed77f commit 8403604
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 106 deletions.
3 changes: 1 addition & 2 deletions apps/meteor/app/livechat/server/api/v1/transcript.ts
Expand Up @@ -2,15 +2,14 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { isPOSTLivechatTranscriptParams } from '@rocket.chat/rest-typings';

import { API } from '../../../../api/server';
import { Livechat } from '../../lib/Livechat';
import { Livechat } from '../../lib/LivechatTyped';

API.v1.addRoute(
'livechat/transcript',
{ validateParams: isPOSTLivechatTranscriptParams },
{
async post() {
const { token, rid, email } = this.bodyParams;
// @ts-expect-error -- typings on sendtranscript are wrong
if (!(await Livechat.sendTranscript({ token, rid, email }))) {
return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_transcript') });
}
Expand Down
Expand Up @@ -3,7 +3,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';

import { callbacks } from '../../../../lib/callbacks';
import { Livechat } from '../lib/Livechat';
import { Livechat } from '../lib/LivechatTyped';
import type { CloseRoomParams } from '../lib/LivechatTyped.d';

type LivechatCloseCallbackParams = {
Expand Down
86 changes: 0 additions & 86 deletions apps/meteor/app/livechat/server/lib/Livechat.js
Expand Up @@ -8,7 +8,6 @@ import { Match, check } from 'meteor/check';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { HTTP } from 'meteor/http';
import moment from 'moment-timezone';
import UAParser from 'ua-parser-js';
import {
Users as UsersRaw,
Expand All @@ -27,7 +26,6 @@ import { QueueManager } from './QueueManager';
import { RoutingManager } from './RoutingManager';
import { Analytics } from './Analytics';
import { settings } from '../../../settings/server';
import { getTimezone } from '../../../utils/server/lib/getTimezone';
import { callbacks } from '../../../../lib/callbacks';
import {
Users,
Expand Down Expand Up @@ -1096,90 +1094,6 @@ export const Livechat = {
});
},

async sendTranscript({ token, rid, email, subject, user }) {
check(rid, String);
check(email, String);
Livechat.logger.debug(`Sending conversation transcript of room ${rid} to user with token ${token}`);

const room = LivechatRooms.findOneById(rid);

const visitor = await LivechatVisitors.getVisitorByToken(token, {
projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 },
});

if (!visitor) {
throw new Meteor.Error('error-invalid-token', 'Invalid token');
}

const userLanguage = (visitor && visitor.language) || settings.get('Language') || 'en';
const timezone = getTimezone(user);
Livechat.logger.debug(`Transcript will be sent using ${timezone} as timezone`);

// allow to only user to send transcripts from their own chats
if (!room || room.t !== 'l' || !room.v || room.v.token !== token) {
throw new Meteor.Error('error-invalid-room', 'Invalid room');
}

const showAgentInfo = settings.get('Livechat_show_agent_info');
const ignoredMessageTypes = [
'livechat_navigation_history',
'livechat_transcript_history',
'command',
'livechat-close',
'livechat-started',
'livechat_video_call',
];
const messages = Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, {
sort: { ts: 1 },
});

let html = '<div> <hr>';
messages.forEach((message) => {
let author;
if (message.u._id === visitor._id) {
author = TAPi18n.__('You', { lng: userLanguage });
} else {
author = showAgentInfo ? message.u.name || message.u.username : TAPi18n.__('Agent', { lng: userLanguage });
}

const datetime = moment.tz(message.ts, timezone).locale(userLanguage).format('LLL');
const singleMessage = `
<p><strong>${author}</strong> <em>${datetime}</em></p>
<p>${message.msg}</p>
`;
html += singleMessage;
});

html = `${html}</div>`;

let fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i);

if (fromEmail) {
fromEmail = fromEmail[0];
} else {
fromEmail = settings.get('From_Email');
}

const mailSubject = subject || TAPi18n.__('Transcript_of_your_livechat_conversation', { lng: userLanguage });

this.sendEmail(fromEmail, email, fromEmail, mailSubject, html);

Meteor.defer(() => {
callbacks.run('livechat.sendTranscript', messages, email);
});

let type = 'user';
if (!user) {
user = Users.findOneById('rocket.cat', { fields: { _id: 1, username: 1, name: 1 } });
type = 'visitor';
}

Messages.createTranscriptHistoryWithRoomIdMessageAndUser(room._id, '', user, {
requestData: { type, visitor, user },
});
return true;
},

getRoomMessages({ rid }) {
check(rid, String);

Expand Down
138 changes: 129 additions & 9 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
@@ -1,20 +1,18 @@
// Goal is to have a typed version of apps/meteor/app/livechat/server/lib/Livechat.js
// This is a work in progress, and is not yet complete
// But it is a start.

// Important note: Try to not use the original Livechat.js file, but use this one instead.
// If possible, move methods from Livechat.js to this file.
// This is because we want to slowly convert the code to typescript, and this is a good way to do it.
import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo } from '@rocket.chat/core-typings';
import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo, IUser, MessageTypesValues } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatInquiry, LivechatRooms, Subscriptions } from '@rocket.chat/models';
import { LivechatDepartment, LivechatInquiry, LivechatRooms, Subscriptions, LivechatVisitors, Messages, Users } from '@rocket.chat/models';
import moment from 'moment-timezone';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';

import { callbacks } from '../../../../lib/callbacks';
import { Logger } from '../../../logger/server';
import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './LivechatTyped.d';
import { sendMessage } from '../../../lib/server/functions/sendMessage';
import { Apps, AppEvents } from '../../../../ee/server/apps';
import { Messages as LegacyMessage } from '../../../models/server';
import { getTimezone } from '../../../utils/server/lib/getTimezone';
import { settings } from '../../../settings/server';
import * as Mailer from '../../../mailer';

class LivechatClass {
logger: Logger;
Expand Down Expand Up @@ -177,6 +175,128 @@ class LivechatClass {
},
};
}

private sendEmail(from: string, to: string, replyTo: string, subject: string, html: string): void {
Mailer.send({
to,
from,
replyTo,
subject,
html,
});
}

async sendTranscript({
token,
rid,
email,
subject,
user,
}: {
token: string;
rid: string;
email: string;
subject?: string;
user?: Pick<IUser, '_id' | 'name' | 'username' | 'utcOffset'>;
}): Promise<boolean> {
check(rid, String);
check(email, String);
this.logger.debug(`Sending conversation transcript of room ${rid} to user with token ${token}`);

const room = await LivechatRooms.findOneById(rid);

const visitor = await LivechatVisitors.getVisitorByToken(token, {
projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 },
});

if (!visitor) {
throw new Error('error-invalid-token');
}

// @ts-expect-error - Visitor typings should include language?
const userLanguage = visitor?.language || settings.get('Language') || 'en';
const timezone = getTimezone(user);
this.logger.debug(`Transcript will be sent using ${timezone} as timezone`);

if (!room) {
throw new Error('error-invalid-room');
}

// allow to only user to send transcripts from their own chats
if (room.t !== 'l' || !room.v || room.v.token !== token) {
throw new Error('error-invalid-room');
}

const showAgentInfo = settings.get<string>('Livechat_show_agent_info');
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',
];
const messages = await Messages.findVisibleByRoomIdNotContainingTypesBeforeTs(
rid,
ignoredMessageTypes,
closingMessage?.ts ? new Date(closingMessage.ts) : new Date(),
{
sort: { ts: 1 },
},
);

let html = '<div> <hr>';
await messages.forEach((message) => {
let author;
if (message.u._id === visitor._id) {
author = TAPi18n.__('You', { lng: userLanguage });
} else {
author = showAgentInfo ? message.u.name || message.u.username : TAPi18n.__('Agent', { lng: userLanguage });
}

const datetime = moment.tz(message.ts, timezone).locale(userLanguage).format('LLL');
const singleMessage = `
<p><strong>${author}</strong> <em>${datetime}</em></p>
<p>${message.msg}</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');
}

const mailSubject = subject || TAPi18n.__('Transcript_of_your_livechat_conversation', { lng: userLanguage });

this.sendEmail(emailFromRegexp, email, emailFromRegexp, mailSubject, html);

Meteor.defer(() => {
callbacks.run('livechat.sendTranscript', messages, email);
});

let type = 'user';
if (!user) {
const cat = await Users.findOneById('rocket.cat', { projection: { _id: 1, username: 1, name: 1 } });
if (!cat) {
this.logger.error('rocket.cat user not found');
throw new Error('No user provided and rocket.cat not found');
}
user = cat;
type = 'visitor';
}

LegacyMessage.createTranscriptHistoryWithRoomIdMessageAndUser(room._id, '', user, {
requestData: { type, visitor, user },
});
return true;
}
}

export const Livechat = new LivechatClass();
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/methods/sendTranscript.js
Expand Up @@ -4,7 +4,7 @@ import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';

import { Users } from '../../../models/server';
import { hasPermission } from '../../../authorization';
import { Livechat } from '../lib/Livechat';
import { Livechat } from '../lib/LivechatTyped';

Meteor.methods({
'livechat:sendTranscript'(token, rid, email, subject) {
Expand Down
16 changes: 10 additions & 6 deletions apps/meteor/app/utils/server/lib/getTimezone.ts
@@ -1,5 +1,4 @@
import moment from 'moment-timezone';
import type { SettingValue } from '@rocket.chat/core-typings';

import { settings } from '../../../settings/server';

Expand All @@ -11,17 +10,22 @@ const padOffset = (offset: string | number): string => {
return `${isNegative ? '-' : '+'}${absOffset < 10 ? `0${absOffset}` : absOffset}:00`;
};

const guessTimezoneFromOffset = (offset: string | number): string =>
moment.tz.names().find((tz) => padOffset(offset) === moment.tz(tz).format('Z').toString()) || moment.tz.guess();
const guessTimezoneFromOffset = (offset?: string | number): string => {
if (!offset) {
return moment.tz.guess();
}

return moment.tz.names().find((tz) => padOffset(offset) === moment.tz(tz).format('Z').toString()) || moment.tz.guess();
};

export const getTimezone = (user: { utcOffset: string | number }): string | void | SettingValue => {
export const getTimezone = (user?: { utcOffset?: string | number }): string => {
const timezone = settings.get('Default_Timezone_For_Reporting');

switch (timezone) {
case 'custom':
return settings.get('Default_Custom_Timezone');
return settings.get<string>('Default_Custom_Timezone');
case 'user':
return guessTimezoneFromOffset(user.utcOffset);
return guessTimezoneFromOffset(user?.utcOffset);
default:
return moment.tz.guess();
}
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/lib/callbacks.ts
Expand Up @@ -184,6 +184,7 @@ type Hook =
| 'livechat.chatQueued'
| 'livechat.checkAgentBeforeTakeInquiry'
| 'livechat.checkDefaultAgentOnNewRoom'
| 'livechat.sendTranscript'
| 'livechat.closeRoom'
| 'livechat.leadCapture'
| 'livechat.newRoom'
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/server/models/raw/BaseRaw.ts
Expand Up @@ -149,7 +149,7 @@ export abstract class BaseRaw<

async findOne(query?: Filter<T> | T['_id'], options?: undefined): Promise<T | null>;

async findOne<P = T>(query: Filter<T> | T['_id'], options: FindOptions<P extends T ? T : P>): Promise<P | null>;
async findOne<P = T>(query: Filter<T> | T['_id'], options?: FindOptions<P extends T ? T : P>): Promise<P | null>;

async findOne<P>(query: Filter<T> | T['_id'] = {}, options?: any): Promise<WithId<T> | WithId<P> | null> {
const q: Filter<T> = typeof query === 'string' ? ({ _id: query } as Filter<T>) : query;
Expand Down

0 comments on commit 8403604

Please sign in to comment.