'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\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 = ''; $twiml .= ''; 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 .= ''; $twiml .= ''; } else { // Incoming call to browser client $twiml .= ''; $twiml .= '' . htmlspecialchars(str_replace('client:', '', $to_number)) . ''; $twiml .= ''; } } else { $twiml .= 'No destination number provided.'; } $twiml .= ''; 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 = ''; $twiml .= ''; // 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 .= 'All our agents are currently busy. We have been notified of your call and will get back to you shortly.'; $twiml .= 'Please stay on the line for voicemail, or hang up and we will call you back.'; // Redirect to voicemail $voicemail_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback'); $twiml .= '' . $voicemail_url . ''; } else { // Other statuses - generic message $twiml .= 'We apologize, but we are unable to connect your call at this time. Please try again later.'; $twiml .= ''; } $twiml .= ''; 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 = ''; $twiml .= ''; $twiml .= 'Please hold while we connect you to an agent.'; // Try browser agents first, then cell agents if (!empty($browser_agents)) { $twiml .= ''; foreach ($browser_agents as $client_name) { $twiml .= '' . htmlspecialchars($client_name) . ''; } $twiml .= ''; } elseif (!empty($cell_agents)) { // No browser agents, try cell phones $twiml .= ''; foreach ($cell_agents as $cell_phone) { $twiml .= '' . htmlspecialchars($cell_phone) . ''; } $twiml .= ''; } else { // No agents available $twiml .= 'All agents are currently unavailable. Please leave a voicemail.'; $twiml .= '' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . ''; } $twiml .= ''; 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 = ''; $twiml .= ''; // 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 .= 'Trying to connect you to another agent.'; $twiml .= ''; foreach ($cell_agents as $cell_phone) { $twiml .= '' . htmlspecialchars($cell_phone) . ''; } $twiml .= ''; // If this also fails, fall through to final fallback below $twiml .= 'All agents are currently busy.'; } else { // No alternative agents available - go to final fallback $twiml .= 'All agents are currently busy.'; } // 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 .= ''; $twiml .= 'Press 1 to request a callback, or press 2 to leave a voicemail.'; $twiml .= ''; // Default to voicemail if no input $twiml .= 'No response received. Transferring you to voicemail.'; $twiml .= '' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . ''; } elseif ($call_data['DialCallStatus'] === 'failed') { // Technical failure - provide different message $twiml .= 'We are experiencing technical difficulties. Please try again later or leave a voicemail.'; $twiml .= '' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . ''; } else { // Other statuses or unknown - generic fallback $twiml .= 'We apologize, but we are unable to connect your call at this time.'; $twiml .= ''; $twiml .= 'Press 1 to request a callback, or press 2 to leave a voicemail.'; $twiml .= ''; $twiml .= '' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . ''; } $twiml .= ''; 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(''); $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(''); $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(''); 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('', 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(''); $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(''); $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(''); $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(''); $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(''); $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(''); $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(''); // 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(''); $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(''); } /** * Handle voicemail complete (after recording) */ public function handle_voicemail_complete($request) { $twiml = ''; $twiml .= ''; $twiml .= 'Thank you for your message. Goodbye.'; $twiml .= ''; $twiml .= ''; 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('', 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('', 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 = 'Returning you to the queue.' . home_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $queue_id) . ''; } else { // Default to callback (digits === '2' or no input) TWP_Callback_Manager::request_callback($phone_number, $queue_id); $twiml = 'Your callback has been requested. We will call you back shortly. Thank you!'; } 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 = 'Your callback has been requested. We will call you back shortly. Thank you!'; } else { $twiml = 'Unable to process your callback request. Please try again.'; } 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 = 'Please hold while we connect the customer.http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.wav'; } else { $twiml = 'Unable to process callback. Hanging up.'; } 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 = 'You are being connected to an agent.' . $conference_name . ''; // Update the agent call to join the same conference $twilio = new TWP_Twilio_API(); $agent_twiml = '' . $conference_name . ''; $twilio->update_call($agent_call_sid, array('twiml' => $agent_twiml)); // Mark callback as completed TWP_Callback_Manager::complete_callback($callback_id); } else { $twiml = 'Unable to connect your call. Please try again later.'; } 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 = 'Unable to process outbound call.'; } 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 = ''; 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 = ''; $twiml .= 'No agents are currently available. Adding you to the queue.'; $twiml .= '' . $queue_name . ''; $twiml .= ''; // 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 = ''; $twiml .= 'No one is available to take your call. Please leave a message after the beep.'; $twiml .= ''; $twiml .= ''; } 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 = 'Unable to connect call.'; 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 = 'The customer call is no longer available.'; 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 = ''; $twiml .= 'Connecting you to the customer now.'; $twiml .= ''; $twiml .= '' . $conference_name . ''; $twiml .= ''; $twiml .= ''; // Connect customer to the same conference $customer_twiml = ''; $customer_twiml .= 'An agent is now available. Connecting you now.'; $customer_twiml .= ''; $customer_twiml .= '' . $conference_name . ''; $customer_twiml .= ''; $customer_twiml .= 'The call has ended. Thank you.'; $customer_twiml .= ''; 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 = 'Unable to connect call.'; 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(''); } /** * 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); } }