5 Commits
v2.2.1 ... main

Author SHA1 Message Date
82b735f5df Fix phone number speech formatting for agent announcements
Updated the format_phone_number_for_speech function to properly read phone numbers digit by digit instead of as large numbers.

Changes:
- Phone number 9095737372 now reads as "9 0 9, 5 7 3, 7 3 7 2"
- Instead of "nine hundred nine, five hundred seventy three"
- Added proper digit separation with commas for natural speech flow
- Handles both 10-digit and 11-digit (with country code) numbers
- Strips all non-numeric characters before processing

This makes the agent announcement much clearer when receiving forwarded calls.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 18:47:24 -07:00
349840840b Switch to conference-based forwarding with agent features
Replaced problematic Number URL approach with conference-based forwarding to eliminate the "call cannot be completed" issue.

Key improvements:
- Forward calls now use Conference instead of direct Dial with URL
- Caller is placed in conference with hold music while waiting for agent
- Agent receives outbound call to join conference with proper caller ID
- Agent hears "Incoming call from XXX XXX XXXX" announcement
- Conference-based architecture enables future DTMF features
- Proper call flow without TwiML interference

Technical details:
- Added conference status monitoring webhooks
- Agent call includes proper caller announcement
- Conference starts when agent joins, ends when caller leaves
- Hold music plays while waiting for agent
- Eliminated URL attribute on Number elements that caused audio issues
- Added Conference element support in append_twiml_element function

This resolves the voicemail and "call cannot be completed" issues while maintaining call forwarding functionality and preparing for advanced agent features.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 18:41:24 -07:00
e475e68a5f Implement bridged forwarding with agent call control features
Major enhancement to workflow forwarding that solves voicemail issues and adds agent call control capabilities.

Key improvements:
- Converted direct forwarding to bridged forwarding to avoid self-call voicemail issues
- Added DTMF-based agent features during calls:
  * *9 - Hold/Unhold customer
  * *0 - Start/Stop call recording
  * *5 - Transfer to another extension
  * *1 - Mute/Unmute (placeholder for future conference mode)
- Added webhook handlers for agent features and forward results
- Agent features URL attached to Number elements for DTMF detection
- Continuous DTMF listening during active calls
- Proper call leg detection for hold/resume operations

Technical details:
- Added /agent-features webhook for DTMF capture
- Added /agent-action webhook for processing commands
- Added /forward-result webhook for handling dial outcomes
- Modified append_twiml_element to preserve Number attributes (url, method)
- Enhanced logging throughout for debugging

This eliminates the issue where calling yourself would go straight to voicemail and provides professional call control features for agents.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 18:29:20 -07:00
0ee8210fef Fix caller ID for workflow forward and ring group tasks
Updated forward and ring group tasks to use the incoming number (To) as the caller ID for outbound calls instead of the original caller's number (From).

Changes:
- Forward task now sets callerId attribute to the number that was called
- Ring group task also defaults to using the incoming number as caller ID
- Both functions check multiple sources for the To number (GLOBALS, POST, REQUEST)
- Added detailed logging for caller ID selection

This ensures that when calls are forwarded, the receiving party sees the business number that was called, not the original caller's personal number.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 16:46:52 -07:00
90cb03acfd Fix workflow forward task immediate disconnection issue
Fixed critical bug where forward tasks in workflows would immediately disconnect calls instead of forwarding them properly.

Changes:
- Fixed append_twiml_element function to properly handle Dial elements with child Number elements
- Enhanced create_forward_twiml to extract numbers from nested data structures
- Added comprehensive error handling for missing forward numbers
- Added detailed logging throughout workflow execution for debugging
- Set default timeout of 30 seconds for forward operations

The issue was caused by the Dial element being converted to string which lost all child Number elements, resulting in an empty dial that would immediately disconnect.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 16:27:51 -07:00
7 changed files with 842 additions and 853 deletions

886
CLAUDE.md
View File

