2025-08-06 15:25:47 -07:00
< ? php
/**
* Webhook handler class
*/
class TWP_Webhooks {
/**
* 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'
));
// Voicemail callback webhook
register_rest_route ( 'twilio-webhook/v1' , '/voicemail-callback' , array (
'methods' => 'POST' ,
'callback' => array ( $this , 'handle_voicemail_callback' ),
'permission_callback' => '__return_true'
));
// 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 ) {
return new WP_REST_Response ( $twiml , 200 , array (
'Content-Type' => 'text/xml; charset=utf-8'
));
}
/**
* 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
*/
private function handle_sms_webhook () {
$sms_data = array (
'MessageSid' => isset ( $_POST [ 'MessageSid' ]) ? $_POST [ 'MessageSid' ] : '' ,
'From' => isset ( $_POST [ 'From' ]) ? $_POST [ 'From' ] : '' ,
'To' => isset ( $_POST [ 'To' ]) ? $_POST [ 'To' ] : '' ,
'Body' => isset ( $_POST [ 'Body' ]) ? $_POST [ 'Body' ] : ''
);
// Process SMS commands
$command = strtolower ( trim ( $sms_data [ 'Body' ]));
switch ( $command ) {
case '1' :
$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
if ( $status_data [ 'CallStatus' ] === 'completed' ) {
TWP_Call_Queue :: remove_from_queue ( $status_data [ 'CallSid' ]);
TWP_Call_Logger :: log_action ( $status_data [ 'CallSid' ], 'Call removed from queue' );
}
// 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
*/
private function handle_ivr_response () {
$digits = isset ( $_POST [ 'Digits' ]) ? $_POST [ 'Digits' ] : '' ;
$workflow_id = isset ( $_GET [ 'workflow_id' ]) ? intval ( $_GET [ 'workflow_id' ]) : 0 ;
$step_id = isset ( $_GET [ 'step_id' ]) ? intval ( $_GET [ 'step_id' ]) : 0 ;
if ( ! $workflow_id || ! $step_id ) {
$this -> send_default_response ();
return ;
}
$workflow = TWP_Workflow :: get_workflow ( $workflow_id );
if ( ! $workflow ) {
$this -> send_default_response ();
return ;
}
$workflow_data = json_decode ( $workflow -> workflow_data , true );
// Find the step and its options
foreach ( $workflow_data [ 'steps' ] as $step ) {
if ( $step [ 'id' ] == $step_id && isset ( $step [ 'options' ][ $digits ])) {
$option = $step [ 'options' ][ $digits ];
switch ( $option [ 'action' ]) {
case 'forward' :
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$dial = $twiml -> addChild ( 'Dial' );
$dial -> addChild ( 'Number' , $option [ 'number' ]);
echo $twiml -> asXML ();
return ;
case 'queue' :
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$enqueue = $twiml -> addChild ( 'Enqueue' , $option [ 'queue_name' ]);
echo $twiml -> asXML ();
return ;
case 'voicemail' :
$elevenlabs = new TWP_ElevenLabs_API ();
$twiml = TWP_Workflow :: create_voicemail_twiml ( $option , $elevenlabs );
echo $twiml ;
return ;
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' );
echo $twiml -> asXML ();
return ;
}
}
}
// 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' );
echo $twiml -> asXML ();
}
/**
* Handle queue wait
*/
private function handle_queue_wait () {
$queue_id = isset ( $_GET [ 'queue_id' ]) ? intval ( $_GET [ 'queue_id' ]) : 0 ;
$call_sid = isset ( $_POST [ 'CallSid' ]) ? $_POST [ 'CallSid' ] : '' ;
// 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 ;
$elevenlabs = new TWP_ElevenLabs_API ();
// Generate position announcement
$message = " You are currently number $position in the queue. Your call is important to us. " ;
$audio_result = $elevenlabs -> text_to_speech ( $message );
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
if ( $audio_result [ 'success' ]) {
$play = $twiml -> addChild ( 'Play' , $audio_result [ 'file_url' ]);
} else {
$say = $twiml -> addChild ( 'Say' , $message );
$say -> addAttribute ( 'voice' , 'alice' );
}
// Add wait music
$queue = TWP_Call_Queue :: get_queue ( $queue_id );
if ( $queue && $queue -> wait_music_url ) {
$play = $twiml -> addChild ( 'Play' , $queue -> wait_music_url );
$play -> addAttribute ( 'loop' , '0' );
}
echo $twiml -> asXML ();
} else {
$this -> send_default_response ();
}
}
/**
* 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 ();
$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 ;
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 () {
$twiml = new SimpleXMLElement ( '<?xml version="1.0" encoding="UTF-8"?><Response></Response>' );
$say = $twiml -> addChild ( 'Say' , 'Thank you for calling. Goodbye.' );
$say -> addAttribute ( 'voice' , 'alice' );
$twiml -> addChild ( 'Hangup' );
echo $twiml -> asXML ();
}
/**
* 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>' ;
$twilio -> update_call ( $agent_call_sid , array ( 'Twiml' => $agent_twiml ));
// 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 ();
$sms_number = get_option ( 'twp_sms_notification_number' );
if ( empty ( $sms_number ) || empty ( $members )) {
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 )) {
// Send SMS notification
$twilio -> send_sms ( $agent_phone , $message );
// Log the notification
error_log ( " TWP: SMS notification sent to agent { $member -> user_id } at { $agent_phone } " );
}
}
}
/**
* Handle agent ready SMS ( when agent texts " 1 " )
*/
private function handle_agent_ready_sms ( $agent_phone ) {
// Find user by phone number
$users = get_users ( array (
'meta_key' => 'twp_phone_number' ,
'meta_value' => $agent_phone ,
'meta_compare' => '='
));
if ( empty ( $users )) {
// Send error message if agent not found
$twilio = new TWP_Twilio_API ();
$twilio -> send_sms ( $agent_phone , " Phone number not found in system. Please contact administrator. " );
return ;
}
$user = $users [ 0 ];
$user_id = $user -> ID ;
// Set agent status to available
TWP_Agent_Manager :: set_agent_status ( $user_id , 'available' );
// Check for waiting calls and assign one if available
$assigned_call = $this -> try_assign_call_to_agent ( $user_id , $agent_phone );
if ( $assigned_call ) {
$twilio = new TWP_Twilio_API ();
$twilio -> send_sms ( $agent_phone , " Call assigned! You should receive the call shortly. " );
} else {
// No waiting calls, just confirm availability
$twilio = new TWP_Twilio_API ();
$twilio -> send_sms ( $agent_phone , " Status updated to available. You'll receive the next waiting call. " );
}
}
/**
* 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 ;
$customer_number = isset ( $params [ 'customer_number' ]) ? $params [ 'customer_number' ] : '' ;
$agent_call_sid = isset ( $params [ 'CallSid' ]) ? $params [ 'CallSid' ] : '' ;
if ( ! $queued_call_id || ! $customer_number ) {
$twiml = '<Response><Say voice="alice">Unable to connect call.</Say><Hangup/></Response>' ;
return $this -> send_twiml_response ( $twiml );
}
// Create conference to connect agent and customer
$conference_name = 'queue-connect-' . $queued_call_id . '-' . time ();
$twiml = '<Response>' ;
$twiml .= '<Say voice="alice">Connecting you to the customer now.</Say>' ;
$twiml .= '<Dial><Conference>' . $conference_name . '</Conference></Dial>' ;
$twiml .= '</Response>' ;
// Get the customer's call and redirect to conference
global $wpdb ;
$calls_table = $wpdb -> prefix . 'twp_queued_calls' ;
$queued_call = $wpdb -> get_row ( $wpdb -> prepare (
" SELECT * FROM $calls_table WHERE id = %d " ,
$queued_call_id
));
if ( $queued_call ) {
// 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><Conference>' . $conference_name . '</Conference></Dial>' ;
$customer_twiml .= '</Response>' ;
$twilio = new TWP_Twilio_API ();
$twilio -> update_call ( $queued_call -> call_sid , array ( 'Twiml' => $customer_twiml ));
// Update call status to connected
$wpdb -> update (
$calls_table ,
array ( 'status' => 'connected' ),
array ( 'id' => $queued_call_id ),
array ( '%s' ),
array ( '%d' )
);
}
return $this -> send_twiml_response ( $twiml );
}
/**
* Try to assign a waiting call to the agent
*/
private function try_assign_call_to_agent ( $user_id , $agent_phone ) {
global $wpdb ;
$calls_table = $wpdb -> prefix . 'twp_queued_calls' ;
// Find the longest waiting call
$waiting_call = $wpdb -> get_row ( "
SELECT * FROM $calls_table
WHERE status = 'waiting'
ORDER BY joined_at ASC
LIMIT 1
" );
if ( ! $waiting_call ) {
return false ;
}
// Make call to agent
$twilio = new TWP_Twilio_API ();
$call_result = $twilio -> make_call (
$agent_phone ,
home_url ( '/wp-json/twilio-webhook/v1/agent-connect' ),
array (
'queued_call_id' => $waiting_call -> id ,
'customer_number' => $waiting_call -> from_number
)
);
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 ;
}
/**
* 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 ) {
// Create TwiML to call the target number with the specified from number
$twiml = '<?xml version="1.0" encoding="UTF-8"?>' ;
$twiml .= '<Response>' ;
$twiml .= '<Say voice="alice">Connecting your outbound call...</Say>' ;
$twiml .= '<Dial callerId="' . esc_attr ( $from_number ) . '" timeout="30">' ;
$twiml .= '<Number>' . esc_html ( $target_number ) . '</Number>' ;
$twiml .= '</Dial>' ;
$twiml .= '<Say voice="alice">The number you called is not available. Please try again later.</Say>' ;
$twiml .= '</Response>' ;
return $this -> send_twiml_response ( $twiml );
} 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 ));
$twiml = '<?xml version="1.0" encoding="UTF-8"?>' ;
$twiml .= '<Response><Say voice="alice">' . esc_html ( $error_msg ) . '</Say><Hangup/></Response>' ;
return $this -> send_twiml_response ( $twiml );
}
} catch ( Exception $e ) {
error_log ( 'TWP Outbound Webhook Exception: ' . $e -> getMessage ());
$twiml = '<?xml version="1.0" encoding="UTF-8"?>' ;
$twiml .= '<Response><Say voice="alice">Technical error occurred. Please try again.</Say><Hangup/></Response>' ;
2025-08-06 15:25:47 -07:00
return $this -> send_twiml_response ( $twiml );
}
}
}