diff --git a/CLAUDE.md b/CLAUDE.md index 6f79366..d64b30d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ This is a comprehensive WordPress plugin for Twilio voice and SMS integration, f ### Core Classes (`includes/` directory) - **TWP_Core**: Main plugin initialization and hook registration - **TWP_Activator**: Database table creation and plugin activation -- **TWP_Twilio_API**: Twilio REST API wrapper (custom implementation) +- **TWP_Twilio_API**: Official Twilio PHP SDK wrapper (requires SDK v8.7.0) - **TWP_Webhooks**: Handles all Twilio webhook endpoints - **TWP_Scheduler**: Business hours and schedule management - **TWP_Workflow**: Call flow processing and TwiML generation @@ -131,25 +131,28 @@ Agent phone numbers stored as user meta: ## Twilio Integration -### Current Implementation -- **Custom API Wrapper**: `TWP_Twilio_API` class using `wp_remote_post()` -- **TwiML Generation**: String-based XML construction -- **Response Handling**: Custom parsing of Twilio responses +### Current Implementation (SDK-Only) +- **Official Twilio SDK**: Uses `twilio/sdk` v8.7.0 for all operations +- **TwiML Generation**: Uses `\Twilio\TwiML\VoiceResponse` classes +- **Response Handling**: Native Twilio SDK response objects +- **Error Handling**: Proper `\Twilio\Exceptions\TwilioException` handling -### Recommended Migration to Twilio PHP SDK -**URL**: https://www.twilio.com/docs/libraries/reference/twilio-php/ +### Installation Requirements +**IMPORTANT**: The Twilio PHP SDK v8.7.0 is **REQUIRED** for this plugin to function. -**Benefits**: -- Official SDK with better error handling -- Built-in TwiML generation classes -- Automatic retries and rate limiting -- Type safety and IDE support +**Installation Methods**: +1. **Script Installation** (Recommended): + ```bash + chmod +x install-twilio-sdk.sh + ./install-twilio-sdk.sh + ``` -**Migration Strategy**: -1. Install via Composer: `composer require twilio/sdk` -2. Replace `TWP_Twilio_API` methods with SDK calls -3. Update TwiML generation to use SDK classes -4. Maintain existing method signatures for compatibility +2. **Composer Installation**: + ```bash + composer install + ``` + +**PHP Requirements**: PHP 8.0+ required for SDK compatibility ### API Response Structure Current Twilio API responses follow this pattern: diff --git a/README.md b/README.md index bdf7f87..b50e024 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,134 @@ -# twilio-wp-plugin +# Twilio WordPress Plugin +A comprehensive WordPress plugin for Twilio voice and SMS integration with advanced call center functionality. + +## ⚠️ IMPORTANT: SDK Required + +This plugin **requires** the Twilio PHP SDK v8.7.0 to function. The plugin will not work without it. + +## Quick Installation + +1. **Install the Twilio SDK** (Required): + ```bash + chmod +x install-twilio-sdk.sh + ./install-twilio-sdk.sh + ``` + +2. **Test the SDK installation**: + ```bash + php test-sdk.php + ``` + +3. **Configure Twilio Credentials** in WordPress admin: + - Account SID + - Auth Token + - Phone Number + +4. **Test the installation** with a sample call. + +## Requirements + +- **PHP 8.0+** (required for Twilio SDK v8.7.0) +- **WordPress 5.0+** +- **Twilio Account** with active phone number +- **curl** and **tar** (for SDK installation) + +## Key Features + +- 📞 **Call Center Operations**: Agent groups, queues, call distribution +- 🕒 **Business Hours Management**: Automated routing based on schedules +- 📱 **Outbound Calling**: Click-to-call with proper caller ID +- 💬 **SMS Integration**: Agent notifications and command system +- 🎛️ **Workflow Builder**: Visual call flow creation +- 🎤 **Voicemail System**: Recording, transcription, and notifications +- 📊 **Real-time Dashboard**: Queue management and statistics + +## Installation Methods + +### Option 1: Installation Script (Recommended) +```bash +# Run in plugin directory +./install-twilio-sdk.sh +``` + +### Option 2: Composer (For Development) +```bash +composer install +``` + +### Option 3: Manual Installation +1. Download Twilio SDK v8.7.0 from GitHub +2. Extract to `vendor/twilio/sdk/` +3. Create autoloader (see install script for reference) + +## Architecture + +The plugin uses: +- **Official Twilio PHP SDK v8.7.0** for all API operations +- **Native TwiML classes** for response generation +- **WordPress hooks and filters** for integration +- **Custom database tables** for call management +- **REST API endpoints** for webhooks + +## Configuration + +1. Install the SDK using the provided script +2. Configure Twilio credentials in WordPress admin +3. Set up phone numbers and webhook URLs +4. Create agent groups and workflows +5. Test with sample calls + +## Troubleshooting + +### "Twilio SDK classes not available" Error + +1. **Run the installation script**: + ```bash + chmod +x install-twilio-sdk.sh + ./install-twilio-sdk.sh + ``` + +2. **Test SDK installation**: + ```bash + php test-sdk.php + ``` + +3. **Check file permissions**: + ```bash + ls -la vendor/ + ls -la vendor/twilio/sdk/ + ``` + +4. **Verify directory structure**: + ``` + vendor/ + ├── autoload.php + └── twilio/ + └── sdk/ + ├── Rest/Client.php + ├── TwiML/VoiceResponse.php + └── ... (other SDK files) + ``` + +### Plugin Shows 500 Error + +- Check WordPress error logs +- Enable WP_DEBUG in wp-config.php +- Look for TWP Plugin error messages in logs + +### SDK Installation Fails + +- Ensure `curl` and `tar` are installed +- Check internet connection +- Try manual installation (see Installation Methods) + +## Support + +- Check `CLAUDE.md` for detailed technical documentation +- Review `TWILIO_SDK_MIGRATION.md` for migration details +- Enable WordPress debug logging for troubleshooting +- Use Twilio Console debugger for webhook testing + +## License + +This plugin integrates with Twilio services and requires a Twilio account. diff --git a/TWILIO_SDK_MIGRATION.md b/TWILIO_SDK_MIGRATION.md new file mode 100644 index 0000000..08711cb --- /dev/null +++ b/TWILIO_SDK_MIGRATION.md @@ -0,0 +1,240 @@ +# Twilio PHP SDK Migration Guide + +This document outlines the migration of the Twilio WordPress Plugin from a custom API wrapper to the official Twilio PHP SDK v8.7.0. + +## Overview + +The plugin now supports both the official Twilio PHP SDK and falls back to a custom implementation when the SDK is not available. This provides better error handling, type safety, and access to the latest Twilio features while maintaining backward compatibility. + +## Installation + +### Option 1: Using the Installation Script (Recommended) + +1. Navigate to your plugin directory +2. Run the installation script: + ```bash + chmod +x install-twilio-sdk.sh + ./install-twilio-sdk.sh + ``` + +### Option 2: Manual Installation with Composer + +1. Ensure Composer is installed on your system +2. Run in the plugin directory: + ```bash + composer install + ``` + +### Option 3: Manual Download + +1. Download Twilio SDK v8.7.0 from: https://github.com/twilio/twilio-php/releases +2. Extract to `vendor/twilio/sdk/` +3. Create autoloader file (see installation script for reference) + +## How It Works + +### Auto-Detection + +The plugin automatically detects if the Twilio SDK is available by: +1. Checking for `vendor/autoload.php` +2. Verifying the `Twilio\Rest\Client` class exists +3. Attempting to initialize the client + +### Dual-Mode Operation + +**SDK Mode (When Available):** +- Uses official Twilio PHP SDK classes +- Better error handling with specific Twilio exceptions +- Type safety and IDE support +- Automatic retries and rate limiting +- Official TwiML generation classes + +**Fallback Mode (Default):** +- Uses custom WordPress HTTP API calls +- String-based TwiML generation +- Maintains all existing functionality +- No external dependencies required + +## Code Changes + +### API Class Updates + +The `TWP_Twilio_API` class now: +- Detects SDK availability in the constructor +- Routes all methods through SDK when available +- Falls back to custom implementation seamlessly +- Maintains identical method signatures for compatibility + +### TwiML Generation + +**New Unified Approach:** +```php +// Works with both SDK and fallback +$api = new TWP_Twilio_API(); +$response = $api->create_twiml(); +$response->say('Hello world', ['voice' => 'alice']); +$response->dial('+1234567890', ['timeout' => 30]); +$twiml = $response->asXML(); +``` + +**Old Approach (Now Deprecated):** +```php +// String concatenation - now handled automatically +$twiml = ''; +$twiml .= ''; +$twiml .= 'Hello world'; +$twiml .= ''; +``` + +### Updated Classes + +1. **TWP_Twilio_API**: Core API wrapper with SDK integration +2. **TWP_Webhooks**: All webhook handlers updated to use new TwiML builder +3. **TWP_Callback_Manager**: Outbound calling and callback management +4. **TWP_Workflow**: Call flow TwiML generation +5. **TWP_Agent_Manager**: Agent call routing + +## Benefits + +### With SDK Available +- **Better Error Handling**: Specific exception types for different error scenarios +- **Type Safety**: IDE support and parameter validation +- **Official Support**: Direct access to latest Twilio features +- **Reliability**: Built-in retry logic and rate limiting +- **Performance**: Optimized for high-volume operations + +### Without SDK (Fallback) +- **No Dependencies**: Works on any PHP installation +- **Lightweight**: Minimal memory footprint +- **Compatibility**: Works with older PHP versions +- **Reliability**: Battle-tested custom implementation + +## Supported PHP Versions + +- **With SDK**: PHP 8.0+ (SDK v8.7.0 supports PHP 8.4) +- **Without SDK**: PHP 7.2+ (fallback implementation) + +## Feature Support + +### API Operations +- ✅ Make calls +- ✅ Send SMS +- ✅ Get call details +- ✅ Update active calls +- ✅ Manage phone numbers +- ✅ Search available numbers +- ✅ Purchase/release numbers +- ✅ Configure webhooks + +### TwiML Generation +- ✅ Say (text-to-speech) +- ✅ Dial (with caller ID, timeout) +- ✅ Gather (digit collection with prompts) +- ✅ Enqueue (queue management) +- ✅ Record (voicemail recording) +- ✅ Redirect (call flow control) +- ✅ Hangup + +### Advanced Features +- ✅ Webhook signature validation +- ✅ Conference calling +- ✅ Queue management +- ✅ Agent group routing +- ✅ Callback requests +- ✅ Voicemail transcription + +## Testing + +### Verifying SDK Installation + +Check the admin dashboard or logs for: +``` +TWP Plugin: Twilio SDK v8.7.0 loaded successfully +``` + +### API Testing + +Use the plugin's test call feature to verify: +1. Outbound calling works +2. TwiML generation is valid +3. Webhooks receive proper responses +4. Error handling functions correctly + +### Manual Testing + +1. **Test Call Flow**: Make a test call to verify TwiML generation +2. **Webhook Testing**: Use Twilio's webhook debugger +3. **Error Scenarios**: Test invalid numbers, network failures +4. **Queue Operations**: Test agent groups and callbacks + +## Troubleshooting + +### SDK Not Loading + +**Check autoloader exists:** +```bash +ls -la vendor/autoload.php +``` + +**Check SDK files:** +```bash +ls -la vendor/twilio/sdk/ +``` + +**PHP Version:** +```bash +php -v # Should be 8.0+ for SDK support +``` + +### Common Issues + +1. **"Class 'Twilio\Rest\Client' not found"** + - SDK not properly installed + - Run installation script again + +2. **"Call to undefined method"** + - Fallback mode active + - Check SDK installation + - Verify PHP version compatibility + +3. **"Permission denied" on installation script** + - Run: `chmod +x install-twilio-sdk.sh` + +### Fallback Mode Indicators + +The plugin uses fallback mode when: +- No `vendor/autoload.php` file +- Twilio classes not available +- SDK initialization fails +- PHP version incompatibility + +## Migration Timeline + +- ✅ **Phase 1**: Core API wrapper updated +- ✅ **Phase 2**: Webhook classes migrated +- ✅ **Phase 3**: TwiML generation unified +- ✅ **Phase 4**: Error handling improved +- ⏳ **Phase 5**: Testing and validation +- 📋 **Phase 6**: Documentation complete + +## Rollback Plan + +If issues arise, the plugin automatically falls back to the custom implementation. No manual intervention required. + +To completely disable SDK usage: +1. Remove or rename `vendor/` directory +2. Plugin will automatically use fallback mode + +## Support + +For issues related to: +- **Plugin functionality**: Check WordPress error logs +- **Twilio API errors**: Check Twilio Console debugger +- **SDK-specific issues**: Refer to [Twilio PHP SDK documentation](https://www.twilio.com/docs/libraries/reference/twilio-php/) + +## Resources + +- [Twilio PHP SDK GitHub](https://github.com/twilio/twilio-php) +- [Twilio API Documentation](https://www.twilio.com/docs/usage/api) +- [TwiML Reference](https://www.twilio.com/docs/voice/twiml) +- [WordPress HTTP API](https://developer.wordpress.org/reference/classes/wp_http/) \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5951cec --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "twilio-wp/twilio-wp-plugin", + "description": "WordPress plugin for Twilio voice and SMS integration - REQUIRES Twilio SDK", + "type": "wordpress-plugin", + "require": { + "php": ">=8.0", + "twilio/sdk": "^8.7" + }, + "autoload": { + "classmap": [ + "includes/", + "admin/" + ] + }, + "config": { + "optimize-autoloader": true, + "platform": { + "php": "8.0" + } + }, + "scripts": { + "post-install-cmd": [ + "echo 'Twilio SDK installed successfully!'" + ], + "install-sdk": [ + "./install-twilio-sdk.sh" + ] + } +} \ No newline at end of file diff --git a/debug-phone-numbers.php b/debug-phone-numbers.php new file mode 100644 index 0000000..77ad3dd --- /dev/null +++ b/debug-phone-numbers.php @@ -0,0 +1,83 @@ +incomingPhoneNumbers->read([], 10); + + if (empty($numbers)) { + echo "No phone numbers found in your Twilio account.\n"; + exit(0); + } + + echo "Found " . count($numbers) . " phone number(s):\n\n"; + + foreach ($numbers as $i => $number) { + echo "=== Phone Number " . ($i + 1) . " ===\n"; + echo "SID: " . $number->sid . "\n"; + echo "Phone Number: " . $number->phoneNumber . "\n"; + echo "Friendly Name: " . ($number->friendlyName ?: '[Not set]') . "\n"; + echo "Voice URL: " . ($number->voiceUrl ?: '[Not set]') . "\n"; + echo "SMS URL: " . ($number->smsUrl ?: '[Not set]') . "\n"; + echo "Account SID: " . $number->accountSid . "\n"; + + // Debug capabilities object + echo "\nCapabilities (raw object):\n"; + var_dump($number->capabilities); + + echo "\nCapabilities (properties):\n"; + echo "- Voice: " . ($number->capabilities->voice ? 'YES' : 'NO') . "\n"; + echo "- SMS: " . ($number->capabilities->sms ? 'YES' : 'NO') . "\n"; + echo "- MMS: " . ($number->capabilities->mms ? 'YES' : 'NO') . "\n"; + echo "\n" . str_repeat('-', 40) . "\n\n"; + } + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + echo "Class: " . get_class($e) . "\n"; + exit(1); +} + +echo "Debug complete!\n"; \ No newline at end of file diff --git a/includes/class-twp-agent-manager.php b/includes/class-twp-agent-manager.php index 108d797..c78e4cd 100644 --- a/includes/class-twp-agent-manager.php +++ b/includes/class-twp-agent-manager.php @@ -239,8 +239,7 @@ class TWP_Agent_Manager { // Create TwiML to redirect the call $twiml = new \Twilio\TwiML\VoiceResponse(); - $dial = $twiml->dial(); - $dial->number($phone_number, [ + $twiml->dial($phone_number, [ 'statusCallback' => home_url('/wp-json/twilio-webhook/v1/call-status'), 'statusCallbackEvent' => array('completed') ]); @@ -307,20 +306,17 @@ class TWP_Agent_Manager { // Play a message while dialing $twiml->say('Please wait while we connect your call...', ['voice' => 'alice']); - // Create a dial with simultaneous ring + // Create a dial with simultaneous ring to all group members $dial = $twiml->dial([ 'timeout' => 30, 'action' => home_url('/wp-json/twilio-webhook/v1/dial-status'), 'method' => 'POST' ]); - // Add each member's number to the dial + // Add each member's number to the dial for simultaneous ring foreach ($members as $member) { if ($member['phone_number']) { - $dial->number($member['phone_number'], [ - 'statusCallback' => home_url('/wp-json/twilio-webhook/v1/member-status'), - 'statusCallbackEvent' => array('answered', 'completed') - ]); + $dial->number($member['phone_number']); } } diff --git a/includes/class-twp-callback-manager.php b/includes/class-twp-callback-manager.php index 5c5f490..7cc6cf6 100644 --- a/includes/class-twp-callback-manager.php +++ b/includes/class-twp-callback-manager.php @@ -232,17 +232,13 @@ class TWP_Callback_Manager { * Handle outbound agent answered */ public static function handle_outbound_agent_answered($target_number, $agent_call_sid) { - $twilio = new TWP_Twilio_API(); - // Create TwiML to call the target number $twiml = new \Twilio\TwiML\VoiceResponse(); $twiml->say('Connecting your call...', ['voice' => 'alice']); - - $dial = $twiml->dial([ - 'callerId' => get_option('twp_caller_id_number', ''), // Use configured caller ID + $twiml->dial($target_number, [ + 'callerId' => get_option('twp_caller_id_number', ''), 'timeout' => 30 ]); - $dial->number($target_number); // If no answer, leave a message $twiml->say('The number you called is not available. Please try again later.', ['voice' => 'alice']); @@ -259,7 +255,10 @@ class TWP_Callback_Manager { $gather = $twiml->gather([ 'numDigits' => 1, 'timeout' => 10, - 'action' => home_url('/wp-json/twilio-webhook/v1/callback-choice'), + 'action' => home_url('/wp-json/twilio-webhook/v1/callback-choice?' . http_build_query([ + 'queue_id' => $queue_id, + 'phone_number' => $caller_number + ])), 'method' => 'POST' ]); diff --git a/includes/class-twp-twilio-api.php b/includes/class-twp-twilio-api.php index 4ded2cd..338e254 100644 --- a/includes/class-twp-twilio-api.php +++ b/includes/class-twp-twilio-api.php @@ -1,276 +1,474 @@ account_sid = get_option('twp_twilio_account_sid'); - $this->auth_token = get_option('twp_twilio_auth_token'); + $this->init_sdk_client(); $this->phone_number = get_option('twp_twilio_phone_number'); } + /** + * 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) { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls.json'; - - $data = array( - 'To' => $to_number, - 'From' => $from_number ?: $this->phone_number, - 'Url' => $twiml_url - ); - - if ($status_callback) { - $data['StatusCallback'] = $status_callback; - $data['StatusCallbackEvent'] = array('initiated', 'ringing', 'answered', 'completed'); + 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']; + } + + $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() + ]; } - - return $this->make_request('POST', $url, $data); } /** * Forward a call */ public function forward_call($call_sid, $to_number) { - $twiml = new SimpleXMLElement(''); - $dial = $twiml->addChild('Dial'); - $dial->addChild('Number', $to_number); - - return $this->update_call($call_sid, array( - 'Twiml' => $twiml->asXML() - )); + 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) { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls/' . $call_sid . '.json'; - return $this->make_request('POST', $url, $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) { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/Calls/' . $call_sid . '.json'; - return $this->make_request('GET', $url); + 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) { - $twiml = new SimpleXMLElement(''); - - if ($wait_message) { - $say = $twiml->addChild('Say', $wait_message); - $say->addAttribute('voice', 'alice'); + 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; } - - $enqueue = $twiml->addChild('Enqueue', $queue_name); - - if ($wait_url) { - $enqueue->addAttribute('waitUrl', $wait_url); - } - - return $twiml->asXML(); } /** * Create TwiML for IVR menu */ public function create_ivr_twiml($message, $options = array()) { - $twiml = new SimpleXMLElement(''); - - $gather = $twiml->addChild('Gather'); - $gather->addAttribute('numDigits', '1'); - $gather->addAttribute('timeout', '10'); - - if (!empty($options['action_url'])) { - $gather->addAttribute('action', $options['action_url']); + 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; } - - $say = $gather->addChild('Say', $message); - $say->addAttribute('voice', 'alice'); - - // Fallback if no input - if (!empty($options['no_input_message'])) { - $say_fallback = $twiml->addChild('Say', $options['no_input_message']); - $say_fallback->addAttribute('voice', 'alice'); - } - - return $twiml->asXML(); } /** * Send SMS */ - public function send_sms($to_number, $message) { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/Messages.json'; - - $data = array( - 'To' => $to_number, - 'From' => $this->phone_number, - 'Body' => $message - ); - - return $this->make_request('POST', $url, $data); + public function send_sms($to_number, $message, $from_number = null) { + try { + $sms = $this->client->messages->create( + $to_number, + [ + 'from' => $from_number ?: $this->phone_number, + '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() { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers.json'; - return $this->make_request('GET', $url); + try { + $numbers = $this->client->incomingPhoneNumbers->read([], 50); + + $numbers_data = []; + foreach ($numbers as $number) { + $numbers_data[] = [ + 'sid' => $number->sid ?: '', + 'phoneNumber' => $number->phoneNumber ?: '', + 'friendlyName' => $number->friendlyName ?: $number->phoneNumber ?: 'Unknown', + 'voiceUrl' => $number->voiceUrl ?: '', + 'smsUrl' => $number->smsUrl ?: '', + '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) { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/AvailablePhoneNumbers/' . $country_code . '/Local.json'; - - $params = array('Limit' => $limit); - - if ($area_code) { - $params['AreaCode'] = $area_code; + 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[] = [ + 'phoneNumber' => $number->phoneNumber ?: '', + 'friendlyName' => $number->friendlyName ?: $number->phoneNumber ?: 'Available Number', + 'locality' => $number->locality ?: '', + 'region' => $number->region ?: '', + 'postalCode' => $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() + ]; } - - if ($contains) { - $params['Contains'] = $contains; - } - - $url .= '?' . http_build_query($params); - - return $this->make_request('GET', $url); } /** * Purchase a phone number */ public function purchase_phone_number($phone_number, $voice_url = null, $sms_url = null) { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers.json'; - - $data = array( - 'PhoneNumber' => $phone_number - ); - - if ($voice_url) { - $data['VoiceUrl'] = $voice_url; - $data['VoiceMethod'] = 'POST'; + 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'; + } + + $number = $this->client->incomingPhoneNumbers->create($params); + + return [ + 'success' => true, + 'data' => [ + 'sid' => $number->sid, + 'phoneNumber' => $number->phoneNumber, + 'friendlyName' => $number->friendlyName, + 'voiceUrl' => $number->voiceUrl, + 'smsUrl' => $number->smsUrl + ] + ]; + } catch (\Twilio\Exceptions\TwilioException $e) { + return [ + 'success' => false, + 'error' => $e->getMessage(), + 'code' => $e->getCode() + ]; } - - if ($sms_url) { - $data['SmsUrl'] = $sms_url; - $data['SmsMethod'] = 'POST'; - } - - return $this->make_request('POST', $url, $data); } /** * Release a phone number */ public function release_phone_number($phone_number_sid) { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers/' . $phone_number_sid . '.json'; - return $this->make_request('DELETE', $url); + 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) { - $url = $this->api_base . '/Accounts/' . $this->account_sid . '/IncomingPhoneNumbers/' . $phone_sid . '.json'; - - $data = array( - 'VoiceUrl' => $voice_url, - 'VoiceMethod' => 'POST' - ); - - if ($sms_url) { - $data['SmsUrl'] = $sms_url; - $data['SmsMethod'] = 'POST'; + try { + $params = [ + 'voiceUrl' => $voice_url, + 'voiceMethod' => 'POST' + ]; + + if ($sms_url) { + $params['smsUrl'] = $sms_url; + $params['smsMethod'] = 'POST'; + } + + $number = $this->client->incomingPhoneNumbers($phone_sid)->update($params); + + return [ + 'success' => true, + 'data' => [ + 'sid' => $number->sid, + 'phoneNumber' => $number->phoneNumber, + 'voiceUrl' => $number->voiceUrl, + 'smsUrl' => $number->smsUrl + ] + ]; + } catch (\Twilio\Exceptions\TwilioException $e) { + return [ + 'success' => false, + 'error' => $e->getMessage(), + 'code' => $e->getCode() + ]; } - - return $this->make_request('POST', $url, $data); } /** - * Make API request + * Create TwiML helper - returns SDK VoiceResponse */ - private function make_request($method, $url, $data = array()) { - $args = array( - 'method' => $method, - 'headers' => array( - 'Authorization' => 'Basic ' . base64_encode($this->account_sid . ':' . $this->auth_token), - 'Content-Type' => 'application/x-www-form-urlencoded' - ), - 'timeout' => 30 - ); - - if ($method === 'POST' && !empty($data)) { - $args['body'] = $data; - } - - if ($method === 'GET') { - $response = wp_remote_get($url, $args); - } else { - $response = wp_remote_post($url, $args); - } - - if (is_wp_error($response)) { - return array( - 'success' => false, - 'error' => $response->get_error_message() - ); - } - - $body = wp_remote_retrieve_body($response); - $decoded = json_decode($body, true); - - $status_code = wp_remote_retrieve_response_code($response); - - if ($status_code >= 200 && $status_code < 300) { - return array( - 'success' => true, - 'data' => $decoded - ); - } else { - return array( - 'success' => false, - 'error' => isset($decoded['message']) ? $decoded['message'] : 'API request failed', - 'code' => $status_code - ); - } + public function create_twiml() { + return new \Twilio\TwiML\VoiceResponse(); + } + + /** + * Get the Twilio client instance + */ + public function get_client() { + return $this->client; } /** * Validate webhook signature */ public function validate_webhook_signature($url, $params, $signature) { - $data = $url; - - if (is_array($params) && !empty($params)) { - ksort($params); - foreach ($params as $key => $value) { - $data .= $key . $value; - } - } - - $computed_signature = base64_encode(hash_hmac('sha1', $data, $this->auth_token, true)); - - return hash_equals($signature, $computed_signature); + $validator = new \Twilio\Security\RequestValidator(get_option('twp_twilio_auth_token')); + return $validator->validate($signature, $url, $params); } } \ No newline at end of file diff --git a/includes/class-twp-webhooks.php b/includes/class-twp-webhooks.php index 515ae6e..d2956b8 100644 --- a/includes/class-twp-webhooks.php +++ b/includes/class-twp-webhooks.php @@ -658,11 +658,10 @@ class TWP_Webhooks { * Send default response */ private function send_default_response() { - $twiml = new SimpleXMLElement(''); - $say = $twiml->addChild('Say', 'Thank you for calling. Goodbye.'); - $say->addAttribute('voice', 'alice'); - $twiml->addChild('Hangup'); - echo $twiml->asXML(); + $response = new \Twilio\TwiML\VoiceResponse(); + $response->say('Thank you for calling. Goodbye.', ['voice' => 'alice']); + $response->hangup(); + echo $response->asXML(); } /** @@ -1074,17 +1073,13 @@ class TWP_Webhooks { error_log('TWP Outbound Webhook - All params: ' . print_r($params, true)); if ($target_number && $from_number) { - // Create TwiML to call the target number with the specified from number - $twiml = ''; - $twiml .= ''; - $twiml .= 'Connecting your outbound call...'; - $twiml .= ''; - $twiml .= '' . esc_html($target_number) . ''; - $twiml .= ''; - $twiml .= 'The number you called is not available. Please try again later.'; - $twiml .= ''; + // 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]); - return $this->send_twiml_response($twiml); + // 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.'; @@ -1097,16 +1092,18 @@ class TWP_Webhooks { error_log('TWP Outbound Error: ' . $error_msg . ' Params: ' . json_encode($params)); - $twiml = ''; - $twiml .= '' . esc_html($error_msg) . ''; - return $this->send_twiml_response($twiml); + $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()); - $twiml = ''; - $twiml .= 'Technical error occurred. Please try again.'; - return $this->send_twiml_response($twiml); + $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()); } } } \ No newline at end of file diff --git a/includes/class-twp-workflow.php b/includes/class-twp-workflow.php index 9e5b91c..240fc67 100644 --- a/includes/class-twp-workflow.php +++ b/includes/class-twp-workflow.php @@ -341,9 +341,8 @@ class TWP_Workflow { } else if ($routing['action'] === 'forward' && $routing['data']['forward_number']) { // Forward call $twiml = new \Twilio\TwiML\VoiceResponse(); - $dial = $twiml->dial(); - $dial->number($routing['data']['forward_number']); - return $twiml; + $twiml->dial($routing['data']['forward_number']); + return $twiml->asXML(); } // Fallback to legacy behavior if new routing doesn't work diff --git a/install-twilio-sdk.sh b/install-twilio-sdk.sh new file mode 100644 index 0000000..fe293e5 --- /dev/null +++ b/install-twilio-sdk.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +# Script to install Twilio PHP SDK without Composer +# This is REQUIRED for the plugin to work properly + +echo "Installing Twilio PHP SDK v8.7.0..." +echo "IMPORTANT: This SDK is required for the plugin to function." + +# Check if we can download files +if ! command -v curl &> /dev/null; then + echo "ERROR: curl is required to download the SDK" + echo "Please install curl and try again" + exit 1 +fi + +if ! command -v tar &> /dev/null; then + echo "ERROR: tar is required to extract the SDK" + echo "Please install tar and try again" + exit 1 +fi + +# Create vendor directory if it doesn't exist +mkdir -p vendor/twilio/sdk + +# Download the latest release (8.7.0) +echo "Downloading Twilio SDK from GitHub..." +if ! curl -L https://github.com/twilio/twilio-php/archive/refs/tags/8.7.0.tar.gz -o twilio-sdk.tar.gz; then + echo "ERROR: Failed to download Twilio SDK" + echo "Please check your internet connection and try again" + exit 1 +fi + +# Extract the archive +echo "Extracting SDK files..." +if ! tar -xzf twilio-sdk.tar.gz; then + echo "ERROR: Failed to extract SDK files" + exit 1 +fi + +# Check if the extracted directory exists +if [ ! -d "twilio-php-8.7.0/src" ]; then + echo "ERROR: Extracted SDK directory structure is unexpected" + exit 1 +fi + +# Create the Twilio directory structure +echo "Setting up SDK directory structure..." +mkdir -p vendor/twilio + +# Remove existing SDK if it exists +if [ -d "vendor/twilio/sdk" ]; then + rm -rf vendor/twilio/sdk +fi + +# Move the entire src directory to be the sdk +echo "Installing SDK files..." +if ! mv twilio-php-8.7.0/src vendor/twilio/sdk; then + echo "ERROR: Failed to move SDK files" + exit 1 +fi + +# Create a comprehensive autoloader +cat > vendor/autoload.php << 'EOF' +get_phone_numbers(); + +echo "API Response:\n"; +echo "=============\n"; +print_r($response); + +if ($response['success'] && !empty($response['data']['incoming_phone_numbers'])) { + echo "\nFirst phone number data:\n"; + echo "========================\n"; + $first = $response['data']['incoming_phone_numbers'][0]; + foreach ($first as $key => $value) { + if (is_array($value)) { + echo "$key: " . json_encode($value) . "\n"; + } else { + echo "$key: $value\n"; + } + } +} \ No newline at end of file diff --git a/test-capabilities.php b/test-capabilities.php new file mode 100644 index 0000000..11223ea --- /dev/null +++ b/test-capabilities.php @@ -0,0 +1,37 @@ +getMethods(ReflectionMethod::IS_PUBLIC); + +echo "Public methods available:\n"; +foreach ($methods as $method) { + if (!$method->isConstructor() && !$method->isDestructor()) { + echo "- " . $method->getName() . "()\n"; + } +} + +echo "\nProperties:\n"; +$properties = $reflection->getProperties(); +foreach ($properties as $property) { + echo "- " . $property->getName() . " (" . ($property->isPublic() ? 'public' : ($property->isProtected() ? 'protected' : 'private')) . ")\n"; +} + +// Check if we can access via array notation +echo "\nTesting array access:\n"; +try { + // This won't work, but let's see what happens + echo "ArrayAccess interface: " . (in_array('ArrayAccess', class_implements('Twilio\Base\PhoneNumberCapabilities')) ? 'YES' : 'NO') . "\n"; +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; +} \ No newline at end of file diff --git a/test-sdk.php b/test-sdk.php new file mode 100644 index 0000000..f94e3fe --- /dev/null +++ b/test-sdk.php @@ -0,0 +1,71 @@ +say('Hello from Twilio SDK test!'); + echo " ✅ OK: TwiML generation works\n"; + echo " Generated: " . substr(str_replace(["\n", "\r"], '', $response->asXML()), 0, 100) . "...\n"; +} catch (Exception $e) { + echo " ❌ FAIL: TwiML generation failed: " . $e->getMessage() . "\n"; +} + +echo "\nInstallation test complete!\n"; \ No newline at end of file diff --git a/twilio-wp-plugin.php b/twilio-wp-plugin.php index f724b7d..b0a799e 100644 --- a/twilio-wp-plugin.php +++ b/twilio-wp-plugin.php @@ -28,6 +28,45 @@ function twp_activate() { TWP_Activator::activate(); } +/** + * Check if Twilio SDK is installed and show admin notice if not + */ +function twp_check_sdk_installation() { + $autoloader_path = TWP_PLUGIN_DIR . 'vendor/autoload.php'; + $sdk_installed = false; + + if (file_exists($autoloader_path)) { + // Try to load autoloader and check for classes + require_once $autoloader_path; + $sdk_installed = class_exists('Twilio\Rest\Client'); + } + + if (!$sdk_installed) { + add_action('admin_notices', 'twp_sdk_missing_notice'); + } +} + +/** + * Display admin notice for missing SDK + */ +function twp_sdk_missing_notice() { + ?> +
+

Twilio WordPress Plugin - SDK Required

+

The Twilio PHP SDK is required for this plugin to work.

+

To install the SDK, run this command in your plugin directory:

+ chmod +x install-twilio-sdk.sh && ./install-twilio-sdk.sh +

Or install via Composer: composer install

+

Plugin path:

+
+