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

[NEW][ENTERPRISE] Support Omnichannel conversations auditing #17692

Merged
merged 5 commits into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -3708,6 +3709,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 @@ -3339,6 +3340,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