zammad/zammad

View on GitHub
app/models/chat.rb

Summary

Maintainability
D
2 days
Test Coverage
# 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').where('updated_at < ?', Time.zone.now - diff).each do |chat_session|
      Chat::Message.where(chat_session_id: chat_session.id).delete_all
      chat_session.destroy
    end

    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 < ?', Time.zone.now - diff).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