zammad/zammad

View on GitHub
app/models/cti/caller_id.rb

Summary

Maintainability
C
1 day
Test Coverage
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

module Cti
  class CallerId < ApplicationModel
    self.table_name = 'cti_caller_ids'

    DEFAULT_COUNTRY_ID = '49'.freeze

    # adopt/orphan matching Cti::Log records
    # (see https://github.com/zammad/zammad/issues/2057)
    after_commit :update_cti_logs, on: :destroy, unless: -> { BulkImportInfo.enabled? }
    after_commit :update_cti_logs_with_fg_optimization, on: :create, unless: -> { BulkImportInfo.enabled? }

=begin

  Cti::CallerId.maybe_add(
    caller_id: '49123456789',
    comment: 'Hairdresser Bob Smith, San Francisco', #optional
    level: 'maybe', # known|maybe
    user_id: 1, # optional
    object: 'Ticket',
    o_id: 123,
  )

=end

    def self.maybe_add(data)
      record = find_or_initialize_by(
        caller_id: data[:caller_id],
        level:     data[:level],
        object:    data[:object],
        o_id:      data[:o_id],
        user_id:   data[:user_id],
      )

      return record if !record.new_record?

      record.comment = data[:comment]
      record.save!
    end

=begin

get items (users) for a certain caller ID

  caller_id_records = Cti::CallerId.lookup('49123456789')

returns

 [record1, record2, ...]

=end

    def self.lookup(caller_id)
      lookup_ids =
        ['known', 'maybe', nil].lazy.map do |level|
          Cti::CallerId.select('MAX(id) as caller_id')
                       .where({ caller_id: caller_id, level: level }.compact)
                       .group(:user_id)
                       .reorder(Arel.sql('caller_id DESC')) # not used as `caller_id: :desc` because is needed for `as caller_id`
                       .limit(20)
                       .map(&:caller_id)
        end.find(&:present?)

      Cti::CallerId.where(id: lookup_ids).reorder(id: :desc).to_a
    end

=begin

  Cti::CallerId.build(ticket)

=end

    def self.build(record)
      map = config
      level = nil
      model = nil
      map.each do |item|
        next if item[:model] != record.class

        level = item[:level]
        model = item[:model]
      end
      return if !level || !model

      build_item(record, model, level)
    end

=begin

  Cti::CallerId.build_item(record, model, level)

=end

    def self.build_item(record, model, level)

      # use first customer article
      if model == Ticket
        article = record.articles.first
        return if !article
        return if article.sender.name != 'Customer'

        record = article
      end

      # set user id
      user_id = record[:created_by_id]
      if model == User
        if record.destroyed?
          Cti::CallerId.where(user_id: user_id).destroy_all
          return
        end
        user_id = record.id
      end
      return if !user_id

      # get caller IDs
      caller_ids = []
      attributes = record.attributes
      attributes.each_value do |value|
        next if value.class != String
        next if value.blank?

        local_caller_ids = Cti::CallerId.extract_numbers(value)
        next if local_caller_ids.blank?

        caller_ids.concat(local_caller_ids)
      end

      # search for caller IDs to keep
      caller_ids_to_add = []
      existing_record_ids = Cti::CallerId.where(object: model.to_s, o_id: record.id).pluck(:id)
      caller_ids.uniq.each do |caller_id|
        existing_record_id = Cti::CallerId.where(
          object:    model.to_s,
          o_id:      record.id,
          caller_id: caller_id,
          level:     level,
          user_id:   user_id,
        ).pluck(:id)
        if existing_record_id[0]
          existing_record_ids.delete(existing_record_id[0])
          next
        end
        caller_ids_to_add.push caller_id
      end

      # delete not longer existing caller IDs
      existing_record_ids.each do |record_id|
        Cti::CallerId.destroy(record_id)
      end

      # create new caller IDs
      caller_ids_to_add.each do |caller_id|
        Cti::CallerId.maybe_add(
          caller_id: caller_id,
          level:     level,
          object:    model.to_s,
          o_id:      record.id,
          user_id:   user_id,
        )
      end
      true
    end

=begin

  Cti::CallerId.rebuild

