Added sipgate integration.

This commit is contained in:
Martin Edenhofer 2016-04-21 09:36:25 +02:00
parent 588b4701a7
commit fcdf62cd13
10 changed files with 677 additions and 8 deletions

View file

@ -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'
)

View file

@ -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()

View file

@ -0,0 +1,78 @@
<form>
<h2><%- @T('Inbound') %></h2>
<p><%- @T('Blocked caller ids based on sender caller id.') %>
<div class="settings-entry">
<table class="settings-list js-inboundBlockCallerId" style="width: 100%;">
<thead>
<tr>
<th width="50%"><%- @T('Caller id to block') %>
<th width="40%"><%- @T('Note') %>
<th width="10%"><%- @T('Action') %>
</thead>
<tbody>
<% for row in @config.inbound.block_caller_ids: %>
<tr class="js-row">
<td class="settings-list-control-cell"><input name="caller_id" value="<%= row.caller_id %>" class="form-control form-control--small js-summary">
<td class="settings-list-control-cell"><input name="note" value="<%= row.note %>" class="form-control form-control--small js-summary">
<td class="settings-list-row-control"><div class="btn btn--text js-remove"><%- @Icon('trash') %> <%- @T('Remove') %></div>
<% end %>
<tr>
<td class="settings-list-control-cell"><input name="caller_id" value="" placeholder="4930609854189" class="form-control form-control--small js-summary">
<td class="settings-list-control-cell"><input name="note" value="" placeholder="<%- @Ti('my onw note') %>" class="form-control form-control--small js-summary">
<td class="settings-list-row-control"><div class="btn btn--text btn--create js-add"><%- @Icon('plus-small') %> <%- @T('Add') %></div>
</tbody>
</table>
</div>
<h2><%- @T('Outbound') %></h2>
<p><%- @T('Set caller id of outbound calls based on destination caller id.') %>
<div class="settings-entry js-outboundRouting">
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="30%"><%- @T('Destination caller id') %>
<th width="30%"><%- @T('Set outbound caller id') %>
<th width="30%"><%- @T('Note') %>
<th width="10%"><%- @T('Action') %>
</thead>
<tbody>
<% for row in @config.outbound.routing_table: %>
<tr class="js-row">
<td class="settings-list-control-cell"><input name="dest" value="<%= row.dest %>" class="form-control form-control--small js-summary">
<td class="settings-list-control-cell"><input name="caller_id" value="<%= row.caller_id %>" class="form-control form-control--small js-summary">
<td class="settings-list-control-cell"><input name="note" value="<%= row.note %>" class="form-control form-control--small js-summary">
<td class="settings-list-row-control"><div class="btn btn--text js-remove"><%- @Icon('trash') %> <%- @T('Remove') %></div>
<% end %>
<tr>
<td class="settings-list-control-cell"><input name="dest" value="" placeholder="49* or 3230123456789" class="form-control form-control--small js-summary">
<td class="settings-list-control-cell"><input name="caller_id" value="" placeholder="4930609854189" class="form-control form-control--small js-summary">
<td class="settings-list-control-cell"><input name="note" value="" placeholder="<%- @Ti('my onw note') %>" class="form-control form-control--small js-summary">
<td class="settings-list-row-control"><div class="btn btn--text btn--create js-add"><%- @Icon('plus-small') %> <%- @T('Add') %></div>
</tbody>
</table>
</div>
<p><%- @T('Default caller id.') %>
<div class="settings-entry">
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="50%"><%- @T('Default caller id') %>
<th width="50%"><%- @T('Note') %>
</thead>
<tbody>
<tr>
<td class="settings-list-control-cell"><input name="default_caller_id" value="<%= @config.outbound.default_caller_id %>" placeholder="4930609854189" class="form-control form-control--small js-summary">
<td class="settings-list-row-control"><%- @T('Default caller id for outbound calls.') %>
</tbody>
</table>
</div>
<button type="submit" class="btn btn--primary"><%- @T('Save') %></button>
</form>

View file

@ -1,6 +1,6 @@
<hr>
<%- @T('Recent logs') %>
<h2><%- @T('Recent logs') %></h2>
<div class="settings-entry">
<table class="settings-list" style="width: 100%;">
<thead>

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

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