Files
twilio-wp-plugin/includes/class-twp-webhooks.php
jknapp 8c78e0cb11 Fix urgent voicemail SMS from number and add Discord/Slack notifications
- Fix SMS notifications to use Default SMS From Number instead of destination
- Add Discord webhook notifications for urgent keyword voicemails
- Add Slack webhook notifications for urgent keyword voicemails
- Make notification methods public to allow external calls
- Add urgent_voicemail type support with custom formatting
- Include transcription, keyword, and admin link in notifications
- Use bright red color for urgent alerts in both Discord and Slack

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-13 20:30:58 -07:00

2254 lines
99 KiB
PHP

<?php
/**
* Webhook handler class
*/
class TWP_Webhooks {
/**
* 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;
}
}
}
/**
* 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'
));
// 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'
));
// Voicemail callback webhook
register_rest_route('twilio-webhook/v1', '/voicemail-callback', array(
'methods' => 'POST',
'callback' => array($this, 'handle_voicemail_callback'),
'permission_callback' => '__return_true'
));
// 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'
));
// 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'
));
// 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);
}
),
),
));
// 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) {
// Send raw XML response for Twilio
header('Content-Type: text/xml; charset=utf-8');
echo $twiml;
exit;
}
/**
* 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) {
// Send Discord/Slack notification for missed browser call
require_once dirname(__FILE__) . '/class-twp-notifications.php';
TWP_Notifications::send_call_notification('missed_call', array(
'type' => 'missed_call',
'caller' => $customer_number,
'queue' => 'Browser Phone',
'workflow_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') {
// 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;
$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') {
// 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;
$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) {
// Send Discord/Slack notification for missed call
require_once dirname(__FILE__) . '/class-twp-notifications.php';
TWP_Notifications::send_call_notification('missed_call', array(
'type' => 'missed_call',
'caller' => $customer_number,
'queue' => 'General',
'workflow_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);
}
}
}
/**
* 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
*/
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();
$sms_data = array(
'MessageSid' => isset($params['MessageSid']) ? $params['MessageSid'] : '',
'From' => isset($params['From']) ? $params['From'] : '',
'To' => isset($params['To']) ? $params['To'] : '',
'Body' => isset($params['Body']) ? $params['Body'] : ''
);
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']);
// Process SMS commands
$command = strtolower(trim($sms_data['Body']));
switch ($command) {
case '1':
error_log('TWP SMS Webhook: Agent texted "1" - calling handle_agent_ready_sms');
$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
// 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'] . ')');
}
}
// 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
*/
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()));
if (!$workflow_id || !$step_id) {
return $this->send_twiml_response($this->get_default_twiml());
}
$workflow = TWP_Workflow::get_workflow($workflow_id);
if (!$workflow) {
return $this->send_twiml_response($this->get_default_twiml());
}
$workflow_data = json_decode($workflow->workflow_data, true);
// 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'));
}
// Find the step and its options
foreach ($workflow_data['steps'] as $step) {
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());
// 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']) {
case 'forward':
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$dial = $twiml->addChild('Dial');
$dial->addChild('Number', $option['number']);
return $this->send_twiml_response($twiml->asXML());
case 'queue':
// 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');
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$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());
case 'voicemail':
$elevenlabs = new TWP_ElevenLabs_API();
$twiml = TWP_Workflow::create_voicemail_twiml($option, $elevenlabs);
return $this->send_twiml_response($twiml);
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');
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));
}
}
}
// 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');
return $this->send_twiml_response($twiml->asXML());
}
/**
* Handle queue wait
*/
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);
// 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;
$status = $call->status;
error_log('TWP Queue Wait: Found call in position ' . $position . ' with status ' . $status);
// 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());
}
// For waiting calls, continue with normal queue behavior
// Create basic TwiML response
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
// Simple position announcement
if ($position > 1) {
$message = "You are currently number $position in the queue. Please continue to hold.";
$say = $twiml->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
}
// Add wait music or pause, then redirect back to continue the loop
$queue = TWP_Call_Queue::get_queue($queue_id);
if ($queue && !empty($queue->wait_music_url)) {
$play = $twiml->addChild('Play', $queue->wait_music_url);
} else {
// Add a pause to prevent rapid loops
$pause = $twiml->addChild('Pause');
$pause->addAttribute('length', '15'); // 15 second pause
}
// 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);
} else {
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());
}
}
/**
* 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;
}
/**
* 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();
// Debug logging
error_log('TWP Voicemail Callback Params: ' . json_encode($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 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);
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');
$default_sms_from = get_option('twp_default_sms_number');
if ($notification_number && $default_sms_from) {
$twilio = new TWP_Twilio_API();
$sms_message = "URGENT voicemail from {$from_number}. Keyword: {$keyword}. Check admin panel immediately.";
// Explicitly pass the Default SMS From Number as the third parameter
$twilio->send_sms($notification_number, $sms_message, $default_sms_from);
}
// Send Discord notification if configured
$discord_webhook = get_option('twp_discord_webhook_url');
if ($discord_webhook) {
TWP_Notifications::send_discord_notification($discord_webhook, array(
'type' => 'urgent_voicemail',
'from_number' => $from_number,
'keyword' => $keyword,
'transcription' => $transcription_text,
'voicemail_id' => $voicemail_id,
'admin_url' => admin_url('admin.php?page=twilio-wp-voicemails')
));
}
// Send Slack notification if configured
$slack_webhook = get_option('twp_slack_webhook_url');
if ($slack_webhook) {
TWP_Notifications::send_slack_notification($slack_webhook, array(
'type' => 'urgent_voicemail',
'from_number' => $from_number,
'keyword' => $keyword,
'transcription' => $transcription_text,
'voicemail_id' => $voicemail_id,
'admin_url' => admin_url('admin.php?page=twilio-wp-voicemails')
));
}
}
/**
* 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();
}
private function get_default_twiml() {
$response = new \Twilio\TwiML\VoiceResponse();
$response->say('Thank you for calling. Goodbye.', ['voice' => 'alice']);
$response->hangup();
return $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();
// 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)) {
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 with proper from number
$twilio->send_sms($agent_phone, $message, $from_number);
// Log the notification
error_log("TWP: SMS notification sent to agent {$member->user_id} at {$agent_phone} from {$from_number}");
}
}
}
/**
* Handle agent ready SMS (when agent texts "1")
*/
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
$users = get_users(array(
'meta_key' => 'twp_phone_number',
'meta_value' => $agent_number,
'meta_compare' => '='
));
// If not found with normalized number, try original
if (empty($users)) {
$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);
}
return;
}
$user = $users[0];
$user_id = $user->ID;
error_log('TWP Agent Ready: Found user ID ' . $user_id . ' for agent_number ' . $agent_number);
// Set agent status to available
TWP_Agent_Manager::set_agent_status($user_id, 'available');
// Check for waiting calls and assign one if available
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();
if ($assigned_call) {
$twilio->send_sms($agent_number, "Call assigned! You should receive the call shortly.", $default_number);
} else {
// No waiting calls, just confirm availability
$twilio->send_sms($agent_number, "Status updated to available. You'll receive the next waiting call.", $default_number);
}
}
/**
* 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_call_sid = isset($params['customer_call_sid']) ? $params['customer_call_sid'] : '';
$customer_number = isset($params['customer_number']) ? $params['customer_number'] : '';
$agent_call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
$agent_phone = isset($params['agent_phone']) ? $params['agent_phone'] : '';
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');
$twiml = '<Response><Say voice="alice">Unable to connect call.</Say><Hangup/></Response>';
return $this->send_twiml_response($twiml);
}
// 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);
}
// Create conference to connect agent and customer
$conference_name = 'queue-connect-' . $queued_call->id . '-' . time();
error_log('TWP Agent Connect: Creating conference ' . $conference_name);
// Agent TwiML - connect agent to conference
$twiml = '<Response>';
$twiml .= '<Say voice="alice">Connecting you to the customer now.</Say>';
$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>';
$twiml .= '</Response>';
// 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>';
try {
$twilio = new TWP_Twilio_API();
$update_result = $twilio->update_call($queued_call->call_sid, array('twiml' => $customer_twiml));
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());
}
error_log('TWP Agent Connect: Returning agent TwiML: ' . $twiml);
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_number) {
error_log('TWP Call Assignment: Starting try_assign_call_to_agent for user ' . $user_id . ' agent_number ' . $agent_number);
global $wpdb;
$calls_table = $wpdb->prefix . 'twp_queued_calls';
// 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("
SELECT qc.*, q.notification_number as queue_notification_number, q.agent_group_id
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("
SELECT qc.*, q.notification_number as queue_notification_number, q.agent_group_id
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'));
}
if (!$waiting_call) {
return false;
}
// 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
error_log('TWP Debug: Queue notification_number field: ' . (empty($waiting_call->queue_notification_number) ? 'EMPTY' : $waiting_call->queue_notification_number));
error_log('TWP Debug: Original workflow_number field: ' . (empty($waiting_call->to_number) ? 'EMPTY' : $waiting_call->to_number));
if (!empty($waiting_call->queue_notification_number)) {
$workflow_number = $waiting_call->queue_notification_number;
error_log('TWP Debug: SELECTED queue notification_number: ' . $workflow_number);
} 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
$twilio = new TWP_Twilio_API();
// 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
));
$call_result = $twilio->make_call(
$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
);
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 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));
}
}
/**
* 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());
}
}
/**
* 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);
}
}