mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 18:22:53 +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
|
||||
conference_sid = conversation.additional_attributes&.dig('conference_sid')
|
||||
if conference_sid.blank?
|
||||
conference_sid = Voice::ConferenceSid.friendly_name(conversation)
|
||||
conference_sid = Voice::Conference::Name.for(conversation)
|
||||
conversation.update!(
|
||||
additional_attributes:
|
||||
(conversation.additional_attributes || {}).merge('conference_sid' => conference_sid)
|
||||
@@ -59,7 +59,7 @@ class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
|
||||
def conference_leave
|
||||
conversation = fetch_conversation_by_display_id
|
||||
# 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 }
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
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
|
||||
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}")
|
||||
|
||||
response = Twilio::TwiML::VoiceResponse.new
|
||||
@@ -129,7 +129,7 @@ class Twilio::VoiceController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
Voice::ConferenceManagerService.new(
|
||||
Voice::Conference::Manager.new(
|
||||
conversation: conversation,
|
||||
event: mapped,
|
||||
call_sid: call_sid,
|
||||
|
||||
@@ -41,7 +41,7 @@ class Voice::BaseCallBuilder
|
||||
attrs = conversation.additional_attributes || {}
|
||||
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)
|
||||
end
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class Voice::CallSessionSyncService
|
||||
# We always persist the human-readable conference friendly name. Twilio's
|
||||
# opaque ConferenceSid is stored separately (see controller callbacks) and
|
||||
# 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)
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ class Voice::CallSessionSyncService
|
||||
conversation: conversation,
|
||||
direction: current_direction,
|
||||
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),
|
||||
to_number: target_number_for(current_direction),
|
||||
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