diff --git a/app/assets/javascripts/app/controllers/cti.coffee b/app/assets/javascripts/app/controllers/cti.coffee new file mode 100644 index 000000000..08b09ec27 --- /dev/null +++ b/app/assets/javascripts/app/controllers/cti.coffee @@ -0,0 +1,114 @@ +class App.CTI extends App.Controller + constructor: -> + super + + @meta = + active: false + + preferences = @Session.get('preferences') || {} + @meta.active = preferences.cti || false + + @load() + + App.Event.bind( + 'cti_event' + (data) => + console.log('cti_event', data) + if data.state is 'newCall' + console.log('notify') + @notify(data) + 'cti_event' + ) + App.Event.bind( + 'cti_list_push' + (data) => + @list = data + @render() + 'cti_list_push' + ) + + # fetch data, render view + load: -> + @ajax( + id: 'cti_log' + type: 'GET' + url: "#{@apiPath}/cti/log" + success: (data) => + @list = data + @render() + ) + + notify: (data) -> + console.log(data) + #return if ! + if data.state is 'newCall' && data.direction is 'in' + App.Event.trigger 'notify', { + type: 'notice' + msg: App.i18n.translateContent('Call from %s for %s', data.from, data.to) + timeout: 2500 + } + + featureActive: => + return true + if @Config.get('sipgate_integration') + return true + false + + render: -> + if !@isRole('CTI') + @renderScreenUnauthorized(objectName: 'CTI') + return + + @html App.view('cti/index')( + list: @list + ) + + @updateNavMenu() + + show: (params) => + @title 'CTI', true + @navupdate '#cti' + + counter: -> + counter = 0 + + switch: (state = undefined) => + + # read state + if state is undefined + return @meta.active + + @meta.active = state + + # update user preferences + @ajax( + id: 'preferences' + type: 'PUT' + url: "#{@apiPath}/users/preferences" + data: JSON.stringify(user: {cti: state}) + processData: true + ) + + updateNavMenu: => + delay = -> + App.Event.trigger('menu:render') + @delay(delay, 200, 'updateNavMenu') + +class CTIRouter extends App.ControllerPermanent + constructor: (params) -> + super + + # check authentication + return if !@authenticate(false, 'CTI') + + App.TaskManager.execute( + key: 'CTI' + controller: 'CTI' + params: {} + show: true + persistent: true + ) + +App.Config.set('cti', CTIRouter, 'Routes') +App.Config.set('CTI', { controller: 'CTI', authentication: true }, 'permanentTask') +App.Config.set('CTI', { prio: 1300, parent: '', name: 'Phone', target: '#cti', key: 'CTI', shown: false, role: ['CTI'], class: 'phone' }, 'NavBar') diff --git a/app/assets/javascripts/app/views/cti/index.jst.eco b/app/assets/javascripts/app/views/cti/index.jst.eco new file mode 100644 index 000000000..d1f660668 --- /dev/null +++ b/app/assets/javascripts/app/views/cti/index.jst.eco @@ -0,0 +1,28 @@ +
+

<%- @T('Caller log') %>

