From e9adb2adc2e7c67b2f76d1ff65972682249e1eea Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 21 Nov 2019 08:47:05 +0100 Subject: [PATCH] Fixes issue #2291 - Chat enhancement: Option to set Agent display name. --- .../_application_controller.coffee | 2 +- .../app/views/customer_chat/setting.jst.eco | 28 ++- app/models/chat.rb | 13 +- app/models/chat/session.rb | 21 ++ .../observer/chat/leave/background_job.rb | 9 +- ...541_fix_notification_email_without_body.rb | 2 +- lib/sessions/event/base.rb | 4 +- lib/sessions/event/chat_agent_state.rb | 17 ++ lib/sessions/event/chat_session_close.rb | 26 ++- lib/sessions/event/chat_session_init.rb | 18 ++ lib/sessions/event/chat_session_message.rb | 17 ++ lib/sessions/event/chat_session_notice.rb | 17 ++ lib/sessions/event/chat_session_start.rb | 24 ++- lib/sessions/event/chat_session_typing.rb | 15 ++ lib/sessions/event/chat_status_agent.rb | 15 ++ lib/sessions/event/chat_status_customer.rb | 18 ++ .../sessions/event/chat_session_start_spec.rb | 189 ++++++++++++++++++ 17 files changed, 402 insertions(+), 33 deletions(-) create mode 100644 spec/lib/sessions/event/chat_session_start_spec.rb diff --git a/app/assets/javascripts/app/controllers/_application_controller.coffee b/app/assets/javascripts/app/controllers/_application_controller.coffee index 42f2faf9f..e89853c01 100644 --- a/app/assets/javascripts/app/controllers/_application_controller.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller.coffee @@ -618,7 +618,7 @@ class App.ControllerModal extends App.Controller onShown: (e) => if @autoFocusOnFirstInput - @$('input:not([disabled]):not([type="hidden"]):not(".btn"):not([type="radio"]:not(:checked)), textarea').first().focus() + @$('.form-group').first().find('input:not([disabled]):not([type="hidden"]):not(".btn"), select:not([disabled]), textarea:not([disabled])').first().focus() @initalFormParams = @formParams() localOnClose: (e) => diff --git a/app/assets/javascripts/app/views/customer_chat/setting.jst.eco b/app/assets/javascripts/app/views/customer_chat/setting.jst.eco index a80c1203e..fda29dda4 100644 --- a/app/assets/javascripts/app/views/customer_chat/setting.jst.eco +++ b/app/assets/javascripts/app/views/customer_chat/setting.jst.eco @@ -1,7 +1,10 @@ +<% if @errors.settings: %> +
<%- @T(@errors.settings) %>
+<% end %>
- +
- <% for count in [1..20]: %> <% end %> @@ -9,9 +12,23 @@ <%- @Icon('arrow-down', 'dropdown-arrow') %>
-<% if @errors.settings: %> -
<%- @T(@errors.settings) %>
-<% end %> +
+ +
+ +
+
+
+ +
+ + <%- @Icon('arrow-down', 'dropdown-arrow') %> +
+
@@ -20,6 +37,7 @@ + <% for chat in @chats: %>
<%- @T('Enabled') %>
<%= chat.name %> diff --git a/app/models/chat.rb b/app/models/chat.rb index 145279937..8b8f89ae4 100644 --- a/app/models/chat.rb +++ b/app/models/chat.rb @@ -67,17 +67,8 @@ reconnect - chat session already exists, serve agent and session chat messages ( if chat_session if chat_session.state == 'running' - user = nil - if chat_session.user_id - chat_user = User.lookup(id: chat_session.user_id) - url = nil - if chat_user.image && chat_user.image != 'none' - url = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/api/v1/users/image/#{chat_user.image}" - end - user = { - name: chat_user.fullname, - avatar: url, - } + user = chat_session.agent_user + if user # get queue position if needed session = Chat::Session.messages_by_session_id(session_id) diff --git a/app/models/chat/session.rb b/app/models/chat/session.rb index 9640458d3..bbd78215b 100644 --- a/app/models/chat/session.rb +++ b/app/models/chat/session.rb @@ -14,6 +14,27 @@ class Chat::Session < ApplicationModel store :preferences + def agent_user + return if user_id.blank? + + user = User.lookup(id: user_id) + return if user.blank? + + fullname = user.fullname + chat_preferences = user.preferences[:chat] || {} + if chat_preferences[:alternative_name].present? + fullname = chat_preferences[:alternative_name] + end + url = nil + if user.image && user.image != 'none' && chat_preferences[:avatar_state] != 'disabled' + url = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/api/v1/users/image/#{user.image}" + end + { + name: fullname, + avatar: url, + } + end + def generate_session_id self.session_id = Digest::MD5.hexdigest(Time.zone.now.to_s + rand(99_999_999_999_999).to_s) end diff --git a/app/models/observer/chat/leave/background_job.rb b/app/models/observer/chat/leave/background_job.rb index d24edde24..1be9cdbe4 100644 --- a/app/models/observer/chat/leave/background_job.rb +++ b/app/models/observer/chat/leave/background_job.rb @@ -16,8 +16,13 @@ class Observer::Chat::Leave::BackgroundJob chat_session.save realname = 'Anonymous' - if @session && @session['id'] - realname = User.lookup(id: @session['id']).fullname + + # if it is a agent session, use the realname if the agent for close message + if @session && @session['id'] && chat_session.user_id + agent_user = chat_session.agent_user + if agent_user[:name] + realname = agent_user[:name] + end end # notify participants diff --git a/db/migrate/20190408000001_issue_2541_fix_notification_email_without_body.rb b/db/migrate/20190408000001_issue_2541_fix_notification_email_without_body.rb index a5d067ef6..1cd2edc41 100644 --- a/db/migrate/20190408000001_issue_2541_fix_notification_email_without_body.rb +++ b/db/migrate/20190408000001_issue_2541_fix_notification_email_without_body.rb @@ -10,7 +10,7 @@ class Issue2541FixNotificationEmailWithoutBody < ActiveRecord::Migration[5.1] # update jobs and triggers [::Job, ::Trigger].each do |model| - model.where(active: true).each do |record| + model.all.each do |record| next if record.perform.blank? %w[notification.email notification.sms].each do |action| diff --git a/lib/sessions/event/base.rb b/lib/sessions/event/base.rb index 7456c6519..94a840ac4 100644 --- a/lib/sessions/event/base.rb +++ b/lib/sessions/event/base.rb @@ -66,7 +66,7 @@ class Sessions::Event::Base def current_user_id if !@session error = { - event: "#{event}_error", + event: "#{@event}_error", data: { state: 'no_session', }, @@ -76,7 +76,7 @@ class Sessions::Event::Base end if @session['id'].blank? error = { - event: "#{event}_error", + event: "#{@event}_error", data: { state: 'no_session_user_id', }, diff --git a/lib/sessions/event/chat_agent_state.rb b/lib/sessions/event/chat_agent_state.rb index bf061b0a7..e41d5aea7 100644 --- a/lib/sessions/event/chat_agent_state.rb +++ b/lib/sessions/event/chat_agent_state.rb @@ -1,5 +1,22 @@ class Sessions::Event::ChatAgentState < Sessions::Event::ChatBase +=begin + +a agent triggers its own chat availability state + +payload + + { + event: 'chat_agent_state', + data: { + active: true, # true|false + }, + } + +return is sent as message back to peer + +=end + def run return super if super diff --git a/lib/sessions/event/chat_session_close.rb b/lib/sessions/event/chat_session_close.rb index 6a1dad8e7..a2d739b1a 100644 --- a/lib/sessions/event/chat_session_close.rb +++ b/lib/sessions/event/chat_session_close.rb @@ -1,18 +1,38 @@ class Sessions::Event::ChatSessionClose < Sessions::Event::ChatBase +=begin + +a agent or customer is closing the chat session + +payload + + { + event: 'chat_session_close', + data: {}, + } + +return is sent as message back to peer + +=end + def run return super if super return if !check_chat_session_exists realname = 'Anonymous' - if @session && @session['id'] - realname = User.lookup(id: @session['id']).fullname + + # if it is a agent session, use the realname if the agent for close message + chat_session = current_chat_session + if @session && @session['id'] && chat_session.user_id + agent_user = chat_session.agent_user + if agent_user[:name] + realname = agent_user[:name] + end end # check count of participents participents_count = 0 - chat_session = current_chat_session if chat_session.preferences[:participents] participents_count = chat_session.preferences[:participents].count end diff --git a/lib/sessions/event/chat_session_init.rb b/lib/sessions/event/chat_session_init.rb index b4fdf8575..935d0ebac 100644 --- a/lib/sessions/event/chat_session_init.rb +++ b/lib/sessions/event/chat_session_init.rb @@ -1,5 +1,23 @@ class Sessions::Event::ChatSessionInit < Sessions::Event::ChatBase +=begin + +a customer requests a new chat session + +payload + + { + event: 'chat_session_init', + data: { + chat_id: 'the id of chat', + url: 'the browser url', + }, + } + +return is sent as message back to peer + +=end + def run return super if super return if !check_chat_exists diff --git a/lib/sessions/event/chat_session_message.rb b/lib/sessions/event/chat_session_message.rb index da7123de3..29f6bf91b 100644 --- a/lib/sessions/event/chat_session_message.rb +++ b/lib/sessions/event/chat_session_message.rb @@ -1,5 +1,22 @@ class Sessions::Event::ChatSessionMessage < Sessions::Event::ChatBase +=begin + +a agent or customer creates a new chat session message + +payload + + { + event: 'chat_session_message', + data: { + content: 'some message', + }, + } + +return is sent as message back to peer + +=end + def run return super if super return if !check_chat_session_exists diff --git a/lib/sessions/event/chat_session_notice.rb b/lib/sessions/event/chat_session_notice.rb index 0dfe36c71..2da0f495e 100644 --- a/lib/sessions/event/chat_session_notice.rb +++ b/lib/sessions/event/chat_session_notice.rb @@ -1,5 +1,22 @@ class Sessions::Event::ChatSessionNotice < Sessions::Event::ChatBase +=begin + +a customer action has triggered a notice to the agent (e. g. view url of customer has changed) + +payload + + { + event: 'chat_session_notice', + data: { + message: 'url has changed to http://localhost', + }, + } + +return is sent as message back to peer + +=end + def run return super if super return if !check_chat_session_exists diff --git a/lib/sessions/event/chat_session_start.rb b/lib/sessions/event/chat_session_start.rb index 7a90a7c7d..c7d2f6eea 100644 --- a/lib/sessions/event/chat_session_start.rb +++ b/lib/sessions/event/chat_session_start.rb @@ -1,5 +1,20 @@ class Sessions::Event::ChatSessionStart < Sessions::Event::ChatBase +=begin + +a agent start`s a new chat session + +payload + + { + event: 'chat_session_start', + data: {}, + } + +return is sent as message back to peer + +=end + def run return super if super return if !permission_check('chat.agent', 'chat') @@ -23,14 +38,7 @@ class Sessions::Event::ChatSessionStart < Sessions::Event::ChatBase chat_session.save # send chat_session_init to client - url = nil - if chat_user.image && chat_user.image != 'none' - url = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/api/v1/users/image/#{chat_user.image}" - end - user = { - name: chat_user.fullname, - avatar: url, - } + user = chat_session.agent_user data = { event: 'chat_session_start', data: { diff --git a/lib/sessions/event/chat_session_typing.rb b/lib/sessions/event/chat_session_typing.rb index 8b5dfcf4f..80d18a66f 100644 --- a/lib/sessions/event/chat_session_typing.rb +++ b/lib/sessions/event/chat_session_typing.rb @@ -1,5 +1,20 @@ class Sessions::Event::ChatSessionTyping < Sessions::Event::ChatBase +=begin + +a agent or customer is typing a chat session message + +payload + + { + event: 'chat_session_typing', + data: {}, + } + +return is sent as message back to peer + +=end + def run return super if super return if !check_chat_session_exists diff --git a/lib/sessions/event/chat_status_agent.rb b/lib/sessions/event/chat_status_agent.rb index 657a0d704..a2da7b97c 100644 --- a/lib/sessions/event/chat_status_agent.rb +++ b/lib/sessions/event/chat_status_agent.rb @@ -1,5 +1,20 @@ class Sessions::Event::ChatStatusAgent < Sessions::Event::ChatBase +=begin + +a agent requests a the current state of all chat sessions + +payload + + { + event: 'chat_status_agent', + data: {}, + } + +return is sent as message back to peer + +=end + def run return super if super diff --git a/lib/sessions/event/chat_status_customer.rb b/lib/sessions/event/chat_status_customer.rb index c1b12018b..5a8bf7a51 100644 --- a/lib/sessions/event/chat_status_customer.rb +++ b/lib/sessions/event/chat_status_customer.rb @@ -1,5 +1,23 @@ class Sessions::Event::ChatStatusCustomer < Sessions::Event::ChatBase +=begin + +a customer requests the current state of a chat + +payload + + { + event: 'chat_status_agent', + data: { + session_id: 'the id of the current chat session', + url: 'optional url', # will trigger a chat_session_notice to agent + }, + } + +return is sent as message back to peer + +=end + def run return super if super return if !check_chat_exists diff --git a/spec/lib/sessions/event/chat_session_start_spec.rb b/spec/lib/sessions/event/chat_session_start_spec.rb new file mode 100644 index 000000000..da2e7711c --- /dev/null +++ b/spec/lib/sessions/event/chat_session_start_spec.rb @@ -0,0 +1,189 @@ +require 'rails_helper' + +RSpec.describe Sessions::Event::ChatSessionStart do + let(:client_id) { rand(123_456_789) } + let(:chat) { Chat.first } + let(:chat_session) do + Sessions.create('customer_session_id', { 'id' => customer.id }, {}) + Sessions.queue('customer_session_id') + Chat::Session.create( + chat_id: chat.id, + user_id: nil, + preferences: { participants: ['customer_session_id'] }, + state: 'waiting', + ) + end + let!(:agent) do + agent = create(:agent_user, preferences: { chat: { active: { chat.id.to_s => 'on' } } }) + file = File.open('test/data/image/1000x1000.png', 'rb') + contents = file.read + avatar = Avatar.add( + object: 'User', + o_id: agent.id, + default: true, + resize: { + content: contents, + mime_type: 'image/jpg', + }, + source: 'web', + deletable: true, + updated_by_id: 1, + created_by_id: 1, + ) + Avatar.set_default('User', agent.id, avatar.id) + agent.image = avatar.store_hash + agent.save! + agent + end + let!(:customer) { create(:customer_user) } + let(:subject_as_agent) do + Sessions.create(client_id, { 'id' => agent.id }, {}) + Sessions.queue(client_id) + described_class.new( + payload: { 'data' => chat_session.session_id }, + user_id: agent.id, + client_id: client_id, + clients: {}, + session: { 'id' => agent.id }, + ) + end + + before do + Setting.set('chat', true) + end + + context 'when starting a chat session as customer' do + let(:subject_as_customer) do + Sessions.create(client_id, { 'id' => customer.id }, {}) + Sessions.queue(client_id) + described_class.new( + payload: { 'data' => chat_session.session_id }, + user_id: customer.id, + client_id: client_id, + clients: {}, + session: { 'id' => customer.id }, + ) + end + + context 'without chat.agent permissions' do + it 'send out no_permission event to user' do + expect(subject_as_customer.run).to eq(nil) + messages = Sessions.queue(client_id) + expect(messages.count).to eq(1) + expect(messages).to eq([ + 'event' => 'chat_error', + 'data' => { + 'state' => 'no_permission' + } + ]) + end + end + end + + context 'when starting a chat session with invalid chat_session_id' do + let(:subject_with_invalid_session_id) do + described_class.new( + payload: { 'data' => 'not_existing_chat_session_id' }, + user_id: agent.id, + client_id: client_id, + clients: {}, + session: { 'id' => agent.id }, + ) + end + + it 'return failed message' do + expect(subject_with_invalid_session_id.run).to eq( + event: 'chat_session_start', + data: { + state: 'failed', + message: 'No session available.', + }, + ) + end + end + + context 'when starting a chat session as agent' do + it 'send out chat_session_start to customer and agent' do + expect(subject_as_agent.run).to eq(nil) + + messages_to_customer = Sessions.queue('customer_session_id') + expect(messages_to_customer.count).to eq(1) + expect(messages_to_customer[0]).to eq( + 'event' => 'chat_session_start', + 'data' => { + 'state' => 'ok', + 'agent' => { + 'name' => agent.fullname, + 'avatar' => 'http://zammad.example.com/api/v1/users/image/4cbd23059d5eb008f28a0f8bfbc723be', + }, + 'chat_id' => chat.id, + 'session_id' => chat_session.session_id, + }, + ) + + messages_to_agent = Sessions.queue(client_id) + expect(messages_to_agent.count).to eq(1) + expect(messages_to_agent[0]).to include( + 'event' => 'chat_session_start', + 'data' => hash_including( + 'session' => hash_including( + 'user_id' => agent.id, + 'state' => 'running', + 'preferences' => hash_including( + 'participants' => ['customer_session_id', client_id] + ), + 'id' => chat_session.id, + 'chat_id' => chat_session.chat_id, + 'session_id' => chat_session.session_id, + 'name' => nil, + ) + ) + ) + end + end + + context 'when starting a chat session as agent with alternative_name and no avatar_state' do + it 'send out chat_session_start to customer (with prepared agent information) and agent' do + agent.preferences[:chat] ||= {} + agent.preferences[:chat][:alternative_name] = 'some name' + agent.preferences[:chat][:avatar_state] = 'disabled' + agent.save! + expect(subject_as_agent.run).to eq(nil) + + messages_to_customer = Sessions.queue('customer_session_id') + expect(messages_to_customer.count).to eq(1) + expect(messages_to_customer[0]).to eq( + 'event' => 'chat_session_start', + 'data' => { + 'state' => 'ok', + 'agent' => { + 'name' => 'some name', + 'avatar' => nil, + }, + 'chat_id' => chat.id, + 'session_id' => chat_session.session_id, + }, + ) + + messages_to_agent = Sessions.queue(client_id) + expect(messages_to_agent.count).to eq(1) + expect(messages_to_agent[0]).to include( + 'event' => 'chat_session_start', + 'data' => hash_including( + 'session' => hash_including( + 'user_id' => agent.id, + 'state' => 'running', + 'preferences' => hash_including( + 'participants' => ['customer_session_id', client_id] + ), + 'id' => chat_session.id, + 'chat_id' => chat_session.chat_id, + 'session_id' => chat_session.session_id, + 'name' => nil, + ) + ) + ) + end + end + +end