2025-08-06 15:25:47 -07:00
< ? php
/**
* Webhook handler class
*/
class TWP_Webhooks {
2025-08-11 20:31:48 -07:00
/**
* Constructor - ensure Twilio SDK is loaded
*/
public function __construct () {
// Load Twilio SDK if not already loaded
if ( ! class_exists ( '\Twilio\Rest\Client' )) {
$autoloader_path = plugin_dir_path ( dirname ( __FILE__ )) . 'vendor/autoload.php' ;
if ( file_exists ( $autoloader_path )) {
require_once $autoloader_path ;
}
}
}
2025-08-06 15:25:47 -07:00
/**
* Register webhook endpoints
*/
public function register_endpoints () {
// Register REST API endpoints for webhooks
add_action ( 'rest_api_init' , function () {
// Voice webhook
register_rest_route ( 'twilio-webhook/v1' , '/voice' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_voice_webhook' ),
'permission_callback' => '__return_true'
));
// SMS webhook
register_rest_route ( 'twilio-webhook/v1' , '/sms' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_sms_webhook' ),
'permission_callback' => '__return_true'
));
// Status webhook
register_rest_route ( 'twilio-webhook/v1' , '/status' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_status_webhook' ),
'permission_callback' => '__return_true'
));
// IVR response webhook
register_rest_route ( 'twilio-webhook/v1' , '/ivr-response' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_ivr_response' ),
'permission_callback' => '__return_true'
));
// Queue wait webhook
register_rest_route ( 'twilio-webhook/v1' , '/queue-wait' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_queue_wait' ),
'permission_callback' => '__return_true'
));
2025-08-11 20:31:48 -07:00
// Queue action webhook (for enqueue/dequeue events)
register_rest_route ( 'twilio-webhook/v1' , '/queue-action' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_queue_action' ),
'permission_callback' => '__return_true'
));
2025-08-06 15:25:47 -07:00
// Voicemail callback webhook
register_rest_route ( 'twilio-webhook/v1' , '/voicemail-callback' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_voicemail_callback' ),
'permission_callback' => '__return_true'
));
2025-08-11 20:31:48 -07:00
// Voicemail complete webhook (after recording)
register_rest_route ( 'twilio-webhook/v1' , '/voicemail-complete' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_voicemail_complete' ),
'permission_callback' => '__return_true'
));
// Agent call status webhook (detect voicemail/no-answer)
register_rest_route ( 'twilio-webhook/v1' , '/agent-call-status' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_agent_call_status' ),
'permission_callback' => '__return_true'
));
2025-08-12 07:05:47 -07:00
// Browser phone voice webhook
register_rest_route ( 'twilio-webhook/v1' , '/browser-voice' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_browser_voice' ),
'permission_callback' => '__return_true'
));
// Browser phone fallback webhook
register_rest_route ( 'twilio-webhook/v1' , '/browser-fallback' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_browser_fallback' ),
'permission_callback' => '__return_true'
));
// Smart routing webhook (checks user preference)
register_rest_route ( 'twilio-webhook/v1' , '/smart-routing' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_smart_routing' ),
'permission_callback' => '__return_true'
));
// Smart routing fallback webhook
register_rest_route ( 'twilio-webhook/v1' , '/smart-routing-fallback' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_smart_routing_fallback' ),
'permission_callback' => '__return_true'
));
2025-08-11 20:31:48 -07:00
// Agent screening webhook (screen agent before connecting)
register_rest_route ( 'twilio-webhook/v1' , '/agent-screen' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_agent_screen' ),
'permission_callback' => '__return_true'
));
// Agent confirmation webhook (after agent presses key)
register_rest_route ( 'twilio-webhook/v1' , '/agent-confirm' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_agent_confirm' ),
'permission_callback' => '__return_true'
));
// Voicemail audio proxy endpoint
register_rest_route ( 'twilio-webhook/v1' , '/voicemail-audio/(?P<id>\d+)' , array (
'methods' => 'GET' ,
'callback' => array ( $this , 'proxy_voicemail_audio' ),
'permission_callback' => function () {
// Check if user is logged in with proper permissions
return is_user_logged_in () && current_user_can ( 'manage_options' );
},
'args' => array (
'id' => array (
'validate_callback' => function ( $param , $request , $key ) {
return is_numeric ( $param );
}
),
),
));
2025-08-06 15:25:47 -07:00
// Transcription webhook
register_rest_route ( 'twilio-webhook/v1' , '/transcription' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_transcription_webhook' ),
'permission_callback' => '__return_true'
));
// Callback choice webhook
register_rest_route ( 'twilio-webhook/v1' , '/callback-choice' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_callback_choice' ),
'permission_callback' => '__return_true'
));
// Request callback webhook
register_rest_route ( 'twilio-webhook/v1' , '/request-callback' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_request_callback' ),
'permission_callback' => '__return_true'
));
// Callback agent webhook
register_rest_route ( 'twilio-webhook/v1' , '/callback-agent' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_callback_agent' ),
'permission_callback' => '__return_true'
));
// Callback customer webhook
register_rest_route ( 'twilio-webhook/v1' , '/callback-customer' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_callback_customer' ),
'permission_callback' => '__return_true'
));
// Outbound agent webhook
register_rest_route ( 'twilio-webhook/v1' , '/outbound-agent' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_outbound_agent' ),
'permission_callback' => '__return_true'
));
// Ring group result webhook
register_rest_route ( 'twilio-webhook/v1' , '/ring-group-result' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_ring_group_result' ),
'permission_callback' => '__return_true'
));
// Agent connect webhook
register_rest_route ( 'twilio-webhook/v1' , '/agent-connect' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_agent_connect' ),
'permission_callback' => '__return_true'
));
// Outbound agent with from number webhook
register_rest_route ( 'twilio-webhook/v1' , '/outbound-agent-with-from' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_outbound_agent_with_from' ),
'permission_callback' => '__return_true'
));
});
}
/**
* Handle webhook requests ( deprecated - using REST API now )
*/
public function handle_webhook () {
// This method is deprecated and no longer used
// Webhooks are now handled via REST API endpoints
}
/**
* Send TwiML response
*/
private function send_twiml_response ( $twiml ) {
2025-08-11 20:31:48 -07:00
// Send raw XML response for Twilio
header ( 'Content-Type: text/xml; charset=utf-8' );
echo $twiml ;
exit ;
2025-08-06 15:25:47 -07:00
}
2025-08-12 07:05:47 -07:00
/**
* Handle browser phone voice webhook
*/
public function handle_browser_voice ( $request ) {
$params = $request -> get_params ();
$call_data = array (
'CallSid' => isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ,
'From' => isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ,
'To' => isset ( $params [ 'To' ]) ? $params [ 'To' ] : '' ,
'CallStatus' => isset ( $params [ 'CallStatus' ]) ? $params [ 'CallStatus' ] : ''
);
// Log the browser call
TWP_Call_Logger :: log_call ( array (
'call_sid' => $call_data [ 'CallSid' ],
'from_number' => $call_data [ 'From' ],
'to_number' => $call_data [ 'To' ],
'status' => 'browser_call_initiated' ,
'actions_taken' => 'Browser phone call initiated'
));
// For outbound calls from browser, handle caller ID properly
$twiml = '<?xml version="1.0" encoding="UTF-8"?>' ;
$twiml .= '<Response>' ;
if ( isset ( $params [ 'To' ]) && ! empty ( $params [ 'To' ])) {
$to_number = $params [ 'To' ];
$from_number = isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ;
// If it's an outgoing call to a phone number
if ( strpos ( $to_number , 'client:' ) !== 0 ) {
$twiml .= '<Dial timeout="30"' ;
// Add caller ID if provided
if ( ! empty ( $from_number ) && strpos ( $from_number , 'client:' ) !== 0 ) {
$twiml .= ' callerId="' . htmlspecialchars ( $from_number ) . '"' ;
}
$twiml .= '>' ;
$twiml .= '<Number>' . htmlspecialchars ( $to_number ) . '</Number>' ;
$twiml .= '</Dial>' ;
} else {
// Incoming call to browser client
$twiml .= '<Dial timeout="30">' ;
$twiml .= '<Client>' . htmlspecialchars ( str_replace ( 'client:' , '' , $to_number )) . '</Client>' ;
$twiml .= '</Dial>' ;
}
} else {
$twiml .= '<Say voice="alice">No destination number provided.</Say>' ;
}
$twiml .= '</Response>' ;
return $this -> send_twiml_response ( $twiml );
}
/**
* Handle browser phone fallback when no browser clients answer
*/
public function handle_browser_fallback ( $request ) {
$params = $request -> get_params ();
$call_data = array (
'CallSid' => isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ,
'From' => isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ,
'To' => isset ( $params [ 'To' ]) ? $params [ 'To' ] : '' ,
'DialCallStatus' => isset ( $params [ 'DialCallStatus' ]) ? $params [ 'DialCallStatus' ] : ''
);
error_log ( 'TWP Browser Fallback: No browser clients answered, status: ' . $call_data [ 'DialCallStatus' ]);
// Log the fallback
TWP_Call_Logger :: log_call ( array (
'call_sid' => $call_data [ 'CallSid' ],
'from_number' => $call_data [ 'From' ],
'to_number' => $call_data [ 'To' ],
'status' => 'browser_fallback' ,
'actions_taken' => 'Browser clients did not answer, using fallback'
));
$twiml = '<?xml version="1.0" encoding="UTF-8"?>' ;
$twiml .= '<Response>' ;
// Fallback options based on call status
if ( $call_data [ 'DialCallStatus' ] === 'no-answer' || $call_data [ 'DialCallStatus' ] === 'busy' ) {
// Try SMS notification to agents as fallback
$this -> send_agent_notification_sms ( $call_data [ 'From' ], $call_data [ 'To' ]);
$twiml .= '<Say voice="alice">All our agents are currently busy. We have been notified of your call and will get back to you shortly.</Say>' ;
$twiml .= '<Say voice="alice">Please stay on the line for voicemail, or hang up and we will call you back.</Say>' ;
// Redirect to voicemail
$voicemail_url = home_url ( '/wp-json/twilio-webhook/v1/voicemail-callback' );
$twiml .= '<Redirect>' . $voicemail_url . '</Redirect>' ;
} else {
// Other statuses - generic message
$twiml .= '<Say voice="alice">We apologize, but we are unable to connect your call at this time. Please try again later.</Say>' ;
$twiml .= '<Hangup/>' ;
}
$twiml .= '</Response>' ;
return $this -> send_twiml_response ( $twiml );
}
/**
* Send SMS notification to agents about missed browser call
*/
private function send_agent_notification_sms ( $customer_number , $twilio_number ) {
// Get agents with phone numbers
$agents = get_users ( array (
'meta_key' => 'twp_phone_number' ,
'meta_compare' => 'EXISTS'
));
$message = " Missed call from { $customer_number } . Browser phone did not answer. Please call back or check voicemail. " ;
foreach ( $agents as $agent ) {
$agent_phone = get_user_meta ( $agent -> ID , 'twp_phone_number' , true );
if ( ! empty ( $agent_phone )) {
$twilio_api = new TWP_Twilio_API ();
$twilio_api -> send_sms ( $agent_phone , $message , $twilio_number );
}
}
}
/**
* Handle smart routing based on user preferences
*/
public function handle_smart_routing ( $request ) {
$params = $request -> get_params ();
$call_data = array (
'CallSid' => isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ,
'From' => isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ,
'To' => isset ( $params [ 'To' ]) ? $params [ 'To' ] : '' ,
'CallStatus' => isset ( $params [ 'CallStatus' ]) ? $params [ 'CallStatus' ] : ''
);
// Log the incoming call
TWP_Call_Logger :: log_call ( array (
'call_sid' => $call_data [ 'CallSid' ],
'from_number' => $call_data [ 'From' ],
'to_number' => $call_data [ 'To' ],
'status' => 'smart_routing' ,
'actions_taken' => 'Smart routing - checking workflows first, then user preferences'
));
// FIRST: Check if there's a workflow assigned to this phone number
$workflow = TWP_Workflow :: get_workflow_by_phone_number ( $call_data [ 'To' ]);
if ( $workflow ) {
error_log ( 'TWP Smart Routing: Found workflow for ' . $call_data [ 'To' ] . ', executing workflow ID: ' . $workflow -> id );
// Execute the workflow instead of direct routing
$workflow_twiml = TWP_Workflow :: execute_workflow ( $workflow -> id , $call_data );
if ( $workflow_twiml ) {
header ( 'Content-Type: application/xml' );
echo $workflow_twiml ;
exit ;
}
}
// FALLBACK: If no workflow found or workflow failed, use direct agent routing
error_log ( 'TWP Smart Routing: No workflow found for ' . $call_data [ 'To' ] . ', falling back to direct agent routing' );
// Check for any active agents and their preferences
$agents = get_users ( array (
'meta_key' => 'twp_phone_number' ,
'meta_compare' => 'EXISTS'
));
$browser_agents = [];
$cell_agents = [];
foreach ( $agents as $agent ) {
$call_mode = get_user_meta ( $agent -> ID , 'twp_call_mode' , true );
$agent_phone = get_user_meta ( $agent -> ID , 'twp_phone_number' , true );
if ( $call_mode === 'browser' ) {
2025-08-13 10:21:57 -07:00
// Twilio requires alphanumeric characters only
$clean_name = preg_replace ( '/[^a-zA-Z0-9]/' , '' , $agent -> display_name );
if ( empty ( $clean_name )) {
$clean_name = 'user' ;
}
$client_name = 'agent' . $agent -> ID . $clean_name ;
2025-08-12 07:05:47 -07:00
$browser_agents [] = $client_name ;
} elseif ( ! empty ( $agent_phone )) {
$cell_agents [] = $agent_phone ;
}
}
$twiml = '<?xml version="1.0" encoding="UTF-8"?>' ;
$twiml .= '<Response>' ;
$twiml .= '<Say voice="alice">Please hold while we connect you to an agent.</Say>' ;
// Try browser agents first, then cell agents
if ( ! empty ( $browser_agents )) {
$twiml .= '<Dial timeout="20" action="' . home_url ( '/wp-json/twilio-webhook/v1/smart-routing-fallback' ) . '" method="POST">' ;
foreach ( $browser_agents as $client_name ) {
$twiml .= '<Client>' . htmlspecialchars ( $client_name ) . '</Client>' ;
}
$twiml .= '</Dial>' ;
} elseif ( ! empty ( $cell_agents )) {
// No browser agents, try cell phones
$twiml .= '<Dial timeout="20" action="' . home_url ( '/wp-json/twilio-webhook/v1/smart-routing-fallback' ) . '" method="POST">' ;
foreach ( $cell_agents as $cell_phone ) {
$twiml .= '<Number>' . htmlspecialchars ( $cell_phone ) . '</Number>' ;
}
$twiml .= '</Dial>' ;
} else {
// No agents available
$twiml .= '<Say voice="alice">All agents are currently unavailable. Please leave a voicemail.</Say>' ;
$twiml .= '<Redirect>' . home_url ( '/wp-json/twilio-webhook/v1/voicemail-callback' ) . '</Redirect>' ;
}
$twiml .= '</Response>' ;
return $this -> send_twiml_response ( $twiml );
}
/**
* Handle smart routing fallback when initial routing fails
*/
public function handle_smart_routing_fallback ( $request ) {
$params = $request -> get_params ();
$call_data = array (
'CallSid' => isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ,
'From' => isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ,
'To' => isset ( $params [ 'To' ]) ? $params [ 'To' ] : '' ,
'DialCallStatus' => isset ( $params [ 'DialCallStatus' ]) ? $params [ 'DialCallStatus' ] : ''
);
error_log ( 'TWP Smart Routing Fallback: Initial routing failed, status: ' . $call_data [ 'DialCallStatus' ]);
// Log the fallback
TWP_Call_Logger :: log_call ( array (
'call_sid' => $call_data [ 'CallSid' ],
'from_number' => $call_data [ 'From' ],
'to_number' => $call_data [ 'To' ],
'status' => 'routing_fallback' ,
'actions_taken' => 'Smart routing failed, trying alternative methods'
));
$twiml = '<?xml version="1.0" encoding="UTF-8"?>' ;
$twiml .= '<Response>' ;
// Get agents and their preferences for fallback routing
$agents = get_users ( array (
'meta_key' => 'twp_phone_number' ,
'meta_compare' => 'EXISTS'
));
$browser_agents = [];
$cell_agents = [];
foreach ( $agents as $agent ) {
$call_mode = get_user_meta ( $agent -> ID , 'twp_call_mode' , true );
$agent_phone = get_user_meta ( $agent -> ID , 'twp_phone_number' , true );
if ( $call_mode === 'browser' ) {
2025-08-13 10:21:57 -07:00
// Twilio requires alphanumeric characters only
$clean_name = preg_replace ( '/[^a-zA-Z0-9]/' , '' , $agent -> display_name );
if ( empty ( $clean_name )) {
$clean_name = 'user' ;
}
$client_name = 'agent' . $agent -> ID . $clean_name ;
2025-08-12 07:05:47 -07:00
$browser_agents [] = $client_name ;
} elseif ( ! empty ( $agent_phone )) {
$cell_agents [] = $agent_phone ;
}
}
// Fallback strategy based on initial failure
if ( $call_data [ 'DialCallStatus' ] === 'no-answer' || $call_data [ 'DialCallStatus' ] === 'busy' ) {
// If browsers didn't answer, try cell phones; if cells didn't answer, try queue or voicemail
if ( ! empty ( $cell_agents ) && ! empty ( $browser_agents )) {
// We tried browsers first, now try cell phones
$twiml .= '<Say voice="alice">Trying to connect you to another agent.</Say>' ;
$twiml .= '<Dial timeout="20">' ;
foreach ( $cell_agents as $cell_phone ) {
$twiml .= '<Number>' . htmlspecialchars ( $cell_phone ) . '</Number>' ;
}
$twiml .= '</Dial>' ;
// If this also fails, fall through to final fallback below
$twiml .= '<Say voice="alice">All agents are currently busy.</Say>' ;
} else {
// No alternative agents available - go to final fallback
$twiml .= '<Say voice="alice">All agents are currently busy.</Say>' ;
}
// Send SMS notification to agents about missed call
$this -> send_missed_call_notification ( $call_data [ 'From' ], $call_data [ 'To' ]);
// Offer callback or voicemail options
$twiml .= '<Gather timeout="10" numDigits="1" action="' . home_url ( '/wp-json/twilio-webhook/v1/callback-choice' ) . '" method="POST">' ;
$twiml .= '<Say voice="alice">Press 1 to request a callback, or press 2 to leave a voicemail.</Say>' ;
$twiml .= '</Gather>' ;
// Default to voicemail if no input
$twiml .= '<Say voice="alice">No response received. Transferring you to voicemail.</Say>' ;
$twiml .= '<Redirect>' . home_url ( '/wp-json/twilio-webhook/v1/voicemail-callback' ) . '</Redirect>' ;
} elseif ( $call_data [ 'DialCallStatus' ] === 'failed' ) {
// Technical failure - provide different message
$twiml .= '<Say voice="alice">We are experiencing technical difficulties. Please try again later or leave a voicemail.</Say>' ;
$twiml .= '<Redirect>' . home_url ( '/wp-json/twilio-webhook/v1/voicemail-callback' ) . '</Redirect>' ;
} else {
// Other statuses or unknown - generic fallback
$twiml .= '<Say voice="alice">We apologize, but we are unable to connect your call at this time.</Say>' ;
$twiml .= '<Gather timeout="10" numDigits="1" action="' . home_url ( '/wp-json/twilio-webhook/v1/callback-choice' ) . '" method="POST">' ;
$twiml .= '<Say voice="alice">Press 1 to request a callback, or press 2 to leave a voicemail.</Say>' ;
$twiml .= '</Gather>' ;
$twiml .= '<Redirect>' . home_url ( '/wp-json/twilio-webhook/v1/voicemail-callback' ) . '</Redirect>' ;
}
$twiml .= '</Response>' ;
return $this -> send_twiml_response ( $twiml );
}
/**
* Send SMS notification to agents about missed call
*/
private function send_missed_call_notification ( $customer_number , $twilio_number ) {
// Get agents with phone numbers
$agents = get_users ( array (
'meta_key' => 'twp_phone_number' ,
'meta_compare' => 'EXISTS'
));
$message = " Missed call from { $customer_number } . All agents were unavailable. Customer offered callback/voicemail options. " ;
foreach ( $agents as $agent ) {
$agent_phone = get_user_meta ( $agent -> ID , 'twp_phone_number' , true );
if ( ! empty ( $agent_phone )) {
$twilio_api = new TWP_Twilio_API ();
$twilio_api -> send_sms ( $agent_phone , $message , $twilio_number );
}
}
}
2025-08-06 15:25:47 -07:00
/**
* Verify Twilio signature
*/
private function verify_twilio_signature () {
// Get signature header
$signature = isset ( $_SERVER [ 'HTTP_X_TWILIO_SIGNATURE' ]) ? $_SERVER [ 'HTTP_X_TWILIO_SIGNATURE' ] : '' ;
if ( ! $signature ) {
return false ;
}
// Get current URL
$protocol = isset ( $_SERVER [ 'HTTPS' ]) && $_SERVER [ 'HTTPS' ] === 'on' ? 'https' : 'http' ;
$url = $protocol . '://' . $_SERVER [ 'HTTP_HOST' ] . $_SERVER [ 'REQUEST_URI' ];
// Get POST data
$postData = file_get_contents ( 'php://input' );
parse_str ( $postData , $params );
// Verify signature
$twilio = new TWP_Twilio_API ();
return $twilio -> validate_webhook_signature ( $url , $params , $signature );
}
/**
* Handle voice webhook
*/
public function handle_voice_webhook ( $request ) {
// Verify Twilio signature
if ( ! $this -> verify_twilio_signature ()) {
return new WP_Error ( 'unauthorized' , 'Unauthorized' , array ( 'status' => 401 ));
}
$params = $request -> get_params ();
$call_data = array (
'CallSid' => isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ,
'From' => isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ,
'To' => isset ( $params [ 'To' ]) ? $params [ 'To' ] : '' ,
'CallStatus' => isset ( $params [ 'CallStatus' ]) ? $params [ 'CallStatus' ] : ''
);
// Log the incoming call
TWP_Call_Logger :: log_call ( array (
'call_sid' => $call_data [ 'CallSid' ],
'from_number' => $call_data [ 'From' ],
'to_number' => $call_data [ 'To' ],
'status' => 'initiated' ,
'actions_taken' => 'Incoming call received'
));
// Check for schedule
$schedule_id = isset ( $params [ 'schedule_id' ]) ? intval ( $params [ 'schedule_id' ]) : 0 ;
if ( $schedule_id ) {
$schedule = TWP_Scheduler :: get_schedule ( $schedule_id );
if ( $schedule && $schedule -> workflow_id ) {
// Execute workflow
TWP_Call_Logger :: log_action ( $call_data [ 'CallSid' ], 'Executing workflow: ' . $schedule -> schedule_name );
$twiml = TWP_Workflow :: execute_workflow ( $schedule -> workflow_id , $call_data );
TWP_Call_Logger :: update_call ( $call_data [ 'CallSid' ], array (
'workflow_id' => $schedule -> workflow_id ,
'workflow_name' => $schedule -> schedule_name
));
return $this -> send_twiml_response ( $twiml );
} elseif ( $schedule && $schedule -> forward_number ) {
// Forward call
TWP_Call_Logger :: log_action ( $call_data [ 'CallSid' ], 'Forwarding to: ' . $schedule -> forward_number );
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$dial = $twiml -> addChild ( 'Dial' );
$dial -> addChild ( 'Number' , $schedule -> forward_number );
return $this -> send_twiml_response ( $twiml -> asXML ());
}
}
// Check for workflow associated with phone number
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_workflows' ;
$workflow = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT * FROM $table_name WHERE phone_number = %s AND is_active = 1 LIMIT 1 " ,
$call_data [ 'To' ]
));
if ( $workflow ) {
TWP_Call_Logger :: log_action ( $call_data [ 'CallSid' ], 'Executing workflow: ' . $workflow -> workflow_name );
$twiml = TWP_Workflow :: execute_workflow ( $workflow -> id , $call_data );
TWP_Call_Logger :: update_call ( $call_data [ 'CallSid' ], array (
'workflow_id' => $workflow -> id ,
'workflow_name' => $workflow -> workflow_name
));
return $this -> send_twiml_response ( $twiml );
}
// Default response
TWP_Call_Logger :: log_action ( $call_data [ 'CallSid' ], 'No workflow found, using default response' );
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$say = $twiml -> addChild ( 'Say' , 'Thank you for calling. Please hold while we connect you.' );
$say -> addAttribute ( 'voice' , 'alice' );
// Add to default queue
$enqueue = $twiml -> addChild ( 'Enqueue' , 'default' );
return $this -> send_twiml_response ( $twiml -> asXML ());
}
/**
* Handle SMS webhook
*/
2025-08-11 20:31:48 -07:00
public function handle_sms_webhook ( $request ) {
error_log ( 'TWP SMS Webhook: ===== WEBHOOK TRIGGERED =====' );
error_log ( 'TWP SMS Webhook: Request method: ' . $_SERVER [ 'REQUEST_METHOD' ]);
error_log ( 'TWP SMS Webhook: Request URI: ' . $_SERVER [ 'REQUEST_URI' ]);
$params = $request -> get_params ();
2025-08-06 15:25:47 -07:00
$sms_data = array (
2025-08-11 20:31:48 -07:00
'MessageSid' => isset ( $params [ 'MessageSid' ]) ? $params [ 'MessageSid' ] : '' ,
'From' => isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ,
'To' => isset ( $params [ 'To' ]) ? $params [ 'To' ] : '' ,
'Body' => isset ( $params [ 'Body' ]) ? $params [ 'Body' ] : ''
2025-08-06 15:25:47 -07:00
);
2025-08-11 20:31:48 -07:00
error_log ( 'TWP SMS Webhook: Raw POST data: ' . print_r ( $_POST , true ));
error_log ( 'TWP SMS Webhook: Parsed params: ' . print_r ( $params , true ));
error_log ( 'TWP SMS Webhook: Received SMS - From: ' . $sms_data [ 'From' ] . ', To: ' . $sms_data [ 'To' ] . ', Body: ' . $sms_data [ 'Body' ]);
2025-08-06 15:25:47 -07:00
// Process SMS commands
$command = strtolower ( trim ( $sms_data [ 'Body' ]));
switch ( $command ) {
case '1' :
2025-08-11 20:31:48 -07:00
error_log ( 'TWP SMS Webhook: Agent texted "1" - calling handle_agent_ready_sms' );
2025-08-06 15:25:47 -07:00
$this -> handle_agent_ready_sms ( $sms_data [ 'From' ]);
break ;
case 'status' :
$this -> send_status_sms ( $sms_data [ 'From' ]);
break ;
case 'help' :
$this -> send_help_sms ( $sms_data [ 'From' ]);
break ;
default :
// Log SMS for later processing
$this -> log_sms ( $sms_data );
break ;
}
// Empty response
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
echo $twiml -> asXML ();
}
/**
* Handle status webhook
*/
public function handle_status_webhook ( $request ) {
// Verify Twilio signature
if ( ! $this -> verify_twilio_signature ()) {
return new WP_Error ( 'unauthorized' , 'Unauthorized' , array ( 'status' => 401 ));
}
$params = $request -> get_params ();
$status_data = array (
'CallSid' => isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ,
'CallStatus' => isset ( $params [ 'CallStatus' ]) ? $params [ 'CallStatus' ] : '' ,
'CallDuration' => isset ( $params [ 'CallDuration' ]) ? intval ( $params [ 'CallDuration' ]) : 0
);
// Update call log with status and duration
TWP_Call_Logger :: update_call ( $status_data [ 'CallSid' ], array (
'status' => $status_data [ 'CallStatus' ],
'duration' => $status_data [ 'CallDuration' ],
'actions_taken' => 'Call status changed to: ' . $status_data [ 'CallStatus' ]
));
// Update call status in queue if applicable
2025-08-11 20:31:48 -07:00
// Remove from queue for any terminal call state
if ( in_array ( $status_data [ 'CallStatus' ], [ 'completed' , 'busy' , 'failed' , 'canceled' , 'no-answer' ])) {
$queue_removed = TWP_Call_Queue :: remove_from_queue ( $status_data [ 'CallSid' ]);
if ( $queue_removed ) {
TWP_Call_Logger :: log_action ( $status_data [ 'CallSid' ], 'Call removed from queue due to status: ' . $status_data [ 'CallStatus' ]);
error_log ( 'TWP Status Webhook: Removed call ' . $status_data [ 'CallSid' ] . ' from queue (status: ' . $status_data [ 'CallStatus' ] . ')' );
}
2025-08-06 15:25:47 -07:00
}
// Empty response
return new WP_REST_Response ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' , 200 , array (
'Content-Type' => 'text/xml; charset=utf-8'
));
}
/**
* Handle IVR response
*/
2025-08-12 09:12:54 -07:00
public function handle_ivr_response ( $request ) {
$digits = $request -> get_param ( 'Digits' ) ? : '' ;
$workflow_id = intval ( $request -> get_param ( 'workflow_id' ) ? : 0 );
$step_id = intval ( $request -> get_param ( 'step_id' ) ? : 0 );
// Debug logging
error_log ( 'TWP IVR: Received digits="' . $digits . '", workflow_id=' . $workflow_id . ', step_id=' . $step_id );
error_log ( 'TWP IVR: All request params: ' . json_encode ( $request -> get_params ()));
2025-08-06 15:25:47 -07:00
if ( ! $workflow_id || ! $step_id ) {
2025-08-12 09:12:54 -07:00
return $this -> send_twiml_response ( $this -> get_default_twiml ());
2025-08-06 15:25:47 -07:00
}
$workflow = TWP_Workflow :: get_workflow ( $workflow_id );
if ( ! $workflow ) {
2025-08-12 09:12:54 -07:00
return $this -> send_twiml_response ( $this -> get_default_twiml ());
2025-08-06 15:25:47 -07:00
}
$workflow_data = json_decode ( $workflow -> workflow_data , true );
2025-08-12 09:12:54 -07:00
// Debug: log all steps in workflow
error_log ( 'TWP IVR: Looking for step_id ' . $step_id . ' in workflow ' . $workflow_id );
foreach ( $workflow_data [ 'steps' ] as $index => $step ) {
error_log ( 'TWP IVR: Step ' . $index . ' has ID: ' . ( isset ( $step [ 'id' ]) ? $step [ 'id' ] : 'NO ID' ));
}
2025-08-06 15:25:47 -07:00
// Find the step and its options
foreach ( $workflow_data [ 'steps' ] as $step ) {
2025-08-12 09:12:54 -07:00
if ( $step [ 'id' ] == $step_id ) {
error_log ( 'TWP IVR: Found matching step with ID ' . $step_id );
// Options can be in step['data']['options'] or step['options']
$options = isset ( $step [ 'data' ][ 'options' ]) ? $step [ 'data' ][ 'options' ] :
( isset ( $step [ 'options' ]) ? $step [ 'options' ] : array ());
2025-08-06 15:25:47 -07:00
2025-08-12 09:12:54 -07:00
// Debug: log all available options
error_log ( 'TWP IVR: All available options for step ' . $step_id . ': ' . json_encode ( $options ));
if ( isset ( $options [ $digits ])) {
$option = $options [ $digits ];
// Log for debugging
error_log ( 'TWP IVR: Found option for digit ' . $digits . ': ' . json_encode ( $option ));
switch ( $option [ 'action' ]) {
2025-08-06 15:25:47 -07:00
case 'forward' :
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$dial = $twiml -> addChild ( 'Dial' );
$dial -> addChild ( 'Number' , $option [ 'number' ]);
2025-08-12 09:12:54 -07:00
return $this -> send_twiml_response ( $twiml -> asXML ());
2025-08-06 15:25:47 -07:00
case 'queue' :
2025-08-12 09:12:54 -07:00
// Determine queue ID - could be in queue_id field or legacy queue_name field
$queue_id = null ;
if ( isset ( $option [ 'queue_id' ]) && is_numeric ( $option [ 'queue_id' ]) && $option [ 'queue_id' ] > 0 ) {
$queue_id = intval ( $option [ 'queue_id' ]);
} elseif ( isset ( $option [ 'queue_name' ]) && is_numeric ( $option [ 'queue_name' ]) && $option [ 'queue_name' ] > 0 ) {
// Legacy format where queue_name contains the queue ID
$queue_id = intval ( $option [ 'queue_name' ]);
} elseif ( isset ( $option [ 'number' ]) && is_numeric ( $option [ 'number' ]) && $option [ 'number' ] > 0 ) {
// Another legacy format where number contains the queue ID
$queue_id = intval ( $option [ 'number' ]);
}
error_log ( 'TWP IVR Queue: Determined queue_id=' . ( $queue_id ? $queue_id : 'NULL' ) . ' from option: ' . json_encode ( $option ));
// Use the TWP queue system if we have a valid queue_id
if ( $queue_id && $queue_id > 0 ) {
$call_data = array (
'call_sid' => $request -> get_param ( 'CallSid' ),
'from_number' => $request -> get_param ( 'From' ),
'to_number' => $request -> get_param ( 'To' )
);
error_log ( 'TWP IVR Queue: Adding call to queue_id=' . $queue_id . ', call_sid=' . $call_data [ 'call_sid' ]);
$position = TWP_Call_Queue :: add_to_queue ( $queue_id , $call_data );
if ( $position ) {
error_log ( 'TWP IVR Queue: Call added to position ' . $position );
// Generate TwiML for queue wait with proper callback URL
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$enqueue = $twiml -> addChild ( 'Enqueue' );
$enqueue -> addAttribute ( 'waitUrl' , home_url ( '/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $queue_id ));
$enqueue -> addChild ( 'Task' , json_encode ( array ( 'queue_id' => $queue_id , 'position' => $position )));
return $this -> send_twiml_response ( $twiml -> asXML ());
} else {
error_log ( 'TWP IVR Queue: Failed to add call to queue' );
}
}
// If we reach here, no valid queue was found - provide helpful message
error_log ( 'TWP IVR Queue: No valid queue_id found, providing error message to caller' );
2025-08-06 15:25:47 -07:00
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
2025-08-12 09:12:54 -07:00
$say = $twiml -> addChild ( 'Say' , 'Sorry, that option is not currently available. Please try again or hang up.' );
$say -> addAttribute ( 'voice' , 'alice' );
$twiml -> addChild ( 'Redirect' ); // Redirect back to IVR menu
return $this -> send_twiml_response ( $twiml -> asXML ());
2025-08-06 15:25:47 -07:00
case 'voicemail' :
$elevenlabs = new TWP_ElevenLabs_API ();
$twiml = TWP_Workflow :: create_voicemail_twiml ( $option , $elevenlabs );
2025-08-12 09:12:54 -07:00
return $this -> send_twiml_response ( $twiml );
2025-08-06 15:25:47 -07:00
case 'message' :
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$say = $twiml -> addChild ( 'Say' , $option [ 'message' ]);
$say -> addAttribute ( 'voice' , 'alice' );
$twiml -> addChild ( 'Hangup' );
2025-08-12 09:12:54 -07:00
return $this -> send_twiml_response ( $twiml -> asXML ());
}
} else {
// Log for debugging when option not found
error_log ( 'TWP IVR: No option found for digit "' . $digits . '" in step ' . $step_id );
error_log ( 'TWP IVR: Available options: ' . json_encode ( array_keys ( $options )));
error_log ( 'TWP IVR: Full step data: ' . json_encode ( $step ));
2025-08-06 15:25:47 -07:00
}
}
}
// Invalid option - replay menu
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$say = $twiml -> addChild ( 'Say' , 'Invalid option. Please try again.' );
$say -> addAttribute ( 'voice' , 'alice' );
$twiml -> addChild ( 'Redirect' );
2025-08-12 09:12:54 -07:00
return $this -> send_twiml_response ( $twiml -> asXML ());
2025-08-06 15:25:47 -07:00
}
/**
* Handle queue wait
*/
2025-08-11 20:31:48 -07:00
public function handle_queue_wait ( $request = null ) {
// Get parameters from request
$params = $request ? $request -> get_params () : $_REQUEST ;
$queue_id = isset ( $params [ 'queue_id' ]) ? intval ( $params [ 'queue_id' ]) : 0 ;
$call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
error_log ( 'TWP Queue Wait: queue_id=' . $queue_id . ', call_sid=' . $call_sid );
2025-08-06 15:25:47 -07:00
// Get caller's position in queue
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_queued_calls' ;
$call = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT * FROM $table_name WHERE call_sid = %s " ,
$call_sid
));
if ( $call ) {
$position = $call -> position ;
2025-08-11 20:31:48 -07:00
$status = $call -> status ;
error_log ( 'TWP Queue Wait: Found call in position ' . $position . ' with status ' . $status );
2025-08-06 15:25:47 -07:00
2025-08-11 20:31:48 -07:00
// If call is being connected to an agent, provide different response
if ( $status === 'connecting' || $status === 'answered' ) {
error_log ( 'TWP Queue Wait: Call is being connected to agent, providing hold message' );
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$say = $twiml -> addChild ( 'Say' , 'We found an available agent. Please hold while we connect you.' );
$say -> addAttribute ( 'voice' , 'alice' );
// Add music or pause while connecting
$queue = TWP_Call_Queue :: get_queue ( $queue_id );
if ( $queue && ! empty ( $queue -> wait_music_url )) {
$play = $twiml -> addChild ( 'Play' , $queue -> wait_music_url );
} else {
$pause = $twiml -> addChild ( 'Pause' );
$pause -> addAttribute ( 'length' , '30' );
}
return $this -> send_twiml_response ( $twiml -> asXML ());
}
2025-08-06 15:25:47 -07:00
2025-08-11 20:31:48 -07:00
// For waiting calls, continue with normal queue behavior
// Create basic TwiML response
2025-08-06 15:25:47 -07:00
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
2025-08-11 20:31:48 -07:00
// Simple position announcement
if ( $position > 1 ) {
$message = " You are currently number $position in the queue. Please continue to hold. " ;
2025-08-06 15:25:47 -07:00
$say = $twiml -> addChild ( 'Say' , $message );
$say -> addAttribute ( 'voice' , 'alice' );
}
2025-08-11 20:31:48 -07:00
// Add wait music or pause, then redirect back to continue the loop
2025-08-06 15:25:47 -07:00
$queue = TWP_Call_Queue :: get_queue ( $queue_id );
2025-08-11 20:31:48 -07:00
if ( $queue && ! empty ( $queue -> wait_music_url )) {
2025-08-06 15:25:47 -07:00
$play = $twiml -> addChild ( 'Play' , $queue -> wait_music_url );
2025-08-11 20:31:48 -07:00
} else {
// Add a pause to prevent rapid loops
$pause = $twiml -> addChild ( 'Pause' );
$pause -> addAttribute ( 'length' , '15' ); // 15 second pause
2025-08-06 15:25:47 -07:00
}
2025-08-11 20:31:48 -07:00
// Redirect back to this same endpoint to create continuous loop
$redirect_url = home_url ( '/wp-json/twilio-webhook/v1/queue-wait' );
$redirect_url = add_query_arg ( array (
'queue_id' => $queue_id ,
'call_sid' => urlencode ( $call_sid ) // URL encode to handle special characters
), $redirect_url );
// Set the text content of Redirect element properly
$redirect = $twiml -> addChild ( 'Redirect' );
$redirect [ 0 ] = $redirect_url ; // Set the URL as the text content
$redirect -> addAttribute ( 'method' , 'POST' );
$response = $twiml -> asXML ();
error_log ( 'TWP Queue Wait: Returning continuous TwiML: ' . $response );
return $this -> send_twiml_response ( $response );
2025-08-06 15:25:47 -07:00
} else {
2025-08-11 20:31:48 -07:00
error_log ( 'TWP Queue Wait: Call not found in queue - providing basic hold' );
// Call not in queue yet (maybe still being processed) - provide basic hold with redirect
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$say = $twiml -> addChild ( 'Say' , 'Please hold while we process your call.' );
$say -> addAttribute ( 'voice' , 'alice' );
// Add a pause then redirect to check again
$pause = $twiml -> addChild ( 'Pause' );
$pause -> addAttribute ( 'length' , '10' ); // 10 second pause
// Redirect back to check if call has been added to queue
$redirect_url = home_url ( '/wp-json/twilio-webhook/v1/queue-wait' );
$redirect_url = add_query_arg ( array (
'queue_id' => $queue_id ,
'call_sid' => urlencode ( $call_sid ) // URL encode to handle special characters
), $redirect_url );
// Set the text content of Redirect element properly
$redirect = $twiml -> addChild ( 'Redirect' );
$redirect [ 0 ] = $redirect_url ; // Set the URL as the text content
$redirect -> addAttribute ( 'method' , 'POST' );
return $this -> send_twiml_response ( $twiml -> asXML ());
2025-08-06 15:25:47 -07:00
}
}
2025-08-11 20:31:48 -07:00
/**
* Handle queue action ( enqueue / dequeue events )
*/
public function handle_queue_action ( $request = null ) {
$params = $request ? $request -> get_params () : $_REQUEST ;
$queue_id = isset ( $params [ 'queue_id' ]) ? intval ( $params [ 'queue_id' ]) : 0 ;
$call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
$queue_result = isset ( $params [ 'QueueResult' ]) ? $params [ 'QueueResult' ] : '' ;
$from_number = isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ;
$to_number = isset ( $params [ 'To' ]) ? $params [ 'To' ] : '' ;
error_log ( 'TWP Queue Action: queue_id=' . $queue_id . ', call_sid=' . $call_sid . ', result=' . $queue_result );
// Call left queue (answered, timeout, hangup, etc.) - update status
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_queued_calls' ;
$status = 'completed' ;
if ( $queue_result === 'timeout' ) {
$status = 'timeout' ;
} elseif ( $queue_result === 'hangup' ) {
$status = 'hangup' ;
} elseif ( $queue_result === 'bridged' ) {
$status = 'answered' ;
} elseif ( $queue_result === 'leave' ) {
$status = 'transferred' ;
}
$updated = $wpdb -> update (
$table_name ,
array (
'status' => $status ,
'ended_at' => current_time ( 'mysql' )
),
array ( 'call_sid' => $call_sid ),
array ( '%s' , '%s' ),
array ( '%s' )
);
if ( $updated ) {
error_log ( 'TWP Queue Action: Updated call status to ' . $status );
} else {
error_log ( 'TWP Queue Action: No call found to update with SID ' . $call_sid );
}
// Return empty response - this is just for tracking
return $this -> send_twiml_response ( '<Response></Response>' );
}
/**
* Handle voicemail complete ( after recording )
*/
public function handle_voicemail_complete ( $request ) {
$twiml = '<?xml version="1.0" encoding="UTF-8"?>' ;
$twiml .= '<Response>' ;
$twiml .= '<Say voice="alice">Thank you for your message. Goodbye.</Say>' ;
$twiml .= '<Hangup/>' ;
$twiml .= '</Response>' ;
return $this -> send_twiml_response ( $twiml );
}
/**
* Proxy voicemail audio through WordPress
*/
public function proxy_voicemail_audio ( $request ) {
// Permission already checked by REST API permission_callback
$voicemail_id = intval ( $request -> get_param ( 'id' ));
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_voicemails' ;
$voicemail = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT recording_url FROM $table_name WHERE id = %d " ,
$voicemail_id
));
if ( ! $voicemail || ! $voicemail -> recording_url ) {
header ( 'HTTP/1.0 404 Not Found' );
exit ( 'Voicemail not found' );
}
// Fetch the audio from Twilio using authenticated request
$twilio = new TWP_Twilio_API ();
$account_sid = get_option ( 'twp_twilio_account_sid' );
$auth_token = get_option ( 'twp_twilio_auth_token' );
// Add .mp3 to the URL if not present
$audio_url = $voicemail -> recording_url ;
if ( strpos ( $audio_url , '.mp3' ) === false && strpos ( $audio_url , '.wav' ) === false ) {
$audio_url .= '.mp3' ;
}
// Fetch audio with authentication
$response = wp_remote_get ( $audio_url , array (
'headers' => array (
'Authorization' => 'Basic ' . base64_encode ( $account_sid . ':' . $auth_token )
),
'timeout' => 30
));
if ( is_wp_error ( $response )) {
return new WP_Error ( 'fetch_error' , 'Unable to fetch audio' , array ( 'status' => 500 ));
}
$body = wp_remote_retrieve_body ( $response );
$content_type = wp_remote_retrieve_header ( $response , 'content-type' ) ? : 'audio/mpeg' ;
// Return audio with proper headers
header ( 'Content-Type: ' . $content_type );
header ( 'Content-Length: ' . strlen ( $body ));
header ( 'Cache-Control: private, max-age=3600' );
echo $body ;
exit ;
}
2025-08-06 15:25:47 -07:00
/**
* Handle voicemail callback
*/
public function handle_voicemail_callback ( $request ) {
// Verify Twilio signature
if ( ! $this -> verify_twilio_signature ()) {
return new WP_Error ( 'unauthorized' , 'Unauthorized' , array ( 'status' => 401 ));
}
$params = $request -> get_params ();
2025-08-11 20:31:48 -07:00
// Debug logging
error_log ( 'TWP Voicemail Callback Params: ' . json_encode ( $params ));
2025-08-06 15:25:47 -07:00
$recording_url = isset ( $params [ 'RecordingUrl' ]) ? $params [ 'RecordingUrl' ] : '' ;
$recording_duration = isset ( $params [ 'RecordingDuration' ]) ? intval ( $params [ 'RecordingDuration' ]) : 0 ;
$call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
$from = isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ;
$workflow_id = isset ( $params [ 'workflow_id' ]) ? intval ( $params [ 'workflow_id' ]) : 0 ;
2025-08-11 20:31:48 -07:00
// If From is not provided in the callback, try to get it from the call log
if ( empty ( $from ) && ! empty ( $call_sid )) {
global $wpdb ;
$call_log_table = $wpdb -> prefix . 'twp_call_log' ;
$call_record = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT from_number FROM $call_log_table WHERE call_sid = %s LIMIT 1 " ,
$call_sid
));
if ( $call_record && $call_record -> from_number ) {
$from = $call_record -> from_number ;
error_log ( 'TWP Voicemail Callback: Retrieved from_number from call log: ' . $from );
}
}
// Debug what we extracted
error_log ( 'TWP Voicemail Callback: recording_url=' . $recording_url . ', from=' . $from . ', workflow_id=' . $workflow_id . ', call_sid=' . $call_sid );
2025-08-06 15:25:47 -07:00
if ( $recording_url ) {
// Save voicemail record
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_voicemails' ;
$voicemail_id = $wpdb -> insert (
$table_name ,
array (
'workflow_id' => $workflow_id ,
'from_number' => $from ,
'recording_url' => $recording_url ,
'duration' => $recording_duration ,
'created_at' => current_time ( 'mysql' )
),
array ( '%d' , '%s' , '%s' , '%d' , '%s' )
);
// Log voicemail action
if ( $call_sid ) {
TWP_Call_Logger :: log_action ( $call_sid , 'Voicemail recorded (' . $recording_duration . 's)' );
}
// Send notification email
$this -> send_voicemail_notification ( $from , $recording_url , $recording_duration , $voicemail_id );
// Schedule transcription if enabled
$this -> schedule_voicemail_transcription ( $voicemail_id , $recording_url );
}
return new WP_REST_Response ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' , 200 , array (
'Content-Type' => 'text/xml; charset=utf-8'
));
}
/**
* Send voicemail notification
*/
private function send_voicemail_notification ( $from_number , $recording_url , $duration , $voicemail_id ) {
$admin_email = get_option ( 'admin_email' );
$site_name = get_bloginfo ( 'name' );
$subject = '[' . $site_name . '] New Voicemail from ' . $from_number ;
$message = " You have received a new voicemail: \n \n " ;
$message .= " From: " . $from_number . " \n " ;
$message .= " Duration: " . $duration . " seconds \n " ;
$message .= " Received: " . current_time ( 'F j, Y g:i A' ) . " \n \n " ;
$message .= " Listen to the voicemail in your admin panel: \n " ;
$message .= admin_url ( 'admin.php?page=twilio-wp-voicemails' ) . " \n \n " ;
$message .= " Direct link to recording: \n " ;
$message .= $recording_url ;
$headers = array ( 'Content-Type: text/plain; charset=UTF-8' );
wp_mail ( $admin_email , $subject , $message , $headers );
// Also send SMS notification if configured
$notification_number = get_option ( 'twp_sms_notification_number' );
if ( $notification_number ) {
$twilio = new TWP_Twilio_API ();
$sms_message = " New voicemail from { $from_number } ( { $duration } s). Check admin panel to listen. " ;
$twilio -> send_sms ( $notification_number , $sms_message );
}
}
/**
* Schedule voicemail transcription
*/
private function schedule_voicemail_transcription ( $voicemail_id , $recording_url ) {
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_voicemails' ;
// Mark transcription as pending - Twilio will call our transcription webhook when ready
$wpdb -> update (
$table_name ,
array ( 'transcription' => 'Transcription pending...' ),
array ( 'id' => $voicemail_id ),
array ( '%s' ),
array ( '%d' )
);
}
/**
* Handle transcription webhook
*/
public function handle_transcription_webhook ( $request ) {
// Verify Twilio signature
if ( ! $this -> verify_twilio_signature ()) {
return new WP_Error ( 'unauthorized' , 'Unauthorized' , array ( 'status' => 401 ));
}
$params = $request -> get_params ();
$transcription_text = isset ( $params [ 'TranscriptionText' ]) ? $params [ 'TranscriptionText' ] : '' ;
$recording_sid = isset ( $params [ 'RecordingSid' ]) ? $params [ 'RecordingSid' ] : '' ;
$transcription_status = isset ( $params [ 'TranscriptionStatus' ]) ? $params [ 'TranscriptionStatus' ] : '' ;
$call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
if ( $transcription_status === 'completed' && $transcription_text ) {
// Find voicemail by recording URL (we need to match by call_sid or recording URL)
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_voicemails' ;
// First try to find by recording URL containing the RecordingSid
$voicemail = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT * FROM $table_name WHERE recording_url LIKE %s ORDER BY created_at DESC LIMIT 1 " ,
'%' . $recording_sid . '%'
));
if ( $voicemail ) {
// Update transcription
$wpdb -> update (
$table_name ,
array ( 'transcription' => $transcription_text ),
array ( 'id' => $voicemail -> id ),
array ( '%s' ),
array ( '%d' )
);
// Log transcription completion
if ( $call_sid ) {
TWP_Call_Logger :: log_action ( $call_sid , 'Voicemail transcription completed' );
}
// Send notification if transcription contains keywords
$this -> check_transcription_keywords ( $voicemail -> id , $transcription_text , $voicemail -> from_number );
}
} elseif ( $transcription_status === 'failed' ) {
// Handle failed transcription
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_voicemails' ;
$voicemail = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT * FROM $table_name WHERE recording_url LIKE %s ORDER BY created_at DESC LIMIT 1 " ,
'%' . $recording_sid . '%'
));
if ( $voicemail ) {
$wpdb -> update (
$table_name ,
array ( 'transcription' => 'Transcription failed' ),
array ( 'id' => $voicemail -> id ),
array ( '%s' ),
array ( '%d' )
);
}
if ( $call_sid ) {
TWP_Call_Logger :: log_action ( $call_sid , 'Voicemail transcription failed' );
}
}
return new WP_REST_Response ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' , 200 , array (
'Content-Type' => 'text/xml; charset=utf-8'
));
}
/**
* Check transcription for important keywords
*/
private function check_transcription_keywords ( $voicemail_id , $transcription_text , $from_number ) {
$urgent_keywords = get_option ( 'twp_urgent_keywords' , 'urgent,emergency,important,asap,help' );
$keywords = array_map ( 'trim' , explode ( ',' , strtolower ( $urgent_keywords )));
$transcription_lower = strtolower ( $transcription_text );
foreach ( $keywords as $keyword ) {
if ( ! empty ( $keyword ) && strpos ( $transcription_lower , $keyword ) !== false ) {
// Send urgent notification
$this -> send_urgent_voicemail_notification ( $voicemail_id , $transcription_text , $from_number , $keyword );
break ;
}
}
}
/**
* Send urgent voicemail notification
*/
private function send_urgent_voicemail_notification ( $voicemail_id , $transcription_text , $from_number , $keyword ) {
$admin_email = get_option ( 'admin_email' );
$site_name = get_bloginfo ( 'name' );
$subject = '[URGENT] ' . $site_name . ' - Voicemail from ' . $from_number ;
$message = " URGENT VOICEMAIL DETECTED: \\ n \\ n " ;
$message .= " From: " . $from_number . " \\ n " ;
$message .= " Keyword detected: " . $keyword . " \\ n " ;
$message .= " Received: " . current_time ( 'F j, Y g:i A' ) . " \\ n \\ n " ;
$message .= " Transcription: \\ n " . $transcription_text . " \\ n \\ n " ;
$message .= " Listen to voicemail: " . admin_url ( 'admin.php?page=twilio-wp-voicemails' );
$headers = array ( 'Content-Type: text/plain; charset=UTF-8' );
wp_mail ( $admin_email , $subject , $message , $headers );
// Also send urgent SMS notification
$notification_number = get_option ( 'twp_sms_notification_number' );
if ( $notification_number ) {
$twilio = new TWP_Twilio_API ();
$sms_message = " URGENT voicemail from { $from_number } . Keyword: { $keyword } . Check admin panel immediately. " ;
$twilio -> send_sms ( $notification_number , $sms_message );
}
}
/**
* Send default response
*/
private function send_default_response () {
2025-08-07 15:24:29 -07:00
$response = new \Twilio\TwiML\VoiceResponse ();
$response -> say ( 'Thank you for calling. Goodbye.' , [ 'voice' => 'alice' ]);
$response -> hangup ();
echo $response -> asXML ();
2025-08-06 15:25:47 -07:00
}
2025-08-12 09:12:54 -07:00
private function get_default_twiml () {
$response = new \Twilio\TwiML\VoiceResponse ();
$response -> say ( 'Thank you for calling. Goodbye.' , [ 'voice' => 'alice' ]);
$response -> hangup ();
return $response -> asXML ();
}
2025-08-06 15:25:47 -07:00
/**
* Send status SMS
*/
private function send_status_sms ( $to_number ) {
$twilio = new TWP_Twilio_API ();
$queue_status = TWP_Call_Queue :: get_queue_status ();
$message = " Queue Status: \n " ;
foreach ( $queue_status as $queue ) {
$message .= $queue [ 'queue_name' ] . ': ' . $queue [ 'waiting_calls' ] . " waiting \n " ;
}
$twilio -> send_sms ( $to_number , $message );
}
/**
* Send help SMS
*/
private function send_help_sms ( $to_number ) {
$twilio = new TWP_Twilio_API ();
$message = " Available commands: \n " ;
$message .= " STATUS - Get queue status \n " ;
$message .= " HELP - Show this message " ;
$twilio -> send_sms ( $to_number , $message );
}
/**
* Log SMS
*/
private function log_sms ( $sms_data ) {
// Store SMS in database for later processing
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_sms_log' ;
$wpdb -> insert (
$table_name ,
array (
'message_sid' => $sms_data [ 'MessageSid' ],
'from_number' => $sms_data [ 'From' ],
'to_number' => $sms_data [ 'To' ],
'body' => $sms_data [ 'Body' ],
'received_at' => current_time ( 'mysql' )
),
array ( '%s' , '%s' , '%s' , '%s' , '%s' )
);
}
/**
* Log call status
*/
private function log_call_status ( $status_data ) {
// Store call status in database
global $wpdb ;
$table_name = $wpdb -> prefix . 'twp_call_log' ;
$wpdb -> insert (
$table_name ,
array (
'call_sid' => $status_data [ 'CallSid' ],
'status' => $status_data [ 'CallStatus' ],
'duration' => $status_data [ 'CallDuration' ],
'updated_at' => current_time ( 'mysql' )
),
array ( '%s' , '%s' , '%d' , '%s' )
);
}
/**
* Handle callback choice webhook
*/
public function handle_callback_choice ( $request ) {
$params = $request -> get_params ();
$digits = isset ( $params [ 'Digits' ]) ? $params [ 'Digits' ] : '' ;
$phone_number = isset ( $params [ 'Caller' ]) ? $params [ 'Caller' ] : '' ;
$queue_id = isset ( $params [ 'queue_id' ]) ? intval ( $params [ 'queue_id' ]) : null ;
if ( $digits === '1' ) {
// User chose to wait - redirect back to queue
$twiml = '<Response><Say voice="alice">Returning you to the queue.</Say><Redirect>' . home_url ( '/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $queue_id ) . '</Redirect></Response>' ;
} else {
// Default to callback (digits === '2' or no input)
TWP_Callback_Manager :: request_callback ( $phone_number , $queue_id );
$twiml = '<Response><Say voice="alice">Your callback has been requested. We will call you back shortly. Thank you!</Say><Hangup/></Response>' ;
}
return $this -> send_twiml_response ( $twiml );
}
/**
* Handle request callback webhook
*/
public function handle_request_callback ( $request ) {
$params = $request -> get_params ();
$phone_number = isset ( $params [ 'phone_number' ]) ? $params [ 'phone_number' ] : '' ;
$queue_id = isset ( $params [ 'queue_id' ]) ? intval ( $params [ 'queue_id' ]) : null ;
if ( $phone_number ) {
TWP_Callback_Manager :: request_callback ( $phone_number , $queue_id );
$twiml = '<Response><Say voice="alice">Your callback has been requested. We will call you back shortly. Thank you!</Say><Hangup/></Response>' ;
} else {
$twiml = '<Response><Say voice="alice">Unable to process your callback request. Please try again.</Say><Hangup/></Response>' ;
}
return $this -> send_twiml_response ( $twiml );
}
/**
* Handle callback agent webhook
*/
public function handle_callback_agent ( $request ) {
$params = $request -> get_params ();
$callback_id = isset ( $params [ 'callback_id' ]) ? intval ( $params [ 'callback_id' ]) : 0 ;
$customer_number = isset ( $params [ 'customer_number' ]) ? $params [ 'customer_number' ] : '' ;
$call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
if ( $callback_id && $customer_number ) {
TWP_Callback_Manager :: handle_agent_answered ( $callback_id , $call_sid );
$twiml = '<Response><Say voice="alice">Please hold while we connect the customer.</Say><Play>http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.wav</Play></Response>' ;
} else {
$twiml = '<Response><Say voice="alice">Unable to process callback. Hanging up.</Say><Hangup/></Response>' ;
}
return $this -> send_twiml_response ( $twiml );
}
/**
* Handle callback customer webhook
*/
public function handle_callback_customer ( $request ) {
$params = $request -> get_params ();
$agent_call_sid = isset ( $params [ 'agent_call_sid' ]) ? $params [ 'agent_call_sid' ] : '' ;
$callback_id = isset ( $params [ 'callback_id' ]) ? intval ( $params [ 'callback_id' ]) : 0 ;
if ( $agent_call_sid ) {
// Conference both calls together
$conference_name = 'callback-' . $callback_id . '-' . time ();
$twiml = '<Response><Say voice="alice">You are being connected to an agent.</Say><Dial><Conference>' . $conference_name . '</Conference></Dial></Response>' ;
// Update the agent call to join the same conference
$twilio = new TWP_Twilio_API ();
$agent_twiml = '<Response><Dial><Conference>' . $conference_name . '</Conference></Dial></Response>' ;
2025-08-11 20:31:48 -07:00
$twilio -> update_call ( $agent_call_sid , array ( 'twiml' => $agent_twiml ));
2025-08-06 15:25:47 -07:00
// Mark callback as completed
TWP_Callback_Manager :: complete_callback ( $callback_id );
} else {
$twiml = '<Response><Say voice="alice">Unable to connect your call. Please try again later.</Say><Hangup/></Response>' ;
}
return $this -> send_twiml_response ( $twiml );
}
/**
* Handle outbound agent webhook
*/
public function handle_outbound_agent ( $request ) {
$params = $request -> get_params ();
$target_number = isset ( $params [ 'target_number' ]) ? $params [ 'target_number' ] : '' ;
$agent_call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
if ( $target_number ) {
$twiml = TWP_Callback_Manager :: handle_outbound_agent_answered ( $target_number , $agent_call_sid );
} else {
$twiml = '<Response><Say voice="alice">Unable to process outbound call.</Say><Hangup/></Response>' ;
}
return $this -> send_twiml_response ( $twiml );
}
/**
* Handle ring group result webhook
*/
public function handle_ring_group_result ( $request ) {
$params = $request -> get_params ();
$dial_call_status = isset ( $params [ 'DialCallStatus' ]) ? $params [ 'DialCallStatus' ] : '' ;
$group_id = isset ( $params [ 'group_id' ]) ? intval ( $params [ 'group_id' ]) : 0 ;
$queue_name = isset ( $params [ 'queue_name' ]) ? $params [ 'queue_name' ] : '' ;
$fallback_action = isset ( $params [ 'fallback_action' ]) ? $params [ 'fallback_action' ] : 'queue' ;
$caller_number = isset ( $params [ 'From' ]) ? $params [ 'From' ] : '' ;
// If the call was answered, just hang up (call is connected)
if ( $dial_call_status === 'completed' ) {
$twiml = '<Response></Response>' ;
return $this -> send_twiml_response ( $twiml );
}
// If no one answered, handle based on fallback action
if ( $dial_call_status === 'no-answer' || $dial_call_status === 'busy' || $dial_call_status === 'failed' ) {
if ( $fallback_action === 'queue' && ! empty ( $queue_name )) {
// Put caller back in queue
$twiml = '<Response>' ;
$twiml .= '<Say voice="alice">No agents are currently available. Adding you to the queue.</Say>' ;
$twiml .= '<Enqueue waitUrl="' . home_url ( '/wp-json/twilio-webhook/v1/queue-wait?queue_name=' . urlencode ( $queue_name )) . '">' . $queue_name . '</Enqueue>' ;
$twiml .= '</Response>' ;
// Notify group members via SMS if no agents are available
$this -> notify_group_members_sms ( $group_id , $caller_number , $queue_name );
} else if ( $fallback_action === 'voicemail' ) {
// Go to voicemail
$twiml = '<Response>' ;
$twiml .= '<Say voice="alice">No one is available to take your call. Please leave a message after the beep.</Say>' ;
$twiml .= '<Record action="' . home_url ( '/wp-json/twilio-webhook/v1/voicemail-callback' ) . '" maxLength="300" playBeep="true" finishOnKey="#"/>' ;
$twiml .= '</Response>' ;
} else {
// Default message and callback option
$callback_twiml = TWP_Callback_Manager :: create_callback_twiml ( null , $caller_number );
return $this -> send_twiml_response ( $callback_twiml );
}
} else {
// Unknown status, provide callback option
$callback_twiml = TWP_Callback_Manager :: create_callback_twiml ( null , $caller_number );
return $this -> send_twiml_response ( $callback_twiml );
}
return $this -> send_twiml_response ( $twiml );
}
/**
* Notify group members via SMS when no agents are available
*/
private function notify_group_members_sms ( $group_id , $caller_number , $queue_name ) {
$members = TWP_Agent_Groups :: get_group_members ( $group_id );
$twilio = new TWP_Twilio_API ();
2025-08-11 20:31:48 -07:00
// Get SMS from number with proper priority (no workflow context here)
$from_number = TWP_Twilio_API :: get_sms_from_number ();
if ( empty ( $from_number ) || empty ( $members )) {
2025-08-06 15:25:47 -07:00
return ;
}
$message = " Call waiting in queue ' { $queue_name } ' from { $caller_number } . Text '1' to this number to receive the next available call. " ;
foreach ( $members as $member ) {
$agent_phone = get_user_meta ( $member -> user_id , 'twp_phone_number' , true );
if ( ! empty ( $agent_phone )) {
2025-08-11 20:31:48 -07:00
// Send SMS notification with proper from number
$twilio -> send_sms ( $agent_phone , $message , $from_number );
2025-08-06 15:25:47 -07:00
// Log the notification
2025-08-11 20:31:48 -07:00
error_log ( " TWP: SMS notification sent to agent { $member -> user_id } at { $agent_phone } from { $from_number } " );
2025-08-06 15:25:47 -07:00
}
}
}
/**
* Handle agent ready SMS ( when agent texts " 1 " )
*/
2025-08-11 20:31:48 -07:00
private function handle_agent_ready_sms ( $incoming_number ) {
error_log ( 'TWP Agent Ready: Processing agent ready SMS from incoming_number: ' . $incoming_number );
// Standardized naming: incoming_number = phone number that sent the SMS to us
// Normalize phone number - add + prefix if missing
$agent_number = $incoming_number ;
if ( ! empty ( $incoming_number ) && substr ( $incoming_number , 0 , 1 ) !== '+' ) {
$agent_number = '+' . $incoming_number ;
error_log ( 'TWP Agent Ready: Normalized agent_number to ' . $agent_number );
}
// Validate that this looks like a real phone number before proceeding
if ( ! preg_match ( '/^\+1[0-9]{10}$/' , $agent_number )) {
error_log ( 'TWP Agent Ready: Invalid phone number format: ' . $agent_number . ' - skipping error message send' );
return ; // Don't send error messages to invalid numbers
}
// Find user by phone number - try both original and normalized versions
2025-08-06 15:25:47 -07:00
$users = get_users ( array (
'meta_key' => 'twp_phone_number' ,
2025-08-11 20:31:48 -07:00
'meta_value' => $agent_number ,
2025-08-06 15:25:47 -07:00
'meta_compare' => '='
));
2025-08-11 20:31:48 -07:00
// If not found with normalized number, try original
2025-08-06 15:25:47 -07:00
if ( empty ( $users )) {
2025-08-11 20:31:48 -07:00
$users = get_users ( array (
'meta_key' => 'twp_phone_number' ,
'meta_value' => $incoming_number ,
'meta_compare' => '='
));
error_log ( 'TWP Agent Ready: Tried original phone format: ' . $incoming_number );
}
if ( empty ( $users )) {
error_log ( 'TWP Agent Ready: No user found for agent_number ' . $agent_number );
// Only send error message to valid real phone numbers (not test numbers)
if ( preg_match ( '/^\+1[0-9]{10}$/' , $agent_number ) && ! preg_match ( '/^\+1951234567[0-9]$/' , $agent_number )) {
$twilio = new TWP_Twilio_API ();
// Get default Twilio number for sending error messages
$default_number = TWP_Twilio_API :: get_sms_from_number ();
$twilio -> send_sms ( $agent_number , " Phone number not found in system. Please contact administrator. " , $default_number );
}
2025-08-06 15:25:47 -07:00
return ;
}
$user = $users [ 0 ];
$user_id = $user -> ID ;
2025-08-11 20:31:48 -07:00
error_log ( 'TWP Agent Ready: Found user ID ' . $user_id . ' for agent_number ' . $agent_number );
2025-08-06 15:25:47 -07:00
// Set agent status to available
TWP_Agent_Manager :: set_agent_status ( $user_id , 'available' );
// Check for waiting calls and assign one if available
2025-08-11 20:31:48 -07:00
error_log ( 'TWP Agent Ready: Calling try_assign_call_to_agent for user ' . $user_id );
$assigned_call = $this -> try_assign_call_to_agent ( $user_id , $agent_number );
$twilio = new TWP_Twilio_API ();
// Get default Twilio number for sending confirmation messages
$default_number = TWP_Twilio_API :: get_sms_from_number ();
2025-08-06 15:25:47 -07:00
if ( $assigned_call ) {
2025-08-11 20:31:48 -07:00
$twilio -> send_sms ( $agent_number , " Call assigned! You should receive the call shortly. " , $default_number );
2025-08-06 15:25:47 -07:00
} else {
// No waiting calls, just confirm availability
2025-08-11 20:31:48 -07:00
$twilio -> send_sms ( $agent_number , " Status updated to available. You'll receive the next waiting call. " , $default_number );
2025-08-06 15:25:47 -07:00
}
}
/**
* Handle agent connect webhook ( when agent answers SMS - triggered call )
*/
public function handle_agent_connect ( $request ) {
$params = $request -> get_params ();
$queued_call_id = isset ( $params [ 'queued_call_id' ]) ? intval ( $params [ 'queued_call_id' ]) : 0 ;
2025-08-11 20:31:48 -07:00
$customer_call_sid = isset ( $params [ 'customer_call_sid' ]) ? $params [ 'customer_call_sid' ] : '' ;
2025-08-06 15:25:47 -07:00
$customer_number = isset ( $params [ 'customer_number' ]) ? $params [ 'customer_number' ] : '' ;
$agent_call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
2025-08-11 20:31:48 -07:00
$agent_phone = isset ( $params [ 'agent_phone' ]) ? $params [ 'agent_phone' ] : '' ;
2025-08-06 15:25:47 -07:00
2025-08-11 20:31:48 -07:00
error_log ( 'TWP Agent Connect: Handling agent connect - queued_call_id=' . $queued_call_id . ', customer_call_sid=' . $customer_call_sid . ', agent_call_sid=' . $agent_call_sid );
if ( ! $queued_call_id && ! $customer_call_sid ) {
error_log ( 'TWP Agent Connect: Missing required parameters' );
2025-08-06 15:25:47 -07:00
$twiml = '<Response><Say voice="alice">Unable to connect call.</Say><Hangup/></Response>' ;
return $this -> send_twiml_response ( $twiml );
}
2025-08-11 20:31:48 -07:00
// Get the queued call from database
global $wpdb ;
$calls_table = $wpdb -> prefix . 'twp_queued_calls' ;
if ( $queued_call_id ) {
$queued_call = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT * FROM $calls_table WHERE id = %d " ,
$queued_call_id
));
} else {
$queued_call = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT * FROM $calls_table WHERE call_sid = %s AND status IN ('waiting', 'connecting') " ,
$customer_call_sid
));
}
if ( ! $queued_call ) {
error_log ( 'TWP Agent Connect: Queued call not found' );
$twiml = '<Response><Say voice="alice">The customer call is no longer available.</Say><Hangup/></Response>' ;
return $this -> send_twiml_response ( $twiml );
}
2025-08-06 15:25:47 -07:00
// Create conference to connect agent and customer
2025-08-11 20:31:48 -07:00
$conference_name = 'queue-connect-' . $queued_call -> id . '-' . time ();
error_log ( 'TWP Agent Connect: Creating conference ' . $conference_name );
2025-08-06 15:25:47 -07:00
2025-08-11 20:31:48 -07:00
// Agent TwiML - connect agent to conference
2025-08-06 15:25:47 -07:00
$twiml = '<Response>' ;
$twiml .= '<Say voice="alice">Connecting you to the customer now.</Say>' ;
2025-08-11 20:31:48 -07:00
$twiml .= '<Dial timeout="30" action="' . home_url ( '/wp-json/twilio-webhook/v1/agent-call-status' ) . '">' ;
$twiml .= '<Conference startConferenceOnEnter="true" endConferenceOnExit="true">' . $conference_name . '</Conference>' ;
$twiml .= '</Dial>' ;
2025-08-06 15:25:47 -07:00
$twiml .= '</Response>' ;
2025-08-11 20:31:48 -07:00
// Connect customer to the same conference
$customer_twiml = '<Response>' ;
$customer_twiml .= '<Say voice="alice">An agent is now available. Connecting you now.</Say>' ;
$customer_twiml .= '<Dial timeout="300">' ;
$customer_twiml .= '<Conference startConferenceOnEnter="false" endConferenceOnExit="false">' . $conference_name . '</Conference>' ;
$customer_twiml .= '</Dial>' ;
$customer_twiml .= '<Say voice="alice">The call has ended. Thank you.</Say>' ;
$customer_twiml .= '</Response>' ;
2025-08-06 15:25:47 -07:00
2025-08-11 20:31:48 -07:00
try {
2025-08-06 15:25:47 -07:00
$twilio = new TWP_Twilio_API ();
2025-08-11 20:31:48 -07:00
$update_result = $twilio -> update_call ( $queued_call -> call_sid , array ( 'twiml' => $customer_twiml ));
2025-08-06 15:25:47 -07:00
2025-08-11 20:31:48 -07:00
if ( $update_result [ 'success' ]) {
error_log ( 'TWP Agent Connect: Successfully updated customer call with conference TwiML' );
// Update call status to connected/answered
$updated = $wpdb -> update (
$calls_table ,
array (
'status' => 'answered' ,
'agent_phone' => $agent_phone ,
'agent_call_sid' => $agent_call_sid ,
'answered_at' => current_time ( 'mysql' )
),
array ( 'id' => $queued_call -> id ),
array ( '%s' , '%s' , '%s' , '%s' ),
array ( '%d' )
);
if ( $updated ) {
error_log ( 'TWP Agent Connect: Updated call status to answered' );
} else {
error_log ( 'TWP Agent Connect: Failed to update call status' );
}
// Reorder queue positions after removing this call
TWP_Call_Queue :: reorder_queue ( $queued_call -> queue_id );
} else {
error_log ( 'TWP Agent Connect: Failed to update customer call: ' . ( $update_result [ 'error' ] ? ? 'Unknown error' ));
}
} catch ( Exception $e ) {
error_log ( 'TWP Agent Connect: Exception updating customer call: ' . $e -> getMessage ());
2025-08-06 15:25:47 -07:00
}
2025-08-11 20:31:48 -07:00
error_log ( 'TWP Agent Connect: Returning agent TwiML: ' . $twiml );
2025-08-06 15:25:47 -07:00
return $this -> send_twiml_response ( $twiml );
}
/**
* Try to assign a waiting call to the agent
*/
2025-08-11 20:31:48 -07:00
private function try_assign_call_to_agent ( $user_id , $agent_number ) {
error_log ( 'TWP Call Assignment: Starting try_assign_call_to_agent for user ' . $user_id . ' agent_number ' . $agent_number );
2025-08-06 15:25:47 -07:00
global $wpdb ;
$calls_table = $wpdb -> prefix . 'twp_queued_calls' ;
2025-08-11 20:31:48 -07:00
// Find the longest waiting call that this agent can handle
// Get agent's groups first
$groups_table = $wpdb -> prefix . 'twp_group_members' ;
$agent_groups = $wpdb -> get_col ( $wpdb -> prepare ( "
SELECT group_id FROM $groups_table WHERE user_id = % d
" , $user_id ));
$queues_table = $wpdb -> prefix . 'twp_call_queues' ;
if ( ! empty ( $agent_groups )) {
// Find waiting calls from queues assigned to this agent's groups
$placeholders = implode ( ',' , array_fill ( 0 , count ( $agent_groups ), '%d' ));
$waiting_call = $wpdb -> get_row ( $wpdb -> prepare ( "
2025-08-12 09:12:54 -07:00
SELECT qc .* , q . notification_number as queue_notification_number , q . agent_group_id
2025-08-11 20:31:48 -07:00
FROM $calls_table qc
LEFT JOIN $queues_table q ON qc . queue_id = q . id
WHERE qc . status = 'waiting'
AND ( q . agent_group_id IN ( $placeholders ) OR q . agent_group_id IS NULL )
ORDER BY qc . joined_at ASC
LIMIT 1
" , ... $agent_groups ));
} else {
// Agent not in any group - can only handle calls from queues with no assigned group
$waiting_call = $wpdb -> get_row ( $wpdb -> prepare ( "
2025-08-12 09:12:54 -07:00
SELECT qc .* , q . notification_number as queue_notification_number , q . agent_group_id
2025-08-11 20:31:48 -07:00
FROM $calls_table qc
LEFT JOIN $queues_table q ON qc . queue_id = q . id
WHERE qc . status = % s
AND q . agent_group_id IS NULL
ORDER BY qc . joined_at ASC
LIMIT 1
" , 'waiting'));
}
2025-08-06 15:25:47 -07:00
if ( ! $waiting_call ) {
return false ;
}
2025-08-11 20:31:48 -07:00
// Determine which Twilio number to use as caller ID when calling the agent
// Priority: 1) Queue's workflow_number, 2) Original workflow_number, 3) default_number
$workflow_number = null ;
error_log ( 'TWP Debug: Waiting call data: ' . print_r ( $waiting_call , true ));
// Detailed debugging of phone number selection
2025-08-12 09:12:54 -07:00
error_log ( 'TWP Debug: Queue notification_number field: ' . ( empty ( $waiting_call -> queue_notification_number ) ? 'EMPTY' : $waiting_call -> queue_notification_number ));
2025-08-11 20:31:48 -07:00
error_log ( 'TWP Debug: Original workflow_number field: ' . ( empty ( $waiting_call -> to_number ) ? 'EMPTY' : $waiting_call -> to_number ));
2025-08-12 09:12:54 -07:00
if ( ! empty ( $waiting_call -> queue_notification_number )) {
$workflow_number = $waiting_call -> queue_notification_number ;
error_log ( 'TWP Debug: SELECTED queue notification_number: ' . $workflow_number );
2025-08-11 20:31:48 -07:00
} elseif ( ! empty ( $waiting_call -> to_number )) {
$workflow_number = $waiting_call -> to_number ;
error_log ( 'TWP Debug: SELECTED original workflow_number: ' . $workflow_number );
} else {
$workflow_number = TWP_Twilio_API :: get_sms_from_number ();
error_log ( 'TWP Debug: SELECTED default_number: ' . $workflow_number );
}
error_log ( 'TWP Debug: Final workflow_number for agent call: ' . $workflow_number );
// Make call to agent using the determined workflow number as caller ID
2025-08-06 15:25:47 -07:00
$twilio = new TWP_Twilio_API ();
2025-08-11 20:31:48 -07:00
// Build agent connect URL with parameters
$agent_connect_url = home_url ( '/wp-json/twilio-webhook/v1/agent-connect' ) . '?' . http_build_query ( array (
'queued_call_id' => $waiting_call -> id ,
'customer_number' => $waiting_call -> from_number
));
2025-08-06 15:25:47 -07:00
$call_result = $twilio -> make_call (
2025-08-11 20:31:48 -07:00
$agent_number , // To: agent's phone number
$agent_connect_url , // TwiML URL with parameters
null , // No status callback needed
$workflow_number // From: workflow number as caller ID
2025-08-06 15:25:47 -07:00
);
if ( $call_result [ 'success' ]) {
// Update queued call status
$wpdb -> update (
$calls_table ,
array (
'status' => 'connecting' ,
'answered_at' => current_time ( 'mysql' )
),
array ( 'id' => $waiting_call -> id ),
array ( '%s' , '%s' ),
array ( '%d' )
);
// Set agent to busy
TWP_Agent_Manager :: set_agent_status ( $user_id , 'busy' , $call_result [ 'call_sid' ]);
return true ;
}
return false ;
}
2025-08-11 20:31:48 -07:00
/**
* Handle agent screening - called when agent answers , before connecting to customer
*/
public function handle_agent_screen ( $request ) {
$params = $request -> get_params ();
$queued_call_id = isset ( $params [ 'queued_call_id' ]) ? intval ( $params [ 'queued_call_id' ]) : 0 ;
$customer_number = isset ( $params [ 'customer_number' ]) ? $params [ 'customer_number' ] : '' ;
$customer_call_sid = isset ( $params [ 'customer_call_sid' ]) ? $params [ 'customer_call_sid' ] : '' ;
$agent_call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
error_log ( " TWP Agent Screen: QueuedCallId= { $queued_call_id } , CustomerCallSid= { $customer_call_sid } , AgentCallSid= { $agent_call_sid } " );
if ( ! $queued_call_id || ! $customer_call_sid ) {
$twiml = '<Response><Say voice="alice">Unable to connect call.</Say><Hangup/></Response>' ;
return $this -> send_twiml_response ( $twiml );
}
// Screen the agent - ask them to press a key to confirm they're human
$screen_url = home_url ( '/wp-json/twilio-webhook/v1/agent-confirm' );
$screen_url = add_query_arg ( array (
'queued_call_id' => $queued_call_id ,
'customer_call_sid' => $customer_call_sid ,
'agent_call_sid' => $agent_call_sid
), $screen_url );
// Use proper TwiML generation
$response = new \Twilio\TwiML\VoiceResponse ();
$gather = $response -> gather ([
'timeout' => 10 ,
'numDigits' => 1 ,
'action' => $screen_url ,
'method' => 'POST'
]);
$gather -> say ( 'You have an incoming call. Press any key to accept and connect to the caller.' , [ 'voice' => 'alice' ]);
$response -> say ( 'No response received. Call cancelled.' , [ 'voice' => 'alice' ]);
$response -> hangup ();
return $this -> send_twiml_response ( $response -> asXML ());
}
/**
* Handle agent confirmation - called when agent presses key to confirm
*/
public function handle_agent_confirm ( $request ) {
$params = $request -> get_params ();
$queued_call_id = isset ( $params [ 'queued_call_id' ]) ? intval ( $params [ 'queued_call_id' ]) : 0 ;
$customer_call_sid = isset ( $params [ 'customer_call_sid' ]) ? $params [ 'customer_call_sid' ] : '' ;
$agent_call_sid = isset ( $params [ 'agent_call_sid' ]) ? $params [ 'agent_call_sid' ] : '' ;
$digits = isset ( $params [ 'Digits' ]) ? $params [ 'Digits' ] : '' ;
error_log ( " TWP Agent Confirm: Digits= { $digits } , QueuedCallId= { $queued_call_id } , CustomerCallSid= { $customer_call_sid } , AgentCallSid= { $agent_call_sid } " );
if ( ! $digits ) {
// No key pressed - agent didn't confirm
error_log ( " TWP Agent Confirm: No key pressed, cancelling call " );
// Requeue the call for another agent
$this -> handle_agent_no_answer ( $queued_call_id , 0 , 'no_response' );
$response = new \Twilio\TwiML\VoiceResponse ();
$response -> say ( 'Call cancelled.' , [ 'voice' => 'alice' ]);
$response -> hangup ();
return $this -> send_twiml_response ( $response -> asXML ());
}
// Agent confirmed - now connect both calls to a conference
$conference_name = 'queue-connect-' . $queued_call_id . '-' . time ();
error_log ( " TWP Agent Confirm: Creating conference { $conference_name } to connect customer and agent " );
// Connect agent to conference using proper TwiML
$response = new \Twilio\TwiML\VoiceResponse ();
$response -> say ( 'Connecting you now.' , [ 'voice' => 'alice' ]);
$dial = $response -> dial ();
$dial -> conference ( $conference_name );
// Connect customer to the same conference
$this -> connect_customer_to_conference ( $customer_call_sid , $conference_name , $queued_call_id );
return $this -> send_twiml_response ( $response -> asXML ());
}
/**
* Connect customer to conference
*/
private function connect_customer_to_conference ( $customer_call_sid , $conference_name , $queued_call_id ) {
$twilio = new TWP_Twilio_API ();
// Create TwiML to connect customer to conference using proper SDK
$customer_response = new \Twilio\TwiML\VoiceResponse ();
$customer_response -> say ( 'Connecting you to an agent.' , [ 'voice' => 'alice' ]);
$customer_dial = $customer_response -> dial ();
$customer_dial -> conference ( $conference_name );
$customer_twiml = $customer_response -> asXML ();
// Update the customer call to join the conference
$result = $twilio -> update_call ( $customer_call_sid , array (
'twiml' => $customer_twiml
));
if ( $result [ 'success' ]) {
// Update queued call status to connected
global $wpdb ;
$calls_table = $wpdb -> prefix . 'twp_queued_calls' ;
$wpdb -> update (
$calls_table ,
array (
'status' => 'connected' ,
'answered_at' => current_time ( 'mysql' )
),
array ( 'id' => $queued_call_id ),
array ( '%s' , '%s' ),
array ( '%d' )
);
error_log ( " TWP Agent Confirm: Successfully connected customer { $customer_call_sid } to conference { $conference_name } " );
} else {
error_log ( " TWP Agent Confirm: Failed to connect customer to conference: " . print_r ( $result , true ));
}
}
2025-08-06 15:25:47 -07:00
/**
* Handle outbound agent with from number webhook
*/
public function handle_outbound_agent_with_from ( $request ) {
2025-08-06 16:04:03 -07:00
try {
$params = $request -> get_params ();
2025-08-06 15:25:47 -07:00
2025-08-06 16:04:03 -07:00
// Get parameters from query string or POST body
$target_number = $request -> get_param ( 'target_number' ) ? : '' ;
$from_number = $request -> get_param ( 'from_number' ) ? : '' ;
$agent_call_sid = $request -> get_param ( 'CallSid' ) ? : '' ;
2025-08-06 15:25:47 -07:00
2025-08-06 16:04:03 -07:00
// Log parameters for debugging
error_log ( 'TWP Outbound Webhook - Target: ' . $target_number . ', From: ' . $from_number . ', CallSid: ' . $agent_call_sid );
error_log ( 'TWP Outbound Webhook - All params: ' . print_r ( $params , true ));
2025-08-06 15:25:47 -07:00
2025-08-06 16:04:03 -07:00
if ( $target_number && $from_number ) {
2025-08-07 15:24:29 -07:00
// Create TwiML using SDK directly
$response = new \Twilio\TwiML\VoiceResponse ();
$response -> say ( 'Connecting your outbound call...' , [ 'voice' => 'alice' ]);
$response -> dial ( $target_number , [ 'callerId' => $from_number , 'timeout' => 30 ]);
2025-08-06 16:04:03 -07:00
2025-08-07 15:24:29 -07:00
// If call isn't answered, the TwiML will handle the fallback
return $this -> send_twiml_response ( $response -> asXML ());
2025-08-06 16:04:03 -07:00
} else {
// Enhanced error message with debugging info
$error_msg = 'Unable to process outbound call.' ;
if ( empty ( $target_number )) {
$error_msg .= ' Missing target number.' ;
}
if ( empty ( $from_number )) {
$error_msg .= ' Missing from number.' ;
}
error_log ( 'TWP Outbound Error: ' . $error_msg . ' Params: ' . json_encode ( $params ));
2025-08-07 15:24:29 -07:00
$error_response = new \Twilio\TwiML\VoiceResponse ();
$error_response -> say ( $error_msg , [ 'voice' => 'alice' ]);
$error_response -> hangup ();
return $this -> send_twiml_response ( $error_response -> asXML ());
2025-08-06 16:04:03 -07:00
}
} catch ( Exception $e ) {
error_log ( 'TWP Outbound Webhook Exception: ' . $e -> getMessage ());
2025-08-07 15:24:29 -07:00
$exception_response = new \Twilio\TwiML\VoiceResponse ();
$exception_response -> say ( 'Technical error occurred. Please try again.' , [ 'voice' => 'alice' ]);
$exception_response -> hangup ();
return $this -> send_twiml_response ( $exception_response -> asXML ());
2025-08-06 15:25:47 -07:00
}
}
2025-08-11 20:31:48 -07:00
/**
* Handle agent call status to detect voicemail / no - answer
*/
public function handle_agent_call_status ( $request ) {
$params = $request -> get_params ();
$call_status = isset ( $params [ 'CallStatus' ]) ? $params [ 'CallStatus' ] : '' ;
$call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
$queued_call_id = isset ( $params [ 'queued_call_id' ]) ? intval ( $params [ 'queued_call_id' ]) : 0 ;
$user_id = isset ( $params [ 'user_id' ]) ? intval ( $params [ 'user_id' ]) : 0 ;
$original_call_sid = isset ( $params [ 'original_call_sid' ]) ? $params [ 'original_call_sid' ] : '' ;
// Check for machine detection
$answered_by = isset ( $params [ 'AnsweredBy' ]) ? $params [ 'AnsweredBy' ] : '' ;
$machine_detection_duration = isset ( $params [ 'MachineDetectionDuration' ]) ? $params [ 'MachineDetectionDuration' ] : '' ;
error_log ( " TWP Agent Call Status: CallSid= { $call_sid } , Status= { $call_status } , AnsweredBy= { $answered_by } , QueuedCallId= { $queued_call_id } " );
// Handle different call statuses
switch ( $call_status ) {
case 'no-answer' :
case 'busy' :
case 'failed' :
// Agent didn't answer or was busy - requeue the call or try next agent
$this -> handle_agent_no_answer ( $queued_call_id , $user_id , $call_status );
break ;
case 'answered' :
// Check if it was answered by a machine (voicemail) or human
if ( $answered_by === 'machine' ) {
// Call went to voicemail - treat as no-answer
error_log ( " TWP Agent Call Status: Agent { $user_id } call went to voicemail - requeing " );
$this -> handle_agent_no_answer ( $queued_call_id , $user_id , 'voicemail' );
} else {
// Agent actually answered - they're already set to busy
error_log ( " TWP Agent Call Status: Agent { $user_id } answered call { $call_sid } " );
}
break ;
case 'completed' :
// Check if call was completed because it went to voicemail
if ( $answered_by === 'machine_start' || $answered_by === 'machine_end_beep' || $answered_by === 'machine_end_silence' ) {
// Call went to voicemail - treat as no-answer and requeue
error_log ( " TWP Agent Call Status: Agent { $user_id } call completed via voicemail ( { $answered_by } ) - requeuing " );
$this -> handle_agent_no_answer ( $queued_call_id , $user_id , 'voicemail' );
} else {
// Call completed normally - set agent back to available
TWP_Agent_Manager :: set_agent_status ( $user_id , 'available' );
error_log ( " TWP Agent Call Status: Agent { $user_id } call completed normally, set to available " );
}
break ;
}
// Return empty response
return $this -> send_twiml_response ( '<Response></Response>' );
}
/**
* Handle when agent doesn ' t answer ( voicemail , busy , no - answer )
*/
private function handle_agent_no_answer ( $queued_call_id , $user_id , $status ) {
error_log ( " TWP Agent No Answer: QueuedCallId= { $queued_call_id } , UserId= { $user_id } , Status= { $status } " );
global $wpdb ;
$calls_table = $wpdb -> prefix . 'twp_queued_calls' ;
// Get the queued call info
$queued_call = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT * FROM $calls_table WHERE id = %d " ,
$queued_call_id
));
if ( ! $queued_call ) {
error_log ( " TWP Agent No Answer: Queued call not found for ID { $queued_call_id } " );
return ;
}
// Set agent back to available
TWP_Agent_Manager :: set_agent_status ( $user_id , 'available' );
// Put the call back in waiting status for other agents to pick up
$wpdb -> update (
$calls_table ,
array (
'status' => 'waiting' ,
'answered_at' => null
),
array ( 'id' => $queued_call_id ),
array ( '%s' , '%s' ),
array ( '%d' )
);
error_log ( " TWP Agent No Answer: Call { $queued_call_id } returned to queue, agent { $user_id } set to available " );
// Optionally: Try to assign to another available agent
// $this->try_assign_to_next_agent($queued_call->queue_id, $queued_call_id);
}
2025-08-06 15:25:47 -07:00
}