Skip to content

Commit

Permalink
[NEW][ENTERPRISE] Support Omnichannel conversations auditing (#17692)
Browse files Browse the repository at this point in the history
Co-authored-by: Rodrigo Nascimento <rodrigoknascimento@gmail.com>
  • Loading branch information
renatobecker and rodrigok authored May 21, 2020
1 parent 9826434 commit d46c864
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 31 deletions.
16 changes: 15 additions & 1 deletion app/livechat/imports/server/rest/visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { check } from 'meteor/check';

import { API } from '../../../../api/server';
import { findVisitorInfo, findVisitedPages, findChatHistory } from '../../../server/api/lib/visitors';
import { findVisitorInfo, findVisitedPages, findChatHistory, findVisitorsToAutocomplete } from '../../../server/api/lib/visitors';

API.v1.addRoute('livechat/visitors.info', { authRequired: true }, {
get() {
Expand Down Expand Up @@ -62,3 +62,17 @@ API.v1.addRoute('livechat/visitors.chatHistory/room/:roomId/visitor/:visitorId',
return API.v1.success(history);
},
});

API.v1.addRoute('livechat/visitors.autocomplete', { authRequired: true }, {
get() {
const { selector } = this.queryParams;
if (!selector) {
return API.v1.failure('The \'selector\' param is required');
}

return API.v1.success(Promise.await(findVisitorsToAutocomplete({
userId: this.userId,
selector: JSON.parse(selector),
})));
},
});
24 changes: 24 additions & 0 deletions app/livechat/server/api/lib/visitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,27 @@ export async function findChatHistory({ userId, roomId, visitorId, pagination: {
total,
};
}

export async function findVisitorsToAutocomplete({ userId, selector }) {
if (!await hasPermissionAsync(userId, 'view-l-room')) {
return { items: [] };
}
const { exceptions = [], conditions = {} } = selector;

const options = {
fields: {
_id: 1,
name: 1,
username: 1,
},
limit: 10,
sort: {
name: 1,
},
};

const items = await LivechatVisitors.findByNameRegexWithExceptionsAndConditions(selector.term, exceptions, conditions, options).toArray();
return {
items,
};
}
10 changes: 10 additions & 0 deletions app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,16 @@ export class LivechatRooms extends Base {
return this.find(query);
}

findByVisitorIdAndAgentId(visitorId, agentId, options) {
const query = {
t: 'l',
...visitorId && { 'v._id': visitorId },
...agentId && { 'servedBy._id': agentId },
};

return this.find(query, options);
}

findByVisitorId(visitorId) {
const query = {
t: 'l',
Expand Down
41 changes: 41 additions & 0 deletions app/models/server/raw/LivechatVisitors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import s from 'underscore.string';

import { BaseRaw } from './BaseRaw';

export class LivechatVisitorsRaw extends BaseRaw {
Expand All @@ -12,4 +14,43 @@ export class LivechatVisitorsRaw extends BaseRaw {

return this.find(query, { fields: { _id: 1 } });
}

findByNameRegexWithExceptionsAndConditions(searchTerm, exceptions = [], conditions = {}, options = {}) {
if (!Array.isArray(exceptions)) {
exceptions = [exceptions];
}

const nameRegex = new RegExp(`^${ s.escapeRegExp(searchTerm).trim() }`, 'i');

const match = {
$match: {
name: nameRegex,
_id: {
$nin: exceptions,
},
...conditions,
},
};

const { fields, sort, offset, count } = options;
const project = {
$project: {
custom_name: { $concat: ['$username', ' - ', '$name'] },
...fields,
},
};

const order = { $sort: sort || { name: 1 } };
const params = [match, project, order];

if (offset) {
params.push({ $skip: offset });
}

if (count) {
params.push({ $limit: count });
}

return this.col.aggregate(params);
}
}
9 changes: 9 additions & 0 deletions app/ui/client/components/popupList.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@
<span class="rc-popup-list__item-name">{{{modifier item.name}}}</span>
</li>
</template>

<template name="popupList_item_custom">
<li class="rc-popup-list__item">
<span class="rc-popup-list__item-image">
{{>avatar username=item.custom_name roomIcon=true}}
</span>
<span class="rc-popup-list__item-name">{{{modifier item.custom_name}}}</span>
</li>
</template>
7 changes: 5 additions & 2 deletions ee/app/auditing/client/templates/audit/audit.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
<select id="type" class="rc-select__element js-type rc-input__wrapper" name="type">
<option value="">{{_ 'Others'}}</option>
<option value="d">{{_ 'Direct_Messages'}}</option>
<option value="l">{{_ 'Omnichannel'}}</option>
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</label>

{{> auditAutocomplete key='room' hide=typeD onChange=onChange prepare=prepareRoom field='name' term='name' icon='hashtag' label="room_name" placeholder='Channel_Name_Placeholder'}}
{{> auditAutocompleteDirectMessage key='users' hide=nTypeD onChange=onChange collection='UserAndRoom' modifier=modifierUser endpoint='users.autocomplete' field='username' icon='at' label="From" placeholder='Username_Placeholder'}}
{{> auditAutocomplete key='room' hide=nTypeOthers onChange=onChange prepare=prepareRoom field='name' term='name' icon='hashtag' label="room_name" placeholder='Channel_Name_Placeholder'}}
{{> auditAutocompleteDirectMessage key='users' hide=nTypeDM onChange=onChange collection='UserAndRoom' modifier=modifierUser endpoint='users.autocomplete' field='username' icon='at' label="From" placeholder='Username_Placeholder'}}
{{> auditAutocomplete key='visitor' hide=nTypeOmni onChange=onChange collection='CachedVisitorsList' modifier=modifierUser endpoint='livechat/visitors.autocomplete' field='custom_name' term='custom_name' icon='omnichannel' label="Visitor" templateItem="popupList_item_custom" placeholder='Visitor_Name_Placeholder'}}
{{> auditAutocomplete key='agent' hide=nTypeOmni onChange=onChange collection='CachedAgentsList' conditions=agentConditions modifier=modifierUser endpoint='users.autocomplete' field='username' icon='omnichannel' label="Agent" placeholder='Agent_Name_Placeholder'}}

<div class="rc-input rc-input--small rc-audit-date">
<label class="rc-input__label">
Expand Down
36 changes: 28 additions & 8 deletions ee/app/auditing/client/templates/audit/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { call, convertDate, scrollTo } from '../../utils.js';

import './audit.html';

const loadMessages = async function({ rid, users, startDate, endDate = new Date(), msg }) {
const loadMessages = async function({ rid, users, startDate, endDate = new Date(), msg, type, visitor, agent }) {
this.messages = this.messages || new ReactiveVar([]);
this.loading = this.loading || new ReactiveVar(true);
try {
this.loading.set(true);
const messages = await call('auditGetMessages', { rid, users, startDate, endDate, msg });
const messages = await call('auditGetMessages', { rid, users, startDate, endDate, msg, type, visitor, agent });
this.messagesContext.set({
...messageContext({ rid }),
messages,
Expand All @@ -40,16 +40,29 @@ Template.audit.events({
async 'click .js-submit'(e, t) {
const form = e.currentTarget.parentElement;

const result = {};
const type = t.type.get();
const result = { type };

e.currentTarget.blur();

if (t.type.get() === 'd') {
if (type === 'd') {
if (!t.users) {
return form.querySelector('#autocomplete-users').classList.add('rc-input--error');
}
form.querySelector('#autocomplete-users').classList.remove('rc-input--error');
result.users = t.users.map((user) => user.username);
} else if (type === 'l') {
if (!t.visitor && !t.agent) {
form.querySelector('#autocomplete-agent').classList.add('rc-input--error');
form.querySelector('#autocomplete-visitor').classList.add('rc-input--error');
return;
}

form.querySelector('#autocomplete-agent').classList.remove('rc-input--error');
form.querySelector('#autocomplete-visitor').classList.remove('rc-input--error');

result.visitor = t.visitor?._id;
result.agent = t.agent?._id;
} else {
if (!t.room || !t.room._id) {
return form.querySelector('#autocomplete-room').classList.add('rc-input--error');
Expand Down Expand Up @@ -103,11 +116,14 @@ Template.audit.helpers({
return `<strong>${ part }</strong>`;
}) }`;
},
nTypeD() {
nTypeOthers() {
return ['d', 'l'].includes(Template.instance().type.get());
},
nTypeDM() {
return Template.instance().type.get() !== 'd';
},
typeD() {
return Template.instance().type.get() === 'd';
nTypeOmni() {
return Template.instance().type.get() !== 'l';
},
type() {
return Template.instance().type.get();
Expand All @@ -121,6 +137,9 @@ Template.audit.helpers({
hasResults() {
return Template.instance().hasResults.get();
},
agentConditions() {
return { role: 'livechat-agent' };
},
});

Template.audit.onCreated(async function() {
Expand Down Expand Up @@ -275,8 +294,9 @@ Template.auditAutocomplete.helpers({
},
config() {
const { filter } = Template.instance();
const { templateItem } = Template.instance().data;
return {
template_item: 'popupList_item_channel',
template_item: templateItem || 'popupList_item_channel',
// noMatchTemplate: Template.roomSearchEmpty,
filter: filter.get(),
noMatchTemplate: 'userSearchEmpty',
Expand Down
13 changes: 1 addition & 12 deletions ee/app/auditing/client/utils.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
import { Meteor } from 'meteor/meteor';

export const call = (...args) => new Promise(function(resolve, reject) {
Meteor.call(...args, function(err, result) {
if (err) {
// eslint-disable-next-line no-undef
handleError(err);
reject(err);
}
resolve(result);
});
});
export { call } from '../../../../app/ui-utils/client/lib/callMethod';

export const convertDate = (date) => {
const [y, m, d] = date.split('-');
Expand Down
34 changes: 26 additions & 8 deletions ee/app/auditing/server/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,31 @@ import { Meteor } from 'meteor/meteor';
import s from 'underscore.string';
import { check } from 'meteor/check';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';

import AuditLog from './auditLog';
import { Rooms, Messages } from '../../../../app/models';
import { hasAllPermission } from '../../../../app/authorization';
import { LivechatRooms, Rooms, Messages } from '../../../../app/models/server';
import { hasAllPermission } from '../../../../app/authorization/server';

const getValue = (room) => room && { rids: [room._id], name: room.name };

const getRoomInfoByAuditParams = ({ type, roomId, users, visitor, agent }) => {
if (roomId) {
return getValue(Rooms.findOne({ _id: roomId }));
}

if (type === 'd') {
return getValue(Rooms.findDirectRoomContainingAllUsernames(users));
}

if (type === 'l') {
const rooms = LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, { fields: { _id: 1 } }).fetch();
return rooms && rooms.length && { rids: rooms.map(({ _id }) => _id), name: TAPi18n.__('Omnichannel') };
}
};

Meteor.methods({
auditGetMessages({ rid, startDate, endDate, users, msg }) {
auditGetMessages({ rid: roomId, startDate, endDate, users, msg, type, visitor, agent }) {
check(startDate, Date);
check(endDate, Date);

Expand All @@ -17,15 +35,15 @@ Meteor.methods({
throw new Meteor.Error('Not allowed');
}

const room = !rid ? Rooms.findDirectRoomContainingAllUsernames(users) : Rooms.findOne({ _id: rid });
if (!room) {
const roomInfo = getRoomInfoByAuditParams({ type, roomId, users, visitor, agent });
if (!roomInfo) {
throw new Meteor.Error('Room doesn`t exist');
}

rid = room._id;
const { rids, name } = roomInfo;

const query = {
rid,
rid: { $in: rids },
ts: {
$gt: startDate,
$lt: endDate,
Expand All @@ -40,7 +58,7 @@ Meteor.methods({

// Once the filter is applied, messages will be shown and a log containing all filters will be saved for further auditing.

AuditLog.insert({ ts: new Date(), results: messages.length, u: user, fields: { msg, users, rid: room._id, room: room.name, startDate, endDate } });
AuditLog.insert({ ts: new Date(), results: messages.length, u: user, fields: { msg, users, rids, room: name, startDate, endDate, type, visitor, agent } });

return messages;
},
Expand Down
2 changes: 2 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@
"Agent_Info": "Agent Info",
"Agents": "Agents",
"Agent_added": "Agent added",
"Agent_Name_Placeholder": "Please enter an agent name...",
"Agent_removed": "Agent removed",
"Alerts": "Alerts",
"Alias": "Alias",
Expand Down Expand Up @@ -3710,6 +3711,7 @@
"Visitor_Email": "Visitor E-mail",
"Visitor_Info": "Visitor Info",
"Visitor_Name": "Visitor Name",
"Visitor_Name_Placeholder": "Please enter a visitor name...",
"Visitor_Navigation": "Visitor Navigation",
"Visitor_page_URL": "Visitor page URL",
"Visitor_time_on_site": "Visitor time on site",
Expand Down
2 changes: 2 additions & 0 deletions packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@
"Agent_Info": "Informações do Agente",
"Agents": "Agentes",
"Agent_added": "Agente adicionado",
"Agent_Name_Placeholder": "Digite o nome do agente ...",
"Agent_removed": "Agente removido",
"Alerts": "Alertas",
"Alias": "Apelido",
Expand Down Expand Up @@ -3341,6 +3342,7 @@
"Visitor_Email": "E-mail do Visitante",
"Visitor_Info": "Informações do Visitante",
"Visitor_Name": "Nome do Visitante",
"Visitor_Name_Placeholder": "Digite o nome do visitante ...",
"Visitor_Navigation": "Navegação do Visitante",
"Visitor_page_URL": "URL da página de visitante",
"Visitor_time_on_site": "Tempo do visitante no site",
Expand Down

0 comments on commit d46c864

Please sign in to comment.