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:
2025-09-18 18:29:20 -07:00
parent 0ee8210fef
commit e475e68a5f
5 changed files with 310 additions and 840 deletions

View File

@@ -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());
}
}

View File

@@ -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();