Files
twilio-wp-plugin/includes/class-twp-webhooks.php
2025-08-07 15:24:29 -07:00

1109 lines
44 KiB
PHP

<?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() {
$response = new \Twilio\TwiML\VoiceResponse();
$response->say('Thank you for calling. Goodbye.', ['voice' => 'alice']);
$response->hangup();
echo $response->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) {
try {
$params = $request->get_params();
// 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') ?: '';
// 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));
if ($target_number && $from_number) {
// 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]);
// If call isn't answered, the TwiML will handle the fallback
return $this->send_twiml_response($response->asXML());
} 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));
$error_response = new \Twilio\TwiML\VoiceResponse();
$error_response->say($error_msg, ['voice' => 'alice']);
$error_response->hangup();
return $this->send_twiml_response($error_response->asXML());
}
} catch (Exception $e) {
error_log('TWP Outbound Webhook Exception: ' . $e->getMessage());
$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());
}
}
}