=end

    def self.rebuild
      transaction do
        delete_all
        config.each do |item|
          level = item[:level]
          model = item[:model]
          item[:model].find_each(batch_size: 500) do |record|
            build_item(record, model, level)
          end
        end
      end
    end

=begin

  Cti::CallerId.config

returns

  [
    {
      model: User,
      level: 'known',
    },
    {
      model: Ticket,
      level: 'maybe',
    },
  ]

=end

    def self.config
      [
        {
          model: User,
          level: 'known',
        },
        {
          model: Ticket,
          level: 'maybe',
        },
      ]
    end

=begin

  caller_ids = Cti::CallerId.extract_numbers('...')

returns

  ['49123456789', '49987654321']

=end

    def self.extract_numbers(text)
      # see specs for example
      return [] if !text.is_a?(String)

      text.scan(%r{([\d\s\-(|)]{6,26})}).map do |match|
        normalize_number(match[0])
      end
    end

    def self.normalize_number(number)
      number = number.gsub(%r{[\s-]}, '')
      number.gsub!(%r{^(00)?(\+?\d\d)\(0?(\d*)\)}, '\\1\\2\\3')
      number.gsub!(%r{\D}, '')
      case number
      when %r{^00}
        number[2..]
      when %r{^0}
        DEFAULT_COUNTRY_ID + number[1..]
      else
        number
      end
    end

=begin

  from_comment, preferences = Cti::CallerId.get_comment_preferences('00491710000000', 'from')

  returns

  [
    "Bob Smith",
    {
      "from"=>[
        {
          "id"=>1961634,
          "caller_id"=>"491710000000",
          "comment"=>nil,
          "level"=>"known",
          "object"=>"User",
          "o_id"=>3,
          "user_id"=>3,
          "preferences"=>nil,
          "created_at"=>Mon, 24 Sep 2018 15:19:48 UTC +00:00,
          "updated_at"=>Mon, 24 Sep 2018 15:19:48 UTC +00:00,
        }
      ]
    }
  ]

=end

    def self.get_comment_preferences(caller_id, direction)
      from_comment_known = ''
      from_comment_maybe = ''
      preferences_known = {}
      preferences_known[direction] = []
      preferences_maybe = {}
      preferences_maybe[direction] = []

      lookup(extract_numbers(caller_id)).each do |record|
        if record.level == 'known'
          preferences_known[direction].push record.attributes
        else
          preferences_maybe[direction].push record.attributes
        end
        comment = ''
        if record.user_id
          user = User.lookup(id: record.user_id)
          if user
            comment += user.fullname
          end
        elsif record.comment.present?
          comment += record.comment
        end
        if record.level == 'known'
          if from_comment_known.present?
            from_comment_known += ','
          end
          from_comment_known += comment
        else
          if from_comment_maybe.present?
            from_comment_maybe += ','
          end
          from_comment_maybe += comment
        end
      end
      return [from_comment_known, preferences_known] if from_comment_known.present?
      return ["maybe #{from_comment_maybe}", preferences_maybe] if from_comment_maybe.present?

      nil
    end

=begin

return users by caller_id

  [user1, user2] = Cti::CallerId.known_agents_by_number('491234567')

=end

    def self.known_agents_by_number(number)
      users = []
      caller_ids = Cti::CallerId.extract_numbers(number)
      caller_id_records = Cti::CallerId.lookup(caller_ids)
      caller_id_records.each do |caller_id_record|
        next if caller_id_record.level != 'known'

        user = User.find_by(id: caller_id_record.user_id)
        next if !user
        next if !user.permissions?('cti.agent')

        users.push user
      end
      users
    end

    def update_cti_logs
      return if object != 'User'

      UpdateCtiLogsByCallerJob.perform_later(caller_id)
    end

    def update_cti_logs_with_fg_optimization
      return if Setting.get('import_mode')
      return if object != 'User'
      return if level != 'known'

      UpdateCtiLogsByCallerJob.perform_now(caller_id, limit: 20)
      UpdateCtiLogsByCallerJob.perform_later(caller_id, limit: 40, offset: 20)
    end
  end
end