Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
class Voice::CallMessageBuilder
|
||||
def self.perform!(conversation:, direction:, payload:, user: nil, timestamps: {})
|
||||
new(
|
||||
conversation: conversation,
|
||||
direction: direction,
|
||||
payload: payload,
|
||||
user: user,
|
||||
timestamps: timestamps
|
||||
).perform!
|
||||
end
|
||||
|
||||
def initialize(conversation:, direction:, payload:, user:, timestamps:)
|
||||
@conversation = conversation
|
||||
@direction = direction
|
||||
@payload = payload
|
||||
@user = user
|
||||
@timestamps = timestamps
|
||||
end
|
||||
|
||||
def perform!
|
||||
validate_sender!
|
||||
message = latest_message
|
||||
message ? update_message!(message) : create_message!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :conversation, :direction, :payload, :user, :timestamps
|
||||
|
||||
def latest_message
|
||||
conversation.messages.voice_calls.order(created_at: :desc).first
|
||||
end
|
||||
|
||||
def update_message!(message)
|
||||
message.update!(
|
||||
message_type: message_type,
|
||||
content_attributes: { 'data' => base_payload },
|
||||
sender: sender
|
||||
)
|
||||
end
|
||||
|
||||
def create_message!
|
||||
params = {
|
||||
content: 'Voice Call',
|
||||
message_type: message_type,
|
||||
content_type: 'voice_call',
|
||||
content_attributes: { 'data' => base_payload }
|
||||
}
|
||||
Messages::MessageBuilder.new(sender, conversation, params).perform
|
||||
end
|
||||
|
||||
def base_payload
|
||||
@base_payload ||= begin
|
||||
data = payload.slice(
|
||||
:call_sid,
|
||||
:status,
|
||||
:call_direction,
|
||||
:conference_sid,
|
||||
:from_number,
|
||||
:to_number
|
||||
).stringify_keys
|
||||
data['call_direction'] = direction
|
||||
data['meta'] = {
|
||||
'created_at' => timestamps[:created_at] || current_timestamp,
|
||||
'ringing_at' => timestamps[:ringing_at] || current_timestamp
|
||||
}.compact
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
def message_type
|
||||
direction == 'outbound' ? 'outgoing' : 'incoming'
|
||||
end
|
||||
|
||||
def sender
|
||||
return user if direction == 'outbound'
|
||||
|
||||
conversation.contact
|
||||
end
|
||||
|
||||
def validate_sender!
|
||||
return unless direction == 'outbound'
|
||||
|
||||
raise ArgumentError, 'Agent sender required for outbound calls' unless user
|
||||
end
|
||||
|
||||
def current_timestamp
|
||||
@current_timestamp ||= Time.zone.now.to_i
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
class Voice::CallSessionSyncService
|
||||
attr_reader :conversation, :call_sid, :message_call_sid, :from_number, :to_number, :direction
|
||||
|
||||
def initialize(conversation:, call_sid:, leg:, message_call_sid: nil)
|
||||
@conversation = conversation
|
||||
@call_sid = call_sid
|
||||
@message_call_sid = message_call_sid || call_sid
|
||||
@from_number = leg[:from_number]
|
||||
@to_number = leg[:to_number]
|
||||
@direction = leg[:direction]
|
||||
end
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
attrs = refreshed_attributes
|
||||
conversation.update!(
|
||||
additional_attributes: attrs,
|
||||
last_activity_at: current_time
|
||||
)
|
||||
sync_voice_call_message!(attrs)
|
||||
end
|
||||
|
||||
conversation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def refreshed_attributes
|
||||
attrs = (conversation.additional_attributes || {}).dup
|
||||
attrs['call_direction'] = direction
|
||||
attrs['call_status'] ||= 'ringing'
|
||||
attrs['conference_sid'] ||= Voice::Conference::Name.for(conversation)
|
||||
attrs['meta'] ||= {}
|
||||
attrs['meta']['initiated_at'] ||= current_timestamp
|
||||
attrs
|
||||
end
|
||||
|
||||
def sync_voice_call_message!(attrs)
|
||||
Voice::CallMessageBuilder.perform!(
|
||||
conversation: conversation,
|
||||
direction: direction,
|
||||
payload: {
|
||||
call_sid: message_call_sid,
|
||||
status: attrs['call_status'],
|
||||
conference_sid: attrs['conference_sid'],
|
||||
from_number: origin_number_for(direction),
|
||||
to_number: target_number_for(direction)
|
||||
},
|
||||
user: agent_for(attrs),
|
||||
timestamps: {
|
||||
created_at: attrs.dig('meta', 'initiated_at'),
|
||||
ringing_at: attrs.dig('meta', 'ringing_at')
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def origin_number_for(current_direction)
|
||||
return outbound_origin if current_direction == 'outbound'
|
||||
|
||||
from_number.presence || inbox_number
|
||||
end
|
||||
|
||||
def target_number_for(current_direction)
|
||||
return conversation.contact&.phone_number || to_number if current_direction == 'outbound'
|
||||
|
||||
to_number || conversation.contact&.phone_number
|
||||
end
|
||||
|
||||
def agent_for(attrs)
|
||||
agent_id = attrs['agent_id']
|
||||
return nil unless agent_id
|
||||
|
||||
agent = conversation.account.users.find_by(id: agent_id)
|
||||
raise ArgumentError, 'Agent sender required for outbound call sync' if direction == 'outbound' && agent.nil?
|
||||
|
||||
agent
|
||||
end
|
||||
|
||||
def current_timestamp
|
||||
@current_timestamp ||= current_time.to_i
|
||||
end
|
||||
|
||||
def current_time
|
||||
@current_time ||= Time.zone.now
|
||||
end
|
||||
|
||||
def outbound_origin
|
||||
inbox_number || from_number
|
||||
end
|
||||
|
||||
def inbox_number
|
||||
conversation.inbox&.channel&.phone_number
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
class Voice::CallStatus::Manager
|
||||
pattr_initialize [:conversation!, :call_sid]
|
||||
|
||||
ALLOWED_STATUSES = %w[ringing in-progress completed no-answer failed].freeze
|
||||
TERMINAL_STATUSES = %w[completed no-answer failed].freeze
|
||||
|
||||
def process_status_update(status, duration: nil, timestamp: nil)
|
||||
return unless ALLOWED_STATUSES.include?(status)
|
||||
|
||||
current_status = conversation.additional_attributes&.dig('call_status')
|
||||
return if current_status == status
|
||||
|
||||
apply_status(status, duration: duration, timestamp: timestamp)
|
||||
update_message(status)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def apply_status(status, duration:, timestamp:)
|
||||
attrs = (conversation.additional_attributes || {}).dup
|
||||
attrs['call_status'] = status
|
||||
|
||||
if status == 'in-progress'
|
||||
attrs['call_started_at'] ||= timestamp || now_seconds
|
||||
elsif TERMINAL_STATUSES.include?(status)
|
||||
attrs['call_ended_at'] = timestamp || now_seconds
|
||||
attrs['call_duration'] = resolved_duration(attrs, duration, timestamp)
|
||||
end
|
||||
|
||||
conversation.update!(
|
||||
additional_attributes: attrs,
|
||||
last_activity_at: current_time
|
||||
)
|
||||
end
|
||||
|
||||
def resolved_duration(attrs, provided_duration, timestamp)
|
||||
return provided_duration if provided_duration
|
||||
|
||||
started_at = attrs['call_started_at']
|
||||
return unless started_at && timestamp
|
||||
|
||||
[timestamp - started_at.to_i, 0].max
|
||||
end
|
||||
|
||||
def update_message(status)
|
||||
message = conversation.messages
|
||||
.where(content_type: 'voice_call')
|
||||
.order(created_at: :desc)
|
||||
.first
|
||||
return unless message
|
||||
|
||||
data = (message.content_attributes || {}).dup
|
||||
data['data'] ||= {}
|
||||
data['data']['status'] = status
|
||||
|
||||
message.update!(content_attributes: data)
|
||||
end
|
||||
|
||||
def now_seconds
|
||||
current_time.to_i
|
||||
end
|
||||
|
||||
def current_time
|
||||
@current_time ||= Time.zone.now
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
class Voice::Conference::Manager
|
||||
pattr_initialize [:conversation!, :event!, :call_sid!, :participant_label]
|
||||
|
||||
def process
|
||||
case event
|
||||
when 'start'
|
||||
ensure_conference_sid!
|
||||
mark_ringing!
|
||||
when 'join'
|
||||
mark_in_progress! if agent_participant?
|
||||
when 'leave'
|
||||
handle_leave!
|
||||
when 'end'
|
||||
finalize_conference!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def status_manager
|
||||
@status_manager ||= Voice::CallStatus::Manager.new(
|
||||
conversation: conversation,
|
||||
call_sid: call_sid
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_conference_sid!
|
||||
attrs = conversation.additional_attributes || {}
|
||||
return if attrs['conference_sid'].present?
|
||||
|
||||
attrs['conference_sid'] = Voice::Conference::Name.for(conversation)
|
||||
conversation.update!(additional_attributes: attrs)
|
||||
end
|
||||
|
||||
def mark_ringing!
|
||||
return if current_status
|
||||
|
||||
status_manager.process_status_update('ringing')
|
||||
end
|
||||
|
||||
def mark_in_progress!
|
||||
status_manager.process_status_update('in-progress', timestamp: current_timestamp)
|
||||
end
|
||||
|
||||
def handle_leave!
|
||||
case current_status
|
||||
when 'ringing'
|
||||
status_manager.process_status_update('no-answer', timestamp: current_timestamp)
|
||||
when 'in-progress'
|
||||
status_manager.process_status_update('completed', timestamp: current_timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
def finalize_conference!
|
||||
return if %w[completed no-answer failed].include?(current_status)
|
||||
|
||||
status_manager.process_status_update('completed', timestamp: current_timestamp)
|
||||
end
|
||||
|
||||
def current_status
|
||||
conversation.additional_attributes&.dig('call_status')
|
||||
end
|
||||
|
||||
def agent_participant?
|
||||
participant_label.to_s.start_with?('agent')
|
||||
end
|
||||
|
||||
def current_timestamp
|
||||
Time.zone.now.to_i
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module Voice::Conference::Name
|
||||
def self.for(conversation)
|
||||
"conf_account_#{conversation.account_id}_conv_#{conversation.display_id}"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
class Voice::InboundCallBuilder
|
||||
attr_reader :account, :inbox, :from_number, :call_sid
|
||||
|
||||
def self.perform!(account:, inbox:, from_number:, call_sid:)
|
||||
new(account: account, inbox: inbox, from_number: from_number, call_sid: call_sid).perform!
|
||||
end
|
||||
|
||||
def initialize(account:, inbox:, from_number:, call_sid:)
|
||||
@account = account
|
||||
@inbox = inbox
|
||||
@from_number = from_number
|
||||
@call_sid = call_sid
|
||||
end
|
||||
|
||||
def perform!
|
||||
timestamp = current_timestamp
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
contact = ensure_contact!
|
||||
contact_inbox = ensure_contact_inbox!(contact)
|
||||
conversation = find_conversation || create_conversation!(contact, contact_inbox)
|
||||
conversation.reload
|
||||
update_conversation!(conversation, timestamp)
|
||||
build_voice_message!(conversation, timestamp)
|
||||
conversation
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_contact!
|
||||
account.contacts.find_or_create_by!(phone_number: from_number) do |record|
|
||||
record.name = from_number if record.name.blank?
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_contact_inbox!(contact)
|
||||
ContactInbox.find_or_create_by!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id
|
||||
) do |record|
|
||||
record.source_id = from_number
|
||||
end
|
||||
end
|
||||
|
||||
def find_conversation
|
||||
return if call_sid.blank?
|
||||
|
||||
account.conversations.includes(:contact).find_by(identifier: call_sid)
|
||||
end
|
||||
|
||||
def create_conversation!(contact, contact_inbox)
|
||||
account.conversations.create!(
|
||||
contact_inbox_id: contact_inbox.id,
|
||||
inbox_id: inbox.id,
|
||||
contact_id: contact.id,
|
||||
status: :open,
|
||||
identifier: call_sid
|
||||
)
|
||||
end
|
||||
|
||||
def update_conversation!(conversation, timestamp)
|
||||
attrs = {
|
||||
'call_direction' => 'inbound',
|
||||
'call_status' => 'ringing',
|
||||
'conference_sid' => Voice::Conference::Name.for(conversation),
|
||||
'meta' => { 'initiated_at' => timestamp }
|
||||
}
|
||||
|
||||
conversation.update!(
|
||||
identifier: call_sid,
|
||||
additional_attributes: attrs,
|
||||
last_activity_at: current_time
|
||||
)
|
||||
end
|
||||
|
||||
def build_voice_message!(conversation, timestamp)
|
||||
Voice::CallMessageBuilder.perform!(
|
||||
conversation: conversation,
|
||||
direction: 'inbound',
|
||||
payload: {
|
||||
call_sid: call_sid,
|
||||
status: 'ringing',
|
||||
conference_sid: conversation.additional_attributes['conference_sid'],
|
||||
from_number: from_number,
|
||||
to_number: inbox.channel&.phone_number
|
||||
},
|
||||
timestamps: { created_at: timestamp, ringing_at: timestamp }
|
||||
)
|
||||
end
|
||||
|
||||
def current_timestamp
|
||||
@current_timestamp ||= current_time.to_i
|
||||
end
|
||||
|
||||
def current_time
|
||||
@current_time ||= Time.zone.now
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
class Voice::OutboundCallBuilder
|
||||
attr_reader :account, :inbox, :user, :contact
|
||||
|
||||
def self.perform!(account:, inbox:, user:, contact:)
|
||||
new(account: account, inbox: inbox, user: user, contact: contact).perform!
|
||||
end
|
||||
|
||||
def initialize(account:, inbox:, user:, contact:)
|
||||
@account = account
|
||||
@inbox = inbox
|
||||
@user = user
|
||||
@contact = contact
|
||||
end
|
||||
|
||||
def perform!
|
||||
raise ArgumentError, 'Contact phone number required' if contact.phone_number.blank?
|
||||
raise ArgumentError, 'Agent required' if user.blank?
|
||||
|
||||
timestamp = current_timestamp
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
contact_inbox = ensure_contact_inbox!
|
||||
conversation = create_conversation!(contact_inbox)
|
||||
conversation.reload
|
||||
conference_sid = Voice::Conference::Name.for(conversation)
|
||||
call_sid = initiate_call!
|
||||
update_conversation!(conversation, call_sid, conference_sid, timestamp)
|
||||
build_voice_message!(conversation, call_sid, conference_sid, timestamp)
|
||||
{ conversation: conversation, call_sid: call_sid }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_contact_inbox!
|
||||
ContactInbox.find_or_create_by!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id
|
||||
) do |record|
|
||||
record.source_id = contact.phone_number
|
||||
end
|
||||
end
|
||||
|
||||
def create_conversation!(contact_inbox)
|
||||
account.conversations.create!(
|
||||
contact_inbox_id: contact_inbox.id,
|
||||
inbox_id: inbox.id,
|
||||
contact_id: contact.id,
|
||||
status: :open
|
||||
)
|
||||
end
|
||||
|
||||
def initiate_call!
|
||||
inbox.channel.initiate_call(
|
||||
to: contact.phone_number
|
||||
)[:call_sid]
|
||||
end
|
||||
|
||||
def update_conversation!(conversation, call_sid, conference_sid, timestamp)
|
||||
attrs = {
|
||||
'call_direction' => 'outbound',
|
||||
'call_status' => 'ringing',
|
||||
'agent_id' => user.id,
|
||||
'conference_sid' => conference_sid,
|
||||
'meta' => { 'initiated_at' => timestamp }
|
||||
}
|
||||
|
||||
conversation.update!(
|
||||
identifier: call_sid,
|
||||
additional_attributes: attrs,
|
||||
last_activity_at: current_time
|
||||
)
|
||||
end
|
||||
|
||||
def build_voice_message!(conversation, call_sid, conference_sid, timestamp)
|
||||
Voice::CallMessageBuilder.perform!(
|
||||
conversation: conversation,
|
||||
direction: 'outbound',
|
||||
payload: {
|
||||
call_sid: call_sid,
|
||||
status: 'ringing',
|
||||
conference_sid: conference_sid,
|
||||
from_number: inbox.channel&.phone_number,
|
||||
to_number: contact.phone_number
|
||||
},
|
||||
user: user,
|
||||
timestamps: { created_at: timestamp, ringing_at: timestamp }
|
||||
)
|
||||
end
|
||||
|
||||
def current_timestamp
|
||||
@current_timestamp ||= current_time.to_i
|
||||
end
|
||||
|
||||
def current_time
|
||||
@current_time ||= Time.zone.now
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
class Voice::Provider::Twilio::Adapter
|
||||
def initialize(channel)
|
||||
@channel = channel
|
||||
end
|
||||
|
||||
def initiate_call(to:, conference_sid: nil, agent_id: nil)
|
||||
call = twilio_client.calls.create(**call_params(to))
|
||||
|
||||
{
|
||||
provider: 'twilio',
|
||||
call_sid: call.sid,
|
||||
status: call.status,
|
||||
call_direction: 'outbound',
|
||||
requires_agent_join: true,
|
||||
agent_id: agent_id,
|
||||
conference_sid: conference_sid
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def call_params(to)
|
||||
phone_digits = @channel.phone_number.delete_prefix('+')
|
||||
|
||||
{
|
||||
from: @channel.phone_number,
|
||||
to: to,
|
||||
url: twilio_call_twiml_url(phone_digits),
|
||||
status_callback: twilio_call_status_url(phone_digits),
|
||||
status_callback_event: %w[
|
||||
initiated ringing answered completed failed busy no-answer canceled
|
||||
],
|
||||
status_callback_method: 'POST'
|
||||
}
|
||||
end
|
||||
|
||||
def twilio_call_twiml_url(phone_digits)
|
||||
Rails.application.routes.url_helpers.twilio_voice_call_url(phone: phone_digits)
|
||||
end
|
||||
|
||||
def twilio_call_status_url(phone_digits)
|
||||
Rails.application.routes.url_helpers.twilio_voice_status_url(phone: phone_digits)
|
||||
end
|
||||
|
||||
def twilio_client
|
||||
Twilio::REST::Client.new(config['account_sid'], config['auth_token'])
|
||||
end
|
||||
|
||||
def config
|
||||
@config ||= @channel.provider_config_hash
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
class Voice::Provider::Twilio::ConferenceService
|
||||
pattr_initialize [:conversation!, { twilio_client: nil }]
|
||||
|
||||
def ensure_conference_sid
|
||||
existing = conversation.additional_attributes&.dig('conference_sid')
|
||||
return existing if existing.present?
|
||||
|
||||
sid = Voice::Conference::Name.for(conversation)
|
||||
merge_attributes('conference_sid' => sid)
|
||||
sid
|
||||
end
|
||||
|
||||
def mark_agent_joined(user:)
|
||||
merge_attributes(
|
||||
'agent_joined' => true,
|
||||
'joined_at' => Time.current.to_i,
|
||||
'joined_by' => { id: user.id, name: user.name }
|
||||
)
|
||||
end
|
||||
|
||||
def end_conference
|
||||
twilio_client
|
||||
.conferences
|
||||
.list(friendly_name: Voice::Conference::Name.for(conversation), status: 'in-progress')
|
||||
.each { |conf| twilio_client.conferences(conf.sid).update(status: 'completed') }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def merge_attributes(attrs)
|
||||
current = conversation.additional_attributes || {}
|
||||
conversation.update!(additional_attributes: current.merge(attrs))
|
||||
end
|
||||
|
||||
def twilio_client
|
||||
@twilio_client ||= ::Twilio::REST::Client.new(account_sid, auth_token)
|
||||
end
|
||||
|
||||
def account_sid
|
||||
@account_sid ||= conversation.inbox.channel.provider_config_hash['account_sid']
|
||||
end
|
||||
|
||||
def auth_token
|
||||
@auth_token ||= conversation.inbox.channel.provider_config_hash['auth_token']
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
class Voice::Provider::Twilio::TokenService
|
||||
pattr_initialize [:inbox!, :user!, :account!]
|
||||
|
||||
def generate
|
||||
{
|
||||
token: access_token.to_jwt,
|
||||
identity: identity,
|
||||
voice_enabled: true,
|
||||
account_sid: config['account_sid'],
|
||||
agent_id: user.id,
|
||||
account_id: account.id,
|
||||
inbox_id: inbox.id,
|
||||
phone_number: inbox.channel.phone_number,
|
||||
twiml_endpoint: twiml_url,
|
||||
has_twiml_app: config['twiml_app_sid'].present?
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def config
|
||||
@config ||= inbox.channel.provider_config_hash || {}
|
||||
end
|
||||
|
||||
def identity
|
||||
@identity ||= "agent-#{user.id}-account-#{account.id}"
|
||||
end
|
||||
|
||||
def access_token
|
||||
Twilio::JWT::AccessToken.new(
|
||||
config['account_sid'],
|
||||
config['api_key_sid'],
|
||||
config['api_key_secret'],
|
||||
identity: identity,
|
||||
ttl: 1.hour.to_i
|
||||
).tap { |token| token.add_grant(voice_grant) }
|
||||
end
|
||||
|
||||
def voice_grant
|
||||
Twilio::JWT::AccessToken::VoiceGrant.new.tap do |grant|
|
||||
grant.incoming_allow = true
|
||||
grant.outgoing_application_sid = config['twiml_app_sid']
|
||||
grant.outgoing_application_params = outgoing_params
|
||||
end
|
||||
end
|
||||
|
||||
def outgoing_params
|
||||
{
|
||||
account_id: account.id,
|
||||
agent_id: user.id,
|
||||
identity: identity,
|
||||
client_name: identity,
|
||||
accountSid: config['account_sid'],
|
||||
is_agent: 'true'
|
||||
}
|
||||
end
|
||||
|
||||
def twiml_url
|
||||
digits = inbox.channel.phone_number.delete_prefix('+')
|
||||
Rails.application.routes.url_helpers.twilio_voice_call_url(phone: digits)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,60 @@
|
||||
class Voice::StatusUpdateService
|
||||
pattr_initialize [:account!, :call_sid!, :call_status, { payload: {} }]
|
||||
|
||||
TWILIO_STATUS_MAP = {
|
||||
'queued' => 'ringing',
|
||||
'initiated' => 'ringing',
|
||||
'ringing' => 'ringing',
|
||||
'in-progress' => 'in-progress',
|
||||
'inprogress' => 'in-progress',
|
||||
'answered' => 'in-progress',
|
||||
'completed' => 'completed',
|
||||
'busy' => 'no-answer',
|
||||
'no-answer' => 'no-answer',
|
||||
'failed' => 'failed',
|
||||
'canceled' => 'failed'
|
||||
}.freeze
|
||||
|
||||
def perform
|
||||
normalized_status = normalize_status(call_status)
|
||||
return if normalized_status.blank?
|
||||
|
||||
conversation = account.conversations.find_by(identifier: call_sid)
|
||||
return unless conversation
|
||||
|
||||
Voice::CallStatus::Manager.new(
|
||||
conversation: conversation,
|
||||
call_sid: call_sid
|
||||
).process_status_update(
|
||||
normalized_status,
|
||||
duration: payload_duration,
|
||||
timestamp: payload_timestamp
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_status(status)
|
||||
return if status.to_s.strip.empty?
|
||||
|
||||
TWILIO_STATUS_MAP[status.to_s.downcase]
|
||||
end
|
||||
|
||||
def payload_duration
|
||||
return unless payload.is_a?(Hash)
|
||||
|
||||
duration = payload['CallDuration'] || payload['call_duration']
|
||||
duration&.to_i
|
||||
end
|
||||
|
||||
def payload_timestamp
|
||||
return unless payload.is_a?(Hash)
|
||||
|
||||
ts = payload['Timestamp'] || payload['timestamp']
|
||||
return unless ts
|
||||
|
||||
Time.zone.parse(ts).to_i
|
||||
rescue ArgumentError
|
||||
nil
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user