123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- module Cti
- class Log < ApplicationModel
- include HasSearchIndexBackend
- self.table_name = 'cti_logs'
- store :preferences, accessors: %i[from_pretty to_pretty]
- validates :state, format: { with: %r{\A(newCall|answer|hangup)\z}, message: 'newCall|answer|hangup is allowed' }
- before_create :set_pretty
- before_update :set_pretty
- after_commit :push_caller_list_update
- =begin
- Cti::Log.create!(
- direction: 'in',
- from: '007',
- from_comment: '',
- to: '008',
- to_comment: 'BBB',
- call_id: '1',
- comment: '',
- state: 'newCall',
- done: true,
- )
- Cti::Log.create!(
- direction: 'in',
- from: '007',
- from_comment: '',
- to: '008',
- to_comment: '',
- call_id: '2',
- comment: '',
- state: 'answer',
- done: true,
- )
- Cti::Log.create!(
- direction: 'in',
- from: '009',
- from_comment: '',
- to: '010',
- to_comment: '',
- call_id: '3',
- comment: '',
- state: 'hangup',
- done: true,
- )
- example data, can be used for demo
- Cti::Log.create!(
- direction: 'in',
- from: '4930609854180',
- from_comment: 'Franz Bauer',
- to: '4930609811111',
- to_comment: 'Bob Smith',
- call_id: '435452113',
- comment: '',
- state: 'newCall',
- done: false,
- preferences: {
- from: [
- {
- caller_id: '4930726128135',
- comment: nil,
- level: 'known',
- object: 'User',
- o_id: 2,
- user_id: 2,
- },
- {
- caller_id: '4930726128135',
- comment: nil,
- level: 'maybe',
- object: 'User',
- o_id: 2,
- user_id: 3,
- },
- ]
- },
- created_at: Time.zone.now,
- )
- Cti::Log.create!(
- direction: 'out',
- from: '4930609854180',
- from_comment: 'Franz Bauer',
- to: '4930609811111',
- to_comment: 'Bob Smith',
- call_id: SecureRandom.uuid,
- comment: '',
- state: 'newCall',
- done: true,
- preferences: {
- to: [
- {
- caller_id: '4930726128135',
- comment: nil,
- level: 'known',
- object: 'User',
- o_id: 2,
- user_id: 2,
- }
- ]
- },
- created_at: Time.zone.now - 20.seconds,
- )
- Cti::Log.create!(
- direction: 'in',
- from: '4930609854180',
- from_comment: 'Franz Bauer',
- to: '4930609811111',
- to_comment: 'Bob Smith',
- call_id: SecureRandom.uuid,
- comment: '',
- state: 'answer',
- done: true,
- preferences: {
- from: [
- {
- caller_id: '4930726128135',
- comment: nil,
- level: 'known',
- object: 'User',
- o_id: 2,
- user_id: 2,
- }
- ]
- },
- initialized_at: Time.zone.now - 20.seconds,
- start_at: Time.zone.now - 30.seconds,
- duration_waiting_time: 20,
- created_at: Time.zone.now - 20.seconds,
- )
- Cti::Log.create!(
- direction: 'in',
- from: '4930609854180',
- from_comment: 'Franz Bauer',
- to: '4930609811111',
- to_comment: 'Bob Smith',
- call_id: SecureRandom.uuid,
- comment: '',
- state: 'hangup',
- comment: 'normalClearing',
- done: false,
- preferences: {
- from: [
- {
- caller_id: '4930726128135',
- comment: nil,
- level: 'known',
- object: 'User',
- o_id: 2,
- user_id: 2,
- }
- ]
- },
- initialized_at: Time.zone.now - 80.seconds,
- start_at: Time.zone.now - 45.seconds,
- end_at: Time.zone.now,
- duration_waiting_time: 35,
- duration_talking_time: 45,
- created_at: Time.zone.now - 80.seconds,
- )
- Cti::Log.create!(
- direction: 'in',
- from: '4930609854180',
- from_comment: 'Franz Bauer',
- to: '4930609811111',
- to_comment: 'Bob Smith',
- call_id: SecureRandom.uuid,
- comment: '',
- state: 'hangup',
- done: true,
- start_at: Time.zone.now - 15.seconds,
- end_at: Time.zone.now,
- preferences: {
- from: [
- {
- caller_id: '4930726128135',
- comment: nil,
- level: 'known',
- object: 'User',
- o_id: 2,
- user_id: 2,
- }
- ]
- },
- initialized_at: Time.zone.now - 5.minutes,
- start_at: Time.zone.now - 3.minutes,
- end_at: Time.zone.now - 20.seconds,
- duration_waiting_time: 120,
- duration_talking_time: 160,
- created_at: Time.zone.now - 5.minutes,
- )
- Cti::Log.create!(
- direction: 'in',
- from: '4930609854180',
- from_comment: 'Franz Bauer',
- to: '4930609811111',
- to_comment: '',
- call_id: SecureRandom.uuid,
- comment: '',
- state: 'hangup',
- done: true,
- start_at: Time.zone.now - 15.seconds,
- end_at: Time.zone.now,
- preferences: {
- from: [
- {
- caller_id: '4930726128135',
- comment: nil,
- level: 'known',
- object: 'User',
- o_id: 2,
- user_id: 2,
- }
- ]
- },
- initialized_at: Time.zone.now - 60.minutes,
- start_at: Time.zone.now - 59.minutes,
- end_at: Time.zone.now - 2.minutes,
- duration_waiting_time: 60,
- duration_talking_time: 3420,
- created_at: Time.zone.now - 60.minutes,
- )
- Cti::Log.create!(
- direction: 'in',
- from: '4930609854180',
- from_comment: 'Franz Bauer',
- to: '4930609811111',
- to_comment: 'Bob Smith',
- call_id: SecureRandom.uuid,
- comment: '',
- state: 'hangup',
- done: true,
- start_at: Time.zone.now - 15.seconds,
- end_at: Time.zone.now,
- preferences: {
- from: [
- {
- caller_id: '4930726128135',
- comment: nil,
- level: 'maybe',
- object: 'User',
- o_id: 2,
- user_id: 2,
- }
- ]
- },
- initialized_at: Time.zone.now - 240.minutes,
- start_at: Time.zone.now - 235.minutes,
- end_at: Time.zone.now - 222.minutes,
- duration_waiting_time: 300,
- duration_talking_time: 1080,
- created_at: Time.zone.now - 240.minutes,
- )
- Cti::Log.create!(
- direction: 'in',
- from: '4930609854180',
- to: '4930609811112',
- call_id: SecureRandom.uuid,
- comment: '',
- state: 'hangup',
- done: true,
- start_at: Time.zone.now - 20.seconds,
- end_at: Time.zone.now,
- preferences: {},
- initialized_at: Time.zone.now - 1440.minutes,
- start_at: Time.zone.now - 1430.minutes,
- end_at: Time.zone.now - 1429.minutes,
- duration_waiting_time: 600,
- duration_talking_time: 660,
- created_at: Time.zone.now - 1440.minutes,
- )
- =end
- =begin
- Cti::Log.log(current_user)
- returns
- {
- list: [log_record1, log_record2, log_record3],
- assets: {...},
- }
- =end
- def self.log(current_user)
- list = Cti::Log.log_records(current_user)
- # add assets
- assets = list.map(&:preferences)
- .map { |p| p.slice(:from, :to) }
- .map(&:values).flatten
- .pluck(:user_id).compact
- .filter_map { |user_id| User.lookup(id: user_id) }
- .each.with_object({}) { |user, a| user.assets(a) }
- {
- list: list,
- assets: assets,
- }
- end
- =begin
- Cti::Log.log_records(current_user)
- returns
- [log_record1, log_record2, log_record3]
- =end
- def self.log_records(current_user)
- cti_config = Setting.get('cti_config')
- if cti_config[:notify_map].present?
- return Cti::Log.where(queue: queues_of_user(current_user, cti_config)).reorder(created_at: :desc).limit(view_limit)
- end
- Cti::Log.reorder(created_at: :desc).limit(view_limit)
- end
- =begin
- processes a incoming event
- Cti::Log.process(
- cause: '',
- event: 'newCall',
- user: 'user 1',
- from: '4912347114711',
- to: '4930600000000',
- callId: '43545211', # or call_id
- direction: 'in',
- queue: 'helpdesk', # optional
- )
- =end
- def self.process(params)
- cause = params['cause']
- event = params['event']
- user = params['user']
- queue = params['queue']
- call_id = params['callId'] || params['call_id']
- if user.instance_of?(Array)
- user = user.join(', ')
- end
- from_comment = nil
- to_comment = nil
- preferences = nil
- done = true
- if params['direction'] == 'in'
- if user.present?
- to_comment = user
- elsif queue.present?
- to_comment = queue
- end
- from_comment, preferences = CallerId.get_comment_preferences(params['from'], 'from')
- if queue.blank?
- queue = params['to']
- end
- else
- from_comment = user
- to_comment, preferences = CallerId.get_comment_preferences(params['to'], 'to')
- if queue.blank?
- queue = params['from']
- end
- end
- log = find_by(call_id: call_id)
- case event
- when 'newCall'
- if params['direction'] == 'in'
- done = false
- end
- raise "call_id #{call_id} already exists!" if log
- log = create(
- direction: params['direction'],
- from: params['from'],
- from_comment: from_comment,
- to: params['to'],
- to_comment: to_comment,
- call_id: call_id,
- comment: cause,
- queue: queue,
- state: event,
- initialized_at: Time.zone.now,
- preferences: preferences,
- done: done,
- )
- when 'answer'
- raise "No such call_id #{call_id}" if !log
- return if log.state == 'hangup' # call is already hangup, ignore answer
- log.with_lock do
- log.state = 'answer'
- log.start_at = Time.zone.now
- log.duration_waiting_time = log.start_at.to_i - log.initialized_at.to_i
- if user
- log.to_comment = user
- end
- log.done = true
- log.comment = cause
- log.save
- end
- when 'hangup'
- raise "No such call_id #{call_id}" if !log
- log.with_lock do
- log.done = done
- if params['direction'] == 'in'
- if (log.state == 'newCall' && cause != 'forwarded') || log.to_comment == 'voicemail' # rubocop:disable Style/SoleNestedConditional
- log.done = false
- end
- end
- log.state = 'hangup'
- log.end_at = Time.zone.now
- if log.start_at
- log.duration_talking_time = log.end_at.to_i - log.start_at.to_i
- elsif !log.duration_waiting_time && log.initialized_at
- log.duration_waiting_time = log.end_at.to_i - log.initialized_at.to_i
- end
- log.comment = cause
- log.save
- end
- else
- raise ArgumentError, "Unknown event #{event.inspect}"
- end
- log
- end
- def self.push_caller_list_update?(record)
- list_ids = Cti::Log.reorder(created_at: :desc).limit(view_limit).pluck(:id)
- return true if list_ids.include?(record.id)
- false
- end
- def push_caller_list_update
- return false if !Cti::Log.push_caller_list_update?(self)
- # send notify on create/update/delete
- users = User.with_permissions('cti.agent')
- users.each do |user|
- Sessions.send_to(
- user.id,
- {
- event: 'cti_list_push',
- },
- )
- end
- true
- end
- =begin
- cleanup caller logs
- Cti::Log.cleanup
- optional you can put the max oldest chat entries as argument
- Cti::Log.cleanup(12.months)
- =end
- def self.cleanup(diff = 12.months)
- where(created_at: ...diff.ago)
- .delete_all
- true
- end
- # adds virtual attributes when rendering #to_json
- # see http://api.rubyonrails.org/classes/ActiveModel/Serialization.html
- def attributes
- if !from_pretty || !to_pretty
- set_pretty
- end
- virtual_attributes = {
- 'from_pretty' => from_pretty,
- 'to_pretty' => to_pretty,
- }
- super.merge(virtual_attributes)
- end
- def attribute_names_for_serialization
- super + %w[from_pretty to_pretty]
- end
- def set_pretty
- %i[from to].each do |field|
- parsed = TelephoneNumber.parse(send(field)&.sub(%r{^\+?}, '+'))
- preferences[:"#{field}_pretty"] = parsed.send(parsed.valid? ? :international_number : :original_number)
- end
- end
- =begin
- returns queues of user
- ['queue1', 'queue2'] = Cti::Log.queues_of_user(User.find(123), config)
- =end
- def self.queues_of_user(user, config)
- queues = []
- config[:notify_map]&.each do |row|
- next if row[:user_ids].blank?
- next if row[:user_ids].exclude?(user.id.to_s) && row[:user_ids].exclude?(user.id)
- queues.push row[:queue]
- end
- if user.phone.present?
- caller_ids = Cti::CallerId.extract_numbers(user.phone)
- queues.concat(caller_ids)
- end
- queues
- end
- =begin
- return best customer id of caller log
- log = Cti::Log.find(123)
- customer_id = log.best_customer_id_of_log_entry
- =end
- def best_customer_id_of_log_entry
- customer_id = nil
- if preferences[:from].present?
- preferences[:from].each do |entry|
- if customer_id.blank?
- customer_id = entry[:user_id]
- end
- next if entry[:level] != 'known'
- customer_id = entry[:user_id]
- break
- end
- end
- customer_id
- end
- def self.view_limit
- Hash(Setting.get('cti_config'))['view_limit'] || 60
- end
- end
- end
|