mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-05 21:48:03 +00:00
chore: clean up
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts::BaseController
|
||||||
skip_before_action :authenticate_user!, :set_current_user, only: [:incoming, :conference_status]
|
skip_before_action :authenticate_user!, :set_current_user, only: [:incoming, :conference_status, :call_status]
|
||||||
protect_from_forgery with: :null_session, only: [:incoming, :conference_status]
|
protect_from_forgery with: :null_session, only: [:incoming, :conference_status, :call_status]
|
||||||
before_action :validate_twilio_signature, only: [:incoming]
|
before_action :validate_twilio_signature, only: [:incoming, :call_status]
|
||||||
before_action :handle_options_request, only: [:incoming, :conference_status]
|
before_action :handle_options_request, only: [:incoming, :conference_status, :call_status]
|
||||||
|
|
||||||
# Handle CORS preflight OPTIONS requests
|
# Handle CORS preflight OPTIONS requests
|
||||||
def handle_options_request
|
def handle_options_request
|
||||||
if request.method == "OPTIONS"
|
if request.method == 'OPTIONS'
|
||||||
set_cors_headers
|
set_cors_headers
|
||||||
head :ok
|
head :ok
|
||||||
return true
|
return true
|
||||||
@@ -26,16 +26,10 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
|
|||||||
# Set CORS headers first to ensure they're included
|
# Set CORS headers first to ensure they're included
|
||||||
set_cors_headers
|
set_cors_headers
|
||||||
|
|
||||||
# Log basic request info
|
|
||||||
Rails.logger.info("🔔 INCOMING CALL WEBHOOK: CallSid=#{params['CallSid']} From=#{params['From']} To=#{params['To']}")
|
|
||||||
|
|
||||||
# Process incoming call using service
|
# Process incoming call using service
|
||||||
begin
|
begin
|
||||||
# Ensure account is set properly
|
# Ensure account is set properly
|
||||||
if !Current.account && params[:account_id].present?
|
Current.account = Account.find(params[:account_id]) if !Current.account && params[:account_id].present?
|
||||||
Current.account = Account.find(params[:account_id])
|
|
||||||
Rails.logger.info("👑 Set Current.account to #{Current.account.id}")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Validate required parameters
|
# Validate required parameters
|
||||||
validate_incoming_params
|
validate_incoming_params
|
||||||
@@ -48,53 +42,102 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
|
|||||||
twiml_response = service.process
|
twiml_response = service.process
|
||||||
|
|
||||||
# Return TwiML response
|
# Return TwiML response
|
||||||
Rails.logger.info("✅ INCOMING CALL: Successfully processed")
|
|
||||||
render xml: twiml_response
|
render xml: twiml_response
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
# Log the error with detailed information
|
# Log the error with detailed information
|
||||||
Rails.logger.error("❌ INCOMING CALL ERROR: #{e.message}")
|
Rails.logger.error("Incoming call error: #{e.message}")
|
||||||
Rails.logger.error("❌ BACKTRACE: #{e.backtrace[0..5].join("\n")}")
|
|
||||||
|
|
||||||
# Return friendly error message to caller
|
# Return friendly error message to caller
|
||||||
render_error("We're sorry, but we're experiencing technical difficulties. Please try your call again later.")
|
render_error("We're sorry, but we're experiencing technical difficulties. Please try your call again later.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handle individual call status updates
|
||||||
|
def call_status
|
||||||
|
# Set CORS headers first to ensure they're always included
|
||||||
|
set_cors_headers
|
||||||
|
|
||||||
|
# Return immediately for OPTIONS requests
|
||||||
|
return head :ok if request.method == 'OPTIONS'
|
||||||
|
|
||||||
|
# Process call status updates
|
||||||
|
begin
|
||||||
|
# Set account for local development if needed
|
||||||
|
Current.account = Account.find(params[:account_id]) if !Current.account && params[:account_id].present?
|
||||||
|
|
||||||
|
# Find conversation by CallSid
|
||||||
|
call_sid = params['CallSid']
|
||||||
|
# For dial action callbacks, use DialCallStatus; fallback to CallStatus for other types
|
||||||
|
call_status = params['DialCallStatus'] || params['CallStatus']
|
||||||
|
|
||||||
|
if call_sid.present? && call_status.present?
|
||||||
|
conversation = Current.account.conversations.where("additional_attributes->>'call_sid' = ?", call_sid).first
|
||||||
|
|
||||||
|
if conversation
|
||||||
|
# Use CallStatusManager to handle the status update
|
||||||
|
status_manager = Voice::CallStatus::Manager.new(
|
||||||
|
conversation: conversation,
|
||||||
|
call_sid: call_sid,
|
||||||
|
provider: :twilio
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map Twilio call/dial statuses to our statuses and update
|
||||||
|
case call_status.downcase
|
||||||
|
when 'completed', 'busy', 'failed', 'no-answer', 'canceled'
|
||||||
|
# Standard call status values
|
||||||
|
if conversation.additional_attributes['call_status'] == 'ringing'
|
||||||
|
status_manager.process_status_update('no_answer')
|
||||||
|
else
|
||||||
|
status_manager.process_status_update('ended')
|
||||||
|
end
|
||||||
|
when 'answered'
|
||||||
|
# DialCallStatus: conference calls return 'answered' when successful
|
||||||
|
# No action needed - call continues in conference
|
||||||
|
else
|
||||||
|
# Handle any other dial statuses (busy, no-answer, failed from dial action)
|
||||||
|
if conversation.additional_attributes['call_status'] == 'ringing'
|
||||||
|
status_manager.process_status_update('no_answer')
|
||||||
|
else
|
||||||
|
status_manager.process_status_update('ended')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Call status processed successfully
|
||||||
|
rescue StandardError => e
|
||||||
|
# Log errors but don't affect the response
|
||||||
|
Rails.logger.error("Call status error: #{e.message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Always return a successful response for Twilio
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
# Handle conference status updates
|
# Handle conference status updates
|
||||||
def conference_status
|
def conference_status
|
||||||
# Set CORS headers first to ensure they're always included
|
# Set CORS headers first to ensure they're always included
|
||||||
set_cors_headers
|
set_cors_headers
|
||||||
|
|
||||||
# Return immediately for OPTIONS requests
|
# Return immediately for OPTIONS requests
|
||||||
if request.method == "OPTIONS"
|
return head :ok if request.method == 'OPTIONS'
|
||||||
return head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
# Log basic request info
|
|
||||||
Rails.logger.info("🎧 CONFERENCE STATUS WEBHOOK: ConferenceSid=#{params['ConferenceSid']} Event=#{params['StatusCallbackEvent']}")
|
|
||||||
|
|
||||||
# Process conference status updates using service
|
# Process conference status updates using service
|
||||||
begin
|
begin
|
||||||
# Set account for local development if needed
|
# Set account for local development if needed
|
||||||
if !Current.account && params[:account_id].present?
|
Current.account = Account.find(params[:account_id]) if !Current.account && params[:account_id].present?
|
||||||
Current.account = Account.find(params[:account_id])
|
|
||||||
Rails.logger.info("👑 Set Current.account to #{Current.account.id}")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Validate required parameters
|
# Validate required parameters - need either ConferenceSid or CallSid
|
||||||
if params['ConferenceSid'].blank? && params['CallSid'].blank?
|
return head :ok if params['ConferenceSid'].blank? && params['CallSid'].blank?
|
||||||
Rails.logger.error("❌ MISSING REQUIRED PARAMS: Need either ConferenceSid or CallSid")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Use service to process conference status
|
# Use service to process conference status
|
||||||
service = Voice::ConferenceStatusService.new(account: Current.account, params: params)
|
service = Voice::ConferenceStatusService.new(account: Current.account, params: params)
|
||||||
service.process
|
service.process
|
||||||
|
|
||||||
Rails.logger.info("✅ CONFERENCE STATUS: Successfully processed")
|
# Conference status processed successfully
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
# Log errors but don't affect the response
|
# Log errors but don't affect the response
|
||||||
Rails.logger.error("❌ CONFERENCE STATUS ERROR: #{e.message}")
|
Rails.logger.error("Conference status error: #{e.message}")
|
||||||
Rails.logger.error("❌ BACKTRACE: #{e.backtrace[0..5].join("\n")}")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Always return a successful response for Twilio
|
# Always return a successful response for Twilio
|
||||||
@@ -104,44 +147,35 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
|
|||||||
private
|
private
|
||||||
|
|
||||||
def validate_incoming_params
|
def validate_incoming_params
|
||||||
if params['CallSid'].blank?
|
raise 'Missing required parameter: CallSid' if params['CallSid'].blank?
|
||||||
raise "Missing required parameter: CallSid"
|
|
||||||
end
|
|
||||||
|
|
||||||
if params['From'].blank?
|
raise 'Missing required parameter: From' if params['From'].blank?
|
||||||
raise "Missing required parameter: From"
|
|
||||||
end
|
|
||||||
|
|
||||||
if params['To'].blank?
|
raise 'Missing required parameter: To' if params['To'].blank?
|
||||||
raise "Missing required parameter: To"
|
|
||||||
end
|
|
||||||
|
|
||||||
if Current.account.nil?
|
return unless Current.account.nil?
|
||||||
raise "Current account not set"
|
|
||||||
end
|
raise 'Current account not set'
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_twilio_signature
|
def validate_twilio_signature
|
||||||
begin
|
|
||||||
validator = Voice::TwilioValidatorService.new(
|
validator = Voice::TwilioValidatorService.new(
|
||||||
account: Current.account,
|
account: Current.account,
|
||||||
params: params,
|
params: params,
|
||||||
request: request
|
request: request
|
||||||
)
|
)
|
||||||
|
|
||||||
if !validator.valid?
|
unless validator.valid?
|
||||||
Rails.logger.error("❌ INVALID TWILIO SIGNATURE")
|
|
||||||
render_error('Invalid Twilio signature')
|
render_error('Invalid Twilio signature')
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error("❌ TWILIO VALIDATION ERROR: #{e.message}")
|
Rails.logger.error("Twilio validation error: #{e.message}")
|
||||||
render_error('Error validating Twilio request')
|
render_error('Error validating Twilio request')
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def render_error(message)
|
def render_error(message)
|
||||||
response = Twilio::TwiML::VoiceResponse.new
|
response = Twilio::TwiML::VoiceResponse.new
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
|
|||||||
provider: :twilio)
|
provider: :twilio)
|
||||||
.process_status_update('completed', nil, false, "Call ended by #{current_user.name}")
|
.process_status_update('completed', nil, false, "Call ended by #{current_user.name}")
|
||||||
|
|
||||||
broadcast_status(call_sid, 'completed')
|
|
||||||
render_success('Call successfully ended')
|
render_success('Call successfully ended')
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
render_error("Failed to end call: #{e.message}")
|
render_error("Failed to end call: #{e.message}")
|
||||||
@@ -35,7 +34,6 @@ class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
conference_sid = convo_attr('conference_sid') || create_conference_sid!
|
conference_sid = convo_attr('conference_sid') || create_conference_sid!
|
||||||
update_join_metadata!(call_sid)
|
update_join_metadata!(call_sid)
|
||||||
broadcast_status(call_sid, 'in-progress')
|
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
@@ -158,18 +156,6 @@ class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
|
|||||||
.process_status_update('in_progress', nil, false, "#{current_user.name} joined the call")
|
.process_status_update('in_progress', nil, false, "#{current_user.name} joined the call")
|
||||||
end
|
end
|
||||||
|
|
||||||
def broadcast_status(call_sid, status)
|
|
||||||
ActionCable.server.broadcast "account_#{@conversation.account_id}", {
|
|
||||||
event_name: 'call_status_changed',
|
|
||||||
data: {
|
|
||||||
call_sid: call_sid,
|
|
||||||
status: status,
|
|
||||||
conversation_id: @conversation.display_id,
|
|
||||||
inbox_id: @conversation.inbox_id,
|
|
||||||
timestamp: Time.current.to_i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# ---- TwiML -----------------------------------------------------------------
|
# ---- TwiML -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,26 @@ class VoiceAPI extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// End the client-side WebRTC call connection
|
||||||
|
endClientCall() {
|
||||||
|
try {
|
||||||
|
if (this.activeConnection) {
|
||||||
|
console.log('📞 Ending client WebRTC call connection');
|
||||||
|
this.activeConnection.disconnect();
|
||||||
|
this.activeConnection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.device && this.device.state === 'busy') {
|
||||||
|
console.log('📞 Disconnecting all device connections');
|
||||||
|
this.device.disconnectAll();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Error ending client call:', error);
|
||||||
|
// Clear the connection reference even if disconnect failed
|
||||||
|
this.activeConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get call status
|
// Get call status
|
||||||
getCallStatus(callSid) {
|
getCallStatus(callSid) {
|
||||||
if (!callSid) {
|
if (!callSid) {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const {
|
|||||||
isAWhatsAppChannel,
|
isAWhatsAppChannel,
|
||||||
isAnEmailChannel,
|
isAnEmailChannel,
|
||||||
isAnInstagramChannel,
|
isAnInstagramChannel,
|
||||||
isAVoiceChannel,
|
|
||||||
} = useInbox();
|
} = useInbox();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -42,8 +41,6 @@ const showStatusIndicator = computed(() => {
|
|||||||
if (status.value === MESSAGE_STATUS.FAILED) return false;
|
if (status.value === MESSAGE_STATUS.FAILED) return false;
|
||||||
// Don't show status for deleted messages
|
// Don't show status for deleted messages
|
||||||
if (contentAttributes.value?.deleted) return false;
|
if (contentAttributes.value?.deleted) return false;
|
||||||
// Don't show status for transcription messages
|
|
||||||
if (isAVoiceChannel.value) return false;
|
|
||||||
|
|
||||||
if (messageType.value === MESSAGE_TYPES.OUTGOING) return true;
|
if (messageType.value === MESSAGE_TYPES.OUTGOING) return true;
|
||||||
if (messageType.value === MESSAGE_TYPES.TEMPLATE) return true;
|
if (messageType.value === MESSAGE_TYPES.TEMPLATE) return true;
|
||||||
|
|||||||
@@ -1528,16 +1528,16 @@ export default {
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<!-- Left side with inbox avatar and call info -->
|
<!-- Left side with inbox avatar and call info -->
|
||||||
<div class="inbox-avatar">
|
<div class="inbox-avatar">
|
||||||
<!-- Use the inbox avatar if available -->
|
<!-- Use the inbox avatar if available and valid -->
|
||||||
<img
|
<img
|
||||||
v-if="inboxAvatarUrl"
|
v-if="inboxAvatarUrl && inboxAvatarUrl.startsWith('http')"
|
||||||
:src="inboxAvatarUrl"
|
:src="inboxAvatarUrl"
|
||||||
:alt="inboxDisplayName"
|
:alt="inboxDisplayName"
|
||||||
class="avatar-image"
|
class="avatar-image"
|
||||||
@error="handleAvatarError"
|
@error="handleAvatarError"
|
||||||
/>
|
/>
|
||||||
<!-- Fallback to initial if no avatar -->
|
<!-- Fallback to phone icon for voice channels -->
|
||||||
<span v-else>{{ inboxDisplayName.charAt(0).toUpperCase() }}</span>
|
<i v-else class="i-ri-phone-fill text-white text-lg"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<div class="voice-label">{{ inboxDisplayName }}</div>
|
<div class="voice-label">{{ inboxDisplayName }}</div>
|
||||||
@@ -1707,7 +1707,7 @@ export default {
|
|||||||
<span class="inbox-name">
|
<span class="inbox-name">
|
||||||
<!-- Add inbox avatar to the header -->
|
<!-- Add inbox avatar to the header -->
|
||||||
<img
|
<img
|
||||||
v-if="inboxAvatarUrl"
|
v-if="inboxAvatarUrl && inboxAvatarUrl.startsWith('http')"
|
||||||
:src="inboxAvatarUrl"
|
:src="inboxAvatarUrl"
|
||||||
:alt="inboxDisplayName"
|
:alt="inboxDisplayName"
|
||||||
class="inline-avatar"
|
class="inline-avatar"
|
||||||
@@ -1718,6 +1718,19 @@ export default {
|
|||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<!-- Fallback to phone icon for voice channels -->
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="i-ri-phone-fill text-white mr-1.5"
|
||||||
|
style="
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
"
|
||||||
|
></i>
|
||||||
{{ inboxDisplayName }}
|
{{ inboxDisplayName }}
|
||||||
</span>
|
</span>
|
||||||
<span class="incoming-call-text">Incoming call</span>
|
<span class="incoming-call-text">Incoming call</span>
|
||||||
|
|||||||
@@ -402,7 +402,10 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canReply || this.isAWhatsAppChannel) {
|
// Voice channels only allow private notes
|
||||||
|
if (this.isAVoiceChannel) {
|
||||||
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||||
|
} else if (canReply || this.isAWhatsAppChannel) {
|
||||||
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
||||||
} else {
|
} else {
|
||||||
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||||
@@ -797,7 +800,12 @@ export default {
|
|||||||
this.$store.dispatch('draftMessages/setReplyEditorMode', {
|
this.$store.dispatch('draftMessages/setReplyEditorMode', {
|
||||||
mode,
|
mode,
|
||||||
});
|
});
|
||||||
if (canReply || this.isAWhatsAppChannel) this.replyType = mode;
|
// Voice channels are restricted to private notes only
|
||||||
|
if (this.isAVoiceChannel) {
|
||||||
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||||
|
} else if (canReply || this.isAWhatsAppChannel) {
|
||||||
|
this.replyType = mode;
|
||||||
|
}
|
||||||
if (this.showRichContentEditor) {
|
if (this.showRichContentEditor) {
|
||||||
if (this.isRecordingAudio) {
|
if (this.isRecordingAudio) {
|
||||||
this.toggleAudioRecorder();
|
this.toggleAudioRecorder();
|
||||||
@@ -1225,7 +1233,7 @@ export default {
|
|||||||
:recording-audio-state="recordingAudioState"
|
:recording-audio-state="recordingAudioState"
|
||||||
:send-button-text="replyButtonLabel"
|
:send-button-text="replyButtonLabel"
|
||||||
:show-audio-recorder="showAudioRecorder"
|
:show-audio-recorder="showAudioRecorder"
|
||||||
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
|
:show-editor-toggle="isAPIInbox && !isOnPrivateNote && !isAVoiceChannel"
|
||||||
:show-emoji-picker="showEmojiPicker"
|
:show-emoji-picker="showEmojiPicker"
|
||||||
:show-file-upload="showFileUpload"
|
:show-file-upload="showFileUpload"
|
||||||
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
'conversation.updated': this.onConversationUpdated,
|
'conversation.updated': this.onConversationUpdated,
|
||||||
'account.cache_invalidated': this.onCacheInvalidate,
|
'account.cache_invalidated': this.onCacheInvalidate,
|
||||||
'copilot.message.created': this.onCopilotMessageCreated,
|
'copilot.message.created': this.onCopilotMessageCreated,
|
||||||
|
|
||||||
// Call events
|
|
||||||
incoming_call: this.onIncomingCall,
|
|
||||||
call_status_changed: this.onCallStatusChanged,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +82,40 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
|
|
||||||
onConversationCreated = data => {
|
onConversationCreated = data => {
|
||||||
this.app.$store.dispatch('addConversation', data);
|
this.app.$store.dispatch('addConversation', data);
|
||||||
|
|
||||||
|
// Check if this is a voice channel conversation (incoming call)
|
||||||
|
if (data.meta?.inbox?.channel_type === 'Channel::Voice' || data.channel === 'Channel::Voice') {
|
||||||
|
if (data.additional_attributes?.call_status === 'ringing' &&
|
||||||
|
data.additional_attributes?.call_sid) {
|
||||||
|
|
||||||
|
const normalizedPayload = {
|
||||||
|
callSid: data.additional_attributes.call_sid,
|
||||||
|
conversationId: data.display_id || data.id,
|
||||||
|
inboxId: data.inbox_id,
|
||||||
|
inboxName: data.meta?.inbox?.name,
|
||||||
|
inboxAvatarUrl: data.meta?.inbox?.avatar_url,
|
||||||
|
inboxPhoneNumber: data.meta?.inbox?.phone_number,
|
||||||
|
contactName: data.meta?.sender?.name || 'Unknown Caller',
|
||||||
|
contactId: data.meta?.sender?.id,
|
||||||
|
accountId: data.account_id,
|
||||||
|
isOutbound: data.additional_attributes?.call_direction === 'outbound',
|
||||||
|
conference_sid: data.additional_attributes?.conference_sid,
|
||||||
|
conferenceId: data.additional_attributes?.conference_sid,
|
||||||
|
conferenceSid: data.additional_attributes?.conference_sid,
|
||||||
|
requiresAgentJoin: data.additional_attributes?.requires_agent_join || false,
|
||||||
|
callDirection: data.additional_attributes?.call_direction,
|
||||||
|
phoneNumber: data.meta?.sender?.phone_number,
|
||||||
|
avatarUrl: data.meta?.sender?.avatar_url,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.app.$store.dispatch('calls/setIncomingCall', normalizedPayload);
|
||||||
|
|
||||||
|
if (window.app && window.app.$data) {
|
||||||
|
window.app.$data.showCallWidget = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.fetchConversationStats();
|
this.fetchConversationStats();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,6 +148,17 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
|
|
||||||
onConversationUpdated = data => {
|
onConversationUpdated = data => {
|
||||||
this.app.$store.dispatch('updateConversation', data);
|
this.app.$store.dispatch('updateConversation', data);
|
||||||
|
|
||||||
|
// Check if this conversation update includes call status changes
|
||||||
|
if (data.additional_attributes?.call_status && data.additional_attributes?.call_sid) {
|
||||||
|
this.app.$store.dispatch('calls/handleCallStatusChanged', {
|
||||||
|
callSid: data.additional_attributes.call_sid,
|
||||||
|
status: data.additional_attributes.call_status,
|
||||||
|
conversationId: data.display_id,
|
||||||
|
inboxId: data.inbox_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.fetchConversationStats();
|
this.fetchConversationStats();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -203,53 +244,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
this.app.$store.dispatch('teams/revalidate', { newKey: keys.team });
|
this.app.$store.dispatch('teams/revalidate', { newKey: keys.team });
|
||||||
};
|
};
|
||||||
|
|
||||||
onIncomingCall = data => {
|
|
||||||
// Normalize snake_case to camelCase for consistency with frontend code
|
|
||||||
const normalizedPayload = {
|
|
||||||
callSid: data.call_sid,
|
|
||||||
conversationId: data.conversation_id,
|
|
||||||
inboxId: data.inbox_id,
|
|
||||||
inboxName: data.inbox_name,
|
|
||||||
inboxAvatarUrl: data.inbox_avatar_url,
|
|
||||||
inboxPhoneNumber: data.inbox_phone_number,
|
|
||||||
contactName: data.contact_name || 'Unknown Caller',
|
|
||||||
contactId: data.contact_id,
|
|
||||||
accountId: data.account_id,
|
|
||||||
isOutbound: data.is_outbound || false,
|
|
||||||
// CRITICAL: Use 'conference_sid' in camelCase format to match field names
|
|
||||||
conference_sid: data.conference_sid,
|
|
||||||
conferenceId: data.conference_sid, // Add aliases for consistency
|
|
||||||
conferenceSid: data.conference_sid, // Add aliases for consistency
|
|
||||||
requiresAgentJoin: data.requires_agent_join || false,
|
|
||||||
callDirection: data.call_direction,
|
|
||||||
phoneNumber: data.phone_number,
|
|
||||||
avatarUrl: data.avatar_url,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update store
|
|
||||||
this.app.$store.dispatch('calls/setIncomingCall', normalizedPayload);
|
|
||||||
|
|
||||||
// Also update App.vue showCallWidget directly for immediate UI feedback
|
|
||||||
if (window.app && window.app.$data) {
|
|
||||||
window.app.$data.showCallWidget = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onCallStatusChanged = data => {
|
|
||||||
// Normalize snake_case to camelCase for consistency with frontend code
|
|
||||||
const normalizedPayload = {
|
|
||||||
callSid: data.call_sid,
|
|
||||||
status: data.status,
|
|
||||||
conversationId: data.conversation_id,
|
|
||||||
inboxId: data.inbox_id,
|
|
||||||
timestamp: data.timestamp || Date.now(),
|
|
||||||
};
|
|
||||||
// Only dispatch to Vuex; Vuex handles widget and call state
|
|
||||||
this.app.$store.dispatch(
|
|
||||||
'calls/handleCallStatusChanged',
|
|
||||||
normalizedPayload
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import VoiceAPI from 'dashboard/api/channels/voice';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
activeCall: null,
|
activeCall: null,
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
@@ -13,22 +15,23 @@ const getters = {
|
|||||||
const actions = {
|
const actions = {
|
||||||
handleCallStatusChanged({ state, dispatch }, { callSid, status, conversationId }) {
|
handleCallStatusChanged({ state, dispatch }, { callSid, status, conversationId }) {
|
||||||
const isActiveCall = callSid === state.activeCall?.callSid;
|
const isActiveCall = callSid === state.activeCall?.callSid;
|
||||||
|
const isIncomingCall = callSid === state.incomingCall?.callSid;
|
||||||
const terminalStatuses = ['ended', 'missed', 'completed', 'failed', 'busy', 'no_answer'];
|
const terminalStatuses = ['ended', 'missed', 'completed', 'failed', 'busy', 'no_answer'];
|
||||||
|
|
||||||
// Update conversation status in the conversation list
|
if (terminalStatuses.includes(status)) {
|
||||||
if (conversationId) {
|
if (isActiveCall) {
|
||||||
dispatch('conversations/updateConversationCallStatus', {
|
dispatch('clearActiveCall');
|
||||||
conversationId,
|
} else if (isIncomingCall) {
|
||||||
callStatus: status
|
dispatch('clearIncomingCall');
|
||||||
}, { root: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isActiveCall && terminalStatuses.includes(status)) {
|
// Hide widget for any terminal status if it matches our call
|
||||||
dispatch('clearActiveCall');
|
if (isActiveCall || isIncomingCall) {
|
||||||
if (window.app?.$data) {
|
if (window.app?.$data) {
|
||||||
window.app.$data.showCallWidget = false;
|
window.app.$data.showCallWidget = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setActiveCall({ commit, dispatch, state }, callData) {
|
setActiveCall({ commit, dispatch, state }, callData) {
|
||||||
@@ -48,6 +51,13 @@ const actions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
clearActiveCall({ commit }) {
|
clearActiveCall({ commit }) {
|
||||||
|
// End the WebRTC connection before clearing the call state
|
||||||
|
try {
|
||||||
|
VoiceAPI.endClientCall();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error ending client call during clearActiveCall:', error);
|
||||||
|
}
|
||||||
|
|
||||||
commit('CLEAR_ACTIVE_CALL');
|
commit('CLEAR_ACTIVE_CALL');
|
||||||
if (window.app?.$data) {
|
if (window.app?.$data) {
|
||||||
window.app.$data.showCallWidget = false;
|
window.app.$data.showCallWidget = false;
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ module Voice
|
|||||||
create_activity_message(activity_message_for_status(normalized_status))
|
create_activity_message(activity_message_for_status(normalized_status))
|
||||||
end
|
end
|
||||||
|
|
||||||
broadcast_status_change(normalized_status)
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_outbound?
|
def is_outbound?
|
||||||
direction = conversation.additional_attributes['call_direction']
|
direction = conversation.additional_attributes['call_direction']
|
||||||
return direction == 'outbound' if direction.present?
|
return direction == 'outbound' if direction.present?
|
||||||
|
|
||||||
conversation.additional_attributes['requires_agent_join'] == true
|
conversation.additional_attributes['requires_agent_join'] == true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -78,7 +78,11 @@ module Voice
|
|||||||
conversation.additional_attributes['call_duration'] = duration if duration
|
conversation.additional_attributes['call_duration'] = duration if duration
|
||||||
end
|
end
|
||||||
|
|
||||||
conversation.update!(last_activity_at: Time.current)
|
# Save both additional_attributes changes and last_activity_at
|
||||||
|
conversation.update!(
|
||||||
|
additional_attributes: conversation.additional_attributes,
|
||||||
|
last_activity_at: Time.current
|
||||||
|
)
|
||||||
update_message_status(status, duration)
|
update_message_status(status, duration)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -105,6 +109,7 @@ module Voice
|
|||||||
return 'Call ended' if status == 'ended'
|
return 'Call ended' if status == 'ended'
|
||||||
return 'Missed call' if status == 'missed'
|
return 'Missed call' if status == 'missed'
|
||||||
return 'No answer' if status == 'no_answer'
|
return 'No answer' if status == 'no_answer'
|
||||||
|
|
||||||
'Call ended'
|
'Call ended'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -120,24 +125,6 @@ module Voice
|
|||||||
additional_attributes: additional_attributes
|
additional_attributes: additional_attributes
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def broadcast_status_change(status)
|
|
||||||
ui_status = normalized_ui_status(status)
|
|
||||||
|
|
||||||
ActionCable.server.broadcast(
|
|
||||||
"account_#{conversation.account_id}",
|
|
||||||
{
|
|
||||||
event_name: 'call_status_changed',
|
|
||||||
data: {
|
|
||||||
call_sid: call_sid,
|
|
||||||
status: ui_status,
|
|
||||||
conversation_id: conversation.display_id,
|
|
||||||
inbox_id: conversation.inbox_id,
|
|
||||||
timestamp: Time.now.to_i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,18 +65,18 @@ module Voice
|
|||||||
def handle_agent_join
|
def handle_agent_join
|
||||||
conversation.additional_attributes['agent_joined_at'] = Time.now.to_i
|
conversation.additional_attributes['agent_joined_at'] = Time.now.to_i
|
||||||
|
|
||||||
if ringing_call?
|
return unless ringing_call?
|
||||||
|
|
||||||
call_status_manager.process_status_update('in_progress')
|
call_status_manager.process_status_update('in_progress')
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def handle_caller_join
|
def handle_caller_join
|
||||||
conversation.additional_attributes['caller_joined_at'] = Time.now.to_i
|
conversation.additional_attributes['caller_joined_at'] = Time.now.to_i
|
||||||
|
|
||||||
if outbound_call? && ringing_call?
|
return unless outbound_call? && ringing_call?
|
||||||
|
|
||||||
call_status_manager.process_status_update('in_progress')
|
call_status_manager.process_status_update('in_progress')
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def agent_participant?
|
def agent_participant?
|
||||||
participant_label&.start_with?('agent')
|
participant_label&.start_with?('agent')
|
||||||
|
|||||||
@@ -78,31 +78,8 @@ module Voice
|
|||||||
end
|
end
|
||||||
|
|
||||||
def broadcast_agent_notification(conversation, info)
|
def broadcast_agent_notification(conversation, info)
|
||||||
contact = conversation.contact
|
# This method is no longer needed since conversation.created events
|
||||||
inbox = conversation.inbox
|
# will handle incoming call notifications
|
||||||
|
|
||||||
ActionCable.server.broadcast(
|
|
||||||
"account_#{account.id}",
|
|
||||||
{
|
|
||||||
event: 'incoming_call',
|
|
||||||
data: {
|
|
||||||
call_sid: info[:call_sid],
|
|
||||||
conversation_id: conversation.display_id,
|
|
||||||
inbox_id: conversation.inbox_id,
|
|
||||||
inbox_name: inbox.name,
|
|
||||||
inbox_avatar_url: inbox.avatar_url,
|
|
||||||
inbox_phone_number: inbox.channel.phone_number,
|
|
||||||
contact_name: contact&.name.presence || contact&.phone_number || 'Outbound Call',
|
|
||||||
contact_id: contact&.id,
|
|
||||||
is_outbound: true,
|
|
||||||
account_id: account.id,
|
|
||||||
conference_sid: info[:conference_sid],
|
|
||||||
phone_number: contact&.phone_number,
|
|
||||||
avatar_url: contact&.avatar_url,
|
|
||||||
call_direction: 'outbound'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -6,16 +6,14 @@ module Voice
|
|||||||
find_inbox
|
find_inbox
|
||||||
create_contact
|
create_contact
|
||||||
|
|
||||||
# Use a transaction to ensure the conversation and voice call message are created together
|
# Use a transaction to ensure conversation, message, and call status are all set together
|
||||||
# This ensures the voice call message is created before any auto-assignment activity messages
|
# This ensures only one conversation.created event with complete call data
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
create_conversation
|
create_conversation
|
||||||
create_voice_call_message
|
create_voice_call_message
|
||||||
|
set_initial_call_status
|
||||||
end
|
end
|
||||||
|
|
||||||
# Create activity message separately, after the voice call message
|
|
||||||
create_activity_message
|
|
||||||
|
|
||||||
generate_twiml_response
|
generate_twiml_response
|
||||||
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
@@ -132,68 +130,42 @@ module Voice
|
|||||||
message_params
|
message_params
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
# Broadcast call notification
|
|
||||||
broadcast_call_status
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Create activity message separately after the voice call message
|
# Set initial call status within the transaction
|
||||||
def create_activity_message
|
def set_initial_call_status
|
||||||
# Use CallStatusManager for consistency
|
# Set call status directly on conversation to avoid separate broadcast
|
||||||
status_manager = Voice::CallStatus::Manager.new(
|
@conversation.additional_attributes['call_status'] = 'ringing'
|
||||||
conversation: @conversation,
|
@conversation.additional_attributes['call_started_at'] = Time.now.to_i
|
||||||
call_sid: caller_info[:call_sid],
|
@conversation.save!
|
||||||
provider: :twilio
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process ringing status with custom activity message
|
# Create activity message directly without CallStatusManager broadcast
|
||||||
custom_message = "Incoming call from #{@contact.name.presence || caller_info[:from_number]}"
|
custom_message = "Incoming call from #{@contact.name.presence || caller_info[:from_number]}"
|
||||||
status_manager.process_status_update('ringing', nil, true, custom_message)
|
@conversation.messages.create!(
|
||||||
end
|
account_id: @conversation.account_id,
|
||||||
|
inbox_id: @conversation.inbox_id,
|
||||||
def broadcast_call_status
|
message_type: :activity,
|
||||||
# Get contact name, ensuring we have a valid value
|
content: custom_message,
|
||||||
contact_name_value = @contact.name.presence || caller_info[:from_number]
|
sender: nil
|
||||||
|
|
||||||
# Create the data payload
|
|
||||||
broadcast_data = {
|
|
||||||
call_sid: caller_info[:call_sid],
|
|
||||||
conversation_id: @conversation.display_id,
|
|
||||||
inbox_id: @inbox.id,
|
|
||||||
inbox_name: @inbox.name,
|
|
||||||
inbox_avatar_url: @inbox.avatar_url,
|
|
||||||
inbox_phone_number: @inbox.channel.phone_number,
|
|
||||||
contact_name: contact_name_value,
|
|
||||||
contact_id: @contact.id,
|
|
||||||
account_id: account.id,
|
|
||||||
phone_number: @contact.phone_number,
|
|
||||||
avatar_url: @contact.avatar_url,
|
|
||||||
call_direction: 'inbound',
|
|
||||||
# CRITICAL: Include the conference_sid
|
|
||||||
conference_sid: @conversation.additional_attributes['conference_sid']
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionCable.server.broadcast(
|
|
||||||
"account_#{account.id}",
|
|
||||||
{
|
|
||||||
event: 'incoming_call',
|
|
||||||
data: broadcast_data
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def generate_twiml_response
|
def generate_twiml_response
|
||||||
conference_name = @conversation.additional_attributes['conference_sid']
|
conference_name = @conversation.additional_attributes['conference_sid']
|
||||||
Rails.logger.info("📞 IncomingCallService: Generating TwiML with conference name: #{conference_name}")
|
|
||||||
|
|
||||||
response = Twilio::TwiML::VoiceResponse.new
|
response = Twilio::TwiML::VoiceResponse.new
|
||||||
response.say(message: 'Thank you for calling. Please wait while we connect you with an agent.')
|
response.say(message: 'Thank you for calling. Please wait while we connect you with an agent.')
|
||||||
|
|
||||||
# Setup callback URLs
|
# Setup callback URLs
|
||||||
conference_callback_url = "#{base_url}/api/v1/accounts/#{account.id}/channels/voice/webhooks/conference_status"
|
conference_callback_url = "#{base_url}/api/v1/accounts/#{account.id}/channels/voice/webhooks/conference_status"
|
||||||
Rails.logger.info("📞 IncomingCallService: Setting conference callback to: #{conference_callback_url}")
|
call_status_callback_url = "#{base_url}/api/v1/accounts/#{account.id}/channels/voice/webhooks/call_status"
|
||||||
|
|
||||||
# Now add the caller to the conference
|
# Now add the caller to the conference with call status callback
|
||||||
response.dial do |dial|
|
response.dial(
|
||||||
|
action: call_status_callback_url,
|
||||||
|
method: 'POST'
|
||||||
|
) do |dial|
|
||||||
dial.conference(
|
dial.conference(
|
||||||
conference_name,
|
conference_name,
|
||||||
startConferenceOnEnter: false,
|
startConferenceOnEnter: false,
|
||||||
@@ -208,9 +180,7 @@ module Voice
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
result = response.to_s
|
response.to_s
|
||||||
Rails.logger.info("📞 IncomingCallService: Generated TwiML: #{result}")
|
|
||||||
result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_twiml(_message)
|
def error_twiml(_message)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ module Voice
|
|||||||
# Add the activity message separately, after the voice call message
|
# Add the activity message separately, after the voice call message
|
||||||
create_activity_message
|
create_activity_message
|
||||||
|
|
||||||
broadcast_to_agent
|
|
||||||
@conversation
|
@conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ module Voice
|
|||||||
@conversation.reload
|
@conversation.reload
|
||||||
|
|
||||||
# Log the conversation ID and display_id for debugging
|
# Log the conversation ID and display_id for debugging
|
||||||
Rails.logger.info("🔍 OUTGOING CALL: Created conversation with ID=#{@conversation.id}, display_id=#{@conversation.display_id}")
|
# Conversation created for outgoing call
|
||||||
|
|
||||||
# The conference_sid should be set by the ConversationFinderService, but we double-check
|
# The conference_sid should be set by the ConversationFinderService, but we double-check
|
||||||
@conference_name = @conversation.additional_attributes['conference_sid']
|
@conference_name = @conversation.additional_attributes['conference_sid']
|
||||||
@@ -57,30 +56,30 @@ module Voice
|
|||||||
@conversation.additional_attributes['conference_sid'] = @conference_name
|
@conversation.additional_attributes['conference_sid'] = @conference_name
|
||||||
@conversation.save!
|
@conversation.save!
|
||||||
|
|
||||||
Rails.logger.info("🔧 OUTGOING CALL: Fixed conference name to #{@conference_name}")
|
# Logging removed
|
||||||
else
|
else
|
||||||
Rails.logger.info("✅ OUTGOING CALL: Using existing conference name #{@conference_name}")
|
# Logging removed
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def initiate_call
|
def initiate_call
|
||||||
# Double-check that we have a valid conference name before calling
|
# Double-check that we have a valid conference name before calling
|
||||||
if @conference_name.blank? || !@conference_name.match?(/^conf_account_\d+_conv_\d+$/)
|
if @conference_name.blank? || !@conference_name.match?(/^conf_account_\d+_conv_\d+$/)
|
||||||
Rails.logger.error("❌ OUTGOING CALL: Invalid conference name before initiating call: #{@conference_name}")
|
# Logging removed
|
||||||
|
|
||||||
# Re-generate the conference name as a last resort
|
# Re-generate the conference name as a last resort
|
||||||
@conference_name = "conf_account_#{account.id}_conv_#{@conversation.display_id}"
|
@conference_name = "conf_account_#{account.id}_conv_#{@conversation.display_id}"
|
||||||
Rails.logger.info("🔧 OUTGOING CALL: Re-generated conference name: #{@conference_name}")
|
# Logging removed
|
||||||
|
|
||||||
# Update the conversation with the new conference name
|
# Update the conversation with the new conference name
|
||||||
@conversation.additional_attributes['conference_sid'] = @conference_name
|
@conversation.additional_attributes['conference_sid'] = @conference_name
|
||||||
@conversation.save!
|
@conversation.save!
|
||||||
else
|
else
|
||||||
Rails.logger.info("✅ OUTGOING CALL: Valid conference name: #{@conference_name}")
|
# Logging removed
|
||||||
end
|
end
|
||||||
|
|
||||||
# Log that we're about to initiate the call
|
# Log that we're about to initiate the call
|
||||||
Rails.logger.info("📞 OUTGOING CALL: Initiating call to #{contact.phone_number} with conference #{@conference_name}")
|
# Logging removed
|
||||||
|
|
||||||
# Initiate the call using the channel's implementation
|
# Initiate the call using the channel's implementation
|
||||||
@call_details = @voice_inbox.channel.initiate_call(
|
@call_details = @voice_inbox.channel.initiate_call(
|
||||||
@@ -90,7 +89,7 @@ module Voice
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Log the returned call details for debugging
|
# Log the returned call details for debugging
|
||||||
Rails.logger.info("📞 OUTGOING CALL: Call initiated with details: #{@call_details.inspect}")
|
# Logging removed
|
||||||
|
|
||||||
# Update conversation with call details, but don't set status
|
# Update conversation with call details, but don't set status
|
||||||
# Status will be properly set by CallStatusManager
|
# Status will be properly set by CallStatusManager
|
||||||
@@ -109,7 +108,7 @@ module Voice
|
|||||||
@conversation.update!(additional_attributes: updated_attributes)
|
@conversation.update!(additional_attributes: updated_attributes)
|
||||||
|
|
||||||
# Log the final conversation state
|
# Log the final conversation state
|
||||||
Rails.logger.info("📞 OUTGOING CALL: Conversation updated with call_sid=#{@call_details[:call_sid]}, conference_sid=#{@conference_name}")
|
# Logging removed
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_voice_call_message
|
def create_voice_call_message
|
||||||
@@ -172,37 +171,5 @@ module Voice
|
|||||||
status_manager.process_status_update('initiated', nil, true, custom_message)
|
status_manager.process_status_update('initiated', nil, true, custom_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def broadcast_to_agent
|
|
||||||
# Get contact name, ensuring we have a valid value
|
|
||||||
contact_name_value = contact.name.presence || contact.phone_number
|
|
||||||
|
|
||||||
# Create the data payload
|
|
||||||
broadcast_data = {
|
|
||||||
call_sid: @call_details[:call_sid],
|
|
||||||
conversation_id: @conversation.display_id,
|
|
||||||
inbox_id: @voice_inbox.id,
|
|
||||||
inbox_name: @voice_inbox.name,
|
|
||||||
inbox_avatar_url: @voice_inbox.avatar_url, # Include inbox avatar
|
|
||||||
inbox_phone_number: @voice_inbox.channel.phone_number, # Include inbox phone number
|
|
||||||
contact_name: contact_name_value,
|
|
||||||
contact_id: contact.id,
|
|
||||||
account_id: account.id,
|
|
||||||
is_outbound: true,
|
|
||||||
conference_sid: @conference_name,
|
|
||||||
requires_agent_join: true,
|
|
||||||
call_direction: 'outbound',
|
|
||||||
phone_number: contact.phone_number, # Include phone number for display in the UI
|
|
||||||
avatar_url: contact.avatar_url # Include avatar URL for display in the UI
|
|
||||||
}
|
|
||||||
|
|
||||||
# Direct notification that agent needs to join
|
|
||||||
ActionCable.server.broadcast(
|
|
||||||
"account_#{account.id}",
|
|
||||||
{
|
|
||||||
event: 'incoming_call',
|
|
||||||
data: broadcast_data
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ module Voice
|
|||||||
|
|
||||||
# Log validation result
|
# Log validation result
|
||||||
if is_valid
|
if is_valid
|
||||||
Rails.logger.info("✅ TWILIO VALIDATION: Valid signature confirmed")
|
# Twilio signature validation successful
|
||||||
else
|
else
|
||||||
Rails.logger.error("⚠️ TWILIO VALIDATION: Invalid signature for URL: #{url}")
|
Rails.logger.error("Invalid Twilio signature for URL: #{url}")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error("❌ TWILIO VALIDATION ERROR: #{e.message}")
|
Rails.logger.error("Twilio validation error: #{e.message}")
|
||||||
return true # Allow on errors for robustness
|
return true # Allow on errors for robustness
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ module Voice
|
|||||||
|
|
||||||
def skip_validation?
|
def skip_validation?
|
||||||
# Skip for OPTIONS requests and in development
|
# Skip for OPTIONS requests and in development
|
||||||
return true if request.method == "OPTIONS"
|
return true if request.method == 'OPTIONS'
|
||||||
return true if Rails.env.development?
|
return true if Rails.env.development?
|
||||||
return true if account.blank?
|
return true if account.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ Rails.application.routes.draw do
|
|||||||
collection do
|
collection do
|
||||||
post :incoming
|
post :incoming
|
||||||
match :conference_status, via: [:post, :options] # Allow both POST and OPTIONS
|
match :conference_status, via: [:post, :options] # Allow both POST and OPTIONS
|
||||||
|
match :call_status, via: [:post, :options] # Allow both POST and OPTIONS for call status
|
||||||
match :incoming, via: [:post, :options] # Allow both POST and OPTIONS
|
match :incoming, via: [:post, :options] # Allow both POST and OPTIONS
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user