## Description Adds a backend endpoint that powers an account-wide calls dashboard, letting users list and filter all calls in the account. ## Linear Ticket - https://linear.app/chatwoot/issue/UPM-28/voice-call-dashboard-view ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
143 lines
4.4 KiB
Ruby
143 lines
4.4 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: calls
|
|
#
|
|
# id :bigint not null, primary key
|
|
# direction :integer not null
|
|
# duration_seconds :integer
|
|
# end_reason :string
|
|
# meta :jsonb
|
|
# provider :integer default("twilio"), not null
|
|
# started_at :datetime
|
|
# status :string default("ringing"), not null
|
|
# transcript :text
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# accepted_by_agent_id :bigint
|
|
# account_id :bigint not null
|
|
# contact_id :bigint not null
|
|
# conversation_id :bigint not null
|
|
# inbox_id :bigint not null
|
|
# message_id :bigint
|
|
# provider_call_id :string not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_calls_on_account_id_and_contact_id (account_id,contact_id)
|
|
# index_calls_on_account_id_and_conversation_id (account_id,conversation_id)
|
|
# index_calls_on_message_id (message_id)
|
|
# index_calls_on_provider_and_provider_call_id (provider,provider_call_id) UNIQUE
|
|
#
|
|
class Call < ApplicationRecord
|
|
STATUSES = %w[ringing in_progress completed no_answer failed rejected].freeze
|
|
TERMINAL_STATUSES = %w[completed no_answer failed rejected].freeze
|
|
|
|
store_accessor :meta, :conference_sid, :twilio_conference_sid, :recording_sid, :parent_call_sid, :initiated_at, :ended_at
|
|
|
|
# Frontend voice bubbles/stores expect inbound/outbound string values
|
|
DISPLAY_DIRECTION = { 'incoming' => 'inbound', 'outgoing' => 'outbound' }.freeze
|
|
|
|
DEFAULT_STUN_URL = 'stun:stun.l.google.com:19302'.freeze
|
|
|
|
enum :provider, { twilio: 0, whatsapp: 1 }
|
|
enum :direction, { incoming: 0, outgoing: 1 }
|
|
|
|
belongs_to :account
|
|
belongs_to :inbox
|
|
belongs_to :conversation
|
|
belongs_to :contact
|
|
belongs_to :message, optional: true, inverse_of: :call
|
|
belongs_to :accepted_by_agent, class_name: 'User', optional: true
|
|
|
|
has_one_attached :recording
|
|
|
|
validates :provider_call_id, presence: true
|
|
validates :provider, presence: true
|
|
validates :direction, presence: true
|
|
validates :status, presence: true, inclusion: { in: STATUSES }
|
|
|
|
scope :active, -> { where.not(status: TERMINAL_STATUSES) }
|
|
scope :by_conference_sid, ->(sid) { where("meta->>'conference_sid' = ?", sid) }
|
|
scope :by_twilio_conference_sid, ->(sid) { where("meta->>'twilio_conference_sid' = ?", sid) }
|
|
|
|
def self.find_by_provider_call_id(provider, sid)
|
|
find_by(provider: provider, provider_call_id: sid)
|
|
end
|
|
|
|
def default_conference_sid
|
|
"conf_account_#{account_id}_call_#{id}"
|
|
end
|
|
|
|
# Browser ↔ Meta WebRTC needs at least one STUN server to discover its public srflx candidate.
|
|
def self.default_ice_servers
|
|
urls = ENV.fetch('VOICE_CALL_STUN_URLS', DEFAULT_STUN_URL).split(',').filter_map { |u| u.strip.presence }
|
|
[{ urls: urls }]
|
|
end
|
|
|
|
def direction_label
|
|
DISPLAY_DIRECTION[direction]
|
|
end
|
|
|
|
# Normalize filter values back to stored forms so API/dashboard clients can
|
|
# query using either the display value (inbound/outbound, in-progress) or the
|
|
# stored value (incoming/outgoing, in_progress).
|
|
def self.direction_from_label(value)
|
|
DISPLAY_DIRECTION.key(value) || value
|
|
end
|
|
|
|
def self.status_from_display(value)
|
|
value.to_s.tr('-', '_')
|
|
end
|
|
|
|
def ringing?
|
|
status == 'ringing'
|
|
end
|
|
|
|
def in_progress?
|
|
status == 'in_progress'
|
|
end
|
|
|
|
def terminal?
|
|
TERMINAL_STATUSES.include?(status)
|
|
end
|
|
|
|
def display_status
|
|
status.to_s.tr('_', '-')
|
|
end
|
|
|
|
def from_number
|
|
incoming? ? contact.phone_number : inbox.channel&.phone_number
|
|
end
|
|
|
|
def to_number
|
|
incoming? ? inbox.channel&.phone_number : contact.phone_number
|
|
end
|
|
|
|
def recording_url
|
|
return nil unless recording.attached?
|
|
|
|
Rails.application.routes.url_helpers.rails_blob_url(recording)
|
|
end
|
|
|
|
def push_event_data
|
|
{
|
|
id: id,
|
|
provider_call_id: provider_call_id,
|
|
provider: provider,
|
|
direction: direction,
|
|
status: display_status,
|
|
duration_seconds: duration_seconds,
|
|
end_reason: end_reason,
|
|
conference_sid: conference_sid,
|
|
accepted_by_agent_id: accepted_by_agent_id,
|
|
accepted_by_agent_name: accepted_by_agent&.available_name,
|
|
started_at: started_at&.to_i,
|
|
ended_at: ended_at,
|
|
from_number: from_number,
|
|
to_number: to_number,
|
|
recording_url: recording_url,
|
|
transcript: transcript
|
|
}
|
|
end
|
|
end
|