'POST', 'callback' => array($this, 'handle_voice_webhook'), 'permission_callback' => '__return_true' )); // SMS webhook register_rest_route('twilio-webhook/v1', '/sms', array( 'methods' => 'POST', 'callback' => array($this, 'handle_sms_webhook'), 'permission_callback' => '__return_true' )); // Status webhook register_rest_route('twilio-webhook/v1', '/status', array( 'methods' => 'POST', 'callback' => array($this, 'handle_status_webhook'), 'permission_callback' => '__return_true' )); // IVR response webhook register_rest_route('twilio-webhook/v1', '/ivr-response', array( 'methods' => 'POST', 'callback' => array($this, 'handle_ivr_response'), 'permission_callback' => '__return_true' )); // Queue wait webhook register_rest_route('twilio-webhook/v1', '/queue-wait', array( 'methods' => 'POST', 'callback' => array($this, 'handle_queue_wait'), 'permission_callback' => '__return_true' )); // Voicemail callback webhook register_rest_route('twilio-webhook/v1', '/voicemail-callback', array( 'methods' => 'POST', 'callback' => array($this, 'handle_voicemail_callback'), 'permission_callback' => '__return_true' )); // Transcription webhook register_rest_route('twilio-webhook/v1', '/transcription', array( 'methods' => 'POST', 'callback' => array($this, 'handle_transcription_webhook'), 'permission_callback' => '__return_true' )); // Callback choice webhook register_rest_route('twilio-webhook/v1', '/callback-choice', array( 'methods' => 'POST', 'callback' => array($this, 'handle_callback_choice'), 'permission_callback' => '__return_true' )); // Request callback webhook register_rest_route('twilio-webhook/v1', '/request-callback', array( 'methods' => 'POST', 'callback' => array($this, 'handle_request_callback'), 'permission_callback' => '__return_true' )); // Callback agent webhook register_rest_route('twilio-webhook/v1', '/callback-agent', array( 'methods' => 'POST', 'callback' => array($this, 'handle_callback_agent'), 'permission_callback' => '__return_true' )); // Callback customer webhook register_rest_route('twilio-webhook/v1', '/callback-customer', array( 'methods' => 'POST', 'callback' => array($this, 'handle_callback_customer'), 'permission_callback' => '__return_true' )); // Outbound agent webhook register_rest_route('twilio-webhook/v1', '/outbound-agent', array( 'methods' => 'POST', 'callback' => array($this, 'handle_outbound_agent'), 'permission_callback' => '__return_true' )); // Ring group result webhook register_rest_route('twilio-webhook/v1', '/ring-group-result', array( 'methods' => 'POST', 'callback' => array($this, 'handle_ring_group_result'), 'permission_callback' => '__return_true' )); // Agent connect webhook register_rest_route('twilio-webhook/v1', '/agent-connect', array( 'methods' => 'POST', 'callback' => array($this, 'handle_agent_connect'), 'permission_callback' => '__return_true' )); // Outbound agent with from number webhook register_rest_route('twilio-webhook/v1', '/outbound-agent-with-from', array( 'methods' => 'POST', 'callback' => array($this, 'handle_outbound_agent_with_from'), 'permission_callback' => '__return_true' )); }); } /** * Handle webhook requests (deprecated - using REST API now) */ public function handle_webhook() { // This method is deprecated and no longer used // Webhooks are now handled via REST API endpoints } /** * Send TwiML response */ private function send_twiml_response($twiml) { return new WP_REST_Response($twiml, 200, array( 'Content-Type' => 'text/xml; charset=utf-8' )); } /** * Verify Twilio signature */ private function verify_twilio_signature() { // Get signature header $signature = isset($_SERVER['HTTP_X_TWILIO_SIGNATURE']) ? $_SERVER['HTTP_X_TWILIO_SIGNATURE'] : ''; if (!$signature) { return false; } // Get current URL $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; $url = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; // Get POST data $postData = file_get_contents('php://input'); parse_str($postData, $params); // Verify signature $twilio = new TWP_Twilio_API(); return $twilio->validate_webhook_signature($url, $params, $signature); } /** * Handle voice webhook */ public function handle_voice_webhook($request) { // Verify Twilio signature if (!$this->verify_twilio_signature()) { return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401)); } $params = $request->get_params(); $call_data = array( 'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '', 'From' => isset($params['From']) ? $params['From'] : '', 'To' => isset($params['To']) ? $params['To'] : '', 'CallStatus' => isset($params['CallStatus']) ? $params['CallStatus'] : '' ); // Log the incoming call TWP_Call_Logger::log_call(array( 'call_sid' => $call_data['CallSid'], 'from_number' => $call_data['From'], 'to_number' => $call_data['To'], 'status' => 'initiated', 'actions_taken' => 'Incoming call received' )); // Check for schedule $schedule_id = isset($params['schedule_id']) ? intval($params['schedule_id']) : 0; if ($schedule_id) { $schedule = TWP_Scheduler::get_schedule($schedule_id); if ($schedule && $schedule->workflow_id) { // Execute workflow TWP_Call_Logger::log_action($call_data['CallSid'], 'Executing workflow: ' . $schedule->schedule_name); $twiml = TWP_Workflow::execute_workflow($schedule->workflow_id, $call_data); TWP_Call_Logger::update_call($call_data['CallSid'], array( 'workflow_id' => $schedule->workflow_id, 'workflow_name' => $schedule->schedule_name )); return $this->send_twiml_response($twiml); } elseif ($schedule && $schedule->forward_number) { // Forward call TWP_Call_Logger::log_action($call_data['CallSid'], 'Forwarding to: ' . $schedule->forward_number); $twiml = new SimpleXMLElement(''); $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 */ private function handle_sms_webhook() { $sms_data = array( 'MessageSid' => isset($_POST['MessageSid']) ? $_POST['MessageSid'] : '', 'From' => isset($_POST['From']) ? $_POST['From'] : '', 'To' => isset($_POST['To']) ? $_POST['To'] : '', 'Body' => isset($_POST['Body']) ? $_POST['Body'] : '' ); // Process SMS commands $command = strtolower(trim($sms_data['Body'])); switch ($command) { case '1': $this->handle_agent_ready_sms($sms_data['From']); break; case 'status': $this->send_status_sms($sms_data['From']); break; case 'help': $this->send_help_sms($sms_data['From']); break; default: // Log SMS for later processing $this->log_sms($sms_data); break; } // Empty response $twiml = new SimpleXMLElement(''); echo $twiml->asXML(); } /** * Handle status webhook */ public function handle_status_webhook($request) { // Verify Twilio signature if (!$this->verify_twilio_signature()) { return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401)); } $params = $request->get_params(); $status_data = array( 'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '', 'CallStatus' => isset($params['CallStatus']) ? $params['CallStatus'] : '', 'CallDuration' => isset($params['CallDuration']) ? intval($params['CallDuration']) : 0 ); // Update call log with status and duration TWP_Call_Logger::update_call($status_data['CallSid'], array( 'status' => $status_data['CallStatus'], 'duration' => $status_data['CallDuration'], 'actions_taken' => 'Call status changed to: ' . $status_data['CallStatus'] )); // Update call status in queue if applicable if ($status_data['CallStatus'] === 'completed') { TWP_Call_Queue::remove_from_queue($status_data['CallSid']); TWP_Call_Logger::log_action($status_data['CallSid'], 'Call removed from queue'); } // Empty response return new WP_REST_Response('', 200, array( 'Content-Type' => 'text/xml; charset=utf-8' )); } /** * Handle IVR response */ private function handle_ivr_response() { $digits = isset($_POST['Digits']) ? $_POST['Digits'] : ''; $workflow_id = isset($_GET['workflow_id']) ? intval($_GET['workflow_id']) : 0; $step_id = isset($_GET['step_id']) ? intval($_GET['step_id']) : 0; if (!$workflow_id || !$step_id) { $this->send_default_response(); return; } $workflow = TWP_Workflow::get_workflow($workflow_id); if (!$workflow) { $this->send_default_response(); return; } $workflow_data = json_decode($workflow->workflow_data, true); // Find the step and its options foreach ($workflow_data['steps'] as $step) { if ($step['id'] == $step_id && isset($step['options'][$digits])) { $option = $step['options'][$digits]; switch ($option['action']) { case 'forward': $twiml = new SimpleXMLElement(''); $dial = $twiml->addChild('Dial'); $dial->addChild('Number', $option['number']); echo $twiml->asXML(); return; case 'queue': $twiml = new SimpleXMLElement(''); $enqueue = $twiml->addChild('Enqueue', $option['queue_name']); echo $twiml->asXML(); return; case 'voicemail': $elevenlabs = new TWP_ElevenLabs_API(); $twiml = TWP_Workflow::create_voicemail_twiml($option, $elevenlabs); echo $twiml; return; case 'message': $twiml = new SimpleXMLElement(''); $say = $twiml->addChild('Say', $option['message']); $say->addAttribute('voice', 'alice'); $twiml->addChild('Hangup'); echo $twiml->asXML(); return; } } } // Invalid option - replay menu $twiml = new SimpleXMLElement(''); $say = $twiml->addChild('Say', 'Invalid option. Please try again.'); $say->addAttribute('voice', 'alice'); $twiml->addChild('Redirect'); echo $twiml->asXML(); } /** * Handle queue wait */ private function handle_queue_wait() { $queue_id = isset($_GET['queue_id']) ? intval($_GET['queue_id']) : 0; $call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] : ''; // Get caller's position in queue global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; $call = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE call_sid = %s", $call_sid )); if ($call) { $position = $call->position; $elevenlabs = new TWP_ElevenLabs_API(); // Generate position announcement $message = "You are currently number $position in the queue. Your call is important to us."; $audio_result = $elevenlabs->text_to_speech($message); $twiml = new SimpleXMLElement(''); if ($audio_result['success']) { $play = $twiml->addChild('Play', $audio_result['file_url']); } else { $say = $twiml->addChild('Say', $message); $say->addAttribute('voice', 'alice'); } // Add wait music $queue = TWP_Call_Queue::get_queue($queue_id); if ($queue && $queue->wait_music_url) { $play = $twiml->addChild('Play', $queue->wait_music_url); $play->addAttribute('loop', '0'); } echo $twiml->asXML(); } else { $this->send_default_response(); } } /** * Handle voicemail callback */ public function handle_voicemail_callback($request) { // Verify Twilio signature if (!$this->verify_twilio_signature()) { return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401)); } $params = $request->get_params(); $recording_url = isset($params['RecordingUrl']) ? $params['RecordingUrl'] : ''; $recording_duration = isset($params['RecordingDuration']) ? intval($params['RecordingDuration']) : 0; $call_sid = isset($params['CallSid']) ? $params['CallSid'] : ''; $from = isset($params['From']) ? $params['From'] : ''; $workflow_id = isset($params['workflow_id']) ? intval($params['workflow_id']) : 0; if ($recording_url) { // Save voicemail record global $wpdb; $table_name = $wpdb->prefix . 'twp_voicemails'; $voicemail_id = $wpdb->insert( $table_name, array( 'workflow_id' => $workflow_id, 'from_number' => $from, 'recording_url' => $recording_url, 'duration' => $recording_duration, 'created_at' => current_time('mysql') ), array('%d', '%s', '%s', '%d', '%s') ); // Log voicemail action if ($call_sid) { TWP_Call_Logger::log_action($call_sid, 'Voicemail recorded (' . $recording_duration . 's)'); } // Send notification email $this->send_voicemail_notification($from, $recording_url, $recording_duration, $voicemail_id); // Schedule transcription if enabled $this->schedule_voicemail_transcription($voicemail_id, $recording_url); } return new WP_REST_Response('', 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'); if ($notification_number) { $twilio = new TWP_Twilio_API(); $sms_message = "URGENT voicemail from {$from_number}. Keyword: {$keyword}. Check admin panel immediately."; $twilio->send_sms($notification_number, $sms_message); } } /** * Send default response */ private function send_default_response() { $response = new \Twilio\TwiML\VoiceResponse(); $response->say('Thank you for calling. Goodbye.', ['voice' => 'alice']); $response->hangup(); echo $response->asXML(); } /** * Send status SMS */ private function send_status_sms($to_number) { $twilio = new TWP_Twilio_API(); $queue_status = TWP_Call_Queue::get_queue_status(); $message = "Queue Status:\n"; foreach ($queue_status as $queue) { $message .= $queue['queue_name'] . ': ' . $queue['waiting_calls'] . " waiting\n"; } $twilio->send_sms($to_number, $message); } /** * Send help SMS */ private function send_help_sms($to_number) { $twilio = new TWP_Twilio_API(); $message = "Available commands:\n"; $message .= "STATUS - Get queue status\n"; $message .= "HELP - Show this message"; $twilio->send_sms($to_number, $message); } /** * Log SMS */ private function log_sms($sms_data) { // Store SMS in database for later processing global $wpdb; $table_name = $wpdb->prefix . 'twp_sms_log'; $wpdb->insert( $table_name, array( 'message_sid' => $sms_data['MessageSid'], 'from_number' => $sms_data['From'], 'to_number' => $sms_data['To'], 'body' => $sms_data['Body'], 'received_at' => current_time('mysql') ), array('%s', '%s', '%s', '%s', '%s') ); } /** * Log call status */ private function log_call_status($status_data) { // Store call status in database global $wpdb; $table_name = $wpdb->prefix . 'twp_call_log'; $wpdb->insert( $table_name, array( 'call_sid' => $status_data['CallSid'], 'status' => $status_data['CallStatus'], 'duration' => $status_data['CallDuration'], 'updated_at' => current_time('mysql') ), array('%s', '%s', '%d', '%s') ); } /** * Handle callback choice webhook */ public function handle_callback_choice($request) { $params = $request->get_params(); $digits = isset($params['Digits']) ? $params['Digits'] : ''; $phone_number = isset($params['Caller']) ? $params['Caller'] : ''; $queue_id = isset($params['queue_id']) ? intval($params['queue_id']) : null; if ($digits === '1') { // User chose to wait - redirect back to queue $twiml = '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(); $sms_number = get_option('twp_sms_notification_number'); if (empty($sms_number) || empty($members)) { return; } $message = "Call waiting in queue '{$queue_name}' from {$caller_number}. Text '1' to this number to receive the next available call."; foreach ($members as $member) { $agent_phone = get_user_meta($member->user_id, 'twp_phone_number', true); if (!empty($agent_phone)) { // Send SMS notification $twilio->send_sms($agent_phone, $message); // Log the notification error_log("TWP: SMS notification sent to agent {$member->user_id} at {$agent_phone}"); } } } /** * Handle agent ready SMS (when agent texts "1") */ private function handle_agent_ready_sms($agent_phone) { // Find user by phone number $users = get_users(array( 'meta_key' => 'twp_phone_number', 'meta_value' => $agent_phone, 'meta_compare' => '=' )); if (empty($users)) { // Send error message if agent not found $twilio = new TWP_Twilio_API(); $twilio->send_sms($agent_phone, "Phone number not found in system. Please contact administrator."); return; } $user = $users[0]; $user_id = $user->ID; // Set agent status to available TWP_Agent_Manager::set_agent_status($user_id, 'available'); // Check for waiting calls and assign one if available $assigned_call = $this->try_assign_call_to_agent($user_id, $agent_phone); if ($assigned_call) { $twilio = new TWP_Twilio_API(); $twilio->send_sms($agent_phone, "Call assigned! You should receive the call shortly."); } else { // No waiting calls, just confirm availability $twilio = new TWP_Twilio_API(); $twilio->send_sms($agent_phone, "Status updated to available. You'll receive the next waiting call."); } } /** * Handle agent connect webhook (when agent answers SMS-triggered call) */ public function handle_agent_connect($request) { $params = $request->get_params(); $queued_call_id = isset($params['queued_call_id']) ? intval($params['queued_call_id']) : 0; $customer_number = isset($params['customer_number']) ? $params['customer_number'] : ''; $agent_call_sid = isset($params['CallSid']) ? $params['CallSid'] : ''; if (!$queued_call_id || !$customer_number) { $twiml = 'Unable to connect call.'; return $this->send_twiml_response($twiml); } // Create conference to connect agent and customer $conference_name = 'queue-connect-' . $queued_call_id . '-' . time(); $twiml = ''; $twiml .= 'Connecting you to the customer now.'; $twiml .= '' . $conference_name . ''; $twiml .= ''; // Get the customer's call and redirect to conference global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $queued_call = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $calls_table WHERE id = %d", $queued_call_id )); if ($queued_call) { // Connect customer to the same conference $customer_twiml = ''; $customer_twiml .= 'An agent is now available. Connecting you now.'; $customer_twiml .= '' . $conference_name . ''; $customer_twiml .= ''; $twilio = new TWP_Twilio_API(); $twilio->update_call($queued_call->call_sid, array('Twiml' => $customer_twiml)); // Update call status to connected $wpdb->update( $calls_table, array('status' => 'connected'), array('id' => $queued_call_id), array('%s'), array('%d') ); } return $this->send_twiml_response($twiml); } /** * Try to assign a waiting call to the agent */ private function try_assign_call_to_agent($user_id, $agent_phone) { global $wpdb; $calls_table = $wpdb->prefix . 'twp_queued_calls'; // Find the longest waiting call $waiting_call = $wpdb->get_row(" SELECT * FROM $calls_table WHERE status = 'waiting' ORDER BY joined_at ASC LIMIT 1 "); if (!$waiting_call) { return false; } // Make call to agent $twilio = new TWP_Twilio_API(); $call_result = $twilio->make_call( $agent_phone, home_url('/wp-json/twilio-webhook/v1/agent-connect'), array( 'queued_call_id' => $waiting_call->id, 'customer_number' => $waiting_call->from_number ) ); if ($call_result['success']) { // Update queued call status $wpdb->update( $calls_table, array( 'status' => 'connecting', 'answered_at' => current_time('mysql') ), array('id' => $waiting_call->id), array('%s', '%s'), array('%d') ); // Set agent to busy TWP_Agent_Manager::set_agent_status($user_id, 'busy', $call_result['call_sid']); return true; } return false; } /** * Handle outbound agent with from number webhook */ public function handle_outbound_agent_with_from($request) { try { $params = $request->get_params(); // Get parameters from query string or POST body $target_number = $request->get_param('target_number') ?: ''; $from_number = $request->get_param('from_number') ?: ''; $agent_call_sid = $request->get_param('CallSid') ?: ''; // Log parameters for debugging error_log('TWP Outbound Webhook - Target: ' . $target_number . ', From: ' . $from_number . ', CallSid: ' . $agent_call_sid); error_log('TWP Outbound Webhook - All params: ' . print_r($params, true)); if ($target_number && $from_number) { // Create TwiML using SDK directly $response = new \Twilio\TwiML\VoiceResponse(); $response->say('Connecting your outbound call...', ['voice' => 'alice']); $response->dial($target_number, ['callerId' => $from_number, 'timeout' => 30]); // If call isn't answered, the TwiML will handle the fallback return $this->send_twiml_response($response->asXML()); } else { // Enhanced error message with debugging info $error_msg = 'Unable to process outbound call.'; if (empty($target_number)) { $error_msg .= ' Missing target number.'; } if (empty($from_number)) { $error_msg .= ' Missing from number.'; } error_log('TWP Outbound Error: ' . $error_msg . ' Params: ' . json_encode($params)); $error_response = new \Twilio\TwiML\VoiceResponse(); $error_response->say($error_msg, ['voice' => 'alice']); $error_response->hangup(); return $this->send_twiml_response($error_response->asXML()); } } catch (Exception $e) { error_log('TWP Outbound Webhook Exception: ' . $e->getMessage()); $exception_response = new \Twilio\TwiML\VoiceResponse(); $exception_response->say('Technical error occurred. Please try again.', ['voice' => 'alice']); $exception_response->hangup(); return $this->send_twiml_response($exception_response->asXML()); } } }