import dns from 'dns'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { HTTP } from 'meteor/http'; import _ from 'underscore'; import s from 'underscore.string'; import moment from 'moment'; import UAParser from 'ua-parser-js'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; import { Analytics } from './Analytics'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../callbacks/server'; import { Users, LivechatRooms, Messages, Subscriptions, Settings, Rooms, LivechatDepartmentAgents, LivechatDepartment, LivechatCustomField, LivechatVisitors, LivechatInquiry, } from '../../../models/server'; import { Logger } from '../../../logger'; import { addUserRoles, hasPermission, hasRole, removeUserFromRoles, canAccessRoom } from '../../../authorization'; import * as Mailer from '../../../mailer'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { FileUpload } from '../../../file-upload/server'; import { normalizeTransferredByData, parseAgentCustomFields, updateDepartmentAgents } from './Helper'; import { Apps, AppEvents } from '../../../apps/server'; import { businessHourManager } from '../business-hour'; import notifications from '../../../notifications/server/lib/Notifications'; export const Livechat = { Analytics, historyMonitorType: 'url', logger: new Logger('Livechat', { sections: { webhook: 'Webhook', }, }), findGuest(token) { return LivechatVisitors.getVisitorByToken(token, { fields: { name: 1, username: 1, token: 1, visitorEmails: 1, department: 1, }, }); }, online(department) { if (settings.get('Livechat_accept_chats_with_no_agents')) { return true; } if (settings.get('Livechat_assign_new_conversation_to_bot')) { const botAgents = Livechat.getBotAgents(department); if (botAgents.count() > 0) { return true; } } return Livechat.checkOnlineAgents(department); }, getNextAgent(department) { return RoutingManager.getNextAgent(department); }, getAgents(department) { if (department) { // TODO: This and all others should get the user's info as well return LivechatDepartmentAgents.findByDepartmentId(department); } return Users.findAgents(); }, getOnlineAgents(department, agent) { if (agent?.agentId) { return Users.findOnlineAgents(agent.agentId); } if (department) { return LivechatDepartmentAgents.getOnlineForDepartment(department); } return Users.findOnlineAgents(); }, checkOnlineAgents(department, agent) { if (agent?.agentId) { return Users.checkOnlineAgents(agent.agentId); } if (department) { return LivechatDepartmentAgents.checkOnlineForDepartment(department); } return Users.checkOnlineAgents(); }, getBotAgents(department) { if (department) { return LivechatDepartmentAgents.getBotsForDepartment(department); } return Users.findBotAgents(); }, getRequiredDepartment(onlineRequired = true) { const departments = LivechatDepartment.findEnabledWithAgents(); return departments.fetch().find((dept) => { if (!dept.showOnRegistration) { return false; } if (!onlineRequired) { return true; } const onlineAgents = LivechatDepartmentAgents.getOnlineForDepartment(dept._id); return onlineAgents && onlineAgents.count() > 0; }); }, async getRoom(guest, message, roomInfo, agent, extraData) { let room = LivechatRooms.findOneById(message.rid); let newRoom = false; if (room && !room.open) { message.rid = Random.id(); room = null; } if (room == null) { const defaultAgent = callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, guest); // if no department selected verify if there is at least one active and pick the first if (!defaultAgent && !guest.department) { const department = this.getRequiredDepartment(); if (department) { guest.department = department._id; } } // delegate room creation to QueueManager room = await QueueManager.requestRoom({ guest, message, roomInfo, agent: defaultAgent, extraData }); newRoom = true; } if (!room || room.v.token !== guest.token) { throw new Meteor.Error('cannot-access-room'); } if (newRoom) { Messages.setRoomIdByToken(guest.token, room._id); } return { room, newRoom }; }, async sendMessage({ guest, message, roomInfo, agent }) { const { room, newRoom } = await this.getRoom(guest, message, roomInfo, agent); if (guest.name) { message.alias = guest.name; } return _.extend(sendMessage(guest, message, room), { newRoom, showConnecting: this.showConnecting() }); }, updateMessage({ guest, message }) { check(message, Match.ObjectIncluding({ _id: String })); const originalMessage = Messages.findOneById(message._id); if (!originalMessage || !originalMessage._id) { return; } const editAllowed = settings.get('Message_AllowEditing'); const editOwn = originalMessage.u && originalMessage.u._id === guest._id; if (!editAllowed || !editOwn) { throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', { method: 'livechatUpdateMessage' }); } updateMessage(message, guest); return true; }, deleteMessage({ guest, message }) { check(message, Match.ObjectIncluding({ _id: String })); const msg = Messages.findOneById(message._id); if (!msg || !msg._id) { return; } const deleteAllowed = settings.get('Message_AllowDeleting'); const editOwn = msg.u && msg.u._id === guest._id; if (!deleteAllowed || !editOwn) { throw new Meteor.Error('error-action-not-allowed', 'Message deleting not allowed', { method: 'livechatDeleteMessage' }); } deleteMessage(message, guest); return true; }, registerGuest({ token, name, email, department, phone, username, connectionData } = {}) { check(token, String); let userId; const updateUser = { $set: { token, }, }; const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); if (user) { userId = user._id; } else { if (!username) { username = LivechatVisitors.getNextVisitorUsername(); } let existingUser = null; if (s.trim(email) !== '' && (existingUser = LivechatVisitors.findOneGuestByEmailAddress(email))) { userId = existingUser._id; } else { const userData = { username, ts: new Date(), }; if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) { const connection = this.connection || connectionData; if (connection && connection.httpHeaders) { userData.userAgent = connection.httpHeaders['user-agent']; userData.ip = connection.httpHeaders['x-real-ip'] || connection.httpHeaders['x-forwarded-for'] || connection.clientAddress; userData.host = connection.httpHeaders.host; } } userId = LivechatVisitors.insert(userData); } } if (phone?.number) { updateUser.$set.phone = [ { phoneNumber: phone.number }, ]; } if (email && email.trim() !== '') { updateUser.$set.visitorEmails = [ { address: email }, ]; } if (name) { updateUser.$set.name = name; } if (!department) { Object.assign(updateUser, { $unset: { department: 1 } }); } else { const dep = LivechatDepartment.findOneByIdOrName(department); updateUser.$set.department = dep && dep._id; } LivechatVisitors.updateById(userId, updateUser); return userId; }, setDepartmentForGuest({ token, department } = {}) { check(token, String); const updateUser = { $set: { department, }, }; const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); if (user) { return LivechatVisitors.updateById(user._id, updateUser); } return false; }, saveGuest({ _id, name, email, phone, livechatData = {} }, userId) { const updateData = {}; if (name) { updateData.name = name; } if (email) { updateData.email = email; } if (phone) { updateData.phone = phone; } const customFields = {}; const fields = LivechatCustomField.find({ scope: 'visitor' }); if (!userId || hasPermission(userId, 'edit-livechat-room-customfields')) { fields.forEach((field) => { if (!livechatData.hasOwnProperty(field._id)) { return; } const value = s.trim(livechatData[field._id]); if (value !== '' && field.regexp !== undefined && field.regexp !== '') { const regexp = new RegExp(field.regexp); if (!regexp.test(value)) { throw new Meteor.Error(TAPi18n.__('error-invalid-custom-field-value', { field: field.label })); } } customFields[field._id] = value; }); updateData.livechatData = customFields; } const ret = LivechatVisitors.saveGuestById(_id, updateData); Meteor.defer(() => { Apps.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); callbacks.run('livechat.saveGuest', updateData); }); return ret; }, closeRoom({ user, visitor, room, comment, options = {} }) { if (!room || room.t !== 'l' || !room.open) { return false; } const params = callbacks.run('livechat.beforeCloseRoom', { room, options }); const { extraData } = params; const now = new Date(); const { _id: rid, servedBy, transcriptRequest } = room; const serviceTimeDuration = servedBy && (now.getTime() - servedBy.ts) / 1000; const closeData = { closedAt: now, chatDuration: (now.getTime() - room.ts) / 1000, ...serviceTimeDuration && { serviceTimeDuration }, ...extraData, }; if (user) { closeData.closer = 'user'; closeData.closedBy = { _id: user._id, username: user.username, }; } else if (visitor) { closeData.closer = 'visitor'; closeData.closedBy = { _id: visitor._id, username: visitor.username, }; } LivechatRooms.closeByRoomId(rid, closeData); LivechatInquiry.removeByRoomId(rid); Subscriptions.removeByRoomId(rid); const message = { t: 'livechat-close', msg: comment, groupable: false, transcriptRequested: !!transcriptRequest, }; // Retreive the closed room room = LivechatRooms.findOneByIdOrName(rid); sendMessage(user || visitor, message, room); Messages.createCommandWithRoomIdAndUser('promptTranscript', rid, closeData.closedBy); Meteor.defer(() => { /** * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed * in the next major version of the Apps-Engine */ Apps.getBridges().getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, room); Apps.getBridges().getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, room); }); callbacks.runAsync('livechat.closeRoom', room); return true; }, removeRoom(rid) { check(rid, String); const room = LivechatRooms.findOneById(rid); if (!room) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:removeRoom' }); } Messages.removeByRoomId(rid); Subscriptions.removeByRoomId(rid); LivechatInquiry.removeByRoomId(rid); return LivechatRooms.removeById(rid); }, setCustomFields({ token, key, value, overwrite } = {}) { check(token, String); check(key, String); check(value, String); check(overwrite, Boolean); const customField = LivechatCustomField.findOneById(key); if (!customField) { throw new Meteor.Error('invalid-custom-field'); } if (customField.regexp !== undefined && customField.regexp !== '') { const regexp = new RegExp(customField.regexp); if (!regexp.test(value)) { throw new Meteor.Error(TAPi18n.__('error-invalid-custom-field-value', { field: key })); } } if (customField.scope === 'room') { return LivechatRooms.updateDataByToken(token, key, value, overwrite); } return LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite); }, enabled() { return settings.get('Livechat_enabled'); }, getInitSettings() { const rcSettings = {}; Settings.findNotHiddenPublic([ 'Livechat_title', 'Livechat_title_color', 'Livechat_enable_message_character_limit', 'Livechat_message_character_limit', 'Message_MaxAllowedSize', 'Livechat_enabled', 'Livechat_registration_form', 'Livechat_allow_switching_departments', 'Livechat_offline_title', 'Livechat_offline_title_color', 'Livechat_offline_message', 'Livechat_offline_success_message', 'Livechat_offline_form_unavailable', 'Livechat_display_offline_form', 'Livechat_videocall_enabled', 'Jitsi_Enabled', 'Language', 'Livechat_enable_transcript', 'Livechat_transcript_message', 'Livechat_fileupload_enabled', 'FileUpload_Enabled', 'Livechat_conversation_finished_message', 'Livechat_conversation_finished_text', 'Livechat_name_field_registration_form', 'Livechat_email_field_registration_form', 'Livechat_registration_form_message', 'Livechat_force_accept_data_processing_consent', 'Livechat_data_processing_consent_text', 'Livechat_show_agent_info', ]).forEach((setting) => { rcSettings[setting._id] = setting.value; }); rcSettings.Livechat_history_monitor_type = settings.get('Livechat_history_monitor_type'); rcSettings.Livechat_Show_Connecting = this.showConnecting(); return rcSettings; }, saveRoomInfo(roomData, guestData, userId) { const { livechatData = {} } = roomData; const customFields = {}; if (!userId || hasPermission(userId, 'edit-livechat-room-customfields')) { const fields = LivechatCustomField.find({ scope: 'room' }); fields.forEach((field) => { if (!livechatData.hasOwnProperty(field._id)) { return; } const value = s.trim(livechatData[field._id]); if (value !== '' && field.regexp !== undefined && field.regexp !== '') { const regexp = new RegExp(field.regexp); if (!regexp.test(value)) { throw new Meteor.Error(TAPi18n.__('error-invalid-custom-field-value', { field: field.label })); } } customFields[field._id] = value; }); roomData.livechatData = customFields; } if (!LivechatRooms.saveRoomById(roomData)) { return false; } Meteor.defer(() => { Apps.triggerEvent(AppEvents.IPostLivechatRoomSaved, roomData._id); }); callbacks.runAsync('livechat.saveRoom', roomData); if (!_.isEmpty(guestData.name)) { const { _id: rid } = roomData; const { name } = guestData; return Rooms.setFnameById(rid, name) && LivechatInquiry.setNameByRoomId(rid, name) // This one needs to be the last since the agent may not have the subscription // when the conversation is in the queue, then the result will be 0(zero) && Subscriptions.updateDisplayNameByRoomId(rid, name); } }, closeOpenChats(userId, comment) { const user = Users.findOneById(userId); LivechatRooms.findOpenByAgent(userId).forEach((room) => { this.closeRoom({ user, room, comment }); }); }, forwardOpenChats(userId) { LivechatRooms.findOpenByAgent(userId).forEach((room) => { const guest = LivechatVisitors.findOneById(room.v._id); const user = Users.findOneById(userId); const { _id, username, name } = user; const transferredBy = normalizeTransferredByData({ _id, username, name }, room); this.transfer(room, guest, { roomId: room._id, transferredBy, departmentId: guest.department }); }); }, savePageHistory(token, roomId, pageInfo) { if (pageInfo.change !== Livechat.historyMonitorType) { return; } const user = Users.findOneById('rocket.cat'); const pageTitle = pageInfo.title; const pageUrl = pageInfo.location.href; const extraData = { navigation: { page: pageInfo, token, }, }; if (!roomId) { // keep history of unregistered visitors for 1 month const keepHistoryMiliseconds = 2592000000; extraData.expireAt = new Date().getTime() + keepHistoryMiliseconds; } if (!settings.get('Livechat_Visitor_navigation_as_a_message')) { extraData._hidden = true; } return Messages.createNavigationHistoryWithRoomIdMessageAndUser(roomId, `${ pageTitle } - ${ pageUrl }`, user, extraData); }, saveTransferHistory(room, transferData) { const { departmentId: previousDepartment } = room; const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData; check(transferredBy, Match.ObjectIncluding({ _id: String, username: String, name: Match.Maybe(String), type: String, })); const { _id, username } = transferredBy; const transfer = { transferData: { transferredBy, ts: new Date(), scope: scope || (nextDepartment ? 'department' : 'agent'), comment, ...previousDepartment && { previousDepartment }, ...nextDepartment && { nextDepartment }, ...transferredTo && { transferredTo }, }, }; return Messages.createTransferHistoryWithRoomIdMessageAndUser(room._id, '', { _id, username }, transfer); }, async transfer(room, guest, transferData) { if (room.onHold) { throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:transfer' }); } if (transferData.departmentId) { transferData.department = LivechatDepartment.findOneById(transferData.departmentId, { fields: { name: 1 } }); } return RoutingManager.transferRoom(room, guest, transferData); }, returnRoomAsInquiry(rid, departmentId) { const room = LivechatRooms.findOneById(rid); if (!room) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:returnRoomAsInquiry' }); } if (!room.open) { throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnRoomAsInquiry' }); } if (room.onHold) { throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:returnRoomAsInquiry' }); } if (!room.servedBy) { return false; } const user = Users.findOne(room.servedBy._id); if (!user || !user._id) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:returnRoomAsInquiry' }); } // find inquiry corresponding to room const inquiry = LivechatInquiry.findOne({ rid }); if (!inquiry) { return false; } const transferredBy = normalizeTransferredByData(user, room); const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy }; try { this.saveTransferHistory(room, transferData); RoutingManager.unassignAgent(inquiry, departmentId); } catch (e) { console.error(e); throw new Meteor.Error('error-returning-inquiry', 'Error returning inquiry to the queue', { method: 'livechat:returnRoomAsInquiry' }); } callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room }); return true; }, sendRequest(postData, callback, attempts = 10) { if (!attempts) { return; } const secretToken = settings.get('Livechat_secret_token'); const headers = { headers: { 'X-RocketChat-Livechat-Token': secretToken } }; const options = { data: postData, ...secretToken !== '' && secretToken !== undefined && { headers } }; try { return HTTP.post(settings.get('Livechat_webhookUrl'), options); } catch (e) { Livechat.logger.webhook.error(`Response error on ${ 11 - attempts } try ->`, e); // try 10 times after 10 seconds each Livechat.logger.webhook.warn('Will try again in 10 seconds ...'); setTimeout(Meteor.bindEnvironment(function() { Livechat.sendRequest(postData, callback, attempts--); }), 10000); } }, getLivechatRoomGuestInfo(room) { const visitor = LivechatVisitors.findOneById(room.v._id); const agent = Users.findOneById(room.servedBy && room.servedBy._id); const ua = new UAParser(); ua.setUA(visitor.userAgent); const postData = { _id: room._id, label: room.fname || room.label, // using same field for compatibility topic: room.topic, createdAt: room.ts, lastMessageAt: room.lm, tags: room.tags, customFields: room.livechatData, visitor: { _id: visitor._id, token: visitor.token, name: visitor.name, username: visitor.username, email: null, phone: null, department: visitor.department, ip: visitor.ip, os: ua.getOS().name && `${ ua.getOS().name } ${ ua.getOS().version }`, browser: ua.getBrowser().name && `${ ua.getBrowser().name } ${ ua.getBrowser().version }`, customFields: visitor.livechatData, }, }; if (agent) { const customFields = parseAgentCustomFields(agent.customFields); postData.agent = { _id: agent._id, username: agent.username, name: agent.name, email: null, ...customFields && { customFields }, }; if (agent.emails && agent.emails.length > 0) { postData.agent.email = agent.emails[0].address; } } if (room.crmData) { postData.crmData = room.crmData; } if (visitor.visitorEmails && visitor.visitorEmails.length > 0) { postData.visitor.email = visitor.visitorEmails; } if (visitor.phone && visitor.phone.length > 0) { postData.visitor.phone = visitor.phone; } return postData; }, addAgent(username) { check(username, String); const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } }); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:addAgent' }); } if (addUserRoles(user._id, 'livechat-agent')) { Users.setOperator(user._id, true); this.setUserStatusLivechat(user._id, 'available'); return user; } return false; }, addManager(username) { check(username, String); const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } }); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:addManager' }); } if (addUserRoles(user._id, 'livechat-manager')) { return user; } return false; }, removeAgent(username) { check(username, String); const user = Users.findOneByUsername(username, { fields: { _id: 1 } }); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:removeAgent' }); } const { _id } = user; if (removeUserFromRoles(_id, 'livechat-agent')) { Users.setOperator(_id, false); Users.removeLivechatData(_id); this.setUserStatusLivechat(_id, 'not-available'); LivechatDepartmentAgents.removeByAgentId(_id); return true; } return false; }, removeManager(username) { check(username, String); const user = Users.findOneByUsername(username, { fields: { _id: 1 } }); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:removeManager' }); } return removeUserFromRoles(user._id, 'livechat-manager'); }, removeGuest(_id) { check(_id, String); const guest = LivechatVisitors.findOneById(_id); if (!guest) { throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:removeGuest' }); } this.cleanGuestHistory(_id); return LivechatVisitors.removeById(_id); }, setUserStatusLivechat(userId, status) { const user = Users.setLivechatStatus(userId, status); callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); return user; }, cleanGuestHistory(_id) { const guest = LivechatVisitors.findOneById(_id); if (!guest) { throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:cleanGuestHistory' }); } const { token } = guest; check(token, String); LivechatRooms.findByVisitorToken(token).forEach((room) => { FileUpload.removeFilesByRoomId(room._id); Messages.removeByRoomId(room._id); }); Subscriptions.removeByVisitorToken(token); LivechatRooms.removeByVisitorToken(token); LivechatInquiry.removeByVisitorToken(token); }, saveDepartmentAgents(_id, departmentAgents) { check(_id, String); check(departmentAgents, { upsert: Match.Maybe([ Match.ObjectIncluding({ agentId: String, username: String, count: Match.Maybe(Match.Integer), order: Match.Maybe(Match.Integer), }), ]), remove: Match.Maybe([ Match.ObjectIncluding({ agentId: String, username: Match.Maybe(String), count: Match.Maybe(Match.Integer), order: Match.Maybe(Match.Integer), }), ]), }); const department = LivechatDepartment.findOneById(_id); if (!department) { throw new Meteor.Error('error-department-not-found', 'Department not found', { method: 'livechat:saveDepartmentAgents' }); } return updateDepartmentAgents(_id, departmentAgents, department.enabled); }, saveDepartment(_id, departmentData, departmentAgents) { check(_id, Match.Maybe(String)); const defaultValidations = { enabled: Boolean, name: String, description: Match.Optional(String), showOnRegistration: Boolean, email: String, showOnOfflineForm: Boolean, requestTagBeforeClosingChat: Match.Optional(Boolean), chatClosingTags: Match.Optional([String]), }; // The Livechat Form department support addition/custom fields, so those fields need to be added before validating Object.keys(departmentData).forEach((field) => { if (!defaultValidations.hasOwnProperty(field)) { defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); } }); check(departmentData, defaultValidations); check(departmentAgents, Match.Maybe({ upsert: Match.Maybe(Array), remove: Match.Maybe(Array), })); const { requestTagBeforeClosingChat, chatClosingTags } = departmentData; if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { throw new Meteor.Error('error-validating-department-chat-closing-tags', 'At least one closing tag is required when the department requires tag(s) on closing conversations.', { method: 'livechat:saveDepartment' }); } if (_id) { const department = LivechatDepartment.findOneById(_id); if (!department) { throw new Meteor.Error('error-department-not-found', 'Department not found', { method: 'livechat:saveDepartment' }); } } const departmentDB = LivechatDepartment.createOrUpdateDepartment(_id, departmentData); if (departmentDB && departmentAgents) { updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); } return departmentDB; }, saveAgentInfo(_id, agentData, agentDepartments) { check(_id, Match.Maybe(String)); check(agentData, Object); check(agentDepartments, [String]); const user = Users.findOneById(_id); if (!user || !hasRole(_id, 'livechat-agent')) { throw new Meteor.Error('error-user-is-not-agent', 'User is not a livechat agent', { method: 'livechat:saveAgentInfo' }); } Users.setLivechatData(_id, agentData); LivechatDepartment.saveDepartmentsByAgent(user, agentDepartments); return true; }, removeDepartment(_id) { check(_id, String); const department = LivechatDepartment.findOneById(_id, { fields: { _id: 1 } }); if (!department) { throw new Meteor.Error('department-not-found', 'Department not found', { method: 'livechat:removeDepartment' }); } const ret = LivechatDepartment.removeById(_id); const agentsIds = LivechatDepartmentAgents.findByDepartmentId(_id).fetch().map((agent) => agent.agentId); LivechatDepartmentAgents.removeByDepartmentId(_id); if (ret) { Meteor.defer(() => { callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); }); } return ret; }, showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; }, sendEmail(from, to, replyTo, subject, html) { Mailer.send({ to, from, replyTo, subject, html, }); }, sendTranscript({ token, rid, email, subject, user }) { check(rid, String); check(email, String); const room = LivechatRooms.findOneById(rid); const visitor = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1, token: 1, language: 1, username: 1, name: 1 } }); const userLanguage = (visitor && visitor.language) || settings.get('Language') || 'en'; // 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 = '
${ author } ${ datetime }
${ message.msg }
`; html += singleMessage; }); html = `${ html }Sent from: ${ host }
`); } html = html.concat(`Visitor name: ${ name }
Visitor email: ${ email }
Message:
${ emailMessage }