diff --git a/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee b/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee new file mode 100644 index 000000000..7baecf237 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee @@ -0,0 +1,156 @@ +class Index extends App.ControllerIntegrationBase + featureIntegration: 'sipgate_integration' + featureName: 'sipgate.io' + featureConfig: 'sipgate_config' + description: [ + ['This service shows you contacts of incoming calls and a caller list in realtime.'] + ['Also caller id of outbound calls can be changed.'] + ] + + render: => + super + new Form( + el: @$('.js-form') + ) + + new App.HttpLog( + el: @$('.js-log') + facility: 'sipgate.io' + ) + +class Form extends App.Controller + events: + 'submit form': 'update' + 'click .js-inboundBlockCallerId .js-add': 'addInboundBlockCallerId' + 'click .js-outboundRouting .js-add': 'addOutboundRouting' + 'click .js-inboundBlockCallerId .js-remove': 'removeInboundBlockCallerId' + 'click .js-outboundRouting .js-remove': 'removeOutboundRouting' + + constructor: -> + super + + # check authentication + return if !@authenticate() + + @subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false) + + currentConfig: -> + config = App.Setting.get('sipgate_config') + if !config.outbound + config.outbound = {} + if !config.outbound.routing_table + config.outbound.routing_table = [] + if !config.inbound + config.inbound = {} + if !config.inbound.block_caller_ids + config.inbound.block_caller_ids = [] + config + + setConfig: (value) -> + App.Setting.set('sipgate_config', value) + + render: => + @config = @currentConfig() + + @html App.view('integration/sipgate')( + config: @config + ) + + updateCurrentConfig: => + config = @config + cleanupInput = @cleanupInput + + # default caller_id + default_caller_id = @$('input[name=default_caller_id]').val() + config.outbound.default_caller_id = cleanupInput(default_caller_id) + + # routing table + config.outbound.routing_table = [] + @$('.js-outboundRouting .js-row').each(-> + dest = cleanupInput($(@).find('input[name="dest"]').val()) + caller_id = cleanupInput($(@).find('input[name="caller_id"]').val()) + note = $(@).find('input[name="note"]').val() + config.outbound.routing_table.push { + dest: dest + caller_id: caller_id + note: note + } + ) + + # blocked caller ids + config.inbound.block_caller_ids = [] + @$('.js-inboundBlockCallerId .js-row').each(-> + caller_id = $(@).find('input[name="caller_id"]').val() + note = $(@).find('input[name="note"]').val() + config.inbound.block_caller_ids.push { + caller_id: cleanupInput(caller_id) + note: note + } + ) + + @config = config + + update: (e) => + e.preventDefault() + @updateCurrentConfig() + @setConfig(@config) + + cleanupInput: (value) -> + return value if !value + value.replace(/\s/g, '').trim() + + addInboundBlockCallerId: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + caller_id = element.find('input[name="caller_id"]').val() + note = element.find('input[name="note"]').val() + @config.inbound.block_caller_ids.push { + caller_id: @cleanupInput(caller_id) + note: note + } + @setConfig(@config) + @render() + + addOutboundRouting: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + dest = @cleanupInput(element.find('input[name="dest"]').val()) + caller_id = @cleanupInput(element.find('input[name="caller_id"]').val()) + note = element.find('input[name="note"]').val() + @config.outbound.routing_table.push { + dest: dest + caller_id: caller_id + note: note + } + @setConfig(@config) + @render() + + removeInboundBlockCallerId: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + + removeOutboundRouting: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + +class State + @current: -> + App.Setting.get('sipgate_integration') + +App.Config.set( + 'IntegrationSipgate' + { + name: 'sipgate.io' + target: '#system/integration/sipgate' + description: 'VoIP services provide.' + controller: Index + state: State + } + 'NavBarIntegrations' +) diff --git a/app/assets/javascripts/app/controllers/widget/http_log.coffee b/app/assets/javascripts/app/controllers/widget/http_log.coffee index 01951edad..4a625b3bd 100644 --- a/app/assets/javascripts/app/controllers/widget/http_log.coffee +++ b/app/assets/javascripts/app/controllers/widget/http_log.coffee @@ -9,22 +9,23 @@ class App.HttpLog extends App.Controller fetch: => @ajax( - id: 'http_logs' - type: 'GET' - url: "#{@apiPath}/http_logs/#{@facility}" + id: 'http_logs' + type: 'GET' + url: "#{@apiPath}/http_logs/#{@facility}" data: limit: @limit || 50 processData: true success: (data) => - @records = data - @render() + if !@records[0] || (data[0] && @records[0] && data[0].updated_at isnt @records[0].updated_at) + @records = data + @render() + @delay(@fetch, 20000) ) render: => @html App.view('widget/http_log')( records: @records ) - #@delay(message, 2000) show: (e) => e.preventDefault() diff --git a/app/assets/javascripts/app/views/integration/sipgate.jst.eco b/app/assets/javascripts/app/views/integration/sipgate.jst.eco new file mode 100644 index 000000000..a21d2c380 --- /dev/null +++ b/app/assets/javascripts/app/views/integration/sipgate.jst.eco @@ -0,0 +1,78 @@ +
+ +

