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 @@
+
\ 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