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>
This commit is contained in:
@@ -196,6 +196,27 @@ class TWP_Webhooks {
|
||||
'callback' => array($this, 'handle_callback_choice'),
|
||||
'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'
|
||||
));
|
||||
|
||||
// Request callback webhook
|
||||
register_rest_route('twilio-webhook/v1', '/request-callback', array(
|
||||
@@ -2618,4 +2639,207 @@ class TWP_Webhooks {
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
@@ -214,16 +214,19 @@ class TWP_Workflow {
|
||||
// Add child Number elements
|
||||
foreach ($element->children() as $child) {
|
||||
$child_name = $child->getName();
|
||||
$child_attrs = self::get_attributes($child);
|
||||
|
||||
if ($child_name === 'Number') {
|
||||
$dial->number((string) $child, self::get_attributes($child));
|
||||
// Number can have url, method attributes for agent features
|
||||
$dial->number((string) $child, $child_attrs);
|
||||
} elseif ($child_name === 'Client') {
|
||||
$dial->client((string) $child, self::get_attributes($child));
|
||||
$dial->client((string) $child, $child_attrs);
|
||||
} elseif ($child_name === 'Queue') {
|
||||
$dial->queue((string) $child, self::get_attributes($child));
|
||||
$dial->queue((string) $child, $child_attrs);
|
||||
} elseif ($child_name === 'Conference') {
|
||||
$dial->conference((string) $child, self::get_attributes($child));
|
||||
$dial->conference((string) $child, $child_attrs);
|
||||
} elseif ($child_name === 'Sip') {
|
||||
$dial->sip((string) $child, self::get_attributes($child));
|
||||
$dial->sip((string) $child, $child_attrs);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -591,10 +594,27 @@ class TWP_Workflow {
|
||||
error_log('TWP Workflow Forward: No To number found for caller ID, will use account default');
|
||||
}
|
||||
|
||||
// Add all forward numbers
|
||||
// Check if we should use bridging with agent features
|
||||
$use_bridge_features = isset($step_data['enable_agent_features']) ? $step_data['enable_agent_features'] : true;
|
||||
|
||||
// Add action URL to handle dial result (for tracking and potential fallback)
|
||||
$action_url = home_url('/wp-json/twilio-webhook/v1/forward-result');
|
||||
$dial->addAttribute('action', $action_url);
|
||||
$dial->addAttribute('method', 'POST');
|
||||
|
||||
// Add all forward numbers with agent features if enabled
|
||||
foreach ($forward_numbers as $number) {
|
||||
error_log('TWP Workflow Forward: Adding number to Dial: ' . $number);
|
||||
$dial->addChild('Number', $number);
|
||||
$number_element = $dial->addChild('Number', $number);
|
||||
|
||||
if ($use_bridge_features) {
|
||||
// Add URL to handle agent-side features (DTMF detection)
|
||||
$agent_url = home_url('/wp-json/twilio-webhook/v1/agent-features');
|
||||
$number_element->addAttribute('url', $agent_url);
|
||||
$number_element->addAttribute('method', 'POST');
|
||||
|
||||
error_log('TWP Workflow Forward: Added agent features URL for DTMF detection');
|
||||
}
|
||||
}
|
||||
|
||||
$result = $twiml->asXML();
|
||||
|
Reference in New Issue
Block a user