feat(voice): wire Twilio voice to unified Call model
Twilio voice flows now persist Call records instead of stuffing state into conversation.additional_attributes + conversation.identifier. - InboundCallBuilder / OutboundCallBuilder create Call rows (provider, direction, status, provider_call_id, contact, meta), link call.message_id to the voice_call message, and denormalize call_status / call_direction / conference_sid onto the conversation for frontend compatibility. - Conference::Name.for(call) uses call.id, removing collisions with multiple calls per conversation. - CallMessageBuilder matches existing voice_call messages by content_attributes.data.call_sid, so each call gets its own bubble. - CallStatus::Manager, StatusUpdateService, Conference::Manager, CallSessionSyncService, ConferenceController and ConferenceService all operate on Call records and look up by provider_call_id. - InboundCallBuilder honours inbox.lock_to_single_conversation: reuses the latest non-resolved conversation when enabled, creates a new conversation otherwise.
This commit is contained in:
@@ -11,9 +11,9 @@ class Api::V1::Accounts::ConferenceController < Api::V1::Accounts::BaseControlle
|
||||
|
||||
def create
|
||||
conversation = fetch_conversation_by_display_id
|
||||
ensure_call_sid!(conversation)
|
||||
call = find_or_initialize_call!(conversation)
|
||||
|
||||
conference_service = Voice::Provider::Twilio::ConferenceService.new(conversation: conversation)
|
||||
conference_service = Voice::Provider::Twilio::ConferenceService.new(call: call)
|
||||
conference_sid = conference_service.ensure_conference_sid
|
||||
conference_service.mark_agent_joined(user: current_user)
|
||||
|
||||
@@ -27,19 +27,34 @@ class Api::V1::Accounts::ConferenceController < Api::V1::Accounts::BaseControlle
|
||||
|
||||
def destroy
|
||||
conversation = fetch_conversation_by_display_id
|
||||
Voice::Provider::Twilio::ConferenceService.new(conversation: conversation).end_conference
|
||||
call = Current.account.calls.where(conversation_id: conversation.id).order(created_at: :desc).first
|
||||
return render(json: { status: 'success', id: conversation.display_id }) unless call
|
||||
|
||||
Voice::Provider::Twilio::ConferenceService.new(call: call).end_conference
|
||||
render json: { status: 'success', id: conversation.display_id }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_call_sid!(conversation)
|
||||
return conversation.identifier if conversation.identifier.present?
|
||||
def find_or_initialize_call!(conversation)
|
||||
sid = params[:call_sid].presence
|
||||
existing = Current.account.calls.where(conversation_id: conversation.id, provider: :twilio)
|
||||
existing = existing.where(provider_call_id: sid) if sid
|
||||
call = existing.order(created_at: :desc).first
|
||||
return call if call
|
||||
|
||||
incoming_sid = params.require(:call_sid)
|
||||
raise ActionController::ParameterMissing, :call_sid unless sid
|
||||
|
||||
conversation.update!(identifier: incoming_sid)
|
||||
incoming_sid
|
||||
Current.account.calls.create!(
|
||||
inbox_id: conversation.inbox_id,
|
||||
conversation: conversation,
|
||||
contact_id: conversation.contact_id,
|
||||
provider: :twilio,
|
||||
direction: :outgoing,
|
||||
status: 'ringing',
|
||||
provider_call_id: sid,
|
||||
accepted_by_agent_id: current_user.id
|
||||
)
|
||||
end
|
||||
|
||||
def set_voice_inbox_for_conference
|
||||
|
||||
@@ -25,8 +25,8 @@ class Twilio::VoiceController < ApplicationController
|
||||
"TWILIO_VOICE_TWIML account=#{account.id} call_sid=#{twilio_call_sid} from=#{twilio_from} direction=#{twilio_direction}"
|
||||
)
|
||||
|
||||
conversation = resolve_conversation
|
||||
conference_sid = ensure_conference_sid!(conversation)
|
||||
call = resolve_call
|
||||
conference_sid = ensure_conference_sid!(call)
|
||||
|
||||
render xml: conference_twiml(conference_sid, agent_leg?(twilio_from))
|
||||
end
|
||||
@@ -35,15 +35,14 @@ class Twilio::VoiceController < ApplicationController
|
||||
event = mapped_conference_event
|
||||
return head :no_content unless event
|
||||
|
||||
conversation = find_conversation_for_conference!(
|
||||
call = find_call_for_conference!(
|
||||
friendly_name: params[:FriendlyName],
|
||||
call_sid: twilio_call_sid
|
||||
)
|
||||
|
||||
Voice::Conference::Manager.new(
|
||||
conversation: conversation,
|
||||
call: call,
|
||||
event: event,
|
||||
call_sid: twilio_call_sid,
|
||||
participant_label: participant_label
|
||||
).process
|
||||
|
||||
@@ -80,8 +79,8 @@ class Twilio::VoiceController < ApplicationController
|
||||
from_number.start_with?('client:')
|
||||
end
|
||||
|
||||
def resolve_conversation
|
||||
return find_conversation_for_agent if agent_leg?(twilio_from)
|
||||
def resolve_call
|
||||
return find_call_for_agent if agent_leg?(twilio_from)
|
||||
|
||||
case twilio_direction
|
||||
when 'inbound'
|
||||
@@ -91,6 +90,7 @@ class Twilio::VoiceController < ApplicationController
|
||||
from_number: twilio_from,
|
||||
call_sid: twilio_call_sid
|
||||
)
|
||||
find_call!(twilio_call_sid)
|
||||
when 'outbound-api', 'outbound-dial'
|
||||
sync_outbound_leg(
|
||||
call_sid: twilio_call_sid,
|
||||
@@ -102,36 +102,47 @@ class Twilio::VoiceController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def find_conversation_for_agent
|
||||
def find_call_for_agent
|
||||
if params[:conversation_id].present?
|
||||
current_account.conversations.find_by!(display_id: params[:conversation_id])
|
||||
conversation = current_account.conversations.find_by!(display_id: params[:conversation_id])
|
||||
current_account.calls.active.where(conversation_id: conversation.id).order(created_at: :desc).first!
|
||||
else
|
||||
current_account.conversations.find_by!(identifier: twilio_call_sid)
|
||||
find_call!(twilio_call_sid)
|
||||
end
|
||||
end
|
||||
|
||||
def find_call!(sid)
|
||||
current_account.calls.find_by!(provider: :twilio, provider_call_id: sid)
|
||||
end
|
||||
|
||||
def sync_outbound_leg(call_sid:, from_number:, direction:)
|
||||
parent_sid = params['ParentCallSid'].presence
|
||||
lookup_sid = direction == 'outbound-dial' ? parent_sid || call_sid : call_sid
|
||||
conversation = current_account.conversations.find_by!(identifier: lookup_sid)
|
||||
call = find_call!(lookup_sid)
|
||||
|
||||
Voice::CallSessionSyncService.new(
|
||||
conversation: conversation,
|
||||
call_sid: call_sid,
|
||||
message_call_sid: conversation.identifier,
|
||||
call: call,
|
||||
leg_call_sid: call_sid,
|
||||
leg: {
|
||||
from_number: from_number,
|
||||
to_number: twilio_to,
|
||||
direction: 'outbound'
|
||||
}
|
||||
).perform
|
||||
call
|
||||
end
|
||||
|
||||
def ensure_conference_sid!(conversation)
|
||||
def ensure_conference_sid!(call)
|
||||
name = call.meta['conference_sid'].presence || Voice::Conference::Name.for(call)
|
||||
call.update!(meta: call.meta.merge('conference_sid' => name)) if call.meta['conference_sid'].blank?
|
||||
|
||||
conversation = call.conversation
|
||||
attrs = conversation.additional_attributes || {}
|
||||
attrs['conference_sid'] ||= Voice::Conference::Name.for(conversation)
|
||||
conversation.update!(additional_attributes: attrs)
|
||||
attrs['conference_sid']
|
||||
if attrs['conference_sid'] != name
|
||||
attrs['conference_sid'] = name
|
||||
conversation.update!(additional_attributes: attrs)
|
||||
end
|
||||
name
|
||||
end
|
||||
|
||||
def conference_twiml(conference_sid, agent_leg)
|
||||
@@ -155,16 +166,16 @@ class Twilio::VoiceController < ApplicationController
|
||||
Rails.application.routes.url_helpers.twilio_voice_conference_status_url(phone: phone_digits)
|
||||
end
|
||||
|
||||
def find_conversation_for_conference!(friendly_name:, call_sid:)
|
||||
def find_call_for_conference!(friendly_name:, call_sid:)
|
||||
name = friendly_name.to_s
|
||||
scope = current_account.conversations
|
||||
scope = current_account.calls
|
||||
|
||||
if name.present?
|
||||
conversation = scope.where("additional_attributes->>'conference_sid' = ?", name).first
|
||||
return conversation if conversation
|
||||
call = scope.where("meta ->> 'conference_sid' = ?", name).first
|
||||
return call if call
|
||||
end
|
||||
|
||||
scope.find_by!(identifier: call_sid)
|
||||
scope.find_by!(provider: :twilio, provider_call_id: call_sid)
|
||||
end
|
||||
|
||||
def set_inbox!
|
||||
|
||||
@@ -19,16 +19,21 @@ class Voice::CallMessageBuilder
|
||||
|
||||
def perform!
|
||||
validate_sender!
|
||||
message = latest_message
|
||||
message ? update_message!(message) : create_message!
|
||||
existing = existing_message
|
||||
existing ? update_message!(existing) : create_message!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :conversation, :direction, :payload, :user, :timestamps
|
||||
|
||||
def latest_message
|
||||
conversation.messages.voice_calls.order(created_at: :desc).first
|
||||
def existing_message
|
||||
sid = payload[:call_sid] || payload['call_sid']
|
||||
return if sid.blank?
|
||||
|
||||
conversation.messages.voice_calls
|
||||
.where("content_attributes -> 'data' ->> 'call_sid' = ?", sid)
|
||||
.first
|
||||
end
|
||||
|
||||
def update_message!(message)
|
||||
@@ -37,6 +42,7 @@ class Voice::CallMessageBuilder
|
||||
content_attributes: { 'data' => base_payload },
|
||||
sender: sender
|
||||
)
|
||||
message
|
||||
end
|
||||
|
||||
def create_message!
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
class Voice::CallSessionSyncService
|
||||
attr_reader :conversation, :call_sid, :message_call_sid, :from_number, :to_number, :direction
|
||||
attr_reader :call, :leg_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
|
||||
def initialize(call:, leg:, leg_call_sid: nil)
|
||||
@call = call
|
||||
@leg_call_sid = leg_call_sid || call.provider_call_id
|
||||
@from_number = leg[:from_number]
|
||||
@to_number = leg[:to_number]
|
||||
@direction = leg[:direction]
|
||||
@@ -20,16 +19,18 @@ class Voice::CallSessionSyncService
|
||||
sync_voice_call_message!(attrs)
|
||||
end
|
||||
|
||||
conversation
|
||||
call
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
delegate :conversation, to: :call
|
||||
|
||||
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['conference_sid'] ||= call.meta['conference_sid'] || Voice::Conference::Name.for(call)
|
||||
attrs['meta'] ||= {}
|
||||
attrs['meta']['initiated_at'] ||= current_timestamp
|
||||
attrs
|
||||
@@ -40,7 +41,7 @@ class Voice::CallSessionSyncService
|
||||
conversation: conversation,
|
||||
direction: direction,
|
||||
payload: {
|
||||
call_sid: message_call_sid,
|
||||
call_sid: call.provider_call_id,
|
||||
status: attrs['call_status'],
|
||||
conference_sid: attrs['conference_sid'],
|
||||
from_number: origin_number_for(direction),
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
class Voice::CallStatus::Manager
|
||||
pattr_initialize [:conversation!, :call_sid]
|
||||
pattr_initialize [:call!]
|
||||
|
||||
ALLOWED_STATUSES = %w[ringing in-progress completed no-answer failed].freeze
|
||||
TERMINAL_STATUSES = %w[completed no-answer failed].freeze
|
||||
|
||||
# Map dashed statuses (Twilio-native / frontend) to underscored Call model statuses.
|
||||
CALL_MODEL_STATUS = {
|
||||
'ringing' => 'ringing',
|
||||
'in-progress' => 'in_progress',
|
||||
'completed' => 'completed',
|
||||
'no-answer' => 'no_answer',
|
||||
'failed' => '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)
|
||||
@@ -16,6 +23,12 @@ class Voice::CallStatus::Manager
|
||||
|
||||
private
|
||||
|
||||
delegate :conversation, to: :call
|
||||
|
||||
def current_status
|
||||
conversation.additional_attributes&.dig('call_status')
|
||||
end
|
||||
|
||||
def apply_status(status, duration:, timestamp:)
|
||||
attrs = (conversation.additional_attributes || {}).dup
|
||||
attrs['call_status'] = status
|
||||
@@ -31,6 +44,16 @@ class Voice::CallStatus::Manager
|
||||
additional_attributes: attrs,
|
||||
last_activity_at: current_time
|
||||
)
|
||||
|
||||
persist_on_call!(status, attrs)
|
||||
end
|
||||
|
||||
def persist_on_call!(status, attrs)
|
||||
updates = { status: CALL_MODEL_STATUS[status] }
|
||||
updates[:started_at] = Time.zone.at(attrs['call_started_at']) if status == 'in-progress' && attrs['call_started_at']
|
||||
updates[:duration_seconds] = attrs['call_duration'] if TERMINAL_STATUSES.include?(status) && attrs['call_duration']
|
||||
|
||||
call.update!(updates)
|
||||
end
|
||||
|
||||
def resolved_duration(attrs, provided_duration, timestamp)
|
||||
@@ -43,10 +66,7 @@ class Voice::CallStatus::Manager
|
||||
end
|
||||
|
||||
def update_message(status)
|
||||
message = conversation.messages
|
||||
.where(content_type: 'voice_call')
|
||||
.order(created_at: :desc)
|
||||
.first
|
||||
message = call.message || fallback_message
|
||||
return unless message
|
||||
|
||||
data = (message.content_attributes || {}).dup
|
||||
@@ -56,6 +76,13 @@ class Voice::CallStatus::Manager
|
||||
message.update!(content_attributes: data)
|
||||
end
|
||||
|
||||
def fallback_message
|
||||
conversation.messages.voice_calls
|
||||
.where("content_attributes -> 'data' ->> 'call_sid' = ?", call.provider_call_id)
|
||||
.order(created_at: :desc)
|
||||
.first
|
||||
end
|
||||
|
||||
def now_seconds
|
||||
current_time.to_i
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Voice::Conference::Manager
|
||||
pattr_initialize [:conversation!, :event!, :call_sid!, :participant_label]
|
||||
pattr_initialize [:call!, :event!, :participant_label]
|
||||
|
||||
def process
|
||||
case event
|
||||
@@ -17,19 +17,21 @@ class Voice::Conference::Manager
|
||||
|
||||
private
|
||||
|
||||
delegate :conversation, to: :call
|
||||
|
||||
def status_manager
|
||||
@status_manager ||= Voice::CallStatus::Manager.new(
|
||||
conversation: conversation,
|
||||
call_sid: call_sid
|
||||
)
|
||||
@status_manager ||= Voice::CallStatus::Manager.new(call: call)
|
||||
end
|
||||
|
||||
def ensure_conference_sid!
|
||||
attrs = conversation.additional_attributes || {}
|
||||
return if attrs['conference_sid'].present?
|
||||
name = Voice::Conference::Name.for(call)
|
||||
call.update!(meta: call.meta.merge('conference_sid' => name)) if call.meta['conference_sid'].blank?
|
||||
|
||||
attrs['conference_sid'] = Voice::Conference::Name.for(conversation)
|
||||
conversation.update!(additional_attributes: attrs)
|
||||
conv_attrs = (conversation.additional_attributes || {}).dup
|
||||
return if conv_attrs['conference_sid'].present?
|
||||
|
||||
conv_attrs['conference_sid'] = name
|
||||
conversation.update!(additional_attributes: conv_attrs)
|
||||
end
|
||||
|
||||
def mark_ringing!
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module Voice::Conference::Name
|
||||
def self.for(conversation)
|
||||
"conf_account_#{conversation.account_id}_conv_#{conversation.display_id}"
|
||||
def self.for(call)
|
||||
"conf_account_#{call.account_id}_call_#{call.id}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,21 +13,32 @@ class Voice::InboundCallBuilder
|
||||
end
|
||||
|
||||
def perform!
|
||||
existing = find_existing_call
|
||||
return existing.conversation if existing
|
||||
|
||||
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 = find_open_conversation(contact_inbox) || create_conversation!(contact, contact_inbox)
|
||||
conversation.reload
|
||||
update_conversation!(conversation, timestamp)
|
||||
build_voice_message!(conversation, timestamp)
|
||||
|
||||
call = create_call!(conversation, contact, timestamp)
|
||||
message = build_voice_message!(conversation, call, timestamp)
|
||||
call.update!(message_id: message.id)
|
||||
|
||||
denormalize_to_conversation!(conversation, call, timestamp)
|
||||
conversation
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_existing_call
|
||||
account.calls.find_by(provider: :twilio, provider_call_id: call_sid)
|
||||
end
|
||||
|
||||
def ensure_contact!
|
||||
account.contacts.find_or_create_by!(phone_number: from_number) do |record|
|
||||
record.name = from_number if record.name.blank?
|
||||
@@ -43,10 +54,10 @@ class Voice::InboundCallBuilder
|
||||
end
|
||||
end
|
||||
|
||||
def find_conversation
|
||||
return if call_sid.blank?
|
||||
def find_open_conversation(contact_inbox)
|
||||
return unless inbox.lock_to_single_conversation
|
||||
|
||||
account.conversations.includes(:contact).find_by(identifier: call_sid)
|
||||
contact_inbox.conversations.where.not(status: :resolved).order(created_at: :desc).first
|
||||
end
|
||||
|
||||
def create_conversation!(contact, contact_inbox)
|
||||
@@ -54,34 +65,33 @@ class Voice::InboundCallBuilder
|
||||
contact_inbox_id: contact_inbox.id,
|
||||
inbox_id: inbox.id,
|
||||
contact_id: contact.id,
|
||||
status: :open,
|
||||
identifier: call_sid
|
||||
status: :open
|
||||
)
|
||||
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
|
||||
def create_call!(conversation, contact, timestamp)
|
||||
call = account.calls.create!(
|
||||
inbox: inbox,
|
||||
conversation: conversation,
|
||||
contact: contact,
|
||||
provider: :twilio,
|
||||
direction: :incoming,
|
||||
status: 'ringing',
|
||||
provider_call_id: call_sid,
|
||||
meta: { 'initiated_at' => timestamp }
|
||||
)
|
||||
call.update!(meta: call.meta.merge('conference_sid' => Voice::Conference::Name.for(call)))
|
||||
call
|
||||
end
|
||||
|
||||
def build_voice_message!(conversation, timestamp)
|
||||
def build_voice_message!(conversation, call, timestamp)
|
||||
Voice::CallMessageBuilder.perform!(
|
||||
conversation: conversation,
|
||||
direction: 'inbound',
|
||||
payload: {
|
||||
call_sid: call_sid,
|
||||
call_sid: call.provider_call_id,
|
||||
status: 'ringing',
|
||||
conference_sid: conversation.additional_attributes['conference_sid'],
|
||||
conference_sid: call.meta['conference_sid'],
|
||||
from_number: from_number,
|
||||
to_number: inbox.channel&.phone_number
|
||||
},
|
||||
@@ -89,6 +99,20 @@ class Voice::InboundCallBuilder
|
||||
)
|
||||
end
|
||||
|
||||
def denormalize_to_conversation!(conversation, call, timestamp)
|
||||
attrs = (conversation.additional_attributes || {}).merge(
|
||||
'call_direction' => 'inbound',
|
||||
'call_status' => 'ringing',
|
||||
'conference_sid' => call.meta['conference_sid'],
|
||||
'meta' => { 'initiated_at' => timestamp }
|
||||
)
|
||||
|
||||
conversation.update!(
|
||||
additional_attributes: attrs,
|
||||
last_activity_at: current_time
|
||||
)
|
||||
end
|
||||
|
||||
def current_timestamp
|
||||
@current_timestamp ||= current_time.to_i
|
||||
end
|
||||
|
||||
@@ -22,10 +22,13 @@ class Voice::OutboundCallBuilder
|
||||
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)
|
||||
call = create_call!(conversation, call_sid, timestamp)
|
||||
message = build_voice_message!(conversation, call, timestamp)
|
||||
call.update!(message_id: message.id)
|
||||
|
||||
denormalize_to_conversation!(conversation, call, timestamp)
|
||||
{ conversation: conversation, call_sid: call_sid }
|
||||
end
|
||||
end
|
||||
@@ -56,30 +59,30 @@ class Voice::OutboundCallBuilder
|
||||
)[: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
|
||||
def create_call!(conversation, call_sid, timestamp)
|
||||
call = account.calls.create!(
|
||||
inbox: inbox,
|
||||
conversation: conversation,
|
||||
contact: contact,
|
||||
provider: :twilio,
|
||||
direction: :outgoing,
|
||||
status: 'ringing',
|
||||
provider_call_id: call_sid,
|
||||
accepted_by_agent_id: user.id,
|
||||
meta: { 'initiated_at' => timestamp }
|
||||
)
|
||||
call.update!(meta: call.meta.merge('conference_sid' => Voice::Conference::Name.for(call)))
|
||||
call
|
||||
end
|
||||
|
||||
def build_voice_message!(conversation, call_sid, conference_sid, timestamp)
|
||||
def build_voice_message!(conversation, call, timestamp)
|
||||
Voice::CallMessageBuilder.perform!(
|
||||
conversation: conversation,
|
||||
direction: 'outbound',
|
||||
payload: {
|
||||
call_sid: call_sid,
|
||||
call_sid: call.provider_call_id,
|
||||
status: 'ringing',
|
||||
conference_sid: conference_sid,
|
||||
conference_sid: call.meta['conference_sid'],
|
||||
from_number: inbox.channel&.phone_number,
|
||||
to_number: contact.phone_number
|
||||
},
|
||||
@@ -88,6 +91,21 @@ class Voice::OutboundCallBuilder
|
||||
)
|
||||
end
|
||||
|
||||
def denormalize_to_conversation!(conversation, call, timestamp)
|
||||
attrs = (conversation.additional_attributes || {}).merge(
|
||||
'call_direction' => 'outbound',
|
||||
'call_status' => 'ringing',
|
||||
'agent_id' => user.id,
|
||||
'conference_sid' => call.meta['conference_sid'],
|
||||
'meta' => { 'initiated_at' => timestamp }
|
||||
)
|
||||
|
||||
conversation.update!(
|
||||
additional_attributes: attrs,
|
||||
last_activity_at: current_time
|
||||
)
|
||||
end
|
||||
|
||||
def current_timestamp
|
||||
@current_timestamp ||= current_time.to_i
|
||||
end
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
class Voice::Provider::Twilio::ConferenceService
|
||||
pattr_initialize [:conversation!, { twilio_client: nil }]
|
||||
pattr_initialize [:call!, { twilio_client: nil }]
|
||||
|
||||
def ensure_conference_sid
|
||||
existing = conversation.additional_attributes&.dig('conference_sid')
|
||||
existing = call.meta['conference_sid']
|
||||
return existing if existing.present?
|
||||
|
||||
sid = Voice::Conference::Name.for(conversation)
|
||||
merge_attributes('conference_sid' => sid)
|
||||
sid = Voice::Conference::Name.for(call)
|
||||
call.update!(meta: call.meta.merge('conference_sid' => sid))
|
||||
merge_conversation_attributes('conference_sid' => sid)
|
||||
sid
|
||||
end
|
||||
|
||||
def mark_agent_joined(user:)
|
||||
merge_attributes(
|
||||
call.update!(accepted_by_agent_id: user.id)
|
||||
merge_conversation_attributes(
|
||||
'agent_joined' => true,
|
||||
'joined_at' => Time.current.to_i,
|
||||
'joined_by' => { id: user.id, name: user.name }
|
||||
@@ -21,13 +23,15 @@ class Voice::Provider::Twilio::ConferenceService
|
||||
def end_conference
|
||||
twilio_client
|
||||
.conferences
|
||||
.list(friendly_name: Voice::Conference::Name.for(conversation), status: 'in-progress')
|
||||
.list(friendly_name: Voice::Conference::Name.for(call), status: 'in-progress')
|
||||
.each { |conf| twilio_client.conferences(conf.sid).update(status: 'completed') }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def merge_attributes(attrs)
|
||||
delegate :conversation, to: :call
|
||||
|
||||
def merge_conversation_attributes(attrs)
|
||||
current = conversation.additional_attributes || {}
|
||||
conversation.update!(additional_attributes: current.merge(attrs))
|
||||
end
|
||||
|
||||
@@ -19,13 +19,10 @@ class Voice::StatusUpdateService
|
||||
normalized_status = normalize_status(call_status)
|
||||
return if normalized_status.blank?
|
||||
|
||||
conversation = account.conversations.find_by(identifier: call_sid)
|
||||
return unless conversation
|
||||
call = account.calls.find_by(provider: :twilio, provider_call_id: call_sid)
|
||||
return unless call
|
||||
|
||||
Voice::CallStatus::Manager.new(
|
||||
conversation: conversation,
|
||||
call_sid: call_sid
|
||||
).process_status_update(
|
||||
Voice::CallStatus::Manager.new(call: call).process_status_update(
|
||||
normalized_status,
|
||||
duration: payload_duration,
|
||||
timestamp: payload_timestamp
|
||||
|
||||
Reference in New Issue
Block a user