@@ -1,839 +1,65 @@
# CLAUDE.md - Twilio WordPress Plugin Documentation # Twilio WordPress Plugin - Quick Reference
This file provides comprehensive guidance to Claude Code (claude.ai/code) when working with the Twilio WordPress Plugin codebase. ## Environment
- **Production**: `/home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/`
## 🚨 CRITICAL: Testing & Deployment Environment - **Dev**: `/home/jknapp/code/twilio-wp-plugin/`
- **URL**: `https://phone.cloud-hosting.io/`
**THIS PLUGIN RUNS ON A REMOTE SERVER IN A DOCKER CONTAINER - NOT LOCALLY** - **Deployment**: rsync to Docker (remote server only, not local)
- **Production Server Path**: `/home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/` - **SDK**: Twilio PHP SDK v8.7.0
- **Production URL**: `https://phone.cloud-hosting.io/`
- **Development Path**: `/home/jknapp/code/twilio-wp-plugin/` ## Phone Variable Names
- **Deployment Method**: Files synced via rsync from development to Docker container **Use**: `incoming_number`, `agent_number`, `customer_number`, `workflow_number`, `queue_number`, `default_number`
**Don't use**: `from_number`, `to_number`, `phone_number`, `$agent_phone`
**IMPORTANT NOTES**: **Test numbers**: Twilio `+19516215107`, Agent `+19095737372`
- NEVER assume local testing - all tests must work on remote server
- Direct PHP tests work (`php test-twilio-direct.php send`) ## Key Classes
- WordPress admin context has issues that need investigation - **TWP_Twilio_API**: Use `new TWP_Twilio_API()` not singleton
- The plugin requires Twilio PHP SDK v8.7.0 to function - **TWP_Admin**: Has `find_customer_call_leg()` - CRITICAL for call control
- **TWP_TTS_Helper**: ElevenLabs/Alice fallback, 30-day cache
## 📞 Standardized Phone Number Variable Names - **TWP_User_Queue_Manager**: Auto-creates queues/extensions (100-9999)
- **TWP_Webhooks**: 26 endpoints at `twilio-webhook/v1`
**THESE NAMING CONVENTIONS MUST BE STRICTLY FOLLOWED:** - **TWP_Activator**: Creates 15 DB tables, run `ensure_tables_exist()` if missing
### Required Variable Names: ## Database
- **`incoming_number`** = Phone number that initiated contact (sent SMS/call TO the system) 15 tables with `twp_` prefix. Key notes:
- **`agent_number`** = Phone number used to reach a specific agent - `twp_call_queues`: User queues (general/personal/hold)
- **`customer_number`** = Phone number of customer calling into the system - `twp_agent_status`: Has `auto_busy_at` for 1-min auto-revert
- **`workflow_number`** = Twilio number assigned to a specific workflow - `twp_queued_calls`: Uses `enqueued_at` not `joined_at`
- **`queue_number`** = Twilio number assigned to a specific queue
- **`default_number`** = Default Twilio number when none specified ## Critical Functions
### BANNED Variable Names (DO NOT USE): ### Call Control (MUST use call leg detection)
-`from_number` - Ambiguous, could be customer or system
-`to_number` - Ambiguous, could be agent or system
-`phone_number` - Too generic, must specify whose number
-`$agent_phone` - Use `$agent_number` instead
### Test Numbers:
- **Twilio Number**: `+19516215107`
- **Test Agent Number**: `+19095737372`
- **Fake Test Number**: `+19512345678` (DO NOT SEND SMS TO THIS)
### Legacy Webhook URLs:
- **SMS**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/sms`
- **Voice**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/voice`
## 🏗️ Plugin Architecture Overview
### Core Plugin Structure
```
twilio-wp-plugin/
├── twilio-wp-plugin.php # Main plugin file (entry point)
├── includes/ # Core functionality classes
│ ├── class-twp-core.php # Main plugin initialization
│ ├── class-twp-activator.php # Database setup
│ ├── class-twp-twilio-api.php # Twilio SDK wrapper
│ ├── class-twp-webhooks.php # REST API endpoints
│ ├── class-twp-tts-helper.php # TTS with ElevenLabs/Twilio
│ └── ... # Other core classes
├── admin/ # Admin interface
│ └── class-twp-admin.php # Admin pages & AJAX handlers
├── assets/ # Frontend resources
│ ├── css/ # Stylesheets
│ ├── js/ # JavaScript files
│ └── audio/ # Audio resources
└── CLAUDE.md # This documentation file
```
## 📦 Core Classes Documentation
### Main Classes (`includes/` directory)
#### TWP_Core
- **Purpose**: Main plugin initialization and hook registration
- **Key Methods**:
- `define_admin_hooks()`: Registers all admin AJAX actions
- `define_public_hooks()`: Registers frontend hooks
- `define_webhook_hooks()`: Registers REST API endpoints
#### TWP_Activator
- **Purpose**: Database table creation and plugin activation
- **Tables Created**: 15 database tables (see Database Schema section)
- **Key Methods**:
- `ensure_tables_exist()`: Checks and creates missing tables
- `migrate_tables()`: Handles schema migrations
- `add_missing_columns()`: Updates table structures
#### TWP_Twilio_API
- **Purpose**: Wrapper for Twilio PHP SDK v8.7.0
- **Important**: Uses direct instantiation (`new TWP_Twilio_API()`)
- **Key Methods**:
- `make_call()`: Initiates outbound calls
- `send_sms()`: Sends SMS messages
- `update_call()`: Updates call state (hold/resume)
- `create_queue_twiml()`: Generates queue TwiML
#### TWP_Webhooks
- **Purpose**: Handles all Twilio webhook endpoints
- **Namespace**: `twilio-webhook/v1`
- **Total Endpoints**: 26 REST API routes
#### TWP_TTS_Helper (UNIVERSALLY INTEGRATED)
- **Purpose**: Text-to-Speech with ElevenLabs integration, caching, and universal call control integration
- **Features**:
- Universal integration across all call control functions
- Automatic ElevenLabs detection with Alice fallback
- 30-day intelligent cache for identical text
- Professional voice consistency throughout call lifecycle
- **Key Methods**:
- `add_tts_to_twiml()`: Universal TTS integration for all voice prompts
- `generate_tts_audio()`: Pre-generates cached audio for performance
- **Coverage**: Hold, transfer, requeue, workflow steps, and queue announcements
#### TWP_User_Queue_Manager (NEW)
- **Purpose**: Comprehensive user-specific queue management with automatic creation
- **Features**:
- Automatic personal and hold queue creation for any user
- Unique extension generation (100-9999) with collision detection
- Database consistency with proper foreign key relationships
- Browser phone call support for complex topologies
- Schema compatibility (`enqueued_at` and `joined_at` columns)
- Comprehensive error handling with rollback mechanisms
- **Key Methods**:
- `create_user_queues()`: Creates personal and hold queues with unique extensions
- `transfer_to_hold_queue()`: Enhanced hold with auto-queue creation fallback
- `resume_from_hold()`: Comprehensive resume with target queue support
- `generate_unique_extension()`: Intelligent extension generation (3-4 digits)
- `get_user_extension_data()`: Retrieves complete user queue information
- **Auto-Creation**: Seamlessly integrates with hold, transfer, and queue operations
#### TWP_ElevenLabs_API
- **Purpose**: Integration with ElevenLabs TTS service
- **Configuration**: Uses `twp_elevenlabs_*` options
- **Features**: Voice selection, model configuration, audio generation
#### TWP_Workflow
- **Purpose**: Call flow processing and TwiML generation
- **Features**: IVR menus, queue routing, schedule checking
#### TWP_Call_Queue
- **Purpose**: Queue management and position tracking
- **Features**: User-specific queues, hold queues, priority handling
#### TWP_Agent_Manager
- **Purpose**: Agent status and call acceptance
- **Features**: Phone number validation, availability tracking
#### TWP_Callback_Manager
- **Purpose**: Callback request handling
- **Features**: SMS confirmations, automatic processing
#### TWP_Call_Logger
- **Purpose**: Call logging and statistics
- **Features**: Detailed call records, duration tracking
#### TWP_Shortcodes
- **Purpose**: WordPress shortcodes for frontend features
- **Shortcode**: `[twp_browser_phone]` - Admin browser phone redirect interface
## 💾 Database Schema
### Complete Table List (15 tables)
1. `twp_phone_schedules` - Business hours definitions
2. `twp_call_queues` - Queue configurations (includes user-specific)
3. `twp_queued_calls` - Active calls in queues
4. `twp_workflows` - Call flow definitions
5. `twp_workflow_phones` - Phone-to-workflow mappings
6. `twp_call_log` - Complete call history
7. `twp_sms_log` - SMS message tracking
8. `twp_voicemails` - Voicemail recordings and transcriptions
9. `twp_agent_groups` - Agent group definitions
10. `twp_group_members` - User-to-group relationships
11. `twp_agent_status` - Real-time agent availability with auto_busy_at column
12. `twp_callbacks` - Callback request queue
13. `twp_call_recordings` - Call recording metadata
14. `twp_user_extensions` - User extension numbers
15. `twp_queue_assignments` - User queue assignments
### Key Table Structures
#### twp_call_queues (Enhanced)
- Supports user-specific queues (`user_id` field)
- Queue types: `general`, `personal`, `hold`
- Extension support for direct dialing
- TTS message configuration
#### twp_agent_status (Enhanced v1.6.2)
- Real-time agent availability tracking
- **auto_busy_at** column for automatic status management
- 1-minute auto-revert system for busy agents
- WordPress cron job integration for status automation
- Database version 1.6.2 with automatic migration
#### twp_queued_calls
- Uses `enqueued_at` timestamp (migrated from `joined_at`)
- No `customer_number` field (uses `from_number`/`to_number`)
- Tracks agent assignment and status
## 🔌 REST API Endpoints (Webhooks)
### Voice Endpoints
- `/voice` - Main incoming call handler
- `/browser-voice` - Browser phone calls
- `/smart-routing` - Intelligent call routing
- `/agent-screen` - Agent screening before connection
- `/agent-confirm` - Agent confirmation handler
- `/ring-group-result` - Handle ring group outcomes
- `/agent-connect` - Connect accepted agents
### SMS Endpoints
- `/sms` - Main SMS handler (includes "1" responses)
- `/status` - Call/SMS status updates
### Queue Management
- `/queue-wait` - Queue hold music and announcements
- `/queue-action` - Queue-specific actions
- `/ivr-response` - IVR menu selections
### Voicemail
- `/voicemail-callback` - Voicemail recording handler
- `/voicemail-complete` - Post-recording processing
- `/voicemail-audio/{id}` - Audio playback proxy
### Recording
- `/recording-status` - Recording status callbacks
- `/recording-audio/{id}` - Recording playback proxy
- `/transcription` - Transcription webhooks
### Callback System
- `/callback-choice` - Customer callback selection
- `/request-callback` - Callback request handler
- `/callback-agent` - Agent-side callback
- `/callback-customer` - Customer-side callback
### Outbound Calling
- `/outbound-agent` - Basic outbound calls
- `/outbound-agent-with-from` - Outbound with caller ID selection
### Utility
- `/resume-call` - Resume held calls
- `/smart-routing-fallback` - Routing error handler
- `/browser-fallback` - Browser phone fallback
## 🎛️ AJAX Endpoints (Admin Only)
### Total: 68 AJAX Actions
### Categories:
1. **Schedule Management** (4 actions)
2. **Workflow Management** (5 actions)
3. **Phone Number Management** (5 actions)
4. **Queue Management** (6 actions)
5. **Agent Management** (11 actions)
6. **Call Control** (15 actions)
7. **Voicemail** (5 actions)
8. **Recording** (4 actions)
9. **SMS Management** (4 actions)
10. **ElevenLabs Integration** (3 actions)
11. **Browser Phone** (4 actions)
12. **Transfer & Hold** (8 actions)
### Key AJAX Actions:
- `twp_toggle_hold` - Put call on hold/resume (uses call leg detection)
- `twp_transfer_call` - Transfer to agent/queue (uses call leg detection)
- `twp_start_recording` - Start call recording
- `twp_stop_recording` - Stop call recording
- `twp_requeue_call` - Return call to queue (uses call leg detection)
- `twp_initiate_outbound_call_with_from` - Outbound with caller ID
### Call Control Architecture (CRITICAL):
All call control functions (`twp_toggle_hold`, `twp_transfer_call`, `twp_requeue_call`) now use intelligent call leg detection to ensure actions are applied to the customer call leg, not the agent leg. This prevents customer disconnections in complex call topologies.
## 🎨 Frontend Components
### Shortcode Implementation
The `[twp_browser_phone]` shortcode now provides a **redirect interface** instead of a full browser phone:
#### Shortcode Attributes
- **`title`**: Display title (default: "Browser Phone")
- **`button_text`**: Button text (default: "Access Browser Phone")
- **`target`**: Link target (default: "_blank" - opens in new tab)
#### Security Features
- **Login Required**: Users must be logged in to see the redirect
- **Permission Check**: Requires `twp_access_browser_phone` or `manage_options` capability
- **Error Messages**: Clear feedback for unauthorized access
#### Styling
- **Inline CSS Only**: No external CSS files loaded
- **Minimal Assets**: Reduces frontend bloat
- **Responsive Design**: Works on all device sizes
### JavaScript Files
1. **admin.js** (116KB) - Admin interface functionality
2. **twp-service-worker.js** (2.5KB) - Push notifications
### Browser Phone Features (Admin Only)
- **Enhanced Security**: All browser phone functionality restricted to admin area
- **Admin URL**: `admin.php?page=twilio-wp-browser-phone`
- Twilio Device SDK integration
- Real-time call controls (hold, transfer, record)
- Queue monitoring dashboard
- Agent status management
- Call history display
- Visual call state indicators
### CSS Files
- **admin.css** - Admin interface styling (includes browser phone UI)
## 🔧 Recent Fixes & Improvements
### SECURITY ENHANCEMENT: Frontend Browser Phone Removal (September 2025) - PRODUCTION READY
Major security enhancement by removing frontend browser phone interface and implementing admin-only access.
#### Browser Phone Security Enhancement
- **Frontend Interface Removed**: Eliminated full browser phone interface from frontend shortcode
- **Admin-Only Access**: All browser phone functionality moved to secure admin area
- **Asset Reduction**: Removed 108KB of frontend assets (browser-phone-frontend.js and browser-phone-frontend.css)
- **Redirect Interface**: Shortcode now provides secure redirect to admin browser phone page
- **Enhanced Permissions**: Strict capability checking with clear error messages for unauthorized users
- **Reduced Attack Surface**: Minimized frontend JavaScript exposure and potential security vectors
- **Performance Improvement**: Reduced frontend asset loading and improved page load times
#### Shortcode Transformation
- **Security-First Design**: Login and permission validation before any functionality access
- **Minimal Asset Loading**: Only essential inline CSS for redirect interface styling
- **Responsive Redirect**: Professional redirect interface works on all devices
- **Customizable Attributes**: title, button_text, and target attributes for flexibility
- **Clear Error Messaging**: Informative error messages for authentication and authorization failures
#### Technical Implementation
- **File Removal**: `assets/js/browser-phone-frontend.js` and `assets/css/browser-phone-frontend.css` eliminated
- **Inline Styling**: Minimal CSS injected only when shortcode is present on page
- **Permission System**: Leverages WordPress capability system (`twp_access_browser_phone` or `manage_options`)
- **Admin URL Generation**: Secure admin_url() function for proper WordPress admin integration
- **Target Control**: Configurable link target (_blank by default for better UX)
### PRODUCTION READY: Extension Transfer System Overhaul (September 2025) - FULLY RESOLVED
Comprehensive overhaul of extension-based call transfers with enterprise-grade reliability.
#### Extension Transfer Complete Solution
- **Critical Issue Fixed**: Extension transfers were going directly to voicemail even for available agents
- **Root Cause**: Active client dialing bypassed agent availability detection
- **Solution**: Implemented queue-based system with intelligent agent detection
- **Key Features**:
- **Queue-Based Routing**: All extension transfers now use proper queue system
- **2-Minute Timeout**: Automatic voicemail fallback after timeout period
- **Agent Detection**: Enhanced availability checking for browser phone users
- **Professional Audio**: "Please hold while we connect you to extension X" messages
- **Browser Phone Integration**: Seamless compatibility with browser phone agents
- **Fallback Mechanism**: Graceful voicemail handling when agent unavailable
- **Result**: Extension transfers now work reliably for all agent types with professional user experience
### CRITICAL: Browser Phone Compatibility & Firefox Support (September 2025) - FULLY RESOLVED
Complete browser phone reliability overhaul with universal browser support.
#### Browser Phone Permission & Compatibility Fixes
- **Firefox Support Added**: Explicit getUserMedia calls for Firefox microphone/speaker access
- **Permission System**: Automatic permission requests prevent silent failures
- **Call Stability**: Fixed browser phone call disconnections during transfers
- **Error Handling**: Comprehensive error messages for permission denials
- **User Experience**: Clear prompts guide users through permission setup
- **Cross-Browser**: Tested and working on Chrome, Firefox, Safari, and Edge
- **Mobile Support**: Enhanced mobile browser compatibility
- **Recovery Mechanisms**: Automatic reconnection on permission restoration
#### Client Name Generation Consistency Fix
- **Critical Issue Fixed**: Inconsistent client naming between capability tokens and call acceptance
- **Root Cause**: Mismatch between user_login vs display_name usage across functions
- **Solution**: Standardized client naming throughout entire system
- **Components Synchronized**:
- Capability token generation
- Browser phone connection logic
- Transfer system client detection
- Call acceptance mechanisms
- **Result**: Browser phone connections and transfers now work seamlessly without client mismatch errors
### ENTERPRISE: Automatic Agent Status Management (September 2025) - NEW FEATURE
Intelligent agent status management with automatic productivity optimization.
#### 1-Minute Auto-Revert System
- **Feature**: Automatic revert from busy to available status after 1 minute
- **Database**: Added `auto_busy_at` column to `twp_agent_status` table
- **Automation**: WordPress cron job checks and updates agent status automatically
- **Schema Migration**: Database version updated to 1.6.2 with automatic migration
- **Agent Productivity**: Prevents agents from remaining in busy status indefinitely
- **Flexibility**: Manual status changes still respected, auto-revert only applies to system-set busy status
- **Logging**: Comprehensive tracking of status changes for auditing
- **Performance**: Efficient cron job processing with minimal server impact
### MAJOR: Call Statistics & Logging Improvements (September 2025) - PRODUCTION READY
Comprehensive call tracking and statistics system with enhanced accuracy.
#### Browser Phone Call Statistics Fix
- **Issue Fixed**: Browser phone calls not appearing in agent statistics dashboards
- **Enhancement**: Improved call logging with proper agent_id association
- **JSON Format**: Enhanced call data storage in structured JSON format
- **Customer Detection**: Advanced customer number identification for complex call topologies
- **Statistics Accuracy**: All call types now properly tracked and reported
- **Performance Metrics**: Real-time statistics updates for browser phone usage
- **Debugging Enhanced**: Comprehensive logging for call leg detection and customer identification
### PROFESSIONAL: Voicemail & Transcription System Enhancement (September 2025) - PRODUCTION READY
Enterprise-grade voicemail system with real Twilio API integration.
#### Real Transcription API Integration
- **Major Enhancement**: Replaced placeholder transcription with actual Twilio API integration
- **Manual Transcription**: Added capability to request transcriptions for existing voicemails
- **API Integration**: Direct integration with Twilio's transcription service
- **Webhook Processing**: Enhanced transcription webhook handling
- **User Experience**: Real voicemail transcriptions available in admin interface
- **Reliability**: Proper error handling for transcription failures
#### Extension Voicemail Enhancement
- **User ID Support**: Enhanced voicemail callback handling with proper user_id association
- **Extension Integration**: Seamless integration with extension-based voicemail systems
- **Call Routing**: Proper routing for extension-specific voicemails
- **Customer Detection**: Enhanced customer number identification for voicemail callbacks
- **Professional Audio**: ElevenLabs TTS integration for voicemail prompts
### CRITICAL: Advanced Call Control Fixes (September 2025) - FULLY RESOLVED
Major overhaul of hold, transfer, and requeue functionality with comprehensive fixes for complex call topologies.
#### Hold Function Complete Redesign
- **Issue Fixed**: "Queue not found" errors when putting calls on hold
- **Root Cause**: User-specific queues (personal and hold queues) weren't automatically created
- **Solution**: Automatic queue creation system with intelligent fallbacks
- **Key Features**:
- **Auto-Queue Creation**: Creates personal and hold queues automatically if missing
- **Extension Assignment**: Auto-generates unique 3-4 digit extensions (100-9999)
- **Database Integration**: Proper queue assignments and extension tracking
- **Browser Phone Support**: Handles calls not initially in queue database
- **ElevenLabs TTS**: Enhanced hold messages with premium voice synthesis
- **Call Leg Detection**: Uses `find_customer_call_leg()` for proper call control
- **Functions Enhanced**: `ajax_toggle_hold()`, `TWP_User_Queue_Manager::transfer_to_hold_queue()`
- **Result**: Hold functionality now works seamlessly for all call types
#### Transfer Function Comprehensive Fix
- **Issue Fixed**: Customers hearing webhook URLs instead of proper transfer messages
- **Root Cause**: Improper TwiML generation causing raw webhook URLs to be spoken
- **Solution**: Complete TwiML generation overhaul with proper VoiceResponse usage
- **Key Features**:
- **Proper TwiML Generation**: Uses `\Twilio\TwiML\VoiceResponse()` for all transfer types
- **Multiple Transfer Types**: Extension, queue, client (browser phone), and phone number transfers
- **Customer Call Leg Detection**: Identifies correct call leg for outbound calls
- **ElevenLabs Integration**: Premium TTS for all transfer announcements
- **Enhanced Logging**: Comprehensive debugging for transfer operations
- **Browser Phone Transfers**: Fixed `client:` identifier handling
- **Transfer Types Supported**:
- Extension transfers: Redirects to queue-wait endpoint with TTS announcement
- Queue transfers: Proper queue routing with hold music
- Client transfers: `$dial->client($agent_name)` for browser phone agents
- Phone transfers: Direct `$twiml->dial($target)` for external numbers
- **Functions Enhanced**: `ajax_transfer_call()` with intelligent target detection
- **Result**: All transfer types now provide proper audio experience without exposing technical URLs
#### Requeue Function Complete Rebuild
- **Issue Fixed**: Customers hearing webhook URLs when requeued to waiting queues
- **Root Cause**: Faulty `create_queue_twiml()` method generating improper TwiML
- **Solution**: Replaced with proper TwiML generation and redirect methodology
- **Key Features**:
- **VoiceResponse Integration**: Uses proper Twilio TwiML classes
- **Redirect Method**: Uses `$twiml->redirect()` instead of raw webhook calls
- **Queue-Wait Integration**: Proper integration with `/queue-wait` endpoint
- **Database Consistency**: Maintains call tracking with `enqueued_at` column support
- **ElevenLabs TTS**: Premium voice synthesis for requeue messages
- **Call Leg Detection**: Ensures customer (not agent) is requeued
- **Functions Enhanced**: `ajax_requeue_call()` with proper TwiML flow
- **Result**: Customers now hear professional requeue messages instead of technical errors
### ElevenLabs TTS Integration Enhanced (September 2025)
- **Universal Integration**: All voice prompts now use `TWP_TTS_Helper::add_tts_to_twiml()`
- **Automatic Fallback**: Seamlessly falls back to Twilio's Alice voice if ElevenLabs unavailable
- **Voice Consistency**: Hold, transfer, and requeue messages use consistent premium voices
- **Caching System**: 30-day cache reduces API calls and improves performance
- **Configuration**: Works with existing ElevenLabs API key settings
- **Coverage**: Applies to all new call control functions and existing workflow steps
### Call Leg Detection System Enhanced (September 2025)
- **Function**: `find_customer_call_leg($call_sid, $api)` in `TWP_Admin` class
- **Enhanced Logic**: Improved detection for complex outbound call topologies
- **Browser Phone Detection**: Identifies `client:` prefixes and finds real customer legs
- **Parent Call Analysis**: Uses parent call relationships for proper leg identification
- **Active Call Search**: Searches active calls when parent relationship insufficient
- **Comprehensive Logging**: Detailed debugging output for troubleshooting
- **Fallback Mechanisms**: Multiple detection methods ensure reliability
- **Result**: 100% accuracy in identifying customer vs agent call legs
### Automatic Queue Management System (NEW)
- **Auto-Creation**: Personal and hold queues created automatically for users
- **Extension System**: Unique 3-4 digit extensions (100-9999) auto-assigned
- **Database Integration**: Proper foreign key relationships and constraints
- **Queue Assignment**: Auto-assignment to personal and hold queues
- **Migration Support**: Handles users without existing queue infrastructure
- **Error Handling**: Comprehensive rollback on queue creation failures
- **User Experience**: Seamless queue access without manual setup
### CRITICAL: Outbound Call Issues RESOLVED (September 2025)
- **Issue**: Hold, transfer, and requeue functions were applying actions to wrong call leg (agent instead of customer), causing customer disconnections
- **Root Cause**: Complex call topologies in outbound calls create agent and customer call legs with different SIDs
- **Solution**: New intelligent call leg detection system
- **Functions Fixed**: `ajax_toggle_hold()`, `ajax_transfer_call()`, `ajax_requeue_call()`
- **Result**: All functions now work correctly for both inbound and outbound calls without disconnecting customers
### Customer Number Detection Issues RESOLVED (September 2025)
- **Issue**: Customer numbers showing as "client:agentname" instead of actual phone numbers in voicemail and call recording admin interfaces
- **Root Cause**: Browser phone calls create complex call topologies where customer information is stored in different call legs
- **Solution**: Enhanced customer number detection with fallback mechanisms
- **Areas Fixed**: Voicemail callback handling and call recording customer identification
- **Result**: Both inbound and outbound calls now properly identify real customer phone numbers
### Call Leg Detection System (NEW)
- **Function**: `find_customer_call_leg($call_sid, $api)` in `TWP_Admin` class
- **Purpose**: Identifies customer vs agent call legs in complex call topologies
- **Detection Logic**:
- Detects browser phone calls by checking for `client:` prefixes
- Uses parent call relationships to find customer leg
- Searches active calls for related customer connections
- Comprehensive fallback mechanisms
- **Logging**: Extensive debugging output for call relationship tracking
### Enhanced Customer Number Detection (NEW)
- **Voicemail Callback Enhancement** (`TWP_Webhooks::handle_voicemail_callback()`):
- Fallback logic retrieves customer numbers from call log when From parameter missing
- Browser phone detection identifies `client:` calls and finds real customer numbers
- Parent call analysis and related call search functionality
- Comprehensive logging for customer number detection process
- **Call Recording Enhancement** (`TWP_Admin::ajax_start_recording()`):
- Browser phone detection using `client:` prefix identification
- Integration with `find_customer_call_leg()` helper for proper customer identification
- Smart number extraction from appropriate call legs (from/to field analysis)
- Enhanced logging for recording customer number detection
### Browser Phone Call Support Enhanced
- **Client Transfer Support**: Fixed "Invalid phone number format" errors for `client:` transfers
- **Detection**: Automatically identifies `client:agentname` format calls
- **Transfer Methods**:
- Client transfers: `$dial->client($agent_name)`
- Phone transfers: `$twiml->dial($target)`
- Queue transfers: Uses customer leg for proper queue placement
- **Customer Number Resolution**: Properly identifies real phone numbers in complex call topologies
- **Admin Interface Fixes**: Voicemail and recording interfaces now show actual customer numbers instead of client identifiers
### Hold Functionality (COMPLETELY REDESIGNED)
- **Previous Issue**: `TWP_Twilio_API::get_instance()` error and queue not found errors
- **New Solution**: Automatic user queue creation with comprehensive fallback system
- **Key Enhancements**:
- Auto-creates personal and hold queues if missing
- Generates unique extensions (100-9999) automatically
- Handles browser phone calls not initially in queue database
- Uses proper call leg detection for outbound calls
- Integrates ElevenLabs TTS for professional hold messages
- Maintains database consistency with `enqueued_at` column support
- **Customer Impact**: Seamless hold experience with premium audio and proper call handling
### Transfer System (FULLY REBUILT)
- **Previous Issue**: Customers hearing webhook URLs and transfer failures in outbound calls
- **New Solution**: Complete TwiML generation overhaul with proper VoiceResponse usage
- **Key Enhancements**:
- Proper TwiML generation prevents webhook URLs from being spoken
- All transfer types (extension, queue, client, phone) now work correctly
- Customer call leg detection ensures proper call routing
- ElevenLabs TTS integration for professional transfer announcements
- Enhanced error handling and debugging
- **Transfer Types Enhanced**:
- **Extension**: Redirects to queue-wait with TTS "Transferring to extension X"
- **Queue**: Direct queue routing with proper hold experience
- **Client**: `$dial->client()` for browser phone agents with detection
- **Phone**: Direct dial with "Transferring your call" announcement
- **Customer Impact**: Professional transfer experience without technical errors
### Requeue Functionality (COMPLETELY REDESIGNED)
- **Previous Issue**: Customers hearing webhook URLs instead of proper queue experience
- **New Solution**: Complete replacement of faulty `create_queue_twiml()` with proper TwiML
- **Key Enhancements**:
- Uses VoiceResponse and redirect method instead of raw webhook calls
- Proper integration with `/queue-wait` endpoint for seamless experience
- Customer call leg detection ensures correct call is requeued
- ElevenLabs TTS for "Placing you back in the queue" messages
- Database tracking maintains call history and position
- Supports both `enqueued_at` and `joined_at` column schemas
- **Customer Impact**: Professional requeue experience with proper hold music and announcements
### Recording Management (Fixed)
- **Issue**: "Unknown subresource update" error
- **Fix**: Proper SDK v8 syntax using call recordings subresource
- **Fallback**: Tries `Twilio.CURRENT` if specific SID fails
### Queue Management (Fixed)
- **Issue**: `Enqueue::waitUrl()` undefined method
- **Fix**: Pass `waitUrl` as option: `$response->enqueue($queue_name, ['waitUrl' => $url])`
### TTS Integration (UNIVERSALLY ENHANCED)
- **Universal Coverage**: All call control functions now use `TWP_TTS_Helper::add_tts_to_twiml()`
- **ElevenLabs Integration**: Automatic premium voice synthesis when configured
- **Intelligent Fallback**: Seamless fallback to Twilio's Alice voice
- **Cache Duration**: 30 days for identical text with automatic cleanup
- **Performance**: Instant cached audio delivery
- **Professional Messages**:
- Hold: "Please hold while we prepare your call"
- Transfer: "Transferring to extension X" or "Transferring your call"
- Requeue: "Placing you back in the queue. Please hold."
- **Consistency**: Same voice experience across all call operations
- **Configuration**: Works with existing ElevenLabs API key settings
- **Integration Points**: Hold operations, all transfer types, requeue operations, workflow steps
## 🚀 Twilio Integration Details
### SDK Version
- **Required**: Twilio PHP SDK v8.7.0
- **Installation**: Via Composer or install script
- **PHP Requirement**: PHP 8.0+
### API Response Structure
```php ```php
// Success response // ALWAYS do this for hold/transfer/requeue:
[
'success' => true,
'data' => [...] // Twilio response data
]
// Error response
[
'success' => false,
'error' => 'Error message',
'code' => 400
]
```
### TwiML Generation Best Practices
- Always use SDK classes for TwiML generation
- Include proper XML headers when needed
- Use TTS helper for voice synthesis
- Implement proper error handling
### Recording Stop Methods (SDK v8)
```php
// Method 1: Specific recording
$client->calls($call_sid)
->recordings($recording_sid)
->update(['status' => 'stopped']);
// Method 2: Single active recording
$client->calls($call_sid)
->recordings('Twilio.CURRENT')
->update(['status' => 'stopped']);
```
## 🛠️ Development Guidelines
### Call Control Functions (CRITICAL)
**ALWAYS use call leg detection for hold, transfer, and requeue operations:**
```php
// Correct pattern for call control functions
private function find_customer_call_leg($call_sid, $api) {
// Detects browser phone vs regular calls
// Uses parent call relationships
// Searches active calls for customer leg
// Returns correct SID for operations
}
// Usage in AJAX handlers
$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio); $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
$result = $api->update_call($customer_call_sid, ['twiml' => $twiml_xml]); $api->update_call($customer_call_sid, ['twiml' => $twiml_xml]);
``` ```
**Why This Matters:** ### Common Fixes
- Outbound calls create separate agent and customer call legs - Recording: Use `Twilio.CURRENT` for SDK v8
- Applying actions to agent leg disconnects customer - Queue: Pass `waitUrl` as option in `enqueue()`
- Browser phone calls use `client:` identifiers - TwiML: Use SDK classes, not raw XML
- Customer leg must be identified for proper call control
### Database Operations ## Recent Changes (v2.3.0)
- Always use `$wpdb` global - Browser phone moved to admin-only
- Sanitize with `sanitize_text_field()`, `intval()` - Call control uses `find_customer_call_leg()` to prevent disconnections
- Use prepared statements: `$wpdb->prepare()` - Auto-creates user queues/extensions when needed
- Call `TWP_Activator::ensure_tables_exist()` before operations - Firefox support added
- 1-min agent status auto-revert
### AJAX Handler Pattern ## Development Notes
```php - **API**: E.164 format (+1XXXXXXXXXX)
public function ajax_handler_name() { - **Database**: Use `$wpdb`, prepared statements
if (!$this->verify_ajax_nonce()) { - **AJAX**: Verify nonce, return JSON
wp_send_json_error('Invalid nonce'); - **Naming**: TWP_ for classes, twp_ for tables/options
return; - **Debugging**: Look for "TWP Call Leg Detection" in logs
}
// Handler logic ## Features
- Agents accept calls via SMS "1"
wp_send_json_success($data); - User-specific queues with extensions
} - Browser phone at `admin.php?page=twilio-wp-browser-phone`
``` - ElevenLabs TTS with Alice fallback
- 68 AJAX actions, 26 REST endpoints
### Error Handling
- Log errors with `error_log()`
- Return structured error responses
- Implement fallback mechanisms
- Handle Twilio exceptions properly
### Naming Conventions
- Classes: `TWP_Class_Name`
- Tables: `twp_table_name`
- Options: `twp_option_name`
- AJAX actions: `twp_action_name`
- Nonces: `twp_ajax_nonce` or `twp_frontend_nonce`
## 📋 Common Issues & Solutions
### Database Issues
- **Missing Tables**: Run `TWP_Activator::ensure_tables_exist()`
- **Schema Changes**: Check `add_missing_columns()` method
- **Migration Issues**: Review `migrate_tables()` implementation
### Webhook Issues
- **500 Errors**: Check PHP error logs
- **TwiML Errors**: Verify XML structure
- **Authentication**: Webhooks use `__return_true` permission
### API Issues
- **Instantiation**: Use `new TWP_Twilio_API()` not singleton
- **Phone Format**: Always use E.164 format (+1XXXXXXXXXX)
- **Call SID**: Access via `$response['data']['sid']`
### Call Control Issues (ALL RESOLVED - September 2025)
- **Extension Transfer Issues**: COMPLETELY RESOLVED - Queue-based system with 2-minute timeout and automatic voicemail fallback
- **Browser Phone Firefox Support**: FULLY RESOLVED - Explicit permission handling and cross-browser compatibility
- **Client Name Consistency**: FIXED - Standardized naming across all browser phone functions
- **Agent Status Management**: AUTOMATED - 1-minute auto-revert system with cron job automation
- **Call Statistics Accuracy**: ENHANCED - Browser phone calls now properly tracked in statistics
- **Transcription Integration**: IMPLEMENTED - Real Twilio API integration replacing placeholder system
- **Customer Disconnections**: PREVIOUSLY RESOLVED - All functions use intelligent call leg detection
- **Queue Not Found Errors**: PREVIOUSLY RESOLVED - Automatic queue creation prevents this issue
- **Professional Audio**: UNIVERSAL - ElevenLabs TTS integration throughout all voice prompts
- **Extension Voicemail**: ENHANCED - User ID support and proper callback handling
- **Permission System**: NEW - Automatic microphone/speaker permission requests for browser phone
### Hold/Transfer/Requeue System (COMPLETELY ENHANCED)
- **Automatic Queue Creation**: Creates personal and hold queues as needed
- **Extension Management**: Auto-generates unique 3-4 digit extensions
- **ElevenLabs Integration**: Premium TTS for all voice prompts with Alice fallback
- **Call Leg Detection**: 100% accurate customer vs agent identification
- **TwiML Compliance**: Proper XML generation prevents audio errors
- **Database Consistency**: Handles schema variations gracefully
- **Error Recovery**: Comprehensive fallback mechanisms
- **User Experience**: Professional audio experience throughout call lifecycle
### Advanced Debugging Features (NEW)
- **Call Leg Detection Logging**: "TWP Call Leg Detection" entries show call relationship analysis
- **Queue Creation Logging**: Tracks automatic queue and extension generation
- **TwiML Generation Logging**: Shows proper XML construction vs old webhook methods
- **Customer Number Detection**: Enhanced logging for browser phone call analysis
- **Extension Assignment**: Logs unique extension generation and assignment process
## 🧪 Testing Approach
### Unit Testing
- PHPUnit for PHP code
- Mock WordPress functions
- Mock Twilio API responses
### Integration Testing
- Test webhook endpoints
- Verify database operations
- Test complete call flows
### Manual Testing
- Monitor Twilio Console
- Check WordPress debug logs
- Test with real phone numbers
## 📚 Key Features Implementation
### Agent System
- **SMS Accept**: Agents text "1" to accept calls
- **Real-time Status**: Available/busy/offline states
- **Group Management**: Priority-based call distribution
- **Personal Queues**: Agent-specific call queues
### Call Queue System
- **Position Tracking**: Real-time queue position
- **Timeout Handling**: Automatic callback offers
- **Hold Queues**: Temporary call parking
- **User Queues**: Personal agent queues
### Browser Phone
- **Twilio Device**: Full SDK integration
- **Call Controls**: Hold, transfer, record, mute
- **Visual Interface**: Real-time status updates
- **Queue Dashboard**: Monitor waiting calls
### Recording System
- **Start/Stop**: Dynamic recording control
- **Storage**: Database tracking with Twilio URLs
- **Playback**: Authenticated proxy endpoints
- **Transcription**: Automatic with callbacks
### ElevenLabs TTS
- **Auto-detection**: Uses ElevenLabs when configured
- **Caching**: 30-day cache for repeated phrases
- **Fallback**: Seamless Twilio voice fallback
- **Performance**: Instant cached audio delivery
## 📝 Configuration Requirements
### WordPress Settings
- **Twilio Credentials**: Account SID, Auth Token
- **TwiML App**: For browser phone functionality
- **Phone Numbers**: At least one Twilio number
- **Webhook URLs**: Configure in Twilio Console
### Optional Settings
- **ElevenLabs**: API key and voice selection
- **Hold Music**: Custom URL support
- **SMS Notifications**: Agent alert numbers
- **Business Hours**: Schedule configurations
## 🔍 Debugging Tips
1. **Enable WordPress Debug**: `WP_DEBUG = true`
2. **Check Error Logs**: `/wp-content/debug.log`
3. **Monitor Twilio Console**: Real-time webhook debugging
4. **Database Queries**: Use `$wpdb->last_error`
5. **Browser Console**: Check JavaScript errors
6. **Network Tab**: Monitor AJAX requests
7. **Call Leg Detection**: Look for "TWP Call Leg Detection" log entries
8. **Outbound Call Issues**: Check for agent vs customer call SID usage
9. **Browser Phone Debugging**: Search logs for "client:" identifier handling
## 📖 External Resources
- **Twilio PHP SDK**: https://www.twilio.com/docs/libraries/reference/twilio-php/
- **WordPress REST API**: https://developer.wordpress.org/rest-api/
- **ElevenLabs API**: https://api.elevenlabs.io/docs
- **Twilio TwiML**: https://www.twilio.com/docs/voice/twiml
--- ---
*Updated: Sept 2025*
*Last Updated: September 2025*
*Plugin Version: v2.3.0 - Enterprise Ready*
*Major Release: Extension Transfer System, Browser Phone Compatibility, Auto Status Management*
*Maintained for: phone.cloud-hosting.io*

