chore: floating button for incoming call
This commit is contained in:
@@ -61,6 +61,16 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
|
||||
}
|
||||
).perform
|
||||
|
||||
# Broadcast incoming call notification on account-level channel
|
||||
broadcast_call_status('incoming_call', {
|
||||
call_sid: call_sid,
|
||||
conversation_id: conversation.id,
|
||||
inbox_id: inbox.id,
|
||||
inbox_name: inbox.name,
|
||||
contact_name: contact.name || from_number,
|
||||
contact_id: contact.id
|
||||
}, account_id: inbox.account_id)
|
||||
|
||||
# Generate minimal TwiML response
|
||||
response = Twilio::TwiML::VoiceResponse.new
|
||||
response.say(message: 'Thank you for calling. An agent will be with you shortly.')
|
||||
@@ -144,18 +154,12 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
|
||||
}
|
||||
).perform
|
||||
|
||||
# Broadcast minimal update to frontend
|
||||
ActionCable.server.broadcast(
|
||||
"#{conversation.account_id}_#{conversation.inbox_id}",
|
||||
{
|
||||
event_name: 'call_status_changed',
|
||||
data: {
|
||||
call_sid: call_sid,
|
||||
status: conversation.additional_attributes['call_status'] || 'in-progress',
|
||||
conversation_id: conversation.id
|
||||
}
|
||||
}
|
||||
)
|
||||
# Broadcast call status updates on account-level channel
|
||||
broadcast_call_status('call_status_changed', {
|
||||
call_sid: call_sid,
|
||||
status: conversation.additional_attributes['call_status'] || 'in-progress',
|
||||
conversation_id: conversation.id
|
||||
}, account_id: conversation.account_id)
|
||||
|
||||
# Return minimal response
|
||||
head :ok
|
||||
@@ -210,4 +214,17 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', "https://#{request.host_with_port}")
|
||||
end
|
||||
|
||||
def broadcast_call_status(event_name, data, account_id:)
|
||||
# Include account_id in the data to help with validation
|
||||
data_with_account = data.merge(account_id: account_id)
|
||||
|
||||
ActionCable.server.broadcast(
|
||||
"account_#{account_id}",
|
||||
{
|
||||
event: event_name,
|
||||
data: data_with_account
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_conversation, only: [:end_call, :call_status]
|
||||
before_action :fetch_conversation, only: [:end_call, :join_call, :reject_call, :call_status]
|
||||
|
||||
def end_call
|
||||
call_sid = params[:call_sid] || @conversation.additional_attributes&.dig('call_sid')
|
||||
@@ -53,6 +53,121 @@ class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
end
|
||||
|
||||
def join_call
|
||||
call_sid = params[:call_sid] || @conversation.additional_attributes&.dig('call_sid')
|
||||
return render json: { error: 'No active call found' }, status: :not_found unless call_sid
|
||||
|
||||
# Get the inbox and channel information
|
||||
inbox = @conversation.inbox
|
||||
channel = inbox&.channel
|
||||
|
||||
if channel.is_a?(Channel::Voice) && channel.provider == 'twilio'
|
||||
config = channel.provider_config_hash
|
||||
|
||||
begin
|
||||
# Create a Twilio client
|
||||
client = Twilio::REST::Client.new(config['account_sid'], config['auth_token'])
|
||||
|
||||
# Get the conference SID from the conversation attributes
|
||||
# For incoming calls, Twilio typically places the caller in a conference that agents can join
|
||||
conference_sid = @conversation.additional_attributes&.dig('conference_sid')
|
||||
|
||||
if conference_sid
|
||||
# Create a call that connects the agent to the conference
|
||||
client.calls.create(
|
||||
to: current_user.phone_number || config['agent_phone_number'],
|
||||
from: channel.phone_number,
|
||||
status_callback: "#{ENV.fetch('FRONTEND_URL', nil)}/twilio/voice/status_callback",
|
||||
status_callback_event: ['initiated', 'ringing', 'answered', 'completed'],
|
||||
status_callback_method: 'POST',
|
||||
url: "#{ENV.fetch('FRONTEND_URL', nil)}/twilio/voice/twiml?conference_sid=#{conference_sid}&agent_id=#{current_user.id}"
|
||||
)
|
||||
|
||||
# Update conversation to show agent joined
|
||||
@conversation.additional_attributes ||= {}
|
||||
@conversation.additional_attributes['agent_joined'] = true
|
||||
@conversation.additional_attributes['joined_at'] = Time.now.to_i
|
||||
@conversation.additional_attributes['joined_by'] = {
|
||||
id: current_user.id,
|
||||
name: current_user.name
|
||||
}
|
||||
@conversation.save!
|
||||
|
||||
# Create an activity message noting the agent joined
|
||||
Messages::MessageBuilder.new(
|
||||
nil,
|
||||
@conversation,
|
||||
{
|
||||
content: "#{current_user.name} joined the call",
|
||||
message_type: :activity,
|
||||
additional_attributes: {
|
||||
call_sid: call_sid,
|
||||
conference_sid: conference_sid,
|
||||
joined_by: current_user.name,
|
||||
joined_at: Time.now.to_i
|
||||
}
|
||||
}
|
||||
).perform
|
||||
|
||||
render json: {
|
||||
status: 'success',
|
||||
message: 'Agent joining call',
|
||||
conference_sid: conference_sid
|
||||
}
|
||||
else
|
||||
render json: { error: 'Conference not found for this call' }, status: :unprocessable_entity
|
||||
end
|
||||
rescue Twilio::REST::RestError => e
|
||||
render json: { error: "Failed to join call: #{e.message}" }, status: :internal_server_error
|
||||
end
|
||||
else
|
||||
render json: { error: 'Unsupported channel provider for call control' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def reject_call
|
||||
call_sid = params[:call_sid] || @conversation.additional_attributes&.dig('call_sid')
|
||||
return render json: { error: 'No active call found' }, status: :not_found unless call_sid
|
||||
|
||||
# Get the inbox and channel information
|
||||
inbox = @conversation.inbox
|
||||
channel = inbox&.channel
|
||||
|
||||
if channel.is_a?(Channel::Voice) && channel.provider == 'twilio'
|
||||
# Update conversation to show agent rejected call
|
||||
@conversation.additional_attributes ||= {}
|
||||
@conversation.additional_attributes['agent_rejected'] = true
|
||||
@conversation.additional_attributes['rejected_at'] = Time.now.to_i
|
||||
@conversation.additional_attributes['rejected_by'] = {
|
||||
id: current_user.id,
|
||||
name: current_user.name
|
||||
}
|
||||
@conversation.save!
|
||||
|
||||
# Create an activity message noting the agent rejected the call
|
||||
Messages::MessageBuilder.new(
|
||||
nil,
|
||||
@conversation,
|
||||
{
|
||||
content: "#{current_user.name} declined to answer",
|
||||
message_type: :activity,
|
||||
additional_attributes: {
|
||||
call_sid: call_sid,
|
||||
rejected_by: current_user.name,
|
||||
rejected_at: Time.now.to_i
|
||||
}
|
||||
}
|
||||
).perform
|
||||
|
||||
render json: {
|
||||
status: 'success',
|
||||
message: 'Call rejected by agent'
|
||||
}
|
||||
else
|
||||
render json: { error: 'Unsupported channel provider for call control' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def call_status
|
||||
call_sid = @conversation.additional_attributes&.dig('call_sid')
|
||||
return render json: { error: 'No call found' }, status: :not_found unless call_sid
|
||||
|
||||
Reference in New Issue
Block a user