init_sdk_client(); // Try to get the SMS notification number first, or get the first available Twilio number $this->phone_number = get_option('twp_sms_notification_number'); // If no SMS number configured, try to get the first phone number from the account if (empty($this->phone_number)) { $this->phone_number = $this->get_default_phone_number(); } } /** * Get the default phone number from the account */ private function get_default_phone_number() { try { // Get the first phone number from the account $numbers = $this->client->incomingPhoneNumbers->read([], 1); if (!empty($numbers)) { return $numbers[0]->phoneNumber; } } catch (\Exception $e) { error_log('TWP: Unable to get default phone number: ' . $e->getMessage()); } return null; } /** * Initialize Twilio SDK client */ private function init_sdk_client() { // Check if autoloader exists $autoloader_path = TWP_PLUGIN_DIR . 'vendor/autoload.php'; if (!file_exists($autoloader_path)) { error_log('TWP Plugin: Autoloader not found at: ' . $autoloader_path); throw new Exception('Twilio SDK not found. Please run: ./install-twilio-sdk.sh'); } // Load the autoloader require_once $autoloader_path; // Give more detailed error information if (!class_exists('Twilio\Rest\Client')) { $sdk_path = TWP_PLUGIN_DIR . 'vendor/twilio/sdk'; $client_file = $sdk_path . '/Rest/Client.php'; error_log('TWP Plugin: Twilio SDK classes not found.'); error_log('TWP Plugin: Looking for SDK at: ' . $sdk_path); error_log('TWP Plugin: Client.php exists: ' . (file_exists($client_file) ? 'YES' : 'NO')); error_log('TWP Plugin: SDK directory contents: ' . print_r(scandir($sdk_path), true)); throw new Exception('Twilio SDK classes not available. Please reinstall with: ./install-twilio-sdk.sh'); } $account_sid = get_option('twp_twilio_account_sid'); $auth_token = get_option('twp_twilio_auth_token'); if (empty($account_sid) || empty($auth_token)) { throw new Exception('Twilio credentials not configured. Please check your WordPress admin settings.'); } try { $this->client = new \Twilio\Rest\Client($account_sid, $auth_token); error_log('TWP Plugin: Twilio SDK initialized successfully'); } catch (Exception $e) { error_log('TWP Plugin: Failed to initialize Twilio client: ' . $e->getMessage()); throw new Exception('Failed to initialize Twilio SDK: ' . $e->getMessage()); } } /** * Make a phone call */ public function make_call($to_number, $twiml_url, $status_callback = null, $from_number = null) { try { $params = [ 'url' => $twiml_url, 'from' => $from_number ?: $this->phone_number, 'to' => $to_number ]; if ($status_callback) { $params['statusCallback'] = $status_callback; $params['statusCallbackEvent'] = ['initiated', 'ringing', 'answered', 'completed']; $params['statusCallbackMethod'] = 'POST'; $params['timeout'] = 20; // Ring for 20 seconds before giving up $params['machineDetection'] = 'Enable'; // Detect if voicemail answers $params['machineDetectionTimeout'] = 30; // Wait 30 seconds to detect machine } $call = $this->client->calls->create( $to_number, $from_number ?: $this->phone_number, $params ); return [ 'success' => true, 'data' => [ 'sid' => $call->sid, 'status' => $call->status, 'from' => $call->from, 'to' => $call->to, 'direction' => $call->direction, 'price' => $call->price, 'priceUnit' => $call->priceUnit ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Forward a call */ public function forward_call($call_sid, $to_number) { try { $twiml = new \Twilio\TwiML\VoiceResponse(); $twiml->dial($to_number); $call = $this->client->calls($call_sid)->update([ 'twiml' => $twiml->asXML() ]); return [ 'success' => true, 'data' => [ 'sid' => $call->sid, 'status' => $call->status ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Update an active call */ public function update_call($call_sid, $params) { try { $call = $this->client->calls($call_sid)->update($params); return [ 'success' => true, 'data' => [ 'sid' => $call->sid, 'status' => $call->status, 'from' => $call->from, 'to' => $call->to ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Get call details */ public function get_call($call_sid) { try { $call = $this->client->calls($call_sid)->fetch(); return [ 'success' => true, 'data' => [ 'sid' => $call->sid, 'status' => $call->status, 'from' => $call->from, 'to' => $call->to, 'direction' => $call->direction, 'duration' => $call->duration, 'price' => $call->price, 'priceUnit' => $call->priceUnit ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Create TwiML for queue */ public function create_queue_twiml($queue_name, $wait_url = null, $wait_message = null) { try { $response = new \Twilio\TwiML\VoiceResponse(); if ($wait_message) { $response->say($wait_message, ['voice' => 'alice']); } $enqueue = $response->enqueue($queue_name); if ($wait_url) { $enqueue->waitUrl($wait_url); } return $response->asXML(); } catch (Exception $e) { error_log('TWP Plugin: Failed to create queue TwiML: ' . $e->getMessage()); throw $e; } } /** * Create TwiML for IVR menu */ public function create_ivr_twiml($message, $options = array()) { try { $response = new \Twilio\TwiML\VoiceResponse(); $gather = $response->gather([ 'numDigits' => 1, 'timeout' => 10, 'action' => isset($options['action_url']) ? $options['action_url'] : null ]); $gather->say($message, ['voice' => 'alice']); if (!empty($options['no_input_message'])) { $response->say($options['no_input_message'], ['voice' => 'alice']); } return $response->asXML(); } catch (Exception $e) { error_log('TWP Plugin: Failed to create IVR TwiML: ' . $e->getMessage()); throw $e; } } /** * Send SMS */ public function send_sms($to_number, $message, $from_number = null) { try { // Determine the from number $from = $from_number ?: $this->phone_number; // Validate we have a from number if (empty($from)) { error_log('TWP SMS Error: No from number available. Please configure SMS notification number in settings.'); return [ 'success' => false, 'error' => 'No SMS from number configured. Please set SMS notification number in plugin settings.' ]; } $sms = $this->client->messages->create( $to_number, [ 'from' => $from, 'body' => $message ] ); return [ 'success' => true, 'data' => [ 'sid' => $sms->sid, 'status' => $sms->status, 'from' => $sms->from, 'to' => $sms->to, 'body' => $sms->body, 'price' => $sms->price, 'priceUnit' => $sms->priceUnit ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Get available phone numbers */ public function get_phone_numbers() { try { $numbers = $this->client->incomingPhoneNumbers->read([], 50); $numbers_data = []; foreach ($numbers as $number) { $numbers_data[] = [ 'sid' => $number->sid ?: '', 'phone_number' => $number->phoneNumber ?: '', 'friendly_name' => $number->friendlyName ?: $number->phoneNumber ?: 'Unknown', 'voice_url' => $number->voiceUrl ?: '', 'sms_url' => $number->smsUrl ?: '', 'status_callback_url' => $number->statusCallback ?: '', 'capabilities' => [ 'voice' => $number->capabilities ? (bool)$number->capabilities->getVoice() : false, 'sms' => $number->capabilities ? (bool)$number->capabilities->getSms() : false, 'mms' => $number->capabilities ? (bool)$number->capabilities->getMms() : false ] ]; } return [ 'success' => true, 'data' => [ 'incoming_phone_numbers' => $numbers_data ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Search for available phone numbers */ public function search_available_numbers($country_code = 'US', $area_code = null, $contains = null, $limit = 20) { try { $params = ['limit' => $limit]; if ($area_code) { $params['areaCode'] = $area_code; } if ($contains) { $params['contains'] = $contains; } $numbers = $this->client->availablePhoneNumbers($country_code) ->local ->read($params, $limit); $numbers_data = []; foreach ($numbers as $number) { $numbers_data[] = [ 'phone_number' => $number->phoneNumber ?: '', 'friendly_name' => $number->friendlyName ?: $number->phoneNumber ?: 'Available Number', 'locality' => $number->locality ?: '', 'region' => $number->region ?: '', 'postal_code' => $number->postalCode ?: '', 'capabilities' => [ 'voice' => $number->capabilities ? (bool)$number->capabilities->getVoice() : false, 'sms' => $number->capabilities ? (bool)$number->capabilities->getSms() : false, 'mms' => $number->capabilities ? (bool)$number->capabilities->getMms() : false ] ]; } return [ 'success' => true, 'data' => [ 'available_phone_numbers' => $numbers_data ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Purchase a phone number */ public function purchase_phone_number($phone_number, $voice_url = null, $sms_url = null) { try { $params = ['phoneNumber' => $phone_number]; if ($voice_url) { $params['voiceUrl'] = $voice_url; $params['voiceMethod'] = 'POST'; } if ($sms_url) { $params['smsUrl'] = $sms_url; $params['smsMethod'] = 'POST'; } // Add status callback for real-time call state tracking $status_callback_url = home_url('/wp-json/twilio-webhook/v1/status'); $params['statusCallback'] = $status_callback_url; $params['statusCallbackMethod'] = 'POST'; $number = $this->client->incomingPhoneNumbers->create($params); return [ 'success' => true, 'data' => [ 'sid' => $number->sid, 'phone_number' => $number->phoneNumber, 'friendly_name' => $number->friendlyName, 'voice_url' => $number->voiceUrl, 'sms_url' => $number->smsUrl ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Release a phone number */ public function release_phone_number($phone_number_sid) { try { $this->client->incomingPhoneNumbers($phone_number_sid)->delete(); return [ 'success' => true, 'data' => [ 'message' => 'Phone number released successfully' ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Configure phone number webhook */ public function configure_phone_number($phone_sid, $voice_url, $sms_url = null) { try { $params = [ 'voiceUrl' => $voice_url, 'voiceMethod' => 'POST' ]; if ($sms_url) { $params['smsUrl'] = $sms_url; $params['smsMethod'] = 'POST'; } // Add status callback for real-time call state tracking $status_callback_url = home_url('/wp-json/twilio-webhook/v1/status'); $params['statusCallback'] = $status_callback_url; $params['statusCallbackMethod'] = 'POST'; $number = $this->client->incomingPhoneNumbers($phone_sid)->update($params); return [ 'success' => true, 'data' => [ 'sid' => $number->sid, 'phone_number' => $number->phoneNumber, 'voice_url' => $number->voiceUrl, 'sms_url' => $number->smsUrl ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage(), 'code' => $e->getCode() ]; } } /** * Create TwiML helper - returns SDK VoiceResponse */ public function create_twiml() { return new \Twilio\TwiML\VoiceResponse(); } /** * Get the Twilio client instance */ public function get_client() { return $this->client; } /** * Get SMS from number with proper priority */ public static function get_sms_from_number($workflow_id = null) { // Priority 1: If we have a workflow_id, get the workflow's phone number if ($workflow_id) { $workflow = TWP_Workflow::get_workflow($workflow_id); if ($workflow && !empty($workflow->phone_number)) { return $workflow->phone_number; } } // Priority 2: Use default SMS number setting $default_sms_number = get_option('twp_default_sms_number'); if (!empty($default_sms_number)) { return $default_sms_number; } // Priority 3: Fall back to first available Twilio number $twilio = new self(); $phone_numbers = $twilio->get_phone_numbers(); if ($phone_numbers['success'] && !empty($phone_numbers['data']['incoming_phone_numbers'])) { return $phone_numbers['data']['incoming_phone_numbers'][0]['phone_number']; } return null; } /** * Validate webhook signature */ public function validate_webhook_signature($url, $params, $signature) { $validator = new \Twilio\Security\RequestValidator(get_option('twp_twilio_auth_token')); return $validator->validate($signature, $url, $params); } /** * Get call information from Twilio */ public function get_call_info($call_sid) { try { $call = $this->client->calls($call_sid)->fetch(); return [ 'sid' => $call->sid, 'status' => $call->status, 'from' => $call->from, 'to' => $call->to, 'duration' => $call->duration, 'start_time' => $call->startTime ? $call->startTime->format('Y-m-d H:i:s') : null, 'end_time' => $call->endTime ? $call->endTime->format('Y-m-d H:i:s') : null, 'direction' => $call->direction, 'price' => $call->price, 'priceUnit' => $call->priceUnit ]; } catch (\Twilio\Exceptions\TwilioException $e) { error_log('TWP: Error fetching call info for ' . $call_sid . ': ' . $e->getMessage()); return null; } } /** * Toggle status callback for a specific phone number */ public function toggle_number_status_callback($phone_sid, $enable = true) { try { $params = []; if ($enable) { $params['statusCallback'] = home_url('/wp-json/twilio-webhook/v1/status'); $params['statusCallbackMethod'] = 'POST'; } else { // Clear the status callback $params['statusCallback'] = ''; } $number = $this->client->incomingPhoneNumbers($phone_sid)->update($params); return [ 'success' => true, 'data' => [ 'sid' => $number->sid, 'phone_number' => $number->phoneNumber, 'status_callback' => $number->statusCallback, 'enabled' => !empty($number->statusCallback) ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Update all existing phone numbers to include status callbacks */ public function enable_status_callbacks_for_all_numbers() { try { $numbers = $this->get_phone_numbers(); if (!$numbers['success']) { return [ 'success' => false, 'error' => 'Failed to retrieve phone numbers: ' . $numbers['error'] ]; } $status_callback_url = home_url('/wp-json/twilio-webhook/v1/status'); $updated_count = 0; $errors = []; foreach ($numbers['data']['incoming_phone_numbers'] as $number) { try { $this->client->incomingPhoneNumbers($number['sid'])->update([ 'statusCallback' => $status_callback_url, 'statusCallbackMethod' => 'POST' ]); $updated_count++; error_log('TWP: Added status callback to phone number: ' . $number['phone_number']); } catch (\Twilio\Exceptions\TwilioException $e) { $errors[] = 'Failed to update ' . $number['phone_number'] . ': ' . $e->getMessage(); error_log('TWP: Error updating phone number ' . $number['phone_number'] . ': ' . $e->getMessage()); } } return [ 'success' => true, 'data' => [ 'updated_count' => $updated_count, 'total_numbers' => count($numbers['data']['incoming_phone_numbers']), 'errors' => $errors ] ]; } catch (\Twilio\Exceptions\TwilioException $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Generate capability token for Browser Phone */ public function generate_capability_token($client_name = null) { $account_sid = get_option('twp_twilio_account_sid'); $auth_token = get_option('twp_twilio_auth_token'); $twiml_app_sid = get_option('twp_twiml_app_sid'); if (empty($account_sid) || empty($auth_token)) { return [ 'success' => false, 'error' => 'Twilio credentials not configured' ]; } if (empty($twiml_app_sid)) { return [ 'success' => false, 'error' => 'TwiML App SID not configured. Please set up a TwiML App in your Twilio Console.' ]; } try { // Create client name if not provided if (!$client_name) { $current_user = wp_get_current_user(); // Twilio requires alphanumeric characters only - remove all non-alphanumeric $clean_name = preg_replace('/[^a-zA-Z0-9]/', '', $current_user->display_name); if (empty($clean_name)) { $clean_name = 'user'; } $client_name = 'agent' . $current_user->ID . $clean_name; } $capability = new \Twilio\Jwt\ClientToken($account_sid, $auth_token); $capability->allowClientOutgoing($twiml_app_sid); $capability->allowClientIncoming($client_name); $token = $capability->generateToken(3600); // Valid for 1 hour return [ 'success' => true, 'data' => [ 'token' => $token, 'client_name' => $client_name, 'expires_in' => 3600 ] ]; } catch (\Exception $e) { return [ 'success' => false, 'error' => 'Failed to generate capability token: ' . $e->getMessage() ]; } } }