123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class Chat < ApplicationModel
- include ChecksHtmlSanitized
- has_many :sessions, dependent: :destroy
- validates :name, presence: true, uniqueness: { case_sensitive: false }
- store :preferences
- validates :note, length: { maximum: 250 }
- sanitized_html :note
- =begin
- get the customer state of a chat
- chat = Chat.find(123)
- chat.customer_state(session_id = nil)
- returns
- chat_disabled - chat is disabled
- {
- state: 'chat_disabled'
- }
- returns (without session_id)
- offline - no agent is online
- {
- state: 'offline'
- }
- no_seats_available - no agent is available (all slots are used, max_queue is full)
- {
- state: 'no_seats_available'
- }
- online - ready for chats
- {
- state: 'online'
- }
- returns (session_id)
- reconnect - position of waiting list for new chat
- {
- state: 'reconnect',
- position: chat_session.position,
- }
- reconnect - chat session already exists, serve agent and session chat messages (to redraw chat history)
- {
- state: 'reconnect',
- session: session,
- agent: user,
- }
- =end
- def customer_state(session_id = nil)
- return { state: 'chat_disabled' } if !Setting.get('chat')
- # reconnect
- if session_id
- chat_session = Chat::Session.find_by(session_id: session_id, state: %w[waiting running])
- if chat_session
- case chat_session.state
- when 'running'
- user = chat_session.agent_user
- if user
- # get queue position if needed
- session = Chat::Session.messages_by_session_id(session_id)
- if session
- return {
- state: 'reconnect',
- session: session,
- agent: user,
- }
- end
- end
- when 'waiting'
- return {
- state: 'reconnect',
- position: chat_session.position,
- }
- end
- end
- end
- # check if agents are available
- if Chat.active_agent_count([id]).zero?
- return { state: 'offline' }
- end
- # if all seads are used
- waiting_count = Chat.waiting_chat_count(id)
- if waiting_count >= max_queue
- return {
- state: 'no_seats_available',
- queue: waiting_count,
- }
- end
- # seads are available
- { state: 'online' }
- end
- =begin
- get available chat_ids for agent
- chat_ids = Chat.agent_active_chat_ids(User.find(123))
- returns
- [1, 2, 3]
- =end
- def self.agent_active_chat_ids(user)
- return [] if user.preferences[:chat].blank?
- return [] if user.preferences[:chat][:active].blank?
- chat_ids = []
- user.preferences[:chat][:active].each do |chat_id, state|
- next if state != 'on'
- chat_ids.push chat_id.to_i
- end
- return [] if chat_ids.blank?
- chat_ids
- end
- =begin
- get current agent state
- Chat.agent_state(123)
- returns
- {
- state: 'chat_disabled'
- }
- {
- waiting_chat_count: 1,
- waiting_chat_session_list: [
- {
- id: 17,
- chat_id: 1,
- session_id: "81e58184385aabe92508eb9233200b5e",
- name: "",
- state: "waiting",
- user_id: nil,
- preferences: {
- "url: "http://localhost:3000/chat.html",
- "participants: ["70332537618180"],
- "remote_ip: nil,
- "geo_ip: nil,
- "dns_name: nil,
- },
- updated_by_id: nil,
- created_by_id: nil,
- created_at: Thu, 02 May 2019 10:10:45 UTC +00:00,
- updated_at: Thu, 02 May 2019 10:10:45 UTC +00:00},
- }
- ],
- running_chat_count: 0,
- running_chat_session_list: [],
- active_agent_count: 2,
- active_agent_ids: [1, 2],
- seads_available: 5,
- seads_total: 15,
- active: true, # agent is available for chats
- assets: { ...related assets... },
- }
- =end
- def self.agent_state(user_id)
- return { state: 'chat_disabled' } if !Setting.get('chat')
- current_user = User.lookup(id: user_id)
- return { error: "No such user with id: #{user_id}" } if !current_user
- chat_ids = agent_active_chat_ids(current_user)
- assets = {}
- Chat.where(active: true).each do |chat|
- assets = chat.assets(assets)
- end
- active_agent_ids = []
- active_agents(chat_ids).each do |user|
- active_agent_ids.push user.id
- assets = user.assets(assets)
- end
- running_chat_session_list_local = running_chat_session_list(chat_ids)
- running_chat_session_list_local.each do |session|
- next if !session['user_id']
- user = User.lookup(id: session['user_id'])
- next if !user
- assets = user.assets(assets)
- end
- {
- waiting_chat_count: waiting_chat_count(chat_ids),
- waiting_chat_count_by_chat: waiting_chat_count_by_chat(chat_ids),
- waiting_chat_session_list: waiting_chat_session_list(chat_ids),
- waiting_chat_session_list_by_chat: waiting_chat_session_list_by_chat(chat_ids),
- running_chat_count: running_chat_count(chat_ids),
- running_chat_session_list: running_chat_session_list_local,
- active_agent_count: active_agent_count(chat_ids),
- active_agent_ids: active_agent_ids,
- seads_available: seads_available(chat_ids),
- seads_total: seads_total(chat_ids),
- active: Chat::Agent.state(user_id),
- assets: assets,
- }
- end
- =begin
- check if agent is available for chat_ids
- chat_ids = Chat.agent_active_chat?(User.find(123), [1, 2])
- returns
- true|false
- =end
- def self.agent_active_chat?(user, chat_ids)
- return true if user.preferences[:chat].blank?
- return true if user.preferences[:chat][:active].blank?
- chat_ids.each do |chat_id|
- return true if user.preferences[:chat][:active][chat_id] == 'on' || user.preferences[:chat][:active][chat_id.to_s] == 'on'
- end
- false
- end
- =begin
- list all active sessins by user_id
- Chat.agent_state_with_sessions(123)
- returns
- the same as Chat.agent_state(123) but with the following addition
- active_sessions: [
- {
- id: 19,
- chat_id: 1,
- session_id: "f28b2704e381c668c9b59215e9481310",
- name: "",
- state: "running",
- user_id: 3,
- preferences: {
- url: "http://localhost/chat.html",
- participants: ["70332475730240", "70332481108980"],
- remote_ip: nil,
- geo_ip: nil,
- dns_name: nil
- },
- updated_by_id: nil,
- created_by_id: nil,
- created_at: Thu, 02 May 2019 11:48:25 UTC +00:00,
- updated_at: Thu, 02 May 2019 11:48:29 UTC +00:00,
- messages: []
- }
- ]
- =end
- def self.agent_state_with_sessions(user_id)
- return { state: 'chat_disabled' } if !Setting.get('chat')
- result = agent_state(user_id)
- result[:active_sessions] = Chat::Session.active_chats_by_user_id(user_id)
- result
- end
- =begin
- get count if waiting sessions in given chats
- Chat.waiting_chat_count(chat_ids)
- returns
- 123
- =end
- def self.waiting_chat_count(chat_ids)
- Chat::Session.where(state: ['waiting'], chat_id: chat_ids).count
- end
- def self.waiting_chat_count_by_chat(chat_ids)
- where(active: true, id: chat_ids)
- .pluck(:id)
- .index_with { |chat_id| waiting_chat_count(chat_id) }
- end
- def self.waiting_chat_session_list(chat_ids)
- Chat::Session
- .where(state: ['waiting'], chat_id: chat_ids)
- .map(&:attributes)
- end
- def self.waiting_chat_session_list_by_chat(chat_ids)
- active_chats = Chat.where(active: true, id: chat_ids).pluck(:id)
- Chat::Session
- .where(chat_id: active_chats, state: ['waiting'])
- .group_by(&:chat_id)
- end
- =begin
- get count running sessions in given chats
- Chat.running_chat_count(chat_ids)
- returns
- 123
- =end
- def self.running_chat_count(chat_ids)
- Chat::Session
- .where(state: ['running'], chat_id: chat_ids)
- .count
- end
- def self.running_chat_session_list(chat_ids)
- Chat::Session
- .where(state: ['running'], chat_id: chat_ids)
- .map(&:attributes)
- end
- =begin
- get count of active sessions in given chats
- Chat.active_chat_count(chat_ids)
- returns
- 123
- =end
- def self.active_chat_count(chat_ids)
- Chat::Session.where(state: %w[waiting running], chat_id: chat_ids).count
- end
- =begin
- get user agents with concurrent
- Chat.available_agents_with_concurrent(chat_ids)
- returns
- {
- 123: 5,
- 124: 1,
- 125: 2,
- }
- =end
- def self.available_agents_with_concurrent(chat_ids, diff = 2.minutes)
- agents = {}
- Chat::Agent.where(active: true).where('updated_at > ?', Time.zone.now - diff).each do |record|
- user = User.lookup(id: record.updated_by_id)
- next if !user
- next if !agent_active_chat?(user, chat_ids)
- agents[record.updated_by_id] = record.concurrent
- end
- agents
- end
- =begin
- get count of active agents in given chats
- Chat.active_agent_count(chat_ids)
- returns
- 123
- =end
- def self.active_agent_count(chat_ids, diff = 2.minutes)
- count = 0
- Chat::Agent.where(active: true).where('updated_at > ?', Time.zone.now - diff).each do |record|
- user = User.lookup(id: record.updated_by_id)
- next if !user
- next if !agent_active_chat?(user, chat_ids)
- count += 1
- end
- count
- end
- =begin
- get active agents in given chats
- Chat.active_agent_count(chat_ids)
- returns
- [User.find(123), User.find(345)]
- =end
- def self.active_agents(chat_ids, diff = 2.minutes)
- users = []
- Chat::Agent.where(active: true).where('updated_at > ?', Time.zone.now - diff).each do |record|
- user = User.lookup(id: record.updated_by_id)
- next if !user
- next if !agent_active_chat?(user, chat_ids)
- users.push user
- end
- users
- end
- =begin
- get count all possible seads (possible chat sessions) in given chats
- Chat.seads_total(chat_ids)
- returns
- 123
- =end
- def self.seads_total(chat_ids, diff = 2.minutes)
- total = 0
- available_agents_with_concurrent(chat_ids, diff).each_value do |concurrent|
- total += concurrent
- end
- total
- end
- =begin
- get count all available seads (available chat sessions) in given chats
- Chat.seads_available(chat_ids)
- returns
- 123
- =end
- def self.seads_available(chat_ids, diff = 2.minutes)
- seads_total(chat_ids, diff) - active_chat_count(chat_ids)
- end
- =begin
- broadcast new agent status to all agents
- Chat.broadcast_agent_state_update(chat_ids)
- optional you can ignore it for dedicated user
- Chat.broadcast_agent_state_update(chat_ids, ignore_user_id)
- =end
- def self.broadcast_agent_state_update(chat_ids, ignore_user_id = nil)
- # send broadcast to agents
- Chat::Agent.where('active = ? OR updated_at > ?', true, 8.hours.ago).each do |item|
- next if item.updated_by_id == ignore_user_id
- user = User.lookup(id: item.updated_by_id)
- next if !user
- next if !agent_active_chat?(user, chat_ids)
- data = {
- event: 'chat_status_agent',
- data: Chat.agent_state(item.updated_by_id),
- }
- Sessions.send_to(item.updated_by_id, data)
- end
- end
- =begin
- broadcast new customer queue position to all waiting customers
- Chat.broadcast_customer_state_update(chat_id)
- =end
- def self.broadcast_customer_state_update(chat_id)
- # send position update to other waiting sessions
- position = 0
- Chat::Session.where(state: 'waiting', chat_id: chat_id).reorder(created_at: :asc).each do |local_chat_session|
- position += 1
- data = {
- event: 'chat_session_queue',
- data: {
- state: 'queue',
- position: position,
- session_id: local_chat_session.session_id,
- },
- }
- local_chat_session.send_to_recipients(data)
- end
- end
- =begin
- cleanup old chat messages
- Chat.cleanup
- optional you can put the max oldest chat entries
- Chat.cleanup(12.months)
- =end
- def self.cleanup(diff = 12.months)
- Chat::Session
- .where(state: 'closed', updated_at: ...diff.ago)
- .each(&:destroy)
- true
- end
- =begin
- close chat sessions where participants are offline
- Chat.cleanup_close
- optional you can put the max oldest chat sessions as argument
- Chat.cleanup_close(5.minutes)
- =end
- def self.cleanup_close(diff = 5.minutes)
- Chat::Session
- .where.not(state: 'closed')
- .where(updated_at: ...diff.ago)
- .each do |chat_session|
- next if chat_session.recipients_active?
- chat_session.state = 'closed'
- chat_session.save
- message = {
- event: 'chat_session_closed',
- data: {
- session_id: chat_session.session_id,
- realname: 'System',
- },
- }
- chat_session.send_to_recipients(message)
- end
- true
- end
- =begin
- check if ip address is blocked for chat
- chat = Chat.find(123)
- chat.blocked_ip?(ip)
- =end
- def blocked_ip?(ip)
- return false if ip.blank?
- return false if block_ip.blank?
- ips = block_ip.split(';')
- ips.each do |local_ip|
- return true if ip == local_ip.strip
- return true if ip.match?(%r{#{local_ip.strip.gsub(%r{\*}, '.+?')}})
- end
- false
- end
- =begin
- check if website is allowed for chat
- chat = Chat.find(123)
- chat.website_allowed?('zammad.org')
- =end
- def website_allowed?(website)
- return true if allowed_websites.blank?
- allowed_websites.split(';').any? do |allowed_website|
- website.downcase.include?(allowed_website.downcase.strip)
- end
- end
- =begin
- check if country is blocked for chat
- chat = Chat.find(123)
- chat.blocked_country?(ip)
- =end
- def blocked_country?(ip)
- return false if ip.blank?
- return false if block_country.blank?
- geo_ip = Service::GeoIp.location(ip)
- return false if geo_ip.blank?
- return false if geo_ip['country_code'].blank?
- countries = block_country.split(';')
- countries.any?(geo_ip['country_code'])
- end
- end
|