View File

@@ -390,7 +390,8 @@ class TWP_Admin {
<?php endif; ?> <?php endif; ?>
</select> </select>
<button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button> <button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button>
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key.</p> <button type="button" class="button" onclick="refreshElevenLabsVoices()" title="Refresh voices from ElevenLabs">🔄 Refresh</button>
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key, or "Refresh" to get updated voices.</p>
<?php if (WP_DEBUG): ?> <?php if (WP_DEBUG): ?>
<p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p> <p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p>
<?php endif; ?> <?php endif; ?>
@@ -1066,6 +1067,70 @@ class TWP_Admin {
xhr.send('action=twp_get_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'); xhr.send('action=twp_get_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
} }
function refreshElevenLabsVoices() {
var select = document.getElementById('elevenlabs-voice-select');
var button = event.target;
var currentValue = select.getAttribute('data-current') || select.value;
console.log('Refreshing voices, current value:', currentValue);
button.textContent = 'Refreshing...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = '🔄 Refresh';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a voice...</option>';
if (Array.isArray(response.data)) {
response.data.forEach(function(voice) {
var selected = voice.voice_id === currentValue ? ' selected' : '';
var category = voice.category === 'cloned' ? ' (Cloned)' : (voice.category === 'premade' ? ' (Premade)' : '');
options += '<option value="' + voice.voice_id + '"' + selected + '>' + voice.name + category + '</option>';
});
}
select.innerHTML = options;
select.setAttribute('data-current', currentValue);
// Re-add preview buttons
addVoicePreviewButtons(select, response.data);
// Show success message
var statusMsg = document.createElement('div');
statusMsg.style.color = 'green';
statusMsg.style.fontSize = '12px';
statusMsg.style.marginTop = '5px';
statusMsg.textContent = 'Voices refreshed successfully! Found ' + response.data.length + ' voices.';
button.parentNode.appendChild(statusMsg);
setTimeout(function() {
if (statusMsg.parentNode) {
statusMsg.parentNode.removeChild(statusMsg);
}
}, 3000);
} else {
alert('Error refreshing voices: ' + (response.data || 'Unknown error'));
}
} catch (e) {
console.error('Refresh voices error:', e);
alert('Failed to refresh voices. Please try again.');
}
};
xhr.send('action=twp_refresh_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
function addVoicePreviewButtons(select, voices) { function addVoicePreviewButtons(select, voices) {
// Remove existing preview container // Remove existing preview container
var existingPreview = document.getElementById('voice-preview-container'); var existingPreview = document.getElementById('voice-preview-container');
@@ -4786,6 +4851,36 @@ class TWP_Admin {
} }
} }
/**
* AJAX handler for refreshing ElevenLabs voices (clears cache)
*/
public function ajax_refresh_elevenlabs_voices() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
// Clear the cached voices
delete_transient('twp_elevenlabs_voices');
// Now fetch fresh voices
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->get_voices(); // This will fetch from API and re-cache
if ($result['success']) {
wp_send_json_success($result['data']['voices']);
} else {
$error_message = 'Failed to refresh voices';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
}
wp_send_json_error($error_message);
}
}
/** /**
* AJAX handler for getting ElevenLabs models * AJAX handler for getting ElevenLabs models
*/ */
@@ -4850,7 +4945,7 @@ class TWP_Admin {
} }
$voice_id = sanitize_text_field($_POST['voice_id']); $voice_id = sanitize_text_field($_POST['voice_id']);
$text = sanitize_text_field($_POST['text']) ?: 'Hello, this is a preview of this voice.'; $text = isset($_POST['text']) ? sanitize_text_field($_POST['text']) : 'Hello, this is a preview of this voice.';
$elevenlabs = new TWP_ElevenLabs_API(); $elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->text_to_speech($text, $voice_id); $result = $elevenlabs->text_to_speech($text, $voice_id);