+ +
+ + + + + + + + + + + + <% for item in @list: %> + + + + + + + + <% end %> + +
<%- @T('From') %><%- @T('To') %><%- @T('State') %><%- @T('Comment') %><%- @T('Time') %>
<%= item.from %><%= item.to %><%= item.state %><%= item.comment %><%- @humanTime(item.created_at) %>
+
+
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 27dfe7ccd..37a12b9d3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -287,12 +287,11 @@ class ApplicationController < ActionController::Base } end - def authentication_check(auth_param = {} ) + def authentication_check(auth_param = {}) result = authentication_check_only(auth_param) # check if basic_auth fallback is possible if auth_param[:basic_auth_promt] && result[:auth] == false - return request_http_basic_authentication end diff --git a/app/controllers/integration/sipgate_controller.rb b/app/controllers/integration/sipgate_controller.rb index 450317584..0cbf6f518 100644 --- a/app/controllers/integration/sipgate_controller.rb +++ b/app/controllers/integration/sipgate_controller.rb @@ -1,54 +1,60 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + require 'builder' class Integration::SipgateController < ApplicationController - before_action { http_log_config facility: 'sipgate.io' } + + # list current caller log + def index + return if !authentication_check + return if deny_if_not_role('CTI') + list = Cti::Log.order('created_at DESC').limit(60) + render json: list + end # notify about inbound call / block inbound call def in + http_log_config facility: 'sipgate.io' return if !configured? - config = Setting.get('sipgate_config') - config_inbound = config[:inbound] || {} - block_caller_ids = config_inbound[:block_caller_ids] || [] - if params['event'] == 'newCall' + config = Setting.get('sipgate_config') + config_inbound = config[:inbound] || {} + block_caller_ids = config_inbound[:block_caller_ids] || [] + # check if call need to be blocked block_caller_ids.each {|item| next unless item[:caller_id] == params['from'] xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do + content = xml.Response(onHangup: url, onAnswer: url) do xml.Reject('reason' => 'busy') end send_data content, type: 'application/xml; charset=UTF-8;' - params['Reject'] = 'busy' - Sessions.broadcast( - event: 'sipgate.io', - data: params - ) + #params['Reject'] = 'busy' + params['comment'] = 'reject, busy' + if params['user'] + params['comment'] = "#{params['user']} -> reject, busy" + end + update_log(params) return true } end + update_log(params) + xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) - + content = xml.Response(onHangup: url, onAnswer: url) send_data content, type: 'application/xml; charset=UTF-8;' - - # search for caller - Sessions.broadcast( - event: 'sipgate.io', - data: params - ) - end # set caller id of outbound call def out + http_log_config facility: 'sipgate.io' return if !configured? config = Setting.get('sipgate_config') @@ -61,31 +67,32 @@ class Integration::SipgateController < ApplicationController # set callerId content = nil to = params[:to] + from = nil if to config_outbound.each {|row| dest = row[:dest].gsub(/\*/, '.+?') next if to !~ /^#{dest}$/ - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do - xml.Dial(callerId: row[:caller_id]) { xml.Number(params[:to]) } + from = row[:caller_id] + content = xml.Response(onHangup: url, onAnswer: url) do + xml.Dial(callerId: from) { xml.Number(params[:to]) } end break } if !content && default_caller_id - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do + from = default_caller_id + content = xml.Response(onHangup: url, onAnswer: url) do xml.Dial(callerId: default_caller_id) { xml.Number(params[:to]) } end end else - content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) + content = xml.Response(onHangup: url, onAnswer: url) end send_data content, type: 'application/xml; charset=UTF-8;' - - # notify about outbound call - Sessions.broadcast( - event: 'sipgate.io:out', - data: params - ) + if from + params['from'] = from + end + update_log(params) end private @@ -103,23 +110,69 @@ class Integration::SipgateController < ApplicationController true end + def update_log(params) + + user = params['user'] + if params['user'] && params['user'].class == Array + user = params['user'].join(', ') + end + from_comment = nil + to_comment = nil + if params['direction'] == 'in' + to_comment = user + else + from_comment = user + end + comment = nil + if params['cause'] + comment = params['cause'] + end + + if params['event'] == 'newCall' + Cti::Log.create( + direction: params['direction'], + from: params['from'], + from_comment: from_comment, + to: params['to'], + to_comment: to_comment, + call_id: params['callId'], + comment: comment, + state: params['event'], + ) + elsif params['event'] == 'answer' + log = Cti::Log.find_by(call_id: params['callId']) + raise "No such call_id #{params['callId']}" if !log + log.state = 'answer' + log.comment = comment + log.save + elsif params['event'] == 'hangup' + log = Cti::Log.find_by(call_id: params['callId']) + raise "No such call_id #{params['callId']}" if !log + log.state = 'hangup' + log.comment = comment + log.save + else + raise "Unknown event #{params['event']}" + end + + end + def xml_error(error) xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! content = xml.Response() do xml.Error(error) end - send_data content, type: 'application/xml; charset=UTF-8;', status: '500' + send_data content, type: 'application/xml; charset=UTF-8;', status: 422 end def base_url http_type = Setting.get('http_type') fqdn = Setting.get('fqdn') - "#{http_type}://#{fqdn}/api/v1/sipgate" end - def in_url - "#{base_url}/in" + def url + "#{base_url}/#{params['direction']}" end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a2c727aae..072fb7e0a 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -85,7 +85,7 @@ class UsersController < ApplicationController group_ids = [] role_ids = [] if count <= 2 - Role.where(name: [ Z_ROLENAME_ADMIN, 'Agent', 'Chat']).each { |role| + Role.where(name: [ Z_ROLENAME_ADMIN, 'Agent', 'Chat', 'CTI']).each { |role| role_ids.push role.id } Group.all().each { |group| diff --git a/app/models/cti/log.rb b/app/models/cti/log.rb new file mode 100644 index 000000000..bf97b138a --- /dev/null +++ b/app/models/cti/log.rb @@ -0,0 +1,78 @@ +module Cti + class Log < ApplicationModel + self.table_name = 'cti_logs' + + after_create :push_event, :push_caller_list + after_update :push_event, :push_caller_list + after_destroy :push_event, :push_caller_list + +=begin + + Cti::Log.create( + direction: 'in', + from: '007', + from_comment: '', + to: '008', + to_comment: '', + call_id: '1', + comment: '', + state: 'newCall', + ) + + Cti::Log.create( + direction: 'in', + from: '007', + from_comment: '', + to: '008', + to_comment: '', + call_id: '2', + comment: '', + state: 'answer', + ) + + Cti::Log.create( + direction: 'in', + from: '009', + from_comment: '', + to: '010', + to_comment: '', + call_id: '3', + comment: '', + state: 'hangup', + ) + +=end + + def push_event + users = User.of_role('CTI') + users.each {|user| + + # send notify about event + Sessions.send_to( + user.id, + { + event: 'cti_event', + data: self, + }, + ) + } + end + + def push_caller_list + list = Cti::Log.order('created_at DESC').limit(60) + + users = User.of_role('CTI') + users.each {|user| + + # send notify on create/update/delete + Sessions.send_to( + user.id, + { + event: 'cti_list_push', + data: list, + }, + ) + } + end + end +end diff --git a/config/routes/integration_sipgate.rb b/config/routes/integration_sipgate.rb index 04d4d8f91..d569898d2 100644 --- a/config/routes/integration_sipgate.rb +++ b/config/routes/integration_sipgate.rb @@ -1,5 +1,6 @@ Zammad::Application.routes.draw do + match '/api/v1/cti/log', to: 'integration/sipgate#index', via: :get match '/api/v1/sipgate/in', to: 'integration/sipgate#in', via: :post match '/api/v1/sipgate/out', to: 'integration/sipgate#out', via: :post diff --git a/db/migrate/20160422000003_create_cti_log.rb b/db/migrate/20160422000003_create_cti_log.rb new file mode 100644 index 000000000..3bbfb0b61 --- /dev/null +++ b/db/migrate/20160422000003_create_cti_log.rb @@ -0,0 +1,26 @@ +class CreateCtiLog < ActiveRecord::Migration + def up + create_table :cti_logs do |t| + t.string :direction, limit: 20, null: false + t.string :state, limit: 20, null: false + t.string :from, limit: 100, null: false + t.string :from_comment, limit: 250, null: true + t.string :to, limit: 100, null: false + t.string :to_comment, limit: 250, null: true + t.string :call_id, limit: 250, null: false + t.string :comment, limit: 500, null: true + t.timestamps null: false + end + add_index :cti_logs, [:call_id], unique: true + add_index :cti_logs, [:direction] + add_index :cti_logs, [:from] + + Role.create_if_not_exists( + name: 'CTI', + note: 'Access to CTI feature.', + updated_by_id: 1, + created_by_id: 1 + ) + + end +end diff --git a/db/seeds.rb b/db/seeds.rb index ae9c440ea..00a85194e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1915,6 +1915,13 @@ Role.create_if_not_exists( updated_by_id: 1, created_by_id: 1 ) +Role.create_if_not_exists( + id: 6, + name: 'CTI', + note: 'Access to CTI feature.', + updated_by_id: 1, + created_by_id: 1 +) Group.create_if_not_exists( id: 1, diff --git a/test/integration/sipgate_controller_test.rb b/test/integration/sipgate_controller_test.rb index fa9432433..98fb66ecc 100644 --- a/test/integration/sipgate_controller_test.rb +++ b/test/integration/sipgate_controller_test.rb @@ -5,6 +5,8 @@ require 'rexml/document' class SipgateControllerTest < ActionDispatch::IntegrationTest setup do + Cti::Log.destroy_all + Setting.create_or_update( title: 'sipgate.io integration', name: 'sipgate_integration', @@ -65,12 +67,27 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest preferences: { prio: 2 }, ) + groups = Group.where(name: 'Users') + roles = Role.where(name: 'Agent') + agent = User.create_or_update( + login: 'cti-agent@example.com', + firstname: 'E', + lastname: 'S', + email: 'cti-agent@example.com', + password: 'agentpw', + active: true, + roles: roles, + groups: groups, + updated_by_id: 1, + created_by_id: 1, + ) + end test 'basic call' do # inbound - I - params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278-1&user%5B%5D=user+1&user%5B%5D=user+2' post '/api/v1/sipgate/in', params assert_response(200) on_hangup = nil @@ -85,7 +102,7 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) # inbound - II - block caller - params = 'event=newCall&direction=in&from=491715000000&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + params = 'event=newCall&direction=in&from=491715000000&to=4930600000000&callId=4991155921769858278-2&user%5B%5D=user+1&user%5B%5D=user+2' post '/api/v1/sipgate/in', params assert_response(200) on_hangup = nil @@ -105,7 +122,7 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest assert_equal('busy', reason) # outbound - I - set default_caller_id - params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=8621106404543334274&user%5B%5D=user+1' + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=8621106404543334274-3&user%5B%5D=user+1' post '/api/v1/sipgate/out', params assert_response(200) on_hangup = nil @@ -126,11 +143,11 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest end assert_equal('4930777000000', caller_id) assert_equal('4912347114711', number_to_dail) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_answer) # outbound - II - set caller_id based on routing_table by explicite number - params = 'event=newCall&direction=out&from=4930600000000&to=491714000000&callId=8621106404543334274&user%5B%5D=user+1' + params = 'event=newCall&direction=out&from=4930600000000&to=491714000000&callId=8621106404543334274-4&user%5B%5D=user+1' post '/api/v1/sipgate/out', params assert_response(200) on_hangup = nil @@ -151,11 +168,11 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest end assert_equal('41715880339000', caller_id) assert_equal('491714000000', number_to_dail) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_answer) # outbound - III - set caller_id based on routing_table by 41* - params = 'event=newCall&direction=out&from=4930600000000&to=4147110000000&callId=8621106404543334274&user%5B%5D=user+1' + params = 'event=newCall&direction=out&from=4930600000000&to=4147110000000&callId=8621106404543334274-5&user%5B%5D=user+1' post '/api/v1/sipgate/out', params assert_response(200) on_hangup = nil @@ -176,14 +193,14 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest end assert_equal('41715880339000', caller_id) assert_equal('4147110000000', number_to_dail) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) - assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/out', on_answer) # no config Setting.set('sipgate_config', {}) - params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278&user%5B%5D=user+1&user%5B%5D=user+2' + params = 'event=newCall&direction=in&from=4912347114711&to=4930600000000&callId=4991155921769858278-6&user%5B%5D=user+1&user%5B%5D=user+2' post '/api/v1/sipgate/in', params - assert_response(500) + assert_response(422) error = nil content = @response.body response = REXML::Document.new(content) @@ -194,4 +211,123 @@ class SipgateControllerTest < ActionDispatch::IntegrationTest end + test 'log call' do + + # outbound - I - new call + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=1234567890-1&user%5B%5D=user+1' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-1') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal(nil, log.comment) + assert_equal('newCall', log.state) + + # outbound - I - hangup by agent + params = 'event=hangup&direction=out&callId=1234567890-1&cause=cancel' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-1') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('cancel', log.comment) + assert_equal('hangup', log.state) + + # outbound - II - new call + params = 'event=newCall&direction=out&from=4930600000000&to=4912347114711&callId=1234567890-2&user%5B%5D=user+1' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal(nil, log.comment) + assert_equal('newCall', log.state) + + # outbound - II - answer by customer + params = 'event=answer&direction=out&callId=1234567890-2&from=4930600000000&to=4912347114711' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal(nil, log.comment) + assert_equal('answer', log.state) + + # outbound - II - hangup by customer + params = 'event=hangup&direction=out&callId=1234567890-2&cause=normalClearing&from=4930600000000&to=4912347114711' + post '/api/v1/sipgate/out', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-2') + assert(log) + assert_equal('4930777000000', log.from) + assert_equal('4912347114711', log.to) + assert_equal('out', log.direction) + assert_equal('user 1', log.from_comment) + assert_equal('normalClearing', log.comment) + assert_equal('hangup', log.state) + + # inbound - I - new call + params = 'event=newCall&direction=in&to=4930600000000&from=4912347114711&callId=1234567890-3&user%5B%5D=user+1' + post '/api/v1/sipgate/in', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal(nil, log.comment) + assert_equal('newCall', log.state) + + # inbound - I - answer by customer + params = 'event=answer&direction=in&callId=1234567890-3&to=4930600000000&from=4912347114711' + post '/api/v1/sipgate/in', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal(nil, log.comment) + assert_equal('answer', log.state) + + # inbound - I - hangup by customer + params = 'event=hangup&direction=in&callId=1234567890-3&cause=normalClearing&to=4930600000000&from=4912347114711' + post '/api/v1/sipgate/in', params + assert_response(200) + log = Cti::Log.find_by(call_id: '1234567890-3') + assert(log) + assert_equal('4930600000000', log.to) + assert_equal('4912347114711', log.from) + assert_equal('in', log.direction) + assert_equal('user 1', log.to_comment) + assert_equal('normalClearing', log.comment) + assert_equal('hangup', log.state) + + get '/api/v1/cti/log' + assert_response(401) + + headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' } + credentials = ActionController::HttpAuthentication::Basic.encode_credentials('cti-agent@example.com', 'agentpw') + get '/api/v1/cti/log', {}, headers.merge('Authorization' => credentials) + assert_response(200) + result = JSON.parse(@response.body) + assert_equal(result.class, Array) + assert_equal(3, result.count) + + end + end