# 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