2025-08-06 15:25:47 -07:00
|
|
|
<?php
|
|
|
|
/**
|
2025-08-07 15:24:29 -07:00
|
|
|
* Twilio API integration class using official Twilio PHP SDK
|
2025-08-06 15:25:47 -07:00
|
|
|
*/
|
|
|
|
class TWP_Twilio_API {
|
|
|
|
|
2025-08-07 15:24:29 -07:00
|
|
|
private $client;
|
2025-08-06 15:25:47 -07:00
|
|
|
private $phone_number;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor
|
|
|
|
*/
|
|
|
|
public function __construct() {
|
2025-08-07 15:24:29 -07:00
|
|
|
$this->init_sdk_client();
|
2025-08-11 20:31:48 -07:00
|
|
|
// 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;
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-08-07 15:24:29 -07:00
|
|
|
* Initialize Twilio SDK client
|
2025-08-06 15:25:47 -07:00
|
|
|
*/
|
2025-08-07 15:24:29 -07:00
|
|
|
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');
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
|
2025-08-07 15:24:29 -07:00
|
|
|
// 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');
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
|
2025-08-07 15:24:29 -07:00
|
|
|
$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.');
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
2025-08-07 15:24:29 -07:00
|
|
|
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'];
|
2025-08-11 20:31:48 -07:00
|
|
|
$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
|
2025-08-07 15:24:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
$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()
|
|
|
|
];
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forward a call
|
|
|
|
*/
|
|
|
|
public function forward_call($call_sid, $to_number) {
|
2025-08-07 15:24:29 -07:00
|
|
|
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()
|
|
|
|
];
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update an active call
|
|
|
|
*/
|
|
|
|
public function update_call($call_sid, $params) {
|
2025-08-07 15:24:29 -07:00
|
|
|
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()
|
|
|
|
];
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get call details
|
|
|
|
*/
|
|
|
|
public function get_call($call_sid) {
|
2025-08-07 15:24:29 -07:00
|
|
|
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()
|
|
|
|
];
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create TwiML for queue
|
|
|
|
*/
|
|
|
|
public function create_queue_twiml($queue_name, $wait_url = null, $wait_message = null) {
|
2025-08-07 15:24:29 -07:00
|
|
|
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;
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create TwiML for IVR menu
|
|
|
|
*/
|
|
|
|
public function create_ivr_twiml($message, $options = array()) {
|
2025-08-07 15:24:29 -07:00
|
|
|
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;
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send SMS
|
|
|
|
*/
|
2025-08-07 15:24:29 -07:00
|
|
|
public function send_sms($to_number, $message, $from_number = null) {
|
|
|
|
try {
|
2025-08-11 20:31:48 -07:00
|
|
|
// 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.'
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2025-08-07 15:24:29 -07:00
|
|
|
$sms = $this->client->messages->create(
|
|
|
|
$to_number,
|
|
|
|
[
|
2025-08-11 20:31:48 -07:00
|
|
|
'from' => $from,
|
2025-08-07 15:24:29 -07:00
|
|
|
'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()
|
|
|
|
];
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get available phone numbers
|
|
|
|
*/
|
|
|
|
public function get_phone_numbers() {
|
2025-08-07 15:24:29 -07:00
|
|
|
try {
|
|
|
|
$numbers = $this->client->incomingPhoneNumbers->read([], 50);
|
|
|
|
|
|
|
|
$numbers_data = [];
|
|
|
|
foreach ($numbers as $number) {
|
|
|
|
$numbers_data[] = [
|
|
|
|
'sid' => $number->sid ?: '',
|
2025-08-07 16:46:51 -07:00
|
|
|
'phone_number' => $number->phoneNumber ?: '',
|
|
|
|
'friendly_name' => $number->friendlyName ?: $number->phoneNumber ?: 'Unknown',
|
|
|
|
'voice_url' => $number->voiceUrl ?: '',
|
|
|
|
'sms_url' => $number->smsUrl ?: '',
|
2025-08-11 20:31:48 -07:00
|
|
|
'status_callback_url' => $number->statusCallback ?: '',
|
2025-08-07 15:24:29 -07:00
|
|
|
'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()
|
|
|
|
];
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search for available phone numbers
|
|
|
|
*/
|
|
|
|
public function search_available_numbers($country_code = 'US', $area_code = null, $contains = null, $limit = 20) {
|
2025-08-07 15:24:29 -07:00
|
|
|
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[] = [
|
2025-08-07 16:46:51 -07:00
|
|
|
'phone_number' => $number->phoneNumber ?: '',
|
|
|
|
'friendly_name' => $number->friendlyName ?: $number->phoneNumber ?: 'Available Number',
|
2025-08-07 15:24:29 -07:00
|
|
|
'locality' => $number->locality ?: '',
|
|
|
|
'region' => $number->region ?: '',
|
2025-08-07 16:46:51 -07:00
|
|
|
'postal_code' => $number->postalCode ?: '',
|
2025-08-07 15:24:29 -07:00
|
|
|
'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()
|
|
|
|
];
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Purchase a phone number
|
|
|
|
*/
|
|
|
|
public function purchase_phone_number($phone_number, $voice_url = null, $sms_url = null) {
|
2025-08-07 15:24:29 -07:00
|
|
|
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';
|
|
|
|
}
|
|
|
|
|
2025-08-11 20:31:48 -07:00
|
|
|
// 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';
|
|
|
|
|
2025-08-07 15:24:29 -07:00
|
|
|
$number = $this->client->incomingPhoneNumbers->create($params);
|
|
|
|
|
|
|
|
return [
|
|
|
|
'success' => true,
|
|
|
|
'data' => [
|
|
|
|
'sid' => $number->sid,
|
2025-08-07 16:46:51 -07:00
|
|
|
'phone_number' => $number->phoneNumber,
|
|
|
|
'friendly_name' => $number->friendlyName,
|
|
|
|
'voice_url' => $number->voiceUrl,
|
|
|
|
'sms_url' => $number->smsUrl
|
2025-08-07 15:24:29 -07:00
|
|
|
]
|
|
|
|
];
|
|
|
|
} catch (\Twilio\Exceptions\TwilioException $e) {
|
|
|
|
return [
|
|
|
|
'success' => false,
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
'code' => $e->getCode()
|
|
|
|
];
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Release a phone number
|
|
|
|
*/
|
|
|
|
public function release_phone_number($phone_number_sid) {
|
2025-08-07 15:24:29 -07:00
|
|
|
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()
|
|
|
|
];
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Configure phone number webhook
|
|
|
|
*/
|
|
|
|
public function configure_phone_number($phone_sid, $voice_url, $sms_url = null) {
|
2025-08-07 15:24:29 -07:00
|
|
|
try {
|
|
|
|
$params = [
|
|
|
|
'voiceUrl' => $voice_url,
|
|
|
|
'voiceMethod' => 'POST'
|
|
|
|
];
|
|
|
|
|
|
|
|
if ($sms_url) {
|
|
|
|
$params['smsUrl'] = $sms_url;
|
|
|
|
$params['smsMethod'] = 'POST';
|
|
|
|
}
|
|
|
|
|
2025-08-11 20:31:48 -07:00
|
|
|
// 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';
|
|
|
|
|
2025-08-07 15:24:29 -07:00
|
|
|
$number = $this->client->incomingPhoneNumbers($phone_sid)->update($params);
|
|
|
|
|
|
|
|
return [
|
|
|
|
'success' => true,
|
|
|
|
'data' => [
|
|
|
|
'sid' => $number->sid,
|
2025-08-07 16:46:51 -07:00
|
|
|
'phone_number' => $number->phoneNumber,
|
|
|
|
'voice_url' => $number->voiceUrl,
|
|
|
|
'sms_url' => $number->smsUrl
|
2025-08-07 15:24:29 -07:00
|
|
|
]
|
|
|
|
];
|
|
|
|
} catch (\Twilio\Exceptions\TwilioException $e) {
|
|
|
|
return [
|
|
|
|
'success' => false,
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
'code' => $e->getCode()
|
|
|
|
];
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-08-07 15:24:29 -07:00
|
|
|
* Create TwiML helper - returns SDK VoiceResponse
|
2025-08-06 15:25:47 -07:00
|
|
|
*/
|
2025-08-07 15:24:29 -07:00
|
|
|
public function create_twiml() {
|
|
|
|
return new \Twilio\TwiML\VoiceResponse();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the Twilio client instance
|
|
|
|
*/
|
|
|
|
public function get_client() {
|
|
|
|
return $this->client;
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
|
|
|
|
2025-08-11 20:31:48 -07:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2025-08-06 15:25:47 -07:00
|
|
|
/**
|
|
|
|
* Validate webhook signature
|
|
|
|
*/
|
|
|
|
public function validate_webhook_signature($url, $params, $signature) {
|
2025-08-07 15:24:29 -07:00
|
|
|
$validator = new \Twilio\Security\RequestValidator(get_option('twp_twilio_auth_token'));
|
|
|
|
return $validator->validate($signature, $url, $params);
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|
2025-08-11 20:31:48 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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()
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
2025-08-12 07:05:47 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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();
|
2025-08-13 10:15:39 -07:00
|
|
|
// 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;
|
2025-08-12 07:05:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
$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()
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
2025-08-06 15:25:47 -07:00
|
|
|
}
|