diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 6ae6a4c42f0f..4c04064432c9 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -827,6 +827,7 @@ "False": "False", "Favorite_Rooms": "Enable Favorite Rooms", "Favorites": "Favorites", + "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "This feature depends on \"Send Visitor Navigation History as a Message\" to be enabled.", "Features_Enabled": "Features Enabled", "Field": "Field", "Field_removed": "Field removed", @@ -1463,6 +1464,8 @@ "Mobile_Notifications_Default_Alert": "Mobile Notifications Default Alert", "Monday": "Monday", "Monitor_history_for_changes_on": "Monitor History for Changes on", + "Send_Visitor_navigation_history_as_a_message": "Send Visitor Navigation History as a Message", + "Send_visitor_navigation_history_on_request": "Send Visitor Navigation History on Request", "More_channels": "More channels", "More_direct_messages": "More direct messages", "More_groups": "More private groups", @@ -1505,6 +1508,7 @@ "New_Trigger": "New Trigger", "New_version_available_(s)": "New version available (%s)", "New_videocall_request": "New Video Call Request", + "New_visitor_navigation": "New Navigation: __history__", "No_available_agents_to_transfer": "No available agents to transfer", "No_channel_with_name_%s_was_found": "No channel with name \"%s\" was found!", "No_channels_yet": "You aren't part of any channel yet", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index dc3e0d9f01e4..ed5812e037ac 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -794,6 +794,7 @@ "False": "Não", "Favorite_Rooms": "Ativar salas favoritas", "Favorites": "Favoritos", + "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Esta Funcionalidade depende que \"Enviar histórico de navegação do visitante como mensagem\" esteja habilitada.", "Features_Enabled": "Funcionalidades habilitadas", "Field": "Campo", "Field_removed": "Campo removido", @@ -1458,6 +1459,7 @@ "New_Trigger": "Novo Gatilho", "New_version_available_(s)": "Nova versão disponível (% s)", "New_videocall_request": "Nova requisição de chamada de vídeo", + "New_visitor_navigation": "Nova Navegação: __history__", "No_available_agents_to_transfer": "Nenhum agente disponível para transferir", "No_channel_with_name_%s_was_found": "Nenhum canal com nome \"%s\" foi encontrado!", "No_channels_yet": "Você não faz parte de nenhum canal ainda.", @@ -1818,6 +1820,8 @@ "Send_request_on_visitor_message": "Enviar requisição para mensagens do Visitante", "Send_request_on_agent_message": "Enviar requisição para mensagens do Agente", "Send_Test": "Enviar teste", + "Send_Visitor_navigation_history_as_a_message": "Enviar histórico de navegação do visitante como mensagem", + "Send_visitor_navigation_history_on_request": "Enviar histórico de navegação do visitante na requisição", "Send_welcome_email": "Enviar e-mail de boas-vindas", "Send_your_JSON_payloads_to_this_URL": "Envie seu payload JSON para esta URL.", "Sending": "Enviando ...", @@ -2348,4 +2352,4 @@ "your_message_optional": "sua mensagem (opcional)", "Your_password_is_wrong": "Sua senha está errada!", "Your_push_was_sent_to_s_devices": "Sua notificação foi enviada para %s dispositivos" -} \ No newline at end of file +} diff --git a/packages/rocketchat-lib/server/models/Messages.js b/packages/rocketchat-lib/server/models/Messages.js index 80792259960a..763f62a3b9fd 100644 --- a/packages/rocketchat-lib/server/models/Messages.js +++ b/packages/rocketchat-lib/server/models/Messages.js @@ -299,6 +299,17 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base { return this.findOne(query); } + findByRoomIdAndType(roomId, type, options) { + const query = { + rid: roomId, + t: type + }; + + if (options == null) { options = {}; } + + return this.find(query, options); + } + findByRoomId(roomId, options) { const query = { rid: roomId @@ -569,6 +580,34 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base { return record; } + createNavigationHistoryWithRoomIdMessageAndUser(roomId, message, user, extraData) { + const type = 'livechat_navigation_history'; + const room = RocketChat.models.Rooms.findOneById(roomId, { fields: { sysMes: 1 }}); + if ((room != null ? room.sysMes : undefined) === false) { + return; + } + const record = { + t: type, + rid: roomId, + ts: new Date, + msg: message, + u: { + _id: user._id, + username: user.username + }, + groupable: false + }; + + if (RocketChat.settings.get('Message_Read_Receipt_Enabled')) { + record.unread = true; + } + + _.extend(record, extraData); + + record._id = this.insertOrUpsert(record); + return record; + } + createUserJoinWithRoomIdAndUser(roomId, user, extraData) { const message = user.username; return this.createWithTypeRoomIdMessageAndUser('uj', roomId, message, user, extraData); diff --git a/packages/rocketchat-livechat/.app/client/lib/hooks.js b/packages/rocketchat-livechat/.app/client/lib/hooks.js index 3a525c34cf5d..89436156dc19 100644 --- a/packages/rocketchat-livechat/.app/client/lib/hooks.js +++ b/packages/rocketchat-livechat/.app/client/lib/hooks.js @@ -7,7 +7,7 @@ const api = { Triggers.processRequest(info); } - Meteor.call('livechat:pageVisited', visitor.getToken(), info); + Meteor.call('livechat:pageVisited', visitor.getToken(), visitor.getRoom(), info); }, setCustomField(key, value, overwrite = true) { diff --git a/packages/rocketchat-livechat/.app/client/views/messages.js b/packages/rocketchat-livechat/.app/client/views/messages.js index f27e7d00f2d3..3f48ab191fe0 100644 --- a/packages/rocketchat-livechat/.app/client/views/messages.js +++ b/packages/rocketchat-livechat/.app/client/views/messages.js @@ -7,7 +7,7 @@ Template.messages.helpers({ return ChatMessage.find({ rid: visitor.getRoom(), t: { - '$ne': 't' + '$nin': ['t', 'livechat_navigation_history'] } }, { sort: { diff --git a/packages/rocketchat-livechat/.app/imports/client/visitor.js b/packages/rocketchat-livechat/.app/imports/client/visitor.js index 75e45843e7bd..76bd78956f18 100644 --- a/packages/rocketchat-livechat/.app/imports/client/visitor.js +++ b/packages/rocketchat-livechat/.app/imports/client/visitor.js @@ -63,10 +63,11 @@ export default { this.roomSubscribed = roomId; + const msgTypesNotDisplayed = ['livechat_video_call', 'livechat_navigation_history', 'au']; msgStream.on(roomId, { token: this.getToken() }, (msg) => { if (msg.t === 'command') { Commands[msg.msg] && Commands[msg.msg](); - } else if ((msg.t !== 'livechat_video_call') && (msg.t !== 'au')) { + } else if (!msgTypesNotDisplayed.includes(msg.t)) { ChatMessage.upsert({ _id: msg._id }, msg); if (msg.t === 'livechat-close') { diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.html b/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.html index b6fc919ce506..be44263dd093 100644 --- a/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.html +++ b/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.html @@ -8,8 +8,8 @@

{{_ "Navigation_History"}}

{{else}}
diff --git a/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.js b/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.js index 06a62d8a7d0d..3679a2d1b7ce 100644 --- a/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.js +++ b/packages/rocketchat-livechat/client/views/app/tabbar/visitorNavigation.js @@ -1,20 +1,21 @@ import moment from 'moment'; +const visitorNavigationHistory = new Mongo.Collection('visitor_navigation_history'); Template.visitorNavigation.helpers({ loadingNavigation() { return !Template.instance().pageVisited.ready(); }, - pageVisited() { + pages() { const room = ChatRoom.findOne({ _id: this.rid }, { fields: { 'v.token': 1 } }); - if (room && room.v && room.v.token) { - return LivechatPageVisited.find({ token: room.v.token }, { sort: { ts: -1 } }); + if (room) { + return visitorNavigationHistory.find({ rid: room._id }, { sort: { ts: -1 } }); } }, pageTitle() { - return this.page.title || t('Empty_title'); + return this.navigation.page.title || t('Empty_title'); }, accessDateTime() { diff --git a/packages/rocketchat-livechat/config.js b/packages/rocketchat-livechat/config.js index 242351f2b909..236da5c59013 100644 --- a/packages/rocketchat-livechat/config.js +++ b/packages/rocketchat-livechat/config.js @@ -181,6 +181,15 @@ Meteor.startup(function() { i18nLabel: 'Send_request_on_agent_message' }); + RocketChat.settings.add('Send_visitor_navigation_history_livechat_webhook_request', false, { + type: 'boolean', + group: 'Livechat', + section: 'CRM_Integration', + i18nLabel: 'Send_visitor_navigation_history_on_request', + i18nDescription: 'Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled', + enableQuery: { _id: 'Livechat_Visitor_navigation_as_a_message', value: true } + }); + RocketChat.settings.add('Livechat_webhook_on_capture', false, { type: 'boolean', group: 'Livechat', @@ -236,6 +245,13 @@ Meteor.startup(function() { ] }); + RocketChat.settings.add('Livechat_Visitor_navigation_as_a_message', false, { + type: 'boolean', + group: 'Livechat', + public: true, + i18nLabel: 'Send_Visitor_navigation_history_as_a_message' + }); + RocketChat.settings.add('Livechat_enable_office_hours', false, { type: 'boolean', group: 'Livechat', diff --git a/packages/rocketchat-livechat/messageTypes.js b/packages/rocketchat-livechat/messageTypes.js index 670ae5b5b8c2..631ecc47166f 100644 --- a/packages/rocketchat-livechat/messageTypes.js +++ b/packages/rocketchat-livechat/messageTypes.js @@ -1,3 +1,17 @@ +RocketChat.MessageTypes.registerType({ + id: 'livechat_navigation_history', + system: true, + message: 'New_visitor_navigation', + data(message) { + if (!message.navigation || !message.navigation.page) { + return; + } + return { + history: `${ (message.navigation.page.title ? `${ message.navigation.page.title } - ` : '') + message.navigation.page.location.href }` + }; + } +}); + RocketChat.MessageTypes.registerType({ id: 'livechat_video_call', system: true, diff --git a/packages/rocketchat-livechat/package.js b/packages/rocketchat-livechat/package.js index 374874fd56e2..537db759cac5 100644 --- a/packages/rocketchat-livechat/package.js +++ b/packages/rocketchat-livechat/package.js @@ -183,6 +183,7 @@ Package.onUse(function(api) { // models api.addFiles('server/models/Users.js', 'server'); api.addFiles('server/models/Rooms.js', 'server'); + api.addFiles('server/models/Messages.js', 'server'); api.addFiles('server/models/LivechatExternalMessage.js', ['client', 'server']); api.addFiles('server/models/LivechatCustomField.js', 'server'); api.addFiles('server/models/LivechatDepartment.js', 'server'); diff --git a/packages/rocketchat-livechat/server/hooks/sendToCRM.js b/packages/rocketchat-livechat/server/hooks/sendToCRM.js index 46e48dcc70fe..fe4f68218063 100644 --- a/packages/rocketchat-livechat/server/hooks/sendToCRM.js +++ b/packages/rocketchat-livechat/server/hooks/sendToCRM.js @@ -1,3 +1,11 @@ +const msgNavType = 'livechat_navigation_history'; + +const sendMessageType = (msgType) => { + const sendNavHistory = RocketChat.settings.get('Livechat_Visitor_navigation_as_a_message') && RocketChat.settings.get('Send_visitor_navigation_history_livechat_webhook_request'); + + return sendNavHistory && msgType === msgNavType; +}; + function sendToCRM(type, room, includeMessages = true) { const postData = RocketChat.Livechat.getLivechatRoomGuestInfo(room); @@ -14,7 +22,7 @@ function sendToCRM(type, room, includeMessages = true) { if (messages) { messages.forEach((message) => { - if (message.t) { + if (message.t && !sendMessageType(message.t)) { return; } const msg = { @@ -28,6 +36,11 @@ function sendToCRM(type, room, includeMessages = true) { if (message.u.username !== postData.visitor.username) { msg.agentId = message.u._id; } + + if (message.t === msgNavType) { + msg.navigation = message.navigation; + } + postData.messages.push(msg); }); } @@ -73,9 +86,9 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { } else if (!RocketChat.settings.get('Livechat_webhook_on_agent_message')) { return message; } - // if the message has a type means it is a special message (like the closing comment), so skips - if (message.t) { + // unless the settings that handle with visitor navigation history are enabled + if (message.t && !sendMessageType(message.t)) { return message; } diff --git a/packages/rocketchat-livechat/server/lib/Livechat.js b/packages/rocketchat-livechat/server/lib/Livechat.js index 2a98b4ace411..c8e19915cb74 100644 --- a/packages/rocketchat-livechat/server/lib/Livechat.js +++ b/packages/rocketchat-livechat/server/lib/Livechat.js @@ -105,6 +105,10 @@ RocketChat.Livechat = { throw new Meteor.Error('cannot-access-room'); } + if (newRoom) { + RocketChat.models.Messages.setRoomIdByToken(guest.token, room._id); + } + return { room, newRoom }; }, sendMessage({ guest, message, roomInfo, agent }) { @@ -315,9 +319,31 @@ RocketChat.Livechat = { }); }, - savePageHistory(token, pageInfo) { + savePageHistory(token, roomId, pageInfo) { if (pageInfo.change === RocketChat.Livechat.historyMonitorType) { - return RocketChat.models.LivechatPageVisited.saveByToken(token, pageInfo); + + const user = RocketChat.models.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 (!RocketChat.settings.get('Livechat_Visitor_navigation_as_a_message')) { + extraData._hidden = true; + } + + return RocketChat.models.Messages.createNavigationHistoryWithRoomIdMessageAndUser(roomId, `${ pageTitle } - ${ pageUrl }`, user, extraData); } return; diff --git a/packages/rocketchat-livechat/server/methods/pageVisited.js b/packages/rocketchat-livechat/server/methods/pageVisited.js index 25027f03d597..0097ac4aab5b 100644 --- a/packages/rocketchat-livechat/server/methods/pageVisited.js +++ b/packages/rocketchat-livechat/server/methods/pageVisited.js @@ -1,5 +1,5 @@ Meteor.methods({ - 'livechat:pageVisited'(token, pageInfo) { - return RocketChat.Livechat.savePageHistory(token, pageInfo); + 'livechat:pageVisited'(token, room, pageInfo) { + RocketChat.Livechat.savePageHistory(token, room, pageInfo); } }); diff --git a/packages/rocketchat-livechat/server/methods/registerGuest.js b/packages/rocketchat-livechat/server/methods/registerGuest.js index 5cef4e7c19a4..0e021d5e6bcb 100644 --- a/packages/rocketchat-livechat/server/methods/registerGuest.js +++ b/packages/rocketchat-livechat/server/methods/registerGuest.js @@ -8,7 +8,7 @@ Meteor.methods({ }); // update visited page history to not expire - RocketChat.models.LivechatPageVisited.keepHistoryForToken(token); + RocketChat.models.Messages.keepHistoryForToken(token); return { userId diff --git a/packages/rocketchat-livechat/server/methods/sendTranscript.js b/packages/rocketchat-livechat/server/methods/sendTranscript.js index 949642ab232e..a53e271d4380 100644 --- a/packages/rocketchat-livechat/server/methods/sendTranscript.js +++ b/packages/rocketchat-livechat/server/methods/sendTranscript.js @@ -19,7 +19,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-room', 'Invalid room'); } - const messages = RocketChat.models.Messages.findVisibleByRoomId(rid, { sort: { 'ts' : 1 }}); + const messages = RocketChat.models.Messages.findVisibleByRoomIdNotContainingTypes(rid, ['livechat_navigation_history'], { sort: { 'ts' : 1 }}); const header = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Header') || ''); const footer = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer') || ''); diff --git a/packages/rocketchat-livechat/server/methods/setDepartmentForVisitor.js b/packages/rocketchat-livechat/server/methods/setDepartmentForVisitor.js index da77d2c2305c..0c4f40598477 100644 --- a/packages/rocketchat-livechat/server/methods/setDepartmentForVisitor.js +++ b/packages/rocketchat-livechat/server/methods/setDepartmentForVisitor.js @@ -6,7 +6,7 @@ Meteor.methods({ }); // update visited page history to not expire - RocketChat.models.LivechatPageVisited.keepHistoryForToken(token); + RocketChat.models.Messages.keepHistoryForToken(token); return true; } diff --git a/packages/rocketchat-livechat/server/models/Messages.js b/packages/rocketchat-livechat/server/models/Messages.js new file mode 100644 index 000000000000..da2046be7426 --- /dev/null +++ b/packages/rocketchat-livechat/server/models/Messages.js @@ -0,0 +1,27 @@ +RocketChat.models.Messages.keepHistoryForToken = function(token) { + return this.update({ + 'navigation.token': token, + expireAt: { + $exists: true + } + }, { + $unset: { + expireAt: 1 + } + }, { + multi: true + }); +}; + +RocketChat.models.Messages.setRoomIdByToken = function(token, rid) { + return this.update({ + 'navigation.token': token, + rid: null + }, { + $set: { + rid + } + }, { + multi: true + }); +}; diff --git a/packages/rocketchat-livechat/server/models/Rooms.js b/packages/rocketchat-livechat/server/models/Rooms.js index 6786e71f3ac1..7ac8f0445caf 100644 --- a/packages/rocketchat-livechat/server/models/Rooms.js +++ b/packages/rocketchat-livechat/server/models/Rooms.js @@ -63,6 +63,21 @@ RocketChat.models.Rooms.findLivechatById = function(_id, fields) { return this.findOne(query, options); }; +RocketChat.models.Rooms.findLivechatById = function(_id, fields) { + const options = {}; + + if (fields) { + options.fields = fields; + } + + const query = { + t: 'l', + _id + }; + + return this.findOne(query, options); +}; + /** * Get the next visitor name * @return {string} The next visitor name diff --git a/packages/rocketchat-livechat/server/publications/visitorPageVisited.js b/packages/rocketchat-livechat/server/publications/visitorPageVisited.js index 8420694b8138..72d7b42df3b1 100644 --- a/packages/rocketchat-livechat/server/publications/visitorPageVisited.js +++ b/packages/rocketchat-livechat/server/publications/visitorPageVisited.js @@ -1,4 +1,5 @@ Meteor.publish('livechat:visitorPageVisited', function({ rid: roomId }) { + if (!this.userId) { return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorPageVisited' })); } @@ -7,11 +8,28 @@ Meteor.publish('livechat:visitorPageVisited', function({ rid: roomId }) { return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorPageVisited' })); } + const self = this; const room = RocketChat.models.Rooms.findOneById(roomId); - if (room && room.v && room.v.token) { - return RocketChat.models.LivechatPageVisited.findByToken(room.v.token); + if (room) { + const handle = RocketChat.models.Messages.findByRoomIdAndType(room._id, 'livechat_navigation_history').observeChanges({ + added(id, fields) { + self.added('visitor_navigation_history', id, fields); + }, + changed(id, fields) { + self.changed('visitor_navigation_history', id, fields); + }, + removed(id) { + self.removed('visitor_navigation_history', id); + } + }); + + self.ready(); + + self.onStop(function() { + handle.stop(); + }); } else { - return this.ready(); + self.ready(); } }); diff --git a/packages/rocketchat-theme/client/imports/general/base_old.css b/packages/rocketchat-theme/client/imports/general/base_old.css index 7005727ff8a3..78602ce7ecab 100644 --- a/packages/rocketchat-theme/client/imports/general/base_old.css +++ b/packages/rocketchat-theme/client/imports/general/base_old.css @@ -3271,6 +3271,12 @@ font-weight: 400; } + + &.livechat_navigation_history { + & .thumb, & .user, & .info { + display: none; + } + } } .rc-old .image-labels { diff --git a/server/startup/migrations/v123.js b/server/startup/migrations/v123.js new file mode 100644 index 000000000000..e1e0b7b4fe1e --- /dev/null +++ b/server/startup/migrations/v123.js @@ -0,0 +1,89 @@ +let pageVisitedCollection; +let messageCollection; +let roomCollection; + +const roomIdByToken = {}; + +const batchSize = 5000; + +async function migrateHistory(total, current) { + console.log(`Livechat history migration ${ current }/${ total }`); + + const items = await pageVisitedCollection.find({}).limit(batchSize).toArray(); + + const tokens = items.filter((item) => item.token && !roomIdByToken[item.token]).map((item) => item.token); + const rooms = await roomCollection.find({ + 'v.token': { + $in: tokens + } + }, { + fields: { + 'v.token': 1 + } + }).toArray(); + + rooms.forEach((room) => { + roomIdByToken[room.v.token] = room._id; + }); + + const actions = items.reduce((result, item) => { + const msg = { + t: 'livechat_navigation_history', + rid: roomIdByToken[item.token] || null, // prevent from being `undefined` + ts: item.ts, + msg: `${ item.page.title } - ${ item.page.location.href }`, + u: { + _id : 'rocket.cat', + username : 'rocket.cat' + }, + groupable : false, + navigation : { + page: item.page, + token: item.token + } + }; + if (!roomIdByToken[item.token] && item.expireAt) { + msg.expireAt = item.expireAt; + } + result.insert.push(msg); + result.remove.push(item._id); + + return result; + }, { insert: [], remove: [] }); + + const batch = Promise.all([ + messageCollection.insertMany(actions.insert), + pageVisitedCollection.removeMany({ _id: { $in: actions.remove } }) + ]); + if (actions.remove.length === batchSize) { + await batch; + return migrateHistory(total, current + batchSize); + } + + return batch; +} + + +RocketChat.Migrations.add({ + version: 123, + up() { + pageVisitedCollection = RocketChat.models.LivechatPageVisited.model.rawCollection(); + messageCollection = RocketChat.models.Messages.model.rawCollection(); + roomCollection = RocketChat.models.Rooms.model.rawCollection(); + + /* + * Move visitor navigation history to messages + */ + Meteor.setTimeout(async() => { + const pages = pageVisitedCollection.find({}); + const total = await pages.count(); + await pages.close(); + + console.log('Migrating livechat visitors navigation history to livechat messages. This might take a long time ...'); + + await migrateHistory(total, 0); + + console.log('Livechat visitors navigation history migration finished.'); + }, 1000); + } +});