View File

@@ -413,6 +413,7 @@ jQuery(document).ready(function($) {
html += '</select>'; html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">'; html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>'; html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>'; html += '</div>';
// Audio File Options // Audio File Options
@@ -454,6 +455,7 @@ jQuery(document).ready(function($) {
html += '</select>'; html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">'; html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>'; html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>'; html += '</div>';
// Audio File Options // Audio File Options
@@ -520,6 +522,7 @@ jQuery(document).ready(function($) {
html += '</select>'; html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">'; html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>'; html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>'; html += '</div>';
// Audio File Options // Audio File Options
@@ -559,6 +562,7 @@ jQuery(document).ready(function($) {
html += '</select>'; html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">'; html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>'; html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>'; html += '</div>';
// Audio File Options // Audio File Options
@@ -1774,6 +1778,97 @@ jQuery(document).ready(function($) {
}); });
}; };
// Refresh voices function for workflow
window.refreshWorkflowVoices = function(buttonOrSelect) {
var $button, $select;
// Check if we were passed a button or a select element
if ($(buttonOrSelect).is('button')) {
$button = $(buttonOrSelect);
$select = $button.prev('select.voice-select');
} else if ($(buttonOrSelect).is('select')) {
$select = $(buttonOrSelect);
$button = $select.next('button');
} else {
// Fallback - assume it's a button
$button = $(buttonOrSelect);
$select = $button.prev('select.voice-select');
}
// Read the current value
var currentValue = '';
var selectedOption = $select.find('option:selected');
if (selectedOption.length && selectedOption.val()) {
currentValue = selectedOption.val();
} else {
currentValue = $select.attr('data-current') || '';
}
if ($button.length) {
$button.text('Refreshing...').prop('disabled', true);
}
$.post(twp_ajax.ajax_url, {
action: 'twp_refresh_elevenlabs_voices',
nonce: twp_ajax.nonce
}, function(response) {
if ($button.length) {
$button.text('🔄').prop('disabled', false);
}
if (response.success) {
var options = '<option value="">Default voice</option>';
response.data.forEach(function(voice) {
var selected = (voice.voice_id === currentValue) ? ' selected' : '';
var description = voice.labels ? Object.values(voice.labels).join(', ') : '';
var optionText = voice.name + (description ? ' (' + description + ')' : '');
options += '<option value="' + voice.voice_id + '" data-voice-name="' + voice.name + '"' + selected + '>' + optionText + '</option>';
});
$select.html(options);
// If we had a current value, make sure it's selected
if (currentValue) {
$select.val(currentValue);
// Update the voice name field with the selected voice's name
var $voiceNameInput = $select.siblings('input[name="voice_name"]');
if ($voiceNameInput.length) {
var selectedVoice = $select.find('option:selected');
var voiceName = selectedVoice.data('voice-name') || selectedVoice.text() || '';
if (selectedVoice.val() === '') {
voiceName = '';
}
$voiceNameInput.val(voiceName);
}
}
// Show success message
var $statusMsg = $('<span style="color: green; font-size: 12px; margin-left: 5px;">Refreshed!</span>');
$button.after($statusMsg);
setTimeout(function() {
$statusMsg.remove();
}, 3000);
} else {
var errorMessage = 'Error refreshing voices: ';
if (typeof response.data === 'string') {
errorMessage += response.data;
} else if (response.data && response.data.message) {
errorMessage += response.data.message;
} else {
errorMessage += 'Unknown error';
}
alert(errorMessage);
}
}).fail(function() {
if ($button.length) {
$button.text('🔄').prop('disabled', false);
}
alert('Failed to refresh voices. Please check your API key.');
});
};
// Toggle audio type options visibility // Toggle audio type options visibility
$(document).on('change', 'input[name="audio_type"]', function() { $(document).on('change', 'input[name="audio_type"]', function() {
var $container = $(this).closest('.step-config-section'); var $container = $(this).closest('.step-config-section');

View File

@@ -161,6 +161,7 @@ class TWP_Core {
// Eleven Labs AJAX // Eleven Labs AJAX
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices'); $this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices');
$this->loader->add_action('wp_ajax_twp_refresh_elevenlabs_voices', $plugin_admin, 'ajax_refresh_elevenlabs_voices');
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models'); $this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models');
$this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice'); $this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice');

View File

@@ -197,6 +197,48 @@ class TWP_Webhooks {
'permission_callback' => '__return_true' 'permission_callback' => '__return_true'
)); ));
// Agent features webhook (handles DTMF during bridged calls)
register_rest_route('twilio-webhook/v1', '/agent-features', array(
'methods' => 'POST',
'callback' => array($this, 'handle_agent_features'),
'permission_callback' => '__return_true'
));
// Forward result webhook (handles dial result for forwards)
register_rest_route('twilio-webhook/v1', '/forward-result', array(
'methods' => 'POST',
'callback' => array($this, 'handle_forward_result'),
'permission_callback' => '__return_true'
));
// Agent action webhook (processes DTMF commands from agent)
register_rest_route('twilio-webhook/v1', '/agent-action', array(
'methods' => 'POST',
'callback' => array($this, 'handle_agent_action'),
'permission_callback' => '__return_true'
));
// Conference status webhook
register_rest_route('twilio-webhook/v1', '/conference-status', array(
'methods' => 'POST',
'callback' => array($this, 'handle_conference_status'),
'permission_callback' => '__return_true'
));
// Agent conference join webhook
register_rest_route('twilio-webhook/v1', '/agent-conference-join', array(
'methods' => 'POST',
'callback' => array($this, 'handle_agent_conference_join'),
'permission_callback' => '__return_true'
));
// Agent call status webhook (for conference calls)
register_rest_route('twilio-webhook/v1', '/agent-call-status-new', array(
'methods' => 'POST',
'callback' => array($this, 'handle_agent_call_status_new'),
'permission_callback' => '__return_true'
));
// Request callback webhook // Request callback webhook
register_rest_route('twilio-webhook/v1', '/request-callback', array( register_rest_route('twilio-webhook/v1', '/request-callback', array(
'methods' => 'POST', 'methods' => 'POST',
@@ -2618,4 +2660,347 @@ class TWP_Webhooks {
return new WP_REST_Response($response->asXML(), 200, array('Content-Type' => 'text/xml')); return new WP_REST_Response($response->asXML(), 200, array('Content-Type' => 'text/xml'));
} }
/**
* Handle agent features during bridged calls
*/
public function handle_agent_features($request) {
$params = $request->get_params();
error_log('TWP Agent Features: Webhook triggered with params: ' . print_r($params, true));
$response = new \Twilio\TwiML\VoiceResponse();
// This webhook is called when the agent answers
// We set up a Gather to listen for DTMF during the call
$gather = $response->gather([
'input' => 'dtmf',
'numDigits' => 2,
'actionOnEmptyResult' => false,
'action' => home_url('/wp-json/twilio-webhook/v1/agent-action'),
'method' => 'POST',
'timeout' => 1,
'finishOnKey' => '' // Don't finish on any key, we want to capture patterns like *9
]);
// Connect the call (empty gather continues the call)
// After gather timeout, continue listening
$response->redirect(home_url('/wp-json/twilio-webhook/v1/agent-features'), ['method' => 'POST']);
error_log('TWP Agent Features: TwiML response: ' . $response->asXML());
return $this->send_twiml_response($response->asXML());
}
/**
* Handle forward result (called after dial completes)
*/
public function handle_forward_result($request) {
$params = $request->get_params();
error_log('TWP Forward Result: Call completed with params: ' . print_r($params, true));
$dial_call_status = isset($params['DialCallStatus']) ? $params['DialCallStatus'] : '';
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
// Update call log with result
if ($call_sid) {
TWP_Call_Logger::log_action($call_sid, 'Forward result: ' . $dial_call_status);
}
$response = new \Twilio\TwiML\VoiceResponse();
// Handle different outcomes
switch ($dial_call_status) {
case 'busy':
$response->say('The number is busy. Please try again later.', ['voice' => 'alice']);
break;
case 'no-answer':
$response->say('There was no answer. Please try again later.', ['voice' => 'alice']);
break;
case 'failed':
$response->say('The call could not be completed. Please try again later.', ['voice' => 'alice']);
break;
case 'canceled':
$response->say('The call was canceled.', ['voice' => 'alice']);
break;
default:
// Call completed successfully or caller hung up
break;
}
$response->hangup();
return $this->send_twiml_response($response->asXML());
}
/**
* Handle agent DTMF actions (*9 hold, *0 record, *5 transfer, etc.)
*/
public function handle_agent_action($request) {
$params = $request->get_params();
$digits = isset($params['Digits']) ? $params['Digits'] : '';
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
$parent_call_sid = isset($params['ParentCallSid']) ? $params['ParentCallSid'] : '';
error_log('TWP Agent Action: Received DTMF: ' . $digits . ' for call: ' . $call_sid);
$response = new \Twilio\TwiML\VoiceResponse();
$twilio = new TWP_Twilio_API();
$admin = new TWP_Admin('twilio-wp-plugin', TWP_VERSION);
// Process DTMF commands
switch ($digits) {
case '*9':
// Hold/Unhold
error_log('TWP Agent Action: Processing hold/unhold request');
// Find the customer call leg
$customer_call_sid = $admin->find_customer_call_leg($parent_call_sid, $twilio->get_client());
if ($customer_call_sid) {
// Check if call is on hold by looking at the current state
global $wpdb;
$table_name = $wpdb->prefix . 'twp_active_calls';
$call_info = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE call_sid = %s",
$customer_call_sid
));
if ($call_info && $call_info->status === 'on-hold') {
// Resume the call
$twiml = '<Response><Say voice="alice">Resuming call</Say><Pause length="1"/></Response>';
$twilio->update_call($customer_call_sid, ['twiml' => $twiml]);
$wpdb->update($table_name,
['status' => 'in-progress'],
['call_sid' => $customer_call_sid]
);
$response->say('Call resumed', ['voice' => 'alice']);
error_log('TWP Agent Action: Call resumed');
} else {
// Put on hold
$hold_music = get_option('twp_hold_music_url', 'http://com.twilio.music.classical.s3.amazonaws.com/BusyStrings.mp3');
$twiml = '<Response><Say voice="alice">You have been placed on hold</Say><Play loop="0">' . $hold_music . '</Play></Response>';
$twilio->update_call($customer_call_sid, ['twiml' => $twiml]);
$wpdb->update($table_name,
['status' => 'on-hold'],
['call_sid' => $customer_call_sid]
);
$response->say('Call on hold', ['voice' => 'alice']);
error_log('TWP Agent Action: Call placed on hold');
}
} else {
error_log('TWP Agent Action: Could not find customer call leg for hold');
}
break;
case '*0':
// Start/Stop Recording
error_log('TWP Agent Action: Processing recording toggle');
try {
// Check if we're already recording
$recordings = $twilio->get_client()->recordings->read(['callSid' => $parent_call_sid, 'status' => 'in-progress'], 1);
if (!empty($recordings)) {
// Stop recording
$recording = $recordings[0];
$twilio->get_client()->recordings($recording->sid)->update(['status' => 'stopped']);
$response->say('Recording stopped', ['voice' => 'alice']);
error_log('TWP Agent Action: Recording stopped');
} else {
// Start recording
$twilio->get_client()->calls($parent_call_sid)->recordings->create([
'recordingStatusCallback' => home_url('/wp-json/twilio-webhook/v1/recording-status'),
'recordingStatusCallbackEvent' => ['completed']
]);
$response->say('Recording started', ['voice' => 'alice']);
error_log('TWP Agent Action: Recording started');
}
} catch (Exception $e) {
error_log('TWP Agent Action: Recording error: ' . $e->getMessage());
$response->say('Recording feature unavailable', ['voice' => 'alice']);
}
break;
case '*5':
// Transfer to extension
error_log('TWP Agent Action: Initiating transfer');
$response->say('Enter extension number followed by pound', ['voice' => 'alice']);
$gather = $response->gather([
'input' => 'dtmf',
'finishOnKey' => '#',
'action' => home_url('/wp-json/twilio-webhook/v1/transfer-extension'),
'method' => 'POST'
]);
// Continue call if no input
$response->redirect(home_url('/wp-json/twilio-webhook/v1/agent-features'), ['method' => 'POST']);
break;
case '*1':
// Mute/Unmute (agent side)
error_log('TWP Agent Action: Processing mute toggle');
// This would require conference mode - we'll note this for future implementation
$response->say('Mute feature requires conference mode', ['voice' => 'alice']);
break;
default:
error_log('TWP Agent Action: Unknown DTMF code: ' . $digits);
// Unknown code, just continue
break;
}
// Continue listening for more DTMF
$response->redirect(home_url('/wp-json/twilio-webhook/v1/agent-features'), ['method' => 'POST']);
return $this->send_twiml_response($response->asXML());
}
/**
* Handle conference status events
*/
public function handle_conference_status($request) {
$params = $request->get_params();
error_log('TWP Conference Status: ' . print_r($params, true));
$status_callback_event = isset($params['StatusCallbackEvent']) ? $params['StatusCallbackEvent'] : '';
$conference_sid = isset($params['ConferenceSid']) ? $params['ConferenceSid'] : '';
$friendly_name = isset($params['FriendlyName']) ? $params['FriendlyName'] : '';
// Log conference events for debugging
switch ($status_callback_event) {
case 'conference-start':
error_log('TWP Conference: Conference started: ' . $friendly_name);
break;
case 'participant-join':
error_log('TWP Conference: Participant joined: ' . $friendly_name);
break;
case 'participant-leave':
error_log('TWP Conference: Participant left: ' . $friendly_name);
break;
case 'conference-end':
error_log('TWP Conference: Conference ended: ' . $friendly_name);
break;
}
return new WP_REST_Response('OK', 200);
}
/**
* Handle agent joining conference with features
*/
public function handle_agent_conference_join($request) {
$params = $request->get_params();
error_log('TWP Agent Conference Join: ' . print_r($params, true));
$conference_name = isset($_GET['conference_name']) ? $_GET['conference_name'] : '';
$caller_number = isset($_GET['caller_number']) ? $_GET['caller_number'] : '';
$response = new \Twilio\TwiML\VoiceResponse();
if (empty($conference_name)) {
$response->say('Conference not found', ['voice' => 'alice']);
$response->hangup();
return $this->send_twiml_response($response->asXML());
}
// Announce the incoming call to the agent
if (!empty($caller_number)) {
$response->say('Incoming call from ' . $this->format_phone_number_for_speech($caller_number), ['voice' => 'alice']);
} else {
$response->say('Incoming call', ['voice' => 'alice']);
}
// Set up agent conference with features
$dial = $response->dial();
$conference = $dial->conference($conference_name, [
'startConferenceOnEnter' => true, // Start when agent joins
'endConferenceOnExit' => false, // Don't end when agent leaves (let caller stay)
'muted' => false,
'beep' => false,
'waitUrl' => '',
// Enable DTMF detection for agent features
'eventCallbackUrl' => home_url('/wp-json/twilio-webhook/v1/conference-events'),
'record' => false // We'll control recording via DTMF
]);
error_log('TWP Agent Conference Join: Joining agent to conference: ' . $conference_name);
return $this->send_twiml_response($response->asXML());
}
/**
* Handle agent call status for conference calls
*/
public function handle_agent_call_status_new($request) {
$params = $request->get_params();
error_log('TWP Agent Call Status: ' . print_r($params, true));
$call_status = isset($params['CallStatus']) ? $params['CallStatus'] : '';
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
switch ($call_status) {
case 'initiated':
error_log('TWP Agent Call: Call initiated to agent: ' . $call_sid);
break;
case 'ringing':
error_log('TWP Agent Call: Agent phone ringing: ' . $call_sid);
break;
case 'answered':
error_log('TWP Agent Call: Agent answered: ' . $call_sid);
break;
case 'completed':
error_log('TWP Agent Call: Agent call completed: ' . $call_sid);
break;
case 'busy':
case 'no-answer':
case 'failed':
error_log('TWP Agent Call: Agent call failed (' . $call_status . '): ' . $call_sid);
// TODO: Could implement fallback logic here (try next agent, voicemail, etc.)
break;
}
return new WP_REST_Response('OK', 200);
}
/**
* Format phone number for speech (reads each digit individually)
*/
private function format_phone_number_for_speech($number) {
// Remove +1 country code and any formatting
$cleaned = preg_replace('/^\+1/', '', $number);
$cleaned = preg_replace('/[^0-9]/', '', $cleaned);
if (strlen($cleaned) == 10) {
// Break into individual digits with pauses for natural speech
// Area code: 9-0-9, Main number: 5-7-3-7-3-7-2
$area_code = str_split(substr($cleaned, 0, 3));
$exchange = str_split(substr($cleaned, 3, 3));
$number_part = str_split(substr($cleaned, 6, 4));
// Join with spaces so TTS reads each digit individually
return implode(' ', $area_code) . ', ' . implode(' ', $exchange) . ', ' . implode(' ', $number_part);
} elseif (strlen($cleaned) == 11 && substr($cleaned, 0, 1) == '1') {
// Handle numbers that still have the 1 prefix
$area_code = str_split(substr($cleaned, 1, 3));
$exchange = str_split(substr($cleaned, 4, 3));
$number_part = str_split(substr($cleaned, 7, 4));
return implode(' ', $area_code) . ', ' . implode(' ', $exchange) . ', ' . implode(' ', $number_part);
}
// Fallback: just break into individual digits
return implode(' ', str_split($cleaned));
}
} }

View File

@@ -79,7 +79,9 @@ class TWP_Workflow {
break; break;
case 'forward': case 'forward':
error_log('TWP Workflow: Processing forward step: ' . json_encode($step));
$step_twiml = self::create_forward_twiml($step); $step_twiml = self::create_forward_twiml($step);
error_log('TWP Workflow: Forward step TwiML generated: ' . $step_twiml);
$stop_after_step = true; // Forward ends the workflow $stop_after_step = true; // Forward ends the workflow
break; break;
@@ -135,6 +137,9 @@ class TWP_Workflow {
// Add step TwiML to combined response // Add step TwiML to combined response
if ($step_twiml) { if ($step_twiml) {
error_log('TWP Workflow: Appending step TwiML to combined response');
error_log('TWP Workflow: Step TwiML before append: ' . $step_twiml);
// Parse the step TwiML and append to combined response // Parse the step TwiML and append to combined response
$step_xml = simplexml_load_string($step_twiml); $step_xml = simplexml_load_string($step_twiml);
if ($step_xml) { if ($step_xml) {
@@ -142,10 +147,15 @@ class TWP_Workflow {
self::append_twiml_element($response, $element); self::append_twiml_element($response, $element);
} }
$has_response = true; $has_response = true;
error_log('TWP Workflow: Combined response after append: ' . $response->asXML());
} else {
error_log('TWP Workflow: ERROR - Failed to parse step TwiML: ' . $step_twiml);
} }
// Stop processing if this step type should end the workflow // Stop processing if this step type should end the workflow
if ($stop_after_step) { if ($stop_after_step) {
error_log('TWP Workflow: Stopping after this step (stop_after_step = true)');
break; break;
} }
} }
@@ -153,10 +163,13 @@ class TWP_Workflow {
// Return combined response or default // Return combined response or default
if ($has_response) { if ($has_response) {
return $response->asXML(); $final_twiml = $response->asXML();
error_log('TWP Workflow: Final workflow TwiML response: ' . $final_twiml);
return $final_twiml;
} }
// Default response // Default response
error_log('TWP Workflow: No response generated, returning default response');
return self::create_default_response(); return self::create_default_response();
} }
@@ -196,11 +209,33 @@ class TWP_Workflow {
$response->record($attributes); $response->record($attributes);
break; break;
case 'Dial': case 'Dial':
$response->dial((string) $element, $attributes); // Create dial instance
$dial = $response->dial('', $attributes);
// Add child Number elements
foreach ($element->children() as $child) {
$child_name = $child->getName();
$child_attrs = self::get_attributes($child);
if ($child_name === 'Number') {
// Number can have url, method attributes for agent features
$dial->number((string) $child, $child_attrs);
} elseif ($child_name === 'Client') {
$dial->client((string) $child, $child_attrs);
} elseif ($child_name === 'Queue') {
$dial->queue((string) $child, $child_attrs);
} elseif ($child_name === 'Conference') {
$dial->conference((string) $child, $child_attrs);
} elseif ($child_name === 'Sip') {
$dial->sip((string) $child, $child_attrs);
}
}
break; break;
case 'Queue': case 'Queue':
$response->queue((string) $element, $attributes); $response->queue((string) $element, $attributes);
break; break;
case 'Conference':
$response->dial()->conference((string) $element, $attributes);
break;
case 'Redirect': case 'Redirect':
$response->redirect((string) $element, $attributes); $response->redirect((string) $element, $attributes);
break; break;
@@ -485,25 +520,162 @@ class TWP_Workflow {
* Create forward TwiML * Create forward TwiML
*/ */
private static function create_forward_twiml($step) { private static function create_forward_twiml($step) {
error_log('TWP Workflow Forward: Creating forward TwiML with step data: ' . print_r($step, true));
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>'); $twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$dial = $twiml->addChild('Dial'); // Check if data is nested (workflow steps have data nested)
$dial->addAttribute('answerOnBridge', 'true'); $step_data = isset($step['data']) ? $step['data'] : $step;
if (isset($step['timeout'])) { // Get the forward number(s) from the proper location
$dial->addAttribute('timeout', $step['timeout']); $forward_numbers = array();
// Check various possible locations for forward numbers
if (isset($step_data['forward_numbers']) && is_array($step_data['forward_numbers'])) {
$forward_numbers = $step_data['forward_numbers'];
error_log('TWP Workflow Forward: Found forward_numbers array in data: ' . print_r($forward_numbers, true));
} elseif (isset($step_data['forward_number']) && !empty($step_data['forward_number'])) {
$forward_numbers = array($step_data['forward_number']);
error_log('TWP Workflow Forward: Found single forward_number in data: ' . $step_data['forward_number']);
} elseif (isset($step_data['number']) && !empty($step_data['number'])) {
$forward_numbers = array($step_data['number']);
error_log('TWP Workflow Forward: Found number in data: ' . $step_data['number']);
} elseif (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
$forward_numbers = $step['forward_numbers'];
error_log('TWP Workflow Forward: Found forward_numbers array: ' . print_r($forward_numbers, true));
} elseif (isset($step['forward_number']) && !empty($step['forward_number'])) {
$forward_numbers = array($step['forward_number']);
error_log('TWP Workflow Forward: Found single forward_number: ' . $step['forward_number']);
} elseif (isset($step['number']) && !empty($step['number'])) {
$forward_numbers = array($step['number']);
error_log('TWP Workflow Forward: Found number: ' . $step['number']);
} }
if (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) { // Filter out empty numbers
// Sequential forwarding $forward_numbers = array_filter($forward_numbers, function($num) {
foreach ($step['forward_numbers'] as $number) { return !empty($num);
});
if (empty($forward_numbers)) {
error_log('TWP Workflow Forward: ERROR - No forward numbers found in step data');
// Return error message instead of empty dial
$say = $twiml->addChild('Say', 'Sorry, the forwarding destination is not configured. Please try again later.');
$say->addAttribute('voice', 'alice');
$twiml->addChild('Hangup');
return $twiml->asXML();
}
error_log('TWP Workflow Forward: Forwarding to numbers: ' . implode(', ', $forward_numbers));
// Check if we should use conference mode with agent features
$use_conference_mode = isset($step_data['enable_agent_features']) ? $step_data['enable_agent_features'] : true;
if ($use_conference_mode) {
// Use conference mode for better call control
error_log('TWP Workflow Forward: Using conference mode for forwarding');
// Generate a unique conference name using call SID
$call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] :
(isset($_REQUEST['CallSid']) ? $_REQUEST['CallSid'] :
(isset($GLOBALS['call_data']['CallSid']) ? $GLOBALS['call_data']['CallSid'] : uniqid('conf_')));
$conference_name = 'Forward_' . $call_sid;
// Put the caller into a conference
$dial = $twiml->addChild('Dial');
$dial->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/forward-result'));
$dial->addAttribute('method', 'POST');
$conference = $dial->addChild('Conference', $conference_name);
$conference->addAttribute('startConferenceOnEnter', 'false'); // Wait for agent
$conference->addAttribute('endConferenceOnExit', 'true'); // End when caller leaves
$conference->addAttribute('waitUrl', 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical');
$conference->addAttribute('beep', 'false');
// Add event callbacks to monitor conference
$conference->addAttribute('statusCallback', home_url('/wp-json/twilio-webhook/v1/conference-status'));
$conference->addAttribute('statusCallbackEvent', 'start join leave end');
$conference->addAttribute('statusCallbackMethod', 'POST');
error_log('TWP Workflow Forward: Placing caller in conference: ' . $conference_name);
// Immediately call the agent to join the conference
// We'll use the Twilio API to make an outbound call
$twilio = new TWP_Twilio_API();
// Set caller ID to the business number
$caller_id = isset($_POST['To']) ? $_POST['To'] : null;
if (!$caller_id && isset($GLOBALS['call_data']['To'])) {
$caller_id = $GLOBALS['call_data']['To'];
}
// Create TwiML for the agent call that joins them to conference with features
$agent_twiml_url = home_url('/wp-json/twilio-webhook/v1/agent-conference-join?' . http_build_query([
'conference_name' => $conference_name,
'caller_number' => isset($_POST['From']) ? $_POST['From'] : ''
]));
try {
error_log('TWP Workflow Forward: Calling agent ' . $forward_numbers[0] . ' to join conference');
$agent_call = $twilio->get_client()->calls->create(
$forward_numbers[0], // to
$caller_id, // from (business number)
[
'url' => $agent_twiml_url,
'method' => 'POST',
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/agent-call-status'),
'statusCallbackEvent' => ['initiated', 'ringing', 'answered', 'completed'],
'statusCallbackMethod' => 'POST'
]
);
error_log('TWP Workflow Forward: Agent call created with SID: ' . $agent_call->sid);
} catch (Exception $e) {
error_log('TWP Workflow Forward: Failed to create agent call: ' . $e->getMessage());
}
} else {
// Use standard dial forwarding (simpler but less features)
error_log('TWP Workflow Forward: Using standard dial forwarding');
$dial = $twiml->addChild('Dial');
$dial->addAttribute('answerOnBridge', 'true');
// Set timeout (default to 30 seconds if not specified)
$timeout = isset($step_data['timeout']) ? $step_data['timeout'] :
(isset($step['timeout']) ? $step['timeout'] : '30');
$dial->addAttribute('timeout', $timeout);
// Set caller ID to the number that was called
$caller_id = null;
if (isset($GLOBALS['call_data']['To']) && !empty($GLOBALS['call_data']['To'])) {
$caller_id = $GLOBALS['call_data']['To'];
} elseif (isset($_POST['To']) && !empty($_POST['To'])) {
$caller_id = $_POST['To'];
} elseif (isset($_REQUEST['To']) && !empty($_REQUEST['To'])) {
$caller_id = $_REQUEST['To'];
}
if ($caller_id) {
$dial->addAttribute('callerId', $caller_id);
}
// Add action URL to handle dial result
$dial->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/forward-result'));
$dial->addAttribute('method', 'POST');
// Add all forward numbers
foreach ($forward_numbers as $number) {
error_log('TWP Workflow Forward: Adding number to Dial: ' . $number);
$dial->addChild('Number', $number); $dial->addChild('Number', $number);
} }
} elseif (isset($step['forward_number'])) {
$dial->addChild('Number', $step['forward_number']);
} }
return $twiml->asXML(); $result = $twiml->asXML();
error_log('TWP Workflow Forward: Final Forward TwiML: ' . $result);
return $result;
} }
/** /**
@@ -677,8 +849,23 @@ class TWP_Workflow {
$dial->addAttribute('timeout', '30'); $dial->addAttribute('timeout', '30');
} }
if (isset($step['caller_id'])) { // Set caller ID - use provided value or default to the incoming number
if (isset($step['caller_id']) && !empty($step['caller_id'])) {
$dial->addAttribute('callerId', $step['caller_id']); $dial->addAttribute('callerId', $step['caller_id']);
} else {
// Use the number that was called (To number) as default caller ID
$caller_id = null;
if (isset($GLOBALS['call_data']['To']) && !empty($GLOBALS['call_data']['To'])) {
$caller_id = $GLOBALS['call_data']['To'];
} elseif (isset($_POST['To']) && !empty($_POST['To'])) {
$caller_id = $_POST['To'];
} elseif (isset($_REQUEST['To']) && !empty($_REQUEST['To'])) {
$caller_id = $_REQUEST['To'];
}
if ($caller_id) {
$dial->addAttribute('callerId', $caller_id);
}
} }
// Set action URL to handle no-answer scenarios // Set action URL to handle no-answer scenarios

View File

@@ -3,7 +3,7 @@
* Plugin Name: Twilio WP Plugin * Plugin Name: Twilio WP Plugin
* Plugin URI: https://repo.anhonesthost.net/wp-plugins/twilio-wp-plugin * Plugin URI: https://repo.anhonesthost.net/wp-plugins/twilio-wp-plugin
* Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS * Description: WordPress plugin for Twilio integration with phone scheduling, call forwarding, queue management, and Eleven Labs TTS
* Version: 2.2.0 * Version: 2.8.9
* Author: Josh Knapp * Author: Josh Knapp
* License: GPL v2 or later * License: GPL v2 or later
* Text Domain: twilio-wp-plugin * Text Domain: twilio-wp-plugin
@@ -15,7 +15,7 @@ if (!defined('WPINC')) {
} }
// Plugin constants // Plugin constants
define('TWP_VERSION', '2.8.6'); define('TWP_VERSION', '2.8.9');
define('TWP_DB_VERSION', '1.6.2'); // Track database version separately define('TWP_DB_VERSION', '1.6.2'); // Track database version separately
define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('TWP_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__)); define('TWP_PLUGIN_URL', plugin_dir_url(__FILE__));