<%- @T('Inbound') %>

+ +

<%- @T('Blocked caller ids based on sender caller id.') %> + +

+ + + + + +<% for row in @config.inbound.block_caller_ids: %> + + + +
<%- @T('Caller id to block') %> + <%- @T('Note') %> + <%- @T('Action') %> +
+ +
<%- @Icon('trash') %> <%- @T('Remove') %>
+<% end %> +
+ +
<%- @Icon('plus-small') %> <%- @T('Add') %>
+
+
+ +

<%- @T('Outbound') %>

+ +

<%- @T('Set caller id of outbound calls based on destination caller id.') %> + +

+ + + + + +<% for row in @config.outbound.routing_table: %> + + + +
<%- @T('Destination caller id') %> + <%- @T('Set outbound caller id') %> + <%- @T('Note') %> + <%- @T('Action') %> +
+ + +
<%- @Icon('trash') %> <%- @T('Remove') %>
+<% end %> +
+ + +
<%- @Icon('plus-small') %> <%- @T('Add') %>
+
+
+ +

<%- @T('Default caller id.') %> + +

+ + + + + + + +
<%- @T('Default caller id') %> + <%- @T('Note') %> +
+ <%- @T('Default caller id for outbound calls.') %> +
+
+ + +
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/widget/http_log.jst.eco b/app/assets/javascripts/app/views/widget/http_log.jst.eco index 4f3790b12..349bab12e 100644 --- a/app/assets/javascripts/app/views/widget/http_log.jst.eco +++ b/app/assets/javascripts/app/views/widget/http_log.jst.eco @@ -1,6 +1,6 @@
-<%- @T('Recent logs') %> +

<%- @T('Recent logs') %>

diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 51c705347..27dfe7ccd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base helper_method :current_user, :authentication_check, :config_frontend, + :http_log_config, :role?, :model_create_render, :model_update_render, @@ -18,7 +19,7 @@ class ApplicationController < ActionController::Base before_action :cors_preflight_check after_action :user_device_update, :set_access_control_headers - after_action :trigger_events + after_action :trigger_events, :http_log # For all responses in this controller, return the CORS access control headers. def set_access_control_headers @@ -47,6 +48,10 @@ class ApplicationController < ActionController::Base false end + def http_log_config(config) + @http_log_support = config + end + private # execute events @@ -98,6 +103,60 @@ class ApplicationController < ActionController::Base session[:user_agent] = request.env['HTTP_USER_AGENT'] end + # log http access + def http_log + return if !@http_log_support + + # request + request_data = { + content: '', + content_type: request.headers['Content-Type'], + content_encoding: request.headers['Content-Encoding'], + source: request.headers['User-Agent'] || request.headers['Server'], + } + request.headers.each {|key, value| + next if key[0, 5] != 'HTTP_' + request_data[:content] += if key == 'HTTP_COOKIE' + "#{key}: xxxxx\n" + else + "#{key}: #{value}\n" + end + } + body = request.body.read + if body + request_data[:content] += "\n" + body + end + request_data[:content] = request_data[:content].slice(0, 8000) + + # response + response_data = { + code: response.status = response.code, + content: '', + content_type: nil, + content_encoding: nil, + source: nil, + } + response.headers.each {|key, value| + response_data[:content] += "#{key}: #{value}\n" + } + body = response.body + if body + response_data[:content] += "\n" + body + end + response_data[:content] = response_data[:content].slice(0, 8000) + record = { + direction: 'in', + facility: @http_log_support[:facility], + url: url_for(only_path: false, overwrite_params: {}), + status: response.status, + ip: request.remote_ip, + request: request_data, + response: response_data, + method: request.method, + } + HttpLog.create(record) + end + # user device recent action update def user_device_update diff --git a/app/controllers/integration/sipgate_controller.rb b/app/controllers/integration/sipgate_controller.rb new file mode 100644 index 000000000..721240765 --- /dev/null +++ b/app/controllers/integration/sipgate_controller.rb @@ -0,0 +1,115 @@ +require 'builder' + +class Integration::SipgateController < ApplicationController + before_action { http_log_config facility: 'sipgate.io' } + + # notify about inbound call / block inbound call + def in + return if feature_disabled + + config = Setting.get('sipgate_config') + config_inbound = config[:inbound] + block_caller_ids = config_inbound[:block_caller_ids] + + if params['event'] == 'newCall' + + # 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 + xml.Reject('reason' => 'busy') + end + + send_data content, type: 'application/xml; charset=UTF-8;' + + params['Reject'] = 'busy' + Sessions.broadcast( + event: 'sipgate.io', + data: params + ) + return true + } + end + + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct! + content = xml.Response('onHangup' => in_url, 'onAnswer' => in_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 + return if feature_disabled + + config = Setting.get('sipgate_config') + config_outbound = config[:outbound][:routing_table] + default_caller_id = config[:outbound][:default_caller_id] + + xml = Builder::XmlMarkup.new(indent: 2) + xml.instruct! + + # set callerId + content = nil + to = params[:to] + 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]) } + end + break + } + if !content && default_caller_id + content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) do + xml.Dial(callerId: default_caller_id) { xml.Number(params[:to]) } + end + end + else + content = xml.Response('onHangup' => in_url, 'onAnswer' => in_url) + end + + send_data content, + type: 'application/xml; charset=UTF-8;' + + # notify about outbound call + Sessions.broadcast( + event: 'sipgate.io:out', + data: params + ) + end + + private + + def feature_disabled + if !Setting.get('sipgate_integration') + render( + json: {}, + status: :unauthorized + ) + return true + end + false + 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" + end +end diff --git a/config/routes/integration_sipgate.rb b/config/routes/integration_sipgate.rb new file mode 100644 index 000000000..04d4d8f91 --- /dev/null +++ b/config/routes/integration_sipgate.rb @@ -0,0 +1,6 @@ +Zammad::Application.routes.draw do + + match '/api/v1/sipgate/in', to: 'integration/sipgate#in', via: :post + match '/api/v1/sipgate/out', to: 'integration/sipgate#out', via: :post + +end diff --git a/db/migrate/20160421000001_add_sipgate_integration.rb b/db/migrate/20160421000001_add_sipgate_integration.rb new file mode 100644 index 000000000..2a046670a --- /dev/null +++ b/db/migrate/20160421000001_add_sipgate_integration.rb @@ -0,0 +1,37 @@ +class AddSipgateIntegration < ActiveRecord::Migration + def up + Setting.create_if_not_exists( + title: 'sipgate.io integration', + name: 'sipgate_integration', + area: 'Integration::Switch', + description: 'Define if sipgate.io (http://www.sipgate.io) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'sipgate_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { prio: 1 }, + frontend: false + ) + Setting.create_if_not_exists( + title: 'sipgate.io config', + name: 'sipgate_config', + area: 'Integration::Sipgate', + description: 'Define the sipgate.io config.', + options: {}, + state: {}, + frontend: false, + preferences: { prio: 2 }, + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index b0b2556e9..9f29ba6d5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1822,6 +1822,39 @@ Setting.create_if_not_exists( frontend: false, preferences: { prio: 2 }, ) +Setting.create_if_not_exists( + title: 'sipgate.io integration', + name: 'sipgate_integration', + area: 'Integration::Switch', + description: 'Define if sipgate.io (http://www.sipgate.io) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'sipgate_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { prio: 1 }, + frontend: false +) +Setting.create_if_not_exists( + title: 'sipgate.io config', + name: 'sipgate_config', + area: 'Integration::Sipgate', + description: 'Define the sipgate.io config.', + options: {}, + state: {}, + frontend: false, + preferences: { prio: 2 }, +) signature = Signature.create_if_not_exists( id: 1, diff --git a/test/integration/sipgate_controller_test.rb b/test/integration/sipgate_controller_test.rb new file mode 100644 index 000000000..cb102e066 --- /dev/null +++ b/test/integration/sipgate_controller_test.rb @@ -0,0 +1,184 @@ +# encoding: utf-8 +require 'test_helper' +require 'rexml/document' + +class SipgateControllerTest < ActionDispatch::IntegrationTest + setup do + + Setting.create_or_update( + title: 'sipgate.io integration', + name: 'sipgate_integration', + area: 'Integration::Switch', + description: 'Define if sipgate.io (http://www.sipgate.io) is enabled or not.', + options: { + form: [ + { + display: '', + null: true, + name: 'sipgate_integration', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { prio: 1 }, + frontend: false + ) + Setting.create_or_update( + title: 'sipgate.io config', + name: 'sipgate_config', + area: 'Integration::Sipgate', + description: 'Define the sipgate.io config.', + options: {}, + state: { + outbound: { + routing_table: [ + { + dest: '41*', + caller_id: '41715880339000', + }, + { + dest: '491714000000', + caller_id: '41715880339000', + }, + ], + default_caller_id: '4930777000000', + }, + inbound: { + block_caller_ids: [ + { + caller_id: '491715000000', + note: 'some note', + } + ], + notify_user_ids: { + 2 => true, + 4 => false, + }, + } + }, + frontend: false, + preferences: { prio: 2 }, + ) + + 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' + post '/api/v1/sipgate/in', params + assert_response(200) + on_hangup = nil + on_answer = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) + 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' + post '/api/v1/sipgate/in', params + assert_response(200) + on_hangup = nil + on_answer = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_hangup) + assert_equal('http://zammad.example.com/api/v1/sipgate/in', on_answer) + reason = nil + response.elements.each('Response/Reject') do |element| + reason = element.attributes['reason'] + end + 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' + post '/api/v1/sipgate/out', params + assert_response(200) + on_hangup = nil + on_answer = nil + caller_id = nil + number_to_dail = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + response.elements.each('Response/Dial') do |element| + caller_id = element.attributes['callerId'] + end + response.elements.each('Response/Dial/Number') do |element| + number_to_dail = element.text + 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) + + # 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' + post '/api/v1/sipgate/out', params + assert_response(200) + on_hangup = nil + on_answer = nil + caller_id = nil + number_to_dail = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + response.elements.each('Response/Dial') do |element| + caller_id = element.attributes['callerId'] + end + response.elements.each('Response/Dial/Number') do |element| + number_to_dail = element.text + 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) + + # 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' + post '/api/v1/sipgate/out', params + assert_response(200) + on_hangup = nil + on_answer = nil + caller_id = nil + number_to_dail = nil + content = @response.body + response = REXML::Document.new(content) + response.elements.each('Response') do |element| + on_hangup = element.attributes['onHangup'] + on_answer = element.attributes['onAnswer'] + end + response.elements.each('Response/Dial') do |element| + caller_id = element.attributes['callerId'] + end + response.elements.each('Response/Dial/Number') do |element| + number_to_dail = element.text + 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) + + end + +end