Browse Source

Implemented issue #2044 - Generic CTI integration.

Martin Edenhofer 6 years ago
parent
commit
29a0336809

+ 158 - 0
app/assets/javascripts/app/controllers/_integration/cti.coffee

@@ -0,0 +1,158 @@
+class Index extends App.ControllerIntegrationBase
+  featureIntegration: 'cti_integration'
+  featureName: 'CTI (generic)'
+  featureConfig: 'cti_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.']
+  ]
+  events:
+    'click .js-select': 'selectAll'
+    'change .js-switch input': 'switch'
+
+  render: =>
+    super
+    new Form(
+      el: @$('.js-form')
+    )
+
+    new App.HttpLog(
+      el: @$('.js-log')
+      facility: 'cti'
+    )
+
+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
+    @render()
+
+  currentConfig: ->
+    config = App.Setting.get('cti_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('cti_config', value, {notify: true})
+
+  render: =>
+    @config = @currentConfig()
+
+    @html App.view('integration/cti')(
+      config: @config
+      cti_token: App.Setting.get('cti_token')
+    )
+
+  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()
+    return if _.isEmpty(caller_id) || _.isEmpty(note)
+    @config.inbound.block_caller_ids.push {
+      caller_id: @cleanupInput(caller_id)
+      note: note
+    }
+    @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()
+    return if _.isEmpty(caller_id) || _.isEmpty(dest) || _.isEmpty(note)
+    @config.outbound.routing_table.push {
+      dest: dest
+      caller_id: caller_id
+      note: note
+    }
+    @render()
+
+  removeInboundBlockCallerId: (e) =>
+    e.preventDefault()
+    @updateCurrentConfig()
+    element = $(e.currentTarget).closest('tr')
+    element.remove()
+    @updateCurrentConfig()
+
+  removeOutboundRouting: (e) =>
+    e.preventDefault()
+    @updateCurrentConfig()
+    element = $(e.currentTarget).closest('tr')
+    element.remove()
+    @updateCurrentConfig()
+
+class State
+  @current: ->
+    App.Setting.get('cti_integration')
+
+App.Config.set(
+  'IntegrationCti'
+  {
+    name: 'CTI (generic)'
+    target: '#system/integration/cti'
+    description: 'Generic API to integrate VoIP service provider with realtime push.'
+    controller: Index
+    state: State
+  }
+  'NavBarIntegrations'
+)

+ 1 - 0
app/assets/javascripts/app/controllers/cti.coffee

@@ -112,6 +112,7 @@ class App.CTI extends App.Controller
 
   featureActive: =>
     return true if @Config.get('sipgate_integration')
+    return true if @Config.get('cti_integration')
     false
 
   render: ->

+ 97 - 0
app/assets/javascripts/app/views/integration/cti.jst.eco

@@ -0,0 +1,97 @@
+<form>
+
+  <h2><%- @T('Settings') %></h2>
+
+  <p><%- @T('You need to configure the Zammad endpoints in the %s', 'PBX') %>:<p>
+
+  <div class="settings-entry">
+    <table class="settings-list" style="width: 100%;">
+      <thead>
+        <tr>
+          <th width="20%"><%- @T('Type') %>
+          <th width="80%"><%- @T('URL') %>
+      </thead>
+      <tbody>
+        <tr>
+          <td class="settings-list-row-control"><%- @T('Endpoint') %>
+          <td class="settings-list-control-cell"><input type="url" class="form-control form-control--small js-select" readonly value="<%- @C('http_type') %>://<%- @C('fqdn') %>/api/v1/cti/<%= @cti_token %>">
+      </tbody>
+    </table>
+  </div>
+
+  <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 js-submit"><%- @T('Save') %></button>
+</form>

+ 5 - 0
app/controllers/cti_controller.rb

@@ -6,6 +6,11 @@ class CtiController < ApplicationController
   # list current caller log
   def index
     backends = [
+      {
+        name: 'CTI (generic)',
+        enabled: Setting.get('cti_integration'),
+        url: '#system/integration/cti',
+      },
       {
         name: 'sipgate.io',
         enabled: Setting.get('sipgate_integration'),

+ 117 - 0
app/controllers/integration/cti_controller.rb

@@ -0,0 +1,117 @@
+# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
+
+class Integration::CtiController < ApplicationController
+  skip_before_action :verify_csrf_token
+  before_action :check_configured, :check_token
+
+  # notify about inbound call / block inbound call
+  def event
+    if params['direction'] == 'in'
+      if params['event'] == 'newCall'
+        config_inbound = config_integration[:inbound] || {}
+        block_caller_ids = config_inbound[:block_caller_ids] || []
+
+        # check if call need to be blocked
+        block_caller_ids.each do |item|
+          next unless item[:caller_id] == params['from']
+
+          render json: { action: 'reject', reason: 'busy' }, status: :ok
+
+          #params['Reject'] = 'busy'
+          params['comment'] = 'reject, busy'
+          if params['user']
+            params['comment'] = "#{params['user']} -> reject, busy"
+          end
+          Cti::Log.process(params)
+          return true
+        end
+      end
+
+      Cti::Log.process(params)
+
+      render json: {}, status: :ok
+      return true
+    elsif params['direction'] == 'out'
+      config_outbound = config_integration[:outbound]
+      routing_table = nil
+      default_caller_id = nil
+      if config_outbound.present?
+        routing_table = config_outbound[:routing_table]
+        default_caller_id = config_outbound[:default_caller_id]
+      end
+
+      # set callerId
+      data    = {}
+      to      = params[:to]
+      from    = nil
+      if to && routing_table.present?
+        routing_table.each do |row|
+          dest = row[:dest].gsub(/\*/, '.+?')
+          next if to !~ /^#{dest}$/
+          from = row[:caller_id]
+          data = {
+            action: 'dial',
+            caller_id: from,
+            number: params[:to]
+          }
+          break
+        end
+        if data.blank? && default_caller_id.present?
+          from = default_caller_id
+          data = {
+            action: 'dial',
+            caller_id: default_caller_id,
+            number: params[:to]
+          }
+        end
+      end
+      render json: data, status: :ok
+
+      if from.present?
+        params['from'] = from
+      end
+      Cti::Log.process(params)
+      return true
+    end
+    render json: { error: 'Invalid direction!' }, status: :unprocessable_entity
+  end
+
+  private
+
+  def check_token
+    if Setting.get('cti_token') != params[:token]
+      response_unauthorized('Invalid token, please contact your admin!')
+      return
+    end
+
+    true
+  end
+
+  def check_configured
+    http_log_config facility: 'cti'
+
+    if !Setting.get('cti_integration')
+      response_error('Feature is disable, please contact your admin to enable it!')
+      return
+    end
+    if config_integration.blank? || config_integration[:inbound].blank? || config_integration[:outbound].blank?
+      response_error('Feature not configured, please contact your admin!')
+      return
+    end
+
+    true
+  end
+
+  def config_integration
+    @config_integration ||= Setting.get('cti_config')
+  end
+
+  def response_error(error)
+    render json: { error: error }, status: :unprocessable_entity
+  end
+
+  def response_unauthorized(error)
+    render json: { error: error }, status: :unauthorized
+  end
+
+end

+ 7 - 6
app/models/cti/log.rb

@@ -276,7 +276,7 @@ Cti::Log.process(
   'user' => 'user 1',
   'from' => '4912347114711',
   'to' => '4930600000000',
-  'callId' => '4991155921769858278-1',
+  'callId' => '4991155921769858278-1', # or call_id
   'direction' => 'in',
 )
 
@@ -286,6 +286,7 @@ Cti::Log.process(
       comment = params['cause']
       event   = params['event']
       user    = params['user']
+      call_id = params['callId'] || params['call_id']
       if user.class == Array
         user = user.join(', ')
       end
@@ -309,14 +310,14 @@ Cti::Log.process(
           from_comment: from_comment,
           to: params['to'],
           to_comment: to_comment,
-          call_id: params['callId'],
+          call_id: call_id,
           comment: comment,
           state: event,
           preferences: preferences,
         )
       when 'answer'
-        log = find_by(call_id: params['callId'])
-        raise "No such call_id #{params['callId']}" if !log
+        log = find_by(call_id: call_id)
+        raise "No such call_id #{call_id}" if !log
         log.state = 'answer'
         log.start = Time.zone.now
         if user
@@ -325,8 +326,8 @@ Cti::Log.process(
         log.comment = comment
         log.save
       when 'hangup'
-        log = find_by(call_id: params['callId'])
-        raise "No such call_id #{params['callId']}" if !log
+        log = find_by(call_id: call_id)
+        raise "No such call_id #{call_id}" if !log
         if params['direction'] == 'in' && log.state == 'newCall'
           log.done = false
         end

+ 5 - 0
config/routes/integration_cti.rb

@@ -0,0 +1,5 @@
+Zammad::Application.routes.draw do
+
+  match '/api/v1/cti/:token',     to: 'integration/cti#event',    via: :post
+
+end

+ 70 - 0
db/migrate/20180420000001_setting_cti.rb

@@ -0,0 +1,70 @@
+class SettingCti < ActiveRecord::Migration[5.1]
+  def up
+
+    # return if it's a new setup
+    return if !Setting.find_by(name: 'system_init_done')
+
+    Setting.create_if_not_exists(
+      title: 'cti integration',
+      name: 'cti_integration',
+      area: 'Integration::Switch',
+      description: 'Defines if generic CTI is enabled or not.',
+      options: {
+        form: [
+          {
+            display: '',
+            null: true,
+            name: 'cti_integration',
+            tag: 'boolean',
+            options: {
+              true  => 'yes',
+              false => 'no',
+            },
+          },
+        ],
+      },
+      state: false,
+      preferences: {
+        prio: 1,
+        trigger: ['menu:render', 'cti:reload'],
+        authentication: true,
+        permission: ['admin.integration'],
+      },
+      frontend: true
+    )
+    Setting.create_if_not_exists(
+      title: 'cti config',
+      name: 'cti_config',
+      area: 'Integration::Cti',
+      description: 'Defines the cti config.',
+      options: {},
+      state: { 'outbound' => { 'routing_table' => [], 'default_caller_id' => '' }, 'inbound' => { 'block_caller_ids' => [] } },
+      preferences: {
+        prio: 2,
+        permission: ['admin.integration'],
+      },
+      frontend: false,
+    )
+    Setting.create_if_not_exists(
+      title: 'CTI Token',
+      name: 'cti_token',
+      area: 'Integration::Cti',
+      description: 'Token for cti.',
+      options: {
+        form: [
+          {
+            display: '',
+            null: false,
+            name: 'cti_token',
+            tag: 'input',
+          },
+        ],
+      },
+      state: SecureRandom.urlsafe_base64(20),
+      preferences: {
+        permission: ['admin.integration'],
+      },
+      frontend: false
+    )
+  end
+end

+ 64 - 2
db/seeds/settings.rb

@@ -2485,7 +2485,7 @@ Setting.create_if_not_exists(
       },
     ],
   },
-  state: SecureRandom.urlsafe_base64(40),
+  state: ENV['MONITORING_TOKEN'] || SecureRandom.urlsafe_base64(40),
   preferences: {
     permission: ['admin.monitoring'],
   },
@@ -3377,7 +3377,7 @@ Setting.create_if_not_exists(
   area: 'Core',
   description: 'Defines the Check_MK token for allowing updates.',
   options: {},
-  state: SecureRandom.hex(16),
+  state: ENV['CHECK_MK_TOKEN'] || SecureRandom.hex(16),
   preferences: {
     permission: ['admin.integration'],
   },
@@ -3722,6 +3722,68 @@ Setting.create_if_not_exists(
   },
   frontend: false,
 )
+Setting.create_if_not_exists(
+  title: 'cti integration',
+  name: 'cti_integration',
+  area: 'Integration::Switch',
+  description: 'Defines if generic CTI is enabled or not.',
+  options: {
+    form: [
+      {
+        display: '',
+        null: true,
+        name: 'cti_integration',
+        tag: 'boolean',
+        options: {
+          true  => 'yes',
+          false => 'no',
+        },
+      },
+    ],
+  },
+  state: false,
+  preferences: {
+    prio: 1,
+    trigger: ['menu:render', 'cti:reload'],
+    authentication: true,
+    permission: ['admin.integration'],
+  },
+  frontend: true
+)
+Setting.create_if_not_exists(
+  title: 'cti config',
+  name: 'cti_config',
+  area: 'Integration::Cti',
+  description: 'Defines the cti config.',
+  options: {},
+  state: { 'outbound' => { 'routing_table' => [], 'default_caller_id' => '' }, 'inbound' => { 'block_caller_ids' => [] } },
+  preferences: {
+    prio: 2,
+    permission: ['admin.integration'],
+  },
+  frontend: false,
+)
+Setting.create_if_not_exists(
+  title: 'CTI Token',
+  name: 'cti_token',
+  area: 'Integration::Cti',
+  description: 'Token for cti.',
+  options: {
+    form: [
+      {
+        display: '',
+        null: false,
+        name: 'cti_token',
+        tag: 'input',
+      },
+    ],
+  },
+  state: ENV['CTI_TOKEN'] || SecureRandom.urlsafe_base64(20),
+  preferences: {
+    permission: ['admin.integration'],
+  },
+  frontend: false
+)
 Setting.create_if_not_exists(
   title: 'Clearbit integration',
   name: 'clearbit_integration',

+ 1 - 0
script/build/cleanup.sh

@@ -5,3 +5,4 @@ set -ex
 rm app/assets/javascripts/app/controllers/layout_ref.coffee
 rm -rf app/assets/javascripts/app/views/layout_ref/
 rm app/assets/javascripts/app/controllers/karma.coffee
+rm app/assets/javascripts/app/controllers/_integration/cti.coffee

Some files were not shown because too many files changed in this diff