log.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. module Cti
  3. class Log < ApplicationModel
  4. include HasSearchIndexBackend
  5. self.table_name = 'cti_logs'
  6. store :preferences, accessors: %i[from_pretty to_pretty]
  7. validates :state, format: { with: %r{\A(newCall|answer|hangup)\z},  message: 'newCall|answer|hangup is allowed' }
  8. before_create :set_pretty
  9. before_update :set_pretty
  10. after_commit :push_caller_list_update
  11. =begin
  12. Cti::Log.create!(
  13. direction: 'in',
  14. from: '007',
  15. from_comment: '',
  16. to: '008',
  17. to_comment: 'BBB',
  18. call_id: '1',
  19. comment: '',
  20. state: 'newCall',
  21. done: true,
  22. )
  23. Cti::Log.create!(
  24. direction: 'in',
  25. from: '007',
  26. from_comment: '',
  27. to: '008',
  28. to_comment: '',
  29. call_id: '2',
  30. comment: '',
  31. state: 'answer',
  32. done: true,
  33. )
  34. Cti::Log.create!(
  35. direction: 'in',
  36. from: '009',
  37. from_comment: '',
  38. to: '010',
  39. to_comment: '',
  40. call_id: '3',
  41. comment: '',
  42. state: 'hangup',
  43. done: true,
  44. )
  45. example data, can be used for demo
  46. Cti::Log.create!(
  47. direction: 'in',
  48. from: '4930609854180',
  49. from_comment: 'Franz Bauer',
  50. to: '4930609811111',
  51. to_comment: 'Bob Smith',
  52. call_id: '435452113',
  53. comment: '',
  54. state: 'newCall',
  55. done: false,
  56. preferences: {
  57. from: [
  58. {
  59. caller_id: '4930726128135',
  60. comment: nil,
  61. level: 'known',
  62. object: 'User',
  63. o_id: 2,
  64. user_id: 2,
  65. },
  66. {
  67. caller_id: '4930726128135',
  68. comment: nil,
  69. level: 'maybe',
  70. object: 'User',
  71. o_id: 2,
  72. user_id: 3,
  73. },
  74. ]
  75. },
  76. created_at: Time.zone.now,
  77. )
  78. Cti::Log.create!(
  79. direction: 'out',
  80. from: '4930609854180',
  81. from_comment: 'Franz Bauer',
  82. to: '4930609811111',
  83. to_comment: 'Bob Smith',
  84. call_id: SecureRandom.uuid,
  85. comment: '',
  86. state: 'newCall',
  87. done: true,
  88. preferences: {
  89. to: [
  90. {
  91. caller_id: '4930726128135',
  92. comment: nil,
  93. level: 'known',
  94. object: 'User',
  95. o_id: 2,
  96. user_id: 2,
  97. }
  98. ]
  99. },
  100. created_at: Time.zone.now - 20.seconds,
  101. )
  102. Cti::Log.create!(
  103. direction: 'in',
  104. from: '4930609854180',
  105. from_comment: 'Franz Bauer',
  106. to: '4930609811111',
  107. to_comment: 'Bob Smith',
  108. call_id: SecureRandom.uuid,
  109. comment: '',
  110. state: 'answer',
  111. done: true,
  112. preferences: {
  113. from: [
  114. {
  115. caller_id: '4930726128135',
  116. comment: nil,
  117. level: 'known',
  118. object: 'User',
  119. o_id: 2,
  120. user_id: 2,
  121. }
  122. ]
  123. },
  124. initialized_at: Time.zone.now - 20.seconds,
  125. start_at: Time.zone.now - 30.seconds,
  126. duration_waiting_time: 20,
  127. created_at: Time.zone.now - 20.seconds,
  128. )
  129. Cti::Log.create!(
  130. direction: 'in',
  131. from: '4930609854180',
  132. from_comment: 'Franz Bauer',
  133. to: '4930609811111',
  134. to_comment: 'Bob Smith',
  135. call_id: SecureRandom.uuid,
  136. comment: '',
  137. state: 'hangup',
  138. comment: 'normalClearing',
  139. done: false,
  140. preferences: {
  141. from: [
  142. {
  143. caller_id: '4930726128135',
  144. comment: nil,
  145. level: 'known',
  146. object: 'User',
  147. o_id: 2,
  148. user_id: 2,
  149. }
  150. ]
  151. },
  152. initialized_at: Time.zone.now - 80.seconds,
  153. start_at: Time.zone.now - 45.seconds,
  154. end_at: Time.zone.now,
  155. duration_waiting_time: 35,
  156. duration_talking_time: 45,
  157. created_at: Time.zone.now - 80.seconds,
  158. )
  159. Cti::Log.create!(
  160. direction: 'in',
  161. from: '4930609854180',
  162. from_comment: 'Franz Bauer',
  163. to: '4930609811111',
  164. to_comment: 'Bob Smith',
  165. call_id: SecureRandom.uuid,
  166. comment: '',
  167. state: 'hangup',
  168. done: true,
  169. start_at: Time.zone.now - 15.seconds,
  170. end_at: Time.zone.now,
  171. preferences: {
  172. from: [
  173. {
  174. caller_id: '4930726128135',
  175. comment: nil,
  176. level: 'known',
  177. object: 'User',
  178. o_id: 2,
  179. user_id: 2,
  180. }
  181. ]
  182. },
  183. initialized_at: Time.zone.now - 5.minutes,
  184. start_at: Time.zone.now - 3.minutes,
  185. end_at: Time.zone.now - 20.seconds,
  186. duration_waiting_time: 120,
  187. duration_talking_time: 160,
  188. created_at: Time.zone.now - 5.minutes,
  189. )
  190. Cti::Log.create!(
  191. direction: 'in',
  192. from: '4930609854180',
  193. from_comment: 'Franz Bauer',
  194. to: '4930609811111',
  195. to_comment: '',
  196. call_id: SecureRandom.uuid,
  197. comment: '',
  198. state: 'hangup',
  199. done: true,
  200. start_at: Time.zone.now - 15.seconds,
  201. end_at: Time.zone.now,
  202. preferences: {
  203. from: [
  204. {
  205. caller_id: '4930726128135',
  206. comment: nil,
  207. level: 'known',
  208. object: 'User',
  209. o_id: 2,
  210. user_id: 2,
  211. }
  212. ]
  213. },
  214. initialized_at: Time.zone.now - 60.minutes,
  215. start_at: Time.zone.now - 59.minutes,
  216. end_at: Time.zone.now - 2.minutes,
  217. duration_waiting_time: 60,
  218. duration_talking_time: 3420,
  219. created_at: Time.zone.now - 60.minutes,
  220. )
  221. Cti::Log.create!(
  222. direction: 'in',
  223. from: '4930609854180',
  224. from_comment: 'Franz Bauer',
  225. to: '4930609811111',
  226. to_comment: 'Bob Smith',
  227. call_id: SecureRandom.uuid,
  228. comment: '',
  229. state: 'hangup',
  230. done: true,
  231. start_at: Time.zone.now - 15.seconds,
  232. end_at: Time.zone.now,
  233. preferences: {
  234. from: [
  235. {
  236. caller_id: '4930726128135',
  237. comment: nil,
  238. level: 'maybe',
  239. object: 'User',
  240. o_id: 2,
  241. user_id: 2,
  242. }
  243. ]
  244. },
  245. initialized_at: Time.zone.now - 240.minutes,
  246. start_at: Time.zone.now - 235.minutes,
  247. end_at: Time.zone.now - 222.minutes,
  248. duration_waiting_time: 300,
  249. duration_talking_time: 1080,
  250. created_at: Time.zone.now - 240.minutes,
  251. )
  252. Cti::Log.create!(
  253. direction: 'in',
  254. from: '4930609854180',
  255. to: '4930609811112',
  256. call_id: SecureRandom.uuid,
  257. comment: '',
  258. state: 'hangup',
  259. done: true,
  260. start_at: Time.zone.now - 20.seconds,
  261. end_at: Time.zone.now,
  262. preferences: {},
  263. initialized_at: Time.zone.now - 1440.minutes,
  264. start_at: Time.zone.now - 1430.minutes,
  265. end_at: Time.zone.now - 1429.minutes,
  266. duration_waiting_time: 600,
  267. duration_talking_time: 660,
  268. created_at: Time.zone.now - 1440.minutes,
  269. )
  270. =end
  271. =begin
  272. Cti::Log.log(current_user)
  273. returns
  274. {
  275. list: [log_record1, log_record2, log_record3],
  276. assets: {...},
  277. }
  278. =end
  279. def self.log(current_user)
  280. list = Cti::Log.log_records(current_user)
  281. # add assets
  282. assets = list.map(&:preferences)
  283. .map { |p| p.slice(:from, :to) }
  284. .map(&:values).flatten
  285. .pluck(:user_id).compact
  286. .filter_map { |user_id| User.lookup(id: user_id) }
  287. .each.with_object({}) { |user, a| user.assets(a) }
  288. {
  289. list: list,
  290. assets: assets,
  291. }
  292. end
  293. =begin
  294. Cti::Log.log_records(current_user)
  295. returns
  296. [log_record1, log_record2, log_record3]
  297. =end
  298. def self.log_records(current_user)
  299. cti_config = Setting.get('cti_config')
  300. if cti_config[:notify_map].present?
  301. return Cti::Log.where(queue: queues_of_user(current_user, cti_config)).reorder(created_at: :desc).limit(view_limit)
  302. end
  303. Cti::Log.reorder(created_at: :desc).limit(view_limit)
  304. end
  305. =begin
  306. processes a incoming event
  307. Cti::Log.process(
  308. cause: '',
  309. event: 'newCall',
  310. user: 'user 1',
  311. from: '4912347114711',
  312. to: '4930600000000',
  313. callId: '43545211', # or call_id
  314. direction: 'in',
  315. queue: 'helpdesk', # optional
  316. )
  317. =end
  318. def self.process(params)
  319. cause = params['cause']
  320. event = params['event']
  321. user = params['user']
  322. queue = params['queue']
  323. call_id = params['callId'] || params['call_id']
  324. if user.instance_of?(Array)
  325. user = user.join(', ')
  326. end
  327. from_comment = nil
  328. to_comment = nil
  329. preferences = nil
  330. done = true
  331. if params['direction'] == 'in'
  332. if user.present?
  333. to_comment = user
  334. elsif queue.present?
  335. to_comment = queue
  336. end
  337. from_comment, preferences = CallerId.get_comment_preferences(params['from'], 'from')
  338. if queue.blank?
  339. queue = params['to']
  340. end
  341. else
  342. from_comment = user
  343. to_comment, preferences = CallerId.get_comment_preferences(params['to'], 'to')
  344. if queue.blank?
  345. queue = params['from']
  346. end
  347. end
  348. log = find_by(call_id: call_id)
  349. case event
  350. when 'newCall'
  351. if params['direction'] == 'in'
  352. done = false
  353. end
  354. raise "call_id #{call_id} already exists!" if log
  355. log = create(
  356. direction: params['direction'],
  357. from: params['from'],
  358. from_comment: from_comment,
  359. to: params['to'],
  360. to_comment: to_comment,
  361. call_id: call_id,
  362. comment: cause,
  363. queue: queue,
  364. state: event,
  365. initialized_at: Time.zone.now,
  366. preferences: preferences,
  367. done: done,
  368. )
  369. when 'answer'
  370. raise "No such call_id #{call_id}" if !log
  371. return if log.state == 'hangup' # call is already hangup, ignore answer
  372. log.with_lock do
  373. log.state = 'answer'
  374. log.start_at = Time.zone.now
  375. log.duration_waiting_time = log.start_at.to_i - log.initialized_at.to_i
  376. if user
  377. log.to_comment = user
  378. end
  379. log.done = true
  380. log.comment = cause
  381. log.save
  382. end
  383. when 'hangup'
  384. raise "No such call_id #{call_id}" if !log
  385. log.with_lock do
  386. log.done = done
  387. if params['direction'] == 'in'
  388. if (log.state == 'newCall' && cause != 'forwarded') || log.to_comment == 'voicemail' # rubocop:disable Style/SoleNestedConditional
  389. log.done = false
  390. end
  391. end
  392. log.state = 'hangup'
  393. log.end_at = Time.zone.now
  394. if log.start_at
  395. log.duration_talking_time = log.end_at.to_i - log.start_at.to_i
  396. elsif !log.duration_waiting_time && log.initialized_at
  397. log.duration_waiting_time = log.end_at.to_i - log.initialized_at.to_i
  398. end
  399. log.comment = cause
  400. log.save
  401. end
  402. else
  403. raise ArgumentError, "Unknown event #{event.inspect}"
  404. end
  405. log
  406. end
  407. def self.push_caller_list_update?(record)
  408. list_ids = Cti::Log.reorder(created_at: :desc).limit(view_limit).pluck(:id)
  409. return true if list_ids.include?(record.id)
  410. false
  411. end
  412. def push_caller_list_update
  413. return false if !Cti::Log.push_caller_list_update?(self)
  414. # send notify on create/update/delete
  415. users = User.with_permissions('cti.agent')
  416. users.each do |user|
  417. Sessions.send_to(
  418. user.id,
  419. {
  420. event: 'cti_list_push',
  421. },
  422. )
  423. end
  424. true
  425. end
  426. =begin
  427. cleanup caller logs
  428. Cti::Log.cleanup
  429. optional you can put the max oldest chat entries as argument
  430. Cti::Log.cleanup(12.months)
  431. =end
  432. def self.cleanup(diff = 12.months)
  433. where(created_at: ...diff.ago)
  434. .delete_all
  435. true
  436. end
  437. # adds virtual attributes when rendering #to_json
  438. # see http://api.rubyonrails.org/classes/ActiveModel/Serialization.html
  439. def attributes
  440. if !from_pretty || !to_pretty
  441. set_pretty
  442. end
  443. virtual_attributes = {
  444. 'from_pretty' => from_pretty,
  445. 'to_pretty' => to_pretty,
  446. }
  447. super.merge(virtual_attributes)
  448. end
  449. def attribute_names_for_serialization
  450. super + %w[from_pretty to_pretty]
  451. end
  452. def set_pretty
  453. %i[from to].each do |field|
  454. parsed = TelephoneNumber.parse(send(field)&.sub(%r{^\+?}, '+'))
  455. preferences[:"#{field}_pretty"] = parsed.send(parsed.valid? ? :international_number : :original_number)
  456. end
  457. end
  458. =begin
  459. returns queues of user
  460. ['queue1', 'queue2'] = Cti::Log.queues_of_user(User.find(123), config)
  461. =end
  462. def self.queues_of_user(user, config)
  463. queues = []
  464. config[:notify_map]&.each do |row|
  465. next if row[:user_ids].blank?
  466. next if row[:user_ids].exclude?(user.id.to_s) && row[:user_ids].exclude?(user.id)
  467. queues.push row[:queue]
  468. end
  469. if user.phone.present?
  470. caller_ids = Cti::CallerId.extract_numbers(user.phone)
  471. queues.concat(caller_ids)
  472. end
  473. queues
  474. end
  475. =begin
  476. return best customer id of caller log
  477. log = Cti::Log.find(123)
  478. customer_id = log.best_customer_id_of_log_entry
  479. =end
  480. def best_customer_id_of_log_entry
  481. customer_id = nil
  482. if preferences[:from].present?
  483. preferences[:from].each do |entry|
  484. if customer_id.blank?
  485. customer_id = entry[:user_id]
  486. end
  487. next if entry[:level] != 'known'
  488. customer_id = entry[:user_id]
  489. break
  490. end
  491. end
  492. customer_id
  493. end
  494. def self.view_limit
  495. Hash(Setting.get('cti_config'))['view_limit'] || 60
  496. end
  497. end
  498. end