mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
refactor: organize voice conference services
This commit is contained in:
@@ -31,7 +31,7 @@ class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
conference_sid = conversation.additional_attributes&.dig('conference_sid')
|
conference_sid = conversation.additional_attributes&.dig('conference_sid')
|
||||||
if conference_sid.blank?
|
if conference_sid.blank?
|
||||||
conference_sid = Voice::ConferenceSid.friendly_name(conversation)
|
conference_sid = Voice::Conference::Name.for(conversation)
|
||||||
conversation.update!(
|
conversation.update!(
|
||||||
additional_attributes:
|
additional_attributes:
|
||||||
(conversation.additional_attributes || {}).merge('conference_sid' => conference_sid)
|
(conversation.additional_attributes || {}).merge('conference_sid' => conference_sid)
|
||||||
@@ -59,7 +59,7 @@ class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
|
|||||||
def conference_leave
|
def conference_leave
|
||||||
conversation = fetch_conversation_by_display_id
|
conversation = fetch_conversation_by_display_id
|
||||||
# End the conference when an agent leaves from the app
|
# End the conference when an agent leaves from the app
|
||||||
Voice::ConferenceEndService.new(conversation: conversation).perform
|
Voice::Conference::EndService.new(conversation: conversation).perform
|
||||||
render json: { status: 'success', conversation_id: conversation.display_id }
|
render json: { status: 'success', conversation_id: conversation.display_id }
|
||||||
rescue ActiveRecord::RecordNotFound => e
|
rescue ActiveRecord::RecordNotFound => e
|
||||||
render json: { error: 'conversation_not_found', code: 'not_found', details: e.message }, status: :not_found
|
render json: { error: 'conversation_not_found', code: 'not_found', details: e.message }, status: :not_found
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class Twilio::VoiceController < ApplicationController
|
|||||||
return render xml: fallback.to_s
|
return render xml: fallback.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
conference_sid = Voice::ConferenceSid.friendly_name(conversation)
|
conference_sid = Voice::Conference::Name.for(conversation)
|
||||||
Rails.logger.info("TWILIO_VOICE_TWIML_CONFERENCE account=#{account.id} conference_sid=#{conference_sid}")
|
Rails.logger.info("TWILIO_VOICE_TWIML_CONFERENCE account=#{account.id} conference_sid=#{conference_sid}")
|
||||||
|
|
||||||
response = Twilio::TwiML::VoiceResponse.new
|
response = Twilio::TwiML::VoiceResponse.new
|
||||||
@@ -129,7 +129,7 @@ class Twilio::VoiceController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Voice::ConferenceManagerService.new(
|
Voice::Conference::Manager.new(
|
||||||
conversation: conversation,
|
conversation: conversation,
|
||||||
event: mapped,
|
event: mapped,
|
||||||
call_sid: call_sid,
|
call_sid: call_sid,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Voice::BaseCallBuilder
|
|||||||
attrs = conversation.additional_attributes || {}
|
attrs = conversation.additional_attributes || {}
|
||||||
return if attrs['conference_sid'].present?
|
return if attrs['conference_sid'].present?
|
||||||
|
|
||||||
attrs['conference_sid'] = Voice::ConferenceSid.friendly_name(conversation)
|
attrs['conference_sid'] = Voice::Conference::Name.for(conversation)
|
||||||
conversation.update!(additional_attributes: attrs)
|
conversation.update!(additional_attributes: attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Voice::CallSessionSyncService
|
|||||||
# We always persist the human-readable conference friendly name. Twilio's
|
# We always persist the human-readable conference friendly name. Twilio's
|
||||||
# opaque ConferenceSid is stored separately (see controller callbacks) and
|
# opaque ConferenceSid is stored separately (see controller callbacks) and
|
||||||
# is only used to correlate webhook payloads.
|
# is only used to correlate webhook payloads.
|
||||||
attrs['conference_sid'] = Voice::ConferenceSid.friendly_name(conversation)
|
attrs['conference_sid'] = Voice::Conference::Name.for(conversation)
|
||||||
conversation.update!(additional_attributes: attrs)
|
conversation.update!(additional_attributes: attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ class Voice::CallSessionSyncService
|
|||||||
conversation: conversation,
|
conversation: conversation,
|
||||||
direction: current_direction,
|
direction: current_direction,
|
||||||
call_sid: message_call_sid,
|
call_sid: message_call_sid,
|
||||||
conference_sid: attrs['conference_sid'] || Voice::ConferenceSid.friendly_name(conversation),
|
conference_sid: attrs['conference_sid'] || Voice::Conference::Name.for(conversation),
|
||||||
from_number: origin_number_for(current_direction),
|
from_number: origin_number_for(current_direction),
|
||||||
to_number: target_number_for(current_direction),
|
to_number: target_number_for(current_direction),
|
||||||
user: agent_for(attrs)
|
user: agent_for(attrs)
|
||||||
|
|||||||
29
enterprise/app/services/voice/conference/end_service.rb
Normal file
29
enterprise/app/services/voice/conference/end_service.rb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
module Voice
|
||||||
|
module Conference
|
||||||
|
class EndService
|
||||||
|
pattr_initialize [:conversation!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
name = Voice::Conference::Name.for(conversation)
|
||||||
|
|
||||||
|
cfg = conversation.inbox.channel.provider_config_hash
|
||||||
|
account_sid = cfg['account_sid']
|
||||||
|
auth_token = cfg['auth_token']
|
||||||
|
return if account_sid.blank? || auth_token.blank?
|
||||||
|
|
||||||
|
client = ::Twilio::REST::Client.new(account_sid, auth_token)
|
||||||
|
client.conferences.list(friendly_name: name, status: 'in-progress').each do |conf|
|
||||||
|
begin
|
||||||
|
client.conferences(conf.sid).update(status: 'completed')
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error("VOICE_CONFERENCE_END_UPDATE_ERROR conf=#{conf.sid} error=#{e.class}: #{e.message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error(
|
||||||
|
"VOICE_CONFERENCE_END_ERROR account=#{conversation.account_id} conversation=#{conversation.display_id} name=#{name} error=#{e.class}: #{e.message}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
74
enterprise/app/services/voice/conference/manager.rb
Normal file
74
enterprise/app/services/voice/conference/manager.rb
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
module Voice
|
||||||
|
module Conference
|
||||||
|
class Manager
|
||||||
|
pattr_initialize [:conversation!, :event!, :call_sid!, :participant_label]
|
||||||
|
|
||||||
|
EVENT_HANDLERS = {
|
||||||
|
'start' => :handle_start,
|
||||||
|
'end' => :handle_end,
|
||||||
|
'join' => :handle_join,
|
||||||
|
'leave' => :handle_leave
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
END_STATUS = {
|
||||||
|
'in-progress' => 'completed',
|
||||||
|
'ringing' => 'no-answer'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def process
|
||||||
|
handler = EVENT_HANDLERS[event]
|
||||||
|
return unless handler
|
||||||
|
|
||||||
|
send(handler)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def call_status_manager
|
||||||
|
@call_status_manager ||= Voice::CallStatus::Manager.new(
|
||||||
|
conversation: conversation,
|
||||||
|
call_sid: call_sid,
|
||||||
|
provider: :twilio
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_start
|
||||||
|
return if %w[in-progress completed].include?(current_status)
|
||||||
|
|
||||||
|
call_status_manager.process_status_update('ringing')
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_end
|
||||||
|
target_status = END_STATUS[current_status] || 'completed'
|
||||||
|
call_status_manager.process_status_update(target_status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_join
|
||||||
|
return unless mark_in_progress?
|
||||||
|
|
||||||
|
call_status_manager.process_status_update('in-progress')
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_leave
|
||||||
|
return unless %w[in-progress ringing].include?(current_status)
|
||||||
|
|
||||||
|
next_status = current_status == 'ringing' ? 'no-answer' : 'completed'
|
||||||
|
call_status_manager.process_status_update(next_status)
|
||||||
|
|
||||||
|
return if conversation.additional_attributes['call_ended_at'].present?
|
||||||
|
|
||||||
|
Voice::Conference::EndService.new(conversation: conversation).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_status
|
||||||
|
conversation.additional_attributes['call_status']
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_in_progress?
|
||||||
|
conversation.additional_attributes['call_ended_at'].blank? &&
|
||||||
|
current_status == 'ringing' &&
|
||||||
|
participant_label.to_s.start_with?('agent')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
9
enterprise/app/services/voice/conference/name.rb
Normal file
9
enterprise/app/services/voice/conference/name.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module Voice
|
||||||
|
module Conference
|
||||||
|
module Name
|
||||||
|
def self.for(conversation)
|
||||||
|
"conf_account_#{conversation.account_id}_conv_#{conversation.display_id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
module Voice
|
|
||||||
class ConferenceEndService
|
|
||||||
pattr_initialize [:conversation!]
|
|
||||||
|
|
||||||
def perform
|
|
||||||
# Compute conference friendly name from readable conversation ID
|
|
||||||
name = Voice::ConferenceSid.friendly_name(conversation)
|
|
||||||
|
|
||||||
cfg = conversation.inbox.channel.provider_config_hash
|
|
||||||
client = ::Twilio::REST::Client.new(cfg['account_sid'], cfg['auth_token'])
|
|
||||||
# Find all in-progress conferences matching this friendly name and end them
|
|
||||||
client.conferences.list(friendly_name: name, status: 'in-progress').each do |conf|
|
|
||||||
begin
|
|
||||||
client.conferences(conf.sid).update(status: 'completed')
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error("VOICE_CONFERENCE_END_UPDATE_ERROR conf=#{conf.sid} error=#{e.class}: #{e.message}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error(
|
|
||||||
"VOICE_CONFERENCE_END_ERROR account=#{conversation.account_id} conversation=#{conversation.display_id} name=#{name} error=#{e.class}: #{e.message}"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
module Voice
|
|
||||||
class ConferenceManagerService
|
|
||||||
pattr_initialize [:conversation!, :event!, :call_sid!, :participant_label]
|
|
||||||
|
|
||||||
def process
|
|
||||||
case event
|
|
||||||
when 'start'
|
|
||||||
handle_conference_start
|
|
||||||
when 'end'
|
|
||||||
handle_conference_end
|
|
||||||
when 'join'
|
|
||||||
handle_any_participant_join
|
|
||||||
when 'leave'
|
|
||||||
handle_any_participant_leave
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def call_status_manager
|
|
||||||
@call_status_manager ||= Voice::CallStatus::Manager.new(
|
|
||||||
conversation: conversation,
|
|
||||||
call_sid: call_sid,
|
|
||||||
provider: :twilio
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_conference_start
|
|
||||||
current_status = conversation.additional_attributes['call_status']
|
|
||||||
return if %w[in-progress completed].include?(current_status)
|
|
||||||
call_status_manager.process_status_update('ringing')
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_conference_end
|
|
||||||
current_status = conversation.additional_attributes['call_status']
|
|
||||||
if current_status == 'in-progress'
|
|
||||||
call_status_manager.process_status_update('completed')
|
|
||||||
elsif current_status == 'ringing'
|
|
||||||
call_status_manager.process_status_update('no-answer')
|
|
||||||
else
|
|
||||||
call_status_manager.process_status_update('completed')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Treat any participant join as moving to in-progress from ringing
|
|
||||||
def handle_any_participant_join
|
|
||||||
return if conversation.additional_attributes['call_ended_at'].present?
|
|
||||||
return unless conversation.additional_attributes['call_status'] == 'ringing'
|
|
||||||
return unless participant_label.to_s.start_with?('agent')
|
|
||||||
|
|
||||||
call_status_manager.process_status_update('in-progress')
|
|
||||||
end
|
|
||||||
|
|
||||||
# If a participant leaves while in-progress, mark completed; if still ringing, mark no-answer
|
|
||||||
def handle_any_participant_leave
|
|
||||||
status = conversation.additional_attributes['call_status']
|
|
||||||
if status == 'in-progress'
|
|
||||||
call_status_manager.process_status_update('completed')
|
|
||||||
elsif status == 'ringing'
|
|
||||||
call_status_manager.process_status_update('no-answer')
|
|
||||||
end
|
|
||||||
|
|
||||||
# Proactively end the conference when any participant leaves
|
|
||||||
Voice::ConferenceEndService.new(conversation: conversation).perform
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module Voice
|
|
||||||
module ConferenceSid
|
|
||||||
def self.friendly_name(conversation)
|
|
||||||
"conf_account_#{conversation.account_id}_conv_#{conversation.display